Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,7 @@ data accepted_source_emits_uncompilable_target: RecurringFailureMode = Recurring


"OCCURRENCE, 2026-09-24 (wise-hawk-615, routed from stern-raven-24's native App Attest row gunbc#12251, whose emitted crate refused at rustc with 45 errors in three emitter classes and one runtime-library gap). Each was worked to its earliest unjustified boundary (DESIGN section 6b) rather than at the call site. (C) STRING ORDERING: `a < b` over Timestamp, an alias of String (extdeps.standards.rfc_5280 x509_validity_at, gunbc.auth.approval_capability utc_instant_before). Timestamp comparison needed no new typed ordering: the interpreter already orders Strings byte-lexicographically, so the defect was the emitter's. The boundary was the operand classifier v1.compiler.emit_rust rust_operand_realization_of_type, which filed the corpus String as OperandIdentityUnavailable although the type renderer realizes it as the host text carrier by is_host_text_carrier_type; ordering then refused by operator class while `==` on the same operand passed. Repair: std.operator_realization HostRealizationReason gains HostTextCarrier and the classifier reads the renderer's own predicate, so both answer one question once; is_string_comparison admits the four ordering operators through the same host-string seam as equality, and an ordering over an OPTIONAL String refuses at emission (the interpreter has no Null-vs-Str ordering; Option's None-first order would be fabricated). (A) PRESENT BINDING: `match opt { null => a o => b }` bound the whole Option in emitted code (E0308, E0609) while v1.compiler.infer narrows `o` to the present value. ALL THREE match renderings now read the checker's optional_scrutinee_binding_is_present over match_unguarded_absent_arm_index and emit `Some(o)` in exactly that arm, never re-deriving the narrowing: emit_typed_match_arm_strs for ordinary matches, and emit_typed_tco_match_arm for matches inside the tail-call lowering, which the native App Attest run surfaced separately (extdeps.standards.rfc_5280 read_extensions_list, the one error left after the first renderer was repaired); and emit_typed_match's String-literal re-emission (needs_string_from), where a review of this PR found the decision dropped -- there a string-literal arm over an optional scrutinee also emits `Some(ref __s)`, since a literal matches only a present value. (D) OCTETS: std.bytes bytes_octets and utf8_encode_bytes were interpreter-intercepted builtins whose .dag bodies were placeholders -- `[pure_dag_seam_unreachable()]`, a one-element list of `1 / 0`, and `s as Bytes`, a cast no Rust row realizes and which emitted a runtime panic. Both are now declared HostRealizedSeams (self-call bodies, std.primitive_projection rows, std.primitive_identity declarations with SourcePreservingOrder traversal facts, extdeps.languages.rust.emit rt_function_registry rows, v1.runtime_rust bodies); bytes_octets is typed List<Int> under the bounded_natural_arithmetic_evaluated_as_unbounded_int ruling, since UInt8 has no emitted realization that holds an octet and bytes_qualified_octets is the one boundary observing the range. Two further links surfaced by execution: a REALIZED seam's self-call was lowered by the name-keyed tail-call rewrite into a non-terminating loop (rust_host_seam_is_realized now exempts it, see host_seam_self_call_diverges_when_its_arm_is_absent), and rustc's deny-by-default unconditional_panic refused every crate reaching std.bytes because of pure_dag_seam_unreachable's constant `1 / 0`. The first cut relaxed that lint for all generated code; review 71098 refused it as widening a real check to admit one placeholder, and the placeholder was the earlier link: pure_dag_seam_unreachable is now a rostered HostRealizedSeam too, refusing in the interpreter with a typed error naming the seam and panicking by name in v1_rt, so the lint stays on. EVIDENCE: the fixture test.fixture.emitted_interpreted_parity.string_order_octets_present_binding exits 0 interpreted and requires the thirteen-field line `T F T F T 6 2962370309 97 0 989 ex none q` -- fields 1-5 string ordering, 6-7 octets, 8-9 the ordinary-match present binding, 10 the tail-call-match present binding, 11-13 the String-literal re-emission -- with a RED control that exits 1 printing the observed line; its emitted crate builds and runs to the same line. The enrolled claim module test.claim.emitter_string_order_present_binding_witness_test carries the emission half: host-string ordering over an alias, the named refusal for an ordering over a String? (which inference admits today, so the arm is reachable), and the present binding in all three match renderings. The integration consumer is `gunbc test //gunbc/instruments:native-app-attest` on gunbc#12251. RUNG unchanged at mitigatable for the class: required CI still neither emits-and-compiles a fixture closure nor executes integration targets, which is this row's existing trigger.",

