Type Bytes.fromList with an all-literal in-range list as total - #808
Merged
Conversation
A call to a recognized `List<Int>` refinement's smart constructor whose single argument is a syntactic list of integer literals, each inside the interval the refinement itself proves, cannot reach the constructor's error branch, so it types as the refined type and lowers to the carrier construction on every backend: `Bytes.fromList([0, 10, 255]) : Bytes`, no `?` and no `match`. Any other argument shape keeps `Result<Bytes, String>` unchanged — an identifier, a computed list, a computed element, an out-of-range or negative literal, or a magnitude beyond `i64`. The gate is derived, never named. A new analysis module builds it from two existing recognizers: the shape tier's `RefinementSmartConstructor` pattern supplies the carrier field and the validating predicate, and the packed layout's element-interval derivation (factored out for reuse) supplies the bound. Sharing the second step is what makes the rule safe on wasm-gc, where a packed carrier writes elements into a raw `i8` array with no range check: "the discharge admits it" and "the packed layout can store it" are now literally the same function. The table lives on the `SymbolTable`, the one place entry items and dependency modules are jointly in hand, so the typechecker and the HIR resolver read one table and cannot fork. Every callee spelling that denotes the constructor decides alike, because the wasm-gc backend flattens a dependency's constructor and all of its call sites — qualified and in-module — into one prefixed bare name before re-resolving. Lean re-establishes the gate's own claim as a `Subtype` obligation; since the predicate is compiled by well-founded recursion the tactic ladder gains a `simp [<predicate>]` rung named from the refinement's invariant. Dafny discharges the same fact as a subset-type constraint. A `Result` or `Option` constructor pattern whose subject is neither is now a type error, so the migration is loud rather than a match that silently walks off the end. Migrates the in-repo call sites, adds the diagnostic slugs and repair hints, and refuses discharged programs at the self-host boundary with an error naming the call sites, since the Aver-in-Aver resolver carries no refinement recognizer. Also migrates one literal-divisor leftover in the workflow-engine corpus that was failing thirteen units. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The discharge matched a call site by the callee's spelling, so a module that declares its own `fromList` over an imported refinement's smart constructor got two answers at once: the type checker obeyed Aver's shadowing rule and typed `fromList([1, 2, 3])` as the local function's return type, while the resolver rewrote the very same call into the refinement's carrier construction. The VM then printed a record where the program asked for a length. The two backends disagreed as well — after the wasm-gc flatten the local and the imported name are no longer spelled alike, so that backend declined the rewrite the VM performed. Both consult sites now key on the function identity ordinary name resolution assigned the call. The checker takes the identity out of the same lookup that produced the signature it checks the call against, and the resolver uses the callee it just classified: the discharge and the normal resolution must agree, or the discharge declines. Two recognized constructors that collapse onto one identity are fail-closed. The self-host rejection scan asks the resolver's own callee classifier, so it reports exactly the calls the rewrite fires on and no others. Keying on identity exposed an older divergence underneath. The wasm-gc resolver context carried no current module, so a program that spelled its own members qualified (`Local.fromList(xs)`) failed to resolve there and lowered to a trap, while every other backend called it. It now carries the declared module name like every other resolution site. Also: say in the resolver what actually makes the synthesised construction safe — the element interval shared with the packed layout, not the AST demotion scans, which never see the generated node; add a law over one body that holds a discharged construction and a fallible call at once; cover the self-host refusal on the replay path; and drop a recomputation of the refinement table the symbol table already carries. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.
Bytes.fromList([0, 10, 255])types as plainBytesand constructs the value directly — no?, nomatch. Any other argument shape keepsResult<Bytes, String>exactly as before: an identifier, a computed list, a computed element, an out-of-range literal ([65, 256]), a negative one, or a magnitude beyondi64. The empty list discharges vacuously.The gate is derived, not named
Nothing in the rule mentions
Bytes,fromList, or0..=255.src/analysis/literal_refinement.rsbuilds the table from two recognizers that already existed:analysis::shape::detect_module_patternssupplies theRefinementSmartConstructorpattern (carrier field + validating predicate);packed_sequence.rsaselement_interval_from_predicate, supplies the bound.Sharing the second step is the load-bearing part. A packed carrier writes elements into a raw
i8array with no range check, so "the discharge admits this value" and "the packed layout can store it" must be one predicate — they now are, literally the same function. A user refinement with a different range discharges against that range; a record with no validating constructor never discharges.The table lives on the
SymbolTable: the single place entry items and dependency modules are jointly in hand, and the right coupling — a call that cannot resolve cannot discharge. The typechecker and the HIR resolver read that one table, which is what keeps the checked and unchecked pipelines from forking.Two design points worth review
Spelling insensitivity is forced, not chosen.
flatten_multimodulerenames a dependency'sfromListto a prefixed bare name and rewrites both the qualified and the in-module call sites to it before the wasm-gc backend re-resolves. After the flatten the two spellings are indistinguishable, so a qualified-only rule would discharge before it and not after. Consequence:stdlib/bytes.av's ownverify fromListblock migrates; a non-literalOkcase was added so theResultcontract keeps its coverage. Two refinements sharing a bare constructor name are fail-closed.The lowering is a construct site, not a call.
RecordCreateis total on all four backends and needs no new IR node;proof_lower::multi_field_record_demotionsalready treats a construct site whose every value is a literal inside the proven interval as exactly as gated as the smart constructor. The rewrite runs on resolved HIR, so the AST-walking demotion scans still see only the constructor call. The alternative — a newUnwrapOkintrinsic — needs a VM opcode plus Rust and wasm-gc arms and has no workable Lean or Dafny story (Byteshas noInhabited;?is unsupported in Dafny).Proof backends
Lean emits
⟨[0, 10, 255], by …⟩, so the kernel re-establishes the gate's own claim instead of assuming it.allInRangeis compiled by well-founded recursion, whichdecidecannot evaluate through the elaborator, so the ladder gains a finalsimp [<predicate>]rung whose name is read off the refinement's invariant. Dafny discharges the same fact as a subset-type constraint.tests/fixtures/discharged_bytes_law.avis gated by both at budget 0; the 32-element stress case lives in a Lean-only fixture, because Dafny's default function fuel does not unfold a sequence that long.Migration is loud
A
Result/Optionconstructor pattern whose subject is neither is now a type error. Without itmatch Bytes.fromList([1, 2])would silently walk off the end at runtime. Two diagnostic slugs with repair hints were added (error-prop-non-result,pattern-subject-mismatch).Self-host
Refused, not silently diverged. The Aver-in-Aver resolver has no refinement recognizer — no type defs, no dependency ASTs, no interval derivation — so a mirror is not the three syntactic predicates the literal-divisor rule needed.
aver run --self-hostand the self-host replay backend now reject a discharged program with an error naming the call sites; the pre-gate symptom was a barefield access on non-record.Tests
tests/typechecker_spec.rs).i8array than the boxed build) and agrees with the VM; the out-of-interval literal declines on all three legs (tests/wasm_gc_packed_sequence.rs).tests/cross_backend_stress.rs,tests/rust_codegen_differential.rs).Notes
projects/k5_fdivfunctions that Type Int.div and Int.mod with a nonzero literal divisor as total #806's literal-divisor discharge made certifiable; they only surface now because thirteenworkflow_engineunits were failing to compile on an unmigratedResult.withDefault(Int.div(prev, 4), 0)left over from that PR.projects/workflow_engine/domain/time.avis migrated here so the corpus builds again.tools/coverage-baseline.jsonis left untouched — refreshing the headline is a separate call.src/analysis/already depends oncodegen::commonandcodegen::ModuleInfo, so the new module introduces no new layering direction.🤖 Generated with Claude Code