From e673061cb69b025bb7915161a9b224da9c036020 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 24 Jun 2026 03:54:25 +0000 Subject: [PATCH 1/6] WIP: Lane A E0308 re-baseline (Route-A last mile): CONTINUE emitter/seed-gree --- src/v1/05_emit_rust.dag | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/src/v1/05_emit_rust.dag b/src/v1/05_emit_rust.dag index 3e37fae02e2..a5df1fc44a8 100644 --- a/src/v1/05_emit_rust.dag +++ b/src/v1/05_emit_rust.dag @@ -219,6 +219,23 @@ fn emit_rust_host_to_dag_string_via_seam(host_expr: String, corpus_repr: RustCor } } +// A map-literal's KEY must receive the SAME type-driven String coercion the VALUE +// gets via emit_typed_expr. The value slot is type-coerced; the key slot used to +// emit a bare "name" &str literal, so a Map data literal inferred +// HashMap<&str,V> and mismatched the declared HashMap (the Family-A +// E0308 cluster). The coercion is DERIVED from the declared key type, not blanket: +// a String-like key gets .to_string(); a non-String declared key emits the literal +// unchanged (fail-closed -- the two slots now consult the same type authority). +fn emit_rust_map_literal_key(field_name: String, key_type_node: Node, corpus_repr: RustCorpusRepr, source_indices: Map) -> String { + let key_literal = concat("\"", field_name, "\"") + let host_expr = if is_rust_string_like(n: key_type_node, source_indices: source_indices) { + concat(key_literal, ".to_string()") + } else { + key_literal + } + emit_rust_host_to_dag_string_via_seam(host_expr: host_expr, corpus_repr: corpus_repr) +} + fn emit_rust_dag_string_to_host_via_seam(dag_expr: String, corpus_repr: RustCorpusRepr) -> String { if corpus_repr_is_faithful(corpus_repr: corpus_repr) { concat("crate::v2_std_text::host_string_text_to_rust_host(", dag_expr, ")") @@ -7849,9 +7866,13 @@ fn emit_data_def_body(type_node: Node, value: Node, registry: Map + let key_type_node = match type_node.children |> first { + Present { value: key_child } => child_type_node(ch: key_child) + Absent => type_node + } let inserts = value.children |> map(f => let val_str = emit_typed_expr(texpr: field_init_node_value(n: f), registry: registry, scope: scope, depth: depth, shared_types: shared_types, emit_info: emit_info, fuel: 1024) - concat(" __m.insert(", emit_rust_host_to_dag_string_via_seam(host_expr: concat("\"", field_init_node_name_at(n: f, source_indices: scope.type_env.source_indices), "\""), corpus_repr: emit_info.corpus_repr), ", ", val_str, ");") + concat(" __m.insert(", emit_rust_map_literal_key(field_name: field_init_node_name_at(n: f, source_indices: scope.type_env.source_indices), key_type_node: key_type_node, corpus_repr: emit_info.corpus_repr, source_indices: scope.type_env.source_indices), ", ", val_str, ");") ) let inserts_str = inserts |> join(separator: "\n") concat(" let mut __m = HashMap::new();\n", From 6c6b94f013458499457e9f0c615179badbf958b3 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 24 Jun 2026 03:58:01 +0000 Subject: [PATCH 2/6] Family-A: type-derived map-literal key coercion (.dag authority) emit_rust_map_literal_key applies .to_string() to a map-literal key iff the declared key type is String-like (key_is_string, derived at the call site from is_rust_string_like over the map's declared key type). Fixes the coercion asymmetry where the value slot was type-coerced but the key slot emitted a bare &str literal -> HashMap<&str,V> vs declared HashMap (Family-A E0308). Authority edit only; emitted-seed splice + witnesses follow. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/v1/05_emit_rust.dag | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/v1/05_emit_rust.dag b/src/v1/05_emit_rust.dag index a5df1fc44a8..b125567db94 100644 --- a/src/v1/05_emit_rust.dag +++ b/src/v1/05_emit_rust.dag @@ -224,11 +224,15 @@ fn emit_rust_host_to_dag_string_via_seam(host_expr: String, corpus_repr: RustCor // emit a bare "name" &str literal, so a Map data literal inferred // HashMap<&str,V> and mismatched the declared HashMap (the Family-A // E0308 cluster). The coercion is DERIVED from the declared key type, not blanket: -// a String-like key gets .to_string(); a non-String declared key emits the literal -// unchanged (fail-closed -- the two slots now consult the same type authority). -fn emit_rust_map_literal_key(field_name: String, key_type_node: Node, corpus_repr: RustCorpusRepr, source_indices: Map) -> String { +// `key_is_string` is computed at the call site from `is_rust_string_like` over the +// map's declared key type, so a String-like key gets `.to_string()` while a +// non-String declared key emits the literal unchanged (fail-closed -- both slots +// now consult the same type authority). The Bool seam keeps each branch directly +// witnessable (a non-String record-literal key is not source-expressible, since +// record keys are field-name identifiers). +fn emit_rust_map_literal_key(field_name: String, key_is_string: Bool, corpus_repr: RustCorpusRepr) -> String { let key_literal = concat("\"", field_name, "\"") - let host_expr = if is_rust_string_like(n: key_type_node, source_indices: source_indices) { + let host_expr = if key_is_string { concat(key_literal, ".to_string()") } else { key_literal From dbc66d18ff14e93f3e5f03cd320b8596e0a8d2e6 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 24 Jun 2026 04:18:07 +0000 Subject: [PATCH 3/6] WIP: Lane A E0308 re-baseline (Route-A last mile): CONTINUE emitter/seed-gree --- src/v1/05_emit_rust.dag | 25 +++--- src/v1/tests/src/lib.rs | 2 + .../tests/src/map_literal_string_key_test.rs | 76 +++++++++++++++++++ 3 files changed, 87 insertions(+), 16 deletions(-) create mode 100644 src/v1/tests/src/map_literal_string_key_test.rs diff --git a/src/v1/05_emit_rust.dag b/src/v1/05_emit_rust.dag index b125567db94..c758bf6ae34 100644 --- a/src/v1/05_emit_rust.dag +++ b/src/v1/05_emit_rust.dag @@ -219,17 +219,6 @@ fn emit_rust_host_to_dag_string_via_seam(host_expr: String, corpus_repr: RustCor } } -// A map-literal's KEY must receive the SAME type-driven String coercion the VALUE -// gets via emit_typed_expr. The value slot is type-coerced; the key slot used to -// emit a bare "name" &str literal, so a Map data literal inferred -// HashMap<&str,V> and mismatched the declared HashMap (the Family-A -// E0308 cluster). The coercion is DERIVED from the declared key type, not blanket: -// `key_is_string` is computed at the call site from `is_rust_string_like` over the -// map's declared key type, so a String-like key gets `.to_string()` while a -// non-String declared key emits the literal unchanged (fail-closed -- both slots -// now consult the same type authority). The Bool seam keeps each branch directly -// witnessable (a non-String record-literal key is not source-expressible, since -// record keys are field-name identifiers). fn emit_rust_map_literal_key(field_name: String, key_is_string: Bool, corpus_repr: RustCorpusRepr) -> String { let key_literal = concat("\"", field_name, "\"") let host_expr = if key_is_string { @@ -240,6 +229,13 @@ fn emit_rust_map_literal_key(field_name: String, key_is_string: Bool, corpus_rep emit_rust_host_to_dag_string_via_seam(host_expr: host_expr, corpus_repr: corpus_repr) } +fn map_literal_key_is_string(map_type_node: Node, source_indices: Map) -> Bool { + match map_type_node.children |> first { + Present { value: key_child } => is_rust_string_like(n: child_type_node(ch: key_child), source_indices: source_indices) + Absent => false + } +} + fn emit_rust_dag_string_to_host_via_seam(dag_expr: String, corpus_repr: RustCorpusRepr) -> String { if corpus_repr_is_faithful(corpus_repr: corpus_repr) { concat("crate::v2_std_text::host_string_text_to_rust_host(", dag_expr, ")") @@ -7870,13 +7866,10 @@ fn emit_data_def_body(type_node: Node, value: Node, registry: Map - let key_type_node = match type_node.children |> first { - Present { value: key_child } => child_type_node(ch: key_child) - Absent => type_node - } + let key_is_string = map_literal_key_is_string(map_type_node: type_node, source_indices: scope.type_env.source_indices) let inserts = value.children |> map(f => let val_str = emit_typed_expr(texpr: field_init_node_value(n: f), registry: registry, scope: scope, depth: depth, shared_types: shared_types, emit_info: emit_info, fuel: 1024) - concat(" __m.insert(", emit_rust_map_literal_key(field_name: field_init_node_name_at(n: f, source_indices: scope.type_env.source_indices), key_type_node: key_type_node, corpus_repr: emit_info.corpus_repr, source_indices: scope.type_env.source_indices), ", ", val_str, ");") + concat(" __m.insert(", emit_rust_map_literal_key(field_name: field_init_node_name_at(n: f, source_indices: scope.type_env.source_indices), key_is_string: key_is_string, corpus_repr: emit_info.corpus_repr), ", ", val_str, ");") ) let inserts_str = inserts |> join(separator: "\n") concat(" let mut __m = HashMap::new();\n", diff --git a/src/v1/tests/src/lib.rs b/src/v1/tests/src/lib.rs index d34fe11631a..b506b9539ed 100644 --- a/src/v1/tests/src/lib.rs +++ b/src/v1/tests/src/lib.rs @@ -81,6 +81,8 @@ mod languages_consumer_census_lens_test; #[cfg(test)] mod list_free_monoid_chokepoint_test; #[cfg(test)] +mod map_literal_string_key_test; +#[cfg(test)] mod map_lookup_dual_dispatch_test; #[cfg(test)] mod measure_field_access_test; diff --git a/src/v1/tests/src/map_literal_string_key_test.rs b/src/v1/tests/src/map_literal_string_key_test.rs new file mode 100644 index 00000000000..7ef126f0fe4 --- /dev/null +++ b/src/v1/tests/src/map_literal_string_key_test.rs @@ -0,0 +1,76 @@ +//! Family-A regression: a map-literal's KEY must receive the same type-driven +//! String coercion the VALUE already gets, so a `Map` data literal emits +//! `HashMap` and not `HashMap<&str, V>` (the E0308 cluster). +//! +//! The coercion is DERIVED from the declared key type (`is_rust_string_like` over +//! the map's key type, computed at the emit call site). These witnesses pin both +//! directions of the derived decision plus the real-source emission shape. + +use v1_compiler::v1_compiler_artifact::RenderTarget; +use v1_compiler::v1_compiler_emit_rust::emit_rust_map_literal_key; +use v1_compiler::v1_compiler_infer_emit_info::RustCorpusRepr; + +// --- Discriminating control: the decision is type-DERIVED, not a blanket +// "always .to_string() the key". A non-String declared key is not expressible as a +// record-literal map (record keys are field-name identifiers), so the false branch +// is exercised directly here. --- + +#[test] +fn string_key_is_coerced_to_owned_string() { + // HostNative repr: the host->dag seam is identity, so the coercion is the only + // transform and is directly observable. + let out = emit_rust_map_literal_key("alpha".to_string(), true, RustCorpusRepr::HostNative); + assert_eq!( + out, "\"alpha\".to_string()", + "a String-keyed map literal must own its key" + ); +} + +#[test] +fn non_string_key_is_left_as_literal() { + // The wall: a NON-String declared key must NOT be `.to_string()`'d. If this ever + // emits `.to_string()`, the coercion has degenerated to blanket (a ยง5 fail-open + // that mis-coerces non-String key maps). + let out = emit_rust_map_literal_key("alpha".to_string(), false, RustCorpusRepr::HostNative); + assert_eq!( + out, "\"alpha\"", + "a non-String declared key must stay an unowned literal" + ); +} + +#[test] +fn faithful_repr_routes_owned_key_through_the_text_seam() { + // Under the faithful FreeMonoid repr the key still becomes an owned String first, + // then crosses the host->dag text seam (which consumes a `String`). + let out = + emit_rust_map_literal_key("alpha".to_string(), true, RustCorpusRepr::FaithfulFreeMonoid); + assert_eq!( + out, + "crate::v2_std_text::host_string_text_from_rust_host(\"alpha\".to_string())", + "faithful repr must hand the seam an owned String" + ); +} + +// --- Source-fixture proof: a real `Map` data literal emits owned +// String keys (so the inferred map matches the declared `HashMap`), +// while the Int value is left uncoerced (the slots are independently type-driven). --- + +#[test] +fn map_string_int_literal_emits_owned_keys_only() { + let source = "module mapkey.fixture\n\ndata lookup: Map = { alpha: 1, beta: 2 }\n"; + let result = crate::helpers::compile_dag_target(source, RenderTarget::Rust); + let emitted: String = result + .files + .iter() + .map(|f| f.content.clone()) + .collect::>() + .join("\n"); + assert!( + emitted.contains("__m.insert(\"alpha\".to_string()"), + "expected owned String key in emitted map literal, got:\n{emitted}" + ); + assert!( + !emitted.contains("1.to_string()"), + "the Int value must not be coerced to String (per-slot type derivation):\n{emitted}" + ); +} From 0cc25d7a61b7a9f33f87a72d3f1350018efc56dc Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 24 Jun 2026 04:31:51 +0000 Subject: [PATCH 4/6] WIP: Lane A E0308 re-baseline (Route-A last mile): CONTINUE emitter/seed-gree --- src/v1/05_emit_rust.dag | 12 ++++- src/v1/stage0/src/v1_compiler_emit_rust.rs | 56 ++++++++++++++++------ 2 files changed, 52 insertions(+), 16 deletions(-) diff --git a/src/v1/05_emit_rust.dag b/src/v1/05_emit_rust.dag index c758bf6ae34..b47514db97d 100644 --- a/src/v1/05_emit_rust.dag +++ b/src/v1/05_emit_rust.dag @@ -231,8 +231,16 @@ fn emit_rust_map_literal_key(field_name: String, key_is_string: Bool, corpus_rep fn map_literal_key_is_string(map_type_node: Node, source_indices: Map) -> Bool { match map_type_node.children |> first { - Present { value: key_child } => is_rust_string_like(n: child_type_node(ch: key_child), source_indices: source_indices) - Absent => false + Present { value: key_child } => is_rust_string_like(n: key_child, source_indices: source_indices) + Absent => + match find_property(props: map_type_node.properties, prop_name: "__applied_type_args", source_indices: source_indices) { + Present { value: applied } => + match applied.children |> first { + Present { value: akey } => is_rust_string_like(n: akey, source_indices: source_indices) + Absent => false + } + Absent => false + } } } diff --git a/src/v1/stage0/src/v1_compiler_emit_rust.rs b/src/v1/stage0/src/v1_compiler_emit_rust.rs index da463d1ac1f..66d4c5e0ee5 100644 --- a/src/v1/stage0/src/v1_compiler_emit_rust.rs +++ b/src/v1/stage0/src/v1_compiler_emit_rust.rs @@ -341,6 +341,35 @@ pub fn emit_rust_host_to_dag_string_via_seam( } } +pub fn emit_rust_map_literal_key( + field_name: String, + key_is_string: bool, + corpus_repr: RustCorpusRepr, +) -> String { + { + let key_literal = v1_rt::concat( + v1_rt::concat("\"".to_string(), field_name), + "\"".to_string(), + ); + let host_expr = if key_is_string { + v1_rt::concat(key_literal, ".to_string()".to_string()) + } else { + key_literal + }; + emit_rust_host_to_dag_string_via_seam(host_expr, corpus_repr) + } +} + +pub fn map_literal_key_is_string( + map_type_node: Rc, + source_indices: Rc>>, +) -> bool { + match map_type_node.children.clone().first().cloned() { + Some(key_child) => is_rust_string_like(child_type_node(key_child.clone()), source_indices), + None => false, + } +} + pub fn emit_rust_dag_string_to_host_via_seam( dag_expr: String, corpus_repr: RustCorpusRepr, @@ -22229,6 +22258,10 @@ pub fn emit_data_def_body( if is_map { match (*value.expr_data.clone()).clone() { ExprData::ExprRecordLit { parent_enum: _, .. } => { + let key_is_string = map_literal_key_is_string( + type_node.clone(), + scope.type_env.clone().source_indices.clone(), + ); let inserts = Rc::new({ let mut __result = Vec::new(); for f in value.children.clone().iter().cloned() { @@ -22247,21 +22280,16 @@ pub fn emit_data_def_body( v1_rt::concat( v1_rt::concat( " __m.insert(".to_string(), - emit_rust_host_to_dag_string_via_seam( - v1_rt::concat( - v1_rt::concat( - "\"".to_string(), - field_init_node_name_at( - f.clone(), - scope - .type_env - .clone() - .source_indices - .clone(), - ), - ), - "\"".to_string(), + emit_rust_map_literal_key( + field_init_node_name_at( + f.clone(), + scope + .type_env + .clone() + .source_indices + .clone(), ), + key_is_string.clone(), emit_info.corpus_repr.clone(), ), ), From cc0628974db95d19f8319dfb2233d0ab4b6b5d1f Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 24 Jun 2026 04:42:29 +0000 Subject: [PATCH 5/6] WIP: Lane A E0308 re-baseline (Route-A last mile): CONTINUE emitter/seed-gree --- src/v1/stage0/src/v1_compiler_emit_rust.rs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/v1/stage0/src/v1_compiler_emit_rust.rs b/src/v1/stage0/src/v1_compiler_emit_rust.rs index 66d4c5e0ee5..b000d7b6894 100644 --- a/src/v1/stage0/src/v1_compiler_emit_rust.rs +++ b/src/v1/stage0/src/v1_compiler_emit_rust.rs @@ -365,8 +365,18 @@ pub fn map_literal_key_is_string( source_indices: Rc>>, ) -> bool { match map_type_node.children.clone().first().cloned() { - Some(key_child) => is_rust_string_like(child_type_node(key_child.clone()), source_indices), - None => false, + Some(key_child) => is_rust_string_like(key_child.clone(), source_indices.clone()), + None => match find_property( + map_type_node.properties.clone(), + "__applied_type_args".to_string(), + source_indices.clone(), + ) { + Some(applied) => match applied.children.clone().first().cloned() { + Some(akey) => is_rust_string_like(akey.clone(), source_indices.clone()), + None => false, + }, + None => false, + }, } } From e7d837f00708883ec28911b783b73ce445bb9764 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 24 Jun 2026 04:47:15 +0000 Subject: [PATCH 6/6] Relax map_string_int fixture assertion to faithful-repr seam output Co-Authored-By: Claude Opus 4.8 --- src/v1/tests/src/map_literal_string_key_test.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/v1/tests/src/map_literal_string_key_test.rs b/src/v1/tests/src/map_literal_string_key_test.rs index 7ef126f0fe4..be293ebd603 100644 --- a/src/v1/tests/src/map_literal_string_key_test.rs +++ b/src/v1/tests/src/map_literal_string_key_test.rs @@ -66,8 +66,8 @@ fn map_string_int_literal_emits_owned_keys_only() { .collect::>() .join("\n"); assert!( - emitted.contains("__m.insert(\"alpha\".to_string()"), - "expected owned String key in emitted map literal, got:\n{emitted}" + emitted.contains("\"alpha\".to_string()"), + "expected owned String key (coerced via .to_string()) in emitted map literal, got:\n{emitted}" ); assert!( !emitted.contains("1.to_string()"),