Repository navigation
Enable strict record-field typecheck: close fail-open infer_record_lit hole (§5) - #5293
Conversation
… predicates as test fn
The 13 `data claim_*: TestClaim = BoolWitnessClaim { witness: BoolWitness { holds: f() } }`
rows (timeseries_passive_map_fold_anchor_test ×7, timeseries_signal_passives_test ×6) are
triple-malformed dead spec that compiled only because the v1-seed record typechecker is
fail-open (`infer_record_lit` silently ignores undefined fields and never validates missing
required ones):
- `holds` is not a field on `BoolWitness` (which has only `entry`, `function`);
- they are typed `: TestClaim`, but `BoolWitnessClaim` is an arm of `UnifiedTestClaim`, not
of `TestClaim` (whose arms are Compiles/Diagnostic/Equals/StructuralEquals/RoundTrip);
- wrong inner shape.
The live floor discovers only `data unified_claim_*` (never `data claim_*`), so these never ran
— pure dead specification, and a §3 fork of the canonical `{ entry, function }` form ratified
by consolidation #4553.
Asymmetry handled per §5 (do not silently drop coverage):
- anchor file: its 7 predicates were already live `test fn` (#5257) — pure deletion.
- signal file: only 1 of its 6 predicates was a live `test fn`; the other 5
(spice_capacitor/inductor_projects_*, passive_topology_preserves_component_name,
named_affine_voltage_preserves_ground_reference, absolute_potential_from_named_preserves_reference)
were plain `fn` wired ONLY through the dead `{holds}` rows — re-homed as `test fn` to
RESTORE their intended floor coverage rather than orphan the assertions.
Drop the now-unused `BoolWitness`/`BoolWitnessClaim` imports from both files.
Green by execution: `claim_batch --source-root src/v2` over all 13 witnesses → 13 PASS,
including the 5 newly-live signal predicates. Their assertions discriminate on Present/Absent
(not tautological), so floor coverage is strictly increased, not faked.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…t hole
The v1-seed typechecker (infer_record_lit) was fail-OPEN on record
construction — it silently ignored fields that exist on no variant of the
constructed type (DESIGN.md §5: "a wrong answer is a loud error, never a
warning"). That hole is what let the malformed `BoolWitness { holds }` rows
typecheck green (see #5272). Closing it.
Host change (src/v1/stage0/src/v1_compiler_infer.rs):
- record_lit_construction_field_names: a *confident* field-name resolver that
follows `type X = Y` aliases and searches the expected coproduct's arms
before the global fallback (the two resolver-precision gaps the leniency was
compensating for: `Set<T> = PointwisePower<T>` alias; variant-vs-collection
name collisions like `GrammarExpr.Optional`). Returns None when not confident
so anonymous/unresolved records never false-positive.
- field_in_any_variant_named: an ambiguity-aware secondary scan, run only on
would-be-errors, that suppresses false positives where a field is valid on a
same-named variant of *another* coproduct (e.g. TextNode) while keeping teeth
on truly-undefined fields.
- emit CompilerDiagnostic::FieldNotFound for an unknown constructed field.
Real bugs this surfaced (silently tolerated before; field name was wrong):
- infer_ground_add_mvp.dag: `NodeLocus { node }` → `node_locus(node:)`
(the field is `anchor`).
- 3 lens/application tests: `PortLocus { port }` → `port_locus(port:)`
(the field is `anchor`).
Proven by execution: full claim_batch floor green (439 witnesses) with the
check live; probe `BoolWitness { holds }` goes red with
"field 'holds' not found in type 'BoolWitness'".
Missing-required-field enforcement deferred (optional-cardinality +
templating risk, unmeasured).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ile (§3/§7)
Per the §3/§7 review (cursor): the strict infer_record_lit check lived only in the
generated seed .rs (a host hand-edit), not the .dag authority. Port the helpers and the
infer_record_lit guard into src/v1/04_infer.dag so the seed is regenerated FROM the model:
- record_lit_construction_field_names: confident field-name resolver (alias-follow +
expected-coproduct-arm union), SKIPS generic/parametric types (newtype flattening makes
their resolved field names non-source-faithful — the algebra-tower false positives).
- field_in_any_variant_named: same-named-coproduct-arm ambiguity guard, run only on a
would-be error (keeps TextNode-style constructions silent; full teeth elsewhere).
- infer_record_lit emits FieldNotFound for a field on no field of a confidently-resolved
non-generic record; none-result paths stay silent (no false positives).
§3 de-confliction: two 'type TextFile' collided in the bootstrap's combined src/v1+dsl
closure — v1 {path, content} (emit output) vs dsl/std {path, document} (markup). The
name-keyed type env clobbered one, so the strict check could not resolve 'content'. Rename
the dsl one to DocumentFile (it holds a Document; parallel to its sibling RenderedTextFile),
the smaller + semantically-correct fix. v1 TextFile {path, content} is now the sole binding.
The regenerated .rs follows (regen against current main's seed, post-merge).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ypecheck # Conflicts: # src/v2/std/timeseries_signal_passives_test.dag
…false positives)
The struct_fields/expected-based resolver false-positived on main's newer types where
resolved field names diverge from source under newtype flattening / name collision:
- `Watt = Measure<Power,One,Nat>` etc. — concrete aliases of the single-field generic
`Measure<Q,S,M> { count: M }`; the wrapper dissolves so `count` isn't in the resolved set.
- `Vendor { legal_name }` — a §3 name collision: `extdeps Vendor<Domain> { legal_name }`
record vs the std `Vendor = UnknownVendor | ...` enum; lookup grabs the enum.
Replace it with a confident-only resolver: fire ONLY when the constructed name resolves to a
plain, fully-grounded record (params == 0, connective == Conj, children > 0), reading source
field names directly from its own children. Generics, concrete aliases of single-field
generics, and coproduct/enum (incl. the losing side of a collision) all return none -> silent.
Keeps full teeth on plain records, where every real bug lives (BoolWitness{holds},
NodeLocus{node}, PortLocus{port}). Drops the now-unused record_lit_expected_coproduct_arm.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The committed seed is the compiler's own emit of the strict 04_infer.dag authority (derived,
not hand-edited) — closes the §3/§7 review: the strict infer_record_lit check now lives in the
.dag model and the .rs is regenerated from it. Single-file diff (+136/-1, no new module deps):
only 04_infer.dag changed, so only its emitted module changes.
Proven by execution on the merged tree:
- floor (claim_executor over src/v2+dsl): 0 field-not-found false positives
- self-compile (src/v1+dsl via regen path): 0 field-not-found
- cargo fmt --all --check + cargo clippy --all-targets -D warnings: clean
- discriminating probe: `BoolWitness { holds: true }` -> loud
'field holds not found in type BoolWitness'
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Addressed in §3/§7 port (the requested change). The strict Tightened to confident-only (§5, no false positives). The first port false-positived on main's newer types where resolved field names diverge from source under newtype flattening / a §3 name collision:
§3 de-confliction. Two Proven by execution on the merged tree:
— sent from silent-bee-439 |
Manager trim: revert accidental #5293 infer rollback, DocumentFile→TextFile rename, and lens/spice collateral. PR-1 scope is src/v2/std/algebra.dag + freemonoid_gap_free_fns_test.dag only (locked plan). Co-authored-by: Cursor <cursoragent@cursor.com>
Land additive v2.std.algebra gap fns (map, flat_map, reverse, skip/take/slice, sort_by, first/last, fold) plus 10 green execution witnesses. Fix last via left-fold (bootstrap-safe); defer enumerate — fold_list into FreeMonoid<IndexedElement<T>> trips variant-not-found when co-loaded with gap fns. Merges main (infer #5293). Co-authored-by: Cursor <cursoragent@cursor.com>
What
Closes the fail-open hole in the v1-seed typechecker's record-construction
path (
infer_record_lit). Before this PR it silently accepted fields thatexist on no variant of the constructed type — a DESIGN.md §5 violation ("a
wrong answer is a loud error, never a warning"). That hole is exactly what
let the malformed
BoolWitness { holds }rows (removed in #5272) typecheckgreen.
Why it wasn't already strict — two resolver-precision gaps
The leniency was compensating for the resolver not being precise enough. Making
it strict required first making field-resolution confident, else real
constructions false-positive:
Set<T> = PointwisePower<T>;Set { member }iscorrect but the resolver returned
Set's children, notPointwisePower's.GrammarExpr.Optional(avariant) vs the
Optional<T>collection; global lookup beat theexpected-coproduct context.
Host change (
src/v1/stage0/src/v1_compiler_infer.rs)record_lit_construction_field_names— a confident field-name resolver:follows
type X = Yaliases and searches the expected coproduct's arms beforethe global fallback. Returns
Nonewhen not confident, so anonymous /unresolved records never false-positive.
field_in_any_variant_named— an ambiguity-aware secondary scan, run onlyon would-be-errors, suppressing the case where a field is valid on a
same-named variant of another coproduct (e.g.
TextNode) while keepingteeth on truly-undefined fields.
CompilerDiagnostic::FieldNotFoundfor an unknown constructed field.Real bugs this surfaced (silently tolerated before)
The strict check immediately found pre-existing constructions with the wrong
field name — fixed here using the existing constructor helpers:
infer_ground_add_mvp.dag:NodeLocus { node }→node_locus(node:)(fieldis
anchor).lens/applicationtests:PortLocus { port }→port_locus(port:)(fieldis
anchor).Proof by execution (§5: executed-clean, not sampled-clean)
Full
claim_batchfloor green against currentmain(merged in):447 discriminating witnesses pass, zero field-not-found false positives.
Discriminating probe
BoolWitness { holds }goes red withfield 'holds' not found in type 'BoolWitness'. The sampled sweep had calledthe corpus clean; only the executed full floor surfaced the real bugs above.
Scope / stacking
{holds}removal). This branch carries those 2commits because the strict check reds on the
{holds}rows until they'regone. If Delete malformed dead {holds} BoolWitness rows; re-home 5 dark signal predicates #5272 merges first I'll rebase to drop the duplicate; this PR
otherwise subsumes it. Recommend merging Delete malformed dead {holds} BoolWitness rows; re-home 5 dark signal predicates #5272 first.
— optional-cardinality + templating risk, unmeasured. Tracked separately.
🤖 Generated with Claude Code