"OCCURRENCE, 2026-09-25 (loyal-boar-23, found by swift-bat-511 on native App Attest P2b): CALLING A LOCALLY BOUND FUNCTION VALUE. extdeps.apple.app_attest first_assertion_refusal folded a List<fn() -> AssertionRefusal?> and called the lambda-bound element `r()`; gunbc#12251 rewrote that one fold as a named AssertionStep sum, leaving the idiom refused everywhere else. The brief placed the defect in emit_rust's callee resolution; re-deriving the chain (DESIGN 6b) placed it three links earlier and found two more downstream. (1) INFERENCE, the earliest unjustified boundary: v1.compiler.infer child_type_node, the total read of a child position's type, returned a child's inferred slot whenever it had one, and an Arrow type node carries its RETURN type there (make_callable_type) -- so the element of List<fn(Int) -> Int> was Int, a lambda parameter bound under it was not callable, and the call refused as \"function 'r' not found in scope\" in `gunbc run` as well as in emission. An Arrow child is now its own type -- landed independently by gunbc#12295 (receipted on arity_proxy_for_callability_misroutes_a_found_local) while this change was in review; this change consumes it rather than restating it. (2) INFERENCE, the list literal: a lambda's typed node carries its BODY type, and a list literal took its first element's type, so [fn() { none }] against List<fn() -> Int?> was typed List<T>; where the declared element is an arrow and any element is a lambda the declared arrow is the element type, every element still judged against it. (3) EMISSION, two value positions of a lambda: a lambda literal in a list was a bare closure (E0308, every closure its own type), and is now the arrow carrier through the record-field row (rust_callable_field_value_wrap) bound at the rendered arrow by a typed let, not an `as` cast, which cuts the expectation typing Some(7) as Option<i32> (E0271); a let-bound lambda was `|x|` with no call site to type x from (E0282 at x.clone()) and is now emitted with typed parameters through emit_typed_collection_lambda, staying a bare closure so it still enters an impl Fn parameter. The call sites themselves (r(), f(v), g(v)) needed no change. EVIDENCE: test.fixture.emitted_interpreted_parity local_callable_value_calls exits 0 interpreted and its emitted crate, driven by a two-line harness over its library, prints the same line `6 8 6 7 9 0` with main returning ExitSuccess; with the expected line mutated both sides refuse (interpreter exit 1, emitted main ExitFailure carrying the observed line); on the pre-fix seed the fixture refuses at resolve. test.claim.local_callable_value_call_witness_test carries three inference controls (fold, unary fold and map lambda parameters over callable elements) and two emission controls (the list-literal carrier and the typed let), all five FAIL on the pre-fix seed and PASS here. RESIDUE, stated: an UNANNOTATED let-bound lambda (`let h = fn(x) { x * 2 }`) has no parameter type a Rust closure can be written at and still emits `|x: _|`-equivalent (E0282); a non-lambda element (a let-bound closure variable) in a List<fn..> keeps its bare rendering. RUNG unchanged at mitigatable: required CI still neither emits-and-compiles a fixture closure nor runs its crate, which is this row's existing trigger.",
"OCCURRENCE, 2026-09-24 (lively-koi-275, gunbc#12208 MQ-1): A FIELD BOUND UNDER A NESTED VARIANT PATTERN KEEPS ITS BOX. The `diagnostics` field of `v2.std.diagnostic` `Outcome` is a recursive Optional, so `v1.compiler.emit_rust` `needs_box_wrapping` boxes it. At a SINGLE-LEVEL arm `Accepted { value: v, diagnostics: d } =>` the emitter binds the field by `ref` and unboxes it (`let d = diagnostics.as_ref()`). Under a NESTED arm `Present { value: Accepted { value: facts, diagnostics: d } } =>` it bound the Box itself, and passing `d` to `v2.std.diagnostic` `diagnostics_merge` failed rustc with E0308 (expected `Option<Rc<NonEmptyDiagnostics>>`, found `Box<Option<..>>`) in the emitted `v2.compiler.infer`. The `.dag` was accepted with zero diagnostics, and only the non-required emit-build lane (`//gunbc/instruments:self-host`) saw it. The author-side repair split the match into two single-level arms, which the rest of the corpus already does by habit. That habit is an unstated workaround: it explains why the population is small and is not evidence that it is zero. The existing trigger of this row covers it: the emitted carrier comes from the declaration, and nested-pattern binders must go through the same unboxing join as single-level arms.",
],

Expand Down
54 changes: 54 additions & 0 deletions dag/test/claim/local_callable_value_call_witness_test.dag
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
module test.claim.local_callable_value_call_witness_test

import std.types { Bool, Int, String }
import gunbc.compile_census_probe { total_blocking_count_for }

// Enrolled controls for calling a LOCALLY BOUND function value (the extdeps.apple.app_attest
// first_assertion_refusal idiom before gunbc#12251 rewrote it). The EXECUTION half -- one program
// interpreted and emitted, built and run to the same line -- is
// test.fixture.emitted_interpreted_parity local_callable_value_calls, receipted on
// gunbc.recurring_failure_mode accepted_source_emits_uncompilable_target.
//
// Inference: a lambda parameter bound under a List<fn..> element is callable. v1.compiler.infer
// child_type_node read an Arrow element's inferred slot, which is the arrow's RETURN, so the element
// of List<fn(Int) -> Int> was Int and `r(1)` refused as "function 'r' not found in scope".
//
// Emission: a lambda literal in a List<fn..> is the arrow carrier, each bound at the rendered arrow
// (a Vec of bare closures is E0308, and an `as` cast would type Some(7) as Option<i32>); a let-bound
// lambda keeps a bare closure with TYPED parameters (an untyped `|x|` is E0282 at x.clone()).

data fold_lambda_param_called_source: String = "module lcv_fold\n\nfn first_some(rs: List<fn() -> Int?>) -> Int? {\n fold(rs, init: none, f: (acc, r) => match acc {\n Present { value: a } => Present { value: a }\n Absent => r()\n })\n}\n"

data unary_element_called_source: String = "module lcv_unary\n\nfn sum_applied(fs: List<fn(Int) -> Int>, v: Int) -> Int {\n fold(fs, init: 0, f: (acc, f) => acc + f(v))\n}\n"

data map_element_called_source: String = "module lcv_map\n\nfn run_all(rs: List<fn() -> Int>) -> List<Int> {\n rs |> map(r => r())\n}\n"

test fn a_lambda_parameter_bound_to_a_callable_element_is_callable() -> Bool {
total_blocking_count_for(source: fold_lambda_param_called_source) == 0
}

test fn a_unary_callable_element_is_callable() -> Bool {
total_blocking_count_for(source: unary_element_called_source) == 0
}

test fn a_map_lambda_parameter_bound_to_a_callable_element_is_callable() -> Bool {
total_blocking_count_for(source: map_element_called_source) == 0
}

test fn a_lambda_list_literal_realizes_each_element_at_the_arrow_carrier() -> Bool {
compile_dag_rust_emit_check(
"module lcvlist\n\nfn first_some(rs: List<fn() -> Int?>) -> Int? {\n fold(rs, init: none, f: (acc, r) => match acc {\n Present { value: a } => Present { value: a }\n Absent => r()\n })\n}\n\nfn pick() -> Int? {\n first_some(rs: [fn() { none }, fn() { Present { value: 7 } }])\n}\n",
"src/lcvlist.rs",
["{ let __callable: Rc<dyn Fn() -> Option<i64>> = { Rc::new(move || Some(7)) }; __callable }", "Option::None => r()"],
["vec![||", "compile_error!"]
)
}

