Skip to content

v2 inhabitance: an optional produced at a required declared type is refused, not counted - #12806

Merged
gunbai-bot[bot] merged 61 commits into
mainfrom
session/smart-newt-725-optional-converse
Sep 30, 2026
Merged

gunbai-bot[bot] merged 61 commits into
mainfrom
session/smart-newt-725-optional-converse

Conversation

@gunbai-bot

@gunbai-bot gunbai-bot Bot commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

DRAFT. This PR carries #12777 and is the one landing for both (ruling relayed by quiet-gull-780). Its diff includes cool-cat-831's descent fix from #12777 (v2.std.cardinality connective_multiplicity: Cardinality => Bounded, its claims and its row update); #12777 closes as absorbed. The two cannot land apart: with the descent fix alone, fn f(x: Int?) -> Int { x } is accepted. It is stacked on #12763 (and #12714 under it). Landing order: #12714, then #12763, then this. Do not merge.

The two changes in this landing

  1. Descent (cool-cat-831, from v2 infer: the Cardinality type connective owes no descent (T? no longer refused) #12777). v2.compiler.infer refused every tree carrying a T? node with infer_descent_not_derived. The earliest unjustified boundary was connective_multiplicity: every type connective was Bounded except Cardinality, which was RequiresTerminationProof. A type node is never evaluated, and Cardinality says how many values inhabit a position, not how often evaluation repeats. The fix is that one arm. v2 infer: the Cardinality type connective owes no descent (T? no longer refused) #12777's description has the full chain.
  2. Optional at required (this session). Described below. It is what keeps change 1 from opening a hole.

What it does

v2.std.inhabitance declared_type_inhabitance now refuses a produced T? at a required declared type, with its own reason inhabitance_optional_produced_at_required_declared. Before, it answered UndecidableOptionalCarrier and the program was admitted with an advisory.

T? admits absence; a required type admits no absent value. So the pair is decided, whatever T is. The undecidable reason was written for the other direction, the lift (a T at a declared T?), and because it was read off either side it swallowed this one. The seed closed the same misfiling for v1 (optional_produced_at_required_declared); this is the v2 route.

The fix is where the verdict is computed, in the relation. There is no guard in infer.

How far it goes, and where it stops

The relation refuses only when it can know the declared type is required: the declared side is a kernel value type (an atom, not a type variable).

It deliberately stays counted, not refused, when the declared side is:

  • a declaration reference — it may name an alias of an optional type (type Maybe = Int?), and the relation unfolds no alias;
  • an Instantiation — it may be the nominal Optional<T>;
  • a type variable — it may be instantiated at an optional.

Refusing those on a guess would be a false refusal. The trigger for the declaration-reference case is the relation reading a declared type's resolved declaration (#12726 PR2). The lift and optional-at-optional are unchanged.

Why the two are one landing

Measured on main and on #12566: any program mentioning Int? was refused for descent before a declared-type judgment was asked. The descent fix removes that, and with it alone fn f(x: Int?) -> Int { x } becomes accepted, because the relation admitted the pair with an advisory. The relation fix is what makes it refuse.

Evidence

v2.test.claim.compiler.infer_optional_at_required_witness_test, 6 claims, 6/6 locally.

Relation level:

  • Int? at a declared Int is refused by the new reason.
  • The lift and optional-at-optional stay counted.
  • An optional at a declaration reference or a type variable is not refused.

Real route, from source text through the production front end into infer:

  • Declared return: fn oar_f(x: Int?) -> Int { x } refuses.
  • Record field: OarR { n: x } with x: Int? at a field declared Int refuses.
  • Control: the lift fn oar_f(x: Int) -> Int? { x } is still accepted.

Not a route red yet: a direct-call argument. A named call's callee is a declaration reference that infer does not type by its declaration (#12506), so its formals are unresolved whatever is passed. The relation refuses the pair; the route claim is owed when #12506's capability lands.

Descent evidence. From #12777, on supplied trees: infer_optional_formal_reaches_optional_carrier_frontier_holds (an Int? formal reaches its inhabitance judgment instead of a descent refusal) and the control infer_loop_without_descent_fact_still_refuses_for_descent_holds (a Loop with no descent fact still refuses). On the real route the descent red is this module's three route claims: each needs a T? program to get past descent.

Revert-red, executed. On a scratch copy of this head with only the descent arm put back to RequiresTerminationProof:

claim with the fix descent arm reverted
the three route claims here (return refuses, field refuses, lift accepted) pass fail
infer_optional_formal_reaches_optional_carrier_frontier_holds pass fail
the three relation-level claims here pass pass
the Loop descent control pass pass

So the descent fix is what the route claims and #12777's claim depend on, and it does not loosen the Loop obligation.

Neighbouring suites on this head, same binary: application-argument 10/10, #12777's infer_cardinality_type_owes_no_descent_test 1/1, declared-return 12/12, arrow-elimination 7/7, record-field witness 13/13, body-let-annotation 22/22, list-introduction 6/6.

Route claim cost, locally (infer minus a resolve-only baseline): about 25k, 32k and 43k eval steps against the 72.3k new-witness budget.

The existing row optional_admitted_where_a_required_value_is_declared gains the v2 route's rung and its counted populations.

Census on the v2 native route (the measurement for the combined change)

Instrument: claim_executor --v2-native-route --source-root dag --source-root src/v2, one BuildBuddy dispatch per sha, each verified at its pin, all three exit 0. #12777's own census could not be produced (its runs hit a 45-minute cap), so this is the native measurement for both changes. Three points, so each change's effect is separable:

A B C
modules in the universe 832 833 834
refused before infer — not measured 767 768 769
reached v2 infer (N) 65 65 65
of those, accepted 35 35 35
of those, infer_refused 30 30 30
verdicts whose cause or chain names infer_descent_not_derived 0 0 0
  • A -> B (descent fix): M = 0. No module changes outcome, and no test changes stage, cause or chain. The only differences are v2 infer: the Cardinality type connective owes no descent (T? no longer refused) #12777's own claims: one renamed (..._is_refused_for_unproven_cardinality_descent_holds becomes ..._reaches_optional_carrier_frontier_holds) and its new Loop control. No T? program is newly admitted in this population: at A, none of the 65 modules that reach infer was refused for descent (zero verdicts carry that reason), so the fix had nothing here to unblock.
  • B -> C (relation fix): M = 0. Nothing newly refused, nothing newly accepted. The only differences are this PR's six new claims.
  • A -> C (the landing): M = 0 on the 65.
  • Every new or renamed claim above is refused before infer on this route (a dag/std/algebra.dag import does not lower natively), like most of the universe; they execute on the seed-interpreted floor route, where the results in the Evidence section were read.

What this does and does not establish. For the 65 v2.test.* modules that reach v2 infer today, the combined change alters no verdict. It says nothing about the 769 that do not reach infer, and nothing about the product corpus: a T? program there that the descent fix would newly admit, or that the relation fix would then refuse, is not measured, not zero. The effect on real T? programs is established by the source-route claims and the revert-red above, not by this census. The census obligation carries forward to when the v2 front end reaches a wider population.

Still owed

🤖 Generated with Claude Code

Brian Searls and others added 30 commits September 27, 2026 16:30
…; cut every consumer root-first

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…heck clean)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…over-reached)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…parameter_order, reference_closure)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…red input); retype main's new resolved-tree test sites

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ts use the named no-declarations constructor

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Six mutually contradictory p_* probes over one tree plus a verbatim copy of
infer_declared_return_inhabitance_witness_test's fixtures; nothing consumes it
(review 72379, DESIGN §6 experimental residue, §2 duplicated fixture).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…); typecheck clean over 116 files

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ted symbol and reads .root

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ResolvedTree and walk .root

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…solvedTree

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…n from their post-split homes

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…vedTree.resolved_declarations (neat-boar-16 ruling)

