Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
28 changes: 27 additions & 1 deletion src/v1/05_emit_rust.dag
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, NewlineIndex>) -> 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, ")")
Expand Down Expand Up @@ -7849,9 +7874,10 @@ fn emit_data_def_body(type_node: Node, value: Node, registry: Map<String, ItemIn
if is_map {
match value.expr_data {
ExprRecordLit { parent_enum: _ } =>
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",
Expand Down
66 changes: 52 additions & 14 deletions src/v1/stage0/src/v1_compiler_emit_rust.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Node>,
source_indices: Rc<HashMap<String, Rc<NewlineIndex>>>,
) -> 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,
Expand Down Expand Up @@ -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() {
Expand All @@ -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(),
),
),
Expand Down
2 changes: 2 additions & 0 deletions src/v1/tests/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
76 changes: 76 additions & 0 deletions src/v1/tests/src/map_literal_string_key_test.rs
Original file line number Diff line number Diff line change
@@ -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<String, V>` data literal emits
//! `HashMap<String, V>` 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<String, Int>` data literal emits owned
// String keys (so the inferred map matches the declared `HashMap<String, i64>`),
// 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<String, Int> = { 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::<Vec<_>>()
.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}"
);
}