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 3ff7f60..9fd6431 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 14515c1..049f381 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 e0e2939..c643f6a 100644 --- a/Documentation/Architecture/ExtractionManifest.json +++ b/Documentation/Architecture/ExtractionManifest.json @@ -297,7 +297,8 @@ "1.5.1 restores 126 original Apache-2.0 test functions and qualification tools from Swift-JSON 1.2.1 (e8b687f822c577311156b13bf1d15c2d5620b84e), with file hashes and stable function identities in Qualification/Release1.5.1/Restoration.json. The diagnostic omission and Git-free provenance repairs are restored original code; current seven-product harness reconciliation and deterministic CLI identity are original contributions. No new upstream adaptation or dependency.", "1.6 S3 G7 seeded grammar, scalar-key permutation tests, public replay consumer and strict cardinality/matrix harness are original Apache-2.0 contributions. The 64-schema authored fixture has SHA-256 cb55604183a6db7df5fa968db283e7abeae794277d2b0cef033b0bdb92e615cb. Independent python-jsonschema 4.26.0 is research qualification only, with exact numeric binding; no Python runtime dependency or copied validator/engine is distributed.", "1.6 S3 G8 materializes the unchanged synthetic corpus per caller access, replacing process-lifetime stored JSON payloads exposed by Linux ASan/LSan. The public process-lifetime consumer and strengthened exact/value-copy test are original Apache-2.0 qualification code; no suppression, runtime fallback, new dependency or corpus/digest change is introduced.", - "Composition reference-load fixtures, serial descriptor staging, public-only adapter, capacity/terminal tests and strict CI receipt checks are original Apache-2.0 code. Generated JSON graphs introduce no upstream protocol fixtures, engine or dependency. Core public APIs, finite profiles and accounting weights are unchanged." + "Composition reference-load fixtures, serial descriptor staging, public-only adapter, capacity/terminal tests and strict CI receipt checks are original Apache-2.0 code. Generated JSON graphs introduce no upstream protocol fixtures, engine or dependency. Core public APIs, finite profiles and accounting weights are unchanged.", + "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 36770c0..b653973 100644 --- a/Documentation/CI/README.md +++ b/Documentation/CI/README.md @@ -1,3 +1,18 @@ +# 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, @@ -14,6 +29,21 @@ The following sections preserve their original increment histories. # 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 @@ -252,3 +282,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/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 7814f17..6fe1266 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, @@ -67,7 +67,7 @@ "JSONRegexRuntimeTests": 16, "JSONStreamingTests": 30, "JSONPatchTests": 16, - "JSONIntegrationTests": 32, + "JSONIntegrationTests": 34, "JSONHTMLTests": 81, "JSONBridgesTests": 10, "JSONCLITests": 88, @@ -80,7 +80,7 @@ "JSONRegexRuntimeTests": 16, "JSONStreamingTests": 30, "JSONPatchTests": 16, - "JSONIntegrationTests": 32, + "JSONIntegrationTests": 34, "JSONHTMLTests": 81, "JSONBridgesTests": 10, "JSONNavigationTests": 39, @@ -102,5 +102,6 @@ "export_functions": { "JSONSchemaTests": 1, "JSONHTMLTests": 39 - } + }, + "composition_public_functions": 26 } diff --git a/Scripts/ci.py b/Scripts/ci.py index fbdc1b4..14075f3 100644 --- a/Scripts/ci.py +++ b/Scripts/ci.py @@ -21,6 +21,11 @@ def snapshot(): def commands(lane, output): # The qualification executable resolves output paths from its repository root. output = output.relative_to(ROOT) + 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': @@ -54,9 +59,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')], ] @@ -202,7 +207,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: @@ -231,6 +236,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=26 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'] @@ -293,7 +300,7 @@ def check_result(command, code, log, events=None): def main(): parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument('--lane', choices=['composition', 'navigation', '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', '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) @@ -302,7 +309,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 == 'composition' and sys.platform == 'linux') else collect() + environment = collect_linux() if args.lane.startswith('linux') or (args.lane.startswith('composition') 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..08cc29f --- /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 = 26 +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=26 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 949484b..094d6ab 100644 --- a/Scripts/test_qualification_harness.py +++ b/Scripts/test_qualification_harness.py @@ -48,11 +48,25 @@ def test_composition_discovery_and_capacity_limits_are_not_blanket_successes(sel for key in ('root_functions', 'portable_root_functions', 'wasi_root_functions'): self.assertEqual(policy[key]['JSONNavigationTests'], 39) self.assertEqual(policy[key]['JSONSchemaExamplesTests'], 98) - self.assertEqual(policy[key]['JSONIntegrationTests'], 32) + # Keep the 32 main integration cases plus PR36's two boundary cases. + 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]['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 setUp(self): self.temporary = tempfile.TemporaryDirectory() @@ -148,6 +162,12 @@ def test_exact_discovery_rejects_missing_duplicate_and_changed_parameters(self): self.assertTrue(check_result(harness, 0, 'HARNESS_TESTS passed cases=15')[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', '']: self.assertFalse(check_result(harness, 0, stale)[0]) + proof = ['python3', '-B', 'Scripts/qualify_composition_proof.py'] + summary = 'COMPOSITION_PROOF passed public_tests=26 configurations=2 mutants_rejected=2 compiler_negative=14' + self.assertTrue(check_result(proof, 0, summary)[0]) + for broken in ['', summary.replace('26', '25'), 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 6de53b5..5114642 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 @@ -56,3 +66,32 @@ unforgeable receipt, and it cannot establish that an arbitrary callback charged all its work. An adopting adapter must own its final accounting boundary and audit all external work and reservations. Independent reports cannot retroactively certify one enforced invocation. + + +## 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 baabdd5..43acc33 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:13096cf62cfd152b53c0c0dcd6292e5798904e6e3ac39b04f4f764322c89086c" + package static let revision = "sha256:456d6d07a6291cc4f1cfe89ed81a8b284c6a0703d30f092b9bdd24844825e6e3" 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..436e811 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") + ]), .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() + } + } +}