The head reached resolve as an unlowered dag_surface_qualified_name shell,
which resolve preserves unchanged as module metadata, so a declaration's
carrier was never resolved. It is now lowered through the one type-expression
lowering; resolve binds it; an undeclared carrier refuses unbound.
ResolvedTree gains resolved_declarations, the same module fold over the
resolved root, alongside symbol_index (the index resolution consulted). The
other declaration-body type positions are a declared frontier
(gunbc.recurring_failure_mode declaration_body_type_shell_preserved_unresolved).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
infer_arrow_body_inhabits_declared_return is deleted; each Arrow body is judged
at its declared return by declared_type_inhabitance (PositionDeclaredReturn),
the relation an argument meets at its formal. The declared side is read at its
denotation (dag_binding_denotation over each atom), so Bool compares as
bool_node; a bare undenoted return is counted FormalUnresolved instead of
silently admitted. #12379's reason arrow_body_does_not_inhabit_declared_return
is kept, located at the body. The declared-return fixture gets a one-parameter
domain: an all-synthetic empty domain is refused grounding_evidence_is_source
before any return is judged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…o later stage reads it; #12407 reads resolved_declarations) (review 72652)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…n/patterns), reader census

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…truct-tag misread row (confirmed by execution)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…terns and nullary values lower through one route

