Skip to content
Merged
68 changes: 68 additions & 0 deletions docs/probes/underscore_named_call_order_treatment_2026-08-23.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# Underscore-idiom named-call order — preregistered treatment

This is one bounded producer decision from the retained `03_ingest` emission board at
`907f19c2cc7`. It is not a partition of that board.

## Current population

The retained log has seven `error[E0308]: arguments to this function are incorrect` blocks in
the emitted `v2.compiler.eval` module whose callees are `eval_value_node`,
`eval_transform_node`, `eval_branch_node`, `eval_loop_node`, `eval_bind_node`, and
`eval_match_node` (two calls reach `eval_loop_node`). Each declaration uses at least one
underscore-prefixed unused parameter and each authored call supplies its caller-visible label.

The other three diagnostics with the same rustc message are controls, not members: two are
positional calls inside fold lambdas, and one supplies `NormalizedTree` values to a function that
requires `Node`. This treatment predicts no change to those three.

## Producer and intended fact

`v1.compiler.emit` `order_typed_call_args` translates source named arguments into target
positional arguments. It previously compared a caller label to the exact declaration spelling.
The accepted label relation already has one authority: `v1.compiler.infer`
`call_arg_label_matches_param`, which defines declaration `_args` as caller label `args` (and also
serves inference's call-shape wall). Exact spelling therefore made emission answer the same fact
differently from inference.

The intended fact is unique: an accepted named call is ordered by declaration parameters using
`call_arg_label_matches_param`. A bare anonymous `_` has no unique caller-visible identity and is
not reordered by name.

## Discriminating treatment and preregistered receipt

The witness compiles two otherwise-identical declarations and calls. The control declares
`direct_order(a, b)`; the treatment declares `underscore_order(_a, b)`. Both calls author `b`
before `a`. Their emitted Rust must contain, respectively, `direct_order(11, 23)` and
`underscore_order(11, 23)`.

Before observing a candidate board, the registered result is:

- all seven in-scope `v2.compiler.eval` E0308 blocks disappear rather than convert: their arity
and types already agree with the declarations, and only their positional order differs, so no
E0061 or type-mismatch successor is predicted;
- the three out-of-scope same-message blocks remain byte-identifiable by callee and enclosing
declaration;
- the control and treatment emit the same positional order;
- any successor diagnostic refutes that prediction and is reported as a conversion, not silently
counted as removal.

## Observed paired receipt

The prediction was tested in one remote dispatch with the annotation-only #9027 correction in
both arms. Because that correction conflicted with the older board tree, both arms took the same
resolved file from #9027; it is therefore common setup rather than a treatment difference. The
before arm was `183e5972999` plus that correction. The after arm was PR #9026 at `e2317ed0838`
(including its regenerated stage0 mirrors) plus the identical correction.

| observation | before | after |
|---|---:|---:|
| coded rustc errors | 324 | 317 |
| E0308 | 128 | 121 |
| scoped `v2.compiler.eval` call-order identities | 7 | 0 |
| three preregistered control identities (location-reference count) | 5 | 5 |
| E0061 | 17 | 17 |

The prediction held exactly: all seven scoped E0308 blocks vanished, none converted, and no
control moved. The paired board total is 324 rather than the authorized retained board's 316
because the common #9027 file resolution changes that composite subject; no count is compared
across those subjects. Only the within-pair delta is claimed here.
21 changes: 16 additions & 5 deletions src/v1/04_infer.dag
Original file line number Diff line number Diff line change
Expand Up @@ -2476,12 +2476,23 @@ fn borrowed_callable_call_type(gnode: Node, typed_args: List<Node>, scope: Infer

fn is_anonymous_bound_name(name: String) -> Bool { name == "_" }

fn call_param_caller_labels(param_name: String) -> List<String> {
if param_name == "_" {
[]
} else if string_length(s: param_name) >= 1
&& substring(s: param_name, start: 0, end: 1) == "_" {
[param_name, substring(s: param_name, start: 1, end: string_length(s: param_name))]
} else {
[param_name]
}
}

fn call_arg_label_matches_param(param_name: String, arg_label: String) -> Bool {
param_name == arg_label
|| param_name == "_"
|| (string_length(s: param_name) >= 1
&& substring(s: param_name, start: 0, end: 1) == "_"
&& substring(s: param_name, start: 1, end: string_length(s: param_name)) == arg_label)
if param_name == "_" {
true
} else {
call_param_caller_labels(param_name: param_name) |> any(caller_label => caller_label == arg_label)
}
}

fn param_binds_positionally(p: Node, source_indices: Map<String, NewlineIndex>) -> Bool {
Expand Down
30 changes: 16 additions & 14 deletions src/v1/05_emit.dag
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ import v1.compiler.infer_lookup { lookup_func_sig, func_sig_if_resolved }

import v1.compiler.infer {
InferScope,
build_params_scope, extend_scope
build_params_scope, extend_scope, call_param_caller_labels
}

import v1.compiler.infer_emit_info { TypeSummary, EmitGraphInfo }
Expand Down Expand Up @@ -303,15 +303,10 @@ fn lookup_func_sig_in_scope(scope: InferScope, name: String) -> ResolvedFuncSig?
func_sig_if_resolved(lookup: lookup_func_sig(func_env: scope.func_env, type_env: scope.type_env, name: name))
}

fn typed_named_arg_matches(arg: Node, name: String, source_indices: Map<String, NewlineIndex>) -> Bool {
let n = arg_name_at(n: arg, source_indices: source_indices)
if n == none {
false
} else {
n.value == name
}
}

// Translate named source arguments to target positional order through infer's
// accepted-label authority. `_args` accepts exact `_args` and caller-facing
// `args`; anonymous `_` has no reorderable identity. Both indexes consume that
// bounded label set, keeping construction and lookup linear.
fn order_typed_call_args(args: List<Node>, func: String, scope: InferScope) -> List<Node> {
let has_unnamed = args |> any(arg => arg_name_at(n: arg, source_indices: scope.type_env.source_indices) == none)
if has_unnamed {
Expand All @@ -324,11 +319,18 @@ fn order_typed_call_args(args: List<Node>, func: String, scope: InferScope) -> L
let n = arg_name_at(n: arg, source_indices: scope.type_env.source_indices)
if n != none { map_insert(acc, n.value, arg) } else { acc }
)
let param_name_set = sig.params |> fold(init: empty_map(), f: (acc, param) =>
map_insert(acc, param_node_name_at(n: param, source_indices: scope.type_env.source_indices), true)
let param_label_set = sig.params |> fold(init: empty_map(), f: (acc, param) =>
call_param_caller_labels(param_name: param_node_name_at(n: param, source_indices: scope.type_env.source_indices))
|> fold(init: acc, f: (labels, label) => map_insert(labels, label, true))
)
let ordered = sig.params |> flat_map(param =>
match map_get(arg_map, param_node_name_at(n: param, source_indices: scope.type_env.source_indices)) {
let param_name = param_node_name_at(n: param, source_indices: scope.type_env.source_indices)
let matching = call_param_caller_labels(param_name: param_name)
|> flat_map(label => match map_get(arg_map, label) {
Present { value: arg } => [arg]
Absent => []
})
match matching |> first {
Present { value: arg } => [arg]
Absent => []
}
Expand All @@ -338,7 +340,7 @@ fn order_typed_call_args(args: List<Node>, func: String, scope: InferScope) -> L
if n == none {
true
} else {
emit_map_has(m: param_name_set, key: n.value) == false
emit_map_has(m: param_label_set, key: n.value) == false
}
)
concat(ordered, leftovers)
Expand Down
2 changes: 1 addition & 1 deletion src/v1/05_emit_go.dag
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ import v1.compiler.emit {
lookup_item,
is_null_coalesce, emit_null_coalesce,
has_nested_records_node,
typed_named_arg_matches, order_typed_call_args,
order_typed_call_args,
is_tco_eligible,
service_fallback_transport, effective_operation_transport,
ServiceFieldSet, compute_service_fields, service_field_decls,
Expand Down
2 changes: 1 addition & 1 deletion src/v1/05_emit_python.dag
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ import v1.compiler.emit {
empty_emit_scope, module_emit_scope,
scope_after_expr,
lookup_item,
typed_named_arg_matches, order_typed_call_args,
order_typed_call_args,
has_nested_records_node,
is_null_coalesce, emit_null_coalesce,
has_service_items,
Expand Down
2 changes: 1 addition & 1 deletion src/v1/05_emit_rust.dag
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ import v1.compiler.emit {
emit_lambda_params, emit_null_coalesce, emit_list_lit_expr, emit_shared_expr,
emit_typed_cast_shared, emit_string_literal, emit_simple_expr, escape_rust_interp_text,
module_emit_scope, scope_after_expr, lookup_item, has_nested_records_node,
emit_data_value_json, EmitterOutcome, Emitted, Refused, is_null_coalesce, has_service_items, typed_named_arg_matches,
emit_data_value_json, EmitterOutcome, Emitted, Refused, is_null_coalesce, has_service_items,
order_typed_call_args, is_tco_eligible, is_self_recursive, emit_shared_tco_expr,
tco_reassign_core, is_tco_identity_passthrough, service_fallback_transport,
effective_operation_transport, ServiceFieldSet, compute_service_fields, service_field_decls,
Expand Down
20 changes: 19 additions & 1 deletion src/v1/compiler_tests_rust.dag
Original file line number Diff line number Diff line change
Expand Up @@ -2080,13 +2080,31 @@ fn ct_call_shape_wall_witness_test() -> String {
" // it, so the compile seam must too) all stay silent.\n",
" let green = compile_one(\n",
" \"green.dag\",\n",
" \"module green\\nfn sub(a: Int, b: Int) -> Int { a - b }\\nfn ignore_ctx(_ctx: Int, b: Int) -> Int { b }\\nfn named() -> Int { sub(a: 10, b: 3) }\\nfn positional() -> Int { sub(10, 3) }\\nfn underscore_idiom() -> Int { ignore_ctx(ctx: 1, b: 2) }\\n\",\n",
" \"module green\\nfn sub(a: Int, b: Int) -> Int { a - b }\\nfn ignore_ctx(_ctx: Int, b: Int) -> Int { b }\\nfn direct_order(a: Int, b: Int) -> Int { b }\\nfn underscore_order(_a: Int, b: Int) -> Int { b }\\nfn underscore_exact_order(_a: Int, b: Int) -> Int { b }\\nfn named() -> Int { sub(a: 10, b: 3) }\\nfn positional() -> Int { sub(10, 3) }\\nfn underscore_idiom() -> Int { ignore_ctx(ctx: 1, b: 2) }\\nfn direct_order_control() -> Int { direct_order(b: 23, a: 11) }\\nfn underscore_order_witness() -> Int { underscore_order(b: 23, a: 11) }\\nfn underscore_exact_order_control() -> Int { underscore_exact_order(b: 23, _a: 11) }\\n\",\n",
" );\n",
" assert!(\n",
" green.diagnostics.is_empty(),\n",
" \"correct call shapes must compile with NO diagnostic of any severity, got: {:?}\",\n",
" green.diagnostics\n",
" );\n",
" let emitted = green.files.iter()\n",
" .find(|f| f.path.ends_with(\"green.rs\"))\n",
" .expect(\"green fixture must emit its Rust module\");\n",
" assert!(\n",
" emitted.content.contains(\"direct_order(11, 23)\"),\n",
" \"control: ordinary named arguments must follow declaration order, got: {}\",\n",
" emitted.content\n",
" );\n",
" assert!(\n",
" emitted.content.contains(\"underscore_exact_order(11, 23)\"),\n",
" \"control: exact underscore-prefixed caller labels must remain accepted and follow declaration order, got: {}\",\n",
" emitted.content\n",
" );\n",
" assert!(\n",
" emitted.content.contains(\"underscore_order(11, 23)\"),\n",
" \"named arguments accepted through the declaration-side underscore idiom must follow that same declaration order, got: {}\",\n",
" emitted.content\n",
" );\n",
" })\n",
" .expect(\"failed to spawn thread\")\n",
" .join();\n",
Expand Down
20 changes: 19 additions & 1 deletion src/v1/stage0/src/compiler_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -522,13 +522,31 @@ mod compiler_tests {
// it, so the compile seam must too) all stay silent.
let green = compile_one(
"green.dag",
"module green\nfn sub(a: Int, b: Int) -> Int { a - b }\nfn ignore_ctx(_ctx: Int, b: Int) -> Int { b }\nfn named() -> Int { sub(a: 10, b: 3) }\nfn positional() -> Int { sub(10, 3) }\nfn underscore_idiom() -> Int { ignore_ctx(ctx: 1, b: 2) }\n",
"module green\nfn sub(a: Int, b: Int) -> Int { a - b }\nfn ignore_ctx(_ctx: Int, b: Int) -> Int { b }\nfn direct_order(a: Int, b: Int) -> Int { b }\nfn underscore_order(_a: Int, b: Int) -> Int { b }\nfn underscore_exact_order(_a: Int, b: Int) -> Int { b }\nfn named() -> Int { sub(a: 10, b: 3) }\nfn positional() -> Int { sub(10, 3) }\nfn underscore_idiom() -> Int { ignore_ctx(ctx: 1, b: 2) }\nfn direct_order_control() -> Int { direct_order(b: 23, a: 11) }\nfn underscore_order_witness() -> Int { underscore_order(b: 23, a: 11) }\nfn underscore_exact_order_control() -> Int { underscore_exact_order(b: 23, _a: 11) }\n",
);
assert!(
green.diagnostics.is_empty(),
"correct call shapes must compile with NO diagnostic of any severity, got: {:?}",
green.diagnostics
);
let emitted = green.files.iter()
.find(|f| f.path.ends_with("green.rs"))
.expect("green fixture must emit its Rust module");
assert!(
emitted.content.contains("direct_order(11, 23)"),
"control: ordinary named arguments must follow declaration order, got: {}",
emitted.content
);
assert!(
emitted.content.contains("underscore_exact_order(11, 23)"),
"control: exact underscore-prefixed caller labels must remain accepted and follow declaration order, got: {}",
emitted.content
);
assert!(
emitted.content.contains("underscore_order(11, 23)"),
"named arguments accepted through the declaration-side underscore idiom must follow that same declaration order, got: {}",
emitted.content
);
})
.expect("failed to spawn thread")
.join();
Expand Down
Loading
Loading