@intentius/tsad-conformance 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +59 -0
- package/dist/adapter.d.ts +65 -0
- package/dist/adapter.d.ts.map +1 -0
- package/dist/adapter.js +2 -0
- package/dist/adapter.js.map +1 -0
- package/dist/bundled.d.ts +5 -0
- package/dist/bundled.d.ts.map +1 -0
- package/dist/bundled.js +28 -0
- package/dist/bundled.js.map +1 -0
- package/dist/fixture.d.ts +49 -0
- package/dist/fixture.d.ts.map +1 -0
- package/dist/fixture.js +95 -0
- package/dist/fixture.js.map +1 -0
- package/dist/host.d.ts +33 -0
- package/dist/host.d.ts.map +1 -0
- package/dist/host.js +79 -0
- package/dist/host.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/rules.d.ts +46 -0
- package/dist/rules.d.ts.map +1 -0
- package/dist/rules.js +135 -0
- package/dist/rules.js.map +1 -0
- package/dist/runner.d.ts +26 -0
- package/dist/runner.d.ts.map +1 -0
- package/dist/runner.js +152 -0
- package/dist/runner.js.map +1 -0
- package/fixtures/F-CallLeak/a-call-returns-a-live-object/expect.json +8 -0
- package/fixtures/F-CallLeak/a-call-returns-a-live-object/project/caller.ts +5 -0
- package/fixtures/F-CallLeak/a-call-returns-a-live-object/project/leaky.ts +7 -0
- package/fixtures/F-CallLeak/a-call-returns-a-live-object/project/pure.ts +5 -0
- package/fixtures/F-Cycle/mutual-imports/expect.json +6 -0
- package/fixtures/F-Cycle/mutual-imports/project/a.ts +3 -0
- package/fixtures/F-Cycle/mutual-imports/project/b.ts +3 -0
- package/fixtures/F-Depth/exhaustion-is-a-fallback/expect.json +19 -0
- package/fixtures/F-Depth/exhaustion-is-a-fallback/project/deep.ts +3 -0
- package/fixtures/F-Depth/exhaustion-is-a-fallback/project/rec.ts +3 -0
- package/fixtures/F-Div-Depth/new-inside-function-body/expect.json +21 -0
- package/fixtures/F-Div-Depth/new-inside-function-body/project/app.ts +3 -0
- package/fixtures/F-Div-Depth/new-inside-function-body/project/fn.ts +3 -0
- package/fixtures/F-Div-Eager/referenced-not-called/expect.json +8 -0
- package/fixtures/F-Div-Eager/referenced-not-called/project/app.ts +3 -0
- package/fixtures/F-Div-NsNew/namespace-callee/expect.json +18 -0
- package/fixtures/F-Div-NsNew/namespace-callee/input.ts +3 -0
- package/fixtures/F-Div-Provenance/helper-name-from-project-import/expect.json +20 -0
- package/fixtures/F-Div-Provenance/helper-name-from-project-import/project/app.ts +3 -0
- package/fixtures/F-Div-Provenance/helper-name-from-project-import/project/local.ts +3 -0
- package/fixtures/F-Div-SameFileNew/no-module-graph/expect.json +8 -0
- package/fixtures/F-Div-SameFileNew/no-module-graph/input.ts +5 -0
- package/fixtures/F-Div-Tag/unregistered-tag/expect.json +15 -0
- package/fixtures/F-Div-Tag/unregistered-tag/input.ts +1 -0
- package/fixtures/F-Eval-Array/spread-string-rejected/expect.json +14 -0
- package/fixtures/F-Eval-Array/spread-string-rejected/input.ts +1 -0
- package/fixtures/F-Eval-Binary/division-and-float/expect.json +19 -0
- package/fixtures/F-Eval-Binary/division-and-float/input.ts +1 -0
- package/fixtures/F-Eval-Binary/logical-return-operand/expect.json +20 -0
- package/fixtures/F-Eval-Binary/logical-return-operand/input.ts +1 -0
- package/fixtures/F-Eval-Binary/plus-dispatch/expect.json +19 -0
- package/fixtures/F-Eval-Binary/plus-dispatch/input.ts +1 -0
- package/fixtures/F-Eval-Binary/relational-on-strings/expect.json +18 -0
- package/fixtures/F-Eval-Binary/relational-on-strings/input.ts +1 -0
- package/fixtures/F-Eval-CallEager/evaluated-at-fold-time/expect.json +8 -0
- package/fixtures/F-Eval-CallEager/evaluated-at-fold-time/project/app.ts +3 -0
- package/fixtures/F-Eval-CallEager/not-a-function/expect.json +22 -0
- package/fixtures/F-Eval-CallEager/not-a-function/project/app.ts +3 -0
- package/fixtures/F-Eval-CallEager/not-a-function/project/local.ts +1 -0
- package/fixtures/F-Eval-CallHelper/helper-folds/expect.json +8 -0
- package/fixtures/F-Eval-CallHelper/helper-folds/project/app.ts +3 -0
- package/fixtures/F-Eval-CallHelper/inside-function-body/expect.json +22 -0
- package/fixtures/F-Eval-CallHelper/inside-function-body/project/app.ts +3 -0
- package/fixtures/F-Eval-CallHelper/inside-function-body/project/fn.ts +5 -0
- package/fixtures/F-Eval-CallIntrinsic/call-form-folds/expect.json +8 -0
- package/fixtures/F-Eval-CallIntrinsic/call-form-folds/project/app.ts +3 -0
- package/fixtures/F-Eval-CallIntrinsic/inside-function-body/expect.json +22 -0
- package/fixtures/F-Eval-CallIntrinsic/inside-function-body/project/app.ts +3 -0
- package/fixtures/F-Eval-CallIntrinsic/inside-function-body/project/fn.ts +5 -0
- package/fixtures/F-Eval-CallLocal/local-function-folds/expect.json +7 -0
- package/fixtures/F-Eval-CallLocal/local-function-folds/project/app.ts +3 -0
- package/fixtures/F-Eval-CallLocal/local-function-folds/project/pure.ts +3 -0
- package/fixtures/F-Eval-CallLocal/recursion-folds/expect.json +17 -0
- package/fixtures/F-Eval-CallLocal/recursion-folds/project/rec.ts +3 -0
- package/fixtures/F-Eval-CallLocal/recursion-folds/project/shallow.ts +3 -0
- package/fixtures/F-Eval-CallLocal/spread-argument-rejected/expect.json +20 -0
- package/fixtures/F-Eval-CallLocal/spread-argument-rejected/project/app.ts +5 -0
- package/fixtures/F-Eval-CallLocal/spread-argument-rejected/project/pure.ts +3 -0
- package/fixtures/F-Eval-CallMethod/method-missing/expect.json +17 -0
- package/fixtures/F-Eval-CallMethod/method-missing/input.ts +1 -0
- package/fixtures/F-Eval-CallMethod/null-receiver-dot/expect.json +13 -0
- package/fixtures/F-Eval-CallMethod/null-receiver-dot/input.ts +2 -0
- package/fixtures/F-Eval-CallMethod/null-receiver-optional/expect.json +2 -0
- package/fixtures/F-Eval-CallMethod/null-receiver-optional/input.ts +2 -0
- package/fixtures/F-Eval-CallMethod/real-receiver/expect.json +2 -0
- package/fixtures/F-Eval-CallMethod/real-receiver/input.ts +1 -0
- package/fixtures/F-Eval-Template/number-formatting/expect.json +11 -0
- package/fixtures/F-Eval-Template/number-formatting/input.ts +1 -0
- package/fixtures/F-Eval-Template/tostring-of-non-numbers/expect.json +10 -0
- package/fixtures/F-Eval-Template/tostring-of-non-numbers/input.ts +1 -0
- package/fixtures/F-Eval-Unary/coercion/expect.json +19 -0
- package/fixtures/F-Eval-Unary/coercion/input.ts +1 -0
- package/fixtures/F-Eval-Undefined/bare-undefined/expect.json +2 -0
- package/fixtures/F-Eval-Undefined/bare-undefined/input.ts +1 -0
- package/fixtures/F-Exc-Registry/call-form-without-registry/expect.json +15 -0
- package/fixtures/F-Exc-Registry/call-form-without-registry/input.ts +1 -0
- package/fixtures/F-Host-Closed-vs-Open/package-call-is-closed/expect.json +9 -0
- package/fixtures/F-Host-Closed-vs-Open/package-call-is-closed/project/closed.ts +3 -0
- package/fixtures/F-Host-Closed-vs-Open/package-call-is-closed/project/open.ts +3 -0
- package/fixtures/F-Host-Closed-vs-Open/package-call-is-closed/project/pure.ts +3 -0
- package/fixtures/F-Host-DataExports/plain-data-export/expect.json +8 -0
- package/fixtures/F-Host-DataExports/plain-data-export/project/app.ts +3 -0
- package/fixtures/F-Host-Registry/tag-called-as-function/expect.json +8 -0
- package/fixtures/F-Host-Registry/tag-called-as-function/project/app.ts +3 -0
- package/fixtures/F-Host-Trust/unowned-package/expect.json +6 -0
- package/fixtures/F-Host-Trust/unowned-package/project/app.ts +3 -0
- package/fixtures/F-Namespace/synthetic-namespace-object/expect.json +7 -0
- package/fixtures/F-Namespace/synthetic-namespace-object/project/use.ts +3 -0
- package/fixtures/F-Namespace/synthetic-namespace-object/project/values.ts +2 -0
- package/fixtures/F-NoExports/nothing-to-produce/expect.json +7 -0
- package/fixtures/F-NoExports/nothing-to-produce/project/defaulted.ts +1 -0
- package/fixtures/F-NoExports/nothing-to-produce/project/empty.ts +3 -0
- package/fixtures/F-Prebuild/same-file-construction-shared/expect.json +19 -0
- package/fixtures/F-Prebuild/same-file-construction-shared/project/shared.ts +6 -0
- package/fixtures/F-Profile-DataHost/plain-data-across-files/expect.json +39 -0
- package/fixtures/F-Profile-DataHost/plain-data-across-files/project/defaults.ts +5 -0
- package/fixtures/F-Profile-DataHost/plain-data-across-files/project/service.ts +10 -0
- package/fixtures/F-Seed/seeded-by-its-own-body/expect.json +8 -0
- package/fixtures/F-Seed/seeded-by-its-own-body/project/app.ts +8 -0
- package/fixtures/F-Seed/seeded-by-its-own-body/project/lone.ts +1 -0
- package/fixtures/F-Succ/backward-along-captures/expect.json +26 -0
- package/fixtures/F-Succ/backward-along-captures/project/breaker.ts +10 -0
- package/fixtures/F-Succ/backward-along-captures/project/shared.ts +1 -0
- package/fixtures/F-Succ/backward-along-captures/project/sibling.ts +3 -0
- package/fixtures/F-Succ/forward-into-imports/expect.json +8 -0
- package/fixtures/F-Succ/forward-into-imports/project/app.ts +10 -0
- package/fixtures/F-Succ/forward-into-imports/project/config.ts +1 -0
- package/fixtures/F-Taint/independent-file-still-folds/expect.json +9 -0
- package/fixtures/F-Taint/independent-file-still-folds/project/alone.ts +1 -0
- package/fixtures/F-Taint/independent-file-still-folds/project/app.ts +8 -0
- package/fixtures/F-Total/one-bad-declarator-fails-the-file/expect.json +7 -0
- package/fixtures/F-Total/one-bad-declarator-fails-the-file/project/mixed.ts +3 -0
- package/fixtures/F-Val-Arity/props-attributes-and-spread/expect.json +14 -0
- package/fixtures/F-Val-Arity/props-attributes-and-spread/project/arities.ts +5 -0
- package/fixtures/F-Val-Callable/host-function-as-value/expect.json +16 -0
- package/fixtures/F-Val-Callable/host-function-as-value/project/app.ts +3 -0
- package/fixtures/F-Val-Fate/revive-every-envelope/expect.json +15 -0
- package/fixtures/F-Val-Fate/revive-every-envelope/project/entities.ts +6 -0
- package/fixtures/F-Val-Live/plain-data-is-not-live/expect.json +9 -0
- package/fixtures/F-Val-Live/plain-data-is-not-live/project/breaker.ts +10 -0
- package/fixtures/F-Val-Live/plain-data-is-not-live/project/quiet.ts +3 -0
- package/fixtures/F-Val-Live/plain-data-is-not-live/project/source.ts +5 -0
- package/fixtures/F-Val-Position/attrref-in-host-arguments/expect.json +8 -0
- package/fixtures/F-Val-Position/attrref-in-host-arguments/project/rejected.ts +4 -0
- package/fixtures/F-Val-Undefined/array-element-undefined/expect.json +15 -0
- package/fixtures/F-Val-Undefined/array-element-undefined/input.ts +1 -0
- package/fixtures/F-Val-Undefined/conditional-yields-undefined/expect.json +13 -0
- package/fixtures/F-Val-Undefined/conditional-yields-undefined/input.ts +2 -0
- package/fixtures/F-Val-Undefined/optional-chain-yields-undefined-property/expect.json +13 -0
- package/fixtures/F-Val-Undefined/optional-chain-yields-undefined-property/input.ts +2 -0
- package/fixtures/F-Val-Undefined/property-literal-undefined/expect.json +16 -0
- package/fixtures/F-Val-Undefined/property-literal-undefined/input.ts +1 -0
- package/fixtures/F-Val-Undefined/spread-carries-undefined/expect.json +14 -0
- package/fixtures/F-Val-Undefined/spread-carries-undefined/input.ts +2 -0
- package/fixtures/S-Array/spread/expect.json +1 -0
- package/fixtures/S-Array/spread/input.ts +1 -0
- package/fixtures/S-Binary/arithmetic-and-concat/expect.json +1 -0
- package/fixtures/S-Binary/arithmetic-and-concat/input.ts +1 -0
- package/fixtures/S-Binary/lazy-untaken-unfoldable/expect.json +1 -0
- package/fixtures/S-Binary/lazy-untaken-unfoldable/input.ts +1 -0
- package/fixtures/S-Binary/nullish-coalesce/expect.json +1 -0
- package/fixtures/S-Binary/nullish-coalesce/input.ts +1 -0
- package/fixtures/S-CallLocal/imported-callee-is-shape-valid/expect.json +10 -0
- package/fixtures/S-CallLocal/imported-callee-is-shape-valid/input.ts +3 -0
- package/fixtures/S-Conditional/taken-branch-only/expect.json +1 -0
- package/fixtures/S-Conditional/taken-branch-only/input.ts +1 -0
- package/fixtures/S-ExportDefault/imported-by-sibling/expect.json +30 -0
- package/fixtures/S-ExportDefault/imported-by-sibling/project/app.ts +3 -0
- package/fixtures/S-ExportDefault/imported-by-sibling/project/policy.ts +1 -0
- package/fixtures/S-ExportDefault/plain-object/expect.json +21 -0
- package/fixtures/S-ExportDefault/plain-object/input.ts +1 -0
- package/fixtures/S-Ident/const-chain/expect.json +1 -0
- package/fixtures/S-Ident/const-chain/input.ts +1 -0
- package/fixtures/S-Ident/process/expect.json +1 -0
- package/fixtures/S-Ident/process/input.ts +1 -0
- package/fixtures/S-Ident/unresolved/expect.json +1 -0
- package/fixtures/S-Ident/unresolved/input.ts +1 -0
- package/fixtures/S-Index/dynamic-key/expect.json +1 -0
- package/fixtures/S-Index/dynamic-key/input.ts +1 -0
- package/fixtures/S-Index/literal-key/expect.json +1 -0
- package/fixtures/S-Index/literal-key/input.ts +1 -0
- package/fixtures/S-Literal/null-and-booleans/expect.json +1 -0
- package/fixtures/S-Literal/null-and-booleans/input.ts +1 -0
- package/fixtures/S-Literal/numeric/expect.json +1 -0
- package/fixtures/S-Literal/numeric/input.ts +1 -0
- package/fixtures/S-LocalFunction/arrow-const-call/expect.json +21 -0
- package/fixtures/S-LocalFunction/arrow-const-call/project/label.ts +3 -0
- package/fixtures/S-LocalFunction/function-as-value-still-rejects/expect.json +11 -0
- package/fixtures/S-LocalFunction/function-as-value-still-rejects/input.ts +3 -0
- package/fixtures/S-LocalFunction/same-file-declaration-call/expect.json +19 -0
- package/fixtures/S-LocalFunction/same-file-declaration-call/project/twice.ts +5 -0
- package/fixtures/S-Member/nested/expect.json +1 -0
- package/fixtures/S-Member/nested/input.ts +1 -0
- package/fixtures/S-Member/nullish-plain-refuses/expect.json +1 -0
- package/fixtures/S-Member/nullish-plain-refuses/input.ts +1 -0
- package/fixtures/S-Member/optional-chain/expect.json +1 -0
- package/fixtures/S-Member/optional-chain/input.ts +1 -0
- package/fixtures/S-Object/spread-later-wins/expect.json +1 -0
- package/fixtures/S-Object/spread-later-wins/input.ts +1 -0
- package/fixtures/S-Object/spread-non-object/expect.json +1 -0
- package/fixtures/S-Object/spread-non-object/input.ts +1 -0
- package/fixtures/S-Reject/arrow-as-value/expect.json +1 -0
- package/fixtures/S-Reject/arrow-as-value/input.ts +1 -0
- package/fixtures/S-Reject/call-as-value/expect.json +1 -0
- package/fixtures/S-Reject/call-as-value/input.ts +1 -0
- package/fixtures/S-Template/coercion/expect.json +2 -0
- package/fixtures/S-Template/coercion/input.ts +2 -0
- package/fixtures/S-Template/composite-step-span/expect.json +2 -0
- package/fixtures/S-Template/composite-step-span/input.ts +1 -0
- package/fixtures/S-Template/const-span/expect.json +2 -0
- package/fixtures/S-Template/const-span/input.ts +2 -0
- package/fixtures/S-Template/envelope-span/expect.json +2 -0
- package/fixtures/S-Template/envelope-span/input.ts +2 -0
- package/fixtures/S-Template/nested-construction-span/expect.json +2 -0
- package/fixtures/S-Template/nested-construction-span/input.ts +1 -0
- package/fixtures/S-Template/no-substitution/expect.json +2 -0
- package/fixtures/S-Template/no-substitution/input.ts +1 -0
- package/fixtures/S-Template/unfoldable-span/expect.json +16 -0
- package/fixtures/S-Template/unfoldable-span/input.ts +1 -0
- package/fixtures/S-Unary/not-and-negate/expect.json +1 -0
- package/fixtures/S-Unary/not-and-negate/input.ts +1 -0
- package/fixtures/S-Unclaimed/composite-step/expect.json +3 -0
- package/fixtures/S-Unclaimed/composite-step/input.ts +1 -0
- package/fixtures/S-Unwrap/as-and-parens/expect.json +1 -0
- package/fixtures/S-Unwrap/as-and-parens/input.ts +1 -0
- package/fixtures/UNCOVERED.md +37 -0
- package/package.json +35 -0
- package/spec/VERSION +1 -0
- package/spec/divergence.md +129 -0
- package/spec/grammar.md +277 -0
- package/spec/hosts.md +175 -0
- package/spec/judgments.md +1049 -0
- package/spec/rules.md +102 -0
- package/spec/values.md +247 -0
- package/src/adapter.ts +52 -0
- package/src/adapters/chant.ts +125 -0
- package/src/bundled.ts +27 -0
- package/src/chant-agreement.test.ts +67 -0
- package/src/corpus.test.ts +66 -0
- package/src/corpus.ts +540 -0
- package/src/fixture.ts +117 -0
- package/src/host.ts +105 -0
- package/src/index.ts +6 -0
- package/src/rules.test.ts +50 -0
- package/src/rules.ts +140 -0
- package/src/runner.test.ts +46 -0
- package/src/runner.ts +119 -0
- package/src/version.test.ts +31 -0
|
@@ -0,0 +1,1049 @@
|
|
|
1
|
+
# Judgments
|
|
2
|
+
|
|
3
|
+
Normative draft. Three judgments: expression evaluation (#13), the per-file
|
|
4
|
+
fold-or-run verdict (#14), and the identity-taint fixpoint (#15). This file
|
|
5
|
+
currently carries the third in full; the first two are stubs pointing at the
|
|
6
|
+
requirements they will formalise. Identifiers are `F-*` per #6: every rule
|
|
7
|
+
here needs resolution, a registry, or the module graph, and is not decidable
|
|
8
|
+
from syntax.
|
|
9
|
+
|
|
10
|
+
Derived from `planFoldTaint` (`fold-import.ts:3890`),
|
|
11
|
+
`buildProjectImportEdges`, `FoldSession`, `FoldFileResult.liveSources`, and
|
|
12
|
+
`FoldableFunction.leakedIdentity` (`fold.ts:378`), at `e4074c17`.
|
|
13
|
+
|
|
14
|
+
## The objective every judgment serves
|
|
15
|
+
|
|
16
|
+
**At a fixed build-parameter binding, folding a file and running it are
|
|
17
|
+
observationally equivalent.**
|
|
18
|
+
|
|
19
|
+
A source file may be reduced from its AST to the entities it declares, or
|
|
20
|
+
imported and executed, and the build cannot tell which happened from the
|
|
21
|
+
output. In chant this is discharged by a differential over the example corpus
|
|
22
|
+
asserting identical errors and byte-identical serialized output
|
|
23
|
+
(INTENTIUS/chant#1025), with the caveat that the differential currently
|
|
24
|
+
compares only entries where every file folded (INTENTIUS/chant#2345), so the
|
|
25
|
+
mixed case, which is where the interesting requirements below actually fire,
|
|
26
|
+
has no differential evidence yet.
|
|
27
|
+
|
|
28
|
+
The binding is part of the statement, not a footnote. `params.<name>` folds to
|
|
29
|
+
a literal supplied at build invocation (F-Import, J2), so output is a function of source
|
|
30
|
+
*and* binding; "same source, same output" is true only with the binding held
|
|
31
|
+
fixed.
|
|
32
|
+
|
|
33
|
+
Equivalence is what makes the fallback safe, and the fallback is what
|
|
34
|
+
distinguishes this from a configuration language that rejects out-of-subset
|
|
35
|
+
source. But, per [`prior-art.md`](./prior-art.md), graceful fallback is not
|
|
36
|
+
itself new. What is new is that the fallback coexists with shared object
|
|
37
|
+
identity across the fold/run boundary, which is why J2 and J3 exist.
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Profiles (#78)
|
|
42
|
+
|
|
43
|
+
A profile names the subset of this specification an implementation claims.
|
|
44
|
+
Two exist. An implementation declares which it implements alongside the
|
|
45
|
+
version (README, Versioning), and the conformance suite judges it on the
|
|
46
|
+
fixtures tagged for that profile and no others.
|
|
47
|
+
|
|
48
|
+
**F-Profile.** The `full` profile is every rule in this document set,
|
|
49
|
+
including the `run` fallback and J3, which need a JavaScript runtime. It is
|
|
50
|
+
the profile chant implements. A rule with no profile annotation belongs to it.
|
|
51
|
+
|
|
52
|
+
**F-Profile-DataHost.** The `data-host` profile is the specification for an
|
|
53
|
+
evaluator that has no JavaScript runtime: the evaluator a platform written in
|
|
54
|
+
another language embeds, or a JavaScript tool that folds and never runs. It is
|
|
55
|
+
`full` with the following subtractions and alterations, and nothing else.
|
|
56
|
+
|
|
57
|
+
| Rule or family | In `data-host` |
|
|
58
|
+
|---|---|
|
|
59
|
+
| J1, every `F-Eval-*` rule, with ECMAScript coercion reproduced (R10) | applies in full |
|
|
60
|
+
| `S-*`, `F-Div-*`, `F-Direction` | apply in full |
|
|
61
|
+
| J2, `F-Bind` through `F-Total` | apply, with `ι = isolated` always and F-IsolatedRefusal the only fallback: a file that does not fold is an **error**, never a demotion to `run` |
|
|
62
|
+
| J3, `F-Seed` through `F-Verdict`, `F-Capture`, `F-CallLeak`, `F-Memo` | absent. Nothing runs, so nothing taints, and every file's verdict is its own |
|
|
63
|
+
| `F-Val-Fate` | altered: revival is serialization. An envelope is the output; `{__resource}`, `{__intrinsic}` and `{__attrRef}` reach the artifact as data for the host's serializer to map. No constructor and no function is invoked |
|
|
64
|
+
| `F-Eval-CallHelper`, `F-Eval-CallEager`, `F-Host-Admission`, `F-Host-NoSubstitution` | absent: there is nothing to invoke. A registered helper or eager name is an ordinary unresolved identifier |
|
|
65
|
+
| `F-Eval-Tagged`, `F-Eval-CallIntrinsic` | apply; the envelope stays an envelope |
|
|
66
|
+
| `F-Call`, `F-Host-Composite` | interpretation only (step 4). A factory that is not interpretable is an error, never invoked |
|
|
67
|
+
| `S-LocalFunction`, `S-CallLocal` | apply in full: a same-file or project-imported function is called by interpretation, which needs no runtime |
|
|
68
|
+
| `S-ExportDefault` | applies: a default export is the declarator named `default`. In `full` it is permitted, not required, and chant does not yet admit it |
|
|
69
|
+
| `F-Eval-New`, `F-Prebuild`, `F-Count` | permitted, not required. An implementation that supports `new` folds it to a `{__resource}` envelope and binds the same envelope at every reference; one that does not rejects `new` under `F-Eval-Reject`. A fixture that uses `new` is tagged `full` unless it says otherwise |
|
|
70
|
+
| `F-Host-Interface` | the host is a description, not code: the intrinsic registry (item 3), the trust set (item 5), and the serialization mapping for envelopes. Items 1, 2, 4 and 6 are absent |
|
|
71
|
+
| `F-Rule-*` | apply; a rule is code in the evaluator's own language (F-Rule-Supply). A source location in a finding stays optional, since provenance is |
|
|
72
|
+
| `F-Obs-Counters` | trivially satisfied: every counter is zero |
|
|
73
|
+
| `F-NoOwnExecution`, `F-Obs-Report`, `F-Obs-Messages`, `F-Reason` | apply in full |
|
|
74
|
+
|
|
75
|
+
The objective above still holds, read against any JavaScript engine: a
|
|
76
|
+
`data-host` fold of a file and a run of the same file in JavaScript are
|
|
77
|
+
observationally equivalent on every file the profile folds, and a consumer
|
|
78
|
+
may offer both and diff them. Where the profile refuses, the run is not
|
|
79
|
+
consulted.
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## J1. Expression evaluation `Γ, H ⊢ e ⇓ v` (#13)
|
|
84
|
+
|
|
85
|
+
Formalises R1, R3, R6.3–R6.6, R7.3, R10. `Γ = (consts, externals, depth)`
|
|
86
|
+
per R6.6, `depth` is the number of project-local function bodies being
|
|
87
|
+
folded around `e`, 0 at a file's top level. `H = (ρ, helpers)` is the host
|
|
88
|
+
(hosts.md): the intrinsic registry and the authoring-helper allowlist. `v`
|
|
89
|
+
ranges over the value domain (values.md); `⟦e⟧` abbreviates
|
|
90
|
+
`Γ, H ⊢ e ⇓ v`. Every rule that fails does so with a located rejection
|
|
91
|
+
(R9.3). Derived from `fold()` (`fold.ts:896–1300`), `foldResource`,
|
|
92
|
+
`foldTaggedTemplate`, `foldIntrinsicValue`, `callFoldableFunction`, at
|
|
93
|
+
`e4074c17` with chant-v0.63.0's `?.` rules. Rules are keyed to the grammar's
|
|
94
|
+
productions.
|
|
95
|
+
|
|
96
|
+
**F-Eval-Unwrap.** `⟦(e)⟧ = ⟦e as T⟧ = ⟦e satisfies T⟧ = ⟦e!⟧ = ⟦e⟧`. A `!`
|
|
97
|
+
that is part of an optional chain continues the chain (F-Eval-Member).
|
|
98
|
+
|
|
99
|
+
**F-Eval-Literal.** A string or no-substitution template folds to its text; a
|
|
100
|
+
numeric literal to `Number(text)`; `true`, `false`, `null` to themselves.
|
|
101
|
+
**F-Eval-Undefined.** The identifier `undefined` folds to `undefined`.
|
|
102
|
+
|
|
103
|
+
**F-Eval-Ident.** For identifier `n`, in order (R6.6):
|
|
104
|
+
1. `n ∈ consts` with initializer `new …`: if `n ∈ externals`, `⟦n⟧ =
|
|
105
|
+
externals[n]` (the one instance F-Prebuild put there, R4.6); else
|
|
106
|
+
**reject** - re-folding would construct a duplicate (F-Div-SameFileNew).
|
|
107
|
+
2. `n ∈ consts` otherwise: `⟦n⟧ = ⟦consts[n]⟧`.
|
|
108
|
+
3. `n ∈ externals`: if it is callable, a `FoldableFunction` or any function
|
|
109
|
+
the host supplied, **reject** (F-Val-Callable): a callable is reached by
|
|
110
|
+
`new`, by a registered call or tag, or by F-Eval-CallLocal, never as a
|
|
111
|
+
value, and a registered eager name says "call it instead" (#69).
|
|
112
|
+
Otherwise `⟦n⟧ = externals[n]` unchanged, a live object passes through
|
|
113
|
+
(F-Val-Live).
|
|
114
|
+
4. `n = process`: **reject** with the build-parameters message (R8).
|
|
115
|
+
5. Otherwise **reject** `unresolved identifier: n` (F-Reference).
|
|
116
|
+
|
|
117
|
+
**F-Eval-Template.** `⟦`h ${e₁} t₁ … ${eₖ} tₖ`⟧ = h ⧺ ToString(⟦e₁⟧) ⧺ t₁ ⧺ …`
|
|
118
|
+
per R10.4. An envelope among the `⟦eᵢ⟧` is a located rejection at that span,
|
|
119
|
+
because a symbolic value has no string form until the build resolves it.
|
|
120
|
+
|
|
121
|
+
- chant refuses such a span on both paths and reports it at lint time as
|
|
122
|
+
EVL011 (chant#2349), and since v0.69.0 it covers every envelope kind that
|
|
123
|
+
can reach a span, which inventory row L3.23 lists (chant#2397).
|
|
124
|
+
|
|
125
|
+
**F-Eval-Tagged.** For `tag`…``: if `depth > 0`, **reject** (R6.3). If `ρ`
|
|
126
|
+
does not register `tag` with `isTag`, **reject**. Otherwise
|
|
127
|
+
`{__intrinsic: tag, strings, values}` where each interpolation is folded by
|
|
128
|
+
**F-Eval-Interior**.
|
|
129
|
+
|
|
130
|
+
**F-Eval-Interior.** Inside an intrinsic's interior, a tag's interpolations
|
|
131
|
+
or a call form's arguments, if `e` is an identifier, or a `.`/`[]`/`!`
|
|
132
|
+
chain rooted at one, that is not `undefined` and is in neither `consts` nor
|
|
133
|
+
`externals`, then `⟦e⟧ = {__symbol: text(e)}` (F-Val-Symbol-Scope).
|
|
134
|
+
Otherwise `⟦e⟧` as usual. A resolvable root is never symbolised: a cross-file
|
|
135
|
+
binding must reach the intrinsic as the shared object (J3).
|
|
136
|
+
|
|
137
|
+
**F-Eval-Object.** Members in source order into a fresh object: `k: e` sets
|
|
138
|
+
`k ↦ ⟦e⟧` (`k` must be a literal key, else reject); shorthand `n` sets
|
|
139
|
+
`n ↦ ⟦n⟧`; `...e` requires `⟦e⟧` to be a non-null object and copies its own
|
|
140
|
+
enumerable entries in their order, later keys winning (R10.5, R10.6). Any
|
|
141
|
+
other member kind rejects.
|
|
142
|
+
|
|
143
|
+
**F-Eval-Array.** Elements in order into a fresh array; `...e` requires
|
|
144
|
+
`⟦e⟧` to be an array (`Array.isArray`) and splices its elements (R10.6).
|
|
145
|
+
|
|
146
|
+
**F-Eval-Member.** For `o.m` (or `o?.m`), in order:
|
|
147
|
+
1. If `o` is an identifier in `consts` whose initializer is `new …`:
|
|
148
|
+
`⟦o.m⟧ = {__attrRef: {entity: o, attribute: m}}` (R10.3), before anything
|
|
149
|
+
else, the reference is by *name*.
|
|
150
|
+
2. If `m = step` and `o` is a call whose callee is an unclaimed bare
|
|
151
|
+
identifier (L3.20): if `depth > 0` reject; else
|
|
152
|
+
`{__compositeStep: callee, args: ⟦args⟧}`.
|
|
153
|
+
3. Let `x = ⟦o⟧`. If `x` is the chain-short-circuit sentinel: propagate it
|
|
154
|
+
if this access continues the chain, else `undefined`.
|
|
155
|
+
4. If `x` is `null` or `undefined`: with `?.`, yield the sentinel (or
|
|
156
|
+
`undefined` if nothing continues the chain); with `.`, **reject**
|
|
157
|
+
naming `m` and pointing at `?.` (R10.2, chant-v0.63.0).
|
|
158
|
+
5. If `x` is a `{__resource}` envelope: if `o` is a plain identifier,
|
|
159
|
+
`{__attrRef: {entity: o, attribute: m}}`; else **reject** (R10.3,
|
|
160
|
+
chant#1535).
|
|
161
|
+
6. Otherwise `⟦o.m⟧ = x[m]`, a plain index, which on a live instance runs
|
|
162
|
+
its real getter and yields a real `AttrRef` (F-Val-Live).
|
|
163
|
+
|
|
164
|
+
**F-Eval-Index.** `o[k]`: `k` must be a string or numeric literal, else
|
|
165
|
+
reject (EVL003). Then as F-Eval-Member with `m = k`.
|
|
166
|
+
|
|
167
|
+
**F-Eval-Unary.** `⟦!e⟧ = ¬truthy(⟦e⟧)`; `⟦-e⟧ = −ToNumber(⟦e⟧)` (R10.1).
|
|
168
|
+
Any other operator rejects.
|
|
169
|
+
|
|
170
|
+
**F-Eval-Binary.** `&&`, `||`, `??`: fold the left; fold the right **only**
|
|
171
|
+
if ECMAScript would (falsy / truthy / nullish respectively); result per
|
|
172
|
+
ECMAScript (F-Exc-Lazy). Every other supported operator: fold both, then
|
|
173
|
+
apply the ECMAScript operator (R10.1). An unsupported operator rejects.
|
|
174
|
+
|
|
175
|
+
**F-Eval-Conditional.** `⟦c ? t : f⟧`: fold `c`; fold **only** the taken
|
|
176
|
+
branch (F-Exc-Lazy).
|
|
177
|
+
|
|
178
|
+
**F-Eval-New.** For `new C(a₁ … aₙ)`: `C` must be a plain identifier, else
|
|
179
|
+
reject (F-Div-NsNew). If `depth > 0`, reject (R6.3). Then
|
|
180
|
+
(F-Val-Arity): with no arguments, `{__resource: C, props: {}}`; with `a₁`
|
|
181
|
+
an object literal and `n = 1`, `{__resource: C, props: ⟦a₁⟧}`; with `n = 2`
|
|
182
|
+
and both object literals, `{…, attributes: ⟦a₂⟧}`; otherwise
|
|
183
|
+
`{__resource: C, props: p, args: [⟦a₁⟧ … ⟦aₙ⟧]}` where `p` is the first
|
|
184
|
+
object-literal argument folded, or `{}`.
|
|
185
|
+
|
|
186
|
+
**F-Eval-CallHelper.** For `h(args)` with `h ∉ consts` and `h ∈ helpers`: if
|
|
187
|
+
`depth > 0` reject; else `{__helper: h, args: ⟦args⟧}`. Provenance is not
|
|
188
|
+
checked here (F-Div-Provenance); revival checks it.
|
|
189
|
+
|
|
190
|
+
**F-Eval-CallIntrinsic.** For `i(args)` with `i ∉ consts` and `ρ` registering
|
|
191
|
+
`i` with `foldsAsCall`: if `depth > 0` reject; else
|
|
192
|
+
`{__intrinsic: i, args}` with each argument by F-Eval-Interior.
|
|
193
|
+
|
|
194
|
+
**F-Eval-CallLocal.** For `φ(args)` where `externals[φ]` is a
|
|
195
|
+
`FoldableFunction` (checked *after* the two registered shapes, so a
|
|
196
|
+
registered name keeps its meaning):
|
|
197
|
+
1. `φ`'s declaration must satisfy S-FnBody, else reject naming the reason.
|
|
198
|
+
2. If the implementation's call-depth bound is exceeded (F-Depth), reject
|
|
199
|
+
("call depth exceeded"). The value is the implementation's; exhaustion
|
|
200
|
+
is a fallback and never wrong output (#71).
|
|
201
|
+
3. A spread argument rejects. Each `aᵢ` folds in the **caller's** `Γ`.
|
|
202
|
+
4. The body folds in `Γ' = (consts_φ, externals_φ, depth + 1)`, the
|
|
203
|
+
*defining* module's scope, with each parameter bound by removing its
|
|
204
|
+
name from `consts_φ` and setting it in `externals_φ` (so it shadows);
|
|
205
|
+
an object-pattern parameter destructures its argument, which must be an
|
|
206
|
+
object; a missing argument with a default folds the default in `Γ'`.
|
|
207
|
+
5. A concise body yields `⟦body⟧`; a block yields `⟦return-expr⟧` after
|
|
208
|
+
folding each `const` in order, or `undefined` if there is no `return`.
|
|
209
|
+
6. If the result carries a live object (F-Val-Live) that no argument
|
|
210
|
+
carried, `leakedIdentity(φ)` is set (F-CallLeak, J3).
|
|
211
|
+
7. A rejection inside the body is re-thrown at the call, naming `φ`, its
|
|
212
|
+
file, the inner position, and the reason (R9.3).
|
|
213
|
+
|
|
214
|
+
**F-Eval-CallEager.** For `g(args)` with `g ∉ consts` and `ρ` registering `g`
|
|
215
|
+
with `foldsEagerly`: `externals[g]` must be a function, else reject; then
|
|
216
|
+
`⟦g(args)⟧ = g(⟦args⟧)`, evaluated now, the result possibly live (R7.3).
|
|
217
|
+
|
|
218
|
+
**F-Eval-CallMethod.** For `o.m(args)` (or `o?.m(args)`): let `x = ⟦o⟧`.
|
|
219
|
+
If `x` is the chain sentinel, propagate. If `x` is `null`/`undefined`: with
|
|
220
|
+
`?.`, sentinel; with `.`, reject. If `x` is an envelope, reject, else
|
|
221
|
+
`toString` would answer with the placeholder's shape (L3.18). If `x[m]` is
|
|
222
|
+
not a function, reject. Else `⟦o.m(args)⟧ = x[m].apply(x, ⟦args⟧)`.
|
|
223
|
+
|
|
224
|
+
**F-Eval-Function.** An arrow or function expression in value position
|
|
225
|
+
rejects (F-Val-Callable).
|
|
226
|
+
|
|
227
|
+
**F-Eval-Reject.** Any expression matching no rule above rejects with
|
|
228
|
+
`unsupported expression` (S-Reject).
|
|
229
|
+
|
|
230
|
+
### What J1 does not do
|
|
231
|
+
|
|
232
|
+
Revive. Every envelope J1 produces is a finished value (F-Val-Envelope) that
|
|
233
|
+
J2 hands to revival. J1 executes nothing of the folded file's own, its only
|
|
234
|
+
executions are F-Eval-CallEager and F-Eval-CallMethod on real receivers,
|
|
235
|
+
both of which run code the file *imported*, not code it wrote (R2, R7.3).
|
|
236
|
+
|
|
237
|
+
## J2. Per-file verdict `B, ι ⊢ f ⇓ fold(X, L) | run(reason)` (#14)
|
|
238
|
+
|
|
239
|
+
Formalises R4.1, R6.1, R6.6, R2.1, R2.2, R7, R8, R9.3. Derived from
|
|
240
|
+
`tryFoldFileCore` (`fold-import.ts:3539`), `buildExternals` (`:3345`),
|
|
241
|
+
`resolveDeclaratorValue` (`:3522`), `resolveLiveValue` (`:1331`),
|
|
242
|
+
`resolveCallExpression` (`:1416`), at `e4074c17`.
|
|
243
|
+
|
|
244
|
+
`ι ∈ {open, isolated}` is the isolation mode (#36). On `fold`, `X` is the
|
|
245
|
+
complete export namespace (R5.1) and `L ⊆ F` the files whose objects `f`
|
|
246
|
+
captured (F-Capture, J3). The verdict is evaluated per file, without regard
|
|
247
|
+
to other files' verdicts except through F-Import; it is *tentative*, and J3
|
|
248
|
+
makes it final. Every `run(reason)` carries a located reason (R9.3).
|
|
249
|
+
|
|
250
|
+
### Preconditions on the file
|
|
251
|
+
|
|
252
|
+
**F-NotProject.** A file inside chant's own module tree is not project source
|
|
253
|
+
and is `run("chant's own module is not project source")`. Trust (R2.1) is
|
|
254
|
+
about what may be *imported*; this is about what may be *folded*.
|
|
255
|
+
|
|
256
|
+
**F-Scan.** The statement gate (grammar §1). If any exported statement matches
|
|
257
|
+
S-Disqualify, `run(reason)` naming the construct. Otherwise the admitted
|
|
258
|
+
declarators are, in source order: resource, single, destructure,
|
|
259
|
+
named-export, re-export, function. In `data-host` a seventh kind, default,
|
|
260
|
+
is admitted as well (S-ExportDefault).
|
|
261
|
+
|
|
262
|
+
**F-NoExports.** If the gate admits the module but yields **zero**
|
|
263
|
+
declarators, a file with no exports, or only type-only ones, the verdict is
|
|
264
|
+
`run("no foldable resource exports")`. A file has to export something for
|
|
265
|
+
folding to have anything to produce; it is not folded to an empty namespace.
|
|
266
|
+
|
|
267
|
+
### Scope
|
|
268
|
+
|
|
269
|
+
**F-Bind.** `consts` is every top-level `const ⟨Identifier⟩ = e` (R6.6,
|
|
270
|
+
exported or not) whose `e` is not a function; `locals` is every top-level
|
|
271
|
+
binding the resolver may read by name, the same set, plus destructured locals
|
|
272
|
+
from a composite call (`const { a } = C({…})`). Every S-LocalFunction binds
|
|
273
|
+
its name in `externals` to a `FoldableFunction` for the file's own scope
|
|
274
|
+
whether or not it is exported (spec `1.2`). A same-file call therefore
|
|
275
|
+
reaches F-Eval-CallLocal, and a use as a value is step 3's rejection.
|
|
276
|
+
Resolution consults `locals`/`consts` before `externals` (R6.6).
|
|
277
|
+
|
|
278
|
+
**F-Import.** For each named import binding `n` of `f`:
|
|
279
|
+
|
|
280
|
+
- *`params`* (R8): if the build has a binding `P` and `n` is `params` from
|
|
281
|
+
chant's params module, bare `@intentius/chant/params`, or a project path
|
|
282
|
+
that resolves to it, then `externals[n] = P`.
|
|
283
|
+
- *bare specifier, active lexicon package* (R2.1 arm 1): `externals[n]` is
|
|
284
|
+
the package's real export, obtained by importing the already-loaded
|
|
285
|
+
package. "Active" is F-Host-Interface item 5's trust set, the packages
|
|
286
|
+
this build resolved and loaded; a build that supplies no package list has
|
|
287
|
+
an empty set, and every bare specifier then takes the next arm
|
|
288
|
+
(F-Host-Trust, L9.4). A callable export is bound like any other and is
|
|
289
|
+
reached only by the form that invokes it (F-Eval-Ident step 3, #69). A
|
|
290
|
+
lexicon package is never a member of `F` and is never folded.
|
|
291
|
+
- *bare specifier, anything else*: **not resolved**. `n` is absent from
|
|
292
|
+
`externals`; a reference to it is an unresolved identifier (F-Reference).
|
|
293
|
+
- *project specifier* `g`: `g` is folded first (F-Memo: at most once per
|
|
294
|
+
build; F-Cycle if `g` is already on the resolution stack). If
|
|
295
|
+
`B, ι ⊢ g ⇓ fold(X_g, _)` then `externals[n] = X_g[imported]`, and if that
|
|
296
|
+
value has identity, `typeof` object **or function**, then `g ∈ L(f)`
|
|
297
|
+
(F-Capture). If `g`'s verdict is `run`, `n` is not resolved and the reason
|
|
298
|
+
is recorded against `n` for diagnostics only.
|
|
299
|
+
|
|
300
|
+
**F-Namespace.** `import * as ns from "./g"` resolves to a *synthetic plain
|
|
301
|
+
object* of `X_g`'s entries, so `ns.x` indexes it like any object; `g ∈ L(f)`
|
|
302
|
+
if any entry has identity. `import * as ns from "<package>"` is **never
|
|
303
|
+
resolved**, which is why `new ns.Type(...)` is rejected (R6.3, L3.15): the
|
|
304
|
+
class is unreachable through a namespace of a package.
|
|
305
|
+
|
|
306
|
+
**F-Reference.** An identifier absent from `consts`, `locals` and
|
|
307
|
+
`externals` is a located rejection `unresolved identifier: n`, or the
|
|
308
|
+
pointed `process` message (R8). An unresolved *import* that is never
|
|
309
|
+
referenced does not by itself force `run`.
|
|
310
|
+
|
|
311
|
+
### Producing the namespace
|
|
312
|
+
|
|
313
|
+
**F-Prebuild.** Every top-level `const n = new T(…)` of `f` is constructed
|
|
314
|
+
once by revival (F-Val-Fate) before any declarator of `f` is evaluated. This
|
|
315
|
+
holds whether or not `n` is exported. Construction runs in source order and
|
|
316
|
+
`externals[n]` is the instance. A later construction therefore sees an
|
|
317
|
+
earlier one, which is what running the module top to bottom does. A
|
|
318
|
+
construction that **fails** is skipped: `n` stays absent from `externals`, so
|
|
319
|
+
F-Eval-Ident step 1 rejects a reference to it and the file falls back to
|
|
320
|
+
`run`. If `n` is exported, F-Declarator reproduces the failure with its own
|
|
321
|
+
located reason. Under `ι = isolated` each construction is subject to
|
|
322
|
+
F-IsolatedRefusal like any other.
|
|
323
|
+
|
|
324
|
+
**F-Declarator.** Each admitted declarator produces one or more entries of
|
|
325
|
+
`X`. Any failure in any declarator is a failure of the file (F-Total).
|
|
326
|
+
|
|
327
|
+
- *resource* `export const x = new T(…)`: `foldResource` (J1) yields a
|
|
328
|
+
`{__resource}` envelope, which is **revived** into a real instance by the
|
|
329
|
+
class `T` resolves to through `f`'s own imports (R1.2, R7.1). F-Prebuild has
|
|
330
|
+
already built this initializer, and the instance it built is the one bound
|
|
331
|
+
here; a second construction would put two entities where running puts one
|
|
332
|
+
(F-Count). Under `isolated`, a `T` from a project file is
|
|
333
|
+
F-IsolatedRefusal.
|
|
334
|
+
- *single* `export const x = e`: if `e` is a call, F-Call; if a member or
|
|
335
|
+
element access on a call's result, F-Call then index (base must be an
|
|
336
|
+
indexable object); otherwise J1 on `e`, revived.
|
|
337
|
+
- *destructure* `export const { a, b: c } = e`: `e` must resolve to a
|
|
338
|
+
composite instance or an indexable object; each element indexes it.
|
|
339
|
+
- *named-export* `export { a, b as c }`: each local name resolves through
|
|
340
|
+
`locals` then `externals`, by F-Eval-Ident and not by re-folding the
|
|
341
|
+
initializer, so a name bound to a same-file `new` reads F-Prebuild's
|
|
342
|
+
instance rather than building another.
|
|
343
|
+
- *re-export* `export { a } from "./g"`: `X_g[a]`, with `g ∈ L(f)` if it has
|
|
344
|
+
identity, a re-export is a capture.
|
|
345
|
+
- *function* `export function φ`: `X[φ]` is the `FoldableFunction` F-Bind
|
|
346
|
+
bound (R1.3); the body's own foldability is judged at the call, never here.
|
|
347
|
+
- *default* `export default e` (`data-host`, S-ExportDefault): `X["default"]`
|
|
348
|
+
is `⟦e⟧` revived, as *single* is; `import n from "./g"` binds it (F-Import).
|
|
349
|
+
|
|
350
|
+
**F-Call.** A call in declarator position, callee `c`:
|
|
351
|
+
|
|
352
|
+
1. `c` must be a bare identifier; otherwise `run(callExpressionMessage)`.
|
|
353
|
+
2. If `externals[c]` is a `FoldableFunction`, the call is J1's project-local
|
|
354
|
+
call (R6.5): evaluated statically, nothing imported.
|
|
355
|
+
3. Otherwise `c` must be an import binding; else `run`.
|
|
356
|
+
4. If the binding is *interpretable* (R7.2, rules 1–5): the factory body is
|
|
357
|
+
**interpreted** against the defining module's scope; the module is never
|
|
358
|
+
imported; `factoryInterpretations += 1` (R9.2).
|
|
359
|
+
5. Otherwise, under `ι = isolated`, F-IsolatedRefusal unless the binding is
|
|
360
|
+
trusted (R2.1).
|
|
361
|
+
6. Otherwise the module is imported (once per build) and `c` **invoked** with
|
|
362
|
+
the resolved arguments; `factoryInvocations += 1`, and
|
|
363
|
+
`projectFactoryInvocations += 1` if the specifier is a project file.
|
|
364
|
+
Arguments that are live objects pass through unchanged; a `{__attrRef}`
|
|
365
|
+
among them stays symbolic (R1.2, L6.9).
|
|
366
|
+
7. The result must be a `CompositeInstance` or a `Declarable`; otherwise
|
|
367
|
+
`run`.
|
|
368
|
+
|
|
369
|
+
**F-Count.** Within `f`, a composite call reached by several member accesses
|
|
370
|
+
or destructured names is resolved once (`ResolveCtx.memo`); a same-file
|
|
371
|
+
`new` bound to a `const` is constructed once, in source order, by F-Prebuild
|
|
372
|
+
(R4.6).
|
|
373
|
+
|
|
374
|
+
**F-IsolatedRefusal.** Under `ι = isolated`, any step above that would
|
|
375
|
+
resolve *and invoke or import* a binding not on the trust allowlist (R2.1) -
|
|
376
|
+
a project-owned factory, constructor, or intrinsic, is
|
|
377
|
+
`run("isolation")`, even where `B, open ⊢ f ⇓ fold(…)`. Interpretation (step
|
|
378
|
+
4) is not an invocation and is unaffected. Observable: R9.2's
|
|
379
|
+
`projectFactoryInvocations` is zero across every folded file under
|
|
380
|
+
`isolated`.
|
|
381
|
+
|
|
382
|
+
### The verdict
|
|
383
|
+
|
|
384
|
+
**F-Total.** If every declarator succeeded, `fold(X, L)` where `X` maps every
|
|
385
|
+
exported name to its value, plain values included, and `L` is the capture
|
|
386
|
+
set accumulated by F-Import, F-Namespace, re-exports, and F-CallLeak (J3).
|
|
387
|
+
If any declarator failed, `run(reason)` for the **whole file**: no partial
|
|
388
|
+
namespace is ever produced (R4.1).
|
|
389
|
+
|
|
390
|
+
**F-Reason.** `reason` names the declarator and carries the innermost located
|
|
391
|
+
cause; for a failure inside a project-local call it is re-anchored at the
|
|
392
|
+
call site with the callee's file and position in the message (R9.3).
|
|
393
|
+
|
|
394
|
+
### What J2 does not decide
|
|
395
|
+
|
|
396
|
+
Whether `f` *finally* folds. A file with `B, ι ⊢ f ⇓ fold(X, L)` may still be
|
|
397
|
+
`run` after J3, because a file it imports runs (forward taint) or because a
|
|
398
|
+
file it captured from runs (backward taint). J2's `fold` is a proposal; J3
|
|
399
|
+
disposes.
|
|
400
|
+
|
|
401
|
+
---
|
|
402
|
+
|
|
403
|
+
## J3. The identity-taint fixpoint
|
|
404
|
+
|
|
405
|
+
### Why this judgment exists
|
|
406
|
+
|
|
407
|
+
Per-file partial evaluation is unsound in the presence of object identity
|
|
408
|
+
unless something makes it sound. If file `A` folds and file `B` runs, and both
|
|
409
|
+
refer to an entity `e` that `A` produced, then `B`'s real import of `A`
|
|
410
|
+
constructs a second `e`, and the build holds two objects for one entity -
|
|
411
|
+
whose `AttrRef`s cannot both receive a logical name and whose `Ref`s silently
|
|
412
|
+
inline instead of referencing. Every comparable system avoids the problem by
|
|
413
|
+
not having it: compile-time function execution copies values across the
|
|
414
|
+
boundary, per-page static rendering shares no runtime objects across it, and a
|
|
415
|
+
whole-program partial evaluator has one heap ([`prior-art.md`](./prior-art.md)).
|
|
416
|
+
chant keeps per-file granularity **and** shared identity, and this judgment is
|
|
417
|
+
the price.
|
|
418
|
+
|
|
419
|
+
### Setting
|
|
420
|
+
|
|
421
|
+
A build is `B = (F, →, P)`:
|
|
422
|
+
|
|
423
|
+
- `F`, the finite set of discovered project files.
|
|
424
|
+
- `f → g`, `f` imports or re-exports from `g`, by a relative or absolute
|
|
425
|
+
specifier, `g ∈ F`. (`buildProjectImportEdges`. Bare specifiers are not
|
|
426
|
+
edges: a package is never a member of `F`.)
|
|
427
|
+
- `P`, the build-parameter binding (R8).
|
|
428
|
+
|
|
429
|
+
J2 gives each `f` a tentative verdict `w(f) ∈ {fold, run}`; when
|
|
430
|
+
`w(f) = fold` it also gives `X(f)` and `L(f)`.
|
|
431
|
+
|
|
432
|
+
### Definitions
|
|
433
|
+
|
|
434
|
+
**F-Identity.** Two identity predicates appear in this specification and they
|
|
435
|
+
are not interchangeable.
|
|
436
|
+
|
|
437
|
+
- The **entity test** (F-Val-Live) recurses through plain objects and arrays.
|
|
438
|
+
- The **reference test** (F-Import) is `typeof` object **or** function. It does
|
|
439
|
+
not recurse and it admits a plain `{ a: 1 }` that the entity test rejects.
|
|
440
|
+
|
|
441
|
+
The proposition below is about *entities* — the values a build names and
|
|
442
|
+
serializes — and the entity test is the normative one for it. Two structurally
|
|
443
|
+
equal plain objects that no `AttrRef` points at cannot be told apart in the
|
|
444
|
+
output, so
|
|
445
|
+
duplicating one breaks nothing claimed here. Recursion is what makes that test
|
|
446
|
+
usable, a plain object *holding* an entity being itself live.
|
|
447
|
+
|
|
448
|
+
F-Import uses the reference test anyway, as a deliberate over-approximation.
|
|
449
|
+
An imported binding is nearly always an entity, and one `typeof` is cheaper
|
|
450
|
+
than a recursive walk of every resolved import. The cost is coverage rather
|
|
451
|
+
than correctness (F-Direction): a file capturing only plain data from an
|
|
452
|
+
import falls back where it need not. Whether to narrow F-Import to the entity
|
|
453
|
+
test is a question for measurement, since it changes which files fold.
|
|
454
|
+
|
|
455
|
+
F-CallLeak uses the entity test, and has to. A parameter helper returning
|
|
456
|
+
computed plain data must taint nothing, and under the reference test every
|
|
457
|
+
one of them would be a taint source.
|
|
458
|
+
|
|
459
|
+
**F-Capture.** `f` *captures* `g`, written `f ⇝ g`, iff `w(f) = fold` and some
|
|
460
|
+
value in `X(f)` is a **non-primitive** obtained from `X(g)`, a `Declarable`, a
|
|
461
|
+
`CompositeInstance`, or any other object reached through `f`'s resolved
|
|
462
|
+
imports of `g`. Recorded as `g ∈ L(f)`. A primitive is never a capture: it
|
|
463
|
+
has no identity to disagree about (L8.5). The test here is F-Identity's
|
|
464
|
+
reference test, so the relation is an over-approximation of the entity
|
|
465
|
+
relation the proposition needs.
|
|
466
|
+
|
|
467
|
+
**F-CallLeak.** Let `φ` be a project-local function defined in `g` and called
|
|
468
|
+
from `f` during `f`'s fold. If the call **returns** a value that carries a live
|
|
469
|
+
object (R1.4) *which was not already carried in by the arguments*, then the
|
|
470
|
+
call leaked `g`'s identity into `f`: `leakedIdentity(φ)` is set and `g ∈ L(f)`
|
|
471
|
+
(L5.9). The test here is F-Identity's entity test. A function returning only
|
|
472
|
+
plain data never leaks, which is what keeps a parameter helper from tainting
|
|
473
|
+
anything. F-CallLeak is F-Capture through
|
|
474
|
+
invocation rather than through import; the edge it records is the same edge.
|
|
475
|
+
|
|
476
|
+
**F-Memo.** Within one build, `w`, `X` and `L` are computed **at most once per
|
|
477
|
+
file**, and every reference to `X(g)` from any `f` resolves to the same
|
|
478
|
+
objects (`FoldSession.cache`, L8.11). Without this, F-Capture would record an
|
|
479
|
+
edge to a *copy*, and the judgment would be vacuous.
|
|
480
|
+
|
|
481
|
+
**F-Count.** Within one file, a composite call reached through several member
|
|
482
|
+
accesses or destructured names is invoked once, and a same-file construction
|
|
483
|
+
bound to a `const` is built once in source order; every reference reads the
|
|
484
|
+
same object (L8.12, L3.8, R4.6). F-Memo across files, F-Count within one.
|
|
485
|
+
|
|
486
|
+
### The fixpoint
|
|
487
|
+
|
|
488
|
+
```
|
|
489
|
+
Seed(B) = { f ∈ F : w(f) = run } -- F-Seed
|
|
490
|
+
Succ(f) = { g : f → g } ∪ { c : c ⇝ f } -- F-Succ
|
|
491
|
+
T(B) = μX . Seed(B) ∪ ⋃_{f ∈ X} Succ(f) -- F-Taint
|
|
492
|
+
Verdict: fold(f) iff f ∉ T(B) run(f) iff f ∈ T(B) -- F-Verdict
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
**F-Seed.** `Seed(B)` is every file whose tentative J2 verdict is `run`.
|
|
496
|
+
Nothing else is ever in the seed: a foldable file enters `T` only by
|
|
497
|
+
propagation.
|
|
498
|
+
|
|
499
|
+
**F-Succ.** `Succ(f)` is the set of files `f` taints. Read carefully, taint
|
|
500
|
+
flows from a tainted file `f` in two directions at once:
|
|
501
|
+
|
|
502
|
+
- *forward along imports*, to every `g` that `f` imports (`f → g`). If `f`
|
|
503
|
+
runs, its real import of `g` will construct `g`'s entities; a folded `g`
|
|
504
|
+
would be a second copy. So `g` runs too, even if `w(g) = fold`.
|
|
505
|
+
- *backward along captures*, to every `c` that captured `f`'s objects
|
|
506
|
+
(`c ⇝ f`). If `f` runs, the instance `c` captured during its fold is no
|
|
507
|
+
longer the instance the build collects; `c`'s folded result is stale. So `c`
|
|
508
|
+
runs too, and re-obtains everything through real imports.
|
|
509
|
+
|
|
510
|
+
The first revision of `requirements.md` R4.2 stated the forward direction as
|
|
511
|
+
"a file that imports a non-folding file is tainted." That is the *opposite*
|
|
512
|
+
edge, and it is not how taint propagates, it is handled earlier, by J2: a
|
|
513
|
+
file whose import cannot be resolved to a folded `X(g)` fails to fold on its
|
|
514
|
+
own and is in `Seed`. Corrected there in the same commit as this file.
|
|
515
|
+
|
|
516
|
+
**F-Taint.** `T(B)` is the least set containing `Seed(B)` and closed under
|
|
517
|
+
`Succ`.
|
|
518
|
+
|
|
519
|
+
**F-Verdict.** A file's final verdict is `fold` iff it is not in `T(B)`.
|
|
520
|
+
|
|
521
|
+
**F-Fix.** `T(B)` is the least fixpoint of a monotone operator on the finite
|
|
522
|
+
lattice `𝒫(F)`, so it exists and is reached in at most `|F|` iterations. The
|
|
523
|
+
implementation computes it by a worklist from `Seed`; the specification
|
|
524
|
+
states the fixpoint so an implementation is free to compute it otherwise.
|
|
525
|
+
|
|
526
|
+
**F-Cycle.** A cycle in `→` encountered during *resolution* (J2, computing
|
|
527
|
+
`X(g)` for an import that transitively needs `X(f)` again) is a located error
|
|
528
|
+
naming the cycle path (`FoldSession.stack`, L8.9), not an infinite regress.
|
|
529
|
+
The fixpoint itself is indifferent to cycles in `→`.
|
|
530
|
+
|
|
531
|
+
### The property
|
|
532
|
+
|
|
533
|
+
**Proposition (one instance per entity).** In a build whose verdicts are
|
|
534
|
+
`F-Verdict`, every entity `e` produced by the build is represented by exactly
|
|
535
|
+
one object, and every reference to `e`, from a folded file's `X`, from a run
|
|
536
|
+
file's import, from an `AttrRef`, is that object.
|
|
537
|
+
|
|
538
|
+
*Sketch.* Let `e` be produced by file `g`.
|
|
539
|
+
|
|
540
|
+
1. If `g ∉ T(B)`, `g` folded, and `e` is the object in `X(g)`, unique by
|
|
541
|
+
F-Memo and F-Count. Any `f` referring to `e` either folded and captured it
|
|
542
|
+
(`f ⇝ g`), in which case `f ∉ T(B)`, else `g ∈ T(B)` by the backward edge,
|
|
543
|
+
contradiction, and `f`'s reference is the F-Memo object; or `f` runs,
|
|
544
|
+
`f → g`, and then `g ∈ T(B)` by the forward edge, contradiction. So every
|
|
545
|
+
referrer folded and holds the one object.
|
|
546
|
+
2. If `g ∈ T(B)`, `g` runs, and `e` is constructed by real execution. Any `f`
|
|
547
|
+
with `f → g` has `g ∈ Succ(f)`; if `f ∉ T(B)` then `g ∉ T(B)`,
|
|
548
|
+
contradiction; so `f` runs and its import of `g` is the run path's own
|
|
549
|
+
module instance, one per file, by the host module system (or the single
|
|
550
|
+
bundle under isolation, R2.2). Any `f` with `f ⇝ g` is in `T(B)` by the
|
|
551
|
+
backward edge and its folded capture is discarded.
|
|
552
|
+
|
|
553
|
+
No entity therefore has both a folded and a run object alive. ∎
|
|
554
|
+
|
|
555
|
+
The sketch relies on two things outside this judgment: F-Memo/F-Count (the
|
|
556
|
+
implementation's session and per-node memos), and the run path's own
|
|
557
|
+
one-instance-per-module guarantee. The second is why isolation bundles every
|
|
558
|
+
run-fallback file into **one** module graph rather than one per file (R2.2):
|
|
559
|
+
two bundles would be two module instances of a shared import, and the
|
|
560
|
+
proposition would fail on the run side.
|
|
561
|
+
|
|
562
|
+
### Consequences worth stating
|
|
563
|
+
|
|
564
|
+
**Leaf fixes buy nothing while an importer still runs.** Because taint flows
|
|
565
|
+
forward into imports, a perfectly foldable leaf imported by a running file
|
|
566
|
+
runs. This was observed empirically before it was understood (chant#1107:
|
|
567
|
+
coverage fell after a leaf was made foldable) and is a direct corollary of
|
|
568
|
+
F-Succ. An implementation must not "optimise" it away; it is the soundness
|
|
569
|
+
condition.
|
|
570
|
+
|
|
571
|
+
**A folded file can still be forced to run by a file it never imports.** The
|
|
572
|
+
backward edge means `c`'s verdict depends on the verdict of a file `c` only
|
|
573
|
+
*captured from*. Nothing in `c`'s own source predicts it. Fallback reporting
|
|
574
|
+
(R9.3) must therefore be able to say "forced to run because `f` runs and you
|
|
575
|
+
captured its objects," not merely quote a construct in `c`.
|
|
576
|
+
|
|
577
|
+
### Validation status
|
|
578
|
+
|
|
579
|
+
Until chant#2345 (landed 2026-09-10 as chant PR #2364) the differential that
|
|
580
|
+
discharges the objective (chant#1025) compared fold against run **only for
|
|
581
|
+
builds where `T(B) = ∅`**, every file folded, which are exactly the builds
|
|
582
|
+
in which this judgment does nothing. chant#2345 removes that early return: every non-empty corpus entry is now
|
|
583
|
+
built both ways and held to error parity and byte-identical output, with a
|
|
584
|
+
shrink-only allowlist for known divergences.
|
|
585
|
+
|
|
586
|
+
**First full run, 2026-09-10:** 107 entries, 95 fully folded, 12 with at
|
|
587
|
+
least one run-fallback file, **drift 0**, allowlist empty. Every mixed
|
|
588
|
+
entry, where `T(B) ≠ ∅` and both taint directions can fire, agrees fold-vs-
|
|
589
|
+
run. That was the proposition's first differential evidence; before it the
|
|
590
|
+
claim rested on the argument above and on the crash class it was written to
|
|
591
|
+
prevent (chant#1044, #1020).
|
|
592
|
+
|
|
593
|
+
**chant-v0.65.0 (chant#2347, PR #2375) added the adversarial entry**
|
|
594
|
+
`examples/fold-adversarial/`: nine files, each named for one resolution-time
|
|
595
|
+
decision point and carrying this specification's inventory row and rule
|
|
596
|
+
identifier. Four of them exercise this judgment *on purpose*, in one build:
|
|
597
|
+
`taint-run-only-importer.ts` (F-Seed, an early `return` in a project-local
|
|
598
|
+
function puts the importer in the seed), `taint-shared-config.ts` (F-Succ
|
|
599
|
+
forward, the running importer pulls a foldable config back), `taint-
|
|
600
|
+
capturing-sibling.ts` (F-Succ backward, the source of a captured object
|
|
601
|
+
pulls the capturer back), and `taint-independent.ts` (F-Taint/F-Fix, the
|
|
602
|
+
control that no edge reaches, and that must still fold). The entry's own test
|
|
603
|
+
asserts which file folds and which runs, and the differential holds fold-vs-
|
|
604
|
+
run across it. The other five cover F-Div-Nullish, the `?.` short-circuit,
|
|
605
|
+
F-Eval-Ident shadowing, F-Div-SpreadType, and F-Depth.
|
|
606
|
+
|
|
607
|
+
So the evidence is now: twelve incidental mixed entries plus one designed to
|
|
608
|
+
fire both taint directions, all agreeing. The corpus is still chant's own,
|
|
609
|
+
and the adversarial entry is one build; it is evidence, not proof.
|
|
610
|
+
|
|
611
|
+
---
|
|
612
|
+
|
|
613
|
+
## J4. Properties and observables
|
|
614
|
+
|
|
615
|
+
Rules that are about the whole mechanism rather than one judgment.
|
|
616
|
+
|
|
617
|
+
**F-NoOwnExecution (property).** For every file with final verdict `fold`,
|
|
618
|
+
none of its top-level statements is executed by the build. What still
|
|
619
|
+
executes is bounded and named: revival of envelopes (F-Val-Fate),
|
|
620
|
+
F-Eval-CallEager, F-Eval-CallMethod on a real receiver, and, under `open` -
|
|
621
|
+
invocation of an imported factory (J2 F-Call step 6). All of it is code the
|
|
622
|
+
file *imported*; none of it is code the file *wrote*. (Was R2.)
|
|
623
|
+
|
|
624
|
+
**F-Depth.** An implementation bounds three recursions and may choose the values.
|
|
625
|
+
|
|
626
|
+
| Bound | chant | Terminates |
|
|
627
|
+
|---|---|---|
|
|
628
|
+
| `MAX_FUNCTION_CALL_DEPTH` | 32 | nested project-local calls (F-Eval-CallLocal step 2) |
|
|
629
|
+
| `MAX_INTERPRETATION_DEPTH` | 16 | nested factory interpretation |
|
|
630
|
+
| `MAX_RESOLUTION_DEPTH` | 200 | the cross-file resolution stack |
|
|
631
|
+
|
|
632
|
+
- The values must be stated, and they are the implementation's to choose;
|
|
633
|
+
F-Eval-CallLocal step 2 names no number (#71).
|
|
634
|
+
- Exhaustion must produce `run`, never a failure and never a silent change of
|
|
635
|
+
evaluation mode. How the recursion is counted is also the
|
|
636
|
+
implementation's: chant's call-depth bound counts nested folded bodies on
|
|
637
|
+
its expression path, a cross-file recursion is bounded by the engine's
|
|
638
|
+
stack instead, and the overflow is caught and reported as a fallback.
|
|
639
|
+
- chant has met both for all three bounds since v0.68.0, when the
|
|
640
|
+
interpretation bound stopped degrading to invocation under a `fold` verdict
|
|
641
|
+
and began throwing a propagated depth error that names the bound and falls
|
|
642
|
+
the file back to run (chant#2370; was R4.5).
|
|
643
|
+
|
|
644
|
+
**F-Obs-Counters.** A conforming implementation exposes, per build and
|
|
645
|
+
resettable, three non-negative integers: in-process factory/constructor
|
|
646
|
+
invocations performed while folding; of those, how many resolved to
|
|
647
|
+
project-owned code; and how many factory bodies were interpreted instead.
|
|
648
|
+
Names are not normative; the shape is. Under `isolated` the second is zero
|
|
649
|
+
across every folded file (F-IsolatedRefusal). (Was R9.2, R2.3.)
|
|
650
|
+
|
|
651
|
+
**F-Obs-Report.** A conforming implementation reports, per file, the final
|
|
652
|
+
verdict and, for `run`, a located reason (F-Reason). A fallback is not an
|
|
653
|
+
error, which is exactly why an unreported one would make F-NoOwnExecution
|
|
654
|
+
unauditable. Summary versus verbose is presentation. A reason produced by
|
|
655
|
+
J3's backward edge must be able to say "forced to run because `f` runs and
|
|
656
|
+
you captured its objects", since nothing in the file's own source predicts
|
|
657
|
+
it. (Was R9.3.)
|
|
658
|
+
|
|
659
|
+
**F-Obs-Provenance.** Path provenance, which composite parameter produced
|
|
660
|
+
which emitted field, first (innermost) writer wins, is an *optional*
|
|
661
|
+
capability outside the equivalence objective: the run path does not
|
|
662
|
+
uniformly produce it, so requiring it would oblige fold and run to agree on
|
|
663
|
+
something one side lacks. Conformance reports whether it is supported.
|
|
664
|
+
(Was R9.1.)
|
|
665
|
+
|
|
666
|
+
**F-Obs-Messages.** Message *stability* is not normative. Location and rule
|
|
667
|
+
identifier are (F-Reason). One-builder-per-kind is a property of one
|
|
668
|
+
implementation's tooling; a second implementation cannot share strings.
|
|
669
|
+
(Was R9.4.)
|
|
670
|
+
|
|
671
|
+
---
|
|
672
|
+
|
|
673
|
+
## Rationale
|
|
674
|
+
|
|
675
|
+
Non-normative. The reasoning that motivated each rule, carried over from the retired `requirements.md` (#46). Keyed by the rule(s) each note supports.
|
|
676
|
+
|
|
677
|
+
**F-NoOwnExecution** *(was R2. Folding executes none of the folded file's own statements, and the spec must say exactly that)*
|
|
678
|
+
|
|
679
|
+
The claim is narrower than "no execution", and stating it loosely is the single
|
|
680
|
+
easiest way to write a specification that is either false or useless.
|
|
681
|
+
|
|
682
|
+
**Guaranteed:** none of the top-level statements of the file being folded are
|
|
683
|
+
executed. That is the whole of the guarantee.
|
|
684
|
+
|
|
685
|
+
**Still executes:** revival (R1.2) and eager evaluation (R7.3). A `__resource`
|
|
686
|
+
envelope names a constructor; the bridge reads the folding file's own `import`
|
|
687
|
+
declarations, imports *that* module, and calls the real class with the folded
|
|
688
|
+
arguments. The implementation's own justification: the run path imports the
|
|
689
|
+
same module to obtain the same class, so the only thing skipped is the file's
|
|
690
|
+
own statements (`fold-import.ts` module doc).
|
|
691
|
+
|
|
692
|
+
**F-IsolatedRefusal** *(was R2.2. Isolation changes what folds, so it is part of the mechanism)*
|
|
693
|
+
|
|
694
|
+
Under sandboxed execution a fold whose revival would invoke *project-owned*
|
|
695
|
+
code is refused and the file falls back to run (L9.5,
|
|
696
|
+
INTENTIUS/chant#1093). The fold/run decision is therefore parameterized by
|
|
697
|
+
whether project code may execute in this process.
|
|
698
|
+
|
|
699
|
+
**Decided (#36): isolation is an optional capability, modelled in the
|
|
700
|
+
judgment.** J2 takes an isolation mode `ι ∈ {open, isolated}`; under
|
|
701
|
+
`isolated`, a file whose fold would require invoking project-owned code is
|
|
702
|
+
`run`, not `fold` (F-IsolatedRefusal, judgments.md). An implementation
|
|
703
|
+
declares whether it supports `isolated`; conformance reports it separately
|
|
704
|
+
and does not require it. The observable is R9.2's `projectFactoryInvocations`
|
|
705
|
+
counter, which must be zero for every folded file under `isolated`. Nix
|
|
706
|
+
import-from-derivation is the cited precedent for evaluation escaping into a
|
|
707
|
+
contained execution and resuming (`prior-art.md`). The reference
|
|
708
|
+
implementation is not required to ship a sandbox before submission.
|
|
709
|
+
|
|
710
|
+
**F-Obs-Counters** *(was R2.3. The observable)*
|
|
711
|
+
|
|
712
|
+
`FoldExecutionCounts` (L10.1), `factoryInvocations`,
|
|
713
|
+
`projectFactoryInvocations`, `factoryInterpretations`, is how this
|
|
714
|
+
requirement is checked rather than trusted. #43 decides what a conforming
|
|
715
|
+
implementation must expose.
|
|
716
|
+
|
|
717
|
+
---
|
|
718
|
+
|
|
719
|
+
**J3** *(was R4. The decision is per file, total, and closed under a bidirectional fixpoint)*
|
|
720
|
+
|
|
721
|
+
|
|
722
|
+
|
|
723
|
+
**F-Total, F-Scan** *(was R4.1. All or nothing, per file, at the normative entry point)*
|
|
724
|
+
|
|
725
|
+
Two entry points exist and the first revision conflated them. `tryFoldFile`
|
|
726
|
+
returns a `FoldFileResult` (L8.1): ok with the complete export namespace, or a
|
|
727
|
+
reason. One unrecognized export disqualifies the file (L8.2). `foldModule`
|
|
728
|
+
(L8.3) is per-export, carries an ok/false entry per declaration, and silently
|
|
729
|
+
skips non-`new` exports. **The per-file entry point is normative**; the
|
|
730
|
+
per-export one is a diagnostic surface, and the spec must say so.
|
|
731
|
+
|
|
732
|
+
The reason fallback is per-module rather than per-declaration is stated in the
|
|
733
|
+
statement gate itself (L1.7) and belongs in the spec: an unfoldable export can
|
|
734
|
+
reference or be referenced by a foldable one in ways only running proves safe.
|
|
735
|
+
|
|
736
|
+
**F-Succ, F-Capture, F-CallLeak** *(was R4.2. Contagion runs in both directions, and through calls)*
|
|
737
|
+
|
|
738
|
+
`planFoldTaint` (L8.6–L8.8):
|
|
739
|
+
|
|
740
|
+
- **Forward, along imports.** A tainted file taints every file it imports or
|
|
741
|
+
re-exports from: if `f` runs, its real import of `g` constructs `g`'s
|
|
742
|
+
entities, and a folded `g` would be a second copy, so `g` runs even if it
|
|
743
|
+
would have folded alone. (The first revision stated this edge backwards -
|
|
744
|
+
"an importer of a non-folding file is tainted", which is not the taint walk
|
|
745
|
+
at all but J2's resolution failure putting the importer in the seed.
|
|
746
|
+
Corrected with judgments.md J3.)
|
|
747
|
+
- **Reverse.** A file whose *objects were captured* by an already-folded file
|
|
748
|
+
taints the capturer. `liveSources` records only non-primitive captures
|
|
749
|
+
(L8.5), a primitive has no identity to disagree about.
|
|
750
|
+
- **Through calls.** A project-local function whose call *returns* a live
|
|
751
|
+
object the body produced, not one merely passed through the arguments -
|
|
752
|
+
records the same taint edge (L5.9, `leakedIdentity`). Identity propagates
|
|
753
|
+
through invocation, not only through import, and the first revision missed
|
|
754
|
+
this entirely.
|
|
755
|
+
|
|
756
|
+
**F-Taint, F-Fix** *(was R4.3. The fixpoint and its termination)*
|
|
757
|
+
|
|
758
|
+
Seed with every file that would not fold on its own; walk the union of forward
|
|
759
|
+
and reverse edges to closure. Monotone over a finite file set, so it
|
|
760
|
+
terminates. State it as a least fixpoint, not as the worklist.
|
|
761
|
+
|
|
762
|
+
**F-Cycle** *(was R4.4. Cycles are a located error, not divergence)*
|
|
763
|
+
|
|
764
|
+
`FoldSession.stack` (L8.9) detects a genuine reference cycle and reports the
|
|
765
|
+
path.
|
|
766
|
+
|
|
767
|
+
**F-Depth** *(was R4.5. Three depth bounds, all of which decide fold versus run)*
|
|
768
|
+
|
|
769
|
+
`MAX_FUNCTION_CALL_DEPTH = 32` (L5.8), `MAX_INTERPRETATION_DEPTH` (L7.8),
|
|
770
|
+
`MAX_RESOLUTION_DEPTH` (L8.10). Each terminates a different recursion and each
|
|
771
|
+
turns exhaustion into a fallback. No requirement in the first revision
|
|
772
|
+
mentioned them. The spec must either fix the bounds or say they are
|
|
773
|
+
implementation-defined and that exceeding one is a fallback, never wrong
|
|
774
|
+
output.
|
|
775
|
+
|
|
776
|
+
**F-Prebuild** *(added by #68, from the corpus cross-check of #25)*
|
|
777
|
+
|
|
778
|
+
F-Eval-Ident step 1 always read its answer out of `externals` and called it
|
|
779
|
+
"the one instance J2 pre-built", and no rule of J2 built it. The rules that
|
|
780
|
+
produce the namespace were F-Bind through F-Declarator, and every arm among
|
|
781
|
+
them that wrote `externals` wrote an import. A reader with L5.2 in hand
|
|
782
|
+
concluded step 1 always rejects, and the reference implementation, written
|
|
783
|
+
from the text, did exactly that; chant, written first, had the pre-pass all
|
|
784
|
+
along (`preresolveResourceConsts`, chant#1169). The rule was lost in the
|
|
785
|
+
abstraction from code to specification, and 22 corpus files disagreed
|
|
786
|
+
because of one missing sentence.
|
|
787
|
+
|
|
788
|
+
**F-Count** *(was R4.6. Evaluation count is observable semantics)*
|
|
789
|
+
|
|
790
|
+
A composite call reached through several member accesses or destructured names
|
|
791
|
+
is invoked **exactly once** (L8.12, `ResolveCtx.memo`), explicitly "matching
|
|
792
|
+
what actually running the file would do." A named same-file construction is
|
|
793
|
+
built once, in source order, and every reference reads the same object (L3.8).
|
|
794
|
+
These are not optimizations; an implementation that invoked twice would produce
|
|
795
|
+
two entities where running produces one.
|
|
796
|
+
|
|
797
|
+
---
|
|
798
|
+
|
|
799
|
+
**F-Memo** *(was R5. A cross-file entity has exactly one instance per build)*
|
|
800
|
+
|
|
801
|
+
Every referrer of a folded file must observe the same objects. A per-build
|
|
802
|
+
session memoizes each file's fold so a file imported by many is folded exactly
|
|
803
|
+
once (L8.11), and revival passes live objects through unchanged rather than
|
|
804
|
+
reconstructing them (L6.1), the generic walk would destroy the identity it
|
|
805
|
+
exists to preserve.
|
|
806
|
+
|
|
807
|
+
**F-Total** *(was R5.1. The exported namespace is complete, and the statement gate is why)*
|
|
808
|
+
|
|
809
|
+
A successful fold yields every exported name's value, not only the
|
|
810
|
+
entity-valued ones (L8.4). This holds *because* the statement gate (R6.1)
|
|
811
|
+
disqualifies any file with an unrecognized export. The first revision stated
|
|
812
|
+
the property without its enforcement.
|
|
813
|
+
|
|
814
|
+
**F-Succ** *(was R5.2. R5 is why R4.2 exists)*
|
|
815
|
+
|
|
816
|
+
The reverse and through-call taint edges are the consequence of this
|
|
817
|
+
requirement: single-instance-per-build cannot hold if one side of a sharing
|
|
818
|
+
relationship folds while the other runs.
|
|
819
|
+
|
|
820
|
+
---
|
|
821
|
+
|
|
822
|
+
**F-Eval-New, F-Eval-Tagged, F-Eval-CallHelper, F-Eval-CallIntrinsic, F-Eval-Member step 2** *(was R6.3. Admissibility depends on where the expression sits)*
|
|
823
|
+
|
|
824
|
+
Five constructs that fold at a file's top level are refused inside a folded
|
|
825
|
+
function body: `new`, a tagged template, a helper call, an intrinsic call, and
|
|
826
|
+
`.step` (L3.16, `functionBodyDepth`). Each produces an envelope revived against
|
|
827
|
+
the *caller's* imports, which is not the scope the body was written in.
|
|
828
|
+
`new ns.Type(...)` is refused everywhere: a namespace-qualified constructor
|
|
829
|
+
cannot be resolved through named imports (L3.15).
|
|
830
|
+
|
|
831
|
+
**F-Eval-Ident** *(was R6.4. Shadowing)*
|
|
832
|
+
|
|
833
|
+
`consts` is consulted before `externals` (L5.3). A local `const` defeats a
|
|
834
|
+
registered helper or intrinsic name, the file's own binding wins, so a local
|
|
835
|
+
`Ref` is not the lexicon's. A parameter or body binding shadows a module-level
|
|
836
|
+
const of the same name.
|
|
837
|
+
|
|
838
|
+
**F-Bind, F-Eval-Ident, F-Eval-CallLocal** *(was R6.6. What a binding is, and the order names resolve in)*
|
|
839
|
+
|
|
840
|
+
**A top-level binding is a `const` with an identifier name and an initializer**
|
|
841
|
+
(`collectConsts`, L5.1). Nothing else is: a top-level destructured `const`
|
|
842
|
+
(`const { a } = …`) does not bind `a` for the folder, and a non-exported
|
|
843
|
+
`const` is collected exactly like an exported one. The spec must say this
|
|
844
|
+
because the first is a surprise, the declaration is valid TypeScript and the
|
|
845
|
+
name is simply invisible.
|
|
846
|
+
|
|
847
|
+
**Lookup order is `consts`, then `externals`** (L5.2, `fold.ts` identifier
|
|
848
|
+
branch). A name in the file's own `consts` is never looked up in `externals`,
|
|
849
|
+
which is the mechanism behind R6.4's shadowing rule and must be stated as the
|
|
850
|
+
order rather than only as its consequence.
|
|
851
|
+
|
|
852
|
+
**A project-local function's body folds in the defining module's scope**
|
|
853
|
+
(L5.5, `callFoldableFunction`): the arguments fold in the caller's `consts`/
|
|
854
|
+
`externals`; the body folds against a copy of the *callee's* `consts` and
|
|
855
|
+
`externals`, with each parameter bound by deleting the name from that copy of
|
|
856
|
+
`consts` and setting it in `externals` (so a parameter shadows a module-level
|
|
857
|
+
const of the same name); `externals` are read live, so a function declared
|
|
858
|
+
before a const it reads still sees the const's value. R7.2 states the same
|
|
859
|
+
scope rule for composite factories; this is its counterpart for plain
|
|
860
|
+
functions, and without it a helper in `lib/` reading `lib/`'s own imports is
|
|
861
|
+
unspecified.
|
|
862
|
+
|
|
863
|
+
---
|
|
864
|
+
|
|
865
|
+
**F-Call** *(was R7. There are three evaluation modes, not two)*
|
|
866
|
+
|
|
867
|
+
The first revision described fold-to-envelope and revival. There is a third,
|
|
868
|
+
and it is the one that makes folding under isolation possible.
|
|
869
|
+
|
|
870
|
+
**F-Call step 4** *(was R7.2. Interpret, never importing the defining module)*
|
|
871
|
+
|
|
872
|
+
A composite factory is *interpreted* (L7.1–L7.8) when: (1) the calling file
|
|
873
|
+
imports it from a project file, by text, never a package; (2) the defining
|
|
874
|
+
module has `export const N = Composite(<fn>, "N")` with `Composite` bound in
|
|
875
|
+
*that* module to chant's own; (3) `<fn>` takes at most one plainly-bound
|
|
876
|
+
parameter; (4) its body is a concise expression or `const`s then a final
|
|
877
|
+
`return`; (5) every expression is in the subset, extended with `new` in value
|
|
878
|
+
position and calls through a bare identifier. A body that references one of
|
|
879
|
+
its module's own module-level resources declines (L7.6), that resource is a
|
|
880
|
+
singleton the run path shares, and interpretation would not, and
|
|
881
|
+
`constResolvesToResource` follows alias chains so it cannot be smuggled in
|
|
882
|
+
(L7.7). The defining module is never imported; members are built by the
|
|
883
|
+
lexicon's constructors from the folded props.
|
|
884
|
+
|
|
885
|
+
**F-Eval-CallEager, F-Eval-CallMethod** *(was R7.3. Evaluate eagerly)*
|
|
886
|
+
|
|
887
|
+
A lexicon function registered with `intrinsicCallFoldsEagerly` (L2.13) is
|
|
888
|
+
called at fold time with folded arguments rather than enveloped, because its
|
|
889
|
+
ordinary use coerces the result to string during folding, before any revival
|
|
890
|
+
would run. A method call on a real receiver (L2.14) is the same mode: the
|
|
891
|
+
receiver is the same object either way, so calling it is what running would
|
|
892
|
+
do. Both are places where fold time and revival time are observably different,
|
|
893
|
+
and the spec must name them as such.
|
|
894
|
+
|
|
895
|
+
---
|
|
896
|
+
|
|
897
|
+
**F-Import** *(was R8. Build parameters are an input to folding)*
|
|
898
|
+
|
|
899
|
+
`FoldSession.buildParams` (L5.11) is consulted for exactly one bare specifier:
|
|
900
|
+
a named `params` import resolving to chant's `params` module. `params.<name>`
|
|
901
|
+
folds to a literal. A bare `process` reference is refused with a message naming
|
|
902
|
+
this mechanism as the alternative (L3.7). The `params` object is tracked by
|
|
903
|
+
identity like an entity (`fold-import.ts:3141`).
|
|
904
|
+
|
|
905
|
+
The objective is stated pointwise at a binding because of this requirement. An
|
|
906
|
+
implementation without build parameters satisfies it trivially; one with them
|
|
907
|
+
must say how the binding enters and that it is recorded.
|
|
908
|
+
|
|
909
|
+
---
|
|
910
|
+
|
|
911
|
+
**J4** *(was R9. Outputs beyond values, and what must be observable)*
|
|
912
|
+
|
|
913
|
+
Folding produces more than the value tree, and the first revision had no place
|
|
914
|
+
for any of it. Four things, each decided here as a recommendation for the
|
|
915
|
+
normative text (#43).
|
|
916
|
+
|
|
917
|
+
**F-Obs-Provenance** *(was R9.1. Provenance is an optional capability, outside the equivalence objective)*
|
|
918
|
+
|
|
919
|
+
`setPathProvenance` (L10.2) records, during fold, which composite parameter
|
|
920
|
+
produced which emitted field; the first (innermost) writer wins. It is real
|
|
921
|
+
output and it is useful. It is **not** part of the objective: the run path
|
|
922
|
+
obtains provenance, where it has any, by a different mechanism, and making
|
|
923
|
+
provenance normative would oblige fold and run to agree on a thing the run path
|
|
924
|
+
does not uniformly produce.
|
|
925
|
+
|
|
926
|
+
So: a conforming implementation *may* expose path provenance; conformance
|
|
927
|
+
reports whether it does; the equivalence claim is over serialized output only.
|
|
928
|
+
The first-writer-wins rule is stated for implementations that do expose it, so
|
|
929
|
+
two of them agree on which writer.
|
|
930
|
+
|
|
931
|
+
**F-Obs-Counters** *(was R9.2. The no-execution observable is normative in shape, not in name)*
|
|
932
|
+
|
|
933
|
+
R2 is checkable only through `FoldExecutionCounts` (L10.1). A conforming
|
|
934
|
+
implementation must expose, per build and resettable: the number of factory or
|
|
935
|
+
constructor invocations performed in-process during folding; of those, how
|
|
936
|
+
many resolved to project-owned code; and how many factory bodies were
|
|
937
|
+
interpreted instead. Three non-negative integers. That is sufficient for a
|
|
938
|
+
conformance harness to assert "zero project-owned invocations" under isolation
|
|
939
|
+
(R2.2) and to measure interpretation coverage (R7.2). Names are not normative.
|
|
940
|
+
|
|
941
|
+
**F-Obs-Report, F-Reason** *(was R9.3. Fallback reporting is normative)*
|
|
942
|
+
|
|
943
|
+
A fallback is not an error (R4.1), which is exactly why it must be reported: an
|
|
944
|
+
unreported fallback is indistinguishable from a fold, and R2's guarantee
|
|
945
|
+
becomes unauditable. A conforming implementation must report, per file, the
|
|
946
|
+
decision taken and, for a fallback, a located reason (L10.3). The reason's
|
|
947
|
+
wording is unconstrained.
|
|
948
|
+
|
|
949
|
+
**Which location, when there are two.** A rejection inside a project-local
|
|
950
|
+
function body has a position in the callee's file and a call site in the
|
|
951
|
+
caller's. The implementation re-throws at the **call site**, naming the callee,
|
|
952
|
+
its file, the position inside it, and the reason, preserving the rule
|
|
953
|
+
identifier (L5.10, `callFoldableFunction`). The reported location is the call;
|
|
954
|
+
the callee position travels in the message. The spec must say which is
|
|
955
|
+
primary, because R-spec.3's "located" is otherwise ambiguous for exactly this
|
|
956
|
+
case. Whether the report is summarized or verbose by
|
|
957
|
+
default is presentation, not conformance.
|
|
958
|
+
|
|
959
|
+
**F-Obs-Messages** *(was R9.4. Message stability is not normative; location and rule are)*
|
|
960
|
+
|
|
961
|
+
chant builds every rejection message of a given kind through one shared
|
|
962
|
+
builder so two sites cannot drift (L10.5). That is a property of one
|
|
963
|
+
implementation's tooling: a second implementation in another language cannot
|
|
964
|
+
share strings, and requiring it would make the wording normative by the back
|
|
965
|
+
door. What conformance requires is already R-spec.3, the node and the rule
|
|
966
|
+
identifier, and `FoldError` carries exactly those (L10.4).
|
|
967
|
+
|
|
968
|
+
---
|
|
969
|
+
|
|
970
|
+
**J1** *(was R10. Semantics a second implementation cannot guess)*
|
|
971
|
+
|
|
972
|
+
R1 defines the domain and R3 defines membership. Neither says what an operator
|
|
973
|
+
*means*. "As ECMAScript" is the obvious discharge and it is available for most
|
|
974
|
+
of this section, but not all of it, and the places where the implementation
|
|
975
|
+
departs from ECMAScript are exactly the ones a second implementation would get
|
|
976
|
+
wrong by assuming it.
|
|
977
|
+
|
|
978
|
+
**F-Eval-Unary, F-Eval-Binary** *(was R10.1. Operators are ECMAScript's, on the folded operand values)*
|
|
979
|
+
|
|
980
|
+
The supported binary operators (L2.8, L3.14): `+` `-` `*` `/` `===` `!==` `>`
|
|
981
|
+
`<` `>=` `<=`, and the lazy `&&` `||` `??` (L3.13). Unary `!` and `-`
|
|
982
|
+
(L3.12). Conditional `?:`. For every one of these the implementation applies
|
|
983
|
+
the host JavaScript operator to the folded values, so the semantics, including
|
|
984
|
+
`+`'s string-versus-numeric dispatch, relational comparison on strings, and
|
|
985
|
+
`&&`/`||` returning an operand rather than a boolean, are ECMAScript's. The
|
|
986
|
+
spec should say so per operator and require an implementation in another
|
|
987
|
+
language to reproduce ECMAScript coercion for these operators, not its host's.
|
|
988
|
+
|
|
989
|
+
**F-Eval-Member step 4** *(was R10.2. Member access on `null`/`undefined` refuses; optional chaining short-circuits)*
|
|
990
|
+
|
|
991
|
+
Resolved by chant-v0.63.0 (INTENTIUS/chant#2328, commit `8ce54e5e`). A plain
|
|
992
|
+
property or element read whose object folds to `null`/`undefined` is
|
|
993
|
+
**refused** with a located rejection naming the member and pointing at `?.`
|
|
994
|
+
(L3.10); the file falls back to run, where the same expression throws a
|
|
995
|
+
`TypeError`. Fold produces no output and run produces an error, equivalence
|
|
996
|
+
in the sense of R4.1's "a fallback is not wrong output". `?.` is implemented
|
|
997
|
+
with ECMAScript short-circuit semantics: a nullish object under `?.` yields a
|
|
998
|
+
chain-short-circuit sentinel that propagates through the remainder of the
|
|
999
|
+
chain, further member and element reads, `!` non-null assertions, and a
|
|
1000
|
+
`?.()` method call, and resolves to `undefined` where the chain ends (L3.21,
|
|
1001
|
+
L3.22). So "as ECMAScript" *is* now available for member access, and the
|
|
1002
|
+
departure the first revision of this clause recorded no longer exists. chant's
|
|
1003
|
+
shape classifier still admits both forms (the object's value is resolution),
|
|
1004
|
+
which is the R3.1 direction; it is added to R3.1's divergence list.
|
|
1005
|
+
|
|
1006
|
+
**F-Eval-Member steps 1, 5** *(was R10.3. Attribute references are produced by two rules and refused by a third)*
|
|
1007
|
+
|
|
1008
|
+
Property or element access on an identifier bound to a same-file `new` yields
|
|
1009
|
+
`{__attrRef: {entity, attribute}}` keyed by the identifier (L3.9). Access on a
|
|
1010
|
+
value that folded to a `{__resource}` envelope yields the same, but only when
|
|
1011
|
+
the object expression is a plain identifier, any other shape is refused,
|
|
1012
|
+
because there is no name to key the reference on and silently indexing the
|
|
1013
|
+
envelope produced wrong output (L3.11, chant#1535).
|
|
1014
|
+
|
|
1015
|
+
**F-Eval-Template** *(was R10.4. Template spans coerce by ECMAScript `ToString`, and the spec must say what that does to an envelope)*
|
|
1016
|
+
|
|
1017
|
+
`String(fold(span))` (L3.2). For scalars that is ECMAScript. For a symbolic
|
|
1018
|
+
envelope it is `"[object Object]"`, and, verified, the run path produces the
|
|
1019
|
+
same, because `AttrRef` defines no `toString`. So fold and run *agree* and the
|
|
1020
|
+
output is silently wrong on both; the implementation guards the
|
|
1021
|
+
eager-intrinsic case for exactly this hazard (R7.3) and not the template span.
|
|
1022
|
+
The spec should refuse an envelope in a plain template span rather than
|
|
1023
|
+
inherit `ToString`; chant's side of it is tracked as a lint gap.
|
|
1024
|
+
|
|
1025
|
+
**F-Eval-Object, F-Eval-Array** *(was R10.5. Key and element ordering are ECMAScript's, and byte-identity depends on it)*
|
|
1026
|
+
|
|
1027
|
+
Object literals evaluate members in source order; spread is `Object.assign`,
|
|
1028
|
+
so spread keys land in the source's insertion order and a later key wins
|
|
1029
|
+
(L3.3). Arrays preserve element order; spread splices in place. This is
|
|
1030
|
+
ECMAScript object-literal evaluation and "as ECMAScript" is available here.
|
|
1031
|
+
|
|
1032
|
+
Whether order is *observable* depends on the emitter, verified in chant: a
|
|
1033
|
+
lexicon whose serializer produces JSON is re-stringified through
|
|
1034
|
+
`sortedJsonReplacer` (`packages/core/src/utils.ts:30`), which sorts every
|
|
1035
|
+
object's keys, and YAML for those lexicons is derived from that sorted JSON
|
|
1036
|
+
(`cli/commands/build.ts:743–746`), so folded key order never reaches the
|
|
1037
|
+
output. Six lexicons serialize YAML themselves (fountain, docker, gitlab, k8s,
|
|
1038
|
+
gcp, github) and their text is emitted as-is, so for them the walker's
|
|
1039
|
+
insertion order *is* the output order. The rule must therefore be stated: an
|
|
1040
|
+
implementation targeting a host that preserves order would fail byte-identity
|
|
1041
|
+
with a different key order, and nothing in the objective says which hosts
|
|
1042
|
+
those are.
|
|
1043
|
+
|
|
1044
|
+
**F-Eval-Object, F-Eval-Array** *(was R10.6. Spread departs from ECMAScript in one direction)*
|
|
1045
|
+
|
|
1046
|
+
Object spread requires a non-null object; array spread requires
|
|
1047
|
+
`Array.isArray` (L3.4, L3.5). ECMAScript array spread accepts any iterable -
|
|
1048
|
+
`[...'ab']` is `['a','b']` there and a rejection here. A deliberate narrowing,
|
|
1049
|
+
and one a second implementation would not infer from "as ECMAScript".
|