diff --git a/.github/workflows/composition-performance.yml b/.github/workflows/composition-performance.yml new file mode 100644 index 0000000..4d31447 --- /dev/null +++ b/.github/workflows/composition-performance.yml @@ -0,0 +1,43 @@ +name: Composition performance +on: + pull_request: + paths: + - '.github/workflows/composition-performance.yml' + - 'Scripts/qualify_composition_performance.py' + - 'Scripts/composition_host.py' + - 'Scripts/test_composition_performance.py' + - 'Sources/JSON/Limits/JSONCompositionScope.swift' + - 'Sources/JSON/Limits/JSONWorkMeter.swift' + - 'Tools/CompositionQualification/Sources/CompositionBenchmark/**' + workflow_dispatch: +permissions: + contents: read +concurrency: + group: composition-performance-${{ github.ref }} + cancel-in-progress: false +jobs: + compare: + name: Composition / quiet native Release + runs-on: xcode-27 + timeout-minutes: 30 + env: + DEVELOPER_DIR: /Applications/Xcode_27.2.app/Contents/Developer + SDKROOT: /Applications/Xcode_27.2.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk + PYTHONDONTWRITEBYTECODE: '1' + steps: + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 + with: + persist-credentials: false + fetch-depth: 0 + - name: Compare exact baseline and candidate on a quiet host + run: python3 -B Scripts/qualify_composition_performance.py --output .build/ci/composition-performance + - name: Preserve actual measurements and eligibility receipts + if: always() + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 + with: + name: composition-performance + if-no-files-found: error + retention-days: 30 + path: | + .build/ci/composition-performance/receipt.json + .build/ci/composition-performance/*.log diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index 5e1a169..a24e79d 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -43,7 +43,9 @@ jobs: - run: python3 -B Scripts/ci.py --lane '${{ matrix.lane }}' - name: Public composition and reference imports if: matrix.lane == 'linux' - run: python3 -B Scripts/ci.py --lane composition + run: | + python3 -B Scripts/ci.py --lane composition + python3 -B Scripts/ci.py --lane composition-proof - name: Preserve Linux receipts if: always() uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 diff --git a/.github/workflows/native.yml b/.github/workflows/native.yml index c202eb9..cf50985 100644 --- a/.github/workflows/native.yml +++ b/.github/workflows/native.yml @@ -17,7 +17,7 @@ jobs: strategy: fail-fast: false matrix: - lane: [native, safety, docs, archive] + lane: [native, safety, docs, archive, composition-proof] env: DEVELOPER_DIR: /Applications/Xcode_27.2.app/Contents/Developer PYTHONDONTWRITEBYTECODE: '1' diff --git a/Documentation/Architecture/API.md b/Documentation/Architecture/API.md index fb8fd0e..72598ba 100644 --- a/Documentation/Architecture/API.md +++ b/Documentation/Architecture/API.md @@ -287,4 +287,25 @@ SHA-256 of the ordered Package.swift plus Sources content manifest, excluding on the generated identity file itself. It is not a Git commit, binary digest or claim of publication. Generation/checking needs no Git, time, environment or network. + +## Borrowed native composition + +`JSON.withComposition(limits:_:)` synchronously borrows a noncopyable, +non-Sendable `JSONCompositionScope` and returns `JSONOperationReport` +only after healthy finalization. The scope has no public initializer. Public +methods parse a source-backed `Data` document, admit an existing value, construct, +parse, append and resolve native Pointers, charge external work, reserve checked +external logical bytes, checkpoint and tighten limits. Importing JSONSchema adds +`uri(_:relativeTo:)` and `removingFragment(_:)`; JSON has no sibling dependency. +URI operations use the core domain, with no compiled-plan or compilation-work +charge. This slice does not compose schema compilation/evaluation or Codable. + +All owned errors latch the original failure. Neither a caught failure, a nested +independent invocation nor later callback failure permits a completed outer +report. Scope handles cannot escape the synchronous borrow or cross suspension. +Native values may be returned; adapter-owned containers must be prepaid. Reports +are ordinary publicly constructible values, not certification of arbitrary +callback work. See [composition qualification](../Qualification/Composition/README.md) +and the JSON DocC composition guide for the exact accounting/ownership contract. + JSONNavigation product promotion preserves all 56 N1 public declarations and resource semantics. Full-source admission precedes invalid depth/record-option validation, including oversized sources; N1Promotion.md records this preserved precedence. JSON imports no Navigation symbols implicitly. diff --git a/Documentation/Architecture/CompositionQualification.md b/Documentation/Architecture/CompositionQualification.md index 47f77bc..675612e 100644 --- a/Documentation/Architecture/CompositionQualification.md +++ b/Documentation/Architecture/CompositionQualification.md @@ -63,7 +63,7 @@ Run python3 Scripts/ci.py --lane composition on the supported native or Linux runner. The lane requires 12 core functions in each build mode, separate public consumers, five external Swift Testing functions in Debug/Release/ASan, all eight scale cells and 11 compiler rejection cases. Exact module/cardinality checks -reject missing tests and misclassified resource failures. The reconciled 597 native / 581 +reject missing tests and misclassified resource failures. The reconciled 599 native / 583 Linux root functions, 325 parameter invocations, restored identities and 49 public metric probes retain their existing contracts. @@ -73,6 +73,13 @@ late missing-target/cancellation failures before attempting normal completion. Finalization must still throw the original native failure. Each report retains its actual source, environment, command exits, counts and input hashes. +Completion additionally owns a 424-byte counter report and charges 50 work units. +The retained threshold assertions include that independent buffer, and source input +capacity is per document while reported bytes accumulate across all documents. +The original two-argument reservation method is retained; a checked `overhead:` +overload is additive. See [reconciliation](../Qualification/Composition/Reconciliation.md) +for the independent ledger/mutant and compiler proof carried from PR #36. + CI runs the focused lane alongside the existing Native / native and both Linux architecture jobs. Their names and protection rules remain unchanged. Sanitizer and full-root lanes continue independently. Linux uses the existing Swift diff --git a/Documentation/Architecture/ExtractionManifest.json b/Documentation/Architecture/ExtractionManifest.json index 4a191c4..4587deb 100644 --- a/Documentation/Architecture/ExtractionManifest.json +++ b/Documentation/Architecture/ExtractionManifest.json @@ -302,7 +302,8 @@ "NAV5 public array-evidence packaging, external consumer and DocC are original Apache-2.0 code. Existing exact shape evidence, exclusive reducer, weights and I01-I17 semantics are unchanged; no upstream engine or dependency is adapted. Promotion is a draft candidate, not a released product.", "NAV5 package-only complete structural snapshot, parent-linked metadata, original source-ownership transfer and S01-S24 synthetic tests are original Apache-2.0 code. The existing read scope admits once; its default public read/traversal/inspection completion behavior is preserved. No persistent identity, upstream algorithm or new dependency is introduced.", "NAV6 snapshot access-level promotion, external consumer, independent 28-record maintainer oracle, terminal-transfer/concurrent lookup proofs and compiled DocC are original Apache-2.0 contributions. No upstream engine, new semantic authority, dependency or additional product is adapted.", - "NAV8 current-main N3/P1 reconciliation, minimal public immutable projection API, independent 16-case consumer and DocC/access-control proofs are original Apache-2.0 contributions. NAV7 donor db991c5/bda8aef and packet SHA-256 57ca72cb817272ed466a7cb8810043d56a079190d830ea7b995abd308070f98d retain provenance. No external engine, dependency or product is added." + "NAV8 current-main N3/P1 reconciliation, minimal public immutable projection API, independent 16-case consumer and DocC/access-control proofs are original Apache-2.0 contributions. NAV7 donor db991c5/bda8aef and packet SHA-256 57ca72cb817272ed466a7cb8810043d56a079190d830ea7b995abd308070f98d retain provenance. No external engine, dependency or product is added.", + "Composition reconciliation retains PR35 native routes/reference fixtures and adds PR36 original Apache-2.0 report-overlap, per-document capacity/history, independent owned-event/mutant and compiler proofs. No third-party source or root dependency is added." ], "jsonhtml_export_original_contributions": { "license": "Apache-2.0", diff --git a/Documentation/Architecture/ResourceLimits.md b/Documentation/Architecture/ResourceLimits.md index 56e9f27..d593890 100644 --- a/Documentation/Architecture/ResourceLimits.md +++ b/Documentation/Architecture/ResourceLimits.md @@ -159,3 +159,34 @@ uninstrumented adapter allocations and process RSS are outside the claim. A publ JSONOperationReport initializer permits pairing statistics with another result; its type alone does not establish provenance or adapter completeness. See the [composition guide](../../Sources/JSON/Documentation.docc/Composition.md). + +## Composition completion accounting + + +The additive borrowed scope uses one original core ledger and the existing 49 +metrics/default profiles. It does not introduce adjustable counters or widen any +cap. `chargeExternalWork` charges aggregate work before adapter work; +`reserveExternalLogicalBytes(bytes,count:overhead:)` checks multiplication then +addition and live retention before growth. Zero is valid but never establishes +that opaque caller storage is free. All reservations are conservative until scope +exit; repeated admission and physical aliases count again. Caller instructions, +allocator capacity and process RSS remain outside this contract. + +`parseDocument` checks the full Data length against input and retained limits +before allocating its copied byte buffer, charges each copied byte, then invokes +the existing strict parser/source mapper. Caller Data is excluded. Input capacity +is per document: the preceding input count is checked and retired into the same +cumulative report before the next document. Work, retention, accepted-capacity +history and first-failure identity never restart. Remaining core counts are +conservative across the invocation; full source bytes/root/map and re-admitted +Pointer inputs/outputs remain charged. URI identity is core work/storage, never +schema compilation or compiledPlanBytes. + +Completion admits an additional 424 logical bytes (32 header + 49 eight-byte +counter slots) and 50 work units (49 counter copies and one final work update). +All result reservations overlap this report buffer. Overflow, quota failure and +cancellation prevent a completed result/report even when caught by the adapter. +No report is emitted after an error. Native accounting-version-1 weights and the +ordinary entry points' accounting stay unchanged. This proof slice has no public +release/promote tokens, refunds or partial reports. Independent adapter events +and failing omitted-charge mutants qualify the authored adapter only. diff --git a/Documentation/CI/README.md b/Documentation/CI/README.md index 2c1de90..76a446d 100644 --- a/Documentation/CI/README.md +++ b/Documentation/CI/README.md @@ -1,3 +1,17 @@ +# R3 composition and NAV8 reconciliation + +This source combines immutable NAV8 main `0cb8ca40` with composition R2 +`1bf49535`. The final source discovery contract is [TestDiscovery.json](../Qualification/TestDiscovery.json); +fresh compiler/event receipts must establish its complete Native/Linux/WASI, +parameter, shared-state and module identities. Both composition lanes, NAV8 +private invariant and public projection consumers, all 49 finite caps and all +126 restoration identities remain mandatory. Root dependencies remain empty; +ten products include nine library catalogs. Controlled composition performance +and universal compiler U02 remain separate held gates. Earlier owner decisions +below apply only to their named source and do not admit this combined candidate. + +The following two sections preserve their original source histories and counts. + # NAV8 Navigation V1 integration Current required discovery retains 779 native /763 Linux /675 WASI functions, @@ -12,6 +26,17 @@ for this owner's guarded merge without performance claims. Earlier sections below retain their original source histories. +# Composition reconciled with admitted Inspection main + +This candidate preserves current main `07912d8b28e186589e994e537b2485877a634a6b` +and adds PR36's two native boundary functions: 722 native / 706 Linux / 618 WASI +functions, 325 native/Linux / 288 WASI parameter invocations, 48 native / 47 Linux +shared functions, ten products and nine catalogs. Both composition lanes, all +49 caps and all 126 restoration identities remain mandatory. Actual receipts +establish outcomes; controlled performance and compiler limitations remain separate. + +The following sections retain their original source identities and counts. + # NAV6 integrated inspection candidate Adds Inspection to schema-example-enabled main: ten products, nine libraries, @@ -19,6 +44,21 @@ Adds Inspection to schema-example-enabled main: ten products, nine libraries, All 49 caps, 126 maintenance identities and schema/composition lanes remain. These are discovery expectations; receipts establish execution. +# Composition proof reconciled with S3 + +Reconciliation with main `c79d48c15f83ef88bed9459d9abcf05e54bd0bea` retains +all schema-example, Navigation and composition behavior. Exact combined roots +are 697 Mac / 681 Linux / 593 WASI functions, with 325 Mac/Linux and 288 WASI +parameter invocations. Shared-state counts remain 47 Mac / 46 Linux. Nine products, +eight library DocC catalogs, all 49 cap probes and all 126 restored identities +remain mandatory. Both focused composition lanes and the complete S3 consumer, +ownership, construction, matrix and corpus-lifetime checks are retained. The +15-case Python harness rejects omitted dispatch and stale integration counts. +Required check names and protections are unchanged. Quiet-host qualification +remains pending separately; no timing comparison is scheduled by this integration. + +The following sections preserve their original source-specific counts. + # Combined S3 / composition / Navigation qualification The current inventory requires 695 Mac / 679 Linux / 591 WASI functions, @@ -57,6 +97,21 @@ memory compilation. Earlier sections retain their original source identities. # Native CI +## Current composition proof + +The additive composition proof retains current main's Navigation product and +adds two native boundary functions: exact discovery is 599 native / 583 Linux / +495 WASI functions, with the same 325 native/Linux and 288 WASI parameter cases. +Shared-state expectations remain 43 native / 42 Linux. Native/Linux still require +33/29 commands, all 126 restored identities and 49 cap probes. The separate +composition-proof lane additionally checks 26 public functions in both modes, +two rejected omitted-charge mutants and 14 normal compiler rejections. Known +compiler crashes remain separate limitations. Native CI now has five matrix +lanes including composition-proof; protected names/settings are unchanged. +The [controlled performance contract](../Qualification/Composition/Performance.md) +requires genuine pre/post idle admission on its isolated native runner. The +following counts retain their original stage context; receipts establish passes. + ## Reconciled Navigation and composition increment Current source retains composition from main and adds the opt-in Navigation @@ -295,3 +350,12 @@ The focused composition lane also runs on the Linux architecture jobs and Native Its separate-package discovery is additive; all root and 49-cap expectations remain intact. The qualification harness now has 15 regression functions, retaining its original 14. See [composition qualification](../Architecture/CompositionQualification.md) for finite capacity outcomes. + +The additive `composition-proof` lane runs two native boundary functions and the +independent 26-function public suite in both configurations, two omitted-charge +mutants and 14 separate diagnostic builds. Both focused lanes run on Linux; +Native / composition-proof is separate from the protected names. The original +reference-load lane and its exact counts remain required. The controlled +composition performance job compares pinned main 30631e8 with this successor; +[the reconciliation record](../Qualification/Composition/Reconciliation.md) states +the expected accounting delta and unchanged idle/review thresholds. diff --git a/Documentation/Qualification/Composition/FreshMainReconciliation.md b/Documentation/Qualification/Composition/FreshMainReconciliation.md new file mode 100644 index 0000000..4047b1a --- /dev/null +++ b/Documentation/Qualification/Composition/FreshMainReconciliation.md @@ -0,0 +1,39 @@ +# Native composition candidate on 1.7.0-dev + +This isolated candidate reconciles draft PR36 at +`3571cb0d8aea5cf2c4ba454c5eab02ea2ce23f11` with admitted development main +`07912d8b28e186589e994e537b2485877a634a6b`. Its native composition runtime is +byte-identical to PR36. The root manifest, schema compiler/generator, Navigation, +Inspection, independent fixtures and CLI version remain those of development main. +The CLI source-content identity is regenerated by the unchanged generator. + +Both histories are retained. The CI dispatch, discovery assertions, provenance and +DocC conflicts combine the two source contracts. The original documentation is +preserved in the unchanged parent histories and source-bound receipts. The Composition +guide fence changes main's inferred closure parameter to an explicit +`(scope: borrowing JSONCompositionScope)` parameter; that fence is not byte-identical +to main. Other original documentation clauses and executable fences remain. +Current roots require 722 native / 706 Linux / 618 WASI functions, with 325/325/288 +parameter invocations and 48 native / 47 Linux shared functions. The separate +public proof now requires 29 functions: the original 26 plus three cases for the +pinned 577/152/578-byte sources, sticky typed parser/resource errors and exact +source/number/scalar/Pointer/URI semantics with all 49 final counter slots. + +The supplied six fixture files retain their byte identities in the public test +resource bundle. They are synthetic JSON and introduce no protocol dependency. +Policy exclusions remain separate from evaluated corpus outcomes. The independent +adapter trace remains 197 work / 664 logical bytes including native completion; +its two causal undercharge mutants and all 14 normal compiler negatives remain. + +Qualification is source-bound. This document defines the candidate and replay +requirements; it does not declare CI, compiler U02 or controlled performance passed. +Use the composition, composition-proof, native, safety, docs, archive, Linux and +WASI lanes and the unchanged quiet-host performance harness. Known compiler aborts +are distinct from ownership diagnostics, and explicit borrowed helper parameters +remain the supported caller form. A passing subset does not establish universal +static non-escape. Performance requires every pre/post window at least 90% idle, +no paging and review of any positive elapsed increase above 10%. + +No PR40 admitted roots, runtime handle, downstream pin, merge or release is included. +Published 1.6.0/1.6.1 tags and assets remain immutable. The downstream source-budget +and final-wrapper migration needs a separate accepted SHA and owner review. diff --git a/Documentation/Qualification/Composition/Performance.md b/Documentation/Qualification/Composition/Performance.md new file mode 100644 index 0000000..1f17b1b --- /dev/null +++ b/Documentation/Qualification/Composition/Performance.md @@ -0,0 +1,70 @@ +# Composition quiet-host qualification + +The Release comparison retains the full baseline +`30631e8fe60201518c00544c172340ec6576e0d5`, identical public benchmark source, +runtime/resource paths, two pinned inputs, five operations, ABBA ordering and +18 observations per revision/row. The review trigger remains a positive elapsed +increase greater than 10%. Every process must have three consecutive one-second +intervals at least 90% idle both before and immediately after execution. There +is no retry or settling period after a failed post window. Failed host admission +produces a blocked receipt, not a performance result or exception. + +## Observer investigation + +The three attempts of workflow `38810354356`, on composition proof head +`7ac75818f7974ab17d1bc44a39b08214eab15f16`, stopped at immediate post windows: +87.79%, 71.38% and 54.29% minimum idle. Those receipts remain immutable. They +show bursts of system CPU but contain no process attribution, so they cannot +establish which process caused every burst or quantify a regression. + +The former observer starts `top -l 4 -n 0 -s 1` for each window. Limiting its +display does not avoid system-wide process inspection. A local ABBA diagnostic +found roughly 1.2 CPU seconds per four-snapshot invocation even with framework +and memory-map display disabled. This is diagnostic evidence on a busy Mac, +not a controlled performance result. It motivates checking observer overhead +on the actual qualification host rather than rerunning the old gate blindly. + +`composition_host.py` samples the supported Darwin +`host_statistics(HOST_CPU_LOAD_INFO)` interface directly. SDK +`mach/host_info.h` and `mach/machine.h` define four unsigned 32-bit CPU tick +counters in user/system/idle/nice order. Four snapshots yield three one-second +interval deltas; boot totals are never treated as interval load. Checked shapes, +normal counter wrap, missing samples and nonfinite values have rejection tests. +All global activity, including the sampler's own CPU, remains in the denominator. +This removes process enumeration from admission without changing its threshold, +interval count or immediate-post policy. The owned Mach send right is released. + +Each new native receipt includes a pre-measurement ABBA observer experiment: +the exact former `top` command twice and sampler-only windows twice. It records +independent CPU ticks, observer CPU time, elapsed time and raw top logs. Process +and thermal snapshots are outside measurement windows; a failed post window +additionally captures diagnostics without changing the failure. Paging between +the pre/post windows now also blocks a comparison. No workload is stopped. + +Executable replacement, binary digest checks and a separately labelled preparation +launch precede pre-run admission for every process, equally for both revisions. +Preparation outputs are retained and checked for discovery/nonfinite values but +are never qualification samples. This lets first-launch cache/resource setup +settle before admission; it does not establish which service caused an old burst. +The measured processes still each perform their original two warmup sets and +nine samples. The first post-run native snapshot precedes JSON decoding, hashing +or receipt serialization. Eight harness tests reject omitted/changed workloads, +nonfinite/incomplete timings, malformed counters and sub-threshold intervals, +and preserve completion's non-additive peak-retention oracle. This Python +harness is distinct from the unchanged Swift Testing behavior suites. + +## Interpretation + +Unchanged native API counters must match. Composition completion intentionally +adds 50 work units and 424 final logical retained bytes; peak is +`max(previous peak, previous final retained + 424)`. Logical accounting does not +measure RSS. The pinned frozen schema resource is JSON/Pointer input here, not +protocol conformance or schema evaluation. Compiler ownership limitations and +bounded adapter-capacity refusals remain documented separately. + +Run `python3 -B Scripts/qualify_composition_performance.py --output ` +on the supported native host. Actual final-source outcomes, command exits, +source/fixture/binary hashes and materialized logs belong in the return evidence. +This contract does not claim an unexecuted pass. Marking the PR ready additionally +requires its current required native/safety/docs checks and relevant Linux and +archive/proof jobs; merge, tagging and publication remain separate actions. diff --git a/Documentation/Qualification/Composition/README.md b/Documentation/Qualification/Composition/README.md new file mode 100644 index 0000000..792bc7c --- /dev/null +++ b/Documentation/Qualification/Composition/README.md @@ -0,0 +1,99 @@ +# Borrowed composition proof + +This additive slice is confined to one synchronous borrowed native scope and +explicitly instrumented adapter work. It is not a full external importer, schema +compiler/evaluator composition, general callback instruction quota or RSS bound. +No package product or runtime dependency is added. + +The exact compiled public declarations live in JSONCompositionScope.swift and +JSONCompositionScope+URI.swift. The native grammar, scalar key identity and exact +number authority are reused. URI work uses core accounting; parsing temporarily +uses the parsing domain and Pointers the pointer domain, then returns to core. +No schema plan ownership is manufactured for URI identity. + +Run `python3 Scripts/ci.py --lane composition` and `--lane composition-proof`, followed by affected native, +safety and docs lanes. The ordinary public consumer package is +`Tools/CompositionQualification`; it has no package access, SPI, @testable or +shim. The root supplementary tests use existing package-only observers solely +to locate cancellation and substitution boundaries. They are not the oracle for +undercharging. + +The independent authored adapter's golden event trace is queue 65 work/96 +logical bytes, scratch 33/64, result 49/80 and final receipt 50/424: totals +197 work and 664 retained bytes. Three buffers remain returned and live. Exact +and one-below caps and an inserted work unit are tested. Omitting one byte's work +or reservation makes the unchanged positive trace assertion fail in a separate +expected-failing test process. This does not prove an arbitrary adapter is honest; +each downstream adapter needs its own ownership ledger and audited charges. + +All owned failures latch the first cause. Finalization verifies the original +ledger identity and health and prepays result/report overlap. Scope access is +borrowed and not Sendable; copying, storing, escaping, suspension, construction, +substitution, reset/fork/refund and private ledger access are separately rejected +by the compiler. A new invocation has new allowances and cannot restore the old +invocation's health. There is no public resource-release token in this slice. + +The 6.4 compiler crashed when a local nested helper captured the borrowed scope. +Use an explicit `borrowing JSONCompositionScope` helper parameter. Evidence must +retain that failure separately from the intended negative compiler probes. + +Qualification receipts belong outside the tracked public source distribution. +They must bind the source manifest, input pins, environment, exact counts, actual +commands/exits, mutant failures and per-platform status. A blocked Linux runtime +or SDK is not a native pass. There is no downstream pin/release authorization +in this proof. The user-facing return supplies materialized receipts and patches. + +The direct escaping-closure capture shape also crashes the observed 6.4 compiler +during SIL ownership verification (`Have operand with incompatible ownership?!`, +`copy_addr`). It is preserved under `CompilerBugs` and in failed exploratory +receipts; it is not counted as an intended compiler-negative pass. The negative +escape probe instead attempts to return an escaping closure from an explicit +borrowed-parameter helper and requires a normal ownership diagnostic. These +compiler defects remain usability/qualification limitations, not runtime passes. + +Forming a bound method reference also triggers the same observed SIL verifier +crash, both directly and through a borrowed helper. Those two reproducers are +retained separately as compiler limitations. Call an explicit borrowed helper +instead of forming a method value. The return probe uses explicit `return`; an +unused noncopyable expression can infer Void and does not attempt escape. +The storage probe explicitly consumes to test the borrowed-value restriction. + +## Compiler investigation follow-up + +Reduced no-import reproducers reproduce the same SIL ownership verifier failures +on Apple Swift 6.4.0.34.1 and the installed Swift.org 6.4 release. An explicit +`(scope: borrowing JSONCompositionScope)` closure parameter fixes the nested local +helper and changes escaping capture into the intended diagnostic. The public +consumer now executes that nested form with an independent 55-work/432-byte ledger, +exact and one-below work ceilings. A separate explicit-capture negative build joins +the existing helper-escape probe: 26 public functions, two configurations, two +rejected mutants and 14 intended diagnostic builds. + +Bound method references remain compiler defects even with explicit borrowing. +The reduced compiler investigation is separate from intended negative builds; +crashes never count as diagnostic passes. See the CompilerBugs catalog and its +replay command. The final local report retains the prior frozen proof unchanged +and binds successor evidence to this branch's final source identity. + +## Controlled native performance replay + +`python3 Scripts/qualify_composition_performance.py --output .build/composition-performance` +compares the approved full main base 30631e8fe60201518c00544c172340ec6576e0d5 +and the current source. Both use the same Swift 6.4+ compiler, native Release +build engine, benchmark source, runtime path and identical schema resource bytes. +Two warmup sets precede nine samples per process; ABBA ordering yields 18 samples +per source/row. Five operations run for wide512 and the pinned frozen JSON input. +`schema-only` always compiles the same small schema, not the frozen definitions. +Standalone Data/document and Pointer reports are unavailable and remain null. + +Every process requires at least 90% idle before and immediately after timing. +A bounded settling period precedes runs; a failed post-run probe invalidates the +controlled result. The 10% review trigger is unchanged. Exit 2 means blocked and +exit 3 requires review; neither is a performance pass or automatic exception. +The [observer investigation and replay contract](Performance.md) specifies the +lightweight native CPU sampler, raw-counter evidence and paging checks. +Logical retention and owned work remain distinct from RSS and caller instructions. +The Composition performance workflow executes this exact harness when its benchmark +files change in a PR, or through optional manual dispatch after default-branch +availability, on an independent native runner, retaining source/fixture/binary/log identities. +It changes no existing required check name, protection or release workflow. diff --git a/Documentation/Qualification/Composition/Reconciliation.md b/Documentation/Qualification/Composition/Reconciliation.md new file mode 100644 index 0000000..aca8cd8 --- /dev/null +++ b/Documentation/Qualification/Composition/Reconciliation.md @@ -0,0 +1,105 @@ +# Composition reconciliation: PR35 and PR36 + +## Reconciliation with schema examples + +The successor incorporates main +`c79d48c15f83ef88bed9459d9abcf05e54bd0bea` (PR39) without changing its +JSONSchemaExamples implementation, schema compiler support, consumers, fixtures +or documentation. The existing composition runtime, proof package and performance +harness remain byte-identical to `47ea4ef71dd7286ef0ec81342a8bb527438ce4b5`. +Main's nine-product graph and empty root dependency graph are retained. + +The combined inventory requires 697 Mac / 681 Linux / 593 WASI root functions, +325 Mac/Linux / 288 WASI parameter invocations and 47 Mac / 46 Linux shared-state +functions. All 98 schema-example functions, 39 Navigation functions, 34 integration +functions, 126 restored identities and 49 baseline cap probes remain required. +The two composition lanes and every S3 consumer, ownership, construction, matrix +and lifetime gate are retained. Eight public library DocC catalogs are required. +The harness rejects omission of either proof dispatch or S3 qualification; +cardinality checks still require the complete module and parameter inventories. + +The four conflicts are resolved by combining CI dispatch/cardinality guards, +combining provenance notes and regenerating the CLI stamp from combined source. +No implementation, quota, safety policy, baseline or review trigger is replaced. +Actual source-bound receipts establish execution results, rather than these counts. +Quiet-host performance remains pending by maintainer instruction. No timing retry, +PR readiness change, merge into main, tag or publication is part of reconciliation. + +The following sections retain their original increment and source context. + +PR35 landed as main 30631e8fe60201518c00544c172340ec6576e0d5. This follow-up +reconciles that implementation with proof head +bab6316d31e9c1aca30ba3f9040b20edf1e6ae85 from PR36. The independent original +proofs and source archives remain historical evidence, not receipts for this tree. +No downstream importer, release or dependency adoption is included. + +## Selected implementation + +There is one noncopyable borrowed `JSONCompositionScope`, one private state and +one native ledger. PR35's native parse, Pointer, trusted admission, URI and +terminal-failure routes are retained. JSON remains independent of JSONSchema. +The original `reserveExternalLogicalBytes(_:count:)` declaration is retained; +the explicit `overhead:` overload checks multiplication and addition before growth. + +PR36 adds per-document input capacity retirement with cumulative reported counts +and persistent accepted-capacity history. It also prepays completion's independent +49-slot report: 424 logical bytes and 50 work units. These are intentional +composition accounting corrections. Existing standalone API counters, all 49 +metric meanings, finite profiles and native accounting weights are unchanged. +Original continuation health/identity is checked before and after completion. +A caught owned quota, arithmetic, syntax or cancellation failure cannot finalize. + +The 12 original root behavior functions and five external reference-import +functions remain, including the failed direct 128/128 capacity hypothesis and +eight characterized cells. Their assertions now include report/result overlap; +the same native work still refuses the direct adapter after 98 references. +The two supplementary native functions and 26 independent public functions from +PR36 add cancellation boundaries, original-ledger identity, per-source capacity, +exact values, event-ledger thresholds and deliberate omitted-charge mutants. +There are 599 native / 583 Linux root functions and 325 parameter invocations. +The 126 restored identities and 49 exact public cap probes remain required. + +## Reproducible checks + +Run Scripts/ci.py lanes `composition`, `composition-proof`, `native`, `safety`, +`docs` and `archive` on the supported native toolchain. Linux architecture jobs +retain `linux` and `linux-safety` plus both focused composition lanes. Existing +protected check names/settings are preserved. The focused proof is a separate +job so root discovery guards and reference-import cardinalities stay explicit. +The original lane requires 12 root / five external functions, eight scale cells, +11 intended compiler diagnostics and distinct consumers. The proof lane requires +two root / 26 public functions, two rejected mutants and 14 intended diagnostics. +Crashes never count as normal diagnostic rejection. Exact discovery guards retain +15 Python harness functions; additional rejection assertions live in that harness. + +Compiler reductions and qualified explicit-borrow helper forms are preserved in +`Tools/CompositionQualification/CompilerBugs`. Known Swift 6.4 inferred-capture and +bound-method-value crashes remain usability limits. No unsafe annotation or +experimental ownership flag is added. Current main’s eight public products are retained and root +external dependencies remain empty. Physical devices are not requalified here; +portable compilation/runtime evidence must identify its actual host and SDK. + +## Performance and evidence policy + +The controlled Release comparison uses main 30631e8 as its full pinned baseline. +Both existing PRs' comparisons against 037e735 remain historical. Identical public +harnesses, runtime/resource paths and fixtures compare unchanged APIs and the +composition document route. Existing API counters must match; composition's +expected completion delta is exactly 50 work / 424 final logical bytes. Peak is +max(previous peak, final retained plus report), rather than a sum of stage peaks. Five operations +on two inputs, nine samples per process and ABBA ordering yield 18 samples per +revision/row. Each pre/post admission requires three interval samples >=90% idle. +The review trigger stays 10%; operational refusals and blocked/noisy timing are +never evaluated successes. No RSS, throughput or arbitrary adapter honesty claim. + +Final commands, exits, source hashes, manifest-backed archives, materialized hosted +receipts, performance disposition and review findings belong in the maintainer's +return evidence. Source documents here specify replay and acceptance, not a +fabricated pass. Private evidence and build caches are excluded from source archives. + +## Current-main reconciliation + +Main `7d72c8825982550f0ee87218136fda12d8bae13b` independently added JSONNavigation. +This branch retains that source, all 39 Navigation functions, the normal public +consumer, exact product/dependency checks and seven DocC catalogues. Composition +proofs remain additive; no Navigation or historical behavior is removed. diff --git a/Documentation/Qualification/README.md b/Documentation/Qualification/README.md index e6c1394..4e4dae3 100644 --- a/Documentation/Qualification/README.md +++ b/Documentation/Qualification/README.md @@ -2,7 +2,7 @@ The 1.5.1 repair candidate restores all 126 functions from the separate 1.2.1 maintenance line without removing the later HTML, bridge, provider or CLI suites. -The experimental composition increment adds 12 functions. Expected full discovery is 558 native / 542 Linux functions, with 325 parameter +The reconciled experimental composition increment retains 12 functions and adds two owned-boundary functions. Expected full discovery is 560 native / 544 Linux functions, with 325 parameter invocations. The restored QA subset is 145 functions; shared-state safety is 42 native / 41 Linux. [TestDiscovery.json](TestDiscovery.json) supplies exact module cardinalities, while the event reader checks identities and each parameter count. diff --git a/Documentation/Qualification/TestDiscovery.json b/Documentation/Qualification/TestDiscovery.json index 6f5fc8a..73db643 100644 --- a/Documentation/Qualification/TestDiscovery.json +++ b/Documentation/Qualification/TestDiscovery.json @@ -6,7 +6,7 @@ "JSONRegexRuntimeTests": 16, "JSONStreamingTests": 30, "JSONPatchTests": 16, - "JSONIntegrationTests": 32, + "JSONIntegrationTests": 34, "JSONHTMLTests": 97, "JSONBridgesTests": 10, "JSONCLITests": 88, @@ -70,7 +70,7 @@ "JSONRegexRuntimeTests": 16, "JSONStreamingTests": 30, "JSONPatchTests": 16, - "JSONIntegrationTests": 32, + "JSONIntegrationTests": 34, "JSONHTMLTests": 81, "JSONBridgesTests": 10, "JSONCLITests": 88, @@ -84,7 +84,7 @@ "JSONRegexRuntimeTests": 16, "JSONStreamingTests": 30, "JSONPatchTests": 16, - "JSONIntegrationTests": 32, + "JSONIntegrationTests": 34, "JSONHTMLTests": 81, "JSONBridgesTests": 10, "JSONNavigationTests": 47, @@ -108,5 +108,6 @@ "export_functions": { "JSONSchemaTests": 1, "JSONHTMLTests": 39 - } + }, + "composition_public_functions": 29 } diff --git a/Scripts/ci.py b/Scripts/ci.py index 096ea07..5591b06 100644 --- a/Scripts/ci.py +++ b/Scripts/ci.py @@ -36,6 +36,11 @@ def commands(lane, output): ['python3', '-B', 'Scripts/qualify_projections.py', '--output', str(output / 'projection-codegen')], ['python3', '-B', 'Scripts/qualify_public_projections.py', '--output', str(output / 'public-projection-consumer')]] return [c + ['--disable-xctest'] if sys.platform == 'linux' and c[:2] == ['swift', 'test'] else c for c in result] + if lane == 'composition-proof': + result = [['swift', 'test', '--filter', 'jcompOwned|jcompOriginal'], + ['swift', 'test', '-c', 'release', '--filter', 'jcompOwned|jcompOriginal'], + ['python3', '-B', 'Scripts/qualify_composition_proof.py', '--output', str(output / 'public')]] + return [c + ['--disable-xctest'] if sys.platform == 'linux' and c[:2] == ['swift', 'test'] else c for c in result] if lane == 'schema-examples-qualification': return commands('schema-examples-advanced', ROOT / output) + [['python3', '-B', 'Scripts/qualify_schema_example_matrix.py', '--output', str(output / 'matrix')]] if lane == 'schema-examples-advanced': @@ -69,9 +74,9 @@ def commands(lane, output): ['swift', 'test', '-c', 'release', '--filter', 'composition|composed|caughtOperationalFailureCannotComplete'], ['swift', 'run', '--package-path', 'Tools/CompositionQualification', 'CompositionConsumer'], ['swift', 'run', '-c', 'release', '--package-path', 'Tools/CompositionQualification', 'CompositionConsumer'], - ['swift', 'test', '--package-path', 'Tools/CompositionQualification'], - ['swift', 'test', '-c', 'release', '--package-path', 'Tools/CompositionQualification'], - ['swift', 'test', '-c', 'release', '--sanitize', 'address', '--scratch-path', '.build/composition-address', '--package-path', 'Tools/CompositionQualification'], + ['swift', 'test', '--package-path', 'Tools/CompositionQualification', '--filter', 'CompositionScaleTests'], + ['swift', 'test', '-c', 'release', '--package-path', 'Tools/CompositionQualification', '--filter', 'CompositionScaleTests'], + ['swift', 'test', '-c', 'release', '--sanitize', 'address', '--scratch-path', '.build/composition-address', '--package-path', 'Tools/CompositionQualification', '--filter', 'CompositionScaleTests'], ['python3', '-B', 'Scripts/qualify_composition_scale.py', '--output', str(output / 'scale')], ['python3', '-B', 'Scripts/qualify_composition.py', '--output', str(output / 'compiler-negative')], ] @@ -222,7 +227,7 @@ def check_result(command, code, log, events=None): passed = code == 0 if events is not None: counts = events['bundle_counts'] if command[:2] == ['swift', 'test']: - expected = [DISCOVERY['composition_external_functions']] if 'Tools/CompositionQualification' in command else [12] if 'composition|composed|caughtOperationalFailureCannotComplete' in command else [79] if 'JSONSchemaTests' in command else [4 if 'thread' in command else 98] if 'JSONSchemaExamplesTests' in command else [88] if 'cli|sharedFilterPlan' in command else sorted(DISCOVERY['export_functions'].values()) if 'export|Export|apple' in command else [14] if 'Tools/WasmAudit' in command else sorted(DISCOVERY['qa_functions'].values()) if 'qa' in command else [7] if R0_TESTS in command else [3] if 'Tools/MCPReadiness' in command or 'Tools/MobileQualification' in command else [1] if 'Tools/Qualification' in command else sorted(SHARED_COUNTS.values()) if 'thread' in command else sorted(ROOT_COUNTS.values()) + expected = [2] if 'jcompOwned|jcompOriginal' in command else [DISCOVERY['composition_external_functions']] if 'Tools/CompositionQualification' in command else [12] if 'composition|composed|caughtOperationalFailureCannotComplete' in command else [79] if 'JSONSchemaTests' in command else [4 if 'thread' in command else 98] if 'JSONSchemaExamplesTests' in command else [88] if 'cli|sharedFilterPlan' in command else sorted(DISCOVERY['export_functions'].values()) if 'export|Export|apple' in command else [14] if 'Tools/WasmAudit' in command else sorted(DISCOVERY['qa_functions'].values()) if 'qa' in command else [7] if R0_TESTS in command else [3] if 'Tools/MCPReadiness' in command or 'Tools/MobileQualification' in command else [1] if 'Tools/Qualification' in command else sorted(SHARED_COUNTS.values()) if 'thread' in command else sorted(ROOT_COUNTS.values()) if 'JSONSchemaExamplesTests' in command and events is not None: passed = passed and events['module_function_counts'] == {'JSONSchemaExamplesTests': expected[0]} and events['parameter_case_invocations'] == 0 if 'JSONSchemaTests' in command and events is not None: @@ -255,6 +260,8 @@ def check_result(command, code, log, events=None): if command[:3] == ['python3', '-B', 'Scripts/qualify_cli.py'] and passed: data = json.loads(log.strip().splitlines()[-1]) passed = data['observed'] == data['passed'] == 53 and data['failed'] == 0 + if command[:3] == ['python3', '-B', 'Scripts/qualify_composition_proof.py'] and passed: + passed = 'COMPOSITION_PROOF passed public_tests=29 configurations=2 mutants_rejected=2 compiler_negative=14' in log.splitlines() if command[:3] == ['python3', '-B', 'Scripts/qualify_composition_scale.py'] and passed: data = json.loads(log.strip().splitlines()[-1]) passed = data['status'] == 'passed' and (data['cells'], data['completed_imports'], data['operational_refusals']) == (8, 5, 3) and data['adoption_status'] == 'bounded_fixtures_only' and data['simple_strategy_capacity_status'] == 'blocked' and data['unmet_capacity_targets'] == ['balanced-128-128'] @@ -333,7 +340,7 @@ def check_result(command, code, log, events=None): def main(): parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument('--lane', choices=['composition', 'navigation', 'inspection', 'native', 'safety', 'docs', 'r0', 'qa', 'linux', 'linux-safety', 'wasm', 'browser', 'export', 'cli', 'schema-examples', 'schema-examples-safety', 'schema-examples-determinism', 'schema-examples-construction', 'schema-examples-advanced', 'schema-examples-qualification', 'coverage', 'linux-coverage', 'linux-qa', 'wasm-qa', 'archive'], required=True) + parser.add_argument('--lane', choices=['composition-proof', 'composition', 'navigation', 'inspection', 'native', 'safety', 'docs', 'r0', 'qa', 'linux', 'linux-safety', 'wasm', 'browser', 'export', 'cli', 'schema-examples', 'schema-examples-safety', 'schema-examples-determinism', 'schema-examples-construction', 'schema-examples-advanced', 'schema-examples-qualification', 'coverage', 'linux-coverage', 'linux-qa', 'wasm-qa', 'archive'], required=True) parser.add_argument('--output') args = parser.parse_args() output = ROOT / (args.output or '.build/ci/' + args.lane) @@ -342,7 +349,7 @@ def main(): repository = subprocess.run(['git', 'rev-parse', '--show-toplevel'], cwd=ROOT, capture_output=True, text=True) is_checkout = repository.returncode == 0 and pathlib.Path(repository.stdout.strip()).resolve() == ROOT.resolve() commit = subprocess.run(['git', 'rev-parse', 'HEAD'], cwd=ROOT, capture_output=True, text=True) if is_checkout else None - environment = collect_linux() if args.lane.startswith('linux') or (args.lane in ('composition', 'inspection') and sys.platform == 'linux') else collect() + environment = collect_linux() if args.lane.startswith('linux') or ((args.lane.startswith('composition') or args.lane == 'inspection') and sys.platform == 'linux') else collect() receipt = {'schema_version': 1, 'lane': args.lane, 'status': 'blocked', 'environment': environment, 'started_utc': datetime.datetime.now(datetime.timezone.utc).isoformat(), 'git_commit': commit.stdout.strip() if commit is not None and commit.returncode == 0 else None, 'github_run_id': os.environ.get('GITHUB_RUN_ID'), 'source_inputs': inputs, 'source_manifest_sha256': hashlib.sha256(json.dumps(inputs, sort_keys=True).encode()).hexdigest(), 'commands': []} def save(): (output / 'receipt.json').write_text(json.dumps(receipt, indent=2) + '\n') diff --git a/Scripts/composition_host.py b/Scripts/composition_host.py new file mode 100644 index 0000000..d6cf29c --- /dev/null +++ b/Scripts/composition_host.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: Apache-2.0 +"""Low-observer-cost Darwin host admission; no process enumeration during timing. + +The SDK's mach/host_info.h defines HOST_CPU_LOAD_INFO=3 and four natural_t +cpu_ticks in USER/SYSTEM/IDLE/NICE order. Preserve raw snapshots in receipts. +All CPU activity, including this observer, remains in the denominator. +""" +import ctypes +import math +import platform +import resource +import time + + +def interval(before, after): + if len(before) != 4 or len(after) != 4 or any(type(v) is not int or not 0 <= v < 2**32 for v in [*before, *after]): + raise ValueError('Invalid HOST_CPU_LOAD_INFO counters') + delta = [(b-a) % 2**32 for a, b in zip(before, after)] + if not sum(delta): + raise ValueError('CPU counters did not advance') + return {'delta_ticks': delta, 'idle_percent': 100 * delta[2] / sum(delta)} + + +def admitted(window): + values = window['interval_idle_percent'] + return len(values) == 3 and all(math.isfinite(v) and 90 <= v <= 100 for v in values) + + +class DarwinCPU: + def __init__(self): + if platform.system() != 'Darwin': + raise OSError('Darwin host CPU counters required') + self.lib = ctypes.CDLL('/usr/lib/libSystem.B.dylib') + self.lib.mach_host_self.restype = ctypes.c_uint32 + self.lib.host_statistics.argtypes = [ctypes.c_uint32, ctypes.c_int, ctypes.POINTER(ctypes.c_uint32), ctypes.POINTER(ctypes.c_uint32)] + self.lib.host_statistics.restype = ctypes.c_int + self.lib.mach_port_deallocate.argtypes = [ctypes.c_uint32, ctypes.c_uint32] + self.lib.mach_port_deallocate.restype = ctypes.c_int + self.port = self.lib.mach_host_self() + + def close(self): + if self.port: + code = self.lib.mach_port_deallocate(ctypes.c_uint32.in_dll(self.lib, 'mach_task_self_').value, self.port) + self.port = 0 + if code: + raise OSError('mach_port_deallocate failed: '+str(code)) + + def snapshot(self): + ticks = (ctypes.c_uint32 * 4)() + count = ctypes.c_uint32(4) + code = self.lib.host_statistics(self.port, 3, ticks, ctypes.byref(count)) + if code or count.value != 4: + raise OSError('HOST_CPU_LOAD_INFO failed: '+str((code, count.value))) + return {'monotonic_seconds': time.monotonic(), 'cpu_ticks': list(ticks)} + + def window(self): + usage = resource.getrusage(resource.RUSAGE_SELF) + snapshots = [self.snapshot()] + for _ in range(3): + time.sleep(1) + snapshots.append(self.snapshot()) + rows = [interval(a['cpu_ticks'], b['cpu_ticks']) for a, b in zip(snapshots, snapshots[1:])] + after = resource.getrusage(resource.RUSAGE_SELF) + return {'method': 'host_statistics(HOST_CPU_LOAD_INFO)', 'snapshots': snapshots, + 'intervals': rows, 'interval_idle_percent': [r['idle_percent'] for r in rows], + 'first_cumulative_snapshot_excluded': True, + 'observer_cpu_seconds': after.ru_utime+after.ru_stime-usage.ru_utime-usage.ru_stime} diff --git a/Scripts/probe_composition_compiler.py b/Scripts/probe_composition_compiler.py new file mode 100644 index 0000000..f667107 --- /dev/null +++ b/Scripts/probe_composition_compiler.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: Apache-2.0 +"""Record standalone compiler outcomes; observed compiler crashes return exit 2.""" +import argparse, hashlib, json, os, pathlib, subprocess, time + +ROOT = pathlib.Path(__file__).resolve().parents[1] + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--compiler', default='swiftc') + parser.add_argument('--output', required=True) + args = parser.parse_args() + output = pathlib.Path(args.output).resolve(); output.mkdir(parents=True, exist_ok=True) + version = subprocess.run([args.compiler, '--version'], capture_output=True, text=True, check=True).stdout + receipt = {'compiler': args.compiler, 'version': version, 'sdkroot': os.environ.get('SDKROOT'), + 'status': 'running', 'commands': []} + for fixture in sorted((ROOT/'Tools/CompositionQualification/CompilerBugs/Reduced').glob('*.swift.txt')): + name = fixture.name.removesuffix('.swift.txt') + source = output/(name+'.swift'); source.write_bytes(fixture.read_bytes()) + command = [args.compiler, '-swift-version', '6', '-module-cache-path', str(output/'cache'), + '-c', str(source), '-o', str(output/(name+'.o'))] + started = time.monotonic() + try: + run = subprocess.run(command, capture_output=True, text=True, timeout=120) + code, text = run.returncode, run.stdout+run.stderr + except subprocess.TimeoutExpired as error: + code, text = None, str(error) + log = output/(name+'.log'); log.write_text(text) + crash = code is not None and (code < 0 or any(s in text for s in ['Stack dump', 'Please submit a bug report', 'SIL verification failed'])) + outcome = 'compiler_crash' if crash else 'compiled' if code == 0 else 'timeout' if code is None else 'diagnostic' + receipt['commands'].append({'fixture': fixture.relative_to(ROOT).as_posix(), + 'source_sha256': hashlib.sha256(source.read_bytes()).hexdigest(), 'command': command, + 'exit_code': code, 'outcome': outcome, 'seconds': time.monotonic()-started, + 'log': log.name, 'log_sha256': hashlib.sha256(log.read_bytes()).hexdigest()}) + (output/'receipt.json').write_text(json.dumps(receipt, indent=2)+'\n') + print(name, outcome, 'exit='+str(code), flush=True) + receipt['counts'] = {key:sum(r['outcome']==key for r in receipt['commands']) + for key in ['compiled', 'diagnostic', 'compiler_crash', 'timeout']} + receipt['status'] = 'compiler_defects_observed' if receipt['counts']['compiler_crash'] else 'recorded' + (output/'receipt.json').write_text(json.dumps(receipt, indent=2)+'\n') + return 2 if receipt['counts']['compiler_crash'] else 1 if receipt['counts']['timeout'] else 0 + +if __name__ == '__main__': raise SystemExit(main()) diff --git a/Scripts/qualify_composition.py b/Scripts/qualify_composition.py index b88f0de..68cb17d 100644 --- a/Scripts/qualify_composition.py +++ b/Scripts/qualify_composition.py @@ -29,7 +29,7 @@ def main(): log = run.stdout + run.stderr (output / (name + '.log')).write_text(log) records.append(dict(name=name, command=command, exit_code=run.returncode, - expected_diagnostic=diagnostic, status='passed' if run.returncode != 0 and diagnostic in log and 'no such module' not in log else 'failed')) + expected_diagnostic=diagnostic, status='passed' if run.returncode == 1 and diagnostic in log and not any(s in log for s in ['no such module', 'Stack dump', 'Please submit a bug report', 'SIL verification failed']) else 'failed')) receipt = dict(status='passed' if all(r['status']=='passed' for r in records) else 'failed', compiler_negative_cases=len(records), records=records) (output / 'receipt.json').write_text(json.dumps(receipt, indent=2)+'\n') diff --git a/Scripts/qualify_composition_performance.py b/Scripts/qualify_composition_performance.py new file mode 100644 index 0000000..7ca3726 --- /dev/null +++ b/Scripts/qualify_composition_performance.py @@ -0,0 +1,226 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: Apache-2.0 +"""Matched native Release ABBA comparison with mandatory quiet-host admission.""" +import argparse, datetime, hashlib, json, math, os, pathlib, re, resource, shutil, statistics, subprocess, tarfile, time +from preflight import collect +from archive_candidate import files +from composition_host import DarwinCPU, admitted + +ROOT = pathlib.Path(__file__).resolve().parents[1] +BASE = '30631e8fe60201518c00544c172340ec6576e0d5' +FIXTURE_SHA = 'ef70b61f99b6d2e5e3b46863822eab08dff6a45bedc7a08914e0e5b133f40203' +NAMES = {'parser-bytes-wide512': 8, 'parser-document-data-wide512': 6, + 'pointer-wide512': 30, 'schema-only': 3, 'composition-document': 4} + +def digest(path): return hashlib.sha256(path.read_bytes()).hexdigest() +def inventory(directory): + return {p.relative_to(directory).as_posix(): digest(p) for p in directory.rglob('*') if p.is_file()} +def source_identity(): + return [{'path':p.relative_to(ROOT).as_posix(), 'sha256':digest(p)} for p in files()] + +def check_samples(rows): + if len(rows)!=5 or {row['name']:row['iterations'] for row in rows} != NAMES: + raise ValueError('Changed benchmark discovery/iterations') + if any(len(row['seconds'])!=9 or any(not math.isfinite(v) or v<=0 for v in row['seconds']) for row in rows): + raise ValueError('Missing/nonpositive/nonfinite timing samples') + +def completion_counters(baseline): + work, peak, retained, size = baseline + return (work+50, max(peak, retained+424), retained+424, size) + +class Blocked(Exception): pass + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--output', required=True) + args = parser.parse_args(); output = pathlib.Path(args.output).resolve(); output.mkdir(parents=True, exist_ok=True) + inputs = source_identity() + receipt = {'status':'running', 'baseline':BASE, 'candidate':None, 'environment':collect(), + 'source_inputs':inputs, 'source_manifest_sha256':hashlib.sha256(json.dumps(inputs, sort_keys=True).encode()).hexdigest(), + 'configuration':'release', 'build_system':'native', 'review_trigger_percent':10, + 'minimum_pre_and_post_idle_percent':90, 'host_sampler':'Darwin HOST_CPU_LOAD_INFO; three one-second intervals; no process enumeration', + 'commands':[], 'host_checks':[], 'observer_experiment':[], 'preparations':[], 'runs':[], + 'started_utc':datetime.datetime.now(datetime.timezone.utc).isoformat()} + def save(): (output/'receipt.json').write_text(json.dumps(receipt, indent=2)+'\n') + def run(name, command, env=None): + log = output/(name+'.log'); start = time.monotonic() + with log.open('w') as stream: + result = subprocess.run(command, cwd=ROOT, env=env, stdout=stream, stderr=subprocess.STDOUT, timeout=1200) + receipt['commands'].append({'name':name, 'command':command, 'exit_code':result.returncode, + 'seconds':time.monotonic()-start, 'log':log.name, 'log_sha256':digest(log)}) + save() + if result.returncode: raise ValueError('Command failed: '+name) + return log.read_text() + cpu = None + def host(name): + window = cpu.window() + log = output/(name+'.log'); log.write_text(json.dumps(window, indent=2)+'\n') + idle = min(window['interval_idle_percent']) + receipt['host_checks'].append(dict(window, name=name, idle_percent=idle, + admitted=admitted(window), log=log.name, log_sha256=digest(log))) + save(); print(name, 'idle='+str(idle), flush=True) + return admitted(window) + def diagnostics(name): + # Outside measurement/admission; never masks an immediate failed post window. + run(name+'-processes', ['ps','-Ao','pid,pcpu,state,comm','-r']) + run(name+'-thermal', ['pmset','-g','therm']) + def swap(name): + text = run(name+'-vm', ['vm_stat']) + values = [re.search(r'^'+key+r':\s*(\d+)\.', text, re.M) for key in ['Swapins','Swapouts']] + if not all(values): raise ValueError('Missing native swap counters') + return [int(v[1]) for v in values] + try: + save() + if receipt['environment']['status'] != 'passed': raise Blocked('Supported native Mac/toolchain/SDK required') + run('harness-tests', ['python3','-B','Scripts/test_composition_performance.py']) + cpu = DarwinCPU() + run('cpu', ['sysctl','-n','machdep.cpu.brand_string']) + run('memory', ['sysctl','-n','hw.memsize']) + run('os-build', ['sw_vers','-buildVersion']) + run('compiler', ['swift','--version']) + receipt['candidate'] = run('candidate', ['git','rev-parse','HEAD']).strip() + if run('baseline', ['git','rev-parse',BASE]).strip() != BASE: raise ValueError('Unexpected baseline') + baseline = output/'baseline-source'; baseline.mkdir(exist_ok=True) + archive = output/'baseline.tar' + with archive.open('wb') as stream: subprocess.run(['git','archive',BASE], cwd=ROOT, stdout=stream, check=True) + with tarfile.open(archive) as tar: + if any(m.issym() or m.islnk() or pathlib.PurePosixPath(m.name).is_absolute() or '..' in pathlib.PurePosixPath(m.name).parts for m in tar): + raise ValueError('Unsafe baseline archive') + tar.extractall(baseline) + source = ROOT/'Tools/CompositionQualification/Sources/CompositionBenchmark/main.swift' + receipt['benchmark_source_sha256'] = digest(source) + fixture = ROOT/'Tools/MCPReadiness/Sources/MCPReadinessKit/Resources/schema-2026-07-28.json' + if digest(fixture) != FIXTURE_SHA: raise ValueError('Changed frozen fixture') + receipt['fixture'] = {'path':fixture.relative_to(ROOT).as_posix(), 'bytes':fixture.stat().st_size, 'sha256':FIXTURE_SHA, + 'scope':'JSON bytes/Pointer only; schema-only always compiles the fixed small schema, not this definitions-only resource'} + binaries = {} + for name, dependency in [('baseline',baseline), ('candidate',ROOT)]: + package = output/(name+'-package'); target = package/'Sources/CompositionBenchmark'; target.mkdir(parents=True, exist_ok=True) + shutil.copy2(source,target/'main.swift') + (package/'Package.swift').write_text('// swift-tools-version: 6.4\nimport PackageDescription\nlet package = Package(name: "MatchedBenchmark", platforms: [.macOS("27.0")], dependencies: [.package(name: "Swift-JSON", path: '+json.dumps(str(dependency))+')], targets: [.executableTarget(name: "CompositionBenchmark", dependencies: [.product(name: "JSON", package: "Swift-JSON"), .product(name: "JSONSchema", package: "Swift-JSON")])], swiftLanguageModes: [.v6])\n') + command = ['swift','build','--build-system','native','--package-path',str(package),'-c','release','--jobs','2'] + run(name+'-build',command+['--product','CompositionBenchmark']) + location = run(name+'-bin-path',command+['--show-bin-path']) + paths = [pathlib.Path(line) for line in location.splitlines() + if line.startswith('/') and (pathlib.Path(line)/'CompositionBenchmark').is_file()] + if len(paths)!=1: raise ValueError('Ambiguous/missing native binary directory') + binaries[name] = paths[0]/'CompositionBenchmark' + bundle = 'Swift-JSON_JSONSchema.bundle' + resources = inventory(binaries['baseline'].parent/bundle) + if not resources or resources != inventory(binaries['candidate'].parent/bundle): raise ValueError('Resource bundles differ') + runtime = output/'Runtime'; runtime.mkdir(exist_ok=True) + if (runtime/bundle).exists(): shutil.rmtree(runtime/bundle) + shutil.copytree(binaries['baseline'].parent/bundle, runtime/bundle) + receipt['resource_files'] = resources + receipt['binaries'] = {name:{'sha256':digest(path), 'bytes':path.stat().st_size} for name,path in binaries.items()} + # ABBA observer experiment, before qualification. The legacy top monitor + # performs system-wide per-process inspection even with -n 0. Record its + # own CPU and independent global ticks, alongside sampler-only windows. + for index, mode in enumerate(['legacy-top','native-only','native-only','legacy-top']): + label = 'observer-'+str(index)+'-'+mode + before = resource.getrusage(resource.RUSAGE_CHILDREN); start = time.monotonic() + if mode == 'legacy-top': + command = ['top','-l','4','-n','0','-s','1'] + with (output/(label+'.log')).open('w') as stream: + monitor = subprocess.Popen(command, stdout=stream, stderr=subprocess.STDOUT) + try: + window = cpu.window() + code = monitor.wait(timeout=20) + finally: + if monitor.poll() is None: + monitor.terminate() + monitor.wait(timeout=5) + if code: raise ValueError('Observer diagnostic command failed') + else: + window = cpu.window(); command = None + (output/(label+'.log')).write_text(json.dumps(window, indent=2)+'\n') + after = resource.getrusage(resource.RUSAGE_CHILDREN) + receipt['observer_experiment'].append(dict(name=label, command=command, + window=window, elapsed_seconds=time.monotonic()-start, + child_cpu_seconds=after.ru_utime+after.ru_stime-before.ru_utime-before.ru_stime, + log=label+'.log', log_sha256=digest(output/(label+'.log')))) + diagnostics('measurement-start') + save() + for workload in ['wide512','frozen-schema']: + env = os.environ.copy(); env.pop('JCOMP_BENCHMARK_INPUT',None) + if workload == 'frozen-schema': env['JCOMP_BENCHMARK_INPUT'] = str(fixture) + for index, name in enumerate(['baseline','candidate','candidate','baseline']): + label = workload+'-'+str(index)+'-'+name + # Finish executable replacement and receipt I/O before pre-admission. + shutil.copy2(binaries[name],runtime/'CompositionBenchmark') + if digest(runtime/'CompositionBenchmark') != receipt['binaries'][name]['sha256']: + raise ValueError('Changed runtime binary') + # A separately labelled first launch precedes admission. This + # prepares Foundation/resources and OS executable caches equally + # for both revisions. Its samples are never qualification data. + prepared = json.loads(run(label+'-prepare', [str(runtime/'CompositionBenchmark')], env)) + check_samples(prepared) + receipt['preparations'].append(dict(workload=workload, name=name, + log=label+'-prepare.log', qualification_samples=False)) + before_swap = swap(label+'-before') + # A bounded settling period is allowed before a measurement. + # Post-run eligibility is checked immediately, with no waiting to hide noise. + for attempt in range(12): + if host(label+'-before-'+str(attempt)): break + time.sleep(3) + else: raise Blocked('No 90% pre-run idle window: '+label) + log = output/(label+'.log'); start = time.monotonic() + with log.open('w') as stream: + result = subprocess.run([str(runtime/'CompositionBenchmark')], cwd=ROOT, env=env, + stdout=stream, stderr=subprocess.STDOUT, timeout=1200) + elapsed = time.monotonic()-start + # No hashing, JSON decoding, receipt serialization or settling delay + # between process exit and the first post-run counter snapshot. + post_ok = host(label+'-after') + receipt['commands'].append(dict(name=label, command=[str(runtime/'CompositionBenchmark')], + exit_code=result.returncode, seconds=elapsed, log=log.name, log_sha256=digest(log))) + after_swap = swap(label+'-after') + if result.returncode: raise ValueError('Command failed: '+label) + rows = json.loads(log.read_text()); check_samples(rows) + receipt['runs'].append({'workload':workload, 'name':name, 'log':log.name, 'samples':rows, + 'swap_before':before_swap, 'swap_after':after_swap, 'post_admitted':post_ok}) + save() + if not post_ok: + diagnostics(label+'-failed-post') + raise Blocked('Post-run idle below 90%: '+label) + if before_swap != after_swap: raise Blocked('Paging during measurement: '+label) + comparison = [] + for workload in ['wide512','frozen-schema']: + for operation in NAMES: + rows = {name:[] for name in binaries}; counters = {name:set() for name in binaries} + for sample in receipt['runs']: + if sample['workload'] != workload: continue + row = next(r for r in sample['samples'] if r['name']==operation) + rows[sample['name']].extend(row['seconds']) + counters[sample['name']].add((row.get('work'),row.get('peak'),row.get('retained'),row['inputBytes'])) + if len(counters['baseline'])!=1 or len(counters['candidate'])!=1: + raise ValueError('Nondeterministic accounting: '+operation) + base_count = next(iter(counters['baseline'])); candidate_count = next(iter(counters['candidate'])) + if operation == 'composition-document': + if candidate_count != completion_counters(base_count): + raise ValueError('Unexpected composition receipt delta') + elif counters['baseline'] != counters['candidate']: + raise ValueError('Changed old-API work/retention/input identity: '+operation) + before, after = [statistics.median(rows[name]) for name in ['baseline','candidate']] + if any(len(v)!=18 for v in rows.values()): raise ValueError('Missing ABBA samples') + comparison.append({'workload':workload,'operation':operation,'baseline_median_seconds':before, + 'candidate_median_seconds':after,'change_percent':100*(after/before-1), + 'samples_per_build':18,'baseline_work_peak_input':list(base_count), 'candidate_work_peak_input':list(candidate_count), + 'accounting_disposition':'completion +50 work/+424 logical bytes' if operation=='composition-document' else 'unchanged'}) + receipt['comparison'] = comparison + receipt['status'] = 'review_required' if any(row['change_percent']>10 for row in comparison) else 'passed' + receipt['disposition'] = '10% review trigger unchanged; no automatic exception, speedup or RSS claim.' + except Blocked as error: + receipt.update(status='blocked', error=str(error)) + except (OSError,ValueError,subprocess.SubprocessError) as error: + receipt.update(status='failed', error=str(error)) + finally: + if cpu is not None: + try: cpu.close() + except OSError as error: receipt.update(status='failed', error=str(error)) + receipt['source_inputs_unchanged'] = source_identity() == inputs + if not receipt['source_inputs_unchanged']: receipt.update(status='failed',error='Source changed during measurement') + receipt['finished_utc'] = datetime.datetime.now(datetime.timezone.utc).isoformat(); save() + return {'passed':0,'blocked':2,'review_required':3}.get(receipt['status'],1) + +if __name__ == '__main__': raise SystemExit(main()) diff --git a/Scripts/qualify_composition_proof.py b/Scripts/qualify_composition_proof.py new file mode 100644 index 0000000..cba8fe0 --- /dev/null +++ b/Scripts/qualify_composition_proof.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: Apache-2.0 +"""Independent public SwiftPM tests, real mutant failures and ownership builds.""" +import argparse, datetime, hashlib, json, os, pathlib, subprocess, sys, tempfile +from testing_events import read_events + +ROOT = pathlib.Path(__file__).resolve().parents[1] +PACKAGE = 'Tools/CompositionQualification' +EXPECTED_TESTS = 29 +NEGATIVE = { + 'Copy': ["'copy' cannot be applied to noncopyable types"], 'Construct': ['inaccessible'], 'Escape': ['borrowed'], + 'ExplicitEscape': ['borrowed'], 'Return': ['Copyable'], 'Sendable': ['Sendable'], 'Async': ['async', 'synchronous'], + 'Reset': ["no member 'reset'"], 'Fork': ["no member 'fork'"], + 'Refund': ["no member 'release'"], 'PackageMeter': ['inaccessible'], + 'RawMeter': ['JSONWorkMeter', 'cannot find'], 'Substitute': ['immutable'], + 'Store': ['borrowed'] +} + +def main(): + p = argparse.ArgumentParser(description=__doc__) + p.add_argument('--output', required=True) + a = p.parse_args(); output = (ROOT / a.output).resolve(); output.mkdir(parents=True, exist_ok=True) + receipt = dict(status='running', commands=[], public_tests_per_configuration=EXPECTED_TESTS, + compiler_negative_cases=len(NEGATIVE), expected_failing_mutants=2) + def save(): (output/'receipt.json').write_text(json.dumps(receipt, indent=2)+'\n') + def run(name, command, env=None, expected_failure=False): + log = output/(name+'.log'); started = datetime.datetime.now(datetime.timezone.utc).isoformat() + with log.open('w') as stream: + result = subprocess.run(command, cwd=ROOT, env=env, stdout=stream, stderr=subprocess.STDOUT, timeout=1800) + record = dict(name=name, command=command, exit_code=result.returncode, + started_utc=started, log=log.name, log_sha256=hashlib.sha256(log.read_bytes()).hexdigest(), + expected_failure=expected_failure) + receipt['commands'].append(record); save() + print(name, 'exit='+str(result.returncode), flush=True) + return record, log.read_text() + save() + for config in ['debug', 'release']: + directory=output/config; directory.mkdir(exist_ok=True) + events=directory/'events.jsonl'; env=os.environ.copy(); env['JCOMP_TRACE_DIRECTORY']=str(directory) + command=['swift','test','--package-path',PACKAGE,'-c',config,'--filter','CompositionPublicTests','--event-stream-output-path',str(events)] + if sys.platform == 'linux': command.append('--disable-xctest') + record, log=run(config,command,env) + assert record['exit_code']==0,log[-6000:] + parsed=read_events(events.read_text()) + assert parsed['module_function_counts']=={'CompositionPublicTests':EXPECTED_TESTS} and parsed['parameter_case_invocations']==0 + record['swift_testing_events']=dict(parsed,path=events.relative_to(output).as_posix(),sha256=hashlib.sha256(events.read_bytes()).hexdigest());save() + assert (directory/'owned-events.tsv').read_text()=='owner\twork\tlogical_bytes\nqueue\t65\t96\nscratch\t33\t64\nresult\t49\t80\nreceipt\t50\t424\n' + # Mutants run the unchanged positive test as an actually failing test process. + for mutation in ['work','reservation']: + events=output/('mutant-'+mutation+'.events.jsonl') + env=os.environ.copy();env['JCOMP_TRACE_MUTATION']=mutation + command=['swift','test','--package-path',PACKAGE,'--skip-build','--filter','externalTraceMatchesIndependentLedger','--event-stream-output-path',str(events)] + if sys.platform == 'linux': command.append('--disable-xctest') + record,log=run('mutant-'+mutation,command,env,expected_failure=True) + assert record['exit_code']==1 and ('workTrace' if mutation=='work' else 'retentionTrace') in log + rows=[json.loads(line) for line in events.read_text().splitlines() if line] + issues=[r for r in rows if r['kind']=='event' and r['payload']['kind']=='issueRecorded'] + completed=[r for r in rows if r['kind']=='event' and r['payload']['kind']=='testEnded'] + assert len(issues)==1 and len(completed)==1 + record.update(expected_issue=mutation,raw_issue_events=1,completed_test_functions=1,event_stream=events.name,event_sha256=hashlib.sha256(events.read_bytes()).hexdigest());save() + # Each fixture is a separate unrelated SwiftPM package/build, using exactly + # normal public products. No package-name override, @testable, SPI or shim. + with tempfile.TemporaryDirectory(prefix='swift-json-composition-negative-') as temp: + for name,diagnostics in NEGATIVE.items(): + directory=pathlib.Path(temp)/name; sources=directory/'Sources/NegativeConsumer';sources.mkdir(parents=True) + (directory/'Package.swift').write_text('// swift-tools-version: 6.4\nimport PackageDescription\nlet package = Package(name: "IndependentNegative'+name+'", platforms: [.macOS("27.0")], dependencies: [.package(name: "Swift-JSON", path: '+json.dumps(str(ROOT))+')], targets: [.executableTarget(name: "NegativeConsumer", dependencies: [.product(name: "JSON", package: "Swift-JSON"), .product(name: "JSONSchema", package: "Swift-JSON")])], swiftLanguageModes: [.v6])\n') + fixture=ROOT/PACKAGE/'ProofNegative'/(name+'.swift.txt') + (sources/'main.swift').write_bytes(fixture.read_bytes()) + command=['swift','build','--build-system','native','--package-path',str(directory),'--scratch-path',str(output/'negative-build')] + record,log=run('negative-'+name,command,expected_failure=True) + assert record['exit_code']==1 and all(d in log for d in diagnostics), log[-6000:] + assert not any(d in log for d in ['no such module','Please submit a bug report','Stack dump','SIL verification failed']) + record.update(expected_diagnostics=diagnostics,fixture_sha256=hashlib.sha256(fixture.read_bytes()).hexdigest());save() + receipt['status']='passed'; save() + print('COMPOSITION_PROOF passed public_tests=29 configurations=2 mutants_rejected=2 compiler_negative=14') + return 0 + +if __name__=='__main__': raise SystemExit(main()) diff --git a/Scripts/test_composition_performance.py b/Scripts/test_composition_performance.py new file mode 100644 index 0000000..597e594 --- /dev/null +++ b/Scripts/test_composition_performance.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: Apache-2.0 +"""Behavior tests for native admission and the matched comparison's rejection paths.""" +import copy +import unittest +from unittest.mock import patch +from composition_host import DarwinCPU, admitted, interval +from qualify_composition_performance import NAMES, check_samples, completion_counters + + +class PerformanceHarnessTests(unittest.TestCase): + def test_interval_uses_deltas_not_boot_totals(self): + row = interval([900, 900, 9000, 0], [903, 907, 9090, 0]) + self.assertEqual(row, {'delta_ticks': [3, 7, 90, 0], 'idle_percent': 90}) + + def test_natural_counter_wrap_is_preserved(self): + self.assertEqual(interval([2**32-2, 0, 0, 0], [1, 0, 27, 0])['idle_percent'], 90) + + def test_malformed_or_frozen_counters_are_errors(self): + for before, after in [([0]*4, [0]*4), ([0]*3, [1]*4), ([0]*4, [True, 0, 1, 0]), + ([0]*4, [0, 0, 2**32, 0]), ([0]*4, [0, -1, 2, 0])]: + with self.assertRaises(ValueError): interval(before, after) + + def test_every_interval_must_meet_unchanged_threshold(self): + self.assertTrue(admitted({'interval_idle_percent': [90, 90, 90]})) + for values in [[100, 89.999, 100], [99, 99], [], [99]*4, [100, float('nan'), 100], + [100, float('inf'), 100], [100, 101, 100]]: + self.assertFalse(admitted({'interval_idle_percent': values})) + + def test_window_excludes_initial_snapshot_and_waits_three_intervals(self): + cpu = object.__new__(DarwinCPU) + snapshots = [{'cpu_ticks': [900+i, 500+i, 100+i*18, 0]} for i in range(4)] + with patch.object(cpu, 'snapshot', side_effect=snapshots) as read, patch('composition_host.time.sleep') as sleep: + window = cpu.window() + self.assertEqual(read.call_count, 4) + self.assertEqual(sleep.call_args_list, [unittest.mock.call(1)]*3) + self.assertEqual(window['interval_idle_percent'], [90]*3) + self.assertTrue(window['first_cumulative_snapshot_excluded']) + + def test_missing_duplicate_or_changed_workload_is_rejected(self): + rows = [dict(name=n, iterations=i, seconds=[0.01]*9) for n, i in NAMES.items()] + check_samples(rows) + for changed in [rows[:-1], rows+[rows[0]], [dict(r, iterations=1) for r in rows]]: + with self.assertRaises(ValueError): check_samples(changed) + + def test_nonfinite_or_incomplete_samples_cannot_pass(self): + rows = [dict(name=n, iterations=i, seconds=[0.01]*9) for n, i in NAMES.items()] + for value in [0, -1, float('nan'), float('inf')]: + changed = copy.deepcopy(rows); changed[0]['seconds'][0] = value + with self.assertRaises(ValueError): check_samples(changed) + rows[0]['seconds'].pop() + with self.assertRaises(ValueError): check_samples(rows) + + def test_completion_overlap_does_not_add_disjoint_stage_peaks(self): + self.assertEqual(completion_counters((10, 1000, 500, 40)), (60, 1000, 924, 40)) + self.assertEqual(completion_counters((10, 1000, 900, 40)), (60, 1324, 1324, 40)) + + +if __name__ == '__main__': + result = unittest.TextTestRunner(verbosity=2).run(unittest.defaultTestLoader.loadTestsFromTestCase(PerformanceHarnessTests)) + complete = result.wasSuccessful() and result.testsRun == 8 + print('PERFORMANCE_HARNESS '+('passed' if complete else 'failed')+' cases='+str(result.testsRun)) + raise SystemExit(0 if complete else 1) diff --git a/Scripts/test_qualification_harness.py b/Scripts/test_qualification_harness.py index 843ec53..506c3d3 100644 --- a/Scripts/test_qualification_harness.py +++ b/Scripts/test_qualification_harness.py @@ -76,12 +76,26 @@ def test_composition_discovery_and_capacity_limits_are_not_blanket_successes(sel self.assertEqual(policy[key]['JSONNavigationTests'], 47) self.assertEqual(policy[key]['JSONSchemaExamplesTests'], 98) self.assertEqual(policy[key]['JSONInspectionTests'], 76) - self.assertEqual(policy[key]['JSONIntegrationTests'], 32) + # Preserve NAV8 plus PR36's two native boundary functions. + self.assertEqual(policy[key]['JSONIntegrationTests'], 34) for key in ('shared_state_functions', 'portable_shared_state_functions'): self.assertEqual(policy[key]['JSONNavigationTests'], 1) self.assertEqual(policy[key]['JSONSchemaExamplesTests'], 4) self.assertEqual(policy[key]['JSONInspectionTests'], 4) self.assertEqual(policy[key]['JSONIntegrationTests'], 11) + output = pathlib.Path(__file__).resolve().parents[1] / 'Artifacts/HarnessProbe' + proof = commands('composition-proof', output) + self.assertEqual(len(proof), 3) + self.assertEqual(proof[0][2:4], ['--filter', 'jcompOwned|jcompOriginal']) + self.assertIn('Scripts/qualify_composition_proof.py', proof[2]) + for count in (1, 3): + self.assertFalse(check_result(proof[0], 0, 'Test run with '+str(count)+' tests in 0 suites passed')[0]) + schema = commands('schema-examples-qualification', output) + self.assertEqual(len(schema), 10) + self.assertEqual({c[2] for c in schema if c[:2] == ['python3', '-B']}, { + 'Scripts/public_check.py', 'Scripts/qualify_schema_examples.py', + 'Scripts/qualify_schema_example_ownership.py', 'Scripts/qualify_schema_example_generation.py', + 'Scripts/qualify_schema_example_advanced.py', 'Scripts/qualify_schema_example_matrix.py'}) def test_private_projection_identities_cannot_be_missing_renamed_or_duplicated(self): names = sorted(PROJECTION_FUNCTIONS) @@ -189,6 +203,12 @@ def test_exact_discovery_rejects_missing_duplicate_and_changed_parameters(self): self.assertTrue(check_result(harness, 0, 'HARNESS_TESTS passed cases=17')[0]) for stale in ['HARNESS_TESTS passed cases=10', 'HARNESS_TESTS passed cases=11', 'HARNESS_TESTS passed cases=12', 'HARNESS_TESTS passed cases=13', 'HARNESS_TESTS passed cases=14', 'HARNESS_TESTS passed cases=15', 'HARNESS_TESTS passed cases=16', '']: self.assertFalse(check_result(harness, 0, stale)[0]) + proof = ['python3', '-B', 'Scripts/qualify_composition_proof.py'] + summary = 'COMPOSITION_PROOF passed public_tests=29 configurations=2 mutants_rejected=2 compiler_negative=14' + self.assertTrue(check_result(proof, 0, summary)[0]) + for broken in ['', summary.replace('29', '28'), summary.replace('rejected=2', 'rejected=0'), summary.replace('negative=14', 'negative=13')]: + self.assertFalse(check_result(proof, 0, broken)[0]) + self.assertFalse(check_result(proof, 1, summary)[0]) lifetime = ['python3', '-B', 'Scripts/qualify_schema_example_lifetime.py'] marker = 'SCHEMA_EXAMPLES_CORPUS passed mode=owned scopes=128 values=6 exact=1' self.assertTrue(check_result(lifetime, 0, marker)[0]) diff --git a/Sources/JSON/Documentation.docc/Composition.md b/Sources/JSON/Documentation.docc/Composition.md index d71a518..e19443e 100644 --- a/Sources/JSON/Documentation.docc/Composition.md +++ b/Sources/JSON/Documentation.docc/Composition.md @@ -1,7 +1,7 @@ # Compose native operations Run synchronous native operations and explicitly instrumented adapter work with -one finite ledger. The experimental composition facade does not meter arbitrary +one finite ledger. The composition facade does not meter arbitrary callback instructions, allocator sizes, physical RSS, or Foundation instructions. ```swift @@ -9,7 +9,7 @@ import Foundation import JSON import JSONSchema -let report = try JSON.withComposition { scope in +let report = try JSON.withComposition { (scope: borrowing JSONCompositionScope) in let document = try scope.parseDocument(Data(#"{"count":9007199254740993}"#.utf8)) try scope.reserveExternalLogicalBytes(64) // Adapter-declared output frame weight. try scope.chargeExternalWork(1) // Before the adapter's corresponding work. @@ -34,11 +34,21 @@ claim about opaque storage. External loops must charge/checkpoint regularly. charges each copied byte and runs the native source-map parser on the same meter. The original caller Data is excluded; the array copy remains conservatively reserved alongside decoded values, source bytes, spans and final output. -Version 1 native weights and internal scratch releases follow the resource +Each document applies `inputBytes` independently. Completed input counts remain +in the cumulative report; work, logical retention, accepted-capacity history and +terminal health never restart. Other native capacities remain conservative across +the scope. Version 1 native weights and internal scratch releases follow the resource contract. Reported peak is this ledger's conservative logical peak, not a sum of independent operation peaks. Final retained bytes include all unreleased reservations, even when the callback returns a smaller result. +Completion reserves 424 logical bytes (32 header bytes plus 49 UInt64 slots) +before allocating the independent report counter buffer. It charges 49 counter +copies plus one final aggregate-work update: 50 work units. The returned result +and all conservative reservations remain live through this final admission. +An adapter must also reserve its result containers before growth; the generic +result type does not reveal arbitrary storage to the library. + All owned helper errors are terminal, including syntax and missing-target errors. Catching one cannot produce a successful report. Scope exit rethrows the original owned failure ahead of any later callback error. Restriction intersects limits @@ -77,3 +87,32 @@ is not a normal rejection, a supported escape or a universal compiler-enforced non-escape proof. Do not infer safety from typecheck success alone. The existing scope, terminal-failure and accounting contracts remain unchanged; runtime invalidation alternatives and later accounting/root proposals need separate review. + + +## Compiler and portability boundary + +Pass the borrowed scope explicitly to adapter helper functions: + +```swift +func visit(_ scope: borrowing JSONCompositionScope) throws { + try scope.chargeExternalWork(1) +} +``` + +Both Apple Swift 6.4.0.34.1 and Swift.org 6.4 reproduce an ownership-verifier +crash when a nested helper captures an inferred closure parameter. Explicitly +annotate `(scope: borrowing JSONCompositionScope)` as above: the nested synchronous +helper compiles, shares the original ledger, and escaping capture receives the +normal borrowed-parameter diagnostic. Ordinary calls also work through explicit +borrowed helper parameters. + +Forming a bound method reference such as `scope.checkpoint` still crashes both +observed compilers, including through an explicit borrowed helper. Call the method +or a helper directly. Reduced reproducers with no JSON imports are retained in the +qualification package. These are compiler defects, not intended negative-test +passes; no experimental flags or weaker ownership model are used. + +Separate expected-failing SwiftPM builds qualify copying, explicit capture escape, +async suspension, Sendable, construction, replacement and ledger-access attempts. +See the qualification package for the exact executed scope. Cross-compilation is +evidence for the observed SDK only and does not establish Linux runtime safety. diff --git a/Sources/JSON/Limits/JSONCompositionScope.swift b/Sources/JSON/Limits/JSONCompositionScope.swift index 3ebf9b3..0e0eeee 100644 --- a/Sources/JSON/Limits/JSONCompositionScope.swift +++ b/Sources/JSON/Limits/JSONCompositionScope.swift @@ -44,6 +44,7 @@ public struct JSONCompositionScope: ~Copyable { /// - Throws: Original native failure or cancellation; caught failures stay terminal. public func parseDocument(_ data: Data) throws -> JSONDocument { try withMeter(domain: .parsing) { meter in + try meter.beginCompositionDocument() try meter.require(.inputBytes, UInt64(data.count)) try meter.retain(UInt64(data.count)) var bytes: [UInt8] = [] @@ -124,6 +125,15 @@ public struct JSONCompositionScope: ~Copyable { try withMeter { try $0.work(units) } } + /// Reserves checked adapter bytes before growth, retaining the original API shape. + /// - Parameters: + /// - bytes: Logical weight per unit. + /// - count: Number of units, multiplied with checked arithmetic. + /// - Throws: Terminal quota/overflow failure or cancellation. + public func reserveExternalLogicalBytes(_ bytes: UInt64, count: UInt64 = 1) throws { + try reserveExternalLogicalBytes(bytes, count: count, overhead: 0) + } + /// Reserves adapter-owned logical bytes until scope completion, without refund. /// /// Call before growth. Separate calls count separately even for physical aliases. @@ -132,16 +142,18 @@ public struct JSONCompositionScope: ~Copyable { /// - Parameters: /// - bytes: Logical weight per unit. /// - count: Number of units, multiplied with checked UInt64 arithmetic. + /// - overhead: Additional logical header or bookkeeping bytes, added before growth. /// - Throws: Quota/overflow failure or cancellation, permanently terminal. - public func reserveExternalLogicalBytes(_ bytes: UInt64, count: UInt64 = 1) throws { + public func reserveExternalLogicalBytes(_ bytes: UInt64, count: UInt64 = 1, overhead: UInt64) throws { try withMeter { meter in - let (total, overflow) = bytes.multipliedReportingOverflow(by: count) - guard !overflow else { + let (total, productOverflow) = bytes.multipliedReportingOverflow(by: count) + let (complete, sumOverflow) = total.addingReportingOverflow(overhead) + guard !productOverflow && !sumOverflow else { throw JSONFailure(.resource, code: .counterOverflow, resource: .init( domain: .core, metric: .retainedBytes, limit: meter.limits[.retainedBytes], attemptedUsage: nil, overflow: true)) } - try meter.retain(total) + try meter.retain(complete) } } @@ -163,7 +175,9 @@ public struct JSONCompositionScope: ~Copyable { do { let result = try body(scope) try original.check(scope.state.context.meter) - return .init(result: result, statistics: scope.state.context.meter.statistics()) + let statistics = try scope.withMeter { try $0.compositionStatistics() } + try original.check(scope.state.context.meter) + return .init(result: result, statistics: statistics) } catch { // A caller error cannot conceal an earlier native terminal failure or substitution. try original.check(scope.state.context.meter) diff --git a/Sources/JSON/Limits/JSONWorkMeter.swift b/Sources/JSON/Limits/JSONWorkMeter.swift index 0f5a1f2..73b19cd 100644 --- a/Sources/JSON/Limits/JSONWorkMeter.swift +++ b/Sources/JSON/Limits/JSONWorkMeter.swift @@ -206,6 +206,35 @@ package struct JSONWorkMeter: ~Copyable { guard bytes <= used(.annotationBytes) else { throw JSONFailure(.configuration, code: .invalidLimits) } counters[JSONResourceMetric.annotationBytes.index] -= bytes } + /// Input is a per-document capacity. Retire its charged count into the same + /// cumulative report; never reset work, retention, accepted history or health. + package mutating func beginCompositionDocument() throws { + try checkpoint() + try retire(.inputBytes) + } + /// Composition-only completion owns a separate 49-slot report through handoff. + /// Existing report entry points retain their original accounting semantics. + package mutating func compositionStatistics() throws -> JSONOperationStatistics { + // Version 1 composition receipt: 32 logical header bytes + 49 UInt64 slots. + try retain(424) + var report: [UInt64] = [] + report.reserveCapacity(JSONResourceMetric.allCases.count) + for metric in JSONResourceMetric.allCases { + try work() + let (total, overflow) = counters[metric.index].addingReportingOverflow(retired[metric.index]) + guard !overflow else { throw violation(metric, nil, overflow: true) } + report.append(total) + } + // The aggregate-work slot may precede the last copied slot. Charge and + // publish the final count only after every copied counter is accounted. + try work() + let (work, overflow) = counters[JSONResourceMetric.workUnits.index] + .addingReportingOverflow(retired[JSONResourceMetric.workUnits.index]) + guard !overflow else { throw violation(.workUnits, nil, overflow: true) } + report[JSONResourceMetric.workUnits.index] = work + try checkpoint() + return .init(domain: domain, counters: report, peak: peak) + } package func statistics() -> JSONOperationStatistics { var report = counters for metric in JSONResourceMetric.allCases { report[metric.index] += retired[metric.index] } diff --git a/Sources/JSONCLIEngine/CLIBuildIdentity.swift b/Sources/JSONCLIEngine/CLIBuildIdentity.swift index 0a2b127..efbbe7b 100644 --- a/Sources/JSONCLIEngine/CLIBuildIdentity.swift +++ b/Sources/JSONCLIEngine/CLIBuildIdentity.swift @@ -2,6 +2,6 @@ // Generated by Scripts/generate_cli_identity.py; excludes this file. // SHA-256 of the ordered Package.swift + Sources content manifest, not a Git commit. package enum CLIBuildIdentity { - package static let revision = "sha256:b7cfb84e34289d4bb2342e2897c637cdf4fd19f2455072696d27f396bf8ef735" + package static let revision = "sha256:3e253a273ce539fcb2a4d8a1870859370d2a845eefb52248c261831eb92660b6" package static let kind = "source-sha256" } diff --git a/Tests/JSONIntegrationTests/CompositionContractTests.swift b/Tests/JSONIntegrationTests/CompositionContractTests.swift index c650339..57c04a5 100644 --- a/Tests/JSONIntegrationTests/CompositionContractTests.swift +++ b/Tests/JSONIntegrationTests/CompositionContractTests.swift @@ -91,11 +91,11 @@ private func composedImport(limits: JSONLimits = .balanced, extra: UInt64 = 0) t try scope.reserveExternalLogicalBytes(64) return 0 } - #expect(two.statistics.peakLogicalRetainedBytes == 128) - #expect(two.statistics[.retainedBytes] == 128) - #expect(one.statistics.peakLogicalRetainedBytes == 64) + #expect(two.statistics.peakLogicalRetainedBytes == 128 + 424) + #expect(two.statistics[.retainedBytes] == 128 + 424) + #expect(one.statistics.peakLogicalRetainedBytes == 64 + 424) var belowOverlap = JSONLimits.balanced - belowOverlap[.retainedBytes] = 127 + belowOverlap[.retainedBytes] = 127 + 424 #expect(throws: JSONFailure.self) { try JSON.withComposition(limits: belowOverlap) { scope in try scope.reserveExternalLogicalBytes(64) @@ -103,7 +103,7 @@ private func composedImport(limits: JSONLimits = .balanced, extra: UInt64 = 0) t return 0 } } - #expect(try JSON.withComposition { try $0.admitTrusted(.null) }.statistics[.retainedBytes] == 32) + #expect(try JSON.withComposition { try $0.admitTrusted(.null) }.statistics[.retainedBytes] == 32 + 424) // Independent stage peaks are not a composition oracle: the parser's scratch // peak and a later URI peak need not be simultaneously live. let parse = try JSON.withComposition { try $0.parseDocument(compositionInput) } @@ -178,7 +178,7 @@ private func composedImport(limits: JSONLimits = .balanced, extra: UInt64 = 0) t _ = try scope.admitTrusted(alias) return alias } - #expect(two.statistics[.retainedBytes] == one.statistics[.retainedBytes] * 2) + #expect(two.statistics[.retainedBytes] == (one.statistics[.retainedBytes] - 424) * 2 + 424) var limits = JSONLimits.balanced limits[.compiledPlanBytes] = 0 limits[.compileWorkUnits] = 0 @@ -303,10 +303,10 @@ private func composedImport(limits: JSONLimits = .balanced, extra: UInt64 = 0) t try inner.chargeExternalWork(5) return 0 } - #expect(inner.statistics[.workUnits] == 5) + #expect(inner.statistics[.workUnits] == 5 + 50) return 0 } - #expect(report.statistics[.workUnits] == 3) + #expect(report.statistics[.workUnits] == 3 + 50) // Substitution of the borrowed facade is rejected by the external compiler // fixture Substitute.swift.txt, including swap/use/restore. Independent // nested invocations are explicitly outside the outer report's claim. diff --git a/Tests/JSONIntegrationTests/CompositionOwnedProofTests.swift b/Tests/JSONIntegrationTests/CompositionOwnedProofTests.swift new file mode 100644 index 0000000..176f348 --- /dev/null +++ b/Tests/JSONIntegrationTests/CompositionOwnedProofTests.swift @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: Apache-2.0 +import Foundation +import Synchronization +import Testing +import JSON +import JSONSchema + +// Native-only rendezvous is supplementary to the unrelated public package. It +// uses the existing invocation-owned observer; it is not an accounting oracle. +@Test func jcompOwnedCancellationOccursInsideNativeWork() async throws { + for stage in 0..<7 { + let seen = Mutex(nil) + let task = Task.detached { + var limits = JSONLimits.offline; limits[.cancellationQuantum] = 1 + let threshold: UInt64 = stage == 1 ? 4200 : stage == 4 ? 6 : 32 + return try JSON.withComposition(limits: limits) { scope in + try scope.withMeter { meter in + meter.checkpointObserver = { work in + if work >= threshold { + seen.withLock { if $0 == nil { $0 = work } } + withUnsafeCurrentTask { $0?.cancel() } + } + } + } + do { + switch stage { + case 0, 1: + _ = try scope.parseDocument(Data(("\"" + String(repeating: "a", count: 4096) + "\"").utf8)) + case 2: _ = try scope.pointer(parsing: "/" + String(repeating: "a", count: 4096)) + case 3: _ = try scope.uri("https://example.invalid/" + String(repeating: "a", count: 4096)) + case 4: try scope.chargeExternalWork(5) // Cancellation must occur in report copying. + case 5: + for _ in 0..<128 { try scope.chargeExternalWork(1) } + default: + let path = try scope.pointer(tokens: []) + _ = try scope.resolve(path, in: .array(Array(repeating: .null, count: 256))) + } + } catch is CancellationError { } + return 1 + } + } + do { _ = try await task.value; Issue.record("caught cancellation completed at stage \(stage)") } + catch is CancellationError { } + #expect(seen.withLock { $0 != nil }) + if stage == 1 { #expect(seen.withLock { ($0 ?? 0) > 4098 }) } + } +} + +@Test func jcompOriginalHealthAndIdentitySurvivePackageSubstitution() throws { + for stage in 0..<3 { + do { + _ = try JSON.withComposition { scope in + try scope.withMeter { meter in + if stage == 1 { + do { try meter.work(UInt64.max) } catch { } + } else if stage == 2 { + do { try meter.retain(UInt64.max) } catch { } + } + // Impossible through public API; exercise the original + // continuation's identity and first-cause check directly. + meter = try JSONWorkMeter(limits: .balanced, domain: .core) + } + return 1 + } + Issue.record("substituted native ledger completed") + } catch let failure as JSONFailure { + if stage == 0 { #expect(failure.category == .configuration && failure.code == .invalidOperation) } + if stage == 1 { #expect(failure.resource?.metric == .workUnits) } + if stage == 2 { #expect(failure.resource?.metric == .retainedBytes) } + } + } +} diff --git a/Tools/CompositionQualification/CompilerBugs/BoundDirect.swift.txt b/Tools/CompositionQualification/CompilerBugs/BoundDirect.swift.txt new file mode 100644 index 0000000..ca8c4f3 --- /dev/null +++ b/Tools/CompositionQualification/CompilerBugs/BoundDirect.swift.txt @@ -0,0 +1,4 @@ +// SPDX-License-Identifier: Apache-2.0 +import JSON +let report = try JSON.withComposition { scope in return scope.checkpoint } +try report.result() diff --git a/Tools/CompositionQualification/CompilerBugs/BoundHelper.swift.txt b/Tools/CompositionQualification/CompilerBugs/BoundHelper.swift.txt new file mode 100644 index 0000000..182a48d --- /dev/null +++ b/Tools/CompositionQualification/CompilerBugs/BoundHelper.swift.txt @@ -0,0 +1,3 @@ +// SPDX-License-Identifier: Apache-2.0 +import JSON +func rejected(_ scope: borrowing JSONCompositionScope) -> () throws -> Void { scope.checkpoint } diff --git a/Tools/CompositionQualification/CompilerBugs/DirectEscape.swift.txt b/Tools/CompositionQualification/CompilerBugs/DirectEscape.swift.txt new file mode 100644 index 0000000..d51543f --- /dev/null +++ b/Tools/CompositionQualification/CompilerBugs/DirectEscape.swift.txt @@ -0,0 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 +import JSON +import JSONSchema +var escaped: (() throws -> Void)? +_ = try JSON.withComposition { scope in escaped = { try scope.checkpoint() } } diff --git a/Tools/CompositionQualification/CompilerBugs/NestedHelper.swift.txt b/Tools/CompositionQualification/CompilerBugs/NestedHelper.swift.txt new file mode 100644 index 0000000..13786e4 --- /dev/null +++ b/Tools/CompositionQualification/CompilerBugs/NestedHelper.swift.txt @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: Apache-2.0 +import JSON +func reproducer() throws { + _ = try JSON.withComposition { scope in + func local() throws { try scope.chargeExternalWork(1) } + try local() + } +} diff --git a/Tools/CompositionQualification/CompilerBugs/README.md b/Tools/CompositionQualification/CompilerBugs/README.md new file mode 100644 index 0000000..47ce497 --- /dev/null +++ b/Tools/CompositionQualification/CompilerBugs/README.md @@ -0,0 +1,42 @@ +# Swift 6.4 ownership compiler investigation + +Original public-product reproducers are in this directory. `Reduced` contains +original Apache-2.0 reductions with no imports, private native meter or package +code. The class holds one mutable integer, the facade is noncopyable and the +synchronous entry borrows it. The defect persists without Foundation or JSON. + +Replay with a selected installed compiler and the supported macOS SDK: + +```sh +python3 -B Scripts/probe_composition_compiler.py --compiler /path/to/swiftc --output .build/composition-compiler +``` + +This investigation deliberately returns **2 when a compiler crash is observed**. +It is separate from the composition lane's intended diagnostic builds. Never +interpret a signal, stack dump or verifier failure as a passing negative test. +The receipt preserves the command, source/log hashes, version, exit and outcome. + +Observed on Apple Swift 6.4.0.34.1 and Swift.org `swift-6.4-RELEASE`: + +| Form | Outcome | +| --- | --- | +| Inferred borrowed closure parameter captured by nested synchronous helper | SIL ownership verifier crash | +| Explicit borrowing closure parameter, nested synchronous helper | Compiles | +| Inferred parameter captured by escaping closure | SIL ownership verifier crash | +| Explicit borrowing closure parameter, escaping capture | Intended borrowed-parameter diagnostic | +| Direct method reference, inferred or explicit borrowing parameter | SIL ownership verifier crash | +| Method reference from explicit borrowed helper | SIL ownership verifier crash | +| Explicit helper parameter with direct method call | Compiles | +| Escaping closure returned from explicit borrowed helper | Intended borrowed-parameter diagnostic | + +The normal diagnostic is `'scope' cannot be captured by an escaping closure since +it is a borrowed parameter`. Crash diagnostics include `Have operand with +incompatible ownership?!` and `copy_addr`. These are observations on two specific +installed compilers; no claim is made about unexecuted newer releases. + +Use an explicit borrowing closure parameter and direct calls, or pass the scope +as an explicit borrowing parameter to ordinary helpers. The independent consumer +executes the nested form, checks exact aggregate work/retention, and fails one unit +below completion. No lifetime experiment flag, copyable facade, unchecked +Sendable or arbitrary callback instruction claim is needed. The method-value forms +remain unqualified. No compiler issue has been sent on the maintainer's behalf. diff --git a/Tools/CompositionQualification/CompilerBugs/Reduced/bound-direct.swift.txt b/Tools/CompositionQualification/CompilerBugs/Reduced/bound-direct.swift.txt new file mode 100644 index 0000000..a94d039 --- /dev/null +++ b/Tools/CompositionQualification/CompilerBugs/Reduced/bound-direct.swift.txt @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: Apache-2.0 +private final class Ledger { var units = 0 } +struct Scope: ~Copyable { + private let ledger = Ledger() + func checkpoint() throws { ledger.units += 1 } +} +func compose(_ body: (borrowing Scope) throws -> R) rethrows -> R { + let scope = Scope() + return try body(scope) +} +func test() throws { let result = try compose { scope in return scope.checkpoint }; try result() } diff --git a/Tools/CompositionQualification/CompilerBugs/Reduced/bound-explicit.swift.txt b/Tools/CompositionQualification/CompilerBugs/Reduced/bound-explicit.swift.txt new file mode 100644 index 0000000..63a5e52 --- /dev/null +++ b/Tools/CompositionQualification/CompilerBugs/Reduced/bound-explicit.swift.txt @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: Apache-2.0 +private final class Ledger { var units = 0 } +struct Scope: ~Copyable { + private let ledger = Ledger() + func checkpoint() throws { ledger.units += 1 } +} +func compose(_ body: (borrowing Scope) throws -> R) rethrows -> R { + let scope = Scope() + return try body(scope) +} +func test() throws { let result = try compose { (scope: borrowing Scope) in return scope.checkpoint }; try result() } diff --git a/Tools/CompositionQualification/CompilerBugs/Reduced/bound-helper.swift.txt b/Tools/CompositionQualification/CompilerBugs/Reduced/bound-helper.swift.txt new file mode 100644 index 0000000..aed3927 --- /dev/null +++ b/Tools/CompositionQualification/CompilerBugs/Reduced/bound-helper.swift.txt @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: Apache-2.0 +private final class Ledger { var units = 0 } +struct Scope: ~Copyable { + private let ledger = Ledger() + func checkpoint() throws { ledger.units += 1 } +} +func compose(_ body: (borrowing Scope) throws -> R) rethrows -> R { + let scope = Scope() + return try body(scope) +} +func test(_ scope: borrowing Scope) -> () throws -> Void { scope.checkpoint } diff --git a/Tools/CompositionQualification/CompilerBugs/Reduced/escape-explicit.swift.txt b/Tools/CompositionQualification/CompilerBugs/Reduced/escape-explicit.swift.txt new file mode 100644 index 0000000..9ba93fc --- /dev/null +++ b/Tools/CompositionQualification/CompilerBugs/Reduced/escape-explicit.swift.txt @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: Apache-2.0 +private final class Ledger { var units = 0 } +struct Scope: ~Copyable { + private let ledger = Ledger() + func checkpoint() throws { ledger.units += 1 } +} +func compose(_ body: (borrowing Scope) throws -> R) rethrows -> R { + let scope = Scope() + return try body(scope) +} +func test() throws { var escaped: (() throws -> Void)?; _ = try compose { (scope: borrowing Scope) in escaped = { try scope.checkpoint() } }; try escaped?() } diff --git a/Tools/CompositionQualification/CompilerBugs/Reduced/escape-inferred.swift.txt b/Tools/CompositionQualification/CompilerBugs/Reduced/escape-inferred.swift.txt new file mode 100644 index 0000000..0b8bb39 --- /dev/null +++ b/Tools/CompositionQualification/CompilerBugs/Reduced/escape-inferred.swift.txt @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: Apache-2.0 +private final class Ledger { var units = 0 } +struct Scope: ~Copyable { + private let ledger = Ledger() + func checkpoint() throws { ledger.units += 1 } +} +func compose(_ body: (borrowing Scope) throws -> R) rethrows -> R { + let scope = Scope() + return try body(scope) +} +func test() throws { var escaped: (() throws -> Void)?; _ = try compose { scope in escaped = { try scope.checkpoint() } }; try escaped?() } diff --git a/Tools/CompositionQualification/CompilerBugs/Reduced/explicit-helper-negative.swift.txt b/Tools/CompositionQualification/CompilerBugs/Reduced/explicit-helper-negative.swift.txt new file mode 100644 index 0000000..37dce69 --- /dev/null +++ b/Tools/CompositionQualification/CompilerBugs/Reduced/explicit-helper-negative.swift.txt @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: Apache-2.0 +private final class Ledger { var units = 0 } +struct Scope: ~Copyable { + private let ledger = Ledger() + func checkpoint() throws { ledger.units += 1 } +} +func compose(_ body: (borrowing Scope) throws -> R) rethrows -> R { + let scope = Scope() + return try body(scope) +} +func test(_ scope: borrowing Scope) -> () throws -> Void { { try scope.checkpoint() } } diff --git a/Tools/CompositionQualification/CompilerBugs/Reduced/explicit-helper-positive.swift.txt b/Tools/CompositionQualification/CompilerBugs/Reduced/explicit-helper-positive.swift.txt new file mode 100644 index 0000000..279f6c5 --- /dev/null +++ b/Tools/CompositionQualification/CompilerBugs/Reduced/explicit-helper-positive.swift.txt @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: Apache-2.0 +private final class Ledger { var units = 0 } +struct Scope: ~Copyable { + private let ledger = Ledger() + func checkpoint() throws { ledger.units += 1 } +} +func compose(_ body: (borrowing Scope) throws -> R) rethrows -> R { + let scope = Scope() + return try body(scope) +} +func visit(_ scope: borrowing Scope) throws { try scope.checkpoint() }; func test() throws { try compose { scope in try visit(scope) } } diff --git a/Tools/CompositionQualification/CompilerBugs/Reduced/nested-explicit.swift.txt b/Tools/CompositionQualification/CompilerBugs/Reduced/nested-explicit.swift.txt new file mode 100644 index 0000000..5bfee16 --- /dev/null +++ b/Tools/CompositionQualification/CompilerBugs/Reduced/nested-explicit.swift.txt @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: Apache-2.0 +private final class Ledger { var units = 0 } +struct Scope: ~Copyable { + private let ledger = Ledger() + func checkpoint() throws { ledger.units += 1 } +} +func compose(_ body: (borrowing Scope) throws -> R) rethrows -> R { + let scope = Scope() + return try body(scope) +} +func test() throws { _ = try compose { (scope: borrowing Scope) in + func local() throws { try scope.checkpoint() }; try local() +} } diff --git a/Tools/CompositionQualification/CompilerBugs/Reduced/nested-inferred.swift.txt b/Tools/CompositionQualification/CompilerBugs/Reduced/nested-inferred.swift.txt new file mode 100644 index 0000000..af781fa --- /dev/null +++ b/Tools/CompositionQualification/CompilerBugs/Reduced/nested-inferred.swift.txt @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: Apache-2.0 +private final class Ledger { var units = 0 } +struct Scope: ~Copyable { + private let ledger = Ledger() + func checkpoint() throws { ledger.units += 1 } +} +func compose(_ body: (borrowing Scope) throws -> R) rethrows -> R { + let scope = Scope() + return try body(scope) +} +func test() throws { _ = try compose { scope in + func local() throws { try scope.checkpoint() }; try local() +} } diff --git a/Tools/CompositionQualification/Package.swift b/Tools/CompositionQualification/Package.swift index 47d1933..6c0df50 100644 --- a/Tools/CompositionQualification/Package.swift +++ b/Tools/CompositionQualification/Package.swift @@ -3,9 +3,15 @@ import PackageDescription let package = Package( name: "CompositionQualification", - platforms: [.macOS("27.0")], + platforms: [.macOS("27.0"), .iOS("27.0")], dependencies: [.package(name: "Swift-JSON", path: "../..")], - targets: [.target(name: "CompositionFixtures", dependencies: [ + targets: [.executableTarget(name: "CompositionBenchmark", dependencies: [ + .product(name: "JSON", package: "Swift-JSON"), .product(name: "JSONSchema", package: "Swift-JSON") + ]), .target(name: "CompositionProbe", dependencies: [ + .product(name: "JSON", package: "Swift-JSON"), .product(name: "JSONSchema", package: "Swift-JSON") + ]), .testTarget(name: "CompositionPublicTests", dependencies: [ + .product(name: "JSON", package: "Swift-JSON"), .product(name: "JSONSchema", package: "Swift-JSON") + ], resources: [.copy("Fixtures")]), .target(name: "CompositionFixtures", dependencies: [ .product(name: "JSON", package: "Swift-JSON"), .product(name: "JSONSchema", package: "Swift-JSON") ]), .testTarget(name: "CompositionScaleTests", dependencies: [ diff --git a/Tools/CompositionQualification/ProofNegative/Async.swift.txt b/Tools/CompositionQualification/ProofNegative/Async.swift.txt new file mode 100644 index 0000000..10a764e --- /dev/null +++ b/Tools/CompositionQualification/ProofNegative/Async.swift.txt @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +import JSON +import JSONSchema +func illegal() async throws { + _ = try await JSON.withComposition { scope in await Task.yield(); try scope.checkpoint() } +} diff --git a/Tools/CompositionQualification/ProofNegative/Construct.swift.txt b/Tools/CompositionQualification/ProofNegative/Construct.swift.txt new file mode 100644 index 0000000..36ec7cf --- /dev/null +++ b/Tools/CompositionQualification/ProofNegative/Construct.swift.txt @@ -0,0 +1,4 @@ +// SPDX-License-Identifier: Apache-2.0 +import JSON +import JSONSchema +let scope = try JSONCompositionScope(limits: .balanced) diff --git a/Tools/CompositionQualification/ProofNegative/Copy.swift.txt b/Tools/CompositionQualification/ProofNegative/Copy.swift.txt new file mode 100644 index 0000000..45f818f --- /dev/null +++ b/Tools/CompositionQualification/ProofNegative/Copy.swift.txt @@ -0,0 +1,4 @@ +// SPDX-License-Identifier: Apache-2.0 +import JSON +import JSONSchema +_ = try JSON.withComposition { scope in let duplicate = copy scope; try duplicate.checkpoint() } diff --git a/Tools/CompositionQualification/ProofNegative/Escape.swift.txt b/Tools/CompositionQualification/ProofNegative/Escape.swift.txt new file mode 100644 index 0000000..9e5c604 --- /dev/null +++ b/Tools/CompositionQualification/ProofNegative/Escape.swift.txt @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +import JSON +import JSONSchema +func rejected(_ scope: borrowing JSONCompositionScope) -> () throws -> Void { + { try scope.checkpoint() } +} diff --git a/Tools/CompositionQualification/ProofNegative/ExplicitEscape.swift.txt b/Tools/CompositionQualification/ProofNegative/ExplicitEscape.swift.txt new file mode 100644 index 0000000..08a5b76 --- /dev/null +++ b/Tools/CompositionQualification/ProofNegative/ExplicitEscape.swift.txt @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: Apache-2.0 +import JSON +func rejected() throws { + var escaped: (() throws -> Void)? + _ = try JSON.withComposition { (scope: borrowing JSONCompositionScope) in + escaped = { try scope.checkpoint() } + } + try escaped?() +} diff --git a/Tools/CompositionQualification/ProofNegative/Fork.swift.txt b/Tools/CompositionQualification/ProofNegative/Fork.swift.txt new file mode 100644 index 0000000..ecb8efe --- /dev/null +++ b/Tools/CompositionQualification/ProofNegative/Fork.swift.txt @@ -0,0 +1,4 @@ +// SPDX-License-Identifier: Apache-2.0 +import JSON +import JSONSchema +_ = try JSON.withComposition { scope in try scope.fork() } diff --git a/Tools/CompositionQualification/ProofNegative/PackageMeter.swift.txt b/Tools/CompositionQualification/ProofNegative/PackageMeter.swift.txt new file mode 100644 index 0000000..fa89bd9 --- /dev/null +++ b/Tools/CompositionQualification/ProofNegative/PackageMeter.swift.txt @@ -0,0 +1,4 @@ +// SPDX-License-Identifier: Apache-2.0 +import JSON +import JSONSchema +_ = try JSON.withComposition { scope in try scope.withMeter { _ in 0 } } diff --git a/Tools/CompositionQualification/ProofNegative/RawMeter.swift.txt b/Tools/CompositionQualification/ProofNegative/RawMeter.swift.txt new file mode 100644 index 0000000..75b2279 --- /dev/null +++ b/Tools/CompositionQualification/ProofNegative/RawMeter.swift.txt @@ -0,0 +1,4 @@ +// SPDX-License-Identifier: Apache-2.0 +import JSON +import JSONSchema +let meter = try JSONWorkMeter(limits: .balanced, domain: .core) diff --git a/Tools/CompositionQualification/ProofNegative/Refund.swift.txt b/Tools/CompositionQualification/ProofNegative/Refund.swift.txt new file mode 100644 index 0000000..b940bf7 --- /dev/null +++ b/Tools/CompositionQualification/ProofNegative/Refund.swift.txt @@ -0,0 +1,4 @@ +// SPDX-License-Identifier: Apache-2.0 +import JSON +import JSONSchema +_ = try JSON.withComposition { scope in try scope.release(1) } diff --git a/Tools/CompositionQualification/ProofNegative/Reset.swift.txt b/Tools/CompositionQualification/ProofNegative/Reset.swift.txt new file mode 100644 index 0000000..c50d1c7 --- /dev/null +++ b/Tools/CompositionQualification/ProofNegative/Reset.swift.txt @@ -0,0 +1,4 @@ +// SPDX-License-Identifier: Apache-2.0 +import JSON +import JSONSchema +_ = try JSON.withComposition { scope in try scope.reset() } diff --git a/Tools/CompositionQualification/ProofNegative/Return.swift.txt b/Tools/CompositionQualification/ProofNegative/Return.swift.txt new file mode 100644 index 0000000..2ebe84e --- /dev/null +++ b/Tools/CompositionQualification/ProofNegative/Return.swift.txt @@ -0,0 +1,4 @@ +// SPDX-License-Identifier: Apache-2.0 +import JSON +import JSONSchema +let scope = try JSON.withComposition { scope in return scope } diff --git a/Tools/CompositionQualification/ProofNegative/Sendable.swift.txt b/Tools/CompositionQualification/ProofNegative/Sendable.swift.txt new file mode 100644 index 0000000..be9a833 --- /dev/null +++ b/Tools/CompositionQualification/ProofNegative/Sendable.swift.txt @@ -0,0 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 +import JSON +import JSONSchema +func needsSendable(_ value: borrowing T) {} +_ = try JSON.withComposition { scope in needsSendable(scope) } diff --git a/Tools/CompositionQualification/ProofNegative/Store.swift.txt b/Tools/CompositionQualification/ProofNegative/Store.swift.txt new file mode 100644 index 0000000..37fd253 --- /dev/null +++ b/Tools/CompositionQualification/ProofNegative/Store.swift.txt @@ -0,0 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 +import JSON +import JSONSchema +var saved: JSONCompositionScope? +_ = try JSON.withComposition { scope in saved = consume scope } diff --git a/Tools/CompositionQualification/ProofNegative/Substitute.swift.txt b/Tools/CompositionQualification/ProofNegative/Substitute.swift.txt new file mode 100644 index 0000000..a679266 --- /dev/null +++ b/Tools/CompositionQualification/ProofNegative/Substitute.swift.txt @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +import JSON +import JSONSchema +_ = try JSON.withComposition { outer in + _ = try JSON.withComposition { inner in swap(&outer, &inner) } +} diff --git a/Tools/CompositionQualification/README.md b/Tools/CompositionQualification/README.md index f61927e..041ff77 100644 --- a/Tools/CompositionQualification/README.md +++ b/Tools/CompositionQualification/README.md @@ -46,3 +46,13 @@ and all 11 compiler rejection cases. Five cells complete and three deliberately those refusals remain operational errors. The initial direct 128/128 balanced capacity assumption failed and is preserved. Staging the reference array on the same scope qualifies that bounded target without changing core APIs or limits. See ../../Documentation/Architecture/CompositionQualification.md. + +## Reconciled independent accounting proof + +Run `python3 Scripts/ci.py --lane composition-proof` in addition to the reference-import +lane above. It runs 26 public functions in Debug/Release, two actual omitted-charge +mutants and 14 unrelated SwiftPM diagnostic builds. `Negative/` preserves the 11 +original compiler cases; `ProofNegative/` contains the independent 14-case suite. +Compiler crashes are retained separately under `CompilerBugs/` and never satisfy +an intended rejection. The package uses ordinary public JSON/JSONSchema products. +See [the reconciliation record](../../Documentation/Qualification/Composition/Reconciliation.md). diff --git a/Tools/CompositionQualification/Sources/CompositionBenchmark/main.swift b/Tools/CompositionQualification/Sources/CompositionBenchmark/main.swift new file mode 100644 index 0000000..99c5abe --- /dev/null +++ b/Tools/CompositionQualification/Sources/CompositionBenchmark/main.swift @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: Apache-2.0 +import Foundation +import JSON +import JSONSchema + +struct Sample: Codable { + let name: String + let inputBytes: Int + let iterations: Int + let seconds: [Double] + let work: UInt64? + let peak: UInt64? + let retained: UInt64? +} +// Deterministic 512-field exact/scalar workload, not an external conformance corpus. +let text = "{" + (0..<512).map { "\"field\($0)\":" + #"{"n":9007199254740993,"s":"é","a":[true,null,0.01]}"# }.joined(separator: ",") + "}" +let realPath = ProcessInfo.processInfo.environment["JCOMP_BENCHMARK_INPUT"] +let data = try realPath.map { try Data(contentsOf: URL(fileURLWithPath: $0)) } ?? Data(text.utf8) +let bytes = Array(data) +let expectedMembers = realPath == nil ? 512 : 2 +let root = try JSONParser().parse(bytes) +let schema = try JSONSchemaDocument(JSONParser().parse(#"{"type":"object","properties":{"n":{"type":"integer"},"s":{"type":"string"}},"required":["n"]}"#)) +let parserProbe = try JSONParser().parseWithReport(bytes) +let schemaProbe = try JSONSchemaCompiler().compileWithReport(schema) +let compositionProbe = try JSON.withComposition(limits: .offline) { try $0.parseDocument(data) } +let clock = ContinuousClock() +func seconds(_ duration: Duration) -> Double { Double(duration.components.seconds) + Double(duration.components.attoseconds) / 1e18 } +func measure(_ name: String, iterations: Int, work: UInt64? = nil, peak: UInt64? = nil, retained: UInt64? = nil, _ operation: () throws -> Void) throws -> Sample { + for _ in 0..<2 { for _ in 0.. JSONOperationReport { + try JSON.withComposition { (scope: borrowing JSONCompositionScope) in + let document = try scope.parseDocument(data) + try adapterStep(scope) + let path = try scope.pointer(tokens: ["count"]) + let uri = try scope.uri("https://example.invalid/schema.json#/$defs/count") + _ = try scope.removingFragment(uri) + return try scope.resolve(path, in: document.root) + } +} + +private func adapterStep(_ scope: borrowing JSONCompositionScope) throws { + try scope.reserveExternalLogicalBytes(1, count: 32, overhead: 32) + try scope.chargeExternalWork(1) +} diff --git a/Tools/CompositionQualification/Tests/CompositionPublicTests/CompositionTests.swift b/Tools/CompositionQualification/Tests/CompositionPublicTests/CompositionTests.swift new file mode 100644 index 0000000..72ad6b1 --- /dev/null +++ b/Tools/CompositionQualification/Tests/CompositionPublicTests/CompositionTests.swift @@ -0,0 +1,370 @@ +// SPDX-License-Identifier: Apache-2.0 +import Foundation +import Testing +import JSON +import JSONSchema + +private enum AuditError: Error { case workTrace, retentionTrace, caller } +private let source = Data(##"{"n":9007199254740993,"adjacent":9007199254740992,"long":123456789012345678901234567890.01234567890123456789,"fraction":0.01,"negative":-0,"é":true,"e\u0301":false,"a/b":{"~":null},"items":[false],"type":["string","null"],"$dynamicRef":"#node","$schema":"https://unknown.invalid/dialect"}"##.utf8) + +// Independent audited-adapter ledger, not derived from operation statistics. +// Three bounded buffer fills: one frame visit and one unit per written byte; +// 32 logical header bytes per buffer. Final native receipt copies 49 slots and +// settles its aggregate slot (50 work), retaining 32 + 49*8 logical bytes. +private let goldenWork: UInt64 = 197 +private let goldenBytes: UInt64 = 664 +private func fill(_ scope: borrowing JSONCompositionScope, count: Int, first: Bool, mutation: String?) throws -> [UInt8] { + let reservation = count - (first && mutation == "reservation" ? 1 : 0) + try scope.reserveExternalLogicalBytes(1, count: UInt64(reservation), overhead: 32) + try scope.chargeExternalWork(1) + var bytes: [UInt8] = [] + bytes.reserveCapacity(count) + for i in 0.. JSONOperationReport<([UInt8], [UInt8], [UInt8])> { + let mutation = ProcessInfo.processInfo.environment["JCOMP_TRACE_MUTATION"] + return try JSON.withComposition(limits: limits) { scope in + let queue = try fill(scope, count: 64, first: true, mutation: mutation) + let scratch = try fill(scope, count: 32, first: false, mutation: mutation) + let output = try fill(scope, count: 48, first: false, mutation: mutation) + try scope.chargeExternalWork(extra) + return (queue, scratch, output) + } +} +private func audit(_ report: JSONOperationReport<([UInt8], [UInt8], [UInt8])>) throws { + guard report.statistics[.workUnits] == goldenWork else { throw AuditError.workTrace } + guard report.statistics[.retainedBytes] == goldenBytes, + report.statistics.peakLogicalRetainedBytes == goldenBytes else { throw AuditError.retentionTrace } +} +private func capture(_ body: () throws -> Void) -> (any Error)? { + do { try body(); Issue.record("expected an error"); return nil } catch { return error } +} + +@Test func externalTraceMatchesIndependentLedger() throws { + let report = try traceAdapter() + #expect(report.result.0.count == 64 && report.result.1.count == 32 && report.result.2.count == 48) + try audit(report) + if let directory = ProcessInfo.processInfo.environment["JCOMP_TRACE_DIRECTORY"] { + let trace = "owner\twork\tlogical_bytes\nqueue\t65\t96\nscratch\t33\t64\nresult\t49\t80\nreceipt\t50\t424\n" + try trace.write(toFile: directory + "/owned-events.tsv", atomically: true, encoding: .utf8) + } +} + +@Test func exactWorkAndRetentionLimitsAreActive() throws { + var exact = JSONLimits.balanced + exact[.workUnits] = goldenWork; exact[.retainedBytes] = goldenBytes + try audit(traceAdapter(exact)) + for metric in [JSONResourceMetric.workUnits, .retainedBytes] { + var below = exact; below[metric] -= 1 + let failure = capture { _ = try traceAdapter(below) } as? JSONFailure + #expect(failure?.category == .resource && failure?.resource?.metric == metric) + } + #expect(throws: JSONFailure.self) { _ = try traceAdapter(exact, extra: 1) } +} + +@Test func reportOverlapIsAdmittedByFinalizer() throws { + var limits = JSONLimits.balanced + limits[.retainedBytes] = 424; limits[.workUnits] = 50 + let report = try JSON.withComposition(limits: limits) { _ in 7 } + #expect(report.result == 7 && report.statistics[.retainedBytes] == 424) + #expect(report.statistics[.workUnits] == 50 && report.statistics.peakLogicalRetainedBytes == 424) + limits[.retainedBytes] = 423 + #expect(throws: JSONFailure.self) { try JSON.withComposition(limits: limits) { _ in 7 } } + limits[.retainedBytes] = 424; limits[.workUnits] = 49 + #expect(throws: JSONFailure.self) { try JSON.withComposition(limits: limits) { _ in 7 } } +} + +@Test func finiteMaximumWorkDoesNotOverflowCompletion() throws { + var limits = JSONLimits.balanced; limits[.workUnits] = UInt64.max - 1 + let report = try JSON.withComposition(limits: limits) { scope in try scope.chargeExternalWork(UInt64.max - 51) } + #expect(report.statistics[.workUnits] == UInt64.max - 1) + #expect(throws: JSONFailure.self) { + try JSON.withComposition(limits: limits) { scope in try scope.chargeExternalWork(UInt64.max - 50) } + } +} + +@Test func checkedProductAndOverheadFailBeforeGrowth() throws { + for mode in 0..<2 { + var growth = false + let error = capture { + _ = try JSON.withComposition { scope in + do { + if mode == 0 { try scope.reserveExternalLogicalBytes(UInt64.max, count: 2) } + else { try scope.reserveExternalLogicalBytes(1, overhead: UInt64.max) } + growth = true + } catch let failure as JSONFailure { #expect(failure.code == .counterOverflow) } + return 0 + } + } as? JSONFailure + #expect(!growth && error?.code == .counterOverflow) + } +} + +@Test func zeroReservationAndZeroQuotaHaveDistinctMeaning() throws { + let report = try JSON.withComposition { try $0.reserveExternalLogicalBytes(0, count: 0) } + #expect(report.statistics[.retainedBytes] == 424) + var none = JSONLimits.balanced; none[.workUnits] = 0 + #expect(throws: JSONFailure.self) { + try JSON.withComposition(limits: none) { scope in do { try scope.chargeExternalWork(1) } catch { } } + } +} + +@Test func firstQuotaFailureSurvivesCatchCleanupAndCallerError() throws { + var limits = JSONLimits.balanced; limits[.workUnits] = 1 + let error = capture { + _ = try JSON.withComposition(limits: limits) { scope in + do { try scope.chargeExternalWork(2) } catch { } + do { try scope.reserveExternalLogicalBytes(UInt64.max, count: 2) } catch { } + throw AuditError.caller + } + } as? JSONFailure + #expect(error?.code == .limitExceeded && error?.resource?.metric == .workUnits) + #expect(error?.resource?.attemptedUsage == 2) +} + +@Test func firstOverflowSurvivesLaterCalls() throws { + let error = capture { + _ = try JSON.withComposition { scope in + try scope.chargeExternalWork(1) + do { try scope.chargeExternalWork(UInt64.max) } catch { } + do { _ = try scope.parseDocument(Data("{".utf8)) } catch { } + return 7 + } + } as? JSONFailure + #expect(error?.code == .counterOverflow && error?.resource?.metric == .workUnits) +} + +@Test func freshInvocationHasFreshLedgerAfterFailure() throws { + #expect(throws: JSONFailure.self) { + try JSON.withComposition { try $0.reserveExternalLogicalBytes(UInt64.max, count: 2) } + } + try audit(traceAdapter()) +} + +@Test func preCopyCapacityFailurePrecedesAnyCopyWork() throws { + var limits = JSONLimits.balanced; limits[.workUnits] = 0 + limits[.inputBytes] = UInt64(source.count - 1) + let input = capture { _ = try JSON.withComposition(limits: limits) { try $0.parseDocument(source) } } as? JSONFailure + #expect(input?.resource?.metric == .inputBytes && input?.resource?.attemptedUsage == UInt64(source.count)) + limits[.inputBytes] = UInt64(source.count); limits[.retainedBytes] = UInt64(source.count - 1) + let bytes = capture { _ = try JSON.withComposition(limits: limits) { try $0.parseDocument(source) } } as? JSONFailure + #expect(bytes?.resource?.metric == .retainedBytes && bytes?.resource?.attemptedUsage == UInt64(source.count)) +} + +@Test func sourceLexemesScalarKeysAndSpansArePreserved() throws { + let report = try JSON.withComposition { try $0.parseDocument(source) } + let doc = report.result; let ordinary = try JSONParser().parseDocument(source) + #expect(doc.root == ordinary.root && doc.sourceBytes == source) + #expect(doc.root.objectValue?["n"]?.numberValue?.originalLexeme == "9007199254740993") + #expect(doc.root.objectValue?["adjacent"]?.numberValue?.originalLexeme == "9007199254740992") + #expect(doc.root.objectValue?["long"]?.numberValue?.originalLexeme == "123456789012345678901234567890.01234567890123456789") + #expect(doc.root.objectValue?["fraction"]?.numberValue?.originalLexeme == "0.01") + #expect(doc.root.objectValue?["negative"]?.numberValue?.originalLexeme == "-0") + #expect(doc.root.objectValue?["é"] == true && doc.root.objectValue?["e\u{301}"] == false) + #expect(doc.sourceMap?.entries.count == ordinary.sourceMap?.entries.count) + #expect(report.statistics[.numericWorkUnits] > 0 && report.statistics[.sourceMapEntries] > 0) +} + +@Test func perDocumentInputBoundAndCumulativeResources() throws { + let bytes = Data("null".utf8) + var limits = JSONLimits.balanced; limits[.inputBytes] = 4 + let report = try JSON.withComposition(limits: limits) { scope in + let entry = try scope.parseDocument(bytes) + let a = try scope.parseDocument(bytes) + let b = try scope.parseDocument(bytes) + return (entry, a, b) + } + #expect(report.result.0.sourceBytes == bytes && report.result.1.sourceBytes == bytes && report.result.2.sourceBytes == bytes) + #expect(report.statistics[.inputBytes] == 12) + // Independent ownership lower bound: 3*(source+copy+node+map) + receipt. + #expect(report.statistics[.retainedBytes] >= 3 * (8 + 32 + 32) + 424) + limits[.inputBytes] = 3 + #expect(throws: JSONFailure.self) { try JSON.withComposition(limits: limits) { try $0.parseDocument(bytes) } } +} + +@Test func emptyAndLongSourceRemainStrictAndBounded() throws { + for bytes in [Data(), Data(" ".utf8), Data("null true".utf8)] { + #expect(throws: JSONFailure.self) { try JSON.withComposition { try $0.parseDocument(bytes) } } + } + let bytes = Data(("\"" + String(repeating: "x", count: 8192) + "\"").utf8) + let doc = try JSON.withComposition { try $0.parseDocument(bytes) }.result + #expect(doc.sourceBytes == bytes && doc.root.stringValue?.utf8.count == 8192) +} + +@Test func decodedDuplicateKeysStillFail() throws { + let bytes = Data(#"{"é":0,"\u00e9":1}"#.utf8) + #expect(throws: JSONFailure.self) { try JSON.withComposition { try $0.parseDocument(bytes) } } +} + +@Test func nativePointerEscapesAndScalarIdentity() throws { + try JSON.withComposition { scope in + let doc = try scope.parseDocument(source) + let root = try scope.pointer(parsing: "") + #expect(try scope.resolve(root, in: doc.root) == doc.root) + let path = try scope.pointer(tokens: ["a/b", "~"]) + #expect(try scope.resolve(path, in: doc.root) == .null) + let scalar = try scope.pointer(uriFragment: "#/%C3%A9") + #expect(try scope.resolve(scalar, in: doc.root) == true) + let decomposed = try scope.appending(root, key: "e\u{301}") + #expect(try scope.resolve(decomposed, in: doc.root) == false) + } +} + +@Test func nativePointerIndexAndPercentGrammarAreNotPermissive() throws { + for text in ["/01", "/-", "/-1", "/999999999999999999999999"] { + #expect(throws: JSONFailure.self) { + try JSON.withComposition { scope in try scope.resolve(scope.pointer(parsing: text), in: .array([.null])) } + } + } + for text in ["#/%", "#/%FF", "#/%7E2"] { + #expect(throws: JSONFailure.self) { try JSON.withComposition { try $0.pointer(uriFragment: text) } } + } + let pointer = try JSON.withComposition { try $0.pointer(uriFragment: "#/%252F") }.result + #expect(pointer.tokens.first?.string == "%2F") +} + +@Test func nativeURICompositionUsesCoreRatherThanCompiledPlanQuota() throws { + var limits = JSONLimits.balanced; limits[.compiledPlanBytes] = 0; limits[.compileWorkUnits] = 0 + let report = try JSON.withComposition(limits: limits) { scope in + let base = try scope.uri("https://EXAMPLE.invalid/a/../api.json#part") + let relative = try scope.uri("schemas.json#/n", relativeTo: base) + let resource = try scope.removingFragment(relative) + #expect(resource.string == "https://example.invalid/schemas.json") + return relative + } + let ordinary = try JSONSchemaURI("schemas.json#/n", relativeTo: JSONSchemaURI("https://EXAMPLE.invalid/a/../api.json#part")) + #expect(report.result == ordinary) + #expect(report.statistics[.compiledPlanBytes] == 0 && report.statistics[.compileWorkUnits] == 0) + #expect(report.statistics.domain == .core) +} + +@Test func URIDataFailureIsStickyAndNeverFetches() throws { + #expect(throws: (any Error).self) { + try JSON.withComposition { scope in do { _ = try scope.uri("https://example.invalid/%ZZ") } catch { }; return 0 } + } +} + +@Test func acceptedInputHistorySurvivesRestriction() throws { + let error = capture { + _ = try JSON.withComposition { scope in + _ = try scope.parseDocument(source) + // Retire the larger input count. The current input is now only four + // bytes; restriction must still consult accepted-capacity history. + _ = try scope.parseDocument(Data("null".utf8)) + var lower = JSONLimits.balanced; lower[.inputBytes] = UInt64(source.count - 1) + do { try scope.restrict(to: lower) } catch { } + return 0 + } + } as? JSONFailure + #expect(error?.code == .limitExceeded && error?.resource?.metric == .inputBytes) + #expect(error?.resource?.attemptedUsage == UInt64(source.count)) +} + +@Test func independentNestedInvocationCannotRestoreOuterHealth() throws { + var limits = JSONLimits.balanced; limits[.workUnits] = 0 + #expect(throws: JSONFailure.self) { + try JSON.withComposition(limits: limits) { outer in + do { try outer.chargeExternalWork(1) } catch { } + _ = try JSON.withComposition { _ in 0 } + return 0 + } + } +} + +@Test func publicScopeCancellationCannotBeCaughtIntoSuccess() async throws { + let task = Task.detached { + try JSON.withComposition { scope in + withUnsafeCurrentTask { $0?.cancel() } + do { try scope.checkpoint() } catch is CancellationError { } + return 9 + } + } + do { _ = try await task.value; Issue.record("cancelled scope finalized") } catch is CancellationError { } +} + +@Test func firstQuotaCauseWinsOverLaterCancellation() async throws { + let task = Task.detached { + var limits = JSONLimits.balanced; limits[.workUnits] = 0 + return try JSON.withComposition(limits: limits) { scope in + do { try scope.chargeExternalWork(1) } catch { } + withUnsafeCurrentTask { $0?.cancel() } + do { try scope.checkpoint() } catch { } + return 9 + } + } + do { _ = try await task.value; Issue.record("terminal scope finalized") } + catch let failure as JSONFailure { #expect(failure.resource?.metric == .workUnits && failure.resource?.attemptedUsage == 1) } +} + +@Test func independentConcurrentSessionsHaveDeterministicReports() async throws { + try await withThrowingTaskGroup(of: JSONOperationStatistics.self) { group in + for _ in 0..<16 { group.addTask { try traceAdapter().statistics } } + for try await report in group { #expect(report[.workUnits] == goldenWork && report.peakLogicalRetainedBytes == goldenBytes) } + } +} + +@Test func callerErrorsAndForgedReportsAreNotCertificates() throws { + #expect(throws: AuditError.self) { try JSON.withComposition { _ in throw AuditError.caller } } + let genuine = try traceAdapter() + let forged = JSONOperationReport(result: "not the audited result", statistics: genuine.statistics) + #expect(forged.result == "not the audited result") + #expect(forged.statistics[.workUnits] == goldenWork) +} + + +@Test func multiResourceSourceAndFinalOutputOverlap() throws { + // Independent native weight ledger for each scalar document: + // 4 copied bytes + 4 preserved source bytes + 32 value + 32 source-map entry. + // All three documents remain in the returned tuple, alongside two buffers. + // 3*72 + (64+32) + (48+32) + 424 report = 816 logical bytes. + let expected: UInt64 = 816 + for ceiling in [expected, expected - 1] { + var limits = JSONLimits.balanced; limits[.inputBytes] = 4; limits[.retainedBytes] = ceiling + do { + let report = try JSON.withComposition(limits: limits) { scope in + let entry = try scope.parseDocument(Data("null".utf8)) + let resourceA = try scope.parseDocument(Data("null".utf8)) + let resourceB = try scope.parseDocument(Data("null".utf8)) + let queue = try fill(scope, count: 64, first: false, mutation: nil) + let output = try fill(scope, count: 48, first: false, mutation: nil) + return (entry, resourceA, resourceB, queue, output) + } + #expect(ceiling == expected) + #expect(report.statistics[.retainedBytes] == expected) + #expect(report.statistics.peakLogicalRetainedBytes == expected) + #expect(report.result.0.sourceBytes == Data("null".utf8) && report.result.4.count == 48) + } catch let failure as JSONFailure { + #expect(ceiling == expected - 1 && failure.resource?.metric == .retainedBytes) + #expect(failure.resource?.attemptedUsage == expected) + } + } +} + +@Test func explicitBorrowAllowsNestedSynchronousHelperOnTheSameLedger() throws { + // Independently counted: 2 entry units, 3 helper units, then 50 finalizer + // units; the helper's 8-byte frame overlaps the 424-byte report. + var limits = JSONLimits.balanced + limits[.workUnits] = 55; limits[.retainedBytes] = 432 + let report = try JSON.withComposition(limits: limits) { (scope: borrowing JSONCompositionScope) in + func local() throws { + try scope.reserveExternalLogicalBytes(8) + try scope.chargeExternalWork(3) + } + try scope.chargeExternalWork(2) + try local() + return 7 + } + #expect(report.result == 7 && report.statistics[.workUnits] == 55) + #expect(report.statistics[.retainedBytes] == 432) + limits[.workUnits] = 54 + #expect(throws: JSONFailure.self) { + try JSON.withComposition(limits: limits) { (scope: borrowing JSONCompositionScope) in + func local() throws { try scope.chargeExternalWork(5) } + try local() + } + } +} diff --git a/Tools/CompositionQualification/Tests/CompositionPublicTests/Fixtures/MANIFEST.json b/Tools/CompositionQualification/Tests/CompositionPublicTests/Fixtures/MANIFEST.json new file mode 100644 index 0000000..8055910 --- /dev/null +++ b/Tools/CompositionQualification/Tests/CompositionPublicTests/Fixtures/MANIFEST.json @@ -0,0 +1,48 @@ +{ + "status": "source fixtures created here; no Swift native execution in control tower", + "nativeExpectedInputPolicy": "PR36 candidate per-document input limit, cumulative final inputBytes, not Swift-OpenAPI total-source policy", + "files": [ + { + "path": "duplicate-decoded-key.json", + "bytes": 19, + "sha256": "cdeecc00c66cefef1c2952a48555a2387569492ecf83cc1fd645e32a306163ff", + "strictJSONValid": true, + "special": "deliberate decoded-key duplicate" + }, + { + "path": "entry-577.json", + "bytes": 577, + "sha256": "ea5e610aacf9c5016c0891764bdfccbea1db6d04263fc0b4b3e7baeea2c3303e", + "strictJSONValid": true, + "special": null + }, + { + "path": "invalid-json.json", + "bytes": 14, + "sha256": "f505862f6c151daf0f53ec480001af4b0a1bbb3da93315e014b8faab1b302600", + "strictJSONValid": false, + "special": null + }, + { + "path": "resource-152.json", + "bytes": 152, + "sha256": "d80a27b12fbd06f2df2d566af0b2f1138b4c0c40e9c7efb7622d4d7e2400662d", + "strictJSONValid": true, + "special": null + }, + { + "path": "resource-578.json", + "bytes": 578, + "sha256": "009c74073c540513a841aecc3396878a962f2c04124170566fb9aa3f1a4ff081", + "strictJSONValid": true, + "special": null + }, + { + "path": "source-exact.json", + "bytes": 164, + "sha256": "f5e4e8ec2bbbc070c857b3e2536cd8cc31c86d49136160a6611fc6cb11544791", + "strictJSONValid": true, + "special": null + } + ] +} diff --git a/Tools/CompositionQualification/Tests/CompositionPublicTests/Fixtures/duplicate-decoded-key.json b/Tools/CompositionQualification/Tests/CompositionPublicTests/Fixtures/duplicate-decoded-key.json new file mode 100644 index 0000000..88abad2 --- /dev/null +++ b/Tools/CompositionQualification/Tests/CompositionPublicTests/Fixtures/duplicate-decoded-key.json @@ -0,0 +1 @@ +{"\u00E9":1,"é":2} \ No newline at end of file diff --git a/Tools/CompositionQualification/Tests/CompositionPublicTests/Fixtures/entry-577.json b/Tools/CompositionQualification/Tests/CompositionPublicTests/Fixtures/entry-577.json new file mode 100644 index 0000000..aa3a250 --- /dev/null +++ b/Tools/CompositionQualification/Tests/CompositionPublicTests/Fixtures/entry-577.json @@ -0,0 +1 @@ +{"padding":"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"} \ No newline at end of file diff --git a/Tools/CompositionQualification/Tests/CompositionPublicTests/Fixtures/invalid-json.json b/Tools/CompositionQualification/Tests/CompositionPublicTests/Fixtures/invalid-json.json new file mode 100644 index 0000000..8ff2995 --- /dev/null +++ b/Tools/CompositionQualification/Tests/CompositionPublicTests/Fixtures/invalid-json.json @@ -0,0 +1 @@ +{"incomplete": \ No newline at end of file diff --git a/Tools/CompositionQualification/Tests/CompositionPublicTests/Fixtures/resource-152.json b/Tools/CompositionQualification/Tests/CompositionPublicTests/Fixtures/resource-152.json new file mode 100644 index 0000000..d38bf8b --- /dev/null +++ b/Tools/CompositionQualification/Tests/CompositionPublicTests/Fixtures/resource-152.json @@ -0,0 +1 @@ +{"padding":"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"} \ No newline at end of file diff --git a/Tools/CompositionQualification/Tests/CompositionPublicTests/Fixtures/resource-578.json b/Tools/CompositionQualification/Tests/CompositionPublicTests/Fixtures/resource-578.json new file mode 100644 index 0000000..53aa5cf --- /dev/null +++ b/Tools/CompositionQualification/Tests/CompositionPublicTests/Fixtures/resource-578.json @@ -0,0 +1 @@ +{"padding":"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"} \ No newline at end of file diff --git a/Tools/CompositionQualification/Tests/CompositionPublicTests/Fixtures/source-exact.json b/Tools/CompositionQualification/Tests/CompositionPublicTests/Fixtures/source-exact.json new file mode 100644 index 0000000..5f863d5 --- /dev/null +++ b/Tools/CompositionQualification/Tests/CompositionPublicTests/Fixtures/source-exact.json @@ -0,0 +1 @@ +{"large":9007199254740993,"fraction":0.0100,"precomposed":"\u00E9","decomposed":"e\u0301","schemaType":["number","integer"],"schemaUnknown":{"x-unrecognized":true}} \ No newline at end of file diff --git a/Tools/CompositionQualification/Tests/CompositionPublicTests/PinnedCompositionTests.swift b/Tools/CompositionQualification/Tests/CompositionPublicTests/PinnedCompositionTests.swift new file mode 100644 index 0000000..ee51779 --- /dev/null +++ b/Tools/CompositionQualification/Tests/CompositionPublicTests/PinnedCompositionTests.swift @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: Apache-2.0 +import Foundation +import Testing +import JSON +import JSONSchema + +private func pinned(_ name: String) throws -> Data { + try Data(contentsOf: #require(Bundle.module.url(forResource: name, withExtension: "json", subdirectory: "Fixtures"))) +} + +private func refused(_ body: () throws -> Void) throws -> JSONFailure { + do { try body() } catch let failure as JSONFailure { return failure } + Issue.record("Expected a structured native failure") + throw PinnedFailure.missingFailure +} +private enum PinnedFailure: Error { case missingFailure, caller } + +@Test func pinnedDocumentsHaveIndependentCapacityAndCumulativeStatistics() throws { + let entry = try pinned("entry-577"), resource = try pinned("resource-152") + let larger = try pinned("resource-578") + #expect(entry.count == 577 && resource.count == 152 && larger.count == 578) + var limits = JSONLimits.balanced + limits[.inputBytes] = 577 + let report = try JSON.withComposition(limits: limits) { scope in + (try scope.parseDocument(entry), try scope.parseDocument(resource)) + } + #expect(report.statistics[.inputBytes] == 729) + #expect(report.result.0.sourceBytes == entry && report.result.1.sourceBytes == resource) + let repeated = try JSON.withComposition(limits: limits) { scope in + (try scope.parseDocument(entry), try scope.parseDocument(resource), try scope.parseDocument(entry)) + } + #expect(repeated.statistics[.inputBytes] == 1306) + #expect(repeated.result.0.sourceBytes == repeated.result.2.sourceBytes) + #expect(repeated.statistics[.retainedBytes] > report.statistics[.retainedBytes]) + #expect(repeated.statistics[.workUnits] > report.statistics[.workUnits]) + limits[.inputBytes] = 576 + let first = try refused { _ = try JSON.withComposition(limits: limits) { try $0.parseDocument(entry) } } + #expect(first.category == .resource && first.code == .limitExceeded) + #expect(first.resource?.domain == .parsing && first.resource?.metric == .inputBytes) + #expect(first.resource?.limit == 576 && first.resource?.attemptedUsage == 577) + limits[.inputBytes] = 577 + let second = try refused { + _ = try JSON.withComposition(limits: limits) { scope in + _ = try scope.parseDocument(entry) + do { _ = try scope.parseDocument(larger) } catch { } + do { _ = try scope.parseDocument(resource) } catch { } + throw PinnedFailure.caller + } + } + #expect(second.category == .resource && second.code == .limitExceeded) + #expect(second.resource?.domain == .parsing && second.resource?.metric == .inputBytes) + #expect(second.resource?.limit == 577 && second.resource?.attemptedUsage == 578) +} + +@Test func pinnedSyntaxFailuresRemainDistinctFromResourceRefusalAndSticky() throws { + for (data, expected) in [(try pinned("invalid-json"), JSONFailure.Code.unexpectedEOF), + (try pinned("duplicate-decoded-key"), .duplicateKey), + (Data(), .unexpectedEOF)] { + let failure = try refused { + _ = try JSON.withComposition { scope in + do { _ = try scope.parseDocument(data) } catch { } + do { try scope.reserveExternalLogicalBytes(UInt64.max, count: 2) } catch { } + return 7 + } + } + #expect(failure.category == .syntax && failure.code == expected) + #expect(failure.resource == nil) + } +} + +@Test func pinnedExactSourceAndAll49CompletionCountersUseNativeAuthority() throws { + let bytes = try pinned("source-exact") + let report = try JSON.withComposition { scope in + let document = try scope.parseDocument(bytes) + let number = try scope.resolve(scope.pointer(parsing: "/large"), in: document.root) + let fraction = try scope.resolve(scope.pointer(uriFragment: "#/%66raction"), in: document.root) + let uri = try scope.uri("https://EXAMPLE.invalid/a/../schema#/%252F") + return (document, number, fraction, try scope.removingFragment(uri)) + } + #expect(report.result.0.sourceBytes == bytes) + #expect(report.result.1.numberValue?.originalLexeme == "9007199254740993") + #expect(report.result.2.numberValue?.originalLexeme == "0.0100") + #expect(report.result.3.string == "https://example.invalid/schema") + let root = report.result.0.root + #expect(Array(try #require(root.objectValue?["precomposed"]?.stringValue).unicodeScalars).map(\.value) == [0xE9]) + #expect(Array(try #require(root.objectValue?["decomposed"]?.stringValue).unicodeScalars).map(\.value) == [0x65, 0x301]) + #expect(root.objectValue?["schemaType"] == .array(["number", "integer"])) + #expect(root.objectValue?["schemaUnknown"]?.objectValue?["x-unrecognized"] == true) + #expect(report.statistics[.compileWorkUnits] == 0 && report.statistics[.compiledPlanBytes] == 0) + let empty = try JSON.withComposition { _ in 7 }.statistics + #expect(JSONResourceMetric.allCases.count == 49 && empty.accountingVersion == 1) + #expect(empty.domain == .core && empty.peakLogicalRetainedBytes == 424) + for metric in JSONResourceMetric.allCases { + let expected: UInt64 = metric == .workUnits ? 50 : metric == .retainedBytes ? 424 : 0 + #expect(empty[metric] == expected) + } + for (metric, exact) in [(JSONResourceMetric.workUnits, UInt64(50)), (.retainedBytes, 424)] { + var limits = JSONLimits.balanced + limits[metric] = exact - 1 + let failure = try refused { _ = try JSON.withComposition(limits: limits) { _ in 7 } } + #expect(failure.category == .resource && failure.code == .limitExceeded) + #expect(failure.resource?.domain == .core && failure.resource?.metric == metric) + #expect(failure.resource?.limit == exact - 1 && failure.resource?.attemptedUsage == exact) + } +}