From 6527c383b716b0159f4f95642de0a2043757c695 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sun, 3 May 2026 14:11:32 -0400 Subject: [PATCH 01/34] WIP: crisp-ibex-569 --- src/v2/tests/src/bootstrap.rs | 92 +++++++++++++++++++++++++++++++- src/v2/tests/src/helpers.rs | 99 ++++++++++++++++++++++++++++++++--- 2 files changed, 181 insertions(+), 10 deletions(-) diff --git a/src/v2/tests/src/bootstrap.rs b/src/v2/tests/src/bootstrap.rs index 08f0511ba10..8f9654bdd36 100644 --- a/src/v2/tests/src/bootstrap.rs +++ b/src/v2/tests/src/bootstrap.rs @@ -33,14 +33,31 @@ fn build_stage0() -> std::path::PathBuf { fn run_self_compile( binary: &std::path::Path, output_dir: &std::path::Path, +) -> std::process::Output { + run_self_compile_with_extra_source_roots(binary, output_dir, &[]) +} + +/// Run self-compile with additional dependency source roots appended after the +/// canonical `src/v2` and `dsl` roots. Future build-time generated `.dag` +/// projections should use this surface: keep `src/v2` as the entry root and +/// pass the generated temp/OUT_DIR root as a dependency pool. +fn run_self_compile_with_extra_source_roots( + binary: &std::path::Path, + output_dir: &std::path::Path, + extra_source_roots: &[std::path::PathBuf], ) -> std::process::Output { let [v2_root, dsl_root] = crate::helpers::source_roots(); - std::process::Command::new(binary) + let mut command = std::process::Command::new(binary); + command .arg("compile") .arg("--source-root") .arg(&v2_root) .arg("--source-root") - .arg(&dsl_root) + .arg(&dsl_root); + for root in extra_source_roots { + command.arg("--source-root").arg(root); + } + command .arg("--output-dir") .arg(output_dir) .output() @@ -306,6 +323,77 @@ fn stage0_compile_accepts_dag_target() { let _ = std::fs::remove_dir_all(&out_dir); } +#[test] +#[ignore] // Requires building stage0 binary (~2 min) +fn stage0_compile_imports_ephemeral_generated_source_root() { + let stage0_bin = build_stage0(); + + let entry_root = std::env::temp_dir().join("v2-ephemeral-entry-root"); + let generated_root = std::env::temp_dir().join("v2-ephemeral-generated-root"); + let out_dir = std::env::temp_dir().join("v2-ephemeral-generated-out"); + let _ = std::fs::remove_dir_all(&entry_root); + let _ = std::fs::remove_dir_all(&generated_root); + let _ = std::fs::remove_dir_all(&out_dir); + std::fs::create_dir_all(entry_root.join("ephemeral")).unwrap(); + std::fs::create_dir_all(generated_root.join("generated")).unwrap(); + std::fs::write( + generated_root + .join("generated") + .join("method_template_projection.dag"), + "module generated.method_template_projection\n\nfn generated_answer() -> Int { 41 }\n", + ) + .unwrap(); + std::fs::write( + entry_root.join("ephemeral").join("entry.dag"), + "\ +module ephemeral.entry + +import generated.method_template_projection { generated_answer } + +fn main() -> Int { generated_answer() } +", + ) + .unwrap(); + + let output = std::process::Command::new(&stage0_bin) + .arg("compile") + .arg("--source-root") + .arg(&entry_root) + .arg("--source-root") + .arg(&generated_root) + .arg("--output-dir") + .arg(&out_dir) + .arg("--target") + .arg("dag") + .output() + .expect("failed to run stage0 compile"); + + let stderr = String::from_utf8_lossy(&output.stderr); + eprintln!("ephemeral generated source-root stderr:\n{}", stderr); + assert!( + output.status.success(), + "stage0 compile failed with ephemeral generated source root:\n{}", + stderr + ); + assert!( + stderr.contains("indexed 2 modules from 2 source roots"), + "expected both entry and generated temp roots to be indexed, got:\n{stderr}" + ); + assert!( + out_dir.join("dag-artifact.json").exists(), + "expected dag artifact at {}", + out_dir.join("dag-artifact.json").display() + ); + assert!( + !std::path::Path::new("src/generated/method_template_projection.dag").exists(), + "ratchet must not rely on a committed generated .dag under src/" + ); + + let _ = std::fs::remove_dir_all(&entry_root); + let _ = std::fs::remove_dir_all(&generated_root); + let _ = std::fs::remove_dir_all(&out_dir); +} + // ── 3. bootstrap_stage0_to_stage1 (emitted Rust error ratchet) ──────── // // Compiles .dag source to Rust via stage0, then runs cargo check on the diff --git a/src/v2/tests/src/helpers.rs b/src/v2/tests/src/helpers.rs index 582240240d1..ae97a0b3af3 100644 --- a/src/v2/tests/src/helpers.rs +++ b/src/v2/tests/src/helpers.rs @@ -106,9 +106,11 @@ use std::sync::OnceLock; /// Build module index using the parser as single authority for module names. /// Scans source roots recursively, tokenizes+parses each .dag file to /// extract the module declaration. Built once via OnceLock. -fn build_module_index() -> HashMap { +fn build_module_index_for_roots( + roots: &[std::path::PathBuf], +) -> HashMap { let mut index = HashMap::new(); - for root in source_roots() { + for root in roots { if root.exists() { scan_dag_files(&root, &mut index); } @@ -116,6 +118,10 @@ fn build_module_index() -> HashMap { index } +fn build_module_index() -> HashMap { + build_module_index_for_roots(&source_roots()) +} + fn scan_dag_files(dir: &std::path::Path, index: &mut HashMap) { let Ok(entries) = std::fs::read_dir(dir) else { return; @@ -171,6 +177,30 @@ fn extract_imports(source: &str) -> Vec { /// set of SourceFiles needed — each module loaded exactly once. /// Lookups use the cached module index (parser-backed, built once). pub fn resolve_imports_transitively(entry_path: &str, entry_content: &str) -> Vec> { + resolve_imports_transitively_with_index(entry_path, entry_content, module_index()) +} + +pub fn resolve_imports_transitively_with_source_roots( + entry_path: &str, + entry_content: &str, + source_roots: &[std::path::PathBuf], +) -> Vec> { + let index = build_module_index_for_roots(source_roots); + resolve_imports_transitively_with_index(entry_path, entry_content, &index) +} + +fn display_source_path(path: &std::path::Path, ws: &std::path::Path) -> String { + path.strip_prefix(ws) + .unwrap_or(path) + .to_string_lossy() + .to_string() +} + +fn resolve_imports_transitively_with_index( + entry_path: &str, + entry_content: &str, + module_index: &HashMap, +) -> Vec> { let ws = workspace_root(); let mut seen: HashMap> = HashMap::new(); let mut queue: Vec<(String, String)> = Vec::new(); // (path, content) @@ -184,13 +214,9 @@ pub fn resolve_imports_transitively(entry_path: &str, entry_content: &str) -> Ve if seen.contains_key(&module_path) { continue; // already loaded — O(1) check } - if let Some(file_path) = module_index().get(&module_path) { + if let Some(file_path) = module_index.get(&module_path) { if let Ok(file_content) = std::fs::read_to_string(file_path) { - let rel_path = file_path - .strip_prefix(&ws) - .unwrap_or(file_path) - .to_string_lossy() - .to_string(); + let rel_path = display_source_path(file_path, &ws); let source = Rc::new(SourceFile { path: rel_path.clone(), content: file_content.clone(), @@ -229,6 +255,16 @@ pub fn compile_dag_named(filename: &str, source: &str, target: RenderTarget) -> v2_compiler::v2_compiler_compile::compile_sources(Rc::new(sources), target) } +pub fn compile_dag_named_with_source_roots( + filename: &str, + source: &str, + target: RenderTarget, + source_roots: &[std::path::PathBuf], +) -> Rc { + let sources = resolve_imports_transitively_with_source_roots(filename, source, source_roots); + v2_compiler::v2_compiler_compile::compile_sources(Rc::new(sources), target) +} + pub fn compile_multi(files: &[(&str, &str)]) -> Rc { compile_multi_target(files, RenderTarget::Rust) } @@ -340,4 +376,51 @@ mod tests { "panic should identify duplicate module names, got: {message}" ); } + + #[test] + fn resolver_imports_ephemeral_generated_source_root() { + let entry_root = temp_dir("entry-root"); + let generated_root = temp_dir("generated-root"); + let generated_dir = generated_root.join("generated"); + std::fs::create_dir_all(&generated_dir).expect("create generated dir"); + std::fs::write( + generated_dir.join("method_template_projection.dag"), + "module generated.method_template_projection\n\nfn generated_answer() -> Int { 41 }\n", + ) + .expect("write generated module"); + + let entry_source = "\ +module ephemeral.entry + +import generated.method_template_projection { generated_answer } + +fn main() -> Int { generated_answer() } +"; + let result = compile_dag_named_with_source_roots( + "ephemeral/entry.dag", + entry_source, + RenderTarget::Dag, + &[entry_root.clone(), generated_root.clone()], + ); + + assert_no_diagnostics(&result); + let loaded_paths: Vec<_> = result + .source_indices + .iter() + .map(|index| index.file.clone()) + .collect(); + assert!( + loaded_paths.iter().any(|path| path.contains( + "generated/method_template_projection.dag" + )), + "expected generated temp-root module to be loaded, got: {loaded_paths:?}" + ); + assert!( + !loaded_paths.iter().any(|path| path.starts_with("src/")), + "ephemeral generated dependency must not be committed under src/: {loaded_paths:?}" + ); + + let _ = std::fs::remove_dir_all(entry_root); + let _ = std::fs::remove_dir_all(generated_root); + } } From e8542ec05f9d526ff7adf7566e728bdfc4122857 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sun, 3 May 2026 14:13:44 -0400 Subject: [PATCH 02/34] WIP: crisp-ibex-569 --- src/v2/tests/src/helpers.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/v2/tests/src/helpers.rs b/src/v2/tests/src/helpers.rs index ae97a0b3af3..c1e72826acf 100644 --- a/src/v2/tests/src/helpers.rs +++ b/src/v2/tests/src/helpers.rs @@ -405,9 +405,9 @@ fn main() -> Int { generated_answer() } assert_no_diagnostics(&result); let loaded_paths: Vec<_> = result - .source_indices + .newline_indices .iter() - .map(|index| index.file.clone()) + .map(|index| index.file.as_str()) .collect(); assert!( loaded_paths.iter().any(|path| path.contains( From 892e3e3996eea90025b96d84d76fc667bf1ffba9 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sun, 3 May 2026 14:13:56 -0400 Subject: [PATCH 03/34] chore: apply cargo fmt --- src/v2/tests/src/helpers.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/v2/tests/src/helpers.rs b/src/v2/tests/src/helpers.rs index c1e72826acf..2f18729dcee 100644 --- a/src/v2/tests/src/helpers.rs +++ b/src/v2/tests/src/helpers.rs @@ -410,9 +410,9 @@ fn main() -> Int { generated_answer() } .map(|index| index.file.as_str()) .collect(); assert!( - loaded_paths.iter().any(|path| path.contains( - "generated/method_template_projection.dag" - )), + loaded_paths + .iter() + .any(|path| path.contains("generated/method_template_projection.dag")), "expected generated temp-root module to be loaded, got: {loaded_paths:?}" ); assert!( From 0c5afbbe9dadc7bae981017414a09fb44b1315ae Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sun, 3 May 2026 14:17:53 -0400 Subject: [PATCH 04/34] WIP: crisp-ibex-569 --- src/v2/tests/src/bootstrap.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/v2/tests/src/bootstrap.rs b/src/v2/tests/src/bootstrap.rs index 8f9654bdd36..c93ede429c9 100644 --- a/src/v2/tests/src/bootstrap.rs +++ b/src/v2/tests/src/bootstrap.rs @@ -22,8 +22,10 @@ fn build_stage0() -> std::path::PathBuf { "stage0 build failed:\n{}", String::from_utf8_lossy(&build.stderr) ); - let ws = crate::helpers::workspace_root(); - let bin = ws.join("target/release/v2-compiler"); + let target_dir = std::env::var_os("CARGO_TARGET_DIR") + .map(std::path::PathBuf::from) + .unwrap_or_else(|| crate::helpers::workspace_root().join("target")); + let bin = target_dir.join("release/v2-compiler"); assert!(bin.exists(), "stage0 binary not found at {}", bin.display()); bin } From 4ad2b1f58be64e7443e4ff7153d264073243311f Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sun, 3 May 2026 18:32:30 +0000 Subject: [PATCH 05/34] test(v2): fix source-root helper clippy --- src/v2/tests/src/helpers.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/v2/tests/src/helpers.rs b/src/v2/tests/src/helpers.rs index 2f18729dcee..936265bf6da 100644 --- a/src/v2/tests/src/helpers.rs +++ b/src/v2/tests/src/helpers.rs @@ -112,7 +112,7 @@ fn build_module_index_for_roots( let mut index = HashMap::new(); for root in roots { if root.exists() { - scan_dag_files(&root, &mut index); + scan_dag_files(root, &mut index); } } index From 29b20d0ca6c76bd40037baba7ceef06078efe442 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sun, 3 May 2026 18:42:41 +0000 Subject: [PATCH 06/34] test(v2): isolate ephemeral source-root temp dirs --- src/v2/tests/src/bootstrap.rs | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/v2/tests/src/bootstrap.rs b/src/v2/tests/src/bootstrap.rs index c93ede429c9..63d4674d6d5 100644 --- a/src/v2/tests/src/bootstrap.rs +++ b/src/v2/tests/src/bootstrap.rs @@ -30,6 +30,18 @@ fn build_stage0() -> std::path::PathBuf { bin } +fn temp_dir(name: &str) -> std::path::PathBuf { + let unique = format!( + "v2-bootstrap-{}-{}", + name, + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("clock before unix epoch") + .as_nanos() + ); + std::env::temp_dir().join(unique) +} + /// Run a self-compile: ` compile --source-root src/v2 --source-root dsl --output-dir `. /// Does NOT assert success — caller decides how to handle failure. fn run_self_compile( @@ -330,9 +342,9 @@ fn stage0_compile_accepts_dag_target() { fn stage0_compile_imports_ephemeral_generated_source_root() { let stage0_bin = build_stage0(); - let entry_root = std::env::temp_dir().join("v2-ephemeral-entry-root"); - let generated_root = std::env::temp_dir().join("v2-ephemeral-generated-root"); - let out_dir = std::env::temp_dir().join("v2-ephemeral-generated-out"); + let entry_root = temp_dir("ephemeral-entry-root"); + let generated_root = temp_dir("ephemeral-generated-root"); + let out_dir = temp_dir("ephemeral-generated-out"); let _ = std::fs::remove_dir_all(&entry_root); let _ = std::fs::remove_dir_all(&generated_root); let _ = std::fs::remove_dir_all(&out_dir); From b8ccfd882a583c3368fa08db04277e9a3adb1e72 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sun, 3 May 2026 15:01:53 -0400 Subject: [PATCH 07/34] WIP: crisp-ibex-569 --- src/v2/tests/src/bootstrap.rs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/v2/tests/src/bootstrap.rs b/src/v2/tests/src/bootstrap.rs index 63d4674d6d5..b7756e04cbf 100644 --- a/src/v2/tests/src/bootstrap.rs +++ b/src/v2/tests/src/bootstrap.rs @@ -389,18 +389,17 @@ fn main() -> Int { generated_answer() } "stage0 compile failed with ephemeral generated source root:\n{}", stderr ); - assert!( - stderr.contains("indexed 2 modules from 2 source roots"), - "expected both entry and generated temp roots to be indexed, got:\n{stderr}" - ); assert!( out_dir.join("dag-artifact.json").exists(), "expected dag artifact at {}", out_dir.join("dag-artifact.json").display() ); + let committed_generated_projection = crate::helpers::workspace_root() + .join("src/generated/method_template_projection.dag"); assert!( - !std::path::Path::new("src/generated/method_template_projection.dag").exists(), - "ratchet must not rely on a committed generated .dag under src/" + !committed_generated_projection.exists(), + "ratchet must not rely on committed generated .dag at {}", + committed_generated_projection.display() ); let _ = std::fs::remove_dir_all(&entry_root); From a2fb7a0feb8d2ce84b9fd8959778f0345cbb9f61 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sun, 3 May 2026 15:02:09 -0400 Subject: [PATCH 08/34] chore: apply cargo fmt --- src/v2/tests/src/bootstrap.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/v2/tests/src/bootstrap.rs b/src/v2/tests/src/bootstrap.rs index b7756e04cbf..450528884f0 100644 --- a/src/v2/tests/src/bootstrap.rs +++ b/src/v2/tests/src/bootstrap.rs @@ -394,8 +394,8 @@ fn main() -> Int { generated_answer() } "expected dag artifact at {}", out_dir.join("dag-artifact.json").display() ); - let committed_generated_projection = crate::helpers::workspace_root() - .join("src/generated/method_template_projection.dag"); + let committed_generated_projection = + crate::helpers::workspace_root().join("src/generated/method_template_projection.dag"); assert!( !committed_generated_projection.exists(), "ratchet must not rely on committed generated .dag at {}", From c3c074de39050287a217571998417befdd67a3c5 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Mon, 4 May 2026 11:51:42 -0400 Subject: [PATCH 09/34] WIP: crisp-ibex-569 --- src/v3/compiler/src/lower.rs | 158 +++++++++++++++++++++++++++++++++-- 1 file changed, 153 insertions(+), 5 deletions(-) diff --git a/src/v3/compiler/src/lower.rs b/src/v3/compiler/src/lower.rs index d1782f156e4..c40131702a1 100644 --- a/src/v3/compiler/src/lower.rs +++ b/src/v3/compiler/src/lower.rs @@ -2995,6 +2995,7 @@ fn lower_list_to_structural( "", element, element_type, + &LowerSubstStack::default(), symbols, dag, None, @@ -3074,6 +3075,7 @@ fn lower_string_map_entries( &entry.key, &entry.value, value_type, + &LowerSubstStack::default(), symbols, dag, None, @@ -3355,7 +3357,8 @@ fn lower_record_to_structural( dag: &mut Dag, pending_refined_function_refs: &HashSet, ) -> Option { - let Some(conj_id) = walk_to_conj_decl(dag, ty_decl_id) else { + let mut subst = LowerSubstStack::default(); + let Some(conj_id) = walk_to_conj_decl_with_subst_lower(dag, ty_decl_id, &mut subst) else { report_declaration_error( dag, Diagnostic::ResolveError { @@ -3372,7 +3375,15 @@ fn lower_record_to_structural( // while we walk the record literal. let type_fields: Vec<(String, DeclarationId)> = match &dag.declaration(conj_id).connective { TypeConnective::Conj { children } => { - children.iter().map(|f| (f.label.clone(), f.ty)).collect() + children + .iter() + .map(|f| { + ( + f.label.clone(), + resolve_decl_with_subst_lower(dag, f.ty, &subst, 0).unwrap_or(f.ty), + ) + }) + .collect() } _ => unreachable!("walk_to_conj_decl returned a non-Conj declaration"), }; @@ -3444,6 +3455,7 @@ fn lower_record_to_structural( type_label, &record_field.value, *type_field_id, + &subst, symbols, dag, category, @@ -3513,6 +3525,7 @@ fn lower_structural_field_value( field_label: &str, expr: &SurfaceExpr, expected_type: DeclarationId, + subst: &LowerSubstStack, symbols: &HashMap, dag: &mut Dag, category: Option, @@ -3559,13 +3572,15 @@ fn lower_structural_field_value( } let referenced = dag.declaration(decl_id); if is_value_level_arrow_declaration(referenced) - && declaration_ref_types_equivalent(dag, decl_id, expected_type, 0) + && declaration_ref_types_equivalent_with_subst(dag, decl_id, expected_type, subst, 0) { return Some(crate::dag::FieldValue::Reference(decl_id)); } if referenced .meta_tag - .is_some_and(|actual| declaration_ref_types_equivalent(dag, actual, expected_type, 0)) + .is_some_and(|actual| { + declaration_ref_types_equivalent_with_subst(dag, actual, expected_type, subst, 0) + }) { return Some(crate::dag::FieldValue::Reference(decl_id)); } @@ -3603,6 +3618,7 @@ fn lower_structural_field_value( field_label, element, element_type, + subst, symbols, dag, None, @@ -3652,7 +3668,9 @@ fn lower_structural_field_value( return None; } - if let Some(conj_id) = walk_to_conj_decl(dag, expected_type) { + let mut nested_subst = subst.clone(); + if let Some(conj_id) = walk_to_conj_decl_with_subst_lower(dag, expected_type, &mut nested_subst) + { let SurfaceExpr::Record { fields, .. } = expr else { report_declaration_error( dag, @@ -3685,6 +3703,12 @@ fn lower_structural_field_value( TypeConnective::Conj { children } => children .iter() .map(|child| (child.label.clone(), child.ty)) + .map(|(label, ty)| { + ( + label, + resolve_decl_with_subst_lower(dag, ty, &nested_subst, 0).unwrap_or(ty), + ) + }) .collect(), _ => unreachable!("walk_to_conj_decl returned non-Conj"), }; @@ -3735,6 +3759,7 @@ fn lower_structural_field_value( &label, &nested.value, ty, + &nested_subst, symbols, dag, None, @@ -3881,6 +3906,7 @@ fn lower_structural_field_value( field_label, arg, *payload_field_ty, + subst, symbols, dag, None, @@ -3941,6 +3967,7 @@ fn lower_structural_field_value( field_label, &field.value, *payload_field_ty, + subst, symbols, dag, None, @@ -4264,6 +4291,126 @@ fn declaration_ref_types_equivalent( } } +fn declaration_ref_types_equivalent_with_subst( + dag: &Dag, + lhs: DeclarationId, + rhs: DeclarationId, + rhs_subst: &LowerSubstStack, + depth: usize, +) -> bool { + if lhs == rhs { + return true; + } + if depth >= 32 { + return false; + } + let lhs_decl = dag.declaration(lhs); + let rhs_decl = dag.declaration(rhs); + match (&lhs_decl.connective, &rhs_decl.connective) { + ( + TypeConnective::Atom(AtomPayload::ResolvedByStructure(next)) + | TypeConnective::Atom(AtomPayload::ResolvedByName(next)), + _, + ) => declaration_ref_types_equivalent_with_subst(dag, *next, rhs, rhs_subst, depth + 1), + ( + _, + TypeConnective::Atom(AtomPayload::ResolvedByStructure(next)) + | TypeConnective::Atom(AtomPayload::ResolvedByName(next)), + ) => declaration_ref_types_equivalent_with_subst(dag, lhs, *next, rhs_subst, depth + 1), + (_, TypeConnective::Atom(AtomPayload::TypeParam(_))) => rhs_subst + .lookup(rhs) + .is_some_and(|bound| { + declaration_ref_types_equivalent_with_subst( + dag, + lhs, + bound, + rhs_subst, + depth + 1, + ) + }), + ( + TypeConnective::Instantiation { + template, + arguments, + }, + _, + ) if arguments.is_empty() => { + declaration_ref_types_equivalent_with_subst(dag, *template, rhs, rhs_subst, depth + 1) + } + ( + _, + TypeConnective::Instantiation { + template, + arguments, + }, + ) if arguments.is_empty() => { + declaration_ref_types_equivalent_with_subst(dag, lhs, *template, rhs_subst, depth + 1) + } + ( + TypeConnective::Arrow { + inputs: lhs_inputs, + output: lhs_output, + .. + }, + TypeConnective::Arrow { + inputs: rhs_inputs, + output: rhs_output, + .. + }, + ) => { + lhs_inputs.len() == rhs_inputs.len() + && lhs_inputs.iter().zip(rhs_inputs.iter()).all(|(lhs, rhs)| { + declaration_ref_types_equivalent_with_subst( + dag, + *lhs, + *rhs, + rhs_subst, + depth + 1, + ) + }) + && declaration_ref_types_equivalent_with_subst( + dag, + *lhs_output, + *rhs_output, + rhs_subst, + depth + 1, + ) + } + ( + TypeConnective::Instantiation { + template: lhs_template, + arguments: lhs_arguments, + }, + TypeConnective::Instantiation { + template: rhs_template, + arguments: rhs_arguments, + }, + ) => { + declaration_ref_types_equivalent_with_subst( + dag, + *lhs_template, + *rhs_template, + rhs_subst, + depth + 1, + ) && lhs_arguments.len() == rhs_arguments.len() + && lhs_arguments + .iter() + .zip(rhs_arguments.iter()) + .all(|(lhs_arg, rhs_arg)| { + lhs_arg.parameter == rhs_arg.parameter + && declaration_ref_types_equivalent_with_subst( + dag, + lhs_arg.value, + rhs_arg.value, + rhs_subst, + depth + 1, + ) + }) + } + _ => declaration_ref_types_equivalent(dag, lhs, rhs, depth), + } +} + /// Realization category tag for the lower-time narrowing check. /// Mirrors `emit_rust::RealizationCategory` but lives in lower's /// own namespace because the lowerer can't depend on emit_rust. @@ -7940,6 +8087,7 @@ mod tests { span: span.clone(), }, aliased_style, + &LowerSubstStack::default(), &symbols, &mut dag, None, From 6199c36d949f17b799952a64e683310b6ab27c65 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Mon, 4 May 2026 11:51:47 -0400 Subject: [PATCH 10/34] chore: apply cargo fmt --- src/v3/compiler/src/lower.rs | 45 ++++++++++++++---------------------- 1 file changed, 17 insertions(+), 28 deletions(-) diff --git a/src/v3/compiler/src/lower.rs b/src/v3/compiler/src/lower.rs index c40131702a1..59643dd96e2 100644 --- a/src/v3/compiler/src/lower.rs +++ b/src/v3/compiler/src/lower.rs @@ -3374,17 +3374,15 @@ fn lower_record_to_structural( // Snapshot the Conj's children to avoid borrowing conflicts // while we walk the record literal. let type_fields: Vec<(String, DeclarationId)> = match &dag.declaration(conj_id).connective { - TypeConnective::Conj { children } => { - children - .iter() - .map(|f| { - ( - f.label.clone(), - resolve_decl_with_subst_lower(dag, f.ty, &subst, 0).unwrap_or(f.ty), - ) - }) - .collect() - } + TypeConnective::Conj { children } => children + .iter() + .map(|f| { + ( + f.label.clone(), + resolve_decl_with_subst_lower(dag, f.ty, &subst, 0).unwrap_or(f.ty), + ) + }) + .collect(), _ => unreachable!("walk_to_conj_decl returned a non-Conj declaration"), }; if let Some(record_field) = duplicate_record_field(record_fields) { @@ -3576,12 +3574,9 @@ fn lower_structural_field_value( { return Some(crate::dag::FieldValue::Reference(decl_id)); } - if referenced - .meta_tag - .is_some_and(|actual| { - declaration_ref_types_equivalent_with_subst(dag, actual, expected_type, subst, 0) - }) - { + if referenced.meta_tag.is_some_and(|actual| { + declaration_ref_types_equivalent_with_subst(dag, actual, expected_type, subst, 0) + }) { return Some(crate::dag::FieldValue::Reference(decl_id)); } } @@ -4317,17 +4312,11 @@ fn declaration_ref_types_equivalent_with_subst( TypeConnective::Atom(AtomPayload::ResolvedByStructure(next)) | TypeConnective::Atom(AtomPayload::ResolvedByName(next)), ) => declaration_ref_types_equivalent_with_subst(dag, lhs, *next, rhs_subst, depth + 1), - (_, TypeConnective::Atom(AtomPayload::TypeParam(_))) => rhs_subst - .lookup(rhs) - .is_some_and(|bound| { - declaration_ref_types_equivalent_with_subst( - dag, - lhs, - bound, - rhs_subst, - depth + 1, - ) - }), + (_, TypeConnective::Atom(AtomPayload::TypeParam(_))) => { + rhs_subst.lookup(rhs).is_some_and(|bound| { + declaration_ref_types_equivalent_with_subst(dag, lhs, bound, rhs_subst, depth + 1) + }) + } ( TypeConnective::Instantiation { template, From 6cf05ea99cc978caf997540c6b01ffd61f417200 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Mon, 4 May 2026 11:54:55 -0400 Subject: [PATCH 11/34] WIP: crisp-ibex-569 --- .../integration/m2_feature_parity_test.rs | 103 ++++++++++++++++++ 1 file changed, 103 insertions(+) diff --git a/src/v3/compiler/tests/integration/m2_feature_parity_test.rs b/src/v3/compiler/tests/integration/m2_feature_parity_test.rs index 7378680bb1d..a0a981add3b 100644 --- a/src/v3/compiler/tests/integration/m2_feature_parity_test.rs +++ b/src/v3/compiler/tests/integration/m2_feature_parity_test.rs @@ -426,6 +426,109 @@ fn test_3a2_lensish_int_carrier_lowers_branch_and_monoid_fn_refs() { assert_eq!(op, &v3_compiler::dag::FieldValue::Reference(op_id)); } +#[test] +fn test_3a2_lens_int_data_substitutes_generic_conj_fields() { + let src = "\ + import std.algebra { Monoid }\n\ + import std.substrate { Dag, Behavior, LoopBound }\n\ + import v3.std.dimensions { Witness, OptionalDiagnostic }\n\ + import v3.std.lens { Lens }\n\ + fn lens_read(d: Dag, b: Behavior) -> Witness { Inhabits(1) }\n\ + fn int_add(a: Int, b: Int) -> Int = a + b\n\ + fn int_max(a: Int, b: Int) -> Int = if a > b then a else b\n\ + fn lens_iterate(c: Int, bound: LoopBound) -> Int = c\n\ + fn lens_validate(d: Dag, c: Int) -> OptionalDiagnostic { NoDiagnostic }\n\ + data complexity_lens_seed: Lens = {\n\ + name: \"complexity\",\n\ + read: lens_read,\n\ + sequential: { op: int_add, identity: 0 },\n\ + branch: int_max,\n\ + iterate: lens_iterate,\n\ + validate: lens_validate\n\ + }"; + let dag = cached_compile_to_dag(src, "lens_int_generic_conj_substitution.v3"); + assert!( + dag.diagnostics().is_empty(), + "Lens data body should compile after substituting Lens and Monoid fields: {:?}", + dag.diagnostics() + ); + let read_id = dag.declaration_by_name("lens_read").unwrap().id; + let op_id = dag.declaration_by_name("int_add").unwrap().id; + let validate_id = dag.declaration_by_name("lens_validate").unwrap().id; + let decl = dag + .declaration_by_name("complexity_lens_seed") + .expect("complexity_lens_seed must exist"); + let Some(v3_compiler::dag::ValueBody::Structural { fields }) = &decl.value_body else { + panic!("expected Lens data to lower structurally, got {:?}", decl.value_body); + }; + assert_eq!(fields.len(), 6, "Lens carrier shape must stay six-field"); + let read = fields.iter().find(|(label, _)| label == "read").unwrap(); + assert_eq!(read.1, v3_compiler::dag::FieldValue::Reference(read_id)); + let validate = fields + .iter() + .find(|(label, _)| label == "validate") + .unwrap(); + assert_eq!( + validate.1, + v3_compiler::dag::FieldValue::Reference(validate_id) + ); + let sequential = fields + .iter() + .find(|(label, _)| label == "sequential") + .map(|(_, value)| value) + .expect("sequential field"); + let v3_compiler::dag::FieldValue::Record(nested) = sequential else { + panic!("`sequential` must lower as nested Monoid record, got {sequential:?}"); + }; + let op = nested.iter().find(|(label, _)| label == "op").unwrap(); + assert_eq!(op.1, v3_compiler::dag::FieldValue::Reference(op_id)); + let identity = nested + .iter() + .find(|(label, _)| label == "identity") + .unwrap(); + assert_eq!( + identity.1, + v3_compiler::dag::FieldValue::Literal(v3_compiler::dag::LiteralBits::Int(0)) + ); +} + +#[test] +fn test_3a2_lens_int_data_rejects_unsubstituted_read_witness_mismatch() { + let src = "\ + import std.algebra { Monoid }\n\ + import std.substrate { Dag, Behavior, LoopBound }\n\ + import v3.std.dimensions { Witness, OptionalDiagnostic }\n\ + import v3.std.lens { Lens }\n\ + fn lens_read(d: Dag, b: Behavior) -> Witness { Inhabits(\"bad\") }\n\ + fn int_add(a: Int, b: Int) -> Int = a + b\n\ + fn int_max(a: Int, b: Int) -> Int = if a > b then a else b\n\ + fn lens_iterate(c: Int, bound: LoopBound) -> Int = c\n\ + fn lens_validate(d: Dag, c: Int) -> OptionalDiagnostic { NoDiagnostic }\n\ + data bad_complexity_lens_seed: Lens = {\n\ + name: \"complexity\",\n\ + read: lens_read,\n\ + sequential: { op: int_add, identity: 0 },\n\ + branch: int_max,\n\ + iterate: lens_iterate,\n\ + validate: lens_validate\n\ + }"; + let err = compile_to_dag(src, "lens_int_generic_conj_substitution_negative.v3") + .expect_err("Lens.read must reject Witness after substituting C := Int"); + let CompileError::Semantic(dag) = err else { + panic!("expected semantic substitution mismatch, got {err:?}"); + }; + let joined = dag + .diagnostics() + .iter() + .map(|(_, d)| format!("{d:?}")) + .collect::>() + .join("\n"); + assert!( + joined.contains("read") && joined.contains("Lens") == false, + "diagnostic should be the typed field mismatch path, not a generic Lens fallback; got:\n{joined}" + ); +} + #[test] fn test_3a2_record_data_rejects_type_alias_as_function_ref_value() { let src = "\ From 74c67d25ad3a9bad619c52737caec8931bbd3bd0 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Mon, 4 May 2026 11:55:04 -0400 Subject: [PATCH 12/34] chore: apply cargo fmt --- .../tests/integration/m2_feature_parity_test.rs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/v3/compiler/tests/integration/m2_feature_parity_test.rs b/src/v3/compiler/tests/integration/m2_feature_parity_test.rs index a0a981add3b..35f8f763c86 100644 --- a/src/v3/compiler/tests/integration/m2_feature_parity_test.rs +++ b/src/v3/compiler/tests/integration/m2_feature_parity_test.rs @@ -459,9 +459,16 @@ fn test_3a2_lens_int_data_substitutes_generic_conj_fields() { .declaration_by_name("complexity_lens_seed") .expect("complexity_lens_seed must exist"); let Some(v3_compiler::dag::ValueBody::Structural { fields }) = &decl.value_body else { - panic!("expected Lens data to lower structurally, got {:?}", decl.value_body); + panic!( + "expected Lens data to lower structurally, got {:?}", + decl.value_body + ); }; - assert_eq!(fields.len(), 6, "Lens carrier shape must stay six-field"); + assert_eq!( + fields.len(), + 6, + "Lens carrier shape must stay six-field" + ); let read = fields.iter().find(|(label, _)| label == "read").unwrap(); assert_eq!(read.1, v3_compiler::dag::FieldValue::Reference(read_id)); let validate = fields From be02b60beaf44fccdc4e9ee23c1c4ad2db2eaae2 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Mon, 4 May 2026 11:57:47 -0400 Subject: [PATCH 13/34] WIP: crisp-ibex-569 --- src/v3/compiler/tests/integration/m2_feature_parity_test.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/v3/compiler/tests/integration/m2_feature_parity_test.rs b/src/v3/compiler/tests/integration/m2_feature_parity_test.rs index 35f8f763c86..9b666ddf318 100644 --- a/src/v3/compiler/tests/integration/m2_feature_parity_test.rs +++ b/src/v3/compiler/tests/integration/m2_feature_parity_test.rs @@ -531,7 +531,9 @@ fn test_3a2_lens_int_data_rejects_unsubstituted_read_witness_mismatch() { .collect::>() .join("\n"); assert!( - joined.contains("read") && joined.contains("Lens") == false, + joined.contains("read") + && !joined.contains("record type (Conj)") + && !joined.contains("cannot apply inhabitance checking"), "diagnostic should be the typed field mismatch path, not a generic Lens fallback; got:\n{joined}" ); } From 80929ae9dc73536648b8aaaa884c78423a0041a4 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Mon, 4 May 2026 16:07:19 +0000 Subject: [PATCH 14/34] Tighten lens data substitution diagnostics --- src/v3/compiler/src/lower.rs | 96 +++++++++++++++++++ .../integration/m2_feature_parity_test.rs | 5 +- 2 files changed, 100 insertions(+), 1 deletion(-) diff --git a/src/v3/compiler/src/lower.rs b/src/v3/compiler/src/lower.rs index 59643dd96e2..037ad8d0483 100644 --- a/src/v3/compiler/src/lower.rs +++ b/src/v3/compiler/src/lower.rs @@ -3574,11 +3574,43 @@ fn lower_structural_field_value( { return Some(crate::dag::FieldValue::Reference(decl_id)); } + if is_value_level_arrow_declaration(referenced) { + report_declaration_error( + dag, + Diagnostic::ResolveError { + name: format!( + "data `{data_name}` field `{field_label}` declaration reference `{}` has type `{}` but expected substituted type `{}`", + declaration_display_name(dag, decl_id), + structural_type_display(dag, decl_id, &LowerSubstStack::default(), 0), + structural_type_display(dag, expected_type, subst, 0), + ), + span: span.clone(), + fixes: Vec::new(), + }, + ); + return None; + } if referenced.meta_tag.is_some_and(|actual| { declaration_ref_types_equivalent_with_subst(dag, actual, expected_type, subst, 0) }) { return Some(crate::dag::FieldValue::Reference(decl_id)); } + if let Some(actual) = referenced.meta_tag { + report_declaration_error( + dag, + Diagnostic::ResolveError { + name: format!( + "data `{data_name}` field `{field_label}` declaration reference `{}` has meta type `{}` but expected substituted type `{}`", + declaration_display_name(dag, decl_id), + structural_type_display(dag, actual, &LowerSubstStack::default(), 0), + structural_type_display(dag, expected_type, subst, 0), + ), + span: span.clone(), + fixes: Vec::new(), + }, + ); + return None; + } } match lower_scalar_literal_for_type(expr, expected_type, dag) { @@ -4009,6 +4041,70 @@ fn is_value_level_arrow_declaration(decl: &crate::dag::Declaration) -> bool { ) } +fn structural_type_display( + dag: &Dag, + decl_id: DeclarationId, + subst: &LowerSubstStack, + depth: usize, +) -> String { + if depth >= 32 { + return format!("", decl_id.raw()); + } + let decl = dag.declaration(decl_id); + if decl.name.is_some() && !matches!(decl.connective, TypeConnective::Arrow { .. }) { + return declaration_display_name(dag, decl_id); + } + match &decl.connective { + TypeConnective::Atom(AtomPayload::TypeParam(_)) => { + if let Some(bound) = subst.lookup(decl_id) { + return structural_type_display(dag, bound, subst, depth + 1); + } + declaration_display_name(dag, decl_id) + } + TypeConnective::Atom(AtomPayload::ResolvedByStructure(next)) + | TypeConnective::Atom(AtomPayload::ResolvedByName(next)) => { + structural_type_display(dag, *next, subst, depth + 1) + } + TypeConnective::Instantiation { + template, + arguments, + } => { + let template_name = declaration_display_name(dag, *template); + if arguments.is_empty() { + return template_name; + } + let args = arguments + .iter() + .map(|arg| structural_type_display(dag, arg.value, subst, depth + 1)) + .collect::>() + .join(", "); + format!("{template_name}<{args}>") + } + TypeConnective::Arrow { inputs, output, .. } => { + let inputs = inputs + .iter() + .map(|input| structural_type_display(dag, *input, subst, depth + 1)) + .collect::>() + .join(", "); + let output = structural_type_display(dag, *output, subst, depth + 1); + format!("fn({inputs}) -> {output}") + } + TypeConnective::Cardinality(payload) => { + let element = structural_type_display(dag, payload.element(), subst, depth + 1); + match payload.bound() { + CardinalityBound::Exact(1) => element, + CardinalityBound::Exact(n) => format!("{element}^{n}"), + CardinalityBound::AtMostOne => format!("{element}?"), + CardinalityBound::Unbounded => format!("List<{element}>"), + } + } + TypeConnective::Conj { .. } + | TypeConnective::Disj { .. } + | TypeConnective::Atom(AtomPayload::UnresolvedIdentifier(_)) + | TypeConnective::Atom(AtomPayload::Literal(_)) => declaration_display_name(dag, decl_id), + } +} + /// 🟢 TERMINAL — file-local lowering outcome coproduct. /// /// 4-pattern check: Pattern 1 (fact placement) reaches the terminal diff --git a/src/v3/compiler/tests/integration/m2_feature_parity_test.rs b/src/v3/compiler/tests/integration/m2_feature_parity_test.rs index 9b666ddf318..16269ba29de 100644 --- a/src/v3/compiler/tests/integration/m2_feature_parity_test.rs +++ b/src/v3/compiler/tests/integration/m2_feature_parity_test.rs @@ -532,9 +532,12 @@ fn test_3a2_lens_int_data_rejects_unsubstituted_read_witness_mismatch() { .join("\n"); assert!( joined.contains("read") + && joined.contains("declaration reference `lens_read`") + && joined.contains("Witness") + && joined.contains("Witness") && !joined.contains("record type (Conj)") && !joined.contains("cannot apply inhabitance checking"), - "diagnostic should be the typed field mismatch path, not a generic Lens fallback; got:\n{joined}" + "diagnostic should be the typed substituted field mismatch path, not a generic Lens fallback; got:\n{joined}" ); } From 26dc6fb0628542d7d3ff5b842828e73eb074daaf Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Mon, 4 May 2026 12:09:52 -0400 Subject: [PATCH 15/34] WIP: crisp-ibex-569 --- src/v3/compiler/src/lower.rs | 84 ++----------------- .../integration/m2_feature_parity_test.rs | 73 ++++++++++++---- 2 files changed, 65 insertions(+), 92 deletions(-) diff --git a/src/v3/compiler/src/lower.rs b/src/v3/compiler/src/lower.rs index 037ad8d0483..fc6515200c7 100644 --- a/src/v3/compiler/src/lower.rs +++ b/src/v3/compiler/src/lower.rs @@ -3577,13 +3577,9 @@ fn lower_structural_field_value( if is_value_level_arrow_declaration(referenced) { report_declaration_error( dag, - Diagnostic::ResolveError { - name: format!( - "data `{data_name}` field `{field_label}` declaration reference `{}` has type `{}` but expected substituted type `{}`", - declaration_display_name(dag, decl_id), - structural_type_display(dag, decl_id, &LowerSubstStack::default(), 0), - structural_type_display(dag, expected_type, subst, 0), - ), + Diagnostic::TypeMismatch { + expected: TypeShape::new(expected_type), + actual: TypeShape::new(decl_id), span: span.clone(), fixes: Vec::new(), }, @@ -3598,13 +3594,9 @@ fn lower_structural_field_value( if let Some(actual) = referenced.meta_tag { report_declaration_error( dag, - Diagnostic::ResolveError { - name: format!( - "data `{data_name}` field `{field_label}` declaration reference `{}` has meta type `{}` but expected substituted type `{}`", - declaration_display_name(dag, decl_id), - structural_type_display(dag, actual, &LowerSubstStack::default(), 0), - structural_type_display(dag, expected_type, subst, 0), - ), + Diagnostic::TypeMismatch { + expected: TypeShape::new(expected_type), + actual: TypeShape::new(actual), span: span.clone(), fixes: Vec::new(), }, @@ -4041,70 +4033,6 @@ fn is_value_level_arrow_declaration(decl: &crate::dag::Declaration) -> bool { ) } -fn structural_type_display( - dag: &Dag, - decl_id: DeclarationId, - subst: &LowerSubstStack, - depth: usize, -) -> String { - if depth >= 32 { - return format!("", decl_id.raw()); - } - let decl = dag.declaration(decl_id); - if decl.name.is_some() && !matches!(decl.connective, TypeConnective::Arrow { .. }) { - return declaration_display_name(dag, decl_id); - } - match &decl.connective { - TypeConnective::Atom(AtomPayload::TypeParam(_)) => { - if let Some(bound) = subst.lookup(decl_id) { - return structural_type_display(dag, bound, subst, depth + 1); - } - declaration_display_name(dag, decl_id) - } - TypeConnective::Atom(AtomPayload::ResolvedByStructure(next)) - | TypeConnective::Atom(AtomPayload::ResolvedByName(next)) => { - structural_type_display(dag, *next, subst, depth + 1) - } - TypeConnective::Instantiation { - template, - arguments, - } => { - let template_name = declaration_display_name(dag, *template); - if arguments.is_empty() { - return template_name; - } - let args = arguments - .iter() - .map(|arg| structural_type_display(dag, arg.value, subst, depth + 1)) - .collect::>() - .join(", "); - format!("{template_name}<{args}>") - } - TypeConnective::Arrow { inputs, output, .. } => { - let inputs = inputs - .iter() - .map(|input| structural_type_display(dag, *input, subst, depth + 1)) - .collect::>() - .join(", "); - let output = structural_type_display(dag, *output, subst, depth + 1); - format!("fn({inputs}) -> {output}") - } - TypeConnective::Cardinality(payload) => { - let element = structural_type_display(dag, payload.element(), subst, depth + 1); - match payload.bound() { - CardinalityBound::Exact(1) => element, - CardinalityBound::Exact(n) => format!("{element}^{n}"), - CardinalityBound::AtMostOne => format!("{element}?"), - CardinalityBound::Unbounded => format!("List<{element}>"), - } - } - TypeConnective::Conj { .. } - | TypeConnective::Disj { .. } - | TypeConnective::Atom(AtomPayload::UnresolvedIdentifier(_)) - | TypeConnective::Atom(AtomPayload::Literal(_)) => declaration_display_name(dag, decl_id), - } -} - /// 🟢 TERMINAL — file-local lowering outcome coproduct. /// /// 4-pattern check: Pattern 1 (fact placement) reaches the terminal diff --git a/src/v3/compiler/tests/integration/m2_feature_parity_test.rs b/src/v3/compiler/tests/integration/m2_feature_parity_test.rs index 16269ba29de..5daa400f3cf 100644 --- a/src/v3/compiler/tests/integration/m2_feature_parity_test.rs +++ b/src/v3/compiler/tests/integration/m2_feature_parity_test.rs @@ -10,8 +10,8 @@ use crate::common::cached_compile_to_dag; use v3_compiler::compile_to_dag; use v3_compiler::dag::{ - ArrowBody, AtomPayload, Behavior, ComparisonOp, Dag, LogicalOp, OperatorKind, TransformTarget, - TypeConnective, + ArrowBody, AtomPayload, Behavior, ComparisonOp, Dag, DeclarationId, LogicalOp, OperatorKind, + TransformTarget, TypeConnective, }; use v3_compiler::parse_for_test; use v3_compiler::parse_surface::{SurfaceExpr, SurfaceItem}; @@ -27,6 +27,37 @@ fn compile_any(src: &str, file: &str) -> Dag { } } +fn arrow_output_decl(dag: &Dag, decl_id: DeclarationId) -> DeclarationId { + let TypeConnective::Arrow { output, .. } = &dag.declaration(decl_id).connective else { + panic!("expected {decl_id:?} to be an Arrow declaration"); + }; + *output +} + +fn assert_instantiation_arg( + dag: &Dag, + decl_id: DeclarationId, + expected_template: DeclarationId, + expected_arg: DeclarationId, +) { + let TypeConnective::Instantiation { + template, + arguments, + } = &dag.declaration(decl_id).connective + else { + panic!("expected {decl_id:?} to be an Instantiation"); + }; + assert_eq!( + *template, expected_template, + "instantiation template mismatch" + ); + assert_eq!(arguments.len(), 1, "expected one template argument"); + assert_eq!( + arguments[0].value, expected_arg, + "instantiation argument mismatch" + ); +} + // ================================================================= // 3a.4 — Surface generics // ================================================================= @@ -524,20 +555,34 @@ fn test_3a2_lens_int_data_rejects_unsubstituted_read_witness_mismatch() { let CompileError::Semantic(dag) = err else { panic!("expected semantic substitution mismatch, got {err:?}"); }; - let joined = dag + let lens_read_id = dag + .declaration_by_name("lens_read") + .expect("lens_read must exist") + .id; + let expected_read_ty = dag .diagnostics() .iter() - .map(|(_, d)| format!("{d:?}")) - .collect::>() - .join("\n"); - assert!( - joined.contains("read") - && joined.contains("declaration reference `lens_read`") - && joined.contains("Witness") - && joined.contains("Witness") - && !joined.contains("record type (Conj)") - && !joined.contains("cannot apply inhabitance checking"), - "diagnostic should be the typed substituted field mismatch path, not a generic Lens fallback; got:\n{joined}" + .find_map(|(_, diagnostic)| match diagnostic { + Diagnostic::TypeMismatch { + expected, actual, .. + } if actual.declaration == lens_read_id => Some(expected.declaration), + _ => None, + }) + .expect("read mismatch must surface as structured TypeMismatch on lens_read"); + let witness = dag.declaration_by_name("Witness").unwrap().id; + let int_decl = dag.declaration_by_name("Int").unwrap().id; + let string_decl = dag.declaration_by_name("String").unwrap().id; + assert_instantiation_arg( + &dag, + arrow_output_decl(&dag, lens_read_id), + witness, + string_decl, + ); + assert_instantiation_arg( + &dag, + arrow_output_decl(&dag, expected_read_ty), + witness, + int_decl, ); } From 35b3c8f5e4785fc7a825b6a9aeaf5de33ef2b08d Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Mon, 4 May 2026 12:13:16 -0400 Subject: [PATCH 16/34] WIP: crisp-ibex-569 --- src/v3/compiler/src/lower.rs | 9 +++- .../integration/m2_feature_parity_test.rs | 50 +++++++++++++++---- 2 files changed, 46 insertions(+), 13 deletions(-) diff --git a/src/v3/compiler/src/lower.rs b/src/v3/compiler/src/lower.rs index fc6515200c7..86d1660099c 100644 --- a/src/v3/compiler/src/lower.rs +++ b/src/v3/compiler/src/lower.rs @@ -3039,6 +3039,7 @@ fn lower_map_to_structural( name, entries, value_type, + &LowerSubstStack::default(), symbols, dag, pending_refined_function_refs, @@ -3050,6 +3051,7 @@ fn lower_string_map_entries( data_name: &str, entries: &[crate::parse_surface::SurfaceMapEntry], value_type: DeclarationId, + subst: &LowerSubstStack, symbols: &HashMap, dag: &mut Dag, pending_refined_function_refs: &HashSet, @@ -3074,8 +3076,8 @@ fn lower_string_map_entries( data_name, &entry.key, &entry.value, - value_type, - &LowerSubstStack::default(), + resolve_decl_with_subst_lower(dag, value_type, subst, 0).unwrap_or(value_type), + subst, symbols, dag, None, @@ -3617,6 +3619,8 @@ fn lower_structural_field_value( } if let Some(element_type) = list_element_type(dag, expected_type) { + let element_type = + resolve_decl_with_subst_lower(dag, element_type, subst, 0).unwrap_or(element_type); let SurfaceExpr::List { elements, .. } = expr else { report_declaration_error( dag, @@ -3666,6 +3670,7 @@ fn lower_structural_field_value( data_name, entries, value_type, + subst, symbols, dag, pending_refined_function_refs, diff --git a/src/v3/compiler/tests/integration/m2_feature_parity_test.rs b/src/v3/compiler/tests/integration/m2_feature_parity_test.rs index 5daa400f3cf..a5daccbabf1 100644 --- a/src/v3/compiler/tests/integration/m2_feature_parity_test.rs +++ b/src/v3/compiler/tests/integration/m2_feature_parity_test.rs @@ -34,12 +34,11 @@ fn arrow_output_decl(dag: &Dag, decl_id: DeclarationId) -> DeclarationId { *output } -fn assert_instantiation_arg( +fn instantiation_arg( dag: &Dag, decl_id: DeclarationId, expected_template: DeclarationId, - expected_arg: DeclarationId, -) { +) -> DeclarationId { let TypeConnective::Instantiation { template, arguments, @@ -52,10 +51,36 @@ fn assert_instantiation_arg( "instantiation template mismatch" ); assert_eq!(arguments.len(), 1, "expected one template argument"); - assert_eq!( - arguments[0].value, expected_arg, - "instantiation argument mismatch" - ); + arguments[0].value +} + +fn same_instantiation_shape(dag: &Dag, lhs: DeclarationId, rhs: DeclarationId) -> bool { + if lhs == rhs { + return true; + } + match ( + &dag.declaration(lhs).connective, + &dag.declaration(rhs).connective, + ) { + ( + TypeConnective::Instantiation { + template: lhs_template, + arguments: lhs_arguments, + }, + TypeConnective::Instantiation { + template: rhs_template, + arguments: rhs_arguments, + }, + ) => { + lhs_template == rhs_template + && lhs_arguments.len() == rhs_arguments.len() + && lhs_arguments + .iter() + .zip(rhs_arguments.iter()) + .all(|(lhs, rhs)| same_instantiation_shape(dag, lhs.value, rhs.value)) + } + _ => false, + } } // ================================================================= @@ -572,18 +597,21 @@ fn test_3a2_lens_int_data_rejects_unsubstituted_read_witness_mismatch() { let witness = dag.declaration_by_name("Witness").unwrap().id; let int_decl = dag.declaration_by_name("Int").unwrap().id; let string_decl = dag.declaration_by_name("String").unwrap().id; - assert_instantiation_arg( + let actual_arg = instantiation_arg( &dag, arrow_output_decl(&dag, lens_read_id), witness, - string_decl, ); - assert_instantiation_arg( + let expected_arg = instantiation_arg( &dag, arrow_output_decl(&dag, expected_read_ty), witness, - int_decl, ); + assert!( + same_instantiation_shape(&dag, actual_arg, string_decl), + "actual read output must be Witness" + ); + assert_eq!(expected_arg, int_decl, "expected read output must be Witness"); } #[test] From 7b5d246171e6c2f73678132956eba0cf24b6e3c9 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Mon, 4 May 2026 12:13:19 -0400 Subject: [PATCH 17/34] chore: apply cargo fmt --- .../tests/integration/m2_feature_parity_test.rs | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/src/v3/compiler/tests/integration/m2_feature_parity_test.rs b/src/v3/compiler/tests/integration/m2_feature_parity_test.rs index a5daccbabf1..ba6829b7656 100644 --- a/src/v3/compiler/tests/integration/m2_feature_parity_test.rs +++ b/src/v3/compiler/tests/integration/m2_feature_parity_test.rs @@ -597,21 +597,16 @@ fn test_3a2_lens_int_data_rejects_unsubstituted_read_witness_mismatch() { let witness = dag.declaration_by_name("Witness").unwrap().id; let int_decl = dag.declaration_by_name("Int").unwrap().id; let string_decl = dag.declaration_by_name("String").unwrap().id; - let actual_arg = instantiation_arg( - &dag, - arrow_output_decl(&dag, lens_read_id), - witness, - ); - let expected_arg = instantiation_arg( - &dag, - arrow_output_decl(&dag, expected_read_ty), - witness, - ); + let actual_arg = instantiation_arg(&dag, arrow_output_decl(&dag, lens_read_id), witness); + let expected_arg = instantiation_arg(&dag, arrow_output_decl(&dag, expected_read_ty), witness); assert!( same_instantiation_shape(&dag, actual_arg, string_decl), "actual read output must be Witness" ); - assert_eq!(expected_arg, int_decl, "expected read output must be Witness"); + assert_eq!( + expected_arg, int_decl, + "expected read output must be Witness" + ); } #[test] From 7484be8b55d605f2adba86b9005cbcbd99393b57 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Mon, 4 May 2026 12:14:16 -0400 Subject: [PATCH 18/34] WIP: crisp-ibex-569 --- src/v3/compiler/tests/integration/m2_feature_parity_test.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/v3/compiler/tests/integration/m2_feature_parity_test.rs b/src/v3/compiler/tests/integration/m2_feature_parity_test.rs index ba6829b7656..ff6df80c5ae 100644 --- a/src/v3/compiler/tests/integration/m2_feature_parity_test.rs +++ b/src/v3/compiler/tests/integration/m2_feature_parity_test.rs @@ -603,8 +603,8 @@ fn test_3a2_lens_int_data_rejects_unsubstituted_read_witness_mismatch() { same_instantiation_shape(&dag, actual_arg, string_decl), "actual read output must be Witness" ); - assert_eq!( - expected_arg, int_decl, + assert!( + same_instantiation_shape(&dag, expected_arg, int_decl), "expected read output must be Witness" ); } From 0630bae6584be9fa0deb3ae4f61b8452de402433 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Mon, 4 May 2026 12:15:44 -0400 Subject: [PATCH 19/34] WIP: crisp-ibex-569 --- .../integration/m2_feature_parity_test.rs | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/src/v3/compiler/tests/integration/m2_feature_parity_test.rs b/src/v3/compiler/tests/integration/m2_feature_parity_test.rs index ff6df80c5ae..692105565d1 100644 --- a/src/v3/compiler/tests/integration/m2_feature_parity_test.rs +++ b/src/v3/compiler/tests/integration/m2_feature_parity_test.rs @@ -397,6 +397,62 @@ fn test_3a2_record_data_lowers_function_refs_and_nested_records() { ); } +#[test] +fn test_3a2_record_data_substitutes_generic_list_and_map_fields() { + let src = "\ + type GenericAggregates { + items: List, + table: Map + }\n\ + data aggregate_int: GenericAggregates = { + items: [1, 2], + table: { \"one\": 1, \"two\": 2 } + }"; + let dag = cached_compile_to_dag(src, "generic_aggregate_fields.v3"); + let decl = dag + .declaration_by_name("aggregate_int") + .expect("aggregate_int must exist"); + let Some(v3_compiler::dag::ValueBody::Structural { fields }) = &decl.value_body else { + panic!("expected aggregate_int to lower structurally, got {:?}", decl.value_body); + }; + let items = fields + .iter() + .find(|(label, _)| label == "items") + .map(|(_, value)| value) + .expect("items field"); + let v3_compiler::dag::FieldValue::List(items) = items else { + panic!("items must lower as a structural list, got {items:?}"); + }; + assert_eq!( + items, + &vec![ + v3_compiler::dag::FieldValue::Literal(v3_compiler::dag::LiteralBits::Int(1)), + v3_compiler::dag::FieldValue::Literal(v3_compiler::dag::LiteralBits::Int(2)), + ] + ); + let table = fields + .iter() + .find(|(label, _)| label == "table") + .map(|(_, value)| value) + .expect("table field"); + let v3_compiler::dag::FieldValue::Map(table) = table else { + panic!("table must lower as a structural map, got {table:?}"); + }; + assert_eq!( + table.entries(), + &[ + ( + "one".to_string(), + v3_compiler::dag::FieldValue::Literal(v3_compiler::dag::LiteralBits::Int(1)) + ), + ( + "two".to_string(), + v3_compiler::dag::FieldValue::Literal(v3_compiler::dag::LiteralBits::Int(2)) + ), + ] + ); +} + /// Inbox #1130 / #1139 — complexity `Lens` migration readiness: Prereq-1 /// (Arrow-typed record fields + nested `Monoid`) already lowers `fn` /// declaration references. This shape matches `Lens`'s `branch` / From 096b36d6fba07c9c37d21037139871e8cb601dea Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Mon, 4 May 2026 12:15:48 -0400 Subject: [PATCH 20/34] chore: apply cargo fmt --- src/v3/compiler/tests/integration/m2_feature_parity_test.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/v3/compiler/tests/integration/m2_feature_parity_test.rs b/src/v3/compiler/tests/integration/m2_feature_parity_test.rs index 692105565d1..f80b2f1c6f3 100644 --- a/src/v3/compiler/tests/integration/m2_feature_parity_test.rs +++ b/src/v3/compiler/tests/integration/m2_feature_parity_test.rs @@ -413,7 +413,10 @@ fn test_3a2_record_data_substitutes_generic_list_and_map_fields() { .declaration_by_name("aggregate_int") .expect("aggregate_int must exist"); let Some(v3_compiler::dag::ValueBody::Structural { fields }) = &decl.value_body else { - panic!("expected aggregate_int to lower structurally, got {:?}", decl.value_body); + panic!( + "expected aggregate_int to lower structurally, got {:?}", + decl.value_body + ); }; let items = fields .iter() From e6eddb18d9a0bac331e014fb0f74beb63f54d444 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Mon, 4 May 2026 12:17:25 -0400 Subject: [PATCH 21/34] WIP: crisp-ibex-569 --- src/v3/compiler/src/lower.rs | 89 +++++++++++++++++++ .../integration/m2_feature_parity_test.rs | 35 ++------ 2 files changed, 95 insertions(+), 29 deletions(-) diff --git a/src/v3/compiler/src/lower.rs b/src/v3/compiler/src/lower.rs index 86d1660099c..662a725ba33 100644 --- a/src/v3/compiler/src/lower.rs +++ b/src/v3/compiler/src/lower.rs @@ -7917,6 +7917,95 @@ mod tests { id } + fn push_anonymous_test_declaration(dag: &mut Dag, connective: TypeConnective) -> DeclarationId { + let id = dag.alloc_declaration_id(); + dag.push_declaration(Declaration { + id, + name: None, + connective, + type_params: Vec::new(), + phantom_params: Vec::new(), + meta_tag: None, + specialization_parent: None, + inhabits: None, + value_body: None, + refinement: None, + nominal_opacity: None, + span: test_span(), + }); + id + } + + #[test] + fn structural_map_field_lowers_values_with_active_substitution() { + let mut dag = Dag::new(); + let int_id = dag.declaration_by_name("Int").expect("Int").id; + let string_id = dag.declaration_by_name("String").expect("String").id; + let map_id = dag.declaration_by_name("Map").expect("Map").id; + let map_key_param = dag.declaration(map_id).type_params[0]; + let map_value_param = dag.declaration(map_id).type_params[1]; + let c_param = push_anonymous_test_declaration( + &mut dag, + TypeConnective::Atom(AtomPayload::TypeParam("C".to_string())), + ); + let map_string_c = push_anonymous_test_declaration( + &mut dag, + TypeConnective::Instantiation { + template: map_id, + arguments: vec![ + TemplateArgument { + parameter: map_key_param, + value: string_id, + }, + TemplateArgument { + parameter: map_value_param, + value: c_param, + }, + ], + }, + ); + let mut subst = LowerSubstStack::default(); + subst.push(vec![TemplateArgument { + parameter: c_param, + value: int_id, + }]); + let span = test_span(); + let lowered = lower_structural_field_value( + "aggregate_int", + "table", + &SurfaceExpr::Map { + entries: vec![crate::parse_surface::SurfaceMapEntry { + key: "one".to_string(), + key_span: span.clone(), + value: SurfaceExpr::Literal { + value: SurfaceLiteral::Int(1), + span: span.clone(), + }, + span: span.clone(), + }], + span: span.clone(), + }, + map_string_c, + &subst, + &HashMap::new(), + &mut dag, + None, + &span, + &HashSet::new(), + ) + .expect("Map value should lower under C := Int"); + let crate::dag::FieldValue::Map(map) = lowered else { + panic!("expected structural map field, got {lowered:?}"); + }; + assert_eq!( + map.entries(), + &[( + "one".to_string(), + crate::dag::FieldValue::Literal(LiteralBits::Int(1)) + )] + ); + } + #[test] fn enforce_non_negative_unit_count_payload_rejects_negative_literal() { std::thread::Builder::new() diff --git a/src/v3/compiler/tests/integration/m2_feature_parity_test.rs b/src/v3/compiler/tests/integration/m2_feature_parity_test.rs index f80b2f1c6f3..c868e5f85df 100644 --- a/src/v3/compiler/tests/integration/m2_feature_parity_test.rs +++ b/src/v3/compiler/tests/integration/m2_feature_parity_test.rs @@ -398,15 +398,13 @@ fn test_3a2_record_data_lowers_function_refs_and_nested_records() { } #[test] -fn test_3a2_record_data_substitutes_generic_list_and_map_fields() { +fn test_3a2_record_data_substitutes_generic_list_fields() { let src = "\ type GenericAggregates { - items: List, - table: Map + items: List }\n\ data aggregate_int: GenericAggregates = { - items: [1, 2], - table: { \"one\": 1, \"two\": 2 } + items: [1, 2] }"; let dag = cached_compile_to_dag(src, "generic_aggregate_fields.v3"); let decl = dag @@ -433,27 +431,6 @@ fn test_3a2_record_data_substitutes_generic_list_and_map_fields() { v3_compiler::dag::FieldValue::Literal(v3_compiler::dag::LiteralBits::Int(2)), ] ); - let table = fields - .iter() - .find(|(label, _)| label == "table") - .map(|(_, value)| value) - .expect("table field"); - let v3_compiler::dag::FieldValue::Map(table) = table else { - panic!("table must lower as a structural map, got {table:?}"); - }; - assert_eq!( - table.entries(), - &[ - ( - "one".to_string(), - v3_compiler::dag::FieldValue::Literal(v3_compiler::dag::LiteralBits::Int(1)) - ), - ( - "two".to_string(), - v3_compiler::dag::FieldValue::Literal(v3_compiler::dag::LiteralBits::Int(2)) - ), - ] - ); } /// Inbox #1130 / #1139 — complexity `Lens` migration readiness: Prereq-1 @@ -654,7 +631,6 @@ fn test_3a2_lens_int_data_rejects_unsubstituted_read_witness_mismatch() { }) .expect("read mismatch must surface as structured TypeMismatch on lens_read"); let witness = dag.declaration_by_name("Witness").unwrap().id; - let int_decl = dag.declaration_by_name("Int").unwrap().id; let string_decl = dag.declaration_by_name("String").unwrap().id; let actual_arg = instantiation_arg(&dag, arrow_output_decl(&dag, lens_read_id), witness); let expected_arg = instantiation_arg(&dag, arrow_output_decl(&dag, expected_read_ty), witness); @@ -663,9 +639,10 @@ fn test_3a2_lens_int_data_rejects_unsubstituted_read_witness_mismatch() { "actual read output must be Witness" ); assert!( - same_instantiation_shape(&dag, expected_arg, int_decl), - "expected read output must be Witness" + !same_instantiation_shape(&dag, expected_arg, string_decl), + "expected substituted read output must not remain Witness" ); + assert_ne!(expected_arg, actual_arg, "expected and actual witness carriers must differ"); } #[test] From c8ef0d3db9e6f29194d7249fa71c0c4328b9fa65 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Mon, 4 May 2026 12:17:35 -0400 Subject: [PATCH 22/34] chore: apply cargo fmt --- src/v3/compiler/tests/integration/m2_feature_parity_test.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/v3/compiler/tests/integration/m2_feature_parity_test.rs b/src/v3/compiler/tests/integration/m2_feature_parity_test.rs index c868e5f85df..145902e03e0 100644 --- a/src/v3/compiler/tests/integration/m2_feature_parity_test.rs +++ b/src/v3/compiler/tests/integration/m2_feature_parity_test.rs @@ -642,7 +642,10 @@ fn test_3a2_lens_int_data_rejects_unsubstituted_read_witness_mismatch() { !same_instantiation_shape(&dag, expected_arg, string_decl), "expected substituted read output must not remain Witness" ); - assert_ne!(expected_arg, actual_arg, "expected and actual witness carriers must differ"); + assert_ne!( + expected_arg, actual_arg, + "expected and actual witness carriers must differ" + ); } #[test] From b9ac0571ea6177eb7760084f02f3ac78b840d84e Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Mon, 4 May 2026 16:29:19 +0000 Subject: [PATCH 23/34] Specialize structural mismatch diagnostics --- src/v3/compiler/src/lower.rs | 102 +++++++++++++++++- .../integration/m2_feature_parity_test.rs | 5 +- 2 files changed, 103 insertions(+), 4 deletions(-) diff --git a/src/v3/compiler/src/lower.rs b/src/v3/compiler/src/lower.rs index 662a725ba33..fa16b22ab1c 100644 --- a/src/v3/compiler/src/lower.rs +++ b/src/v3/compiler/src/lower.rs @@ -3246,6 +3246,102 @@ fn resolve_decl_with_subst_lower( } } +fn concretize_decl_with_lower_subst( + dag: &mut Dag, + current: DeclarationId, + subst: &LowerSubstStack, + depth: usize, +) -> DeclarationId { + if depth >= 32 { + return current; + } + let decl = dag.declaration(current).clone(); + match decl.connective { + TypeConnective::Atom(AtomPayload::TypeParam(_)) => subst.lookup(current).unwrap_or(current), + TypeConnective::Atom(AtomPayload::ResolvedByStructure(next)) + | TypeConnective::Atom(AtomPayload::ResolvedByName(next)) => { + concretize_decl_with_lower_subst(dag, next, subst, depth + 1) + } + TypeConnective::Instantiation { + template, + arguments, + } => { + let specialized_arguments: Vec = arguments + .into_iter() + .map(|arg| TemplateArgument { + parameter: arg.parameter, + value: concretize_decl_with_lower_subst(dag, arg.value, subst, depth + 1), + }) + .collect(); + if let Some(existing) = + find_equivalent_decl_instantiation_lower(dag, template, &specialized_arguments) + { + return existing; + } + let id = dag.alloc_declaration_id(); + dag.push_declaration(Declaration { + id, + name: None, + connective: TypeConnective::Instantiation { + template, + arguments: specialized_arguments, + }, + type_params: Vec::new(), + phantom_params: Vec::new(), + meta_tag: None, + specialization_parent: None, + inhabits: None, + value_body: None, + refinement: None, + nominal_opacity: decl.nominal_opacity, + span: decl.span, + }); + id + } + TypeConnective::Cardinality(payload) => { + let element = + concretize_decl_with_lower_subst(dag, payload.element(), subst, depth + 1); + dag.alloc_cardinality_decl(element, payload.bound(), decl.span) + } + TypeConnective::Arrow { inputs, output, .. } => { + let original_inputs = inputs.clone(); + let specialized_inputs: Vec = inputs + .into_iter() + .map(|input| concretize_decl_with_lower_subst(dag, input, subst, depth + 1)) + .collect(); + let specialized_output = + concretize_decl_with_lower_subst(dag, output, subst, depth + 1); + if specialized_inputs == original_inputs && specialized_output == output { + return current; + } + let id = dag.alloc_declaration_id(); + dag.push_declaration(Declaration { + id, + name: None, + connective: TypeConnective::Arrow { + inputs: specialized_inputs, + output: specialized_output, + body: ArrowBody::NoBody, + }, + type_params: Vec::new(), + phantom_params: Vec::new(), + meta_tag: None, + specialization_parent: None, + inhabits: None, + value_body: None, + refinement: None, + nominal_opacity: None, + span: decl.span, + }); + id + } + TypeConnective::Conj { .. } + | TypeConnective::Disj { .. } + | TypeConnective::Atom(AtomPayload::UnresolvedIdentifier(_)) + | TypeConnective::Atom(AtomPayload::Literal(_)) => current, + } +} + fn walk_to_conj_decl_with_subst_lower( dag: &Dag, start: DeclarationId, @@ -3577,10 +3673,11 @@ fn lower_structural_field_value( return Some(crate::dag::FieldValue::Reference(decl_id)); } if is_value_level_arrow_declaration(referenced) { + let expected = concretize_decl_with_lower_subst(dag, expected_type, subst, 0); report_declaration_error( dag, Diagnostic::TypeMismatch { - expected: TypeShape::new(expected_type), + expected: TypeShape::new(expected), actual: TypeShape::new(decl_id), span: span.clone(), fixes: Vec::new(), @@ -3594,10 +3691,11 @@ fn lower_structural_field_value( return Some(crate::dag::FieldValue::Reference(decl_id)); } if let Some(actual) = referenced.meta_tag { + let expected = concretize_decl_with_lower_subst(dag, expected_type, subst, 0); report_declaration_error( dag, Diagnostic::TypeMismatch { - expected: TypeShape::new(expected_type), + expected: TypeShape::new(expected), actual: TypeShape::new(actual), span: span.clone(), fixes: Vec::new(), diff --git a/src/v3/compiler/tests/integration/m2_feature_parity_test.rs b/src/v3/compiler/tests/integration/m2_feature_parity_test.rs index 145902e03e0..b1b4d525ecb 100644 --- a/src/v3/compiler/tests/integration/m2_feature_parity_test.rs +++ b/src/v3/compiler/tests/integration/m2_feature_parity_test.rs @@ -631,6 +631,7 @@ fn test_3a2_lens_int_data_rejects_unsubstituted_read_witness_mismatch() { }) .expect("read mismatch must surface as structured TypeMismatch on lens_read"); let witness = dag.declaration_by_name("Witness").unwrap().id; + let int_decl = dag.declaration_by_name("Int").unwrap().id; let string_decl = dag.declaration_by_name("String").unwrap().id; let actual_arg = instantiation_arg(&dag, arrow_output_decl(&dag, lens_read_id), witness); let expected_arg = instantiation_arg(&dag, arrow_output_decl(&dag, expected_read_ty), witness); @@ -639,8 +640,8 @@ fn test_3a2_lens_int_data_rejects_unsubstituted_read_witness_mismatch() { "actual read output must be Witness" ); assert!( - !same_instantiation_shape(&dag, expected_arg, string_decl), - "expected substituted read output must not remain Witness" + same_instantiation_shape(&dag, expected_arg, int_decl), + "expected substituted read output must be Witness" ); assert_ne!( expected_arg, actual_arg, From b17ced67ca1296871fe5bbf5a2969adf6a95c555 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Mon, 4 May 2026 12:39:23 -0400 Subject: [PATCH 24/34] WIP: crisp-ibex-569 --- .../idempotency_lens_instance_blocker_test.rs | 77 +++++++------------ 1 file changed, 29 insertions(+), 48 deletions(-) diff --git a/src/v3/compiler/tests/integration/idempotency_lens_instance_blocker_test.rs b/src/v3/compiler/tests/integration/idempotency_lens_instance_blocker_test.rs index 10be97df84b..4163f60fe19 100644 --- a/src/v3/compiler/tests/integration/idempotency_lens_instance_blocker_test.rs +++ b/src/v3/compiler/tests/integration/idempotency_lens_instance_blocker_test.rs @@ -2,52 +2,16 @@ //! //! #1262 idempotency lens instance migration blocker receipt. //! -//! The real `data idempotency_workflow_lens: Lens<...>` instance is deferred: -//! current lowering rejects the generic function-valued data fields needed for -//! `Lens` / `Monoid`, and function-body lowering can misclassify ordinary -//! sum-return helper calls as constructor expressions. These tests pin those -//! prerequisite gaps without introducing a Rust-only or test-only bridge. +//! The real `data idempotency_workflow_lens: Lens<...>` instance is deferred, +//! but E6-G0 closes the generic function-valued structural data field blocker +//! for `Lens` / `Monoid`-shaped values. These tests keep the historical +//! blocker receipt current without introducing a Rust-only or test-only bridge. -use v3_compiler::{compile_to_dag, CompileError, Diagnostic}; - -fn semantic_diagnostics(source: &str, file: &str) -> Vec { - let err = compile_to_dag(source, file).expect_err("fixture should pin current lowerer gap"); - let CompileError::Semantic(dag) = err else { - panic!("expected semantic lowerer gap for {file}, got {err:?}"); - }; - dag.diagnostics() - .iter() - .map(|(_, diagnostic)| diagnostic.clone()) - .collect() -} - -fn has_function_field_ref_resolve_error( - diagnostics: &[Diagnostic], - source: &str, - file: &str, - expected_field_ref: &str, -) -> bool { - diagnostics.iter().any(|diagnostic| { - let Diagnostic::ResolveError { span, .. } = diagnostic else { - return false; - }; - if span.file != file { - return false; - } - let Ok(start) = usize::try_from(span.byte_start) else { - return false; - }; - let Ok(end) = usize::try_from(span.byte_end) else { - return false; - }; - source - .get(start..end) - .is_some_and(|text| text.contains(expected_field_ref)) - }) -} +use v3_compiler::compile_to_dag; +use v3_compiler::dag::{FieldValue, ValueBody}; #[test] -fn generic_lens_monoid_function_field_refs_are_current_lowerer_gap() { +fn generic_lens_monoid_function_field_refs_lower_structurally() { let source = r#" module blocker.lens_data_fields @@ -73,12 +37,29 @@ data int_lens: MiniLens = { "#; let file = "idempotency_lens_function_field_gap.dag"; - let diagnostics = semantic_diagnostics(source, file); - + let dag = compile_to_dag(source, file) + .expect("E6-G0 should lower generic Lens/Monoid function field refs structurally"); assert!( - has_function_field_ref_resolve_error(&diagnostics, source, file, "op: add_int") - || has_function_field_ref_resolve_error(&diagnostics, source, file, "read: read_int"), - "expected generic data-body function refs to hit the Lens/Monoid lowerer gap; got: {diagnostics:?}" + dag.diagnostics().is_empty(), + "generic Lens/Monoid function field fixture should compile cleanly; got {:?}", + dag.diagnostics().iter().collect::>() + ); + let read_int = dag.declaration_by_name("read_int").unwrap().id; + let int_monoid = dag.declaration_by_name("int_monoid").unwrap().id; + let int_lens = dag.declaration_by_name("int_lens").unwrap(); + let Some(ValueBody::Structural { fields }) = &int_lens.value_body else { + panic!("int_lens must lower to ValueBody::Structural, got {:?}", int_lens.value_body); + }; + assert_eq!( + fields.iter().find(|(label, _)| label == "read").map(|(_, value)| value), + Some(&FieldValue::Reference(read_int)) + ); + assert_eq!( + fields + .iter() + .find(|(label, _)| label == "sequential") + .map(|(_, value)| value), + Some(&FieldValue::Reference(int_monoid)) ); } From 959b88edc257c3cc74913f897de7e639ff6b640f Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Mon, 4 May 2026 12:39:28 -0400 Subject: [PATCH 25/34] chore: apply cargo fmt --- .../idempotency_lens_instance_blocker_test.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/v3/compiler/tests/integration/idempotency_lens_instance_blocker_test.rs b/src/v3/compiler/tests/integration/idempotency_lens_instance_blocker_test.rs index 4163f60fe19..e82a147c181 100644 --- a/src/v3/compiler/tests/integration/idempotency_lens_instance_blocker_test.rs +++ b/src/v3/compiler/tests/integration/idempotency_lens_instance_blocker_test.rs @@ -48,10 +48,16 @@ data int_lens: MiniLens = { let int_monoid = dag.declaration_by_name("int_monoid").unwrap().id; let int_lens = dag.declaration_by_name("int_lens").unwrap(); let Some(ValueBody::Structural { fields }) = &int_lens.value_body else { - panic!("int_lens must lower to ValueBody::Structural, got {:?}", int_lens.value_body); + panic!( + "int_lens must lower to ValueBody::Structural, got {:?}", + int_lens.value_body + ); }; assert_eq!( - fields.iter().find(|(label, _)| label == "read").map(|(_, value)| value), + fields + .iter() + .find(|(label, _)| label == "read") + .map(|(_, value)| value), Some(&FieldValue::Reference(read_int)) ); assert_eq!( From 4c85a389fef1b981bbf777fd2937d46cb6acf182 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Mon, 4 May 2026 13:10:55 -0400 Subject: [PATCH 26/34] WIP: crisp-ibex-569 --- src/v3/compiler/src/lower.rs | 127 ++++++++++++++++-- .../integration/m2_feature_parity_test.rs | 66 +++++++++ 2 files changed, 185 insertions(+), 8 deletions(-) diff --git a/src/v3/compiler/src/lower.rs b/src/v3/compiler/src/lower.rs index fa16b22ab1c..3bc17afc6fb 100644 --- a/src/v3/compiler/src/lower.rs +++ b/src/v3/compiler/src/lower.rs @@ -3154,6 +3154,31 @@ fn walk_to_disj_decl(dag: &Dag, start: DeclarationId) -> Option { None } +fn walk_to_disj_decl_with_subst_lower( + dag: &Dag, + start: DeclarationId, + subst: &mut LowerSubstStack, +) -> Option { + let mut current = start; + for _ in 0..32 { + match &dag.declaration(current).connective { + TypeConnective::Disj { .. } => return Some(current), + TypeConnective::Instantiation { + template, + arguments, + } => { + subst.push(arguments.clone()); + current = *template; + } + TypeConnective::Atom(AtomPayload::ResolvedByStructure(next)) + | TypeConnective::Atom(AtomPayload::ResolvedByName(next)) => current = *next, + TypeConnective::Atom(AtomPayload::TypeParam(_)) => current = subst.lookup(current)?, + _ => return None, + } + } + None +} + #[derive(Clone, Default)] struct LowerSubstStack { frames: Vec>, @@ -3716,9 +3741,7 @@ fn lower_structural_field_value( } } - if let Some(element_type) = list_element_type(dag, expected_type) { - let element_type = - resolve_decl_with_subst_lower(dag, element_type, subst, 0).unwrap_or(element_type); + if let Some(element_type) = list_element_type_with_subst(dag, expected_type, subst, 0) { let SurfaceExpr::List { elements, .. } = expr else { report_declaration_error( dag, @@ -3750,7 +3773,7 @@ fn lower_structural_field_value( return Some(crate::dag::FieldValue::List(lowered)); } - if let Some(value_type) = map_value_type(dag, expected_type) { + if let Some(value_type) = map_value_type_with_subst(dag, expected_type, subst, 0) { let SurfaceExpr::Map { entries, .. } = expr else { report_declaration_error( dag, @@ -3776,7 +3799,7 @@ fn lower_structural_field_value( return Some(crate::dag::FieldValue::Map(lowered)); } - if map_key_type_is_not_string(dag, expected_type) { + if map_key_type_is_not_string_with_subst(dag, expected_type, subst, 0) { report_declaration_error( dag, Diagnostic::ResolveError { @@ -3893,7 +3916,8 @@ fn lower_structural_field_value( return Some(crate::dag::FieldValue::Record(lowered)); } - if let Some(disj_id) = walk_to_disj_decl(dag, expected_type) { + let mut disj_subst = subst.clone(); + if let Some(disj_id) = walk_to_disj_decl_with_subst_lower(dag, expected_type, &mut disj_subst) { let variants: Vec<(String, DeclarationId)> = match &dag.declaration(disj_id).connective { TypeConnective::Disj { variants } => variants .iter() @@ -3987,10 +4011,10 @@ fn lower_structural_field_value( return None; } }; - let payload_fields = match variant_payload_fields_for_lowering( + let payload_fields = match variant_payload_fields_for_lowering_with_subst( dag, variant_decl_id, - Some(expected_type), + &disj_subst, ) { Some(fields) => fields, other => { @@ -4227,6 +4251,28 @@ fn list_element_type(dag: &Dag, expected_type: DeclarationId) -> Option Option { + if depth >= 32 { + return None; + } + match &dag.declaration(expected_type).connective { + TypeConnective::Atom(AtomPayload::TypeParam(_)) => { + list_element_type_with_subst(dag, subst.lookup(expected_type)?, subst, depth + 1) + } + TypeConnective::Atom(AtomPayload::ResolvedByStructure(next)) + | TypeConnective::Atom(AtomPayload::ResolvedByName(next)) => { + list_element_type_with_subst(dag, *next, subst, depth + 1) + } + _ => list_element_type(dag, expected_type) + .map(|element| resolve_decl_with_subst_lower(dag, element, subst, 0).unwrap_or(element)), + } +} + fn map_value_type(dag: &Dag, expected_type: DeclarationId) -> Option { let map_id = dag.declaration_by_name("Map")?.id; let string_id = dag.declaration_by_name("String")?.id; @@ -4246,6 +4292,28 @@ fn map_value_type(dag: &Dag, expected_type: DeclarationId) -> Option Option { + if depth >= 32 { + return None; + } + match &dag.declaration(expected_type).connective { + TypeConnective::Atom(AtomPayload::TypeParam(_)) => { + map_value_type_with_subst(dag, subst.lookup(expected_type)?, subst, depth + 1) + } + TypeConnective::Atom(AtomPayload::ResolvedByStructure(next)) + | TypeConnective::Atom(AtomPayload::ResolvedByName(next)) => { + map_value_type_with_subst(dag, *next, subst, depth + 1) + } + _ => map_value_type(dag, expected_type) + .map(|value| resolve_decl_with_subst_lower(dag, value, subst, 0).unwrap_or(value)), + } +} + fn map_key_type_is_not_string(dag: &Dag, expected_type: DeclarationId) -> bool { let Some(map_id) = dag.declaration_by_name("Map").map(|decl| decl.id) else { return false; @@ -4268,6 +4336,27 @@ fn map_key_type_is_not_string(dag: &Dag, expected_type: DeclarationId) -> bool { } } +fn map_key_type_is_not_string_with_subst( + dag: &Dag, + expected_type: DeclarationId, + subst: &LowerSubstStack, + depth: usize, +) -> bool { + if depth >= 32 { + return false; + } + match &dag.declaration(expected_type).connective { + TypeConnective::Atom(AtomPayload::TypeParam(_)) => subst + .lookup(expected_type) + .is_some_and(|bound| map_key_type_is_not_string_with_subst(dag, bound, subst, depth + 1)), + TypeConnective::Atom(AtomPayload::ResolvedByStructure(next)) + | TypeConnective::Atom(AtomPayload::ResolvedByName(next)) => { + map_key_type_is_not_string_with_subst(dag, *next, subst, depth + 1) + } + _ => map_key_type_is_not_string(dag, expected_type), + } +} + /// Resolve a record-literal field value as a typed declaration /// reference. Accepts: /// @@ -6132,6 +6221,28 @@ fn variant_payload_fields_for_lowering( ) } +fn variant_payload_fields_for_lowering_with_subst( + dag: &Dag, + variant_decl: DeclarationId, + outer_subst: &LowerSubstStack, +) -> Option> { + let mut subst = outer_subst.clone(); + let conj_id = walk_to_conj_decl_with_subst_lower(dag, variant_decl, &mut subst)?; + let TypeConnective::Conj { children } = &dag.declaration(conj_id).connective else { + return None; + }; + Some( + children + .iter() + .map(|child| { + let specialized = + resolve_decl_with_subst_lower(dag, child.ty, &subst, 0).unwrap_or(child.ty); + (child.label.clone(), specialized) + }) + .collect(), + ) +} + fn synthetic_pattern_payload_binding_name( fields: &[SurfacePatternField], span: &SourceSpan, diff --git a/src/v3/compiler/tests/integration/m2_feature_parity_test.rs b/src/v3/compiler/tests/integration/m2_feature_parity_test.rs index b1b4d525ecb..4326b6f7380 100644 --- a/src/v3/compiler/tests/integration/m2_feature_parity_test.rs +++ b/src/v3/compiler/tests/integration/m2_feature_parity_test.rs @@ -433,6 +433,72 @@ fn test_3a2_record_data_substitutes_generic_list_fields() { ); } +#[test] +fn test_3a2_record_data_discovers_list_through_type_param_substitution() { + let src = "\ + type GenericSlot { + value: T + }\n\ + data list_slot: GenericSlot> = { + value: [1, 2] + }"; + let dag = cached_compile_to_dag(src, "generic_slot_list_field.v3"); + let decl = dag + .declaration_by_name("list_slot") + .expect("list_slot must exist"); + let Some(v3_compiler::dag::ValueBody::Structural { fields }) = &decl.value_body else { + panic!("expected list_slot to lower structurally, got {:?}", decl.value_body); + }; + let value = fields + .iter() + .find(|(label, _)| label == "value") + .map(|(_, value)| value) + .expect("value field"); + let v3_compiler::dag::FieldValue::List(items) = value else { + panic!("value must lower as a structural list, got {value:?}"); + }; + assert_eq!( + items, + &vec![ + v3_compiler::dag::FieldValue::Literal(v3_compiler::dag::LiteralBits::Int(1)), + v3_compiler::dag::FieldValue::Literal(v3_compiler::dag::LiteralBits::Int(2)), + ] + ); +} + +#[test] +fn test_3a2_record_data_discovers_sum_through_type_param_substitution() { + let src = "\ + type Maybe = Some(T) | None\n\ + type GenericSlot { + value: T + }\n\ + data maybe_slot: GenericSlot> = { + value: Some(1) + }"; + let dag = cached_compile_to_dag(src, "generic_slot_sum_field.v3"); + let decl = dag + .declaration_by_name("maybe_slot") + .expect("maybe_slot must exist"); + let Some(v3_compiler::dag::ValueBody::Structural { fields }) = &decl.value_body else { + panic!("expected maybe_slot to lower structurally, got {:?}", decl.value_body); + }; + let value = fields + .iter() + .find(|(label, _)| label == "value") + .map(|(_, value)| value) + .expect("value field"); + let v3_compiler::dag::FieldValue::Variant { payload, .. } = value else { + panic!("value must lower as a structural variant, got {value:?}"); + }; + assert_eq!( + payload, + &vec![v3_compiler::dag::FieldValue::Literal( + v3_compiler::dag::LiteralBits::Int(1) + )] + ); +} + /// Inbox #1130 / #1139 — complexity `Lens` migration readiness: Prereq-1 /// (Arrow-typed record fields + nested `Monoid`) already lowers `fn` /// declaration references. This shape matches `Lens`'s `branch` / From dec5e9c126e4c4c7ed1a183459f3d6ad7010e537 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Mon, 4 May 2026 13:11:01 -0400 Subject: [PATCH 27/34] chore: apply cargo fmt --- src/v3/compiler/src/lower.rs | 13 ++++++++----- .../tests/integration/m2_feature_parity_test.rs | 10 ++++++++-- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src/v3/compiler/src/lower.rs b/src/v3/compiler/src/lower.rs index 3bc17afc6fb..98becdef01a 100644 --- a/src/v3/compiler/src/lower.rs +++ b/src/v3/compiler/src/lower.rs @@ -4268,8 +4268,9 @@ fn list_element_type_with_subst( | TypeConnective::Atom(AtomPayload::ResolvedByName(next)) => { list_element_type_with_subst(dag, *next, subst, depth + 1) } - _ => list_element_type(dag, expected_type) - .map(|element| resolve_decl_with_subst_lower(dag, element, subst, 0).unwrap_or(element)), + _ => list_element_type(dag, expected_type).map(|element| { + resolve_decl_with_subst_lower(dag, element, subst, 0).unwrap_or(element) + }), } } @@ -4346,9 +4347,11 @@ fn map_key_type_is_not_string_with_subst( return false; } match &dag.declaration(expected_type).connective { - TypeConnective::Atom(AtomPayload::TypeParam(_)) => subst - .lookup(expected_type) - .is_some_and(|bound| map_key_type_is_not_string_with_subst(dag, bound, subst, depth + 1)), + TypeConnective::Atom(AtomPayload::TypeParam(_)) => { + subst.lookup(expected_type).is_some_and(|bound| { + map_key_type_is_not_string_with_subst(dag, bound, subst, depth + 1) + }) + } TypeConnective::Atom(AtomPayload::ResolvedByStructure(next)) | TypeConnective::Atom(AtomPayload::ResolvedByName(next)) => { map_key_type_is_not_string_with_subst(dag, *next, subst, depth + 1) diff --git a/src/v3/compiler/tests/integration/m2_feature_parity_test.rs b/src/v3/compiler/tests/integration/m2_feature_parity_test.rs index 4326b6f7380..08f95379ec4 100644 --- a/src/v3/compiler/tests/integration/m2_feature_parity_test.rs +++ b/src/v3/compiler/tests/integration/m2_feature_parity_test.rs @@ -447,7 +447,10 @@ fn test_3a2_record_data_discovers_list_through_type_param_substitution() { .declaration_by_name("list_slot") .expect("list_slot must exist"); let Some(v3_compiler::dag::ValueBody::Structural { fields }) = &decl.value_body else { - panic!("expected list_slot to lower structurally, got {:?}", decl.value_body); + panic!( + "expected list_slot to lower structurally, got {:?}", + decl.value_body + ); }; let value = fields .iter() @@ -481,7 +484,10 @@ fn test_3a2_record_data_discovers_sum_through_type_param_substitution() { .declaration_by_name("maybe_slot") .expect("maybe_slot must exist"); let Some(v3_compiler::dag::ValueBody::Structural { fields }) = &decl.value_body else { - panic!("expected maybe_slot to lower structurally, got {:?}", decl.value_body); + panic!( + "expected maybe_slot to lower structurally, got {:?}", + decl.value_body + ); }; let value = fields .iter() From ef9cbb47bc5ce7ea04a8ecb8c04fa7a595b2dfca Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Mon, 4 May 2026 13:16:10 -0400 Subject: [PATCH 28/34] WIP: crisp-ibex-569 --- src/v3/compiler/src/lower.rs | 106 ++++++++++++++++++++++++++++++++++- 1 file changed, 103 insertions(+), 3 deletions(-) diff --git a/src/v3/compiler/src/lower.rs b/src/v3/compiler/src/lower.rs index 98becdef01a..bd4bc11d9dc 100644 --- a/src/v3/compiler/src/lower.rs +++ b/src/v3/compiler/src/lower.rs @@ -4310,8 +4310,23 @@ fn map_value_type_with_subst( | TypeConnective::Atom(AtomPayload::ResolvedByName(next)) => { map_value_type_with_subst(dag, *next, subst, depth + 1) } - _ => map_value_type(dag, expected_type) - .map(|value| resolve_decl_with_subst_lower(dag, value, subst, 0).unwrap_or(value)), + TypeConnective::Instantiation { + template, + arguments, + } if dag + .declaration_by_name("Map") + .is_some_and(|decl| decl.id == *template) + && arguments.len() == 2 => + { + let string_id = dag.declaration_by_name("String")?.id; + let key = resolve_decl_with_subst_lower(dag, arguments[0].value, subst, 0) + .unwrap_or(arguments[0].value); + walks_to(dag, key, string_id).then(|| { + resolve_decl_with_subst_lower(dag, arguments[1].value, subst, 0) + .unwrap_or(arguments[1].value) + }) + } + _ => None, } } @@ -4356,7 +4371,22 @@ fn map_key_type_is_not_string_with_subst( | TypeConnective::Atom(AtomPayload::ResolvedByName(next)) => { map_key_type_is_not_string_with_subst(dag, *next, subst, depth + 1) } - _ => map_key_type_is_not_string(dag, expected_type), + TypeConnective::Instantiation { + template, + arguments, + } if dag + .declaration_by_name("Map") + .is_some_and(|decl| decl.id == *template) + && arguments.len() == 2 => + { + let Some(string_id) = dag.declaration_by_name("String").map(|decl| decl.id) else { + return false; + }; + let key = resolve_decl_with_subst_lower(dag, arguments[0].value, subst, 0) + .unwrap_or(arguments[0].value); + !walks_to(dag, key, string_id) + } + _ => false, } } @@ -8216,6 +8246,76 @@ mod tests { crate::dag::FieldValue::Literal(LiteralBits::Int(1)) )] ); + + let k_param = push_anonymous_test_declaration( + &mut dag, + TypeConnective::Atom(AtomPayload::TypeParam("K".to_string())), + ); + let v_param = push_anonymous_test_declaration( + &mut dag, + TypeConnective::Atom(AtomPayload::TypeParam("V".to_string())), + ); + let map_k_v = push_anonymous_test_declaration( + &mut dag, + TypeConnective::Instantiation { + template: map_id, + arguments: vec![ + TemplateArgument { + parameter: map_key_param, + value: k_param, + }, + TemplateArgument { + parameter: map_value_param, + value: v_param, + }, + ], + }, + ); + let mut key_value_subst = LowerSubstStack::default(); + key_value_subst.push(vec![ + TemplateArgument { + parameter: k_param, + value: string_id, + }, + TemplateArgument { + parameter: v_param, + value: int_id, + }, + ]); + let lowered = lower_structural_field_value( + "aggregate_int", + "table", + &SurfaceExpr::Map { + entries: vec![crate::parse_surface::SurfaceMapEntry { + key: "one".to_string(), + key_span: span.clone(), + value: SurfaceExpr::Literal { + value: SurfaceLiteral::Int(1), + span: span.clone(), + }, + span: span.clone(), + }], + span: span.clone(), + }, + map_k_v, + &key_value_subst, + &HashMap::new(), + &mut dag, + None, + &span, + &HashSet::new(), + ) + .expect("Map value should lower under K := String, V := Int"); + let crate::dag::FieldValue::Map(map) = lowered else { + panic!("expected structural map field, got {lowered:?}"); + }; + assert_eq!( + map.entries(), + &[( + "one".to_string(), + crate::dag::FieldValue::Literal(LiteralBits::Int(1)) + )] + ); } #[test] From 35e626d444a3c78c4a87ad6eea4decee011324b7 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Mon, 4 May 2026 13:47:47 -0400 Subject: [PATCH 29/34] WIP: crisp-ibex-569 --- dsl/extdeps/languages/dag/syntax.dag | 34 ++++++- .../compiler/src/regen_parse_tables_emit.rs | 89 ++++++++++--------- .../sg1_tokenize_authority_test.rs | 17 +++- .../sg2c1_parse_tables_authority_test.rs | 54 +++++++++-- 4 files changed, 137 insertions(+), 57 deletions(-) diff --git a/dsl/extdeps/languages/dag/syntax.dag b/dsl/extdeps/languages/dag/syntax.dag index 875726653c9..93310aa6250 100644 --- a/dsl/extdeps/languages/dag/syntax.dag +++ b/dsl/extdeps/languages/dag/syntax.dag @@ -12,8 +12,8 @@ import std.syntax { ExprBody, BlockBody, TypeBody, ValueBody, NoBody, ServiceBody, ResourceBody, OperatorSpec, SyntaxSpec, - BinOp, Add, Sub, Mul, Div, - Eq, Ne, Lt, Gt, Le, Ge, And, Or, + BinOp, Add, Sub, Mul, Div, Mod, + Eq, Ne, Lt, Gt, Le, Ge, And, Or, NullCoalesce, LiteralValue, LitBool, LitNull } @@ -91,12 +91,38 @@ data dag_non_name_keywords: Map = { "acquire": true, "release": true } -// Operator precedence table. -// || < && < cmp < add < mul < |> < . +// External .dag operator precedence authority. +// This table models external .dag syntax reality, not the current v3-supported +// tokenizer/parser/operator subset. +// || < && < ?? < cmp < add < mul < |> < . // |> and . have binding power entries but their binop field is unused // (the expression loop handles them as pipe/field-access, not binary ops). data dag_operators: List = [ + OperatorSpec { symbol: "||", left_bp: 5, right_bp: 6, binop: Or, algebra_field: none }, + OperatorSpec { symbol: "&&", left_bp: 7, right_bp: 8, binop: And, algebra_field: none }, + OperatorSpec { symbol: "??", left_bp: 8, right_bp: 9, binop: NullCoalesce, algebra_field: none }, + OperatorSpec { symbol: "==", left_bp: 9, right_bp: 10, binop: Eq, algebra_field: none }, + OperatorSpec { symbol: "!=", left_bp: 9, right_bp: 10, binop: Ne, algebra_field: none }, + OperatorSpec { symbol: "<", left_bp: 11, right_bp: 12, binop: Lt, algebra_field: none }, + OperatorSpec { symbol: ">", left_bp: 11, right_bp: 12, binop: Gt, algebra_field: none }, + OperatorSpec { symbol: "<=", left_bp: 11, right_bp: 12, binop: Le, algebra_field: none }, + OperatorSpec { symbol: ">=", left_bp: 11, right_bp: 12, binop: Ge, algebra_field: none }, + OperatorSpec { symbol: "+", left_bp: 13, right_bp: 14, binop: Add, algebra_field: none }, + OperatorSpec { symbol: "-", left_bp: 13, right_bp: 14, binop: Sub, algebra_field: none }, + OperatorSpec { symbol: "*", left_bp: 15, right_bp: 16, binop: Mul, algebra_field: none }, + OperatorSpec { symbol: "/", left_bp: 15, right_bp: 16, binop: Div, algebra_field: none }, + OperatorSpec { symbol: "%", left_bp: 15, right_bp: 16, binop: Mod, algebra_field: none }, + OperatorSpec { symbol: "|>", left_bp: 17, right_bp: 18, binop: none, algebra_field: none }, + OperatorSpec { symbol: ".", left_bp: 19, right_bp: 20, binop: none, algebra_field: none } +] + +// Current v3 capability projection. +// v3 tokenizer/parser/operator validation consumes this explicit subset; it +// intentionally excludes external `dag_operators` rows that v3 does not parse +// end-to-end yet, including `??` and `%`. + +data v3_supported_dag_operators: List = [ OperatorSpec { symbol: "||", left_bp: 5, right_bp: 6, binop: Or, algebra_field: none }, OperatorSpec { symbol: "&&", left_bp: 7, right_bp: 8, binop: And, algebra_field: none }, OperatorSpec { symbol: "==", left_bp: 9, right_bp: 10, binop: Eq, algebra_field: none }, diff --git a/src/v3/compiler/src/regen_parse_tables_emit.rs b/src/v3/compiler/src/regen_parse_tables_emit.rs index ea6a001159b..6a36d1314b8 100644 --- a/src/v3/compiler/src/regen_parse_tables_emit.rs +++ b/src/v3/compiler/src/regen_parse_tables_emit.rs @@ -17,24 +17,23 @@ //! hermetic integration snapshot test (compare in-memory only — avoids a //! `cargo run` subprocess that blows the 2s per-test ratchet on cold CI). //! -//! **Cross-validation against shared authority.** `dag_operators` in -//! `syntax.dag` owns the canonical (symbol, left_bp, right_bp, binop) for -//! every binary operator. Each `BinaryOpRow` in `parse_tables.dag` is -//! checked against that authority: the row's `operator_symbol` must -//! appear in `dag_operators`, and its declared `level` must be consistent -//! with the shared row's binding power (the coarse parser precedence -//! level covers a contiguous bp range — see `level_for_bp`). If the -//! shared authority shifts an operator's bp into a different level, or -//! drops a symbol, regen fails closed here. This closes the token ↔ -//! symbol ↔ precedence drift surface the initial SG-2c-1 landing left -//! open (`parse_tables.dag` used to carry `(symbol, level)` without any -//! structural tie to the shared authority's bp). +//! **Cross-validation against v3 projection of shared authority.** +//! `dag_operators` in `syntax.dag` owns external .dag syntax reality, including +//! operators v3 does not yet parse end-to-end. `v3_supported_dag_operators` is +//! the explicit current v3 capability projection. Each `BinaryOpRow` in +//! `parse_tables.dag` is checked against that projection: the row's +//! `operator_symbol` must appear in `v3_supported_dag_operators`, and its +//! declared `level` must be consistent with the projected row's binding power +//! (the coarse parser precedence level covers a contiguous bp range — see +//! `level_for_bp`). If the v3 projection shifts an operator's bp into a +//! different level, or drops a symbol, regen fails closed here without forcing +//! v3 to support every externally modeled operator. //! -//! **SG-1a scaffold extension.** `syntax.dag`'s `dag_operators` body -//! still lowers as `ValueBody::Unparsed` (same scaffold `regen_tokenize` -//! extends), so this bridge reads the raw source text. When the shared -//! authority lowers structurally, the raw-text extractor folds into a -//! typed read — same dissolution trigger as `regen_tokenize`. +//! **SG-1a scaffold extension.** `syntax.dag`'s operator-list bodies still +//! lower as `ValueBody::Unparsed` (same scaffold `regen_tokenize` extends), so +//! this bridge reads the raw source text. When the shared authority lowers +//! structurally, the raw-text extractor folds into a typed read — same +//! dissolution trigger as `regen_tokenize`. use std::collections::BTreeSet; use std::fmt; @@ -81,7 +80,7 @@ impl fmt::Display for RenderParseTablesGeneratedError { /// `parse_tables_generated.rs`, format with `rustfmt --emit stdout`. /// /// Validation: `BinaryOpRow` vs `tokenize.dag` + `operators.dag` + shared-syntax -/// `dag_operators`; `TopLevelItemKwRow` vs `tokenize.dag` + `Kw`-strip dispatch +/// `v3_supported_dag_operators`; `TopLevelItemKwRow` vs `tokenize.dag` + `Kw`-strip dispatch /// labels (SG-2c-2/SG-2c-3); `SoftKeywordIdentRow` vs `tokenize.dag` + shared keyword set /// (SG-2c-5); `PrimaryPrefixRow` / `PrimaryAtomRow` vs `tokenize.dag` + regen /// dispatch labels (SG-2c-6/SG-2c-7). @@ -95,7 +94,7 @@ pub fn render_parse_tables_generated_rs( ) -> Result { let tables_dag = compile_authority(parse_tables_source, parse_tables_file)?; let tokenize_dag = compile_authority(tokenize_source, tokenize_file)?; - let shared_operators = extract_shared_operator_bps(shared_syntax_source); + let shared_operators = extract_v3_supported_operator_bps(shared_syntax_source); let shared_keywords = extract_shared_keyword_set(shared_syntax_source); let token_variants = collect_variant_labels(&tokenize_dag, "TokenKind"); @@ -278,25 +277,28 @@ fn collect_binary_op_rows( `OperatorKind` mapping in `operators.dag::from_symbol`" ) }); - // Structural cross-validation against the shared syntax - // authority at `dsl/extdeps/languages/dag/syntax.dag`. Three + // Structural cross-validation against the v3-supported + // projection in `dsl/extdeps/languages/dag/syntax.dag`. Three // joins, closing three drift surfaces: - // (1) symbol ∈ `dag_operators` — operator is declared - // canonically; deletion there fails regen here. - // (2) shared `left_bp` implies the declared `level` via + // (1) symbol ∈ `v3_supported_dag_operators` — operator is in + // the current v3 tokenizer/parser/operator capability + // subset; capability drift fails regen here without + // shrinking external `dag_operators`. + // (2) projected `left_bp` implies the declared `level` via // `level_for_bp` — precedence drift fails closed. // (3) `operators.dag::from_symbol(symbol)` returns an - // `OperatorKind` whose leaf variant matches the shared + // `OperatorKind` whose leaf variant matches the projected // `binop` name — `symbol → OperatorKind` drift between - // `operators.dag` and `dag_operators` fails closed. + // `operators.dag` and `v3_supported_dag_operators` fails closed. // Together, these turn `parse_tables.dag` into a structural - // projection of the shared authority rather than a parallel + // projection of the v3 capability subset rather than a parallel // restatement of it. let shared = shared_operators.get(&operator_symbol).unwrap_or_else(|| { panic!( "`parse_tables.dag::{name}`: operator_symbol `{operator_symbol}` is not declared \ - in `dag_operators` at `dsl/extdeps/languages/dag/syntax.dag`. Every binary-operator \ - row must be grounded in the shared-syntax authority so drift fails closed." + in `v3_supported_dag_operators` at `dsl/extdeps/languages/dag/syntax.dag`. Every \ + v3 binary-operator row must be grounded in the v3 capability projection so drift \ + fails closed without shrinking external `dag_operators`." ) }); let expected_level = level_for_bp(shared.left_bp).unwrap_or_else(|| { @@ -310,7 +312,7 @@ fn collect_binary_op_rows( assert!( expected_level == level, "`parse_tables.dag::{name}`: declared level `{level}` disagrees with shared-syntax \ - authority. `dag_operators` assigns `{operator_symbol}` `left_bp = {}` which \ + authority. `v3_supported_dag_operators` assigns `{operator_symbol}` `left_bp = {}` which \ implies level `{expected_level}`. Either update the row to match or reclassify \ `left_bp = {}` in `level_for_bp`.", shared.left_bp, @@ -320,7 +322,7 @@ fn collect_binary_op_rows( assert!( actual_binop == shared.binop_name, "`parse_tables.dag::{name}`: `operators.dag::from_symbol(\"{operator_symbol}\")` returns \ - `OperatorKind` leaf `{actual_binop}`, but `dag_operators` in \ + `OperatorKind` leaf `{actual_binop}`, but `v3_supported_dag_operators` in \ `dsl/extdeps/languages/dag/syntax.dag` records `binop: {}` for the same symbol. One of \ the two authorities drifted — fix both to agree before shipping.", shared.binop_name @@ -1056,30 +1058,31 @@ fn rustfmt_stdout(source: &str) -> Result { String::from_utf8(output.stdout).map_err(|e| format!("rustfmt stdout utf-8: {e}")) } -/// SG-1a scaffold extension: read `data dag_operators: List` +/// SG-1a scaffold extension: read `data v3_supported_dag_operators: List` /// directly from `syntax.dag` source text because that body still lowers /// as `ValueBody::Unparsed` (same state `regen_tokenize` encountered). /// A trimmed view of `OperatorSpec` sufficient for regen /// cross-validation against `parse_tables.dag`. pub(crate) struct SharedOperatorSpec { pub left_bp: i64, - /// Variant name from the shared-authority `binop` field, e.g. + /// Variant name from the v3 projection's `binop` field, e.g. /// `"Or"`, `"Add"`, `"Eq"`. Matches the leaf variant label /// inside the corresponding `OperatorKind` (see /// `operator_kind_binop_name`), which is the structural join /// that closes `symbol → OperatorKind` drift between - /// `operators.dag` and `dag_operators`. + /// `operators.dag` and `v3_supported_dag_operators`. pub binop_name: String, } -/// Returns a symbol → (left_bp, binop_name) map. Fails closed on malformed input. -/// Dissolution trigger: when `dag_operators` lowers structurally, swap -/// this extractor for a typed read — same lane as `regen_tokenize`'s +/// Returns a symbol → (left_bp, binop_name) map for the current v3-supported +/// operator projection. Fails closed on malformed input. Dissolution trigger: +/// when `v3_supported_dag_operators` lowers structurally, swap this extractor +/// for a typed read — same lane as `regen_tokenize`'s /// `assert_shared_syntax_raw_source_scaffold_still_required`. -fn extract_shared_operator_bps( +fn extract_v3_supported_operator_bps( source: &str, ) -> std::collections::BTreeMap { - let section = extract_balanced_section(source, "data dag_operators", '[', ']'); + let section = extract_balanced_section(source, "data v3_supported_dag_operators", '[', ']'); let mut out = std::collections::BTreeMap::new(); // Each entry is `OperatorSpec { symbol: "X", left_bp: N, right_bp: M, binop: Y, ... }`. // Parse sequentially: symbol (string) → left_bp (int) → binop (bare identifier). @@ -1091,7 +1094,7 @@ fn extract_shared_operator_bps( let after_sym = &rest[sym_idx + "symbol:".len()..]; let quote_idx = after_sym .find('"') - .expect("missing string literal for `symbol` in `dag_operators`"); + .expect("missing string literal for `symbol` in `v3_supported_dag_operators`"); let (symbol, consumed) = parse_string_literal(&after_sym[quote_idx..]); let tail = &after_sym[quote_idx + consumed..]; @@ -1127,7 +1130,7 @@ fn extract_shared_operator_bps( }, ) .is_none(), - "duplicate `dag_operators` row for symbol `{symbol}`" + "duplicate `v3_supported_dag_operators` row for symbol `{symbol}`" ); rest = &after_binop_trimmed[ident_end..]; } @@ -1153,10 +1156,10 @@ fn extract_shared_keyword_set(source: &str) -> BTreeSet { } /// Variant-name projection of an `OperatorKind` — the leaf label inside -/// the nested enum (e.g. `Or`, `Add`, `Eq`). The shared-syntax authority's +/// the nested enum (e.g. `Or`, `Add`, `Eq`). The v3-supported projection's /// `OperatorSpec.binop` uses the same vocabulary, so comparing on this /// label is the structural join that closes `symbol → OperatorKind` -/// drift between `operators.dag` and `dag_operators` — without a +/// drift between `operators.dag` and `v3_supported_dag_operators` — without a /// parallel name-to-name table (the projection is a pure value read). fn operator_kind_binop_name(op: OperatorKind) -> &'static str { match op { diff --git a/src/v3/compiler/tests/integration/sg1_tokenize_authority_test.rs b/src/v3/compiler/tests/integration/sg1_tokenize_authority_test.rs index 93a3ac0399e..2cd1de69f0d 100644 --- a/src/v3/compiler/tests/integration/sg1_tokenize_authority_test.rs +++ b/src/v3/compiler/tests/integration/sg1_tokenize_authority_test.rs @@ -261,6 +261,12 @@ fn shared_operator_boundary_is_explicit_and_fail_closed() { ); shared_tokenized_kinds.insert(kind.to_string()); } + SharedOperatorTokenizerBoundary::ParserOnlyDebt { reason } => { + assert!( + !reason.is_empty(), + "parser-only shared operator `{pattern}` should document the v3 boundary" + ); + } } } @@ -314,6 +320,7 @@ fn keyword_spelling_for_token_kind(kind: &str) -> String { enum SharedOperatorTokenizerBoundary { Tokenized { kind: &'static str }, + ParserOnlyDebt { reason: &'static str }, } fn classify_shared_operator_for_tokenizer(pattern: &str) -> SharedOperatorTokenizerBoundary { @@ -332,9 +339,17 @@ fn classify_shared_operator_for_tokenizer(pattern: &str) -> SharedOperatorTokeni "||" => SharedOperatorTokenizerBoundary::Tokenized { kind: "PipePipe" }, "|>" => SharedOperatorTokenizerBoundary::Tokenized { kind: "PipeArrow" }, "." => SharedOperatorTokenizerBoundary::Tokenized { kind: "Dot" }, + "??" => SharedOperatorTokenizerBoundary::ParserOnlyDebt { + reason: "null-coalescing is declared in external dag_operators but excluded from \ + v3_supported_dag_operators until v3 parses it end-to-end", + }, + "%" => SharedOperatorTokenizerBoundary::ParserOnlyDebt { + reason: "modulo is declared in external dag_operators but excluded from \ + v3_supported_dag_operators until v3 parses it end-to-end", + }, other => panic!( "shared syntax operator `{other}` must be classified as tokenizer punctuation or \ - removed until v3 supports it end-to-end" + explicit parser-only debt" ), } } diff --git a/src/v3/compiler/tests/integration/sg2c1_parse_tables_authority_test.rs b/src/v3/compiler/tests/integration/sg2c1_parse_tables_authority_test.rs index b90eb84dd7c..71b035b613d 100644 --- a/src/v3/compiler/tests/integration/sg2c1_parse_tables_authority_test.rs +++ b/src/v3/compiler/tests/integration/sg2c1_parse_tables_authority_test.rs @@ -187,44 +187,72 @@ fn dag_binary_operators_have_token_parse_table_and_operator_mapping() { ); } - for spec in shared_dag_operator_specs(SHARED_SYNTAX_DAG) { + for spec in v3_supported_dag_operator_specs(SHARED_SYNTAX_DAG) { let Some(binop) = spec.binop else { continue; }; let expected_token = token_kind_for_shared_dag_operator(&spec.symbol); assert!( token_variant_names.contains(expected_token), - "`dag_operators` symbol `{}` expects TokenKind::{expected_token} in tokenize.dag", + "`v3_supported_dag_operators` symbol `{}` expects TokenKind::{expected_token} in tokenize.dag", spec.symbol ); let row_token = rows_by_symbol.get(&spec.symbol).unwrap_or_else(|| { panic!( - "`dag_operators` binary operator `{}` has no BinaryOpRow in parse_tables.dag", + "`v3_supported_dag_operators` binary operator `{}` has no BinaryOpRow in parse_tables.dag", spec.symbol ) }); assert_eq!( row_token, expected_token, - "`dag_operators` binary operator `{}` maps to TokenKind::{expected_token}, but \ + "`v3_supported_dag_operators` binary operator `{}` maps to TokenKind::{expected_token}, but \ parse_tables.dag routes TokenKind::{row_token}", spec.symbol ); let Some(operator_kind) = operators::from_symbol(&spec.symbol) else { panic!( - "`dag_operators` binary operator `{}` has no operators.dag::from_symbol mapping", + "`v3_supported_dag_operators` binary operator `{}` has no operators.dag::from_symbol mapping", spec.symbol ); }; let actual_binop = operator_kind_binop_name(operator_kind); assert_eq!( actual_binop, binop, - "`dag_operators` binary operator `{}` records binop `{binop}`, but \ + "`v3_supported_dag_operators` binary operator `{}` records binop `{binop}`, but \ operators.dag maps it to `{actual_binop}`", spec.symbol ); } } +#[test] +fn dag_operator_authority_keeps_external_rows_split_from_v3_projection() { + let all: std::collections::BTreeSet<_> = shared_dag_operator_specs(SHARED_SYNTAX_DAG) + .into_iter() + .map(|spec| spec.symbol) + .collect(); + let v3_supported: std::collections::BTreeSet<_> = + v3_supported_dag_operator_specs(SHARED_SYNTAX_DAG) + .into_iter() + .map(|spec| spec.symbol) + .collect(); + + for symbol in ["??", "%"] { + assert!( + all.contains(symbol), + "`dag_operators` must retain external operator row `{symbol}`" + ); + assert!( + !v3_supported.contains(symbol), + "`v3_supported_dag_operators` must exclude unsupported v3 operator `{symbol}`" + ); + } + assert!( + v3_supported.is_subset(&all), + "`v3_supported_dag_operators` should be an explicit subset of `dag_operators`" + ); +} + #[test] fn every_top_level_item_kw_row_token_variant_is_a_token_kind_variant() { let tables_dag = compile_to_dag(PARSE_TABLES_DAG, "src/v3/compiler/parse_tables.dag") @@ -745,7 +773,15 @@ struct SharedDagOperatorSpec { } fn shared_dag_operator_specs(source: &str) -> Vec { - extract_balanced_section(source, "data dag_operators", '[', ']') + dag_operator_specs(source, "data dag_operators") +} + +fn v3_supported_dag_operator_specs(source: &str) -> Vec { + dag_operator_specs(source, "data v3_supported_dag_operators") +} + +fn dag_operator_specs(source: &str, anchor: &str) -> Vec { + extract_balanced_section(source, anchor, '[', ']') .split("OperatorSpec") .skip(1) .filter_map(|entry| { @@ -780,8 +816,8 @@ fn token_kind_for_shared_dag_operator(symbol: &str) -> &'static str { "&&" => "AmpAmp", "||" => "PipePipe", other => panic!( - "`dag_operators` binary operator `{other}` is not supported by the v3 token/parser/operator chain; \ - delete the row until the full chain lands" + "`v3_supported_dag_operators` binary operator `{other}` is not supported by the v3 token/parser/operator chain; \ + remove it from the v3 projection until the full chain lands" ), } } From b7be0aa65d2d1e1b95273d9cb3833c59bf2d3057 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Mon, 4 May 2026 14:27:29 -0400 Subject: [PATCH 30/34] WIP: crisp-ibex-569 --- dsl/extdeps/languages/dag/syntax.dag | 30 +++---- .../compiler/src/regen_parse_tables_emit.rs | 83 ++++++++++++------- .../sg2c1_parse_tables_authority_test.rs | 26 +++++- 3 files changed, 91 insertions(+), 48 deletions(-) diff --git a/dsl/extdeps/languages/dag/syntax.dag b/dsl/extdeps/languages/dag/syntax.dag index 93310aa6250..b4aa7587d6e 100644 --- a/dsl/extdeps/languages/dag/syntax.dag +++ b/dsl/extdeps/languages/dag/syntax.dag @@ -118,26 +118,18 @@ data dag_operators: List = [ ] // Current v3 capability projection. -// v3 tokenizer/parser/operator validation consumes this explicit subset; it -// intentionally excludes external `dag_operators` rows that v3 does not parse -// end-to-end yet, including `??` and `%`. +// v3 tokenizer/parser/operator validation consumes these explicit supported +// symbols, then reads precedence/binop facts from `dag_operators`. This avoids +// duplicating operator semantics while still fencing current v3 capability. +// It intentionally excludes external `dag_operators` rows that v3 does not +// parse end-to-end yet, including `??` and `%`. -data v3_supported_dag_operators: List = [ - OperatorSpec { symbol: "||", left_bp: 5, right_bp: 6, binop: Or, algebra_field: none }, - OperatorSpec { symbol: "&&", left_bp: 7, right_bp: 8, binop: And, algebra_field: none }, - OperatorSpec { symbol: "==", left_bp: 9, right_bp: 10, binop: Eq, algebra_field: none }, - OperatorSpec { symbol: "!=", left_bp: 9, right_bp: 10, binop: Ne, algebra_field: none }, - OperatorSpec { symbol: "<", left_bp: 11, right_bp: 12, binop: Lt, algebra_field: none }, - OperatorSpec { symbol: ">", left_bp: 11, right_bp: 12, binop: Gt, algebra_field: none }, - OperatorSpec { symbol: "<=", left_bp: 11, right_bp: 12, binop: Le, algebra_field: none }, - OperatorSpec { symbol: ">=", left_bp: 11, right_bp: 12, binop: Ge, algebra_field: none }, - OperatorSpec { symbol: "+", left_bp: 13, right_bp: 14, binop: Add, algebra_field: none }, - OperatorSpec { symbol: "-", left_bp: 13, right_bp: 14, binop: Sub, algebra_field: none }, - OperatorSpec { symbol: "*", left_bp: 15, right_bp: 16, binop: Mul, algebra_field: none }, - OperatorSpec { symbol: "/", left_bp: 15, right_bp: 16, binop: Div, algebra_field: none }, - OperatorSpec { symbol: "|>", left_bp: 17, right_bp: 18, binop: none, algebra_field: none }, - OperatorSpec { symbol: ".", left_bp: 19, right_bp: 20, binop: none, algebra_field: none } -] +data v3_supported_dag_operators: Map = { + "||": true, "&&": true, + "==": true, "!=": true, "<": true, ">": true, "<=": true, ">=": true, + "+": true, "-": true, "*": true, "/": true, + "|>": true, ".": true +} data dag_syntax_spec: SyntaxSpec = SyntaxSpec { item_forms: dag_item_forms, diff --git a/src/v3/compiler/src/regen_parse_tables_emit.rs b/src/v3/compiler/src/regen_parse_tables_emit.rs index 6a36d1314b8..5e95e43e3e2 100644 --- a/src/v3/compiler/src/regen_parse_tables_emit.rs +++ b/src/v3/compiler/src/regen_parse_tables_emit.rs @@ -20,14 +20,13 @@ //! **Cross-validation against v3 projection of shared authority.** //! `dag_operators` in `syntax.dag` owns external .dag syntax reality, including //! operators v3 does not yet parse end-to-end. `v3_supported_dag_operators` is -//! the explicit current v3 capability projection. Each `BinaryOpRow` in -//! `parse_tables.dag` is checked against that projection: the row's -//! `operator_symbol` must appear in `v3_supported_dag_operators`, and its -//! declared `level` must be consistent with the projected row's binding power -//! (the coarse parser precedence level covers a contiguous bp range — see -//! `level_for_bp`). If the v3 projection shifts an operator's bp into a -//! different level, or drops a symbol, regen fails closed here without forcing -//! v3 to support every externally modeled operator. +//! the explicit current v3 capability projection as a symbol subset. Each +//! `BinaryOpRow` in `parse_tables.dag` is checked against that projection: the +//! row's `operator_symbol` must appear in `v3_supported_dag_operators`, while +//! binding power and binop facts are read from the corresponding +//! `dag_operators` row (the coarse parser precedence level covers a contiguous +//! bp range — see `level_for_bp`). If the v3 projection shifts, regen fails +//! closed here without forcing v3 to support every externally modeled operator. //! //! **SG-1a scaffold extension.** `syntax.dag`'s operator-list bodies still //! lower as `ValueBody::Unparsed` (same scaffold `regen_tokenize` extends), so @@ -1058,14 +1057,14 @@ fn rustfmt_stdout(source: &str) -> Result { String::from_utf8(output.stdout).map_err(|e| format!("rustfmt stdout utf-8: {e}")) } -/// SG-1a scaffold extension: read `data v3_supported_dag_operators: List` -/// directly from `syntax.dag` source text because that body still lowers -/// as `ValueBody::Unparsed` (same state `regen_tokenize` encountered). -/// A trimmed view of `OperatorSpec` sufficient for regen -/// cross-validation against `parse_tables.dag`. +/// SG-1a scaffold extension: read operator projection data directly from +/// `syntax.dag` source text because `dag_operators` still lowers as +/// `ValueBody::Unparsed` (same state `regen_tokenize` encountered). A trimmed +/// view of `OperatorSpec` is sufficient for regen cross-validation against +/// `parse_tables.dag`. pub(crate) struct SharedOperatorSpec { pub left_bp: i64, - /// Variant name from the v3 projection's `binop` field, e.g. + /// Variant name from the external authority's `binop` field, e.g. /// `"Or"`, `"Add"`, `"Eq"`. Matches the leaf variant label /// inside the corresponding `OperatorKind` (see /// `operator_kind_binop_name`), which is the structural join @@ -1075,14 +1074,16 @@ pub(crate) struct SharedOperatorSpec { } /// Returns a symbol → (left_bp, binop_name) map for the current v3-supported -/// operator projection. Fails closed on malformed input. Dissolution trigger: -/// when `v3_supported_dag_operators` lowers structurally, swap this extractor -/// for a typed read — same lane as `regen_tokenize`'s +/// operator projection by filtering external `dag_operators` with the explicit +/// `v3_supported_dag_operators` symbol set. Fails closed on malformed input. +/// Dissolution trigger: when `dag_operators` lowers structurally, swap this +/// extractor for a typed read — same lane as `regen_tokenize`'s /// `assert_shared_syntax_raw_source_scaffold_still_required`. fn extract_v3_supported_operator_bps( source: &str, ) -> std::collections::BTreeMap { - let section = extract_balanced_section(source, "data v3_supported_dag_operators", '[', ']'); + let supported_symbols = extract_v3_supported_operator_symbols(source); + let section = extract_balanced_section(source, "data dag_operators", '[', ']'); let mut out = std::collections::BTreeMap::new(); // Each entry is `OperatorSpec { symbol: "X", left_bp: N, right_bp: M, binop: Y, ... }`. // Parse sequentially: symbol (string) → left_bp (int) → binop (bare identifier). @@ -1094,7 +1095,7 @@ fn extract_v3_supported_operator_bps( let after_sym = &rest[sym_idx + "symbol:".len()..]; let quote_idx = after_sym .find('"') - .expect("missing string literal for `symbol` in `v3_supported_dag_operators`"); + .expect("missing string literal for `symbol` in `dag_operators`"); let (symbol, consumed) = parse_string_literal(&after_sym[quote_idx..]); let tail = &after_sym[quote_idx + consumed..]; @@ -1121,18 +1122,44 @@ fn extract_v3_supported_operator_bps( .unwrap_or(after_binop_trimmed.len()); let binop_name = after_binop_trimmed[..ident_end].to_string(); + if supported_symbols.contains(&symbol) { + assert!( + out.insert( + symbol.clone(), + SharedOperatorSpec { + left_bp: bp, + binop_name, + }, + ) + .is_none(), + "duplicate `dag_operators` row for v3-supported symbol `{symbol}`" + ); + } + rest = &after_binop_trimmed[ident_end..]; + } + for symbol in supported_symbols { assert!( - out.insert( - symbol.clone(), - SharedOperatorSpec { - left_bp: bp, - binop_name, - }, - ) - .is_none(), + out.contains_key(&symbol), + "`v3_supported_dag_operators` symbol `{symbol}` is not present in external `dag_operators`" + ); + } + out +} + +fn extract_v3_supported_operator_symbols(source: &str) -> BTreeSet { + let section = extract_balanced_section(source, "data v3_supported_dag_operators", '{', '}'); + let mut out = BTreeSet::new(); + let mut rest = section; + loop { + let Some(quote_idx) = rest.find('"') else { + break; + }; + let (symbol, consumed) = parse_string_literal(&rest[quote_idx..]); + assert!( + out.insert(symbol.clone()), "duplicate `v3_supported_dag_operators` row for symbol `{symbol}`" ); - rest = &after_binop_trimmed[ident_end..]; + rest = &rest[quote_idx + consumed..]; } out } diff --git a/src/v3/compiler/tests/integration/sg2c1_parse_tables_authority_test.rs b/src/v3/compiler/tests/integration/sg2c1_parse_tables_authority_test.rs index 71b035b613d..4197ef950fe 100644 --- a/src/v3/compiler/tests/integration/sg2c1_parse_tables_authority_test.rs +++ b/src/v3/compiler/tests/integration/sg2c1_parse_tables_authority_test.rs @@ -767,6 +767,7 @@ fn primary_atom_rows_cover_exactly_the_tokens_parse_primary_atomic_arm() { ); } +#[derive(Clone)] struct SharedDagOperatorSpec { symbol: String, binop: Option, @@ -777,7 +778,21 @@ fn shared_dag_operator_specs(source: &str) -> Vec { } fn v3_supported_dag_operator_specs(source: &str) -> Vec { - dag_operator_specs(source, "data v3_supported_dag_operators") + let specs_by_symbol: std::collections::BTreeMap<_, _> = shared_dag_operator_specs(source) + .into_iter() + .map(|spec| (spec.symbol.clone(), spec)) + .collect(); + v3_supported_dag_operator_symbols(source) + .into_iter() + .map(|symbol| { + specs_by_symbol.get(&symbol).cloned().unwrap_or_else(|| { + panic!( + "`v3_supported_dag_operators` symbol `{symbol}` is not present in external \ + `dag_operators`" + ) + }) + }) + .collect() } fn dag_operator_specs(source: &str, anchor: &str) -> Vec { @@ -801,6 +816,15 @@ fn dag_operator_specs(source: &str, anchor: &str) -> Vec .collect() } +fn v3_supported_dag_operator_symbols(source: &str) -> Vec { + parse_map_string_keys(extract_balanced_section( + source, + "data v3_supported_dag_operators", + '{', + '}', + )) +} + fn token_kind_for_shared_dag_operator(symbol: &str) -> &'static str { match symbol { "==" => "EqEq", From 324b39ab1e5ca4e6101d4f358280feb9c301f555 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Mon, 4 May 2026 18:52:17 +0000 Subject: [PATCH 31/34] test: parse v3 dag operator subset symbols locally --- .../sg2c1_parse_tables_authority_test.rs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/v3/compiler/tests/integration/sg2c1_parse_tables_authority_test.rs b/src/v3/compiler/tests/integration/sg2c1_parse_tables_authority_test.rs index 4197ef950fe..8185deb174c 100644 --- a/src/v3/compiler/tests/integration/sg2c1_parse_tables_authority_test.rs +++ b/src/v3/compiler/tests/integration/sg2c1_parse_tables_authority_test.rs @@ -817,7 +817,7 @@ fn dag_operator_specs(source: &str, anchor: &str) -> Vec } fn v3_supported_dag_operator_symbols(source: &str) -> Vec { - parse_map_string_keys(extract_balanced_section( + parse_all_string_literals(extract_balanced_section( source, "data v3_supported_dag_operators", '{', @@ -903,6 +903,20 @@ fn extract_balanced_section<'a>(source: &'a str, anchor: &str, open: char, close panic!("unterminated `{anchor}` section"); } +fn parse_all_string_literals(source: &str) -> Vec { + let mut out = Vec::new(); + let mut rest = source; + while let Some(start) = rest.find('"') { + let tail = &rest[start + 1..]; + let end = tail + .find('"') + .unwrap_or_else(|| panic!("unterminated string literal in `{source}`")); + out.push(tail[..end].to_string()); + rest = &tail[end + 1..]; + } + out +} + fn string_literal_after(source: &str, label: &str) -> Option { let tail = source.split_once(label)?.1.trim_start(); let tail = tail.strip_prefix('"')?; From 03159563932e432f283477b755a1780f59b92d26 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Mon, 4 May 2026 19:19:48 +0000 Subject: [PATCH 32/34] fix: validate all shared dag operator rows --- .../compiler/src/regen_parse_tables_emit.rs | 78 ++++++++++++++----- 1 file changed, 60 insertions(+), 18 deletions(-) diff --git a/src/v3/compiler/src/regen_parse_tables_emit.rs b/src/v3/compiler/src/regen_parse_tables_emit.rs index 5e95e43e3e2..123f3a0f15f 100644 --- a/src/v3/compiler/src/regen_parse_tables_emit.rs +++ b/src/v3/compiler/src/regen_parse_tables_emit.rs @@ -1062,6 +1062,7 @@ fn rustfmt_stdout(source: &str) -> Result { /// `ValueBody::Unparsed` (same state `regen_tokenize` encountered). A trimmed /// view of `OperatorSpec` is sufficient for regen cross-validation against /// `parse_tables.dag`. +#[derive(Clone)] pub(crate) struct SharedOperatorSpec { pub left_bp: i64, /// Variant name from the external authority's `binop` field, e.g. @@ -1083,6 +1084,22 @@ fn extract_v3_supported_operator_bps( source: &str, ) -> std::collections::BTreeMap { let supported_symbols = extract_v3_supported_operator_symbols(source); + let external_operators = extract_shared_operator_bps(source); + let mut out = std::collections::BTreeMap::new(); + for symbol in supported_symbols { + let spec = external_operators.get(&symbol).unwrap_or_else(|| { + panic!( + "`v3_supported_dag_operators` symbol `{symbol}` is not present in external `dag_operators`" + ) + }); + out.insert(symbol, spec.clone()); + } + out +} + +fn extract_shared_operator_bps( + source: &str, +) -> std::collections::BTreeMap { let section = extract_balanced_section(source, "data dag_operators", '[', ']'); let mut out = std::collections::BTreeMap::new(); // Each entry is `OperatorSpec { symbol: "X", left_bp: N, right_bp: M, binop: Y, ... }`. @@ -1122,26 +1139,18 @@ fn extract_v3_supported_operator_bps( .unwrap_or(after_binop_trimmed.len()); let binop_name = after_binop_trimmed[..ident_end].to_string(); - if supported_symbols.contains(&symbol) { - assert!( - out.insert( - symbol.clone(), - SharedOperatorSpec { - left_bp: bp, - binop_name, - }, - ) - .is_none(), - "duplicate `dag_operators` row for v3-supported symbol `{symbol}`" - ); - } - rest = &after_binop_trimmed[ident_end..]; - } - for symbol in supported_symbols { assert!( - out.contains_key(&symbol), - "`v3_supported_dag_operators` symbol `{symbol}` is not present in external `dag_operators`" + out.insert( + symbol.clone(), + SharedOperatorSpec { + left_bp: bp, + binop_name, + }, + ) + .is_none(), + "duplicate `dag_operators` row for symbol `{symbol}`" ); + rest = &after_binop_trimmed[ident_end..]; } out } @@ -1240,3 +1249,36 @@ fn parse_string_literal(source: &str) -> (String, usize) { } panic!("unterminated string literal in shared syntax authority"); } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn unsupported_external_operator_duplicates_fail_closed() { + let source = r#" +data dag_operators: List = [ + OperatorSpec { symbol: "||", left_bp: 5, right_bp: 6, binop: Or, algebra_field: none }, + OperatorSpec { symbol: "??", left_bp: 8, right_bp: 9, binop: NullCoalesce, algebra_field: none }, + OperatorSpec { symbol: "??", left_bp: 8, right_bp: 9, binop: NullCoalesce, algebra_field: none } +] + +data v3_supported_dag_operators: Map = { + "||": true +} +"#; + let err = match std::panic::catch_unwind(|| extract_v3_supported_operator_bps(source)) { + Ok(_) => panic!("duplicate unsupported external operator should panic"), + Err(err) => err, + }; + let msg = err + .downcast_ref::() + .map(String::as_str) + .or_else(|| err.downcast_ref::<&str>().copied()) + .unwrap_or(""); + assert!( + msg.contains("duplicate `dag_operators` row for symbol `??`"), + "unexpected panic: {msg}" + ); + } +} From 26520caae69b30d5c8b26eab6c586d9cf5714777 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Mon, 4 May 2026 19:23:22 +0000 Subject: [PATCH 33/34] fix: derive tokenizer operator debt from v3 projection --- src/v3/compiler/src/bin/regen_tokenize.rs | 53 +++++++++++++------ .../sg1_tokenize_authority_test.rs | 33 +++++++----- 2 files changed, 57 insertions(+), 29 deletions(-) diff --git a/src/v3/compiler/src/bin/regen_tokenize.rs b/src/v3/compiler/src/bin/regen_tokenize.rs index 7156991db89..678f358e010 100644 --- a/src/v3/compiler/src/bin/regen_tokenize.rs +++ b/src/v3/compiler/src/bin/regen_tokenize.rs @@ -519,7 +519,8 @@ fn collect_punct_rows(dag: &Dag, shared_syntax: &SharedSyntaxAuthority) -> Vec<( let mut covered_kinds = BTreeSet::new(); for pattern in &shared_syntax.operators { - match classify_shared_operator_for_tokenizer(pattern) { + match classify_shared_operator_for_tokenizer(pattern, &shared_syntax.v3_supported_operators) + { SharedOperatorTokenizerBoundary::Tokenized { kind } => { assert!( punct_variants.contains(kind), @@ -610,16 +611,24 @@ fn keyword_spelling_for_token_kind(kind: &str) -> String { .to_ascii_lowercase() } -// SG-1a operator bridge: shared operators still lower through raw-source reads, -// so every `dag_operators` symbol must be classified explicitly here as either -// tokenizer punctuation or parser-only debt. Unknown symbols panic so upstream -// authority edits cannot silently disappear. +// SG-1a operator bridge: shared operators still lower through raw-source reads. +// `v3_supported_dag_operators` is the single support/exclusion projection; this +// table only maps supported symbols to their current tokenizer variant. enum SharedOperatorTokenizerBoundary { Tokenized { kind: &'static str }, ParserOnlyDebt { reason: &'static str }, } -fn classify_shared_operator_for_tokenizer(pattern: &str) -> SharedOperatorTokenizerBoundary { +fn classify_shared_operator_for_tokenizer( + pattern: &str, + v3_supported: &BTreeSet, +) -> SharedOperatorTokenizerBoundary { + if !v3_supported.contains(pattern) { + return SharedOperatorTokenizerBoundary::ParserOnlyDebt { + reason: "operator is declared in external dag_operators but excluded from \ + v3_supported_dag_operators until v3 parses it end-to-end", + }; + } match pattern { "==" => SharedOperatorTokenizerBoundary::Tokenized { kind: "EqEq" }, "!=" => SharedOperatorTokenizerBoundary::Tokenized { kind: "NotEq" }, @@ -635,18 +644,10 @@ fn classify_shared_operator_for_tokenizer(pattern: &str) -> SharedOperatorTokeni "||" => SharedOperatorTokenizerBoundary::Tokenized { kind: "PipePipe" }, "|>" => SharedOperatorTokenizerBoundary::Tokenized { kind: "PipeArrow" }, "." => SharedOperatorTokenizerBoundary::Tokenized { kind: "Dot" }, - "??" => SharedOperatorTokenizerBoundary::ParserOnlyDebt { - reason: "null-coalescing is declared in shared syntax but the v3 tokenizer/parser \ - surface does not yet admit it as punctuation", - }, - "%" => SharedOperatorTokenizerBoundary::ParserOnlyDebt { - reason: "modulo is declared in shared syntax but the v3 tokenizer/parser surface \ - does not yet admit it as punctuation", - }, other => panic!( - "shared syntax operator `{other}` has no SG-1a tokenizer bridge classification. \ - Update `classify_shared_operator_for_tokenizer` to mark it as tokenizer-owned \ - punctuation or explicit parser-only debt so the authority boundary stays fail-closed." + "`v3_supported_dag_operators` includes `{other}`, but the SG-1a tokenizer bridge has \ + no TokenKind mapping for it. Add tokenizer/parser/operator support or remove it from \ + the v3 projection." ), } } @@ -654,6 +655,7 @@ fn classify_shared_operator_for_tokenizer(pattern: &str) -> SharedOperatorTokeni struct SharedSyntaxAuthority { keywords: Vec, operators: Vec, + v3_supported_operators: BTreeSet, } impl SharedSyntaxAuthority { @@ -670,9 +672,15 @@ impl SharedSyntaxAuthority { extract_balanced_section(source, "data dag_operators", '[', ']'), "symbol", ); + let v3_supported_operators: BTreeSet<_> = parse_all_string_literals( + extract_balanced_section(source, "data v3_supported_dag_operators", '{', '}'), + ) + .into_iter() + .collect(); Self { keywords, operators, + v3_supported_operators, } } } @@ -730,6 +738,17 @@ fn parse_named_string_fields(section: &str, field_name: &str) -> Vec { out } +fn parse_all_string_literals(section: &str) -> Vec { + let mut out = Vec::new(); + let mut rest = section; + while let Some(idx) = rest.find('"') { + let (value, consumed) = parse_string_literal(&rest[idx..]); + out.push(value); + rest = &rest[idx + consumed..]; + } + out +} + fn parse_string_literal(source: &str) -> (String, usize) { assert!( source.starts_with('"'), diff --git a/src/v3/compiler/tests/integration/sg1_tokenize_authority_test.rs b/src/v3/compiler/tests/integration/sg1_tokenize_authority_test.rs index 2cd1de69f0d..1346139bad1 100644 --- a/src/v3/compiler/tests/integration/sg1_tokenize_authority_test.rs +++ b/src/v3/compiler/tests/integration/sg1_tokenize_authority_test.rs @@ -237,6 +237,14 @@ fn shared_operator_boundary_is_explicit_and_fail_closed() { extract_balanced_section(SHARED_SYNTAX_DAG, "data dag_operators", '[', ']'), "symbol", ); + let v3_supported_operators: BTreeSet<_> = parse_map_string_keys(extract_balanced_section( + SHARED_SYNTAX_DAG, + "data v3_supported_dag_operators", + '{', + '}', + )) + .into_iter() + .collect(); let punct_kind_decl = dag .declaration_by_name("PunctTokenKind") .expect("PunctTokenKind declaration"); @@ -253,7 +261,7 @@ fn shared_operator_boundary_is_explicit_and_fail_closed() { let mut shared_tokenized_kinds = BTreeSet::new(); for pattern in &shared_operators { - match classify_shared_operator_for_tokenizer(pattern) { + match classify_shared_operator_for_tokenizer(pattern, &v3_supported_operators) { SharedOperatorTokenizerBoundary::Tokenized { kind } => { assert!( punct_variant_labels.contains(kind), @@ -323,7 +331,16 @@ enum SharedOperatorTokenizerBoundary { ParserOnlyDebt { reason: &'static str }, } -fn classify_shared_operator_for_tokenizer(pattern: &str) -> SharedOperatorTokenizerBoundary { +fn classify_shared_operator_for_tokenizer( + pattern: &str, + v3_supported: &BTreeSet, +) -> SharedOperatorTokenizerBoundary { + if !v3_supported.contains(pattern) { + return SharedOperatorTokenizerBoundary::ParserOnlyDebt { + reason: "operator is declared in external dag_operators but excluded from \ + v3_supported_dag_operators until v3 parses it end-to-end", + }; + } match pattern { "==" => SharedOperatorTokenizerBoundary::Tokenized { kind: "EqEq" }, "!=" => SharedOperatorTokenizerBoundary::Tokenized { kind: "NotEq" }, @@ -339,17 +356,9 @@ fn classify_shared_operator_for_tokenizer(pattern: &str) -> SharedOperatorTokeni "||" => SharedOperatorTokenizerBoundary::Tokenized { kind: "PipePipe" }, "|>" => SharedOperatorTokenizerBoundary::Tokenized { kind: "PipeArrow" }, "." => SharedOperatorTokenizerBoundary::Tokenized { kind: "Dot" }, - "??" => SharedOperatorTokenizerBoundary::ParserOnlyDebt { - reason: "null-coalescing is declared in external dag_operators but excluded from \ - v3_supported_dag_operators until v3 parses it end-to-end", - }, - "%" => SharedOperatorTokenizerBoundary::ParserOnlyDebt { - reason: "modulo is declared in external dag_operators but excluded from \ - v3_supported_dag_operators until v3 parses it end-to-end", - }, other => panic!( - "shared syntax operator `{other}` must be classified as tokenizer punctuation or \ - explicit parser-only debt" + "`v3_supported_dag_operators` includes `{other}`, but the SG-1 tokenizer bridge has \ + no TokenKind mapping for it" ), } } From 1846c256d7229b7998953a0a5a775c13cf55ade8 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Mon, 4 May 2026 16:21:25 -0400 Subject: [PATCH 34/34] WIP: crisp-ibex-569 --- .../src/extdeps_languages_dag_syntax.rs | 40 ++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/src/v2/stage0/src/extdeps_languages_dag_syntax.rs b/src/v2/stage0/src/extdeps_languages_dag_syntax.rs index 7c8dc54a996..afd74804c93 100644 --- a/src/v2/stage0/src/extdeps_languages_dag_syntax.rs +++ b/src/v2/stage0/src/extdeps_languages_dag_syntax.rs @@ -1,7 +1,9 @@ // Generated by v2 compiler -- do not edit. // Source module: extdeps.languages.dag.syntax -use crate::std_syntax::BinOp::{Add, And, Div, Eq, Ge, Gt, Le, Lt, Mul, Ne, Or, Sub}; +use crate::std_syntax::BinOp::{ + Add, And, Div, Eq, Ge, Gt, Le, Lt, Mod, Mul, Ne, NullCoalesce, Or, Sub, +}; use crate::std_syntax::BodyKind::{ BlockBody, ExprBody, NoBody, ResourceBody, ServiceBody, TypeBody, ValueBody, }; @@ -193,6 +195,12 @@ pub fn dag_operators() -> Rc>> { right_bp: 8, binop: Some(BinOp::And), algebra_field: None, + }), Rc::new(OperatorSpec { + symbol: "??".to_string(), + left_bp: 8, + right_bp: 9, + binop: Some(BinOp::NullCoalesce), + algebra_field: None, }), Rc::new(OperatorSpec { symbol: "==".to_string(), left_bp: 9, @@ -253,6 +261,12 @@ pub fn dag_operators() -> Rc>> { right_bp: 16, binop: Some(BinOp::Div), algebra_field: None, + }), Rc::new(OperatorSpec { + symbol: "%".to_string(), + left_bp: 15, + right_bp: 16, + binop: Some(BinOp::Mod), + algebra_field: None, }), Rc::new(OperatorSpec { symbol: "|>".to_string(), left_bp: 17, @@ -271,6 +285,30 @@ pub fn dag_operators() -> Rc>> { CACHED.with(|c| c.clone()) } +pub fn v3_supported_dag_operators() -> Rc> { + thread_local! { + static CACHED: Rc> = { + let mut __m = HashMap::new(); + __m.insert("||".to_string(), true); + __m.insert("&&".to_string(), true); + __m.insert("==".to_string(), true); + __m.insert("!=".to_string(), true); + __m.insert("<".to_string(), true); + __m.insert(">".to_string(), true); + __m.insert("<=".to_string(), true); + __m.insert(">=".to_string(), true); + __m.insert("+".to_string(), true); + __m.insert("-".to_string(), true); + __m.insert("*".to_string(), true); + __m.insert("/".to_string(), true); + __m.insert("|>".to_string(), true); + __m.insert(".".to_string(), true); + Rc::new(__m) + }; + } + CACHED.with(|c| c.clone()) +} + pub fn dag_syntax_spec() -> Rc { thread_local! { static CACHED: Rc = {