test fn a_let_bound_lambda_is_emitted_with_typed_parameters() -> Bool {
compile_dag_rust_emit_check(
"module lcvlet\n\nfn apply_it(g: fn(Int) -> Int, v: Int) -> Int {\n g(v)\n}\n\nfn go() -> Int {\n let h: fn(Int) -> Int = fn(x) { x * 2 }\n h(3) + apply_it(g: h, v: 4)\n}\n",
"src/lcvlet.rs",
["let h = |x: i64|", "h(3)"],
["let h = |x| ", "compile_error!"]
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
module test.fixture.emitted_interpreted_parity.local_callable_value_calls

import std.process { ProcessExit, ExitSuccess, ExitFailure }

// ONE PROGRAM, RUN TWICE: interpreted (`gunbc run`) and emitted (`gunbc compile --target rust`, then
// the crate built and executed). The two outputs must be byte-identical, and the expected line is
// `6 8 6 7 9 0`. Each field calls a LOCALLY BOUND function value, the idiom
// extdeps.apple.app_attest first_assertion_refusal used before gunbc#12251 rewrote it:
// 1 a let-bound lambda called (annotated: an unannotated lambda's parameter has no type
// a Rust closure can be written at)
// 2 a parameter of function type called
// 3 a closure returned by a function, let-bound, then called
// 4 a LAMBDA PARAMETER bound to a List<fn() -> Int?> element, called inside a fold -- the
// specimen shape, which refused at inference as "function 'r' not found in scope" because
// v1.compiler.infer child_type_node read an Arrow element's RETURN type as the element type
// 5 the same through a unary element type, List<fn(Int) -> Int>, whose elements are lambda
// LITERALS: each realizes as the arrow carrier Rc<dyn Fn>, never a bare closure
// 6 the fold's absent arm: no step answers, so the default holds
fn apply_it(g: fn(Int) -> Int, v: Int) -> Int {
g(v)
}

fn make_adder(n: Int) -> fn(Int) -> Int {
fn(x) { x + n }
}

fn first_some(rs: List<fn() -> Int?>) -> Int? {
fold(rs, init: none, f: (acc, r) => match acc {
Present { value: a } => Present { value: a }
Absent => r()
})
}

fn sum_applied(fs: List<fn(Int) -> Int>, v: Int) -> Int {
fold(fs, init: 0, f: (acc, f) => acc + f(v))
}

fn or_zero(o: Int?) -> Int {
match o {
Present { value: v } => v
Absent => 0
}
}

fn observed_line() -> String {
let h: fn(Int) -> Int = fn(x) { x * 2 }
let add5 = make_adder(n: 5)
let parts = [
to_string(value: h(3)),
to_string(value: apply_it(g: h, v: 4)),
to_string(value: add5(1)),
to_string(value: or_zero(o: first_some(rs: [fn() { none }, fn() { Present { value: 7 } }, fn() { Present { value: 9 } }]))),
to_string(value: sum_applied(fs: [fn(x) { x + 1 }, fn(x) { x * 3 }], v: 2)),
to_string(value: or_zero(o: first_some(rs: [fn() { none }])))
]
join(parts, separator: " ")
}

data expected_line: String = "6 8 6 7 9 0"

fn main() -> ProcessExit {
let line = observed_line()
if line == expected_line { ExitSuccess } else { ExitFailure { code: 1, reason: line } }
}
53 changes: 37 additions & 16 deletions src/v1/04_infer.dag
Original file line number Diff line number Diff line change
Expand Up @@ -1874,6 +1874,22 @@ fn node_admits_list_literal(n: Node, source_indices: Map<String, NewlineIndex>)
})
}

// A LAMBDA ELEMENT'S VALUE IS ITS ARROW, but its typed node carries its BODY type, so a list
// literal typed from its first element named its element as the lambda's return -- or, under a
// generic body, as an unbound variable: [fn() { none }] against List<fn() -> Int?> was typed
// List<T>, and the emitter had no arrow to realize each element at. Where the declared element is
// an arrow and any element is a lambda, the declared arrow IS the element type -- every element
// is still judged against it (PositionListElement), and `any` is the same predicate the emitter's
// rust_list_lit_holds_callables uses, so a mixed list is typed at the arrow it realizes at.
fn declared_callable_list_element(elem_expected: Node?, elements: List<Node>) -> Node? {
match elem_expected {
Present { value: de } =>
if de.connective == Arrow && elements |> any(e => is_lambda_expr(e: e)) { Present { value: de } }
else { none }
Absent => none
}
}

fn type_node_is_established(n: Node, source_indices: Map<String, NewlineIndex>) -> Bool {
authored_name_at(source_indices: source_indices, node: n) != ""
|| n.children |> count > 0
Expand Down Expand Up @@ -8422,22 +8438,27 @@ fn infer_expr_body(texpr: Node, scope: InferScope, expected: Node?) -> InferResu
Absent => list_literal_optional_member_diags(typed_elements: typed_elements, scope: scope)
}
let elem_diags = concat(flat_map(elem_results, r => r.diagnostics), elem_inhabitance_diags)
let elem_type_node = if count(elem_results) > 0 {
match first(elem_results) {
Present { value: r } => resolved_type(n: r.typed)
Absent => unit_type
}
} else {
match expected {
Present { value: exp } =>
if node_is_element_collection(n: exp, source_indices: scope.type_env.source_indices) {
match exp.children |> first {
Present { value: elem } => child_type_node(ch: elem)
Absent => unit_type
}
} else { unit_type }
Absent => type_variable_node(id: "empty_list_element")
}
let declared_callable_elem = declared_callable_list_element(elem_expected: elem_expected, elements: elements)
let elem_type_node = match declared_callable_elem {
Present { value: de } => de
Absent =>
if count(elem_results) > 0 {
match first(elem_results) {
Present { value: r } => resolved_type(n: r.typed)
Absent => unit_type
}
} else {
match expected {
Present { value: exp } =>
if node_is_element_collection(n: exp, source_indices: scope.type_env.source_indices) {
match exp.children |> first {
Present { value: elem } => child_type_node(ch: elem)
Absent => unit_type
}
} else { unit_type }
Absent => type_variable_node(id: "empty_list_element")
}
}
}
let empty_list_diags = if count(elem_results) == 0 {
match expected {
Expand Down
Loading