Repository navigation
feat: restore the Elm constraint tree, add representation coverage and fix type-system crashes - #44
Merged
Merged
Conversation
nash build strips tests blocks before canonicalization, so module-level imports used only by tests were reported as unused. Signed-off-by: microproofs <kwhitemsg@gmail.com>
… bodies Signed-off-by: microproofs <kwhitemsg@gmail.com>
Signed-off-by: microproofs <kwhitemsg@gmail.com>
Signed-off-by: microproofs <kwhitemsg@gmail.com>
Signed-off-by: microproofs <kwhitemsg@gmail.com>
Signed-off-by: microproofs <kwhitemsg@gmail.com>
Signed-off-by: microproofs <kwhitemsg@gmail.com>
Signed-off-by: microproofs <kwhitemsg@gmail.com>
Kind checks, recovery closure, stranded lookups, scheme lookups, and the per-scope grouping in finish scanned every earlier record. Each is now an index that follows union-find merges through the new UnionFind::merges log. Debug builds keep an ORACLE that recomputes the old scan and asserts the same result on every call; step 13 removes them after the constraint tree and coverage changes land. Signed-off-by: microproofs <kwhitemsg@gmail.com>
… and evidence Inference, superclass entailment, and evidence each had their own impl selection loop. They now call nash_ast::head::select_impl with a SelectPolicy: Defer (inference) lets any undecided candidate defer the selection; Exact (rigid or ground arguments) treats it as not proven. Every candidate is matched under both policies. A second match panics as an impl coherence violation instead of keeping the first or the last: canonicalization already rejects overlapping impls. Signed-off-by: microproofs <kwhitemsg@gmail.com>
…lications
check_representation_consistency only grouped predicates on plain type
variables, so a context such as (Big ('f 'a), Little ('f 'a)) passed
canonicalization. In inference, the givens then proved an empty
representation set, which in_class read as proof of every class: both
disjoint blanket impls matched and select_impl stopped the compiler.
Group predicates by location-free type identity (nash_ast::same_type) and
report the existing contradictory-representation error with the subject
type. in_class now asserts the proven set is non-empty.
Signed-off-by: microproofs <kwhitemsg@gmail.com>
nash-constrain again turns the canonical AST into Elm's constraint tree and nash-solve walks it, in place of direct inference. The generator and walker are the pre-removal files (recursive, Elm order) plus the features added since: SaveType node metadata, module tests (TestScope), overloaded Bool and Unit literals, assert/fail/todo/trace/comptime, native pair patterns, diagnostic regions, alias identity of annotated functions, fix A, gap B, and the step 03/04 machinery. Let branches without definitions record no schemes (decision N4). Types convert once per constraint, as in Elm. Definition types convert on their own, so an annotated definition publishes its declared type: a call with more arguments in the same module no longer replaces an alias in its annotation, and importers find the impls of that alias. Direct inference lost the alias there (regression test added). No existing snapshot changes. Signed-off-by: microproofs <kwhitemsg@gmail.com>
… annotation A local annotation shares the enclosing annotation's type variables, as Elm's rtv does (Constrain/Expression.hs constrainDef). Canonicalization checks each context alone, so 'Little 'a' inside 'Big 'a => ...' passed, and the joined given frames reached impl selection with an empty proven set, where an assert stopped the compiler. enter_givens now groups the representation givens of all active frames and the new frame by subject, as in_class compares them, and reports a contradiction with the existing contradictory-representation error at the local definition. The subject is poisoned, so obligations on it are dropped and the frame's second entry (ambiguity loop) does not report again. check_ambiguity shares the grouping. A requirement implied by another one (Storable next to Big) is left out of the report. The in_class assert stays: contradictory givens reaching impl selection are a compiler bug. Its comment now names both checks. Signed-off-by: microproofs <kwhitemsg@gmail.com>
check_annotation rejects a context that admits no representation once its
superclasses are added (Foo 'a with trait Foo ('a : Big) next to
Little 'a). check_impl ran only the formation check; the superclass check
reached an impl only through the annotation of each method it defines. An
impl that defines no method skipped it, and superclass entailment then saw
an empty proven set, so both disjoint blankets matched and select_impl
stopped the compiler.
check_impl now runs the same superclass check (check_superclass_consistency,
moved out of check_annotation), so such an impl gets the existing
contradictory-representation error at its context. kinds::in_class asserts
a non-empty proven set instead of silently proving every class, as the
solver's in_class does.
Signed-off-by: microproofs <kwhitemsg@gmail.com>
Impls are disjoint only over finite types. A let-bound definition's
wanted predicates are resolved when it is generalized, before the occurs
check of the enclosing binding, so a cyclic argument reached impl
selection. Two impls that are disjoint on finite types both matched it
(('a, 'a) and ('a, 'a -> 'b) for f f), and select_impl stopped the
compiler; an impl that matches at every depth repeated its own context
until the work limit and gave a false impl-resolution-limit error.
resolve_wanted now defers a wanted whose arguments reach a cycle, before
impl selection and relational improvement. The binding's occurs check
reports the infinite type and poisons it, as Elm's CLet occurs fold does,
which drops the wanted. The cycle check (occurs::reaches_cycle) follows
the edges of Elm's occurs but visits each variable once, so types with
shared parts stay linear. The select_impl doc comment now states the real
invariant.
Signed-off-by: microproofs <kwhitemsg@gmail.com>
A rigid type variable at an impl-head position that tests a
representation class kept impl selection from deciding:
same : Eq 'a => list 'a -> list 'a -> bool was rejected with
"No impl for Eq (list 'a)", although Base has an Eq list impl for every
element representation.
Inference now proves such a predicate by coverage (audit item 08,
owner decisions D2, N2, N3):
- nash-ast head::cover_impls finds the rigid subjects, checks the limit
first (the given closure holds the wanted trait at each subject), then
selects an impl for every admitted Big/Const/Term cell. One work budget
pays for all of it.
- nash-solve runs it in resolve_wanted when selection defers or misses,
and proves each covered impl's context under its cell with a
non-deferring prover (givens, representations, impls, coverage again).
The proofs are recorded as Solution::Specialize subs, so the growing
evidence check rejects polymorphic recursion through coverage. A missing
element trait or branch context is a missing-constraint error; an
uncovered cell keeps today's missing-impl text.
- The evidence is Evidence::Specialize { trait_, args }. Code generation
selects the impl for each use (method B): the demand analysis gives each
template the type variables that coverage selects with, propagated
through calls, and the specialization key holds their types.
Superclass proofs in nash-can do not use the rule (N3). nash-can is not
changed.
Signed-off-by: microproofs <kwhitemsg@gmail.com>
wrapper : tag 'p -> bool; wrapper t = same t t, with
same : Ord 'a => 'a -> 'a -> bool, type-checked, but nash test stopped
with "superclass resolution requires a ground type, found p". No use
fixes the phantom 'p, so it stays open in code generation, and the
superclass step of Grounder::one rejected every type variable before
resolution.
nash_solve::evidence::resolve now keeps open type variables. An open
variable equals only itself, so impl heads bind it and reflexive Lift
proves Lift 'a 'a. Resolution reports NonGround only when a decision
needs the variable: impl selection (the new nash_ast::head::Open under
SelectPolicy::Defer), a representation test, a reflexive Lift that the
variable could decide, or a requirement that formation of a type
leaves. A predicate whose open variable no decision tests, such as
Keep ('a -> int) without a function impl, reports MissingImpl.
In nash-codegen, the Super arm keeps only the arity check: inspect_type
loses its require_ground mode, and Error::NonGround is removed. A rigid
variable that would select a superclass impl is already a
missing-constraint error in type checking (new solver test).
Signed-off-by: microproofs <kwhitemsg@gmail.com>
Step 03 replaced linear scans with indexes for kind contracts, recovery closure, stranded lookups, scheme lookups and the per-scope grouping in finish. Debug builds kept each old scan as an ORACLE and asserted on every call that it agreed with the index, so debug runs still paid the quadratic cost. The constraint tree (05) and coverage (08) changes have landed, so the oracles go: the debug-build call sites, the scan helpers, the stranded_handles field that fed one of them, and the two randomized tests that compared the indexes with the scans (contract_index_agrees_with_the_contract_scan and historical_closure_and_stranded_lookup_agree_with_the_scans). Signed-off-by: microproofs <kwhitemsg@gmail.com>
…cannot decide 09 made open-type evidence select under SelectPolicy::Defer, where any undecided candidate defers the whole selection. With impl Label (twins 'a 'a) and impl Label (twins int bool), the open twins p p matches the first impl for every p, but the second cannot decide p, so resolution failed with NonGround. A reflexive Lift that p could decide also returned NonGround before impl search, although impl Lift (twins 'a 'a) (twins int bool) proves the pair. Type checking treats p as rigid and selects those impls, so both programs passed nash check and then stopped nash test with "cannot resolve superclass: NonGround" (driver test phantom_pair_through_a_superclass). The resolver uses SelectPolicy::Exact again, so a selected impl wins over an undecided candidate. Under Exact, select_impl now returns Deferred when nothing is selected and a candidate is undecided, so the resolver still reports NonGround there. Entailment never sees Deferred: its rigid types decide every test. An undecided reflexive Lift falls through to impl selection and reports NonGround only when no impl is selected. docs/traits.md no longer says that reflexive Lift needs a Big representation. It needs nominally equal arguments only: Lift int int and Lift () () resolve, and canonicalization accepts it for Big and Const types. Signed-off-by: microproofs <kwhitemsg@gmail.com>
render_type::tuple and record joined their entries with Doc::cat, which is Elm's <> and has no break point. Elm's Render/Type.hs joins them with D.cat, wl-pprint's cat: the entries stay on one line when they fit and go one per line otherwise. A tuple wider than the page therefore broke only inside its last entries and drifted to the right. Doc::pcat is D.cat (a group of entries separated by LineBreak). tuple and the record fields use it. No existing snapshot changes. The new source_pipeline_tuple_wider_than_the_page shows the layout for a type mismatch. Signed-off-by: microproofs <kwhitemsg@gmail.com>
missing_constraint and missing_impl rendered the wanted predicate to a string at width 80 and spliced it into Doc::text. A predicate wider than 80 columns rendered with a line break, so Doc::text's assertion stopped nash check with "Doc::text must not contain newlines". Both help lines are now built from document parts, as the messages already were. with_predicate wraps each message and help sentence of the two producers in hang 4 (sep [lead, predicate]): a sentence that does not fit puts the predicate on its own line indented by 4, and a sentence that fits renders exactly as before. Three existing snapshots change: two help lines of 88 and 97 columns (via_requires_generator, map_lift_rejects_inferred_native_pair_components) and an 81-column representation message (missing_storable_constraint_for_list_element) now break before the predicate. Doc::text and its assertion are unchanged. Signed-off-by: microproofs <kwhitemsg@gmail.com>
The missing-field hint concatenated "Available fields: " with a vertical record, so the record's later lines started at the left margin instead of under its opening brace. Doc::align keeps them under the brace. Six snapshots change, each only in that indentation. Signed-off-by: microproofs <kwhitemsg@gmail.com>
Constraint::TestScope wrapped each module test so that the walker set a test_scope flag while it solved the test, and DeferredField copied the flag to choose between tables.test_fields and tables.fields. The scope is a lexical fact the generator already knows, so the walker no longer keeps state for it. Rtv, the generator's lexical environment, becomes a struct with the rigid variables and a tests flag, and constrain_test starts from Rtv::tests(). Constraint::Record and Constraint::Field carry tests, set from Rtv at the accessor, access and update sites, and from pattern::State (empty_state(tests)) at record patterns. The solver reads the flag from the constraint. TestScope and Solver::test_scope are removed, so the constraint language is Elm's plus SaveType. Field visibility is unchanged and no snapshot changes. The new test_imports_grant_field_visibility_to_every_field_form_in_tests covers the accessor, update and pattern forms in both scopes; its snapshot was recorded before this change. Signed-off-by: microproofs <kwhitemsg@gmail.com>
Trait search, overlap checks, superclass entailment, evidence and code generation each had their own work or depth budget. They counted work differently, so a program could pass `nash check` and then fail `nash build` with no location (depth 178 to 255 of a nested `impl C 'a => C (wrap 'a)`). Replace them with one step counter, `nash_ast::fuel`, that the driver installs around each module compile. The command line runs with unlimited fuel. The language server runs with 10,000,000 steps per module, so a looping program reports `nash::type::out_of_fuel` instead of freezing the editor. Cycle detection and the polymorphic recursion error stay. Signed-off-by: microproofs <kwhitemsg@gmail.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.
Summary
This branch holds the type-system audit work. It restores Elm's constraint tree in place of direct inference, fixes four type-system crashes and a crash in long diagnostics, fixes four wrong results in type checking and code generation, adds the representation coverage rule for impls that are split by element representation, and replaces the separate search budgets with one compile fuel counter. Twenty-four commits, seventeen changesets.
Behavior changes:
same : Eq 'a => list 'a -> list 'a -> boolcompiles. When the representation of a rigid variable selects the impl, the annotation must state the trait at that variable. A missing one is a missing-constraint error such asEq 'a, not a missing impl for the container.nash testornash buildwith "superclass resolution requires a ground type".(Big ('f 'a), Little ('f 'a)),Little 'ainsideBig 'a => ..., and an impl context that contradicts a superclass of the impl's trait.type alias Xs = List Intworks withList.lengthand other functions over'f 'a.nash check,build,testanddocshave no search limit. Programs that the fixed work and depth limits rejected now compile, such as an impl context that rewraps its argument used at 200 nested types. Onmain, depths 127 and up failed. A looping impl now runs until stopped.COMPILATION STEP LIMIT(nash::type::out_of_fuel) after about a second and a half instead of freezing the editor. This one error replacesnash::type::impl_resolution_limit,nash::names::impl_pattern_limit, and the code generation specialization, evidence and recursion limits.nash checkno longer crashes with "Doc::text must not contain newlines" when a missing-constraint or missing-impl predicate is wider than 80 columns. When such a sentence does not fit the line, the predicate moves to its own line, indented by 4.Versioning: minor bumps for
nash-ast,nash-can,nash-cli,nash-codegen,nash-constrain,nash-driver,nash-language-server,nash-reportandnash-solve; patch fornash-docs. See.sampo/changesets/.Elm constraint tree (
refactor(types): restore the Elm constraint tree)nash-constraingenerates Elm's constraint tree from the canonical AST again, andnash-solvewalks it. The direct-inference files are deleted.0ed0c75a("refactor(types): infer directly from AST") deleted. The features that six later commits added to direct inference are ported into them: per-node type records for code generation (SaveType), module tests, overloadedBoolandUnitliterals,assert,fail,todo,traceandcomptime, native pair patterns, and the regions that diagnostics point at.annotated_alias_result_keeps_its_impl_for_importers_after_a_wider_call.refactor(constrain): carry the tests scope on record field constraintsremoves the Nash-onlyTestScopeconstraint.RecordandFieldconstraints carry whether they come from a module test, so the solver keeps no state for it. The constraint language is now Elm's plusSaveType. Field visibility does not change.test_imports_grant_field_visibility_to_every_field_form_in_testscovers the accessor, update and record-pattern forms in both scopes.Crash fixes
Each fix has a test for the audit case that crashed.
fix(traits): reject contradictory representation contexts on type applicationsgroups representation predicates by type, not only by plain variable.fix(solve): reject representation givens that contradict an enclosing annotationchecks a local annotation together with the enclosing annotations, which share its type variables as in Elm.fix(traits): check every impl context through its superclassesruns the superclass check incheck_impl. An impl with no methods used to skip it.fix(solve): do not select impls for infinite typesmakes the constraint wait for the occurs check of its binding, as in Elm.occurs::reaches_cyclevisits each variable once.Representation coverage (
feat(traits): select representation-split impls by coverage)nash_ast::head::cover_implsfirst checks that a given states the trait at each rigid variable that selects the impl. It then selects an impl for every admitted representation of those variables.Evidence::Specialize. Code generation selects the impl for each use and keeps the selecting types in the specialization key.nash-cando not use the rule.Open types in superclass evidence
fix(solve): resolve superclass evidence for open phantom types.nash_solve::evidence::resolvekeeps open type variables. An open variable equals only itself. Resolution reportsNonGroundonly when impl selection, a representation test, a reflexiveLiftor the requirements of a type need the variable.nash_ast::head::Opengives impl selection that view.nash_codegen::evidence::Error::NonGroundis removed.phantom_type_through_a_superclassfailed before the fix. The solver testopen_type_that_selects_a_superclass_impl_is_a_type_errorshows that type checking rejects the case where the open variable would select the impl.fix(solve): let a selected impl prove an open type that another impl cannot decidekeeps impl selection underSelectPolicy::Exact, so an impl that matches whatever the variable is wins over a candidate that only the variable could decide. An undecided reflexiveLiftgoes on to impl search. The driver testphantom_pair_through_a_superclassfailed before this commit with "cannot resolve superclass: NonGround".Solver diagnostics
fix(solve): unify type applications with transparent aliases by their bodiesfix(solve): keep a failed annotated body's annotation for its callersfix(solve): do not report types stranded by a failed call as ambiguousLong diagnostics
fix(report): break tuple and record entries as Elm's D.cat doesaddsDoc::pcat, Elm'sD.cat. Tuple and record entries were joined with no break point, so a wide type broke only inside its last entries and drifted to the right.source_pipeline_tuple_wider_than_the_pageshows the new layout.fix(report): build long trait help lines from document partsfixes the crash. The help line rendered the predicate to a string and placed it in one piece of text, which cannot hold a line break. Each sentence with a predicate is now laid out like the rest of the report, and a sentence that does not fit moves its predicate to its own line. Sentences that fit render as before. Three existing snapshots of 81 to 97 columns now wrap.fix(report): align the record after Available fields under its bracechanges six snapshots, in indentation only.Compile fuel, shared trait search and indexes
refactor: replace search budgets with one compile fuel counter. Trait search, overlap checks, superclass entailment, evidence and code generation each had their own work or depth budget, and they counted work differently. On this branch before this commit, a nestedimpl C 'a => C (wrap 'a)used at depth 178 to 255 passednash checkand failednash buildwith no location.nash_ast::fuelis now the one step counter. The driver installs it around each module's compile (Unlimitedfor the command line,Limited(10_000_000)for the language server), and every search loop callstick(). The constantsWORK_LIMIT,DEPTH_LIMIT,MAX_SPECIALIZATIONSandMAX_TYPE_DEPTH, the 4096-step normalization limit, and every budget parameter are deleted. Superclass cycle detection and the polymorphic recursion error stay.deeply_nested_impl_context_checks_and_builds_without_a_depth_limitchecks and builds the depth-200 case, and fails before this commit.a_looping_program_reports_running_out_of_fuel_instead_of_hangingcovers the language server.refactor(traits): keep the trait search limits in one place,feat: raise the trait search and specialization depth limits to 256andtest(codegen): build the deep type from the depth limitare superseded by the fuel commit. Their changesets are removed.refactor(traits): share impl selection between inference, entailment, and evidenceaddsselect_implwithSelectPolicy::DeferandSelectPolicy::Exact. A second matching impl stops the compiler as a coherence bug.perf(solve): index kind contracts, recovery edges, and scheme recordsreplaces scans of every earlier record with indexes that follow union-find merges.refactor(solve): remove the step 03 scan oraclesremoves the debug-build checks that compared each index with its old scan, and the two randomized tests that drove them.Build and examples
build: optimize the compiler crates in the dev profilefix(examples): move test-only imports into tests blocks.nash buildstrips tests blocks, so imports that only tests use were reported as unused.Not in this PR
plan-11.Test plan
cargo fmt --all -- --checkcargo clippy --all-targets --all-features -- -D warningscargo nextest run --workspace(3,936 tests)nash buildandnash testinexamples/vesting(10 tests pass)nash buildandnash testinexamples/order. The propertycompare is antisymmetricfails becauseinvertreturns its argument unchanged. The example has had that sinceltmkrkqv.nash checkgives a diagnostic for each of the 13 audit reproductions, with no crash.nash checkandnash buildsucceed at nesting depths 177, 178 and 200 ofimpl C 'a => C (wrap 'a).