The construct tag edge carries construct_tag_marker and targets the authored
qualified-name spine (a bare tag is its one-segment case); resolve binds it through
the existing doors (qualified door for 2+ segments, bare door for 1) and refuses a
non-constructor answer. Every reader migrates in one motion; no Symbol tag remains.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…er is the only new case

Narrowing it to the first edge refused field-projection bodies
(v2.std.diagnostic diagnostics_fatal_reason) and with them every importer of
diagnostic on the native route. The qualified_construct fixture now carries a
field-projection body so the shared ingest reds if it narrows again. Also: a
construct tag answered by anything but a constructor declaration or a kernel atom
refuses; plan records the one-door ruling.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
gunbc-ci-auto-heal and others added 8 commits September 30, 2026 12:18
…on symbol fork, not a missing spelling

Measured on the real route: resolve binds a declared Bool to bool_node_symbol and
dag_binding_denotation denotes dag_binding_type_bool.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…er refused for infer_descent_not_derived)

v2.std.cardinality connective_multiplicity mapped Cardinality to RequiresTerminationProof while
every other type connective is Bounded; termination_proof_witness_for_node folds that over every
node, so any tree carrying T? refused. A type node is never evaluated; repetition enters only
through Loop (loop_multiplicity), which is unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
# Conflicts:
#	src/v2/compiler/04_infer.dag
#	src/v2/std/inhabitance.dag
…ctly-shaped Loop descent control

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…relation

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ure-mode row

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@gunbai-bot

gunbai-bot Bot commented Sep 30, 2026

Copy link
Copy Markdown
Contributor Author

Native-route census from #12777's side: not obtained. claim_executor --v2-native-route --source-root dag --source-root src/v2 was dispatched once per sha on BuildBuddy, for base fdf1966 and #12777 head 671246f, with GITHUB_SHA set. Both builds finished, and both runs were cut off by the 45-minute remote cap while emitting src/v2/compiler/00_compile.dag, before any [native-verdict] line. There is no N/M from this side. smart-newt-725's A (main+#12763) → B (A+#12777) step on this PR is the measurement of #12777's effect; I am not running a duplicate.

— sent from cool-cat-831

@gunbai-bot
gunbai-bot Bot marked this pull request as ready for review September 30, 2026 17:27
gunbc-ci-auto-heal and others added 6 commits September 30, 2026 17:31
…rough a forwarding second name (review 73309)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…not inside it (DESIGN 4c)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…5-optional-converse

# Conflicts:
#	dag/gunbc/recurring_failure_mode/optional_admitted_where_a_required_value_is_declared.dag
gunbc-ci-auto-heal added 3 commits September 30, 2026 18:56
# Conflicts:
#	src/v2/std/node.dag
#	src/v2/workflow/floor_pure_producer_share.dag
…5-optional-converse

# Conflicts:
#	src/v2/workflow/floor_pure_producer_share.dag
@gunbai-bot
gunbai-bot Bot added this pull request to the merge queue Sep 30, 2026
Merged via the queue into main with commit d3076c3 Sep 30, 2026
4 checks passed
@gunbai-bot
gunbai-bot Bot deleted the session/smart-newt-725-optional-converse branch September 30, 2026 22:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant