Repository navigation
fix(oracles): types label a survivor as outside-annotation, and never skip a mutant or mark it dead - #36
Merged
Conversation
…its survivor (RED) The mutant-validity oracle judges every site over the inputs JavaScript allows: each type's values and foreign ties (a string beside its digits, null beside a zero, a number beside a bigint, -0 beside 0, and an object whose valueOf has an effect). No contract site may be skipped, dead code is marked exactly where no input distinguishes it, and a site that no declared input distinguishes carries the outside-annotation label. Hostile programs from five reviews must keep their sites when their types are erased. The classify oracle gains the outside-annotation class, and the model and end-to-end oracles gain F17's two operators. Co-authored-by: Isaac <no-reply@databricks.com>
PR #8's ordering-swap skip and the forced-tie skip become the outside-annotation label, which names the input class outside the annotations that distinguishes the mutant (a string of the literal's digits, a number in place of a bigint, -0 and 0 in place of bigints, an object whose valueOf has an effect, null). A survivor with the label is classed outside-annotation, and its directive names that input. F17's operators come too: return-undefined evaluates the returned value and drops it, and optional-chain is never skipped; a literal or a module-local literal constant receiver is dead code. The syntactic evidence is repaired: 0x0, 0.0, 0b0, and 0x0n are zeros, a nested var and an import bind a name, a let or const is no local binding, a dead-code mark under identical branches needs no ordering comparison to run or not because of the site, and a forced tie needs a local name. Validity oracle: 0 of 32972 fail. Co-authored-by: Isaac <no-reply@databricks.com>
… its dead-code site A hostile program whose condition reads a let through a closure before its assignment: the swap of && there throws or not, so no dead-code mark may sit on it. The mutant that counts a let or const as a local binding fails it (1 of 32973); production passes (32973 of 32973). Co-authored-by: Isaac <no-reply@databricks.com>
Co-authored-by: Isaac <no-reply@databricks.com>
A family of short programs that the model judges by running their text: a logical swap whose operand may be in its dead zone (a parameter default, a for-let or for-of head, against the body), each form that binds the name undefined and three that only name it, a sloppy script with or without a direct eval or a with, a returned call, and a coerced comparison at five annotations. The label law is now exact: a label only where an annotation excludes every distinguishing input. A returned call is a site in the narrowed family, and a hostile program returns import(). On fc6a2e2 this fails 42 of 33022. Co-authored-by: Isaac <no-reply@databricks.com>
… or with defeats; a returned call is a site From the sixth soundness review. A parameter read before its own declaration ends, and a loop's lexical variable read in its own head, are no local bindings, so a logical swap there is no dead code. A name binds undefined wherever it names a declaration (a function or class expression's own name included), not as a method name or a label. A direct eval or a with anywhere in the file, found in the syntax tree, keeps every return of undefined and every constant receiver. A returned call and import() are return-undefined sites. The coercion label needs annotations that admit primitives only, and an optional chain carries no label, since the CLI does not know the package's strict null checks. Validity oracle: 0 of 33022 fail; the static oracle models the fixture's returned calls. Co-authored-by: Isaac <no-reply@databricks.com>
TypeScript's CommonJS emit of export { s } assigns exports.s once and keeps
the module's own reads of s local, so nothing outside can change the value
the function reads. The check that left such a receiver unmarked had no
witness: its mutant passed the oracle (0 of 33022), and the emit shows
why. Validity oracle: 33022 of 33022.
Co-authored-by: Isaac <no-reply@databricks.com>
…he spellings of eval, and a label across && (RED) The seventh review's causes as generated dimensions: a read of x under identical branches, where x is bound by a parameter, a destructured parameter, a let, a const, a class, a caught error's pattern, a for-of or for-in pattern, a nested pattern, or a var (nested, or in a class's static block), and read before its declaration ends, inside it, after it, or in a hoisted function. The scope family gains (eval)(code), (eval as any)(code), and the indirect (0, eval)(code). A chain family puts a second comparison in the swap's && chain, at a primitive and at object. On 10853a2 this fails 27 of 33092. Co-authored-by: Isaac <no-reply@databricks.com>
The maintainer's constraint: no hand-written scope analysis. declarationOf, varOf, importsName, bindsName, declaresName, enclosedByFunction, the binding scan for undefined, and the per-construct dead-zone checks are gone (148 lines of code). A one-file program, built lazily with noLib and noResolve, answers which declaration a name resolves to; only symbols and declarations are read, never types. Dead zones follow one rule: a parameter, a caught error, a let, or a const read before its declaration ends (a for-of or for-in head's iterated value included), from another function, or across a switch case, is no local name. A direct eval is a call of the global eval through parentheses or a type's wrapper. A swap's label needs every comparison in its condition to compare primitives. Validity oracle: 0 of 33092; static, classify, probe-form: 31 of 31. Co-authored-by: Isaac <no-reply@databricks.com>
Co-authored-by: Isaac <no-reply@databricks.com>
…eck in localBinding TypeScript's binder resolves no name inside a with statement, so the localBinding check for one guarded nothing: its mutant passed the oracle (0 of 33092). The file-wide with check stays where it matters, for a returned undefined, which resolves to no declaration either way. Co-authored-by: Isaac <no-reply@databricks.com>
… binding, and labels without strict null checks (RED) The text programs' model and driver now await a returned promise and drive a returned iterator, eight steps at most, and compare how each settles; the model's runs are computed when the file loads. The dead-zone, scope, binding, and returned-call programs are generated again as async functions, generators, and async generators, with the generator witness of a yield before a let. The scope family calls eval through a parameter, a destructured var, and a caught error that hold the real eval. The label law's declared inputs admit null and undefined, since the CLI does not know a package's strict null checks. On f16fce3 this fails 1177 of 33395. Co-authored-by: Isaac <no-reply@databricks.com>
…a nonzero bigint literal From the eighth review. A sloppy call of the identifier eval is direct whenever the binding holds the real eval, whatever binding it is (a parameter, a destructured var, a caught error), so any such call makes the file's scopes dynamic. A bigint admits null without strict null checks, and null ties with 0n, so the swap label needs a nonzero bigint literal at the tie. The forced-tie label stays: === never ties null with a bigint. Validity oracle: 0 of 33395. Co-authored-by: Isaac <no-reply@databricks.com>
Co-authored-by: Isaac <no-reply@databricks.com>
…, and state the settled comparison's limits A run that has not settled after two seconds is recorded as such, and its program's test fails, so a program that never settles cannot hang the file's collection. A thrown value's name is an error's name, and otherwise its kind, so a throw null no longer throws in the oracle. The law's limits state the blind spots of the settled comparison that the ninth review confirmed; none reaches a defect in today's grammar. Co-authored-by: Isaac <no-reply@databricks.com>
Co-authored-by: Isaac <no-reply@databricks.com>
|
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars.
|
commit: |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The gap CLI no longer skips a mutant, or marks it dead code, on the basis of types (maintainer decision, 2026-11-11). A mutant that no input of its annotations distinguishes now carries a label. Its survivor is reported in a new class,
outside-annotation.This pull request replaces PR #8's type-based skip on main. It also replaces the held F11 and F17 branches, which were never pushed. It keeps F17's two operators and the analyses that still earn their place, which now feed labels only.
An unsound type analysis can now only mislabel a survivor. It can no longer hide one.
What changes
PR feat(oracles): an oracle for mutant validity; dead code and the -0 boundary hint #8's ordering-swap skip becomes the label. For a
numbertie at a nonzero literal, its input class is "a string of the literal's digits". For a bigint tie at a nonzero bigint literal, it is "a number in place of a bigint" (round 8).The forced-tie skip for two annotated bigints becomes the label, with the input class "
-0and0in place of the bigints".A site under identical branches that only an object's coercion distinguishes gets the label "an object whose
valueOfhas an effect".An optional chain on a parameter whose annotation admits no nullish value is always emitted, without a label. The CLI does not read the package's strict null checks, and without them the annotation admits
null(round 6).The survivor class
outside-annotationhas a directive that names a step, so it follows the exit law (law 7). The directive is this:Add an owner history that reaches line N with <input>, which the annotation excludes, so that it changes an asserted result, and assert on the result.An argument typed
anyis an input that the types allow. So the class claims only that the annotations exclude the input, not that no legal input exists.F17's operators come in.
return-undefinedevaluates the returned value and then drops it.optional-chainreadsa?.basa.b.What still skips or marks dead code, and why it needs no types
return void xis no site, because the mutant evaluatesxand returns the sameundefined.return undefinedis no site only where nothing in the file binds the name, and the file has no directevaland nowith. The mutant is then the same program.a && a) is dead code, because both sides read the same value. A parameter read before its own declaration ends, a loop's lexical variable read in its own head, and aletorconstare no local names.ifwhose other side impliesx === Lis dead code.Lmust be a literal that===matches only in its own value: a bigint, or a nonzero number.xmust be a local name.constthat holds such a literal. Theconstmust not be exported by a modifier, ambient, or in a namespace. Its file must have no directevaland nowith. An export list keeps the module's own reads local in TypeScript's CommonJS emit, so it does not count.Each of these depends only on JavaScript's semantics. The validity oracle checks each one against inputs that JavaScript allows.
The fifth review's causes
varin a nested block, and an import binding, now resolve. Round 7 moved all binding resolution to TypeScript's binder.0x0,0.0,0b0, and0x0nare zeros. This also fixes main.const, or one exported by a modifier, is never a constant receiver.letorconstis no longer a local binding, because a closure can read it before its assignment.The F11 and F17 trust machinery is gone: the checker plumbing,
trusted, the exhaustive-switch skip, and strict null checks from the tsconfig. The label analysis reads parameter annotations by syntax, as PR #8 did.Size
Minified with esbuild, the CLI's shipped files are 47,074 bytes, and 18,021 bytes with gzip. They were 48,400 bytes (18,284 with gzip) on F17 at b6a4c2c, which this replaces. So the change saves 1,326 bytes, or 263 bytes with gzip.
Against main at d38af38, they grow by 3,870 bytes (1,171 with gzip). The growth pays for F17's two operators and for the soundness fixes. Round 7 saved 1,315 bytes (178 with gzip), because TypeScript's binder replaced the hand-written scope analysis. Round 8 adds 75 bytes (26 with gzip).
Round 6
The round-6 soundness review of fc6a2e2 found 9 cases from 4 causes, and one accuracy problem in the labels. Each cause is now a generated dimension of the validity oracle, judged by running the program's text. Fixed programs had missed whole classes twice.
function f(c, b = (c && a ? 1 : 1), a). A parameter read before its own declaration ends, and a loop's lexical variable read in its own head, are no longer local names. The generated programs cross the context (body, parameter default,for (let …)head,for (const … of …)head) with an earlier or a later binding, at&&,||, and??.undefined. A function or class expression's own name bound it unseen. Every declaration's name now binds, and a method name, a label, or an export specifier does not. The generated programs hold each binding form and each form that only namesundefined.evalor awithanywhere in the file now keeps every return ofundefinedand every constant receiver. Thewithis found in the syntax tree, not by a regular expression. The generated programs are sloppy scripts with a directeval, awith, or neither. Each holds a return ofundefined, a constant receiver, ora && a. Undereval,a && astays dead code, because anevalcannot make a read of a local name run code. The oracle checks it.import()are nowreturn-undefinedsites. A test that checks only a call's effect killsvalue-undefined, never this mutant. The narrowed family's returned calls and a hostilereturn import('./five')judge it.if (c || p < q)atnumber,string | number,object,unknown, and in JavaScript.The reviewer found these sound, and they stay. They are
a && aon a local, forced ties with an exact literal, and an ordering swap under identical branches. Also the optional chains that are not dead code, andreturn void x.Round 7
The round-7 review of 10853a2 found 7 cases from 4 causes. It found the three departures of round 6 sound. The causes were dead zones in
for…of,for…in, and catch patterns, and avarin a class static block. Also(eval)(…)as a direct eval, and a swap label beside a coercing comparison in the same&&chain.These were rounds 5 to 7 of the same classes, so the fix replaces the construct-by-construct checks with general rules. The maintainer also asked that scope questions come from real tooling, not from a hand-written walk of the syntax tree.
noLibandnoResolve, and asks the checker which declaration a name resolves to. It reads only symbols and declarations, never types. It works for JavaScript withallowJs. This deletes 148 lines of code:declarationOf,varOf,importsName,bindsName,declaresName, andenclosedByFunction. It also deletes the binding scan forundefinedand the dead-zone checks per construct.let, or aconstis no local name before its declaration ends. Afor…oforfor…inhead's iterated value counts as before the end. A read from another function, or across aswitchcase, also counts, because that code can run before the declaration. Avarhas no dead zone.evalthrough parentheses,as,!,satisfies, or a type assertion is direct. The binder decides that the name is the global one.with: the binder resolves no name inside awith, so a name there is never local. The file-widewithcheck stays only for a returnedundefined. The check that localBinding had for it guarded nothing, and its mutant passed, so f16fce3 deletes it.I did not need oxc or
@typescript-eslint/scope-manager. The binder answered each question, and both would add a dependency.The generated dimensions are the binding forms crossed with the read positions:
let, aconst, and a class. Also a caught error's pattern, afor…oforfor…inpattern, a nested pattern, and avar(nested, or in a static block).(eval)(code),(eval as any)(code), and the indirect(0, eval)(code). A chain family puts a second comparison beside the swap, at a primitive and atobject.Round 8
The round-8 review found that the binder version fixes everything from rounds 5 to 7. Three findings remained.
A direct
evalthrough a local binding. A sloppy call of the identifierevalis direct whenever its binding holds the realeval. That binding can be a parameter, a destructuredvar, or a caught error. Now any call whose unwrapped callee is the identifierevalmakes the file's scopes dynamic, whatever it resolves to. The scope family generates the three bindings.Bigint labels without strict null checks. A
bigintadmitsnull, which ties with0n. The CLI does not read the package's tsconfig, so the label must hold without strict null checks too. A bigint swap label now needs a nonzero bigint literal at the tie. The forced-tie label stays, because===never tiesnullwith a bigint. The label law's declared inputs now admitnullandundefined, and the first family judges the zero literal, the nonzero literal, and the two-parameter cases.Async functions and generators. The oracle saw every promise and iterator as
"object". The model and the driver now await a returned promise, and drive a returned iterator for eight steps at most. They compare how each run settles: fulfilled with which value, rejected with which error, or which yields, return, and throw. The model's runs are computed when the file loads, because a settled run is asynchronous.The dead-zone, scope, binding, and returned-call programs are generated again as async functions, generators, and async generators. The reviewer's generator witness is a generator that yields before a
let. Production needed no change for this class. The mutant that makes a parameter local at any position now fails 27 async or generator programs,async function f(c, b = (c && x ? 1 : 1), x)among them. The mutant with no dead zone for a lexical name fails 60, the yield witness among them.Round 9
The round-9 review found no remaining unsound skip, dead-code mark, or wrong label in production. It found the bigint forced-tie label sound, because only
-0and0tie at two values under===. It also found the model independent of production: its runs come from the grammar's text,mutateSite, andts.transpileModule.The last changes are to the oracle:
next()is driven, neverreturn(),throw(), or afinally.throw 1andthrow 2are alike.throw nullno longer throws in the oracle.This pull request changes no end-to-end test name and adds no end-to-end test. The shard partition of #35 holds:
partitionErrorsonvitest list --jsonfinds 96 tests and no error.Changes
schemata.mjs: no type skip.outsideOfandswapOutsidecompute the label, andaddrecords it. The dead-code rules get the fixes above. F17's operators are included.classify.mjs,report.mjs,index.mjs: theoutside-annotationclass, its directive, and its legend placeholder.capture-setup.mjs: the switch'schain, foroptional-chain.gap-report-mutant-validity-oracle.property.test.mjs: the new law, the foreign inputs, the label law, and the hostile programs.gap-report-classify-oracle.property.test.mjs: the label in its grammar, the class in its model and directives, and a constructed history.gap-report-model-oracle.mjs,gap-report-oracle.property.test.mjs: F17's operators, and the class in the end-to-end class list.Evidence
Law: no mutant is skipped or marked dead code on the basis of types. Every emitted mutant that is not dead code differs from the program on some input that JavaScript allows. No site of the operator contract is skipped. A dead-code mark sits exactly where no input distinguishes the mutant. A mutant that no declared input distinguishes, and some other input does, carries the label. Erasing a hostile program's types changes none of its sites and no dead-code mark. Authority: the maintainer's decision of 2026-11-11, and freeze.md, "Undetectable mutants are invalid".
Reach: the validity oracle is enumerated, and its reach test passes. The label state and the impure-operand state are entered. So are an annotated optional receiver, a constant receiver in each language, and each excluded and shadowed return. Each of the 83 hostile programs has an input that detects a mutant, and each of the 49 round-6 programs decides its law.
RED: on main's production with this oracle (3506293), the validity oracle fails 32,247 of 32,972. Most failures are F17's missing operators. Among the hostile programs, main fails the type-erasure law for PR feat(oracles): an oracle for mutant validity; dead code and the -0 boundary hint #8's swap and forced-tie skips. These include the short-circuit cases
a < 1 && check() ? a : 1anda < b || check(). Main also marksif (a !== 0x0),0.0, and0b0as dead code, and an input distinguishes each. Its coercion marks under identical branches fail the same way. The classify oracle fails 2 of 11, because main has nooutside-annotationclass.GREEN: on c0fc574, the validity oracle passes 32,972 of 32,972, and on 6b03ea0 it passes 32,973 of 32,973. The static, classification, and probe-form oracles pass 31 of 31. The unit tests pass 56 of 56, and the framework suite passes 120 of 120. The end-to-end oracle's fixed seed 20261105 passes under the lock, in 295 s. After the merge of origin/main c34b693 (fc6a2e2), the validity oracle passes 32,973 of 32,973. The static, classification, and probe-form oracles pass 31 of 31, and the unit tests pass 56 of 56. The framework suite passes 124 of 124, and the end-to-end fixed seed passes under the lock, in 494 s.
Mutant: 10 hostile mutants each restore a type-based skip or weaken a syntactic rule, and the validity oracle rejects every one:
0x0,0.0, and0b0programs.undefinedignored: 12.letorconstas a local binding: 1, on the closure program that 6b03ea0 adds. It survived until that program was added.Round 6: RED: on fc6a2e2, the extended validity oracle fails 42 of 33,022. The failures cover all 5 causes: 9 dead-zone programs, 4 binding programs (2 missed, 2 over-bound), and 3 scope programs. Also 19 returned calls (16 narrowed programs, the generated call, and 2 hostile programs), 3 coerced programs, and 4 chain labels. GREEN: on 10853a2, the validity oracle passes 33,022 of 33,022. The static, classification, and probe-form oracles pass 31 of 31, and the unit tests pass 56 of 56. The framework suite passes 124 of 124, and the end-to-end fixed seed 20261105 passes under the lock, in 202 s. Mutants, one per fix, each rejected:
undefinedbinding it: 8.evalleaving scopes static: 2.withleaving scopes static: 1.Round 7: RED: on 10853a2, the extended validity oracle fails 27 of 33,092:
let,for…of,for…in, nested-pattern, and static-block forms.(eval)spellings, and 2 chain labels besideobject.GREEN: after the merge of origin/main f8a58a3 (ed779a5), the validity oracle passes 33,092 of 33,092. The static, classification, and probe-form oracles pass 31 of 31, and the unit tests pass 56 of 56. The framework suite passes 133 of 133, and the end-to-end fixed seed 20261105 passes under the lock, in 1,593 s on a loaded machine. On f16fce3 the validity and probe-form oracles pass 33,096 of 33,096, and the fast gap oracles pass 31 of 31.
Mutants, one per rule, each rejected:
evalonly as a bare name: 4.withcheck's mutant passed (0 failures), because the binder already resolves nothing there. f16fce3 deletes the check.Round 8: RED: on f16fce3, the extended validity oracle fails 1,177 of 33,395. These are 1,152 first-family bigint labels, 24 programs that call
evalthrough a local binding, and 1 bigint chain label. GREEN: on c846069, the validity oracle passes 33,395 of 33,395. After the merge of origin/main a19c01f (7cd842f), the validity oracle passes 33,395 of 33,395. The static, classification, and probe-form oracles pass 31 of 31, and the unit tests pass 56 of 56. The end-to-end fixed seed 20261105 passes under the lock, in 1,032 s. The framework suite passes 132 of 133. Itsdeterminismtest timed out at 600 s under a load average of 26 to 29 on 18 cores. The same test on main a19c01f timed out the same way under the same load, and the branch changes no framework file. It passed 133 of 133 on ed779a5. Mutants, each rejected:evalthrough a local binding not direct: 24 failures.Round 9 (final): after the merge of origin/main d38af38 (815a999):
determinismunits included.Limits: the validity domain is finite: each type's values and a fixed set of foreign ties. The label law covers the first family and the coerced programs, and forbids a label elsewhere. Narrowing is not labelled, so a survivor in code that a narrowing excludes takes another class. The hostile programs are fixed texts. Their types are compared by erasure, as operators and marks, and their dead-code marks are judged on their own inputs. The
outside-annotationdirective names no test, so the report order of fix(oracle-gaps): name tests in report order, whatever the context count or owner order #30 does not apply to it.This pull request and its description were written by Isaac.