diff --git a/src/v1/05_emit_rust.dag b/src/v1/05_emit_rust.dag index 3e37fae02e2..b47514db97d 100644 --- a/src/v1/05_emit_rust.dag +++ b/src/v1/05_emit_rust.dag @@ -219,6 +219,31 @@ fn emit_rust_host_to_dag_string_via_seam(host_expr: String, corpus_repr: RustCor } } +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 { + 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 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: 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 + } + } +} + 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 +7874,10 @@ fn emit_data_def_body(type_node: Node, value: Node, registry: Map + 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_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_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/stage0/src/v1_compiler_emit_rust.rs b/src/v1/stage0/src/v1_compiler_emit_rust.rs index da463d1ac1f..b000d7b6894 100644 --- a/src/v1/stage0/src/v1_compiler_emit_rust.rs +++ b/src/v1/stage0/src/v1_compiler_emit_rust.rs @@ -341,6 +341,45 @@ 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(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, + }, + } +} + pub fn emit_rust_dag_string_to_host_via_seam( dag_expr: String, corpus_repr: RustCorpusRepr, @@ -22229,6 +22268,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 +22290,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(), ), ), 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..be293ebd603 --- /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("\"alpha\".to_string()"), + "expected owned String key (coerced via .to_string()) 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}" + ); +}