Skip to content
20 changes: 19 additions & 1 deletion src/v1/04_types.dag
Original file line number Diff line number Diff line change
Expand Up @@ -1224,8 +1224,26 @@ fn infer_binop_type_node(op: BinOp, left_type: Node, source_indices: Map<String,
}
}

// MAP BINDS THE ELEMENT OF ITS CARRIER, AND THE ? CARRIER WAS OMITTED FROM THAT RULE.
//
// The many-carrier holds its element as a single CHILD, so the extraction below finds it. The
// optional carrier does not: `T?` marks optionality as the kernel CardOptional CARDINALITY ON THE
// NODE ITSELF, with no child to descend into. So an optional carrier fell past the extraction and
// landed in the Absent arm, which returns `normed` -- the CARRIER -- and the caller recorded the
// carrier where the element was meant.
//
// That silently-wrong fact was invisible for as long as the Rust route emitted lambda parameters
// untyped; once the route annotates them, it renders as `|m: Option<Rc<SpanMapping>>|` against a
// body that names the element's fields, and rustc refuses. The defect was never in the emitter.
//
// Stripping the cardinality is the whole fix, and it is a RESTORATION of uniformity rather than a
// new rule: `mappings` (many) and `mappings |> last` (?) are two carriers of one concept, and map
// already bound the element of the first. Stripping runs BEFORE the child extraction so a `List<T>?`
// yields `List<T>` rather than skipping a level -- one cardinality is removed, never two.
fn for_each_element_type_node(n: Node, source_indices: Map<String, NewlineIndex>) -> Node {
let normed = normalize_access_type_node(n: n)
let carrier = normalize_access_type_node(n: n)
let carrier_is_optional = carrier.return_cardinality == CardOptional
let normed = if carrier_is_optional { with_required_cardinality(n: carrier) } else { carrier }
let is_single_child = normed.connective == NoConnective && normed.children |> count == 1

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Stop extracting after unwrapping an optional carrier

When the optional value is itself parameterized, such as List<Int>?, normed becomes the required List<Int>, but is_single_child remains true and the following branch extracts and returns Int. Consequently, an optional map lambda is inferred as receiving Int even though Rust's Option::map supplies the whole List<Int>, causing incorrect body resolution or lambda annotations. Gate child extraction on !carrier_is_optional, or return normed immediately after removing optionality.

Useful? React with 👍 / 👎.

let extracted = if is_single_child {
match normed.children |> first {
Expand Down
8 changes: 5 additions & 3 deletions src/v1/05_emit.dag
Original file line number Diff line number Diff line change
Expand Up @@ -2432,7 +2432,8 @@ fn emit_shared_expr(
emit_for_each: fn(Node) -> String,
emit_index: fn(Node) -> String,
emit_slice: fn(Node) -> String,
emit_bin_op: fn(Node) -> String
emit_bin_op: fn(Node) -> String,
emit_lambda_params_for: fn(Node) -> String
) -> String {
match texpr.expr_data {
ExprLiteral { value: v } =>
Expand Down Expand Up @@ -2468,7 +2469,7 @@ fn emit_shared_expr(
if go_lambda_emits_statement_body(body: body) { raw_body } else { concat("return ", raw_body) }
_ => raw_body
}
wrap_result(emit_lambda(params_str: emit_lambda_params(param_names: lambda_param_names_at(texpr: texpr, source_indices: source_indices), target: target), body_str: final_body, target: target))
wrap_result(emit_lambda(params_str: emit_lambda_params_for(texpr), body_str: final_body, target: target))
ExprStringInterp =>
emit_string_interp(texpr)
ExprBlock =>
Expand Down Expand Up @@ -3061,7 +3062,8 @@ fn emit_unified_typed_expr(texpr: Node, target: RenderTarget, registry: Map<Stri
emit_bin_op: expr =>
emit_default_bin_op(texpr: expr, target: target, source_indices: si,
recurse: child => emit_unified_typed_expr(texpr: child, target: target, registry: registry, scope: scope, depth: depth, fuel: fuel - 1, render_pattern: render_pattern),
wrap_result: result => result))
wrap_result: result => result),
emit_lambda_params_for: expr => emit_lambda_params(param_names: lambda_param_names_at(texpr: expr, source_indices: si), target: target))
}

fn is_tco_identity_passthrough(arg_val: Node, param_name: String, si: Map<String, NewlineIndex>) -> Bool {
Expand Down
72 changes: 67 additions & 5 deletions src/v1/05_emit_rust.dag
Original file line number Diff line number Diff line change
Expand Up @@ -9148,7 +9148,8 @@ fn emit_typed_expr(texpr: Node, registry: Map<String, ItemInfo>, scope: InferSco
emit_for_each: expr => emit_rust_expr_for_each(expr: expr, registry: registry, scope: scope, depth: depth, shared_types: shared_types, emit_info: emit_info),
emit_index: expr => emit_rust_expr_index(expr: expr, registry: registry, scope: scope, depth: depth, shared_types: shared_types, emit_info: emit_info),
emit_slice: expr => emit_rust_expr_slice(expr: expr, registry: registry, scope: scope, depth: depth, shared_types: shared_types, emit_info: emit_info),
emit_bin_op: expr => emit_rust_expr_bin_op(expr: expr, registry: registry, scope: scope, depth: depth, shared_types: shared_types, emit_info: emit_info))
emit_bin_op: expr => emit_rust_expr_bin_op(expr: expr, registry: registry, scope: scope, depth: depth, shared_types: shared_types, emit_info: emit_info),
emit_lambda_params_for: expr => emit_rust_lambda_params_typed(lambda_expr: expr, scope: scope, shared_types: shared_types, emit_info: emit_info))
}

fn emit_cloned_arg(texpr: Node, registry: Map<String, ItemInfo>, scope: InferScope, depth: Int, shared_types: Set<String>, emit_info: EmitGraphInfo) -> String {
Expand Down Expand Up @@ -9747,7 +9748,37 @@ fn lambda_scope_from_children(scope: InferScope, params: List<String>, param_nod
)
}

fn lambda_param_type_strs(params: List<String>, param_nodes: List<Node>, fallback_types: List<String>, shared_types: Set<String>, source_indices: Map<String, NewlineIndex>, emit_info: EmitGraphInfo, fold_acc_uses_fallback: Bool, type_env: TypeEnv) -> List<String> {
// THE TWO OUTPUT POLICIES FOR A LAMBDA PARAMETER WHOSE EXACT TYPE DID NOT RENDER.
//
// These are named operations rather than a Bool argument because a Bool at a call site is
// invertible by accident and says nothing about what either value means: unavailable_stays_untyped
// false reads as a negation of a policy rather than as the selection of one. Naming the operation
// puts the policy in the thing the caller invokes, so selecting the wrong one requires calling a
// differently-named function rather than passing the wrong literal.
//
// The two policies are genuinely different and both are consumed today, which is why this axis
// exists at all and is not speculative widening: the collection and fold routes arrive holding a
// real element type and render it when the exact type is unavailable, while the generic Rust route
// has no fallback to offer and preserves the untyped parameter it would have emitted anyway.
//
// WHAT THIS DELIBERATELY DOES NOT MODEL: why the exact type was unavailable. Five distinct causes
// currently reach that arm -- fold-accumulator elision, parameter carrier absent, inference absent,
// type-variable or compiler-error result, and an exact type naming a generic that cannot be spelled
// in this Rust scope -- and this change assigns them all the same disposition, exactly as before it.
// Splitting them requires measuring the population first; a taxonomy guessed here would replace one
// conflation with a coproduct whose residual arm still conflates the meaningful causes, and naming
// any of them intentionally-elided would fabricate intent this code has no evidence for.
type UnavailableLambdaParamPolicy = PreserveUntyped | RenderFallback

fn lambda_param_strs_preserving_unavailable(params: List<String>, param_nodes: List<Node>, shared_types: Set<String>, source_indices: Map<String, NewlineIndex>, emit_info: EmitGraphInfo, type_env: TypeEnv) -> List<String> {
lambda_param_type_strs(params: params, param_nodes: param_nodes, fallback_types: [], shared_types: shared_types, source_indices: source_indices, emit_info: emit_info, fold_acc_uses_fallback: false, type_env: type_env, unavailable_policy: PreserveUntyped)
}

fn lambda_param_strs_using_fallback(params: List<String>, param_nodes: List<Node>, fallback_types: List<String>, shared_types: Set<String>, source_indices: Map<String, NewlineIndex>, emit_info: EmitGraphInfo, fold_acc_uses_fallback: Bool, type_env: TypeEnv) -> List<String> {
lambda_param_type_strs(params: params, param_nodes: param_nodes, fallback_types: fallback_types, shared_types: shared_types, source_indices: source_indices, emit_info: emit_info, fold_acc_uses_fallback: fold_acc_uses_fallback, type_env: type_env, unavailable_policy: RenderFallback)
}

fn lambda_param_type_strs(params: List<String>, param_nodes: List<Node>, fallback_types: List<String>, shared_types: Set<String>, source_indices: Map<String, NewlineIndex>, emit_info: EmitGraphInfo, fold_acc_uses_fallback: Bool, type_env: TypeEnv, unavailable_policy: UnavailableLambdaParamPolicy) -> List<String> {
params |> enumerate |> map(pair =>
let idx = pair.first
let param_name = pair.second
Expand Down Expand Up @@ -9780,18 +9811,49 @@ fn lambda_param_type_strs(params: List<String>, param_nodes: List<Node>, fallbac
let ident = emit_ident(name: param_name, target: Rust)
match inferred_type {
Present { value: ty } => apply_type_template2(template: spec.annotations.lambda_param_typed, arg0: ident, arg1: ty)
Absent => apply_type_template2(template: spec.annotations.lambda_param_typed, arg0: ident, arg1: fallback_type)
Absent =>
match unavailable_policy {
PreserveUntyped => apply_type_template1(template: spec.annotations.lambda_param_untyped, arg0: ident)
RenderFallback => apply_type_template2(template: spec.annotations.lambda_param_typed, arg0: ident, arg1: fallback_type)
}
}
)
}

// THE RUST LAMBDA PARAMETER PRODUCER FOR THE GENERIC EXPRESSION ROUTE.
//
// Rust closures reached emit_shared_expr's ExprLambda arm through emit_lambda_params, whose
// param_names: List<String> signature is structurally incapable of carrying a type -- so an
// inferred parameter type that this compiler had already resolved was discarded one call before
// the point it was needed. That is a REACH gap, not a missing capability: the typed machinery
// below already annotates 971 closures on the collection route, and languages.dag already binds
// Rust's lambda_param_typed. This routes the remaining population onto that same producer rather
// than growing a second one.
//
// This route calls lambda_param_strs_preserving_unavailable where the collection and fold routes
// call lambda_param_strs_using_fallback, and the split is deliberate rather than incidental. Those
// routes arrive holding a real element type, so when the exact type does not render they have
// something truthful to put in its place. This route has no fallback to offer, so rendering an
// explicit _ here would convert an honest absence into a claim -- and it would do so across a
// population this caller cannot distinguish. Preserving the untyped parameter reproduces exactly
// the bytes this route emits today wherever the type is unavailable, which is what keeps the
// change additive: it annotates where an exact renderable type exists and moves nothing else.
// Whether an unavailable type should instead refuse is a separate decision over a separately
// measured population, and it is deliberately not taken here.
fn emit_rust_lambda_params_typed(lambda_expr: Node, scope: InferScope, shared_types: Set<String>, emit_info: EmitGraphInfo) -> String {
let ps = lambda_param_names_at(texpr: lambda_expr, source_indices: scope.type_env.source_indices)
let pn = lambda_expr.children |> skip(1)
let param_strs = lambda_param_strs_preserving_unavailable(params: ps, param_nodes: pn, shared_types: shared_types, source_indices: scope.type_env.source_indices, emit_info: emit_info, type_env: scope.type_env)
param_strs |> join(separator: ", ")
}

fn emit_typed_collection_lambda(lambda_expr: Node, elem_type_str: String, registry: Map<String, ItemInfo>, scope: InferScope, depth: Int, shared_types: Set<String>, emit_info: EmitGraphInfo) -> String {
match lambda_expr.expr_data {
ExprLambda =>
let ps = lambda_param_names_at(texpr: lambda_expr, source_indices: scope.type_env.source_indices)
let bd = lambda_body(texpr: lambda_expr)
let pn = lambda_expr.children |> skip(1)
let param_strs = lambda_param_type_strs(params: ps, param_nodes: pn, fallback_types: [elem_type_str], shared_types: shared_types, source_indices: scope.type_env.source_indices, emit_info: emit_info, fold_acc_uses_fallback: false, type_env: scope.type_env)
let param_strs = lambda_param_strs_using_fallback(params: ps, param_nodes: pn, fallback_types: [elem_type_str], shared_types: shared_types, source_indices: scope.type_env.source_indices, emit_info: emit_info, fold_acc_uses_fallback: false, type_env: scope.type_env)
let params_str = param_strs |> join(separator: ", ")
let lambda_scope = lambda_scope_from_children(scope: scope, params: ps, param_nodes: pn)
let body_str = emit_typed_expr(texpr: bd, registry: registry, scope: lambda_scope, depth: depth, shared_types: shared_types, emit_info: emit_info, fuel: 1024)
Expand All @@ -9811,7 +9873,7 @@ fn emit_typed_fold_lambda(lambda_expr: Node, acc_type_str: String, elem_type_str
let fallback_types = ps |> enumerate |> map(pair =>
if pair.first == 0 { safe_acc_type } else { elem_type_str }
)
let param_strs_raw = lambda_param_type_strs(params: ps, param_nodes: pn, fallback_types: fallback_types, shared_types: shared_types, source_indices: scope.type_env.source_indices, emit_info: emit_info, fold_acc_uses_fallback: true, type_env: scope.type_env)
let param_strs_raw = lambda_param_strs_using_fallback(params: ps, param_nodes: pn, fallback_types: fallback_types, shared_types: shared_types, source_indices: scope.type_env.source_indices, emit_info: emit_info, fold_acc_uses_fallback: true, type_env: scope.type_env)
let param_strs = if elem_borrowed {
param_strs_raw |> enumerate |> map(pair => if pair.first == 1 { "_" } else { pair.second })
} else { param_strs_raw }
Expand Down
8 changes: 6 additions & 2 deletions src/v1/stage0/src/cli_run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -818,7 +818,10 @@ mod roadmap_acceptance_history_projection_tests {
#[test]
fn merge_base_authority_projection_matches_jsonl_carrier() {
let authority = std::process::Command::new("git")
.args(["show", "9ce6526c528:dag/gunbc/roadmap/roadmap_authority.dag"])
.args([
"show",
"9ce6526c528:dag/gunbc/roadmap/roadmap_authority.dag",
])
.output()
.expect("git show merge-base authority");
assert!(
Expand All @@ -828,7 +831,8 @@ mod roadmap_acceptance_history_projection_tests {
);
let authority = String::from_utf8(authority.stdout).expect("utf8 authority");
let jsonl = std::fs::read_to_string(
super::workspace_root().join("dag/gunbc/roadmap/roadmap_acceptance_event_history.jsonl"),
super::workspace_root()
.join("dag/gunbc/roadmap/roadmap_acceptance_event_history.jsonl"),
)
.expect("jsonl carrier");
let projected = project_roadmap_acceptance_event_history_from_authority_text(&authority);
Expand Down
Loading
Loading