candor-ts 0.39.0 → 0.39.2
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/package.json +1 -1
- package/scan.mjs +295 -9
package/package.json
CHANGED
package/scan.mjs
CHANGED
|
@@ -242,7 +242,29 @@ const noteRefusalTarget = (t) => { if (refusalTarget === null) refusalTarget =
|
|
|
242
242
|
const writeRefusalMarker = (why) => {
|
|
243
243
|
if (!refusalPrefix) return;
|
|
244
244
|
try {
|
|
245
|
-
|
|
245
|
+
// ⟨SOUNDNESS R520⟩ THE MARKER MAY CREATE ITS OWN `.candor`, AND NOT A TREE ABOVE IT.
|
|
246
|
+
//
|
|
247
|
+
// `recursive: true` made every refusal a `mkdir -p` of the prefix's WHOLE path, and that prefix is
|
|
248
|
+
// derived from the first bare token — so `candor-ts nonexistent-target /also-bogus` exited 2
|
|
249
|
+
// (correctly) and left `./nonexistent-target/.candor/report.refused.json` in the operator's cwd: a
|
|
250
|
+
// directory tree named after a TYPO. Found as litter in the candor umbrella's own checkout, by no
|
|
251
|
+
// gate. candor-java and candor-swift create nothing here, so this is two engines disagreeing with
|
|
252
|
+
// two and no clause has to move.
|
|
253
|
+
//
|
|
254
|
+
// The distinction the recursive form missed: ⟨0.28⟩'s fail-closed sink exists to REPLACE a previous
|
|
255
|
+
// run's document, so a later reader cannot mistake a stale report for a current one. A target that
|
|
256
|
+
// does not exist never held a report, so the write has nothing to fail closed OVER — it is pure
|
|
257
|
+
// side effect. Where the target DOES exist the marker still lands, `.candor` and all: CLI-1b's own
|
|
258
|
+
// control asserts that on a FRESH project, which is why this is not a blanket `existsSync` on the
|
|
259
|
+
// dirname — that spelling would mute the marker on exactly the case the rung is FOR.
|
|
260
|
+
//
|
|
261
|
+
// A bare non-recursive `mkdirSync` would encode the same rule (ENOENT into the catch below), but it
|
|
262
|
+
// would also throw EEXIST on the ordinary path, so both conditions are spelled out instead.
|
|
263
|
+
const markerDir = path.dirname(refusalPrefix);
|
|
264
|
+
if (!fs.existsSync(markerDir)) {
|
|
265
|
+
if (!fs.existsSync(path.dirname(markerDir))) return; // nothing to fail closed OVER
|
|
266
|
+
fs.mkdirSync(markerDir);
|
|
267
|
+
}
|
|
246
268
|
fs.writeFileSync(`${refusalPrefix}.refused.json`, JSON.stringify({
|
|
247
269
|
candor: { spec: SPEC_VERSION }, refused: true,
|
|
248
270
|
prefix: refusalPrefix, target: refusalTarget ?? ".", reason: String(why),
|
|
@@ -4582,11 +4604,38 @@ function contextualInterfaceDeclsFor(node, depth = 0) {
|
|
|
4582
4604
|
}
|
|
4583
4605
|
return direct;
|
|
4584
4606
|
}
|
|
4607
|
+
// ⟨R64⟩'s unit local, spelled ONCE. `ownedByDecoratorArgument` below and `decoratorArgUnit` itself are
|
|
4608
|
+
// its only two readers; a second literal spelling is a second thing to keep in step. Declared above
|
|
4609
|
+
// `walkStructural` rather than beside `decoratorArgUnit`, because that walk RUNS at this point in module
|
|
4610
|
+
// evaluation and a `const` declared further down would be in its temporal dead zone.
|
|
4611
|
+
const DECORATOR_ARG_LOCAL = "<decorator-arg>@";
|
|
4612
|
+
/** ⟨SOUNDNESS R531, conformance PART 82⟩ Is this node's attribution owner ⟨R64⟩'s synthesized
|
|
4613
|
+
* decorator-ARGUMENT unit? Asks `enclosing` — the authority — rather than re-deriving "am I inside a
|
|
4614
|
+
* decorator argument" from the AST: two paths computing one fact is how this file has been bitten
|
|
4615
|
+
* before, and `enclosing`'s own R64 climb is subtler than any restatement of it (it distinguishes an
|
|
4616
|
+
* effect embedded in the argument DATA from an unresolvable decorator FACTORY, and `prev`-vs-`p`
|
|
4617
|
+
* decides which). `enclosing` may CREATE that unit as a side effect; it is dropped from `functions[]`
|
|
4618
|
+
* when nothing effectful lands in it, which PART 82's own `ctrllit`/`ctrlpure` arms assert. */
|
|
4619
|
+
function ownedByDecoratorArgument(node) {
|
|
4620
|
+
const owner = fns.get(enclosing(node));
|
|
4621
|
+
return !!owner && typeof owner.local === "string" && owner.local.startsWith(DECORATOR_ARG_LOCAL);
|
|
4622
|
+
}
|
|
4585
4623
|
// Mint (or ALIAS to an existing unit) every member of a structural implementor `container`
|
|
4586
4624
|
// (ObjectLiteralExpression's `.properties` or a ClassExpression's `.members` — both walked generically,
|
|
4587
4625
|
// same shape as the nominal branch's own `cls.members`). Position-keyed, like `decoratorArgUnit` /
|
|
4588
4626
|
// `staticBlockUnit` above — two structural implementors in one file must not collide.
|
|
4589
|
-
|
|
4627
|
+
// ⟨SOUNDNESS R531⟩ `bodiesOnly` — mint the members whose BODY is written here, and take no position on
|
|
4628
|
+
// the members that merely REFERENCE a function declared elsewhere. The alias arm at the bottom of this
|
|
4629
|
+
// function sets `nodeName` to an EXISTING unit, which makes `enclosing()` stop at that property; that is
|
|
4630
|
+
// right when the literal is a structural IMPLEMENTOR (a dispatch on the abstraction has to land
|
|
4631
|
+
// somewhere) and wrong for a bare literal in any other position. MEASURED, and it is not hypothetical:
|
|
4632
|
+
// `({ count: c.count } = { count: 7 })` parses as an ObjectLiteralExpression in DESTRUCTURING-TARGET
|
|
4633
|
+
// position, its `count: c.count` alias resolves to the accessor unit `C.count`, and the setter
|
|
4634
|
+
// invocation the assignment performs was then attributed to the ACCESSOR instead of to the function
|
|
4635
|
+
// containing the assignment — test.mjs `[32]` went red on the first build of this fix, which is the
|
|
4636
|
+
// only reason the arm is split. Body-minting cannot do that: a unit minted from a body it owns moves
|
|
4637
|
+
// no effect that was not already inside it.
|
|
4638
|
+
function mintStructuralMembers(container, bodiesOnly = false) {
|
|
4590
4639
|
const sf = container.getSourceFile();
|
|
4591
4640
|
const mod = moduleOf(sf);
|
|
4592
4641
|
const members = container.properties ?? container.members ?? [];
|
|
@@ -4612,6 +4661,7 @@ function mintStructuralMembers(container) {
|
|
|
4612
4661
|
// result, a conditional, an opaque holder), is left UNRESOLVED — `nodeName.has(prop)` stays false, so
|
|
4613
4662
|
// the dispatch site's `allResolved` gate reads this implementor as incomplete and forces Unknown: the
|
|
4614
4663
|
// disjunction's (b) arm, never silence.
|
|
4664
|
+
if (bodiesOnly) continue; // ⟨R531⟩ see the header — aliasing is an implementor's privilege, not a literal's
|
|
4615
4665
|
const bound = unwrapBind(init);
|
|
4616
4666
|
const ref = bound ? bound.ref : ((ts.isIdentifier(init) || ts.isPropertyAccessExpression(init)) ? init : null);
|
|
4617
4667
|
if (ref) {
|
|
@@ -4621,6 +4671,25 @@ function mintStructuralMembers(container) {
|
|
|
4621
4671
|
}
|
|
4622
4672
|
}
|
|
4623
4673
|
}
|
|
4674
|
+
/** ⟨SOUNDNESS R531⟩ THE MINTED UNIT IS KEYED ON THE PROPERTY, AND RESOLUTION ARRIVES AT THE
|
|
4675
|
+
* INITIALIZER. `mintStructuralMembers` sets `nodeName` on the PropertyAssignment/PropertyDeclaration,
|
|
4676
|
+
* but `realDecl` unwraps `a.roll` straight to the ArrowFunction that initialises it — so a plain
|
|
4677
|
+
* `nodeName.get(decl)` misses by exactly one node. Spelled ONCE, because ⟨R531b⟩'s pre-pass has to ask
|
|
4678
|
+
* the SAME question the call walk asks ("can this decl already be resolved to a unit?") and two
|
|
4679
|
+
* spellings of one fact is how this file has been bitten before (§G). MEASURED: the first build of
|
|
4680
|
+
* R531b asked only `nodeName.has(arrow)` and so minted a SECOND unit over a body R531 had already
|
|
4681
|
+
* minted — bson's `src.extended_json.serializeDocument` went from calling `<structural>@10049.Binary`
|
|
4682
|
+
* to calling `<callable>@10057`, one body through two units, which is exactly the double count
|
|
4683
|
+
* conformance PART 82 pins. Narrow by construction: only an arrow/fn-expression sitting DIRECTLY in a
|
|
4684
|
+
* property, and only a `nodeName` entry that already exists — no unit is invented here. */
|
|
4685
|
+
function callTargetUnit(decl) {
|
|
4686
|
+
const direct = nodeName.get(decl);
|
|
4687
|
+
if (direct) return direct;
|
|
4688
|
+
if ((ts.isArrowFunction(decl) || ts.isFunctionExpression(decl)) && decl.parent
|
|
4689
|
+
&& (ts.isPropertyAssignment(decl.parent) || ts.isPropertyDeclaration(decl.parent)))
|
|
4690
|
+
return nodeName.get(decl.parent);
|
|
4691
|
+
return undefined;
|
|
4692
|
+
}
|
|
4624
4693
|
function mintPositionalStructuralUnit(mod, sf, prop, name) {
|
|
4625
4694
|
const qual = `${mod}.<structural>@${prop.getStart()}.${name}`;
|
|
4626
4695
|
if (!fns.has(qual)) {
|
|
@@ -4632,6 +4701,50 @@ function mintPositionalStructuralUnit(mod, sf, prop, name) {
|
|
|
4632
4701
|
endLine: sf.getLineAndCharacterOfPosition(prop.getEnd()).line + 1 });
|
|
4633
4702
|
}
|
|
4634
4703
|
nodeName.set(prop, qual);
|
|
4704
|
+
// ⟨SOUNDNESS R519⟩ CONTAINMENT. Minting is an ADDITION, never a MOVE.
|
|
4705
|
+
//
|
|
4706
|
+
// `enclosing()` returns the FIRST ancestor carrying a `nodeName`, so the line above — which exists to
|
|
4707
|
+
// give the member an addressable unit a dispatch can land on — also silently STOPS the member's body
|
|
4708
|
+
// being charged to the function that lexically contains it. Whenever no in-scan dispatch resolves to
|
|
4709
|
+
// the minted unit, the effects then sit in a unit reachable from nothing and the enclosing function
|
|
4710
|
+
// reads PURE: SPEC §2 rule 3's positive purity claim over code this engine read and understood.
|
|
4711
|
+
//
|
|
4712
|
+
// Nothing in a producer package need ever dispatch on a DEPENDENCY's abstraction — R512's own measured
|
|
4713
|
+
// shape is a package that merely SUPPLIES an implementor — and the orphaning is not confined to that
|
|
4714
|
+
// case: a union arm's type-literal member, an index-signature interface's member, and any member the
|
|
4715
|
+
// matched interface does not declare are unreachable by name in ANY scan. MEASURED, v0.38.3 vs the
|
|
4716
|
+
// released v0.39.0, one package, no chain, no unscanned dispatcher: ten spellings went `['Rand']` ->
|
|
4717
|
+
// ABSENT with `deny Rand` exit 1 -> exit 0 (the R519 block in test.mjs, generated from one template).
|
|
4718
|
+
//
|
|
4719
|
+
// THE CONDITION IS UNCONDITIONAL, and that is a decision, not an omission. The obvious narrowing —
|
|
4720
|
+
// "charge unless the member is provably dispatched in-scan" — is UNSOUND for this property: a
|
|
4721
|
+
// dispatch elsewhere in the scan charges the DISPATCHING unit, never the enclosing one, so the
|
|
4722
|
+
// containing function would still read pure over a body written inside it. MEASURED: a function that
|
|
4723
|
+
// builds a class expression and calls its `size` two lines later still lost that `Rand` on v0.39.0.
|
|
4724
|
+
// The exception would also remove charges v0.38.3 made, which is the one direction a fix for a silent
|
|
4725
|
+
// under-report may not move in.
|
|
4726
|
+
//
|
|
4727
|
+
// FAILURE DIRECTION: OVER-charge. A function that builds an implementor it never runs is charged for
|
|
4728
|
+
// what that implementor would do. That is the same over-approximation this engine already makes for
|
|
4729
|
+
// EVERY closure it does not mint — an untyped `{ run(){ … } }` in the same position has always been
|
|
4730
|
+
// charged to its enclosing function (asserted as `viaUntyped` in the R519 block) — so the fix removes
|
|
4731
|
+
// an inconsistency rather than introducing an approximation.
|
|
4732
|
+
//
|
|
4733
|
+
// ALIASES ARE DELIBERATELY EXCLUDED: `mintStructuralMembers`'s `go: other.method` arm sets `nodeName`
|
|
4734
|
+
// to an EXISTING unit and never reaches here. That body is not lexically contained, referencing a
|
|
4735
|
+
// function is not calling it, and v0.38.3 charged nothing for it either — charging it would be a
|
|
4736
|
+
// fabrication introduced in the name of closing an under-report.
|
|
4737
|
+
const owner = enclosing(prop.parent);
|
|
4738
|
+
if (owner && owner !== qual) {
|
|
4739
|
+
fns.get(owner)?.edges.add(qual);
|
|
4740
|
+
// REACH PROBE, env-gated and left in place deliberately. A byte-identical A/B over a corpus that
|
|
4741
|
+
// cannot reach a changed branch is the most flattering number available and the least informative,
|
|
4742
|
+
// and it has been mistaken for evidence four times in this family. `CANDOR_R519_REACH=1` makes this
|
|
4743
|
+
// branch announce itself on stderr so `bin/corpus-ab.py --mark` can COUNT the hits rather than
|
|
4744
|
+
// anyone inferring them afterwards by grep. Off by default: no output, one env read per minted
|
|
4745
|
+
// member.
|
|
4746
|
+
if (process.env.CANDOR_R519_REACH) console.error(`R519-REACH ${qual} <- ${owner}`);
|
|
4747
|
+
}
|
|
4635
4748
|
}
|
|
4636
4749
|
for (const sf of sources) {
|
|
4637
4750
|
(function walkStructural(node) {
|
|
@@ -4642,11 +4755,67 @@ for (const sf of sources) {
|
|
|
4642
4755
|
// never structurally satisfy an interface with any member on its own.
|
|
4643
4756
|
if (ts.isObjectLiteralExpression(node) && node.properties.length > 0) {
|
|
4644
4757
|
const decls = contextualInterfaceDeclsFor(node);
|
|
4645
|
-
|
|
4646
|
-
|
|
4647
|
-
|
|
4648
|
-
|
|
4649
|
-
|
|
4758
|
+
for (const d of decls.local) registerStructuralImpl(d, node);
|
|
4759
|
+
for (const d of decls.foreign) registerForeignStructuralImpl(d, node);
|
|
4760
|
+
// ⟨SOUNDNESS R531⟩ MINT THE MEMBERS OF EVERY OBJECT LITERAL, not only of a contextually-typed one.
|
|
4761
|
+
//
|
|
4762
|
+
// Minting used to be gated on the literal matching some interface, because minting existed only to
|
|
4763
|
+
// give a DISPATCH somewhere to land. But a minted unit is also the only thing a DIRECT call on the
|
|
4764
|
+
// literal — `const u = { roll: (n) => Math.random() }; u.roll(n)` — can resolve to, and an untyped
|
|
4765
|
+
// literal never reached here. The resolution then landed on the ARROW ITSELF, which the call site
|
|
4766
|
+
// waves through as "already walked lexically" (see the `isArrowFunction` arm in the call walk): true
|
|
4767
|
+
// of the arrow's LEXICAL owner, false of the CALLER. So the caller vanished from `functions[]`
|
|
4768
|
+
// entirely — SPEC §2 rule 3's positive purity claim over a body this engine had read and charged
|
|
4769
|
+
// one unit over.
|
|
4770
|
+
//
|
|
4771
|
+
// MEASURED at v0.38.3, v0.39.0, v0.39.1 and HEAD alike (so NOT an R519 regression): with
|
|
4772
|
+
// `const a = { roll: (n: number) => Math.floor(Math.random() * n) }`, `function A1(n) { return
|
|
4773
|
+
// a.roll(n) }` is ABSENT from `functions[]` and `deny Rand src.index.A1` exits 0 while
|
|
4774
|
+
// `deny Rand src.index.A8` — the same body reached through a plain function — exits 1. Seven
|
|
4775
|
+
// spellings: arrow and function-expression initializers, `as const`, a nested two-hop literal, an
|
|
4776
|
+
// exported literal, a factory-returned literal, and the untyped-parameter caller.
|
|
4777
|
+
//
|
|
4778
|
+
// THE SPELLING ASYMMETRY IS THE TELL, and it is why this was invisible: `{ roll(n){…} }` — method
|
|
4779
|
+
// SHORTHAND on the very same untyped literal — is a MethodDeclaration, not an arrow, so it misses
|
|
4780
|
+
// that arm and discloses an honest `Unknown[callback:roll]`. One literal, two spellings, one loud
|
|
4781
|
+
// and one silent.
|
|
4782
|
+
//
|
|
4783
|
+
// MINTING IS SAFE TO WIDEN ONLY BECAUSE ⟨R519⟩ MADE IT ADDITIVE. Before `mintPositionalStructuralUnit`
|
|
4784
|
+
// gained its containment edge, extending minting to every literal would have MOVED the body of every
|
|
4785
|
+
// `{ onClick: () => … }` out of its enclosing function — R519's own defect, multiplied. The
|
|
4786
|
+
// containment edge is what makes this monotone-up: the enclosing unit keeps the charge, and the
|
|
4787
|
+
// caller gains a unit to resolve to.
|
|
4788
|
+
//
|
|
4789
|
+
// FAILURE DIRECTION: PRECISION-POSITIVE, not over-charge. This resolves rather than hedges — the
|
|
4790
|
+
// method-shorthand spelling goes `Unknown` -> the concrete effect, and a PURE member's caller stays
|
|
4791
|
+
// ABSENT (asserted as the `P1`/pure twin in the R531 block). The rejected alternative was to disclose
|
|
4792
|
+
// `Unknown` at the call site whenever the arrow is not lexically inside the caller; it closes the
|
|
4793
|
+
// same silence and was MEASURED to charge `Unknown` to a caller of `{ twice: (n) => n * 2 }`, which
|
|
4794
|
+
// is closing a silence by flooding the other channel — the trade ⟨0.39⟩'s cost model forbids.
|
|
4795
|
+
//
|
|
4796
|
+
// ONE POSITION IS EXCLUDED, AND CONFORMANCE PART 82 IS WHY. ⟨R64⟩ already mints a unit for a
|
|
4797
|
+
// closure embedded directly in a DECORATOR'S ARGUMENT DATA — `@Factory({ init: () => { … } })` is
|
|
4798
|
+
// the literal example in `decoratorArgUnit`'s own docstring — and that unit exists precisely
|
|
4799
|
+
// because there is no declaration anywhere else for the body to land on. Minting the property as
|
|
4800
|
+
// well splits one non-callable piece of syntax into TWO reported units, and ⟨R519⟩'s containment
|
|
4801
|
+
// edge then feeds the effect back up, so `deny Fs` scores the same `readFileSync` twice:
|
|
4802
|
+
// PART 82 `defect-shape2-argclosure-gate` went `notok count:1` -> `notok count:2`. Fails closed
|
|
4803
|
+
// either way, so it is a double count and not a soundness regression — but a count the family has
|
|
4804
|
+
// pinned four-way is a contract.
|
|
4805
|
+
//
|
|
4806
|
+
// THE BOUND IS "NOT A BINDING SITE", NOT "MAKE PART 82 PASS". A decorator argument is an anonymous
|
|
4807
|
+
// expression: TypeScript gives no way to name that literal, so no call site anywhere can ever
|
|
4808
|
+
// resolve `init`, and the unit R531 mints to be resolved AGAINST can never be reached. Skipping it
|
|
4809
|
+
// costs nothing and returns this position to exactly its pre-R531 behaviour, which was never
|
|
4810
|
+
// silent — the body is charged to `<decorator-arg>@N` and the gate fires.
|
|
4811
|
+
//
|
|
4812
|
+
// THE NEIGHBOURS WERE CHECKED RATHER THAN ASSUMED, because an audit drawn around its own trigger
|
|
4813
|
+
// is this project's most expensive habit. The other synthetic attribution units are `moduleUnit`
|
|
4814
|
+
// and `staticBlockUnit`, and BOTH are real binding sites: `static { const u = { roll: () => … };
|
|
4815
|
+
// u.roll() }` resolves, and so does the module-scope form that R531 was filed for. They are NOT
|
|
4816
|
+
// excluded, and the R531 block asserts both still charge their caller.
|
|
4817
|
+
const bodiesOnly = !(decls.local.length || decls.foreign.length);
|
|
4818
|
+
if (!(bodiesOnly && ownedByDecoratorArgument(node))) mintStructuralMembers(node, bodiesOnly);
|
|
4650
4819
|
} else if (ts.isClassExpression(node)) {
|
|
4651
4820
|
// Explicit `implements` on an anonymous/named class EXPRESSION — the nominal branch above only
|
|
4652
4821
|
// ever visits `ts.isClassDeclaration`, so `held = new (class implements Task { go(){…} })()`
|
|
@@ -5340,7 +5509,7 @@ function staticBlockUnit(node) {
|
|
|
5340
5509
|
function decoratorArgUnit(callNode) {
|
|
5341
5510
|
const sf = callNode.getSourceFile();
|
|
5342
5511
|
const mod = moduleOf(sf);
|
|
5343
|
-
const local =
|
|
5512
|
+
const local = `${DECORATOR_ARG_LOCAL}${callNode.getStart()}`;
|
|
5344
5513
|
const qual = `${mod}.${local}`;
|
|
5345
5514
|
let rec = fns.get(qual);
|
|
5346
5515
|
if (!rec) {
|
|
@@ -7237,7 +7406,17 @@ function visitCalls(node) {
|
|
|
7237
7406
|
}
|
|
7238
7407
|
}
|
|
7239
7408
|
if (mod === "<local>") {
|
|
7240
|
-
|
|
7409
|
+
// ⟨SOUNDNESS R531⟩ the property mint is keyed one node ABOVE the arrow resolution lands on;
|
|
7410
|
+
// `callTargetUnit` is that lookup, spelled once and shared with ⟨R531b⟩'s pre-pass (see its
|
|
7411
|
+
// docstring). Widening the mint without this line leaves the sin open — measured: the R531
|
|
7412
|
+
// arms stayed ABSENT — so the two halves are one fix.
|
|
7413
|
+
const mintedOnProp = nodeName.get(decl) ? undefined : callTargetUnit(decl);
|
|
7414
|
+
// REACH PROBE, env-gated, same contract as ⟨R519⟩'s above: a byte-identical A/B over a corpus
|
|
7415
|
+
// that cannot reach the changed branch is the most flattering number available and the least
|
|
7416
|
+
// informative. `CANDOR_R531_REACH=1` makes THIS branch — the one that turns a silent caller
|
|
7417
|
+
// into a charged one — announce itself so `bin/corpus-ab.py --mark` can count it.
|
|
7418
|
+
if (mintedOnProp && process.env.CANDOR_R531_REACH) console.error(`R531-REACH ${owner} -> ${mintedOnProp}`);
|
|
7419
|
+
const targetName = nodeName.get(decl) ?? mintedOnProp;
|
|
7241
7420
|
if (targetName) {
|
|
7242
7421
|
rec.edges.add(targetName); // (EDGE) — cross-FILE edges resolve the same way
|
|
7243
7422
|
// Class-CHA fan-out: resolution landed on a base-class member that LOCAL subclasses
|
|
@@ -8834,6 +9013,113 @@ function visitCalls(node) {
|
|
|
8834
9013
|
}
|
|
8835
9014
|
ts.forEachChild(node, visitCalls);
|
|
8836
9015
|
}
|
|
9016
|
+
/** ⟨SOUNDNESS R531b⟩ AN ARROW A CALL RESOLVES TO, THAT NO BINDING SITE EVER NAMED.
|
|
9017
|
+
*
|
|
9018
|
+
* ⟨R531⟩ closed the OBJECT-LITERAL-PROPERTY route by minting the property. This is the same silence one
|
|
9019
|
+
* construct over, and it has no property to key on: `const c = (() => (n: number) => Math.random()*n)();
|
|
9020
|
+
* function C(n){ return c(n) }` — the checker resolves `c(n)` to the ARROW returned by the IIFE, that
|
|
9021
|
+
* arrow is nobody's initializer, so `nodeName` has no entry and the call walk's last `else if` waves it
|
|
9022
|
+
* through on the grounds that "its body is visible and already walked lexically". That is true of the
|
|
9023
|
+
* arrow's LEXICAL owner (here the module initializer) and FALSE of the caller, which then vanishes from
|
|
9024
|
+
* `functions[]` entirely — SPEC §2 rule 3's positive purity claim over a body this engine read and
|
|
9025
|
+
* charged one unit over. MEASURED byte-identical on published v0.38.3, v0.39.0, v0.39.1 and HEAD, so
|
|
9026
|
+
* pre-existing, not an R519/R531 regression; `deny Rand src.lib.C1` exits 0 while the same body reached
|
|
9027
|
+
* through a plain `const c4 = (n) => …` exits 1.
|
|
9028
|
+
*
|
|
9029
|
+
* THE BOUNDARY IS THE WAIVER'S OWN CLAIM, NOT A LIST OF SPELLINGS. The audit was deliberately not drawn
|
|
9030
|
+
* around the IIFE that triggered it: a sweep of twenty-three spellings on the published engine found the
|
|
9031
|
+
* identical silence in EIGHT — IIFE→arrow, factory→arrow, IIFE→function-expression, a ternary selecting
|
|
9032
|
+
* between two arrows, `.bind()`, an array-literal element indexed at the call site, an arrow returned
|
|
9033
|
+
* from a class method, and an arrow round-tripped through a generic identity `id<T>(x: T): T`. They are
|
|
9034
|
+
* one defect, because the engine has one waiver, and the general property is "resolution landed on an
|
|
9035
|
+
* arrow/function-expression node that no binding site minted". So the condition here is that property —
|
|
9036
|
+
* every spelling that produces it is covered, including ones nobody has written down yet.
|
|
9037
|
+
*
|
|
9038
|
+
* WHEN THE WAIVER IS TRUE WE DO NOTHING. `enclosing(fn.parent)` is the unit the arrow's body is ALREADY
|
|
9039
|
+
* charged to; when that is the caller's own unit, the caller really has the effects and there is no
|
|
9040
|
+
* silence to close — a plain `(() => { … })()` IIFE inside a function is exactly this, and minting it
|
|
9041
|
+
* would add a report row for no gain. Skipping is safe in the direction that matters: the skipped case is
|
|
9042
|
+
* the one where the pre-fix engine was already correct.
|
|
9043
|
+
*
|
|
9044
|
+
* (a) MINT, NOT (b) DISCLOSE `Unknown` — and (b) was BUILT AND RUN for this route rather than inherited
|
|
9045
|
+
* from R531's measurement of a different one. The decisive result is not the flood, it is that (b) does
|
|
9046
|
+
* not close the gate: `Unknown` is not `Rand`, so `deny Rand C1` stays at EXIT 0 over a real
|
|
9047
|
+
* `Math.random()` — measured on this file's own R531b fixture. The flood is real too, measured on the
|
|
9048
|
+
* 53-entry npm corpus: (b) recovered ZERO concrete effects, charged `Unknown` to 63 previously-absent
|
|
9049
|
+
* callers of which 24 resolve to targets this engine can SEE are pure (axios's `isStream`, `isThenable`,
|
|
9050
|
+
* `toArray`, `isSpecCompliantForm`), and added a spurious reason to 202 existing rows. Minting RESOLVES
|
|
9051
|
+
* instead: a pure target leaves the caller absent exactly as before (asserted by the R531b PRECISION
|
|
9052
|
+
* arms), an effectful one charges the concrete effect and the scoped gate fires.
|
|
9053
|
+
*
|
|
9054
|
+
* MINTING IS ADDITIVE ONLY BECAUSE OF ⟨R519⟩'s CONTAINMENT EDGE, AND THAT STILL HOLDS FOR A NON-PROPERTY
|
|
9055
|
+
* PARENT — it was CHECKED, not assumed, because if it did not hold this would be R519 re-created. R519's
|
|
9056
|
+
* edge is `enclosing(prop.parent) -> minted`; the only thing it needs of the parent is that `enclosing`
|
|
9057
|
+
* can answer for it, and `enclosing` answers for ANY node (it falls through to `moduleUnit` at the
|
|
9058
|
+
* SourceFile). Here the owner is read BEFORE `nodeName.set`, so it is the pre-mint attribution owner by
|
|
9059
|
+
* construction, and the edge gives that owner everything the mint would otherwise have moved out of it.
|
|
9060
|
+
* Asserted directly in test.mjs's R531b block (`viaEnclosing`): the module initializer keeps its `Rand`
|
|
9061
|
+
* after the mint.
|
|
9062
|
+
*
|
|
9063
|
+
* FAILURE DIRECTION: PRECISION-POSITIVE, and additive. No effect is removed from any unit, so no firing
|
|
9064
|
+
* gate can go green; the report gains one row per arrow some call actually resolves to. It is NOT a
|
|
9065
|
+
* blanket mint of every closure — an inline callback (`arr.map(x => …)`) is invoked by the CALLEE, never
|
|
9066
|
+
* resolved by a call in this scan, so it is not a candidate and stays charged to its lexical owner.
|
|
9067
|
+
* ⟨R64⟩'s decorator-argument position, which is what PART 82 pins, is likewise untouched: TypeScript
|
|
9068
|
+
* gives no way to name a decorator argument's closure, so no call resolves to one, and the only closure
|
|
9069
|
+
* there that IS called — an IIFE — takes the waiver-true skip above. PART 82's `count:1` is asserted
|
|
9070
|
+
* unchanged.
|
|
9071
|
+
*
|
|
9072
|
+
* FIXPOINT, because the predicate reads `enclosing` and minting REFINES it. Minting arrow B can change
|
|
9073
|
+
* the owner of a call that resolves to a sibling arrow A from "the shared lexical owner" to B — at which
|
|
9074
|
+
* point A's waiver is no longer true and A must be minted too, or B under-reports A's effects. One pass
|
|
9075
|
+
* in document order is not enough to see that, and the monotonicity (owners only ever get finer, the
|
|
9076
|
+
* candidate set only shrinks) makes the loop terminate in the number of candidates. */
|
|
9077
|
+
function mintCallTargetUnit(fn) {
|
|
9078
|
+
const sf = fn.getSourceFile();
|
|
9079
|
+
const local = `<callable>@${fn.getStart()}`;
|
|
9080
|
+
const qual = `${moduleOf(sf)}.${local}`;
|
|
9081
|
+
const owner = enclosing(fn.parent); // BEFORE the nodeName.set below — the PRE-mint attribution owner
|
|
9082
|
+
if (!fns.has(qual)) {
|
|
9083
|
+
const { line, character } = sf.getLineAndCharacterOfPosition(fn.getStart());
|
|
9084
|
+
fns.set(qual, { local, direct: new Set(), fsKinds: new Set(), edges: new Set(), hosts: new Set(),
|
|
9085
|
+
tables: new Set(), cmds: new Set(), paths: new Set(), blind: new Set(),
|
|
9086
|
+
incomplete: new Set(), dispatch: new Set(), why: new Set(), entry: false,
|
|
9087
|
+
loc: `${path.relative(rootDir, sf.fileName)}:${line + 1}:${character + 1}`,
|
|
9088
|
+
endLine: sf.getLineAndCharacterOfPosition(fn.getEnd()).line + 1 });
|
|
9089
|
+
}
|
|
9090
|
+
nodeName.set(fn, qual);
|
|
9091
|
+
// ⟨R519⟩ CONTAINMENT — minting is an ADDITION, never a MOVE. Same edge, same reason, non-property parent.
|
|
9092
|
+
if (owner && owner !== qual) fns.get(owner)?.edges.add(qual);
|
|
9093
|
+
// REACH PROBE, env-gated, same contract as ⟨R519⟩'s and ⟨R531⟩'s. "CHANGED 0" and "the branch never
|
|
9094
|
+
// ran" print identically; `bin/corpus-ab.py --mark` counts this rather than anyone inferring it later.
|
|
9095
|
+
if (process.env.CANDOR_R531B_REACH) console.error(`R531B-REACH ${owner} -> ${qual}`);
|
|
9096
|
+
return qual;
|
|
9097
|
+
}
|
|
9098
|
+
{
|
|
9099
|
+
const cands = [];
|
|
9100
|
+
for (const sf of sources) (function findAnonCallTargets(n) {
|
|
9101
|
+
if (ts.isCallExpression(n) || ts.isTaggedTemplateExpression(n)) {
|
|
9102
|
+
let d; try { d = checker.getResolvedSignature(n)?.declaration; } catch { d = undefined; }
|
|
9103
|
+
if (d && (ts.isArrowFunction(d) || ts.isFunctionExpression(d)) && d.body
|
|
9104
|
+
&& projectFiles.has(path.resolve(d.getSourceFile().fileName)))
|
|
9105
|
+
cands.push({ call: n, fn: d });
|
|
9106
|
+
}
|
|
9107
|
+
ts.forEachChild(n, findAnonCallTargets);
|
|
9108
|
+
})(sf);
|
|
9109
|
+
for (let round = 0; round <= cands.length; round++) {
|
|
9110
|
+
const mint = [];
|
|
9111
|
+
for (const c of cands) {
|
|
9112
|
+
// ASK THE AUTHORITY: anything the call walk can already resolve — including ⟨R531⟩'s
|
|
9113
|
+
// property mint, which is keyed one node ABOVE this arrow — must not be minted again.
|
|
9114
|
+
if (callTargetUnit(c.fn)) continue;
|
|
9115
|
+
if (enclosing(c.fn.parent) === enclosing(c.call)) continue; // the waiver is TRUE here — already walked into the caller
|
|
9116
|
+
mint.push(c.fn);
|
|
9117
|
+
}
|
|
9118
|
+
if (!mint.length) break;
|
|
9119
|
+
mint.sort((a, b) => a.getStart() - b.getStart()); // outer before inner: a nested mint must see its container's unit
|
|
9120
|
+
for (const fn of mint) if (!callTargetUnit(fn)) mintCallTargetUnit(fn);
|
|
9121
|
+
}
|
|
9122
|
+
}
|
|
8837
9123
|
for (const sf of sources) visitCalls(sf);
|
|
8838
9124
|
|
|
8839
9125
|
// ---- pass 2b: callback-flow resolution (the callback_named move) ----------------------------------
|