From a96fc9fe944e9079b34ccc285ebb064b92b7aea3 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 05:49:13 +0000 Subject: [PATCH 001/169] Fix daglang clippy-deny warnings in syntax and resolve Co-authored-by: Brian Searls --- core/daglang/daglang-resolve/src/lib.rs | 8 +++++--- core/daglang/daglang-syntax/src/lexer.rs | 4 ++-- core/daglang/daglang-syntax/src/parser.rs | 15 ++++++--------- 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/core/daglang/daglang-resolve/src/lib.rs b/core/daglang/daglang-resolve/src/lib.rs index 8f93de126df..e6bad8dc09e 100644 --- a/core/daglang/daglang-resolve/src/lib.rs +++ b/core/daglang/daglang-resolve/src/lib.rs @@ -25,6 +25,8 @@ use daglang_syntax::ast::SourceFile; use daglang_syntax::diagnostic::Diagnostic; use daglang_syntax::parser; +type ParsedModuleRecord = (PathBuf, Vec, Vec>, SourceFile); + /// A resolved module in the dependency graph. #[derive(Debug)] pub struct ResolvedModule { @@ -55,7 +57,7 @@ impl ModuleGraph { } dag_files.sort(); - let mut parsed: Vec<(PathBuf, Vec, Vec>, SourceFile)> = Vec::new(); + let mut parsed: Vec = Vec::new(); let mut parse_errors: Vec<(PathBuf, Vec)> = Vec::new(); for path in &dag_files { @@ -200,8 +202,8 @@ fn topo_sort(modules: &mut Vec) -> Result<(), ResolveError> { for idx in &order { seen[*idx] = true; } - for idx in 0..n { - if !seen[idx] { + for (idx, visited) in seen.iter().enumerate().take(n) { + if !visited { order.push(idx); } } diff --git a/core/daglang/daglang-syntax/src/lexer.rs b/core/daglang/daglang-syntax/src/lexer.rs index 5e1ba8959f2..927f832f725 100644 --- a/core/daglang/daglang-syntax/src/lexer.rs +++ b/core/daglang/daglang-syntax/src/lexer.rs @@ -432,8 +432,8 @@ mod tests { #[test] fn numbers() { - assert_eq!(kinds("42 3.14"), vec![ - TokenKind::Int(42), TokenKind::Float(3.14), TokenKind::Eof, + assert_eq!(kinds("42 2.5"), vec![ + TokenKind::Int(42), TokenKind::Float(2.5), TokenKind::Eof, ]); } diff --git a/core/daglang/daglang-syntax/src/parser.rs b/core/daglang/daglang-syntax/src/parser.rs index 9690a7229e5..97c571bd485 100644 --- a/core/daglang/daglang-syntax/src/parser.rs +++ b/core/daglang/daglang-syntax/src/parser.rs @@ -879,11 +879,9 @@ impl Parser { inputs = self.parse_field_list_until_rparen()?; self.expect(&TokenKind::RParen)?; } - if self.eat(&TokenKind::Arrow) { - if self.eat(&TokenKind::LBrace) { - outputs = self.parse_field_list_until_rbrace()?; - self.expect(&TokenKind::RBrace)?; - } + if self.eat(&TokenKind::Arrow) && self.eat(&TokenKind::LBrace) { + outputs = self.parse_field_list_until_rbrace()?; + self.expect(&TokenKind::RBrace)?; } while self.check(&TokenKind::At) { annotations.push(self.parse_annotation()?); @@ -1346,9 +1344,8 @@ impl Parser { } if self.eat(&TokenKind::Colon) { - match self.parse_expr(0) { - Ok(e) => args.push(e), - Err(_) => {} + if let Ok(e) = self.parse_expr(0) { + args.push(e); } self.skip_annotation_value(); } @@ -2058,7 +2055,7 @@ impl Parser { kind if Self::token_kind_as_ident(&kind).is_some() => { let name = Self::token_kind_as_ident(&kind).unwrap(); self.advance(); - if name.chars().next().map_or(false, |c| c.is_uppercase()) { + if name.chars().next().is_some_and(|c| c.is_uppercase()) { if self.eat(&TokenKind::LParen) { let mut fields = Vec::new(); let mut positional_index = 0usize; From ab41b4abd3063ba19a2ff1ea666fe4867a173c1f Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 05:59:11 +0000 Subject: [PATCH 002/169] Implement daglang phase-1 scaffold compile path across typecheck/lower/derive/emit Co-authored-by: Brian Searls --- Cargo.lock | 1 + core/daglang/daglang-cli/src/compile.rs | 253 +++++++++ core/daglang/daglang-cli/src/lib.rs | 1 + core/daglang/daglang-cli/src/main.rs | 78 ++- .../daglang-cli/tests/compile_commands.rs | 126 +++++ core/daglang/daglang-derive/src/lib.rs | 307 +++++++++++ core/daglang/daglang-emit/src/lib.rs | 265 +++++++++ core/daglang/daglang-lower/Cargo.toml | 3 + core/daglang/daglang-lower/src/lib.rs | 206 +++++++ core/daglang/daglang-typecheck/src/lib.rs | 509 ++++++++++++++++++ 10 files changed, 1727 insertions(+), 22 deletions(-) create mode 100644 core/daglang/daglang-cli/src/compile.rs create mode 100644 core/daglang/daglang-cli/tests/compile_commands.rs diff --git a/Cargo.lock b/Cargo.lock index 844f179c78b..266aef535f9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -160,6 +160,7 @@ dependencies = [ name = "daglang-lower" version = "0.1.0" dependencies = [ + "daglang-resolve", "daglang-syntax", "daglang-typecheck", "gunbc-ir", diff --git a/core/daglang/daglang-cli/src/compile.rs b/core/daglang/daglang-cli/src/compile.rs new file mode 100644 index 00000000000..b4af181a5bf --- /dev/null +++ b/core/daglang/daglang-cli/src/compile.rs @@ -0,0 +1,253 @@ +use std::path::PathBuf; + +use crate::pipeline::PipelineContext; +use daglang_derive::{derive_artifacts, DerivedArtifacts, ProgressManifest}; +use daglang_emit::{emit_rust_bundle, EmissionBundle}; +use daglang_lower::{lower_typed_project, LoweredOp}; +use daglang_resolve::{ModuleGraph, ResolveError, ResolvedModule}; +use daglang_syntax::parser; +use daglang_typecheck::typecheck_module_graph; +use gunbc_ir::Dag; + +#[derive(Debug)] +pub struct CompileOutput { + pub lowered_dag: Dag, + pub derived: DerivedArtifacts, + pub emitted: EmissionBundle, +} + +pub fn build_context(input: Option<&String>) -> PipelineContext { + let parsed = input.map(PathBuf::from); + let (roots, target_file) = match parsed { + Some(path) if path.extension().and_then(|ext| ext.to_str()) == Some("dag") => { + let root = path + .parent() + .map(PathBuf::from) + .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| ".".into())); + (vec![root], Some(path)) + } + Some(path) => (vec![path], None), + None => (vec![resolve_default_root()], None), + }; + + PipelineContext { roots, target_file } +} + +pub fn resolve_default_root() -> PathBuf { + let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")); + let dsl = cwd.join("dsl"); + if dsl.exists() { + dsl + } else { + PathBuf::from("dsl") + } +} + +pub fn compile_from_context(context: &PipelineContext) -> Result { + let module_graph = discover_module_graph_for_context(context)?; + let typed = typecheck_module_graph(module_graph).map_err(|errors| { + let mut message = String::from("typecheck errors:\n"); + for error in errors { + message.push_str(&format!(" {error}\n")); + } + message + })?; + let lowered = lower_typed_project(&typed).map_err(|error| format!("lower error: {error}"))?; + let derived = derive_artifacts(&lowered).map_err(|error| format!("derive error: {error}"))?; + let emitted = + emit_rust_bundle(&lowered, &derived).map_err(|error| format!("emit error: {error}"))?; + + Ok(CompileOutput { + lowered_dag: lowered, + derived, + emitted, + }) +} + +fn discover_module_graph_for_context(context: &PipelineContext) -> Result { + if let Some(target_file) = &context.target_file { + let source = std::fs::read_to_string(target_file) + .map_err(|error| format!("failed to read {}: {error}", target_file.display()))?; + let ast = parser::parse(&source).map_err(|errors| { + let mut message = String::from("compile diagnostics:\n"); + for error in &errors { + message.push_str(&format!( + " {}\n", + error.format_with_source(target_file, &source) + )); + } + message + })?; + let module_path = ast + .module_path + .as_ref() + .map(|module| module.node.segments.clone()) + .unwrap_or_else(|| { + target_file + .file_stem() + .and_then(|stem| stem.to_str()) + .map(|stem| vec![stem.to_string()]) + .unwrap_or_else(|| vec!["unknown".to_string()]) + }); + return Ok(ModuleGraph { + modules: vec![ResolvedModule { + path: target_file.clone(), + ast, + module_path, + dependencies: Vec::new(), + }], + }); + } + + ModuleGraph::discover(&context.roots).map_err(format_resolve_error) +} + +fn format_resolve_error(error: ResolveError) -> String { + match error { + ResolveError::ParseErrors(files) => { + let mut message = String::from("compile diagnostics:\n"); + for (_path, diagnostics) in files { + for diagnostic in diagnostics { + message.push_str(&format!(" {}\n", diagnostic.render())); + } + } + message + } + other => format!("resolve error: {other}"), + } +} + +pub fn render_expand(dag: &Dag) -> String { + let mut out = String::new(); + out.push_str("Nodes:\n"); + for node in &dag.nodes { + let kind = match &node.body { + gunbc_ir::node::NodeBody::Opaque(LoweredOp::Callable { kind, module, name }) => { + format!("callable::{kind:?} {module}.{name}") + } + gunbc_ir::node::NodeBody::Opaque(LoweredOp::Pipeline { + module, + name, + stages, + }) => format!("pipeline {module}.{name} ({stages} stages)"), + gunbc_ir::node::NodeBody::SubDag(_) => "subdag".to_string(), + }; + + out.push_str(&format!(" - {} [{kind}]\n", node.id.0)); + if !node.inputs.is_empty() { + out.push_str(" inputs:\n"); + for input in &node.inputs { + out.push_str(&format!( + " * {}: {} ({})\n", + input.name.0, input.type_id.0, input.cardinality + )); + } + } + if !node.outputs.is_empty() { + out.push_str(" outputs:\n"); + for output in &node.outputs { + out.push_str(&format!( + " * {}: {} ({})\n", + output.name.0, output.type_id.0, output.cardinality + )); + } + } + } + + out.push_str("Edges:\n"); + if dag.edges.is_empty() { + out.push_str(" (none)\n"); + } else { + for edge in &dag.edges { + out.push_str(&format!( + " - {}.{} -> {}.{}\n", + edge.from_node.0, edge.from_port.0, edge.to_node.0, edge.to_port.0 + )); + } + } + out +} + +pub fn render_manifest(manifest: &ProgressManifest) -> String { + let mut out = String::new(); + out.push_str("ProgressManifest:\n"); + out.push_str(&format!(" total_nodes: {}\n", manifest.total_nodes)); + out.push_str(&format!(" total_edges: {}\n", manifest.total_edges)); + out.push_str(" waves:\n"); + for (index, wave) in manifest.waves.iter().enumerate() { + out.push_str(&format!(" [{index}] {}\n", wave.join(", "))); + } + out.push_str(&format!( + " entrypoint_nodes: {}\n", + if manifest.entrypoint_nodes.is_empty() { + "(none)".to_string() + } else { + manifest.entrypoint_nodes.join(", ") + } + )); + out.push_str(&format!( + " boundary_nodes: {}\n", + if manifest.boundary_nodes.is_empty() { + "(none)".to_string() + } else { + manifest.boundary_nodes.join(", ") + } + )); + out +} + +#[cfg(test)] +mod tests { + use super::*; + use std::time::{SystemTime, UNIX_EPOCH}; + + fn unique_temp_file(name: &str) -> PathBuf { + let nanos = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("system clock should be after unix epoch") + .as_nanos(); + std::env::temp_dir().join(format!( + "daglang_cli_compile_{name}_{}_{}.dag", + std::process::id(), + nanos + )) + } + + #[test] + fn compile_single_file_makegen_produces_non_empty_outputs() { + let file = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("../../../dsl/tools/makegen.dag"); + let context = PipelineContext { + roots: vec![file.parent().expect("file should have parent").to_path_buf()], + target_file: Some(file), + }; + + let output = compile_from_context(&context).expect("compile should succeed"); + assert!(!output.lowered_dag.nodes.is_empty()); + assert!(output.derived.manifest.total_nodes > 0); + assert!(!output.emitted.files.is_empty()); + } + + #[test] + fn compile_reports_pipeline_diagnostics_for_invalid_source() { + let broken_file = unique_temp_file("broken"); + std::fs::write(&broken_file, "module broken\nfn bad( -> Unit {}") + .expect("failed to write broken source"); + + let context = PipelineContext { + roots: vec![ + broken_file + .parent() + .expect("temp file should have parent") + .to_path_buf(), + ], + target_file: Some(broken_file.clone()), + }; + + let error = compile_from_context(&context).expect_err("compile should fail"); + assert!(error.contains("compile diagnostics")); + assert!(error.contains(":2:")); + + std::fs::remove_file(broken_file).expect("failed to cleanup broken source"); + } +} diff --git a/core/daglang/daglang-cli/src/lib.rs b/core/daglang/daglang-cli/src/lib.rs index 626c2e4ca57..df698104003 100644 --- a/core/daglang/daglang-cli/src/lib.rs +++ b/core/daglang/daglang-cli/src/lib.rs @@ -1 +1,2 @@ +pub mod compile; pub mod pipeline; diff --git a/core/daglang/daglang-cli/src/main.rs b/core/daglang/daglang-cli/src/main.rs index 0a0ad5bd775..d68d93d5557 100644 --- a/core/daglang/daglang-cli/src/main.rs +++ b/core/daglang/daglang-cli/src/main.rs @@ -15,6 +15,9 @@ use std::path::PathBuf; +use daglang_cli::compile::{ + build_context, compile_from_context, render_expand, render_manifest, resolve_default_root, +}; use daglang_cli::pipeline::{build_pipeline_dag, run_pipeline, PipelineContext, PipelineStop}; fn main() { @@ -39,19 +42,47 @@ fn main() { let dag = build_pipeline_dag(); println!("{}", dag.to_mermaid("daglang-compiler-pipeline")); } else { - eprintln!("TODO: dag viz -- file visualization is not implemented yet."); + let context = build_context(args.get(2)); + match compile_from_context(&context) { + Ok(output) => { + println!("{}", output.lowered_dag.to_mermaid("daglang-compiled")); + } + Err(error) => { + eprintln!("{error}"); + std::process::exit(1); + } + } } } "expand" => { - eprintln!("TODO: dag expand -- Phase 0 deliverable"); - eprintln!("Will show lowered GraphIR: every Node, Edge, Port."); + let context = build_context(args.get(2)); + match compile_from_context(&context) { + Ok(output) => { + println!("{}", render_expand(&output.lowered_dag)); + } + Err(error) => { + eprintln!("{error}"); + std::process::exit(1); + } + } } "manifest" => { - eprintln!("TODO: dag manifest -- Phase 0 deliverable"); - eprintln!("Will show derived ProgressManifest: topology, waves, boundaries."); + let context = build_context(args.get(2)); + match compile_from_context(&context) { + Ok(output) => { + println!("{}", render_manifest(&output.derived.manifest)); + } + Err(error) => { + eprintln!("{error}"); + std::process::exit(1); + } + } } "modules" => { - let roots = vec![resolve_root(args.get(2))]; + let roots = vec![args + .get(2) + .map(PathBuf::from) + .unwrap_or_else(resolve_default_root)]; let context = PipelineContext { roots, target_file: None, @@ -79,7 +110,7 @@ fn main() { (vec![root], Some(path)) } Some(path) => (vec![path], None), - None => (vec![resolve_root(None)], None), + None => (vec![resolve_default_root()], None), }; let context = PipelineContext { roots, target_file }; @@ -101,8 +132,24 @@ fn main() { } } "compile" => { - eprintln!("TODO: dag compile -- Phase 1 deliverable"); - eprintln!("Will run full pipeline: parse → resolve → typecheck → lower → validate → derive → emit."); + let context = build_context(args.get(2)); + match compile_from_context(&context) { + Ok(output) => { + println!( + "Compiled {} module(s) to {} node(s), {} file(s) emitted.", + output.emitted.summary.module_count, + output.derived.manifest.total_nodes, + output.emitted.files.len() + ); + for file in &output.emitted.files { + println!(" - {}", file.path); + } + } + Err(error) => { + eprintln!("{error}"); + std::process::exit(1); + } + } } cmd => { eprintln!("Unknown command: {cmd}"); @@ -110,16 +157,3 @@ fn main() { } } } - -fn resolve_root(arg: Option<&String>) -> PathBuf { - if let Some(path) = arg { - return PathBuf::from(path); - } - let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")); - let dsl = cwd.join("dsl"); - if dsl.exists() { - dsl - } else { - PathBuf::from("dsl") - } -} diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs new file mode 100644 index 00000000000..59ee1352acd --- /dev/null +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -0,0 +1,126 @@ +use std::path::PathBuf; +use std::process::Command; +use std::time::{SystemTime, UNIX_EPOCH}; + +fn workspace_root() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../..") +} + +fn daglang_bin() -> &'static str { + env!("CARGO_BIN_EXE_daglang") +} + +fn makegen_file() -> PathBuf { + workspace_root().join("dsl/tools/makegen.dag") +} + +fn unique_temp_file(name: &str) -> PathBuf { + let nanos = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("system clock should be after unix epoch") + .as_nanos(); + std::env::temp_dir().join(format!( + "daglang_cli_compile_cmd_{name}_{}_{}.dag", + std::process::id(), + nanos + )) +} + +#[test] +fn compile_command_emits_summary_for_single_file() { + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(makegen_file()) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile"); + + assert!( + output.status.success(), + "compile command failed: {}", + String::from_utf8_lossy(&output.stderr) + ); + let stdout = String::from_utf8_lossy(&output.stdout); + assert!(stdout.contains("Compiled 1 module(s)")); + assert!(stdout.contains("target/generated/rust/main.rs")); +} + +#[test] +fn expand_command_shows_lowered_nodes_and_edges() { + let output = Command::new(daglang_bin()) + .arg("expand") + .arg(makegen_file()) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang expand"); + + assert!( + output.status.success(), + "expand command failed: {}", + String::from_utf8_lossy(&output.stderr) + ); + let stdout = String::from_utf8_lossy(&output.stdout); + assert!(stdout.contains("Nodes:")); + assert!(stdout.contains("tools.makegen::render_makefile")); + assert!(stdout.contains("tools.makegen::makegen")); +} + +#[test] +fn manifest_command_shows_derived_progress_manifest() { + let output = Command::new(daglang_bin()) + .arg("manifest") + .arg(makegen_file()) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang manifest"); + + assert!( + output.status.success(), + "manifest command failed: {}", + String::from_utf8_lossy(&output.stderr) + ); + let stdout = String::from_utf8_lossy(&output.stdout); + assert!(stdout.contains("ProgressManifest:")); + assert!(stdout.contains("total_nodes:")); + assert!(stdout.contains("waves:")); +} + +#[test] +fn viz_command_renders_mermaid_for_compiled_file() { + let output = Command::new(daglang_bin()) + .arg("viz") + .arg(makegen_file()) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang viz file"); + + assert!( + output.status.success(), + "viz command failed: {}", + String::from_utf8_lossy(&output.stderr) + ); + let stdout = String::from_utf8_lossy(&output.stdout); + assert!(stdout.contains("flowchart TB")); + assert!(stdout.contains("tools.makegen::render_makefile")); +} + +#[test] +fn compile_command_reports_diagnostics_for_invalid_file() { + let broken = unique_temp_file("broken"); + std::fs::write(&broken, "module sample.broken\nfn broken( -> String {") + .expect("failed to write broken source"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&broken) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile for broken file"); + + assert!(!output.status.success(), "broken source should fail compile"); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("compile diagnostics")); + assert!(stderr.contains(":2:")); + + std::fs::remove_file(broken).expect("failed to remove temp broken source"); +} diff --git a/core/daglang/daglang-derive/src/lib.rs b/core/daglang/daglang-derive/src/lib.rs index 3417b624fe1..97461198f6a 100644 --- a/core/daglang/daglang-derive/src/lib.rs +++ b/core/daglang/daglang-derive/src/lib.rs @@ -17,9 +17,316 @@ //! → ToolMetadata //! ``` +use std::collections::{BTreeMap, VecDeque}; + +use daglang_lower::LoweredOp; +use gunbc_ir::{detect_boundaries, detect_entrypoints, Dag, Node}; + +/// Derived artifacts produced from lowered GraphIR. +#[derive(Debug, Clone)] +pub struct DerivedArtifacts { + pub manifest: ProgressManifest, + pub obligations: TestObligations, + pub tool_metadata: ToolMetadata, +} + +/// Minimal progress manifest for Phase-1 compiler scaffolding. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ProgressManifest { + pub total_nodes: usize, + pub total_edges: usize, + pub waves: Vec>, + pub entrypoint_nodes: Vec, + pub boundary_nodes: Vec, +} + +/// Minimal obligation summary derived from graph structure. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct TestObligations { + pub dry_run_completion_required: bool, + pub transport_execution_targets: usize, + pub pure_node_determinism_targets: usize, +} + +/// Metadata summary for lowered modules. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ToolMetadata { + pub modules: Vec, +} + +/// Module-level metadata counts. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ModuleMetadata { + pub module: String, + pub callable_count: usize, + pub pipeline_count: usize, +} + /// Errors during derivation. #[derive(Debug)] pub enum DeriveError { /// The IR graph is not valid for manifest derivation. InvalidGraph(String), } + +impl std::fmt::Display for DeriveError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::InvalidGraph(reason) => write!(f, "invalid graph for derivation: {reason}"), + } + } +} + +/// Derive manifest, obligations, and metadata from a lowered daglang graph. +pub fn derive_artifacts(dag: &Dag) -> Result { + if dag.nodes.is_empty() { + return Err(DeriveError::InvalidGraph( + "cannot derive artifacts for an empty graph".to_string(), + )); + } + + let manifest = ProgressManifest { + total_nodes: dag.nodes.len(), + total_edges: dag.edges.len(), + waves: compute_waves(dag)?, + entrypoint_nodes: { + let mut nodes = detect_entrypoints(dag) + .entrypoint_nodes + .into_iter() + .map(|node_id| node_id.0) + .collect::>(); + nodes.sort(); + nodes + }, + boundary_nodes: { + let mut nodes = detect_boundaries(dag) + .boundary_nodes + .into_iter() + .map(|node_id| node_id.0) + .collect::>(); + nodes.sort(); + nodes + }, + }; + + let obligations = TestObligations { + dry_run_completion_required: true, + transport_execution_targets: dag + .nodes + .iter() + .filter(|node| { + node.inputs + .iter() + .any(|port| port.type_id.0 == "TransportRequest") + }) + .count(), + pure_node_determinism_targets: dag + .nodes + .iter() + .filter(|node| { + !node + .inputs + .iter() + .any(|port| port.type_id.0 == "TransportRequest") + }) + .count(), + }; + + let tool_metadata = ToolMetadata { + modules: derive_module_metadata(&dag.nodes), + }; + + Ok(DerivedArtifacts { + manifest, + obligations, + tool_metadata, + }) +} + +fn compute_waves(dag: &Dag) -> Result>, DeriveError> { + let mut indegree = BTreeMap::::new(); + let mut outgoing = BTreeMap::>::new(); + + for node in &dag.nodes { + indegree.insert(node.id.0.clone(), 0); + outgoing.insert(node.id.0.clone(), Vec::new()); + } + + for edge in &dag.edges { + let to = edge.to_node.0.clone(); + let from = edge.from_node.0.clone(); + let degree = indegree + .get_mut(&to) + .ok_or_else(|| DeriveError::InvalidGraph(format!("edge targets missing node `{to}`")))?; + *degree += 1; + outgoing + .get_mut(&from) + .ok_or_else(|| DeriveError::InvalidGraph(format!("edge source missing node `{from}`")))? + .push(to); + } + + let mut current_wave = indegree + .iter() + .filter_map(|(node, degree)| (*degree == 0).then_some(node.clone())) + .collect::>(); + current_wave.sort(); + + if current_wave.is_empty() { + return Err(DeriveError::InvalidGraph( + "graph has no entrypoint wave (likely cyclic)".to_string(), + )); + } + + let mut waves = Vec::new(); + let mut processed = 0usize; + + while !current_wave.is_empty() { + waves.push(current_wave.clone()); + processed += current_wave.len(); + let mut next_wave = Vec::new(); + + for node in ¤t_wave { + let mut queue = outgoing + .get(node) + .cloned() + .unwrap_or_default() + .into_iter() + .collect::>(); + while let Some(target) = queue.pop_front() { + let degree = indegree.get_mut(&target).ok_or_else(|| { + DeriveError::InvalidGraph(format!("missing indegree for `{target}`")) + })?; + *degree = degree.saturating_sub(1); + if *degree == 0 { + next_wave.push(target); + } + } + } + + next_wave.sort(); + next_wave.dedup(); + current_wave = next_wave; + } + + if processed != dag.nodes.len() { + return Err(DeriveError::InvalidGraph( + "graph contains a cycle and cannot be wave-partitioned".to_string(), + )); + } + + Ok(waves) +} + +fn derive_module_metadata(nodes: &[Node]) -> Vec { + let mut by_module: BTreeMap = BTreeMap::new(); + for node in nodes { + let Some(op) = node.body.as_opaque() else { + continue; + }; + match op { + LoweredOp::Callable { module, .. } => { + let entry = by_module + .entry(module.clone()) + .or_insert_with(|| ModuleMetadata { + module: module.clone(), + callable_count: 0, + pipeline_count: 0, + }); + entry.callable_count += 1; + } + LoweredOp::Pipeline { module, .. } => { + let entry = by_module + .entry(module.clone()) + .or_insert_with(|| ModuleMetadata { + module: module.clone(), + callable_count: 0, + pipeline_count: 0, + }); + entry.pipeline_count += 1; + } + } + } + by_module.into_values().collect() +} + +trait NodeBodyExt { + fn as_opaque(&self) -> Option<&LoweredOp>; +} + +impl NodeBodyExt for gunbc_ir::node::NodeBody { + fn as_opaque(&self) -> Option<&LoweredOp> { + match self { + gunbc_ir::node::NodeBody::Opaque(op) => Some(op), + gunbc_ir::node::NodeBody::SubDag(_) => None, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use daglang_lower::{CallableKind, LoweredOp}; + use gunbc_ir::{Edge, Node, Port}; + + fn callable_node(id: &str, module: &str, name: &str) -> Node { + Node::opaque( + id, + vec![Port::scalar("input", "String")], + vec![Port::scalar("output", "String")], + LoweredOp::Callable { + module: module.to_string(), + kind: CallableKind::Func, + name: name.to_string(), + }, + ) + } + + #[test] + fn derive_manifest_builds_stable_waves() { + let mut dag = Dag::new(); + dag.add_node(callable_node("a", "tools.makegen", "a")); + dag.add_node(callable_node("b", "tools.makegen", "b")); + dag.add_node(callable_node("c", "tools.makegen", "c")); + dag.add_edge(Edge::new("a", "output", "c", "input")); + dag.add_edge(Edge::new("b", "output", "c", "input")); + + let artifacts = derive_artifacts(&dag).expect("derivation should succeed"); + assert_eq!( + artifacts.manifest.waves, + vec![ + vec!["a".to_string(), "b".to_string()], + vec!["c".to_string()] + ] + ); + assert_eq!(artifacts.manifest.total_nodes, 3); + assert_eq!(artifacts.manifest.total_edges, 2); + } + + #[test] + fn derive_module_metadata_counts_callable_and_pipeline_nodes() { + let mut dag = Dag::new(); + dag.add_node(callable_node("makegen", "tools.makegen", "makegen")); + dag.add_node(Node::opaque( + "ci", + vec![], + vec![Port::scalar("stages", "Int")], + LoweredOp::Pipeline { + module: "pipelines.ci".to_string(), + name: "ci".to_string(), + stages: 12, + }, + )); + + let artifacts = derive_artifacts(&dag).expect("derivation should succeed"); + assert!(artifacts + .tool_metadata + .modules + .iter() + .any(|module| module.module == "tools.makegen" && module.callable_count == 1)); + assert!(artifacts + .tool_metadata + .modules + .iter() + .any(|module| module.module == "pipelines.ci" && module.pipeline_count == 1)); + } +} diff --git a/core/daglang/daglang-emit/src/lib.rs b/core/daglang/daglang-emit/src/lib.rs index 0d4b9646cd7..1b5e190c387 100644 --- a/core/daglang/daglang-emit/src/lib.rs +++ b/core/daglang/daglang-emit/src/lib.rs @@ -27,6 +27,10 @@ //! └── makefile/ Makefile target (from module metadata) //! ``` +use daglang_derive::{DerivedArtifacts, ProgressManifest}; +use daglang_lower::{CallableKind, LoweredOp}; +use gunbc_ir::Dag; + /// The codegen backend trait. Each target language implements this. pub trait CodegenBackend { /// Emit a type definition (record, enum, alias). @@ -51,9 +55,270 @@ pub trait CodegenBackend { fn emit_progress_manifest(&self, manifest: &str) -> String; } +/// A file emitted by a backend. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct EmittedFile { + pub path: String, + pub content: String, +} + +/// Backend emission summary. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct EmissionSummary { + pub module_count: usize, + pub callable_count: usize, + pub pipeline_count: usize, +} + +/// Aggregated emission output for a compile request. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct EmissionBundle { + pub backend: String, + pub files: Vec, + pub summary: EmissionSummary, +} + /// Errors during emission. #[derive(Debug)] pub enum EmitError { /// A construct couldn't be emitted for the target backend. UnsupportedConstruct { backend: String, construct: String }, + /// A lowered graph node could not be rendered. + InvalidLoweredNode(String), +} + +impl std::fmt::Display for EmitError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::UnsupportedConstruct { backend, construct } => { + write!(f, "backend `{backend}` does not support `{construct}`") + } + Self::InvalidLoweredNode(reason) => { + write!(f, "invalid lowered node encountered during emit: {reason}") + } + } + } +} + +/// Minimal Rust backend used by Phase-1 compiler scaffolding. +#[derive(Debug, Default, Clone, Copy)] +pub struct RustBackend; + +impl CodegenBackend for RustBackend { + fn emit_type(&self, ty: &str) -> String { + format!("// TODO(type): emit `{ty}`") + } + + fn emit_fn(&self, name: &str) -> String { + format!( + "pub fn {name}() {{\n // TODO(fn): compile pure functor body\n}}\n" + ) + } + + fn emit_func(&self, name: &str) -> String { + format!( + "pub fn {name}() {{\n // TODO(func): execute lowered DAG orchestration\n}}\n" + ) + } + + fn emit_transport(&self, spec: &str) -> String { + format!("// TODO(transport): emit transport wiring for `{spec}`") + } + + fn emit_test(&self, obligation: &str) -> String { + format!( + "#[test]\nfn obligation_{}() {{\n // TODO(test): satisfy obligation\n assert!(true);\n}}\n", + sanitize_identifier(obligation) + ) + } + + fn emit_cli(&self, entrypoints: &[String]) -> String { + format!( + "pub fn cli_entrypoints() -> &'static [&'static str] {{\n &{:?}\n}}\n", + entrypoints + ) + } + + fn emit_progress_manifest(&self, manifest: &str) -> String { + format!("// progress-manifest\n{manifest}\n") + } +} + +/// Emit a minimal Rust project bundle from lowered GraphIR and derived artifacts. +pub fn emit_rust_bundle( + dag: &Dag, + artifacts: &DerivedArtifacts, +) -> Result { + let backend = RustBackend; + let mut emitted_functions = Vec::new(); + let mut callable_count = 0usize; + let mut pipeline_count = 0usize; + + for node in &dag.nodes { + let Some(op) = node.body.as_opaque() else { + return Err(EmitError::InvalidLoweredNode(format!( + "subdag node `{}` is not supported in phase-1 emit", + node.id.0 + ))); + }; + + match op { + LoweredOp::Callable { module, kind, name } => { + callable_count += 1; + let fn_name = sanitize_identifier(&format!("{module}_{name}")); + let rendered = match kind { + CallableKind::Fn => backend.emit_fn(&fn_name), + CallableKind::Func | CallableKind::Pattern => backend.emit_func(&fn_name), + }; + emitted_functions.push(rendered); + } + LoweredOp::Pipeline { module, name, .. } => { + pipeline_count += 1; + let fn_name = sanitize_identifier(&format!("{module}_{name}")); + emitted_functions.push(backend.emit_func(&fn_name)); + } + } + } + + let module_count = artifacts.tool_metadata.modules.len(); + let manifest_rendered = render_manifest(&artifacts.manifest); + + let files = vec![ + EmittedFile { + path: "target/generated/rust/main.rs".to_string(), + content: format!( + "// Generated by daglang-emit (phase-1 scaffold)\n\n{}\n{}", + backend.emit_cli( + &artifacts + .manifest + .entrypoint_nodes + .iter() + .map(|entry| sanitize_identifier(entry)) + .collect::>() + ), + emitted_functions.join("\n") + ), + }, + EmittedFile { + path: "target/generated/rust/progress_manifest.txt".to_string(), + content: backend.emit_progress_manifest(&manifest_rendered), + }, + ]; + + Ok(EmissionBundle { + backend: "rust".to_string(), + files, + summary: EmissionSummary { + module_count, + callable_count, + pipeline_count, + }, + }) +} + +fn render_manifest(manifest: &ProgressManifest) -> String { + let mut out = String::new(); + out.push_str(&format!("total_nodes={}\n", manifest.total_nodes)); + out.push_str(&format!("total_edges={}\n", manifest.total_edges)); + out.push_str("waves=\n"); + for (idx, wave) in manifest.waves.iter().enumerate() { + out.push_str(&format!(" [{idx}] {}\n", wave.join(", "))); + } + out.push_str(&format!( + "entrypoint_nodes={}\n", + manifest.entrypoint_nodes.join(", ") + )); + out.push_str(&format!( + "boundary_nodes={}\n", + manifest.boundary_nodes.join(", ") + )); + out +} + +fn sanitize_identifier(value: &str) -> String { + let mut out = value + .chars() + .map(|ch| if ch.is_alphanumeric() { ch } else { '_' }) + .collect::(); + if out.is_empty() { + out.push('_'); + } + if out + .chars() + .next() + .is_some_and(|ch| ch.is_ascii_digit()) + { + out.insert(0, '_'); + } + out +} + +trait NodeBodyExt { + fn as_opaque(&self) -> Option<&LoweredOp>; +} + +impl NodeBodyExt for gunbc_ir::node::NodeBody { + fn as_opaque(&self) -> Option<&LoweredOp> { + match self { + gunbc_ir::node::NodeBody::Opaque(op) => Some(op), + gunbc_ir::node::NodeBody::SubDag(_) => None, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use daglang_derive::derive_artifacts; + use gunbc_ir::{Edge, Node, Port}; + + fn sample_dag() -> Dag { + let mut dag = Dag::new(); + dag.add_node(Node::opaque( + "tools.makegen::render_makefile", + vec![Port::scalar("registry", "ToolRegistry")], + vec![Port::scalar("return", "String")], + LoweredOp::Callable { + module: "tools.makegen".to_string(), + kind: CallableKind::Fn, + name: "render_makefile".to_string(), + }, + )); + dag.add_node(Node::opaque( + "tools.makegen::makegen", + vec![Port::scalar("registry", "ToolRegistry")], + vec![Port::scalar("written", "Bool")], + LoweredOp::Callable { + module: "tools.makegen".to_string(), + kind: CallableKind::Func, + name: "makegen".to_string(), + }, + )); + dag.add_edge(Edge::new( + "tools.makegen::render_makefile", + "return", + "tools.makegen::makegen", + "registry", + )); + dag + } + + #[test] + fn emit_rust_bundle_generates_main_and_manifest_files() { + let dag = sample_dag(); + let artifacts = derive_artifacts(&dag).expect("derive should succeed"); + let bundle = emit_rust_bundle(&dag, &artifacts).expect("emit should succeed"); + + assert_eq!(bundle.backend, "rust"); + assert_eq!(bundle.files.len(), 2); + assert!(bundle + .files + .iter() + .any(|file| file.path.ends_with("main.rs") && file.content.contains("tools_makegen_makegen"))); + assert!(bundle + .files + .iter() + .any(|file| file.path.ends_with("progress_manifest.txt") && file.content.contains("total_nodes="))); + assert_eq!(bundle.summary.callable_count, 2); + } } diff --git a/core/daglang/daglang-lower/Cargo.toml b/core/daglang/daglang-lower/Cargo.toml index ecb56205d37..c4fd3bce385 100644 --- a/core/daglang/daglang-lower/Cargo.toml +++ b/core/daglang/daglang-lower/Cargo.toml @@ -9,3 +9,6 @@ description = "Lowers typed .dag AST to gunbc GraphIR (Dag/Node/Port/Edge)" daglang-syntax = { path = "../daglang-syntax" } daglang-typecheck = { path = "../daglang-typecheck" } gunbc-ir = { path = "../../ir" } + +[dev-dependencies] +daglang-resolve = { path = "../daglang-resolve" } diff --git a/core/daglang/daglang-lower/src/lib.rs b/core/daglang/daglang-lower/src/lib.rs index 32f55c70712..ea517e5298a 100644 --- a/core/daglang/daglang-lower/src/lib.rs +++ b/core/daglang/daglang-lower/src/lib.rs @@ -16,6 +16,32 @@ //! TypedAST → [daglang-lower] → GraphIR (gunbc Dag/Node/Port/Edge) //! ``` +use daglang_typecheck::{TypedCallableSignature, TypedItemSignature, TypedProject}; +use gunbc_ir::{Cardinality, Dag, Node, Port}; + +/// Lowered operation payload for daglang graph nodes. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum LoweredOp { + Callable { + module: String, + kind: CallableKind, + name: String, + }, + Pipeline { + module: String, + name: String, + stages: usize, + }, +} + +/// Kind of lowered callable declaration. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CallableKind { + Fn, + Func, + Pattern, +} + /// Errors during lowering. #[derive(Debug)] pub enum LowerError { @@ -27,4 +53,184 @@ pub enum LowerError { InvalidAcquireBlock { resource: String, reason: String }, /// Interface could not be resolved to a concrete resource. UnresolvedInterface { interface: String }, + /// No executable declarations were available for lowering. + NoLowerableItems, +} + +impl std::fmt::Display for LowerError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::UnknownPattern(name) => write!(f, "unknown pattern `{name}`"), + Self::MissingTransport { service, operation } => { + write!( + f, + "service `{service}` operation `{operation}` has no transport annotation" + ) + } + Self::InvalidAcquireBlock { resource, reason } => { + write!(f, "invalid acquire block for `{resource}`: {reason}") + } + Self::UnresolvedInterface { interface } => { + write!(f, "unresolved interface `{interface}`") + } + Self::NoLowerableItems => write!(f, "no callable or pipeline declarations to lower"), + } + } +} + +/// Lower a typed project into a structural GraphIR DAG. +/// +/// Phase-1 lowering focuses on callable and pipeline signatures: +/// - `fn` / `func` / `pattern` become opaque callable nodes +/// - `pipeline` declarations become opaque pipeline nodes +/// - type/service/resource/interface declarations remain metadata and are not +/// lowered into executable graph nodes yet. +pub fn lower_typed_project(project: &TypedProject) -> Result, LowerError> { + let mut dag = Dag::new(); + + for module in &project.modules { + let module_name = module.module_path.join("."); + for signature in &module.signatures { + match signature { + TypedItemSignature::Fn(callable) => { + dag.add_node(lower_callable(callable, &module_name, CallableKind::Fn)); + } + TypedItemSignature::Func(callable) => { + dag.add_node(lower_callable(callable, &module_name, CallableKind::Func)); + } + TypedItemSignature::Pattern(callable) => { + dag.add_node(lower_callable(callable, &module_name, CallableKind::Pattern)); + } + TypedItemSignature::Pipeline { name, stages } => { + let node_id = lowered_node_id(&module_name, name); + dag.add_node(Node::opaque( + node_id, + vec![], + vec![Port::with_cardinality( + "stages", + "Int", + Cardinality::ONE, + )], + LoweredOp::Pipeline { + module: module_name.clone(), + name: name.clone(), + stages: *stages, + }, + )); + } + TypedItemSignature::Type { .. } + | TypedItemSignature::Service { .. } + | TypedItemSignature::Resource { .. } + | TypedItemSignature::Interface { .. } => {} + } + } + } + + if dag.nodes.is_empty() { + return Err(LowerError::NoLowerableItems); + } + + Ok(dag) +} + +fn lower_callable( + callable: &TypedCallableSignature, + module_name: &str, + kind: CallableKind, +) -> Node { + let node_id = lowered_node_id(module_name, &callable.name); + let inputs = callable + .params + .iter() + .map(|binding| { + Port::with_cardinality(binding.name.as_str(), binding.ty.as_str(), Cardinality::ONE) + }) + .collect(); + let outputs = if callable.outputs.is_empty() { + vec![Port::with_cardinality("return", "Unit", Cardinality::ONE)] + } else { + callable + .outputs + .iter() + .map(|binding| { + Port::with_cardinality( + binding.name.as_str(), + binding.ty.as_str(), + Cardinality::ONE, + ) + }) + .collect() + }; + Node::opaque( + node_id, + inputs, + outputs, + LoweredOp::Callable { + module: module_name.to_string(), + kind, + name: callable.name.clone(), + }, + ) +} + +fn lowered_node_id(module_name: &str, item_name: &str) -> String { + format!("{module_name}::{item_name}").replace([' ', '/'], "_") +} + +#[cfg(test)] +mod tests { + use super::*; + use daglang_resolve::{ModuleGraph, ResolvedModule}; + use daglang_syntax::parser; + use daglang_typecheck::typecheck_module_graph; + use std::fs; + use std::path::{Path, PathBuf}; + + fn typed_project_from_sources(sources: &[(&str, &str)]) -> TypedProject { + let modules = sources + .iter() + .map(|(path, source)| { + let ast = parser::parse(source).expect("source should parse"); + let module_path = ast + .module_path + .as_ref() + .map(|module| module.node.segments.clone()) + .expect("module declaration is required"); + ResolvedModule { + path: PathBuf::from(path), + ast, + module_path, + dependencies: Vec::new(), + } + }) + .collect(); + typecheck_module_graph(ModuleGraph { modules }).expect("typecheck should succeed") + } + + #[test] + fn lower_makegen_produces_callable_nodes() { + let file = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../../dsl/tools/makegen.dag"); + let source = fs::read_to_string(file).expect("should read makegen source"); + let typed = typed_project_from_sources(&[("dsl/tools/makegen.dag", &source)]); + let dag = lower_typed_project(&typed).expect("lowering should succeed"); + + let node_ids = dag + .nodes + .iter() + .map(|node| node.id.0.as_str()) + .collect::>(); + assert!(node_ids.contains(&"tools.makegen::render_makefile")); + assert!(node_ids.contains(&"tools.makegen::makegen")); + } + + #[test] + fn lower_errors_when_no_callable_items_exist() { + let typed = typed_project_from_sources(&[( + "dsl/types_only.dag", + "module sample.types\ntype Name = String", + )]); + let error = lower_typed_project(&typed).expect_err("should fail without callable items"); + assert!(matches!(error, LowerError::NoLowerableItems)); + } } diff --git a/core/daglang/daglang-typecheck/src/lib.rs b/core/daglang/daglang-typecheck/src/lib.rs index 12fbd3b8cb9..bea0421acb9 100644 --- a/core/daglang/daglang-typecheck/src/lib.rs +++ b/core/daglang/daglang-typecheck/src/lib.rs @@ -20,6 +20,70 @@ //! - `@contract` annotation validation (behavioral specs are well-typed) //! - Subtyping via the bounded lattice (§4.1.4 of dsl-design.md) +use std::collections::HashSet; +use std::path::PathBuf; + +use daglang_resolve::{ModuleGraph, ResolvedModule}; +use daglang_syntax::ast::{Field, Item, Param, SourceFile, TypeExpr}; + +/// A typechecked project snapshot. +#[derive(Debug)] +pub struct TypedProject { + pub modules: Vec, +} + +/// A typechecked module. +#[derive(Debug)] +pub struct TypedModule { + pub path: PathBuf, + pub module_path: Vec, + pub imports: Vec>, + pub ast: SourceFile, + pub signatures: Vec, +} + +/// A normalized signature captured from a top-level item. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum TypedItemSignature { + Type { + name: String, + }, + Fn(TypedCallableSignature), + Func(TypedCallableSignature), + Pattern(TypedCallableSignature), + Service { + name: String, + operations: usize, + }, + Resource { + name: String, + implements: Option, + }, + Interface { + name: String, + capabilities: usize, + }, + Pipeline { + name: String, + stages: usize, + }, +} + +/// A normalized callable signature for fn/func/pattern items. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct TypedCallableSignature { + pub name: String, + pub params: Vec, + pub outputs: Vec, +} + +/// A single typed binding in a callable signature. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct TypedBinding { + pub name: String, + pub ty: String, +} + /// Errors during type checking. #[derive(Debug)] pub enum TypeError { @@ -39,4 +103,449 @@ pub enum TypeError { UnsatisfiableRefinement { ty: String, constraint: String }, /// Generic type parameter count mismatch. ArityMismatch { name: String, expected: usize, got: usize }, + /// Duplicate top-level item name in a module. + DuplicateDefinition { module: String, name: String }, + /// Duplicate parameter name in a callable signature. + DuplicateParameter { item: String, param: String }, + /// Duplicate output field name in a callable signature. + DuplicateOutputField { item: String, field: String }, +} + +impl std::fmt::Display for TypeError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::UndefinedType(name) => write!(f, "undefined type `{name}`"), + Self::NoSuchField { ty, field } => { + write!(f, "type `{ty}` has no field `{field}`") + } + Self::TypeMismatch { expected, got } => { + write!(f, "type mismatch: expected `{expected}`, got `{got}`") + } + Self::MissingCapability { + resource, + interface, + capability, + } => write!( + f, + "resource `{resource}` is missing capability `{capability}` for interface `{interface}`" + ), + Self::UnsatisfiableRefinement { ty, constraint } => { + write!(f, "unsatisfiable refinement on `{ty}`: {constraint}") + } + Self::ArityMismatch { + name, + expected, + got, + } => write!( + f, + "generic arity mismatch for `{name}`: expected {expected}, got {got}" + ), + Self::DuplicateDefinition { module, name } => { + write!(f, "duplicate definition `{name}` in module `{module}`") + } + Self::DuplicateParameter { item, param } => { + write!(f, "duplicate parameter `{param}` in `{item}`") + } + Self::DuplicateOutputField { item, field } => { + write!(f, "duplicate output field `{field}` in `{item}`") + } + } + } +} + +/// Typecheck a discovered module graph and produce typed module signatures. +pub fn typecheck_module_graph(graph: ModuleGraph) -> Result> { + let known_types = collect_known_types(&graph.modules); + let mut errors = Vec::new(); + let mut typed_modules = Vec::with_capacity(graph.modules.len()); + + for module in graph.modules { + let imports = module + .ast + .imports + .iter() + .map(|import| import.node.path.segments.clone()) + .collect::>(); + let module_name = module.module_path.join("."); + let signatures = collect_signatures(&module, &known_types, &module_name, &mut errors); + typed_modules.push(TypedModule { + path: module.path, + module_path: module.module_path, + imports, + ast: module.ast, + signatures, + }); + } + + if errors.is_empty() { + Ok(TypedProject { + modules: typed_modules, + }) + } else { + Err(errors) + } +} + +fn collect_signatures( + module: &ResolvedModule, + known_types: &HashSet, + module_name: &str, + errors: &mut Vec, +) -> Vec { + let mut module_known_types = known_types.clone(); + for import in &module.ast.imports { + if let Some(bindings) = &import.node.bindings { + for binding in bindings { + module_known_types.insert(binding.clone()); + } + } + } + + let mut seen_items = HashSet::new(); + let mut signatures = Vec::new(); + + for item in &module.ast.items { + match &item.node { + Item::TypeDef(def) => { + if !seen_items.insert(def.name.clone()) { + errors.push(TypeError::DuplicateDefinition { + module: module_name.to_string(), + name: def.name.clone(), + }); + } + signatures.push(TypedItemSignature::Type { + name: def.name.clone(), + }); + } + Item::FnDef(def) => { + record_duplicate_item_name(module_name, &def.name, &mut seen_items, errors); + validate_params(&def.name, &def.params, &module_known_types, errors); + validate_type_expr( + &def.return_type, + &module_known_types, + &format!("{}.return", def.name), + errors, + ); + let outputs = vec![TypedBinding { + name: "return".to_string(), + ty: type_expr_to_string(&def.return_type), + }]; + signatures.push(TypedItemSignature::Fn(TypedCallableSignature { + name: def.name.clone(), + params: def + .params + .iter() + .map(|param| TypedBinding { + name: param.name.clone(), + ty: type_expr_to_string(¶m.ty), + }) + .collect(), + outputs, + })); + } + Item::FuncDef(def) => { + record_duplicate_item_name(module_name, &def.name, &mut seen_items, errors); + validate_params(&def.name, &def.params, &module_known_types, errors); + validate_outputs(&def.name, &def.outputs, &module_known_types, errors); + signatures.push(TypedItemSignature::Func(TypedCallableSignature { + name: def.name.clone(), + params: def + .params + .iter() + .map(|param| TypedBinding { + name: param.name.clone(), + ty: type_expr_to_string(¶m.ty), + }) + .collect(), + outputs: def + .outputs + .iter() + .map(|field| TypedBinding { + name: field.name.clone(), + ty: type_expr_to_string(&field.ty), + }) + .collect(), + })); + } + Item::PatternDef(def) => { + record_duplicate_item_name(module_name, &def.name, &mut seen_items, errors); + validate_params(&def.name, &def.params, &module_known_types, errors); + validate_outputs(&def.name, &def.outputs, &module_known_types, errors); + signatures.push(TypedItemSignature::Pattern(TypedCallableSignature { + name: def.name.clone(), + params: def + .params + .iter() + .map(|param| TypedBinding { + name: param.name.clone(), + ty: type_expr_to_string(¶m.ty), + }) + .collect(), + outputs: def + .outputs + .iter() + .map(|field| TypedBinding { + name: field.name.clone(), + ty: type_expr_to_string(&field.ty), + }) + .collect(), + })); + } + Item::ServiceDef(def) => { + record_duplicate_item_name(module_name, &def.name, &mut seen_items, errors); + signatures.push(TypedItemSignature::Service { + name: def.name.clone(), + operations: def.operations.len(), + }); + } + Item::ResourceDef(def) => { + record_duplicate_item_name(module_name, &def.name, &mut seen_items, errors); + signatures.push(TypedItemSignature::Resource { + name: def.name.clone(), + implements: def.implements.clone(), + }); + } + Item::InterfaceDef(def) => { + record_duplicate_item_name(module_name, &def.name, &mut seen_items, errors); + signatures.push(TypedItemSignature::Interface { + name: def.name.clone(), + capabilities: def.capabilities.len(), + }); + } + Item::PipelineDef(def) => { + record_duplicate_item_name(module_name, &def.name, &mut seen_items, errors); + signatures.push(TypedItemSignature::Pipeline { + name: def.name.clone(), + stages: def.stages.len(), + }); + } + } + } + + signatures +} + +fn collect_known_types(modules: &[ResolvedModule]) -> HashSet { + let mut known = builtin_type_names(); + for module in modules { + let module_prefix = module.module_path.join("."); + for item in &module.ast.items { + if let Item::TypeDef(def) = &item.node { + known.insert(def.name.clone()); + known.insert(format!("{module_prefix}.{}", def.name)); + } + } + } + known +} + +fn builtin_type_names() -> HashSet { + HashSet::from([ + "Any".to_string(), + "Unit".to_string(), + "Bool".to_string(), + "Int".to_string(), + "Float".to_string(), + "String".to_string(), + "Bytes".to_string(), + "Json".to_string(), + "Record".to_string(), + "List".to_string(), + "Map".to_string(), + "Option".to_string(), + "Result".to_string(), + "Queue".to_string(), + "Self".to_string(), + ]) +} + +fn record_duplicate_item_name( + module_name: &str, + item_name: &str, + seen_items: &mut HashSet, + errors: &mut Vec, +) { + if !seen_items.insert(item_name.to_string()) { + errors.push(TypeError::DuplicateDefinition { + module: module_name.to_string(), + name: item_name.to_string(), + }); + } +} + +fn validate_params( + item_name: &str, + params: &[Param], + known_types: &HashSet, + errors: &mut Vec, +) { + let mut seen = HashSet::new(); + for param in params { + if !seen.insert(param.name.clone()) { + errors.push(TypeError::DuplicateParameter { + item: item_name.to_string(), + param: param.name.clone(), + }); + } + validate_type_expr( + ¶m.ty, + known_types, + &format!("{}.{}", item_name, param.name), + errors, + ); + } +} + +fn validate_outputs( + item_name: &str, + outputs: &[Field], + known_types: &HashSet, + errors: &mut Vec, +) { + let mut seen = HashSet::new(); + for output in outputs { + if !seen.insert(output.name.clone()) { + errors.push(TypeError::DuplicateOutputField { + item: item_name.to_string(), + field: output.name.clone(), + }); + } + validate_type_expr( + &output.ty, + known_types, + &format!("{}.{}", item_name, output.name), + errors, + ); + } +} + +fn validate_type_expr( + ty: &TypeExpr, + known_types: &HashSet, + context: &str, + errors: &mut Vec, +) { + match ty { + TypeExpr::Named(name) => { + if should_validate_named_type(name) && !known_types.contains(name) { + let tail = name.rsplit('.').next().unwrap_or(name); + if !known_types.contains(tail) { + errors.push(TypeError::UndefinedType(format!("{name} (in {context})"))); + } + } + } + TypeExpr::Generic(name, args) => { + if should_validate_named_type(name) && !known_types.contains(name) { + let tail = name.rsplit('.').next().unwrap_or(name); + if !known_types.contains(tail) { + errors.push(TypeError::UndefinedType(format!("{name} (in {context})"))); + } + } + for arg in args { + validate_type_expr(arg, known_types, context, errors); + } + } + TypeExpr::Optional(inner) => validate_type_expr(inner, known_types, context, errors), + TypeExpr::Annotated(inner, _) => validate_type_expr(inner, known_types, context, errors), + } +} + +fn should_validate_named_type(name: &str) -> bool { + name.chars() + .all(|ch| ch.is_alphanumeric() || matches!(ch, '_' | '.')) +} + +fn type_expr_to_string(expr: &TypeExpr) -> String { + match expr { + TypeExpr::Named(name) => name.clone(), + TypeExpr::Generic(name, args) => format!( + "{name}<{}>", + args.iter() + .map(type_expr_to_string) + .collect::>() + .join(", ") + ), + TypeExpr::Optional(inner) => format!("{}?", type_expr_to_string(inner)), + TypeExpr::Annotated(inner, annotations) => format!( + "{} {}", + type_expr_to_string(inner), + annotations + .iter() + .map(|annotation| format!("@{}", annotation.name)) + .collect::>() + .join(" ") + ), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::fs; + use std::path::Path; + + fn module_graph_from_sources(sources: &[(&str, &str)]) -> ModuleGraph { + let modules = sources + .iter() + .map(|(path, source)| { + let ast = daglang_syntax::parser::parse(source).expect("source should parse"); + let module_path = ast + .module_path + .as_ref() + .map(|module| module.node.segments.clone()) + .expect("module declarations are required in tests"); + ResolvedModule { + path: PathBuf::from(path), + ast, + module_path, + dependencies: Vec::new(), + } + }) + .collect(); + ModuleGraph { modules } + } + + #[test] + fn typecheck_accepts_makegen_module() { + let file = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../../dsl/tools/makegen.dag"); + let source = fs::read_to_string(file).expect("should read makegen source"); + let graph = module_graph_from_sources(&[("dsl/tools/makegen.dag", &source)]); + let typed = typecheck_module_graph(graph).expect("makegen should typecheck"); + + assert_eq!(typed.modules.len(), 1); + assert_eq!(typed.modules[0].module_path.join("."), "tools.makegen"); + assert!(typed.modules[0] + .signatures + .iter() + .any(|signature| matches!(signature, TypedItemSignature::Fn(_)))); + assert!(typed.modules[0] + .signatures + .iter() + .any(|signature| matches!(signature, TypedItemSignature::Func(_)))); + } + + #[test] + fn duplicate_param_names_are_reported() { + let graph = module_graph_from_sources(&[( + "dup_params.dag", + "module sample.dup\nfn bad(a: String, a: Int) -> String { a }", + )]); + let errors = typecheck_module_graph(graph).expect_err("duplicate params should fail"); + assert!(errors + .iter() + .any(|error| matches!(error, TypeError::DuplicateParameter { item, param } if item == "bad" && param == "a"))); + } + + #[test] + fn undefined_types_are_reported() { + let graph = module_graph_from_sources(&[( + "unknown_type.dag", + "module sample.unknown\nfn run(input: MissingType) -> String { \"ok\" }", + )]); + let errors = typecheck_module_graph(graph).expect_err("unknown type should fail"); + assert!(errors + .iter() + .any(|error| matches!(error, TypeError::UndefinedType(msg) if msg.contains("MissingType")))); + } } From 5bfe97a4a71fbc3dd80d15181061cbdea5e33970 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 06:00:36 +0000 Subject: [PATCH 003/169] Add configurable import strictness to typecheck and refine compile discovery Co-authored-by: Brian Searls --- core/daglang/daglang-cli/src/compile.rs | 10 +++- core/daglang/daglang-typecheck/src/lib.rs | 60 +++++++++++++++++++++++ 2 files changed, 68 insertions(+), 2 deletions(-) diff --git a/core/daglang/daglang-cli/src/compile.rs b/core/daglang/daglang-cli/src/compile.rs index b4af181a5bf..2a7e377f7a5 100644 --- a/core/daglang/daglang-cli/src/compile.rs +++ b/core/daglang/daglang-cli/src/compile.rs @@ -6,7 +6,7 @@ use daglang_emit::{emit_rust_bundle, EmissionBundle}; use daglang_lower::{lower_typed_project, LoweredOp}; use daglang_resolve::{ModuleGraph, ResolveError, ResolvedModule}; use daglang_syntax::parser; -use daglang_typecheck::typecheck_module_graph; +use daglang_typecheck::{typecheck_module_graph_with_options, TypecheckOptions}; use gunbc_ir::Dag; #[derive(Debug)] @@ -45,7 +45,13 @@ pub fn resolve_default_root() -> PathBuf { pub fn compile_from_context(context: &PipelineContext) -> Result { let module_graph = discover_module_graph_for_context(context)?; - let typed = typecheck_module_graph(module_graph).map_err(|errors| { + let typed = typecheck_module_graph_with_options( + module_graph, + TypecheckOptions { + allow_unresolved_imports: context.target_file.is_some(), + }, + ) + .map_err(|errors| { let mut message = String::from("typecheck errors:\n"); for error in errors { message.push_str(&format!(" {error}\n")); diff --git a/core/daglang/daglang-typecheck/src/lib.rs b/core/daglang/daglang-typecheck/src/lib.rs index bea0421acb9..fbc830f1a4f 100644 --- a/core/daglang/daglang-typecheck/src/lib.rs +++ b/core/daglang/daglang-typecheck/src/lib.rs @@ -32,6 +32,19 @@ pub struct TypedProject { pub modules: Vec, } +#[derive(Debug, Clone, Copy)] +pub struct TypecheckOptions { + pub allow_unresolved_imports: bool, +} + +impl Default for TypecheckOptions { + fn default() -> Self { + Self { + allow_unresolved_imports: true, + } + } +} + /// A typechecked module. #[derive(Debug)] pub struct TypedModule { @@ -109,6 +122,8 @@ pub enum TypeError { DuplicateParameter { item: String, param: String }, /// Duplicate output field name in a callable signature. DuplicateOutputField { item: String, field: String }, + /// Import target does not exist in the available module graph. + UnresolvedImport { module: String, target: String }, } impl std::fmt::Display for TypeError { @@ -149,13 +164,29 @@ impl std::fmt::Display for TypeError { Self::DuplicateOutputField { item, field } => { write!(f, "duplicate output field `{field}` in `{item}`") } + Self::UnresolvedImport { module, target } => { + write!(f, "unresolved import `{target}` in module `{module}`") + } } } } /// Typecheck a discovered module graph and produce typed module signatures. pub fn typecheck_module_graph(graph: ModuleGraph) -> Result> { + typecheck_module_graph_with_options(graph, TypecheckOptions::default()) +} + +/// Typecheck a discovered module graph with explicit options. +pub fn typecheck_module_graph_with_options( + graph: ModuleGraph, + options: TypecheckOptions, +) -> Result> { let known_types = collect_known_types(&graph.modules); + let available_modules = graph + .modules + .iter() + .map(|module| module.module_path.join(".")) + .collect::>(); let mut errors = Vec::new(); let mut typed_modules = Vec::with_capacity(graph.modules.len()); @@ -167,6 +198,17 @@ pub fn typecheck_module_graph(graph: ModuleGraph) -> Result>(); let module_name = module.module_path.join("."); + if !options.allow_unresolved_imports { + for import in &imports { + let target = import.join("."); + if !available_modules.contains(&target) { + errors.push(TypeError::UnresolvedImport { + module: module_name.clone(), + target, + }); + } + } + } let signatures = collect_signatures(&module, &known_types, &module_name, &mut errors); typed_modules.push(TypedModule { path: module.path, @@ -548,4 +590,22 @@ mod tests { .iter() .any(|error| matches!(error, TypeError::UndefinedType(msg) if msg.contains("MissingType")))); } + + #[test] + fn strict_mode_reports_unresolved_imports() { + let graph = module_graph_from_sources(&[( + "missing_import.dag", + "module sample.main\nimport missing.dep\nfn run() -> Unit {}", + )]); + let options = TypecheckOptions { + allow_unresolved_imports: false, + }; + let errors = typecheck_module_graph_with_options(graph, options) + .expect_err("strict mode should fail on unresolved import"); + assert!(errors.iter().any(|error| matches!( + error, + TypeError::UnresolvedImport { module, target } + if module == "sample.main" && target == "missing.dep" + ))); + } } From eb4242a9f59ef15397aff2df7f8d8a3d8b99b168 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 06:04:45 +0000 Subject: [PATCH 004/169] Add lowering topology parity report and strict compile unresolved-import regression Co-authored-by: Brian Searls --- .../daglang-cli/tests/compile_commands.rs | 40 ++++ core/daglang/daglang-lower/src/lib.rs | 178 +++++++++++++++++- 2 files changed, 217 insertions(+), 1 deletion(-) diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index 59ee1352acd..521ab258b26 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -26,6 +26,18 @@ fn unique_temp_file(name: &str) -> PathBuf { )) } +fn unique_temp_dir(name: &str) -> PathBuf { + let nanos = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("system clock should be after unix epoch") + .as_nanos(); + std::env::temp_dir().join(format!( + "daglang_cli_compile_cmd_dir_{name}_{}_{}", + std::process::id(), + nanos + )) +} + #[test] fn compile_command_emits_summary_for_single_file() { let output = Command::new(daglang_bin()) @@ -124,3 +136,31 @@ fn compile_command_reports_diagnostics_for_invalid_file() { std::fs::remove_file(broken).expect("failed to remove temp broken source"); } + +#[test] +fn compile_command_directory_mode_fails_on_unresolved_imports() { + let root = unique_temp_dir("unresolved_import"); + std::fs::create_dir_all(&root).expect("failed to create temp dir"); + std::fs::write( + root.join("main.dag"), + "module sample.main\nimport missing.dep\nfn run() -> Unit {}", + ) + .expect("failed to write source"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&root) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile on directory"); + + assert!( + !output.status.success(), + "directory compile should fail on unresolved imports" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("typecheck errors")); + assert!(stderr.contains("unresolved import")); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); +} diff --git a/core/daglang/daglang-lower/src/lib.rs b/core/daglang/daglang-lower/src/lib.rs index ea517e5298a..1be963d7f35 100644 --- a/core/daglang/daglang-lower/src/lib.rs +++ b/core/daglang/daglang-lower/src/lib.rs @@ -17,7 +17,7 @@ //! ``` use daglang_typecheck::{TypedCallableSignature, TypedItemSignature, TypedProject}; -use gunbc_ir::{Cardinality, Dag, Node, Port}; +use gunbc_ir::{diff_topologies, Cardinality, Dag, Node, Port}; /// Lowered operation payload for daglang graph nodes. #[derive(Debug, Clone, PartialEq, Eq)] @@ -34,6 +34,30 @@ pub enum LoweredOp { }, } +/// Structural parity report between two DAGs. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ParityReport { + pub candidate_nodes: usize, + pub reference_nodes: usize, + pub candidate_edges: usize, + pub reference_edges: usize, + pub added_nodes: usize, + pub removed_nodes: usize, + pub changed_nodes: usize, + pub added_edges: usize, + pub removed_edges: usize, +} + +impl ParityReport { + pub fn is_exact_match(&self) -> bool { + self.added_nodes == 0 + && self.removed_nodes == 0 + && self.changed_nodes == 0 + && self.added_edges == 0 + && self.removed_edges == 0 + } +} + /// Kind of lowered callable declaration. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum CallableKind { @@ -133,6 +157,30 @@ pub fn lower_typed_project(project: &TypedProject) -> Result, Low Ok(dag) } +/// Compare a lowered daglang graph against a reference graph topology. +/// +/// This enables incremental parity harness adoption: +/// - exact parity: `report.is_exact_match() == true` +/// - scaffold mode: report still gives deterministic deltas while lowering +/// coverage grows. +pub fn compare_topology(candidate: &Dag, reference: &Dag) -> ParityReport { + let candidate_topology = candidate.topology(); + let reference_topology = reference.topology(); + let diff = diff_topologies(&reference_topology, &candidate_topology); + + ParityReport { + candidate_nodes: candidate_topology.nodes.len(), + reference_nodes: reference_topology.nodes.len(), + candidate_edges: candidate_topology.edges.len(), + reference_edges: reference_topology.edges.len(), + added_nodes: diff.added_nodes.len(), + removed_nodes: diff.removed_nodes.len(), + changed_nodes: diff.changed_nodes.len(), + added_edges: diff.added_edges.len(), + removed_edges: diff.removed_edges.len(), + } +} + fn lower_callable( callable: &TypedCallableSignature, module_name: &str, @@ -183,6 +231,7 @@ mod tests { use daglang_resolve::{ModuleGraph, ResolvedModule}; use daglang_syntax::parser; use daglang_typecheck::typecheck_module_graph; + use gunbc_ir::{Edge, Port}; use std::fs; use std::path::{Path, PathBuf}; @@ -233,4 +282,131 @@ mod tests { let error = lower_typed_project(&typed).expect_err("should fail without callable items"); assert!(matches!(error, LowerError::NoLowerableItems)); } + + #[test] + fn makegen_parity_report_is_deterministic() { + let file = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../../dsl/tools/makegen.dag"); + let source = fs::read_to_string(file).expect("should read makegen source"); + let typed = typed_project_from_sources(&[("dsl/tools/makegen.dag", &source)]); + let lowered = lower_typed_project(&typed).expect("lowering should succeed"); + let reference = reference_makegen_shape(); + + let report_a = compare_topology(&lowered, &reference); + let report_b = compare_topology(&lowered, &reference); + assert_eq!(report_a, report_b, "parity reports must be deterministic"); + assert!( + !report_a.is_exact_match(), + "phase-1 scaffold should still report makegen parity deltas" + ); + assert!( + report_a.reference_nodes > report_a.candidate_nodes, + "reference graph should currently be richer than scaffold lowering" + ); + } + + fn reference_makegen_shape() -> Dag<()> { + let mut dag = Dag::new(); + dag.add_node(Node::opaque( + "fs_env", + vec![], + vec![Port::scalar("FilesystemHandle", "FilesystemHandle")], + (), + )); + dag.add_node(Node::opaque( + "load_registry", + vec![], + vec![Port::scalar("registry", "ToolRegistry")], + (), + )); + dag.add_node(Node::opaque( + "render_makefile", + vec![Port::scalar("registry", "ToolRegistry")], + vec![Port::scalar("makefile_content", "String")], + (), + )); + dag.add_node(Node::opaque( + "prepare_read_makegen", + vec![ + Port::scalar("path", "String"), + Port::scalar("res:file:Makefile", "FilesystemHandle"), + ], + vec![Port::scalar("request", "TransportRequest")], + (), + )); + dag.add_node(Node::opaque( + "execute_read_makegen", + vec![Port::scalar("request", "TransportRequest")], + vec![Port::scalar("response", "TransportResponse")], + (), + )); + dag.add_node(Node::opaque( + "compare_makegen_content", + vec![ + Port::scalar("expected_content", "String"), + Port::scalar("response", "TransportResponse"), + ], + vec![Port::scalar("fresh", "Bool"), Port::scalar("skip", "Bool")], + (), + )); + dag.add_node(Node::opaque( + "prepare_write_makegen", + vec![Port::scalar("content", "String"), Port::scalar("path", "String")], + vec![Port::scalar("request", "TransportRequest")], + (), + )); + dag.add_node(Node::opaque( + "execute_makegen_transport", + vec![ + Port::scalar("request", "TransportRequest"), + Port::scalar("skip", "Bool"), + ], + vec![Port::scalar("makegen_response", "TransportResponse")], + (), + )); + + dag.add_edge(Edge::new( + "load_registry", + "registry", + "render_makefile", + "registry", + )); + dag.add_edge(Edge::new( + "render_makefile", + "makefile_content", + "compare_makegen_content", + "expected_content", + )); + dag.add_edge(Edge::new( + "render_makefile", + "makefile_content", + "prepare_write_makegen", + "content", + )); + dag.add_edge(Edge::new( + "prepare_read_makegen", + "request", + "execute_read_makegen", + "request", + )); + dag.add_edge(Edge::new( + "execute_read_makegen", + "response", + "compare_makegen_content", + "response", + )); + dag.add_edge(Edge::new( + "prepare_write_makegen", + "request", + "execute_makegen_transport", + "request", + )); + dag.add_edge(Edge::new( + "compare_makegen_content", + "skip", + "execute_makegen_transport", + "skip", + )); + dag + } } From a738e022080096bddad51b74c5faf9c18a603131 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 06:06:05 +0000 Subject: [PATCH 005/169] Validate module declaration path consistency in directory compile mode Co-authored-by: Brian Searls --- core/daglang/daglang-cli/src/compile.rs | 77 +++++++++++++++++++ .../daglang-cli/tests/compile_commands.rs | 32 +++++++- 2 files changed, 107 insertions(+), 2 deletions(-) diff --git a/core/daglang/daglang-cli/src/compile.rs b/core/daglang/daglang-cli/src/compile.rs index 2a7e377f7a5..727aa054e45 100644 --- a/core/daglang/daglang-cli/src/compile.rs +++ b/core/daglang/daglang-cli/src/compile.rs @@ -45,6 +45,9 @@ pub fn resolve_default_root() -> PathBuf { pub fn compile_from_context(context: &PipelineContext) -> Result { let module_graph = discover_module_graph_for_context(context)?; + if context.target_file.is_none() { + validate_module_path_consistency(&module_graph, &context.roots)?; + } let typed = typecheck_module_graph_with_options( module_graph, TypecheckOptions { @@ -123,6 +126,52 @@ fn format_resolve_error(error: ResolveError) -> String { } } +fn validate_module_path_consistency( + graph: &ModuleGraph, + roots: &[PathBuf], +) -> Result<(), String> { + let mismatches = graph + .modules + .iter() + .filter_map(|module| { + let derived = derive_module_path(&module.path, roots)?; + (derived != module.module_path).then_some(( + module.path.clone(), + module.module_path.join("."), + derived.join("."), + )) + }) + .collect::>(); + + if mismatches.is_empty() { + return Ok(()); + } + + let mut message = String::from("module path mismatches:\n"); + for (path, declared, derived) in mismatches { + message.push_str(&format!( + " {}: declared `{declared}` but filesystem implies `{derived}`\n", + path.display() + )); + } + Err(message) +} + +fn derive_module_path(path: &std::path::Path, roots: &[PathBuf]) -> Option> { + for root in roots { + if let Ok(relative) = path.strip_prefix(root) { + return Some( + relative + .with_extension("") + .components() + .filter_map(|segment| segment.as_os_str().to_str().map(String::from)) + .collect(), + ); + } + } + None +} + pub fn render_expand(dag: &Dag) -> String { let mut out = String::new(); out.push_str("Nodes:\n"); @@ -256,4 +305,32 @@ mod tests { std::fs::remove_file(broken_file).expect("failed to cleanup broken source"); } + + #[test] + fn compile_directory_reports_module_path_mismatch() { + let root = std::env::temp_dir().join(format!( + "daglang_compile_mismatch_{}_{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("system clock should be after unix epoch") + .as_nanos() + )); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + std::fs::write( + root.join("main.dag"), + "module mismatch.main\nfn run() -> Unit {}", + ) + .expect("failed to write source"); + + let context = PipelineContext { + roots: vec![root.clone()], + target_file: None, + }; + let error = compile_from_context(&context).expect_err("compile should fail"); + assert!(error.contains("module path mismatches")); + assert!(error.contains("main")); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); + } } diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index 521ab258b26..3cd0eed32f6 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -140,9 +140,9 @@ fn compile_command_reports_diagnostics_for_invalid_file() { #[test] fn compile_command_directory_mode_fails_on_unresolved_imports() { let root = unique_temp_dir("unresolved_import"); - std::fs::create_dir_all(&root).expect("failed to create temp dir"); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp dir"); std::fs::write( - root.join("main.dag"), + root.join("sample/main.dag"), "module sample.main\nimport missing.dep\nfn run() -> Unit {}", ) .expect("failed to write source"); @@ -164,3 +164,31 @@ fn compile_command_directory_mode_fails_on_unresolved_imports() { std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); } + +#[test] +fn compile_command_directory_mode_reports_module_path_mismatch() { + let root = unique_temp_dir("path_mismatch"); + std::fs::create_dir_all(&root).expect("failed to create temp dir"); + std::fs::write( + root.join("main.dag"), + "module wrong.name\nfn run() -> Unit {}", + ) + .expect("failed to write source"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&root) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile on mismatch directory"); + + assert!( + !output.status.success(), + "directory compile should fail on module path mismatch" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("module path mismatches")); + assert!(stderr.contains("declared `wrong.name`")); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); +} From 4fdba93b6a36d60fef9877d1321024e08930fca2 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 06:12:11 +0000 Subject: [PATCH 006/169] Capture callable bodies and derive call dependency edges Co-authored-by: Brian Searls --- core/daglang/daglang-lower/src/lib.rs | 271 +++++++++++++++++- core/daglang/daglang-syntax/src/parser.rs | 52 +++- .../tests/representative_ast.rs | 26 ++ 3 files changed, 322 insertions(+), 27 deletions(-) diff --git a/core/daglang/daglang-lower/src/lib.rs b/core/daglang/daglang-lower/src/lib.rs index 1be963d7f35..f3233a50bd3 100644 --- a/core/daglang/daglang-lower/src/lib.rs +++ b/core/daglang/daglang-lower/src/lib.rs @@ -16,8 +16,11 @@ //! TypedAST → [daglang-lower] → GraphIR (gunbc Dag/Node/Port/Edge) //! ``` +use std::collections::{HashMap, HashSet}; + +use daglang_syntax::ast::{Expr, Item, Stmt, StringPart}; use daglang_typecheck::{TypedCallableSignature, TypedItemSignature, TypedProject}; -use gunbc_ir::{diff_topologies, Cardinality, Dag, Node, Port}; +use gunbc_ir::{diff_topologies, Cardinality, Dag, Edge, Node, Port}; /// Lowered operation payload for daglang graph nodes. #[derive(Debug, Clone, PartialEq, Eq)] @@ -66,6 +69,12 @@ pub enum CallableKind { Pattern, } +#[derive(Debug, Clone, PartialEq, Eq)] +struct LoweredEndpoint { + node_id: String, + primary_output: String, +} + /// Errors during lowering. #[derive(Debug)] pub enum LowerError { @@ -111,19 +120,47 @@ impl std::fmt::Display for LowerError { /// lowered into executable graph nodes yet. pub fn lower_typed_project(project: &TypedProject) -> Result, LowerError> { let mut dag = Dag::new(); + let mut endpoints_by_full = HashMap::<(String, String), LoweredEndpoint>::new(); + let mut endpoints_by_name = HashMap::>::new(); for module in &project.modules { let module_name = module.module_path.join("."); for signature in &module.signatures { match signature { TypedItemSignature::Fn(callable) => { - dag.add_node(lower_callable(callable, &module_name, CallableKind::Fn)); + let (node, endpoint) = lower_callable(callable, &module_name, CallableKind::Fn); + register_endpoint( + &mut endpoints_by_full, + &mut endpoints_by_name, + &module_name, + &callable.name, + endpoint, + ); + dag.add_node(node); } TypedItemSignature::Func(callable) => { - dag.add_node(lower_callable(callable, &module_name, CallableKind::Func)); + let (node, endpoint) = + lower_callable(callable, &module_name, CallableKind::Func); + register_endpoint( + &mut endpoints_by_full, + &mut endpoints_by_name, + &module_name, + &callable.name, + endpoint, + ); + dag.add_node(node); } TypedItemSignature::Pattern(callable) => { - dag.add_node(lower_callable(callable, &module_name, CallableKind::Pattern)); + let (node, endpoint) = + lower_callable(callable, &module_name, CallableKind::Pattern); + register_endpoint( + &mut endpoints_by_full, + &mut endpoints_by_name, + &module_name, + &callable.name, + endpoint, + ); + dag.add_node(node); } TypedItemSignature::Pipeline { name, stages } => { let node_id = lowered_node_id(&module_name, name); @@ -154,6 +191,8 @@ pub fn lower_typed_project(project: &TypedProject) -> Result, Low return Err(LowerError::NoLowerableItems); } + add_dependency_edges(&mut dag, project, &endpoints_by_full, &endpoints_by_name); + Ok(dag) } @@ -185,15 +224,20 @@ fn lower_callable( callable: &TypedCallableSignature, module_name: &str, kind: CallableKind, -) -> Node { +) -> (Node, LoweredEndpoint) { let node_id = lowered_node_id(module_name, &callable.name); - let inputs = callable + let mut inputs = callable .params .iter() .map(|binding| { Port::with_cardinality(binding.name.as_str(), binding.ty.as_str(), Cardinality::ONE) }) - .collect(); + .collect::>(); + inputs.push(Port::with_cardinality( + "__deps", + "Any", + Cardinality::ZERO_OR_MORE, + )); let outputs = if callable.outputs.is_empty() { vec![Port::with_cardinality("return", "Unit", Cardinality::ONE)] } else { @@ -209,14 +253,24 @@ fn lower_callable( }) .collect() }; - Node::opaque( - node_id, - inputs, - outputs, - LoweredOp::Callable { - module: module_name.to_string(), - kind, - name: callable.name.clone(), + let primary_output = outputs + .first() + .map(|port| port.name.0.clone()) + .unwrap_or_else(|| "return".to_string()); + ( + Node::opaque( + node_id.clone(), + inputs, + outputs, + LoweredOp::Callable { + module: module_name.to_string(), + kind, + name: callable.name.clone(), + }, + ), + LoweredEndpoint { + node_id, + primary_output, }, ) } @@ -225,6 +279,188 @@ fn lowered_node_id(module_name: &str, item_name: &str) -> String { format!("{module_name}::{item_name}").replace([' ', '/'], "_") } +fn register_endpoint( + by_full: &mut HashMap<(String, String), LoweredEndpoint>, + by_name: &mut HashMap>, + module_name: &str, + callable_name: &str, + endpoint: LoweredEndpoint, +) { + by_full.insert( + (module_name.to_string(), callable_name.to_string()), + endpoint.clone(), + ); + by_name + .entry(callable_name.to_string()) + .and_modify(|existing| { + if let Some(current) = existing { + if current != &endpoint { + *existing = None; + } + } + }) + .or_insert(Some(endpoint)); +} + +fn add_dependency_edges( + dag: &mut Dag, + project: &TypedProject, + endpoints_by_full: &HashMap<(String, String), LoweredEndpoint>, + endpoints_by_name: &HashMap>, +) { + let mut seen_edges = HashSet::<(String, String, String, String)>::new(); + + for module in &project.modules { + let module_name = module.module_path.join("."); + for item in &module.ast.items { + let Some((item_name, stmts)) = item_callable_body(&item.node) else { + continue; + }; + let Some(target) = + endpoints_by_full.get(&(module_name.clone(), item_name.to_string())) + else { + continue; + }; + + let mut calls = HashSet::new(); + collect_calls_from_stmts(stmts, &mut calls); + for call in calls { + let Some(Some(source)) = endpoints_by_name.get(&call) else { + continue; + }; + if source.node_id == target.node_id { + continue; + } + let key = ( + source.node_id.clone(), + source.primary_output.clone(), + target.node_id.clone(), + "__deps".to_string(), + ); + if seen_edges.insert(key.clone()) { + dag.add_edge(Edge::new( + key.0.clone(), + key.1.clone(), + key.2.clone(), + key.3.clone(), + )); + } + } + } + } +} + +fn item_callable_body(item: &Item) -> Option<(&str, &[Stmt])> { + match item { + Item::FnDef(def) => Some((def.name.as_str(), def.body.stmts.as_slice())), + Item::FuncDef(def) => Some((def.name.as_str(), def.body.stmts.as_slice())), + Item::PatternDef(def) => Some((def.name.as_str(), def.body.stmts.as_slice())), + _ => None, + } +} + +fn collect_calls_from_stmts(stmts: &[Stmt], calls: &mut HashSet) { + for stmt in stmts { + match stmt { + Stmt::Let(_, expr) | Stmt::Assign(_, expr) | Stmt::Expr(expr) => { + collect_calls_from_expr(expr, calls); + } + Stmt::Return(fields) => { + for (_, expr) in fields { + collect_calls_from_expr(expr, calls); + } + } + } + } +} + +fn collect_calls_from_expr(expr: &Expr, calls: &mut HashSet) { + match expr { + Expr::Call(name, args) => { + if should_track_call(name) { + calls.insert(name.clone()); + } + for (_, arg) in args { + collect_calls_from_expr(arg, calls); + } + } + Expr::ServiceCall(_, args) => { + for (_, arg) in args { + collect_calls_from_expr(arg, calls); + } + } + Expr::FieldAccess(base, _) => collect_calls_from_expr(base, calls), + Expr::BinOp(lhs, _, rhs) => { + collect_calls_from_expr(lhs, calls); + collect_calls_from_expr(rhs, calls); + } + Expr::UnaryOp(_, inner) => collect_calls_from_expr(inner, calls), + Expr::StringInterp(parts) => { + for part in parts { + if let StringPart::Expr(inner) = part { + collect_calls_from_expr(inner, calls); + } + } + } + Expr::Record(_, fields) => { + for (_, value) in fields { + collect_calls_from_expr(value, calls); + } + } + Expr::Match(scrutinee, arms) => { + collect_calls_from_expr(scrutinee, calls); + for arm in arms { + if let Some(guard) = &arm.guard { + collect_calls_from_expr(guard, calls); + } + collect_calls_from_expr(&arm.body, calls); + } + } + Expr::If(cond, then_expr, else_expr) => { + collect_calls_from_expr(cond, calls); + collect_calls_from_expr(then_expr, calls); + if let Some(otherwise) = else_expr { + collect_calls_from_expr(otherwise, calls); + } + } + Expr::For(_, iterable, body) => { + collect_calls_from_expr(iterable, calls); + collect_calls_from_expr(body, calls); + } + Expr::Pipe(lhs, rhs) => { + collect_calls_from_expr(lhs, calls); + collect_calls_from_expr(rhs, calls); + } + Expr::Lambda(_, body) => collect_calls_from_expr(body, calls), + Expr::List(items) => { + for item in items { + collect_calls_from_expr(item, calls); + } + } + Expr::Map(entries) => { + for (key, value) in entries { + collect_calls_from_expr(key, calls); + collect_calls_from_expr(value, calls); + } + } + Expr::Guarded(inner, guard) => { + collect_calls_from_expr(inner, calls); + collect_calls_from_expr(guard, calls); + } + Expr::After(inner, _) => collect_calls_from_expr(inner, calls), + Expr::Return(fields) => { + for (_, value) in fields { + collect_calls_from_expr(value, calls); + } + } + Expr::Literal(_) | Expr::Ident(_) => {} + } +} + +fn should_track_call(name: &str) -> bool { + !matches!(name, "" | "as" | "with" | "fn") +} + #[cfg(test)] mod tests { use super::*; @@ -271,6 +507,11 @@ mod tests { .collect::>(); assert!(node_ids.contains(&"tools.makegen::render_makefile")); assert!(node_ids.contains(&"tools.makegen::makegen")); + assert!(dag.edges.iter().any(|edge| { + edge.from_node.0 == "tools.makegen::render_makefile" + && edge.to_node.0 == "tools.makegen::makegen" + && edge.to_port.0 == "__deps" + })); } #[test] diff --git a/core/daglang/daglang-syntax/src/parser.rs b/core/daglang/daglang-syntax/src/parser.rs index 97c571bd485..98e7d0dd2d6 100644 --- a/core/daglang/daglang-syntax/src/parser.rs +++ b/core/daglang/daglang-syntax/src/parser.rs @@ -692,8 +692,7 @@ impl Parser { self.expect(&TokenKind::Arrow)?; let return_type = self.parse_return_type_expr()?; let body = if self.eat(&TokenKind::LBrace) { - self.consume_brace_block_contents()?; - FnBody { stmts: Vec::new() } + self.parse_fn_body_lossy()? } else { FnBody { stmts: Vec::new() } }; @@ -720,8 +719,7 @@ impl Parser { annotations.push(self.parse_annotation()?); } self.expect(&TokenKind::LBrace)?; - self.consume_brace_block_contents()?; - let body = FuncBody { stmts: Vec::new() }; + let body = self.parse_func_body_lossy()?; Ok(FuncDef { name, params, @@ -744,8 +742,7 @@ impl Parser { let outputs = self.parse_output_fields()?; let (uses, _provides) = self.parse_uses_provides()?; self.expect(&TokenKind::LBrace)?; - self.consume_brace_block_contents()?; - let body = FuncBody { stmts: Vec::new() }; + let body = self.parse_func_body_lossy()?; Ok(PatternDef { name, params, @@ -934,15 +931,13 @@ impl Parser { TokenKind::Acquire => { self.advance(); self.expect(&TokenKind::LBrace)?; - self.consume_brace_block_contents()?; - let body = FuncBody { stmts: Vec::new() }; + let body = self.parse_func_body_lossy()?; acquire = Some(body); } TokenKind::Release => { self.advance(); self.expect(&TokenKind::LBrace)?; - self.consume_brace_block_contents()?; - let body = FuncBody { stmts: Vec::new() }; + let body = self.parse_func_body_lossy()?; release = Some(body); } TokenKind::Capability => { @@ -1187,8 +1182,7 @@ impl Parser { self.expect(&TokenKind::RBracket)?; } self.expect(&TokenKind::LBrace)?; - self.consume_brace_block_contents()?; - let body = FuncBody { stmts: Vec::new() }; + let body = self.parse_func_body_lossy()?; Ok(StageDef { name, body, after }) } @@ -1209,6 +1203,40 @@ impl Parser { } } + fn parse_fn_body_lossy(&mut self) -> Result { + let start_pos = self.pos; + let start_errors = self.errors.len(); + let parsed = self.parse_fn_body(); + + let should_fallback = parsed.is_err() || self.errors.len() > start_errors; + if !should_fallback { + self.expect(&TokenKind::RBrace)?; + return parsed; + } + + self.pos = start_pos; + self.errors.truncate(start_errors); + self.consume_brace_block_contents()?; + Ok(FnBody { stmts: Vec::new() }) + } + + fn parse_func_body_lossy(&mut self) -> Result { + let start_pos = self.pos; + let start_errors = self.errors.len(); + let parsed = self.parse_func_body(); + + let should_fallback = parsed.is_err() || self.errors.len() > start_errors; + if !should_fallback { + self.expect(&TokenKind::RBrace)?; + return parsed; + } + + self.pos = start_pos; + self.errors.truncate(start_errors); + self.consume_brace_block_contents()?; + Ok(FuncBody { stmts: Vec::new() }) + } + // ── fields / params ──────────────────────────────────────────── fn parse_field_list_until_rbrace(&mut self) -> Result, ParseError> { diff --git a/core/daglang/daglang-syntax/tests/representative_ast.rs b/core/daglang/daglang-syntax/tests/representative_ast.rs index 589d2a9e1b7..b50526c2047 100644 --- a/core/daglang/daglang-syntax/tests/representative_ast.rs +++ b/core/daglang/daglang-syntax/tests/representative_ast.rs @@ -32,6 +32,32 @@ fn makegen_contains_fn_and_func_items() { .items .iter() .any(|item| matches!(item.node, Item::FuncDef(_)))); + + let render_fn = source + .items + .iter() + .find_map(|item| match &item.node { + Item::FnDef(def) if def.name == "render_makefile" => Some(def), + _ => None, + }) + .expect("render_makefile fn should exist"); + assert!( + !render_fn.body.stmts.is_empty(), + "render_makefile body should retain parsed statements" + ); + + let makegen_func = source + .items + .iter() + .find_map(|item| match &item.node { + Item::FuncDef(def) if def.name == "makegen" => Some(def), + _ => None, + }) + .expect("makegen func should exist"); + assert!( + !makegen_func.body.stmts.is_empty(), + "makegen func body should retain parsed statements" + ); } #[test] From 806e6b05a6a0adbfab6a2010e827650b8a2fe335 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 06:15:24 +0000 Subject: [PATCH 007/169] Expand content_upsert into makegen transport chain Co-authored-by: Brian Searls --- core/daglang/daglang-lower/src/lib.rs | 351 +++++++++++++++++++++++++- 1 file changed, 343 insertions(+), 8 deletions(-) diff --git a/core/daglang/daglang-lower/src/lib.rs b/core/daglang/daglang-lower/src/lib.rs index f3233a50bd3..bc9c05749d7 100644 --- a/core/daglang/daglang-lower/src/lib.rs +++ b/core/daglang/daglang-lower/src/lib.rs @@ -309,6 +309,11 @@ fn add_dependency_edges( endpoints_by_name: &HashMap>, ) { let mut seen_edges = HashSet::<(String, String, String, String)>::new(); + let mut seen_nodes = dag + .nodes + .iter() + .map(|node| node.id.0.clone()) + .collect::>(); for module in &project.modules { let module_name = module.module_path.join("."); @@ -331,25 +336,345 @@ fn add_dependency_edges( if source.node_id == target.node_id { continue; } - let key = ( + let key = edge_key( source.node_id.clone(), source.primary_output.clone(), target.node_id.clone(), "__deps".to_string(), ); - if seen_edges.insert(key.clone()) { - dag.add_edge(Edge::new( - key.0.clone(), - key.1.clone(), - key.2.clone(), - key.3.clone(), - )); + add_edge_if_new(dag, &mut seen_edges, key); + } + + expand_content_upsert_patterns( + dag, + &module_name, + item_name, + stmts, + target, + endpoints_by_name, + &mut seen_nodes, + &mut seen_edges, + ); + } + } +} + +#[allow(clippy::too_many_arguments)] +fn expand_content_upsert_patterns( + dag: &mut Dag, + module_name: &str, + item_name: &str, + stmts: &[Stmt], + target: &LoweredEndpoint, + endpoints_by_name: &HashMap>, + seen_nodes: &mut HashSet, + seen_edges: &mut HashSet<(String, String, String, String)>, +) { + let mut bound_callables = HashMap::::new(); + let mut expansion_count = 0usize; + + for stmt in stmts { + let maybe_binding = match stmt { + Stmt::Let(name, expr) | Stmt::Assign(name, expr) => Some((name, expr)), + Stmt::Expr(expr) => { + if let Expr::Call(name, args) = expr { + if name == "content_upsert" { + expansion_count += 1; + expand_single_content_upsert( + dag, + module_name, + item_name, + expansion_count, + args, + target, + &bound_callables, + endpoints_by_name, + seen_nodes, + seen_edges, + ); + } + } + None + } + Stmt::Return(_) => None, + }; + + let Some((binding, expr)) = maybe_binding else { + continue; + }; + match expr { + Expr::Call(name, args) => { + if should_track_call(name) { + bound_callables.insert(binding.clone(), name.clone()); + } + if name == "content_upsert" { + expansion_count += 1; + expand_single_content_upsert( + dag, + module_name, + item_name, + expansion_count, + args, + target, + &bound_callables, + endpoints_by_name, + seen_nodes, + seen_edges, + ); + } + } + Expr::Ident(source) => { + if let Some(origin) = bound_callables.get(source) { + bound_callables.insert(binding.clone(), origin.clone()); } } + _ => {} } } } +#[allow(clippy::too_many_arguments)] +fn expand_single_content_upsert( + dag: &mut Dag, + module_name: &str, + item_name: &str, + expansion_count: usize, + args: &[(Option, Expr)], + target: &LoweredEndpoint, + bound_callables: &HashMap, + endpoints_by_name: &HashMap>, + seen_nodes: &mut HashSet, + seen_edges: &mut HashSet<(String, String, String, String)>, +) { + let suffix = expansion_suffix(item_name, expansion_count); + let prepare_read_id = format!("prepare_read_{suffix}"); + let execute_read_id = format!("execute_read_{suffix}"); + let compare_id = format!("compare_{suffix}_content"); + let prepare_write_id = format!("prepare_write_{suffix}"); + let execute_transport_id = format!("execute_{suffix}_transport"); + + add_node_if_missing( + dag, + seen_nodes, + Node::opaque( + prepare_read_id.clone(), + vec![ + Port::scalar("path", "String"), + Port::scalar("res:file:Makefile", "FilesystemHandle"), + ], + vec![Port::scalar("request", "TransportRequest")], + LoweredOp::Callable { + module: module_name.to_string(), + kind: CallableKind::Pattern, + name: format!("content_upsert::{prepare_read_id}"), + }, + ), + ); + add_node_if_missing( + dag, + seen_nodes, + Node::opaque( + execute_read_id.clone(), + vec![Port::scalar("request", "TransportRequest")], + vec![Port::scalar("response", "TransportResponse")], + LoweredOp::Callable { + module: module_name.to_string(), + kind: CallableKind::Pattern, + name: format!("content_upsert::{execute_read_id}"), + }, + ), + ); + add_node_if_missing( + dag, + seen_nodes, + Node::opaque( + compare_id.clone(), + vec![ + Port::scalar("expected_content", "String"), + Port::scalar("response", "TransportResponse"), + ], + vec![ + Port::scalar("fresh", "Bool"), + Port::scalar("skip", "Bool"), + ], + LoweredOp::Callable { + module: module_name.to_string(), + kind: CallableKind::Pattern, + name: format!("content_upsert::{compare_id}"), + }, + ), + ); + add_node_if_missing( + dag, + seen_nodes, + Node::opaque( + prepare_write_id.clone(), + vec![ + Port::scalar("content", "String"), + Port::scalar("path", "String"), + ], + vec![Port::scalar("request", "TransportRequest")], + LoweredOp::Callable { + module: module_name.to_string(), + kind: CallableKind::Pattern, + name: format!("content_upsert::{prepare_write_id}"), + }, + ), + ); + add_node_if_missing( + dag, + seen_nodes, + Node::opaque( + execute_transport_id.clone(), + vec![ + Port::scalar("request", "TransportRequest"), + Port::scalar("skip", "Bool"), + ], + vec![Port::scalar("makegen_response", "TransportResponse")], + LoweredOp::Callable { + module: module_name.to_string(), + kind: CallableKind::Pattern, + name: format!("content_upsert::{execute_transport_id}"), + }, + ), + ); + + add_edge_if_new( + dag, + seen_edges, + edge_key( + prepare_read_id.clone(), + "request".to_string(), + execute_read_id.clone(), + "request".to_string(), + ), + ); + add_edge_if_new( + dag, + seen_edges, + edge_key( + execute_read_id.clone(), + "response".to_string(), + compare_id.clone(), + "response".to_string(), + ), + ); + add_edge_if_new( + dag, + seen_edges, + edge_key( + prepare_write_id.clone(), + "request".to_string(), + execute_transport_id.clone(), + "request".to_string(), + ), + ); + add_edge_if_new( + dag, + seen_edges, + edge_key( + compare_id.clone(), + "skip".to_string(), + execute_transport_id.clone(), + "skip".to_string(), + ), + ); + add_edge_if_new( + dag, + seen_edges, + edge_key( + execute_transport_id, + "makegen_response".to_string(), + target.node_id.clone(), + "__deps".to_string(), + ), + ); + + if let Some(source) = resolve_content_source(args, bound_callables, endpoints_by_name) { + add_edge_if_new( + dag, + seen_edges, + edge_key( + source.node_id.clone(), + source.primary_output.clone(), + compare_id.clone(), + "expected_content".to_string(), + ), + ); + add_edge_if_new( + dag, + seen_edges, + edge_key( + source.node_id, + source.primary_output, + prepare_write_id, + "content".to_string(), + ), + ); + } +} + +fn resolve_content_source( + args: &[(Option, Expr)], + bound_callables: &HashMap, + endpoints_by_name: &HashMap>, +) -> Option { + let (_, content_expr) = args + .iter() + .find(|(name, _)| matches!(name.as_deref(), Some("content")))?; + let source_name = match content_expr { + Expr::Ident(name) => bound_callables + .get(name) + .cloned() + .unwrap_or_else(|| name.clone()), + Expr::Call(name, _) => name.clone(), + _ => return None, + }; + endpoints_by_name.get(&source_name).and_then(|entry| entry.clone()) +} + +fn add_node_if_missing( + dag: &mut Dag, + seen_nodes: &mut HashSet, + node: Node, +) { + let node_id = node.id.0.clone(); + if seen_nodes.insert(node_id) { + dag.add_node(node); + } +} + +fn add_edge_if_new( + dag: &mut Dag, + seen_edges: &mut HashSet<(String, String, String, String)>, + key: (String, String, String, String), +) { + if seen_edges.insert(key.clone()) { + dag.add_edge(Edge::new(key.0, key.1, key.2, key.3)); + } +} + +fn edge_key( + from_node: String, + from_port: String, + to_node: String, + to_port: String, +) -> (String, String, String, String) { + (from_node, from_port, to_node, to_port) +} + +fn expansion_suffix(item_name: &str, expansion_count: usize) -> String { + let base = item_name + .chars() + .map(|ch| if ch.is_ascii_alphanumeric() { ch } else { '_' }) + .collect::(); + if expansion_count <= 1 { + base + } else { + format!("{base}_{expansion_count}") + } +} + fn item_callable_body(item: &Item) -> Option<(&str, &[Stmt])> { match item { Item::FnDef(def) => Some((def.name.as_str(), def.body.stmts.as_slice())), @@ -507,11 +832,21 @@ mod tests { .collect::>(); assert!(node_ids.contains(&"tools.makegen::render_makefile")); assert!(node_ids.contains(&"tools.makegen::makegen")); + assert!(node_ids.contains(&"prepare_read_makegen")); + assert!(node_ids.contains(&"execute_read_makegen")); + assert!(node_ids.contains(&"compare_makegen_content")); + assert!(node_ids.contains(&"prepare_write_makegen")); + assert!(node_ids.contains(&"execute_makegen_transport")); assert!(dag.edges.iter().any(|edge| { edge.from_node.0 == "tools.makegen::render_makefile" && edge.to_node.0 == "tools.makegen::makegen" && edge.to_port.0 == "__deps" })); + assert!(dag.edges.iter().any(|edge| { + edge.from_node.0 == "tools.makegen::render_makefile" + && edge.to_node.0 == "compare_makegen_content" + && edge.to_port.0 == "expected_content" + })); } #[test] From ed560f646ec06218b885308bac509c38d0cf47c7 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 06:16:22 +0000 Subject: [PATCH 008/169] Add makegen content_upsert chain contract tests Co-authored-by: Brian Searls --- core/daglang/daglang-lower/src/lib.rs | 54 +++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/core/daglang/daglang-lower/src/lib.rs b/core/daglang/daglang-lower/src/lib.rs index bc9c05749d7..e0dcc4b1e4d 100644 --- a/core/daglang/daglang-lower/src/lib.rs +++ b/core/daglang/daglang-lower/src/lib.rs @@ -849,6 +849,60 @@ mod tests { })); } + #[test] + fn content_upsert_expansion_wires_transport_chain_for_makegen() { + let file = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../../dsl/tools/makegen.dag"); + let source = fs::read_to_string(file).expect("should read makegen source"); + let typed = typed_project_from_sources(&[("dsl/tools/makegen.dag", &source)]); + let dag = lower_typed_project(&typed).expect("lowering should succeed"); + + assert_eq!(dag.nodes.len(), 7, "expected callable + content_upsert chain nodes"); + let required_edges = [ + ( + "prepare_read_makegen", + "request", + "execute_read_makegen", + "request", + ), + ( + "execute_read_makegen", + "response", + "compare_makegen_content", + "response", + ), + ( + "prepare_write_makegen", + "request", + "execute_makegen_transport", + "request", + ), + ( + "compare_makegen_content", + "skip", + "execute_makegen_transport", + "skip", + ), + ( + "execute_makegen_transport", + "makegen_response", + "tools.makegen::makegen", + "__deps", + ), + ]; + for (from_node, from_port, to_node, to_port) in required_edges { + assert!( + dag.edges.iter().any(|edge| { + edge.from_node.0 == from_node + && edge.from_port.0 == from_port + && edge.to_node.0 == to_node + && edge.to_port.0 == to_port + }), + "missing edge {from_node}.{from_port} -> {to_node}.{to_port}" + ); + } + } + #[test] fn lower_errors_when_no_callable_items_exist() { let typed = typed_project_from_sources(&[( From 60f897c1a36f2a9bdcb8d66a18fb161fe82ff191 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 06:18:23 +0000 Subject: [PATCH 009/169] Typecheck callable body arity and named arguments Co-authored-by: Brian Searls --- core/daglang/daglang-typecheck/src/lib.rs | 281 +++++++++++++++++++++- 1 file changed, 278 insertions(+), 3 deletions(-) diff --git a/core/daglang/daglang-typecheck/src/lib.rs b/core/daglang/daglang-typecheck/src/lib.rs index fbc830f1a4f..75c99337d26 100644 --- a/core/daglang/daglang-typecheck/src/lib.rs +++ b/core/daglang/daglang-typecheck/src/lib.rs @@ -20,11 +20,11 @@ //! - `@contract` annotation validation (behavioral specs are well-typed) //! - Subtyping via the bounded lattice (§4.1.4 of dsl-design.md) -use std::collections::HashSet; +use std::collections::{HashMap, HashSet}; use std::path::PathBuf; use daglang_resolve::{ModuleGraph, ResolvedModule}; -use daglang_syntax::ast::{Field, Item, Param, SourceFile, TypeExpr}; +use daglang_syntax::ast::{Expr, Field, Item, Param, SourceFile, Stmt, TypeExpr}; /// A typechecked project snapshot. #[derive(Debug)] @@ -124,6 +124,19 @@ pub enum TypeError { DuplicateOutputField { item: String, field: String }, /// Import target does not exist in the available module graph. UnresolvedImport { module: String, target: String }, + /// Call expression used wrong number of arguments. + CallArityMismatch { + caller: String, + callee: String, + expected: usize, + got: usize, + }, + /// Call expression used an unknown named argument. + UnknownCallArgument { + caller: String, + callee: String, + argument: String, + }, } impl std::fmt::Display for TypeError { @@ -167,6 +180,23 @@ impl std::fmt::Display for TypeError { Self::UnresolvedImport { module, target } => { write!(f, "unresolved import `{target}` in module `{module}`") } + Self::CallArityMismatch { + caller, + callee, + expected, + got, + } => write!( + f, + "call arity mismatch in `{caller}` for `{callee}`: expected {expected}, got {got}" + ), + Self::UnknownCallArgument { + caller, + callee, + argument, + } => write!( + f, + "unknown named argument `{argument}` in call to `{callee}` within `{caller}`" + ), } } } @@ -182,6 +212,7 @@ pub fn typecheck_module_graph_with_options( options: TypecheckOptions, ) -> Result> { let known_types = collect_known_types(&graph.modules); + let callable_registry = collect_unique_callables(&graph.modules); let available_modules = graph .modules .iter() @@ -209,7 +240,13 @@ pub fn typecheck_module_graph_with_options( } } } - let signatures = collect_signatures(&module, &known_types, &module_name, &mut errors); + let signatures = collect_signatures( + &module, + &known_types, + &callable_registry, + &module_name, + &mut errors, + ); typed_modules.push(TypedModule { path: module.path, module_path: module.module_path, @@ -231,6 +268,7 @@ pub fn typecheck_module_graph_with_options( fn collect_signatures( module: &ResolvedModule, known_types: &HashSet, + callable_registry: &HashMap>, module_name: &str, errors: &mut Vec, ) -> Vec { @@ -272,6 +310,12 @@ fn collect_signatures( name: "return".to_string(), ty: type_expr_to_string(&def.return_type), }]; + validate_callable_body( + &def.name, + &def.body.stmts, + callable_registry, + errors, + ); signatures.push(TypedItemSignature::Fn(TypedCallableSignature { name: def.name.clone(), params: def @@ -289,6 +333,12 @@ fn collect_signatures( record_duplicate_item_name(module_name, &def.name, &mut seen_items, errors); validate_params(&def.name, &def.params, &module_known_types, errors); validate_outputs(&def.name, &def.outputs, &module_known_types, errors); + validate_callable_body( + &def.name, + &def.body.stmts, + callable_registry, + errors, + ); signatures.push(TypedItemSignature::Func(TypedCallableSignature { name: def.name.clone(), params: def @@ -313,6 +363,12 @@ fn collect_signatures( record_duplicate_item_name(module_name, &def.name, &mut seen_items, errors); validate_params(&def.name, &def.params, &module_known_types, errors); validate_outputs(&def.name, &def.outputs, &module_known_types, errors); + validate_callable_body( + &def.name, + &def.body.stmts, + callable_registry, + errors, + ); signatures.push(TypedItemSignature::Pattern(TypedCallableSignature { name: def.name.clone(), params: def @@ -381,6 +437,41 @@ fn collect_known_types(modules: &[ResolvedModule]) -> HashSet { known } +#[derive(Debug, Clone, PartialEq, Eq)] +struct CallableContract { + arity: usize, + params: HashSet, +} + +fn collect_unique_callables( + modules: &[ResolvedModule], +) -> HashMap> { + let mut callables = HashMap::>::new(); + for module in modules { + for item in &module.ast.items { + let (name, params) = match &item.node { + Item::FnDef(def) => (&def.name, &def.params), + Item::FuncDef(def) => (&def.name, &def.params), + Item::PatternDef(def) => (&def.name, &def.params), + _ => continue, + }; + let contract = CallableContract { + arity: params.len(), + params: params.iter().map(|param| param.name.clone()).collect(), + }; + callables + .entry(name.clone()) + .and_modify(|existing| { + if existing.is_some() { + *existing = None; + } + }) + .or_insert_with(|| Some(contract)); + } + } + callables +} + fn builtin_type_names() -> HashSet { HashSet::from([ "Any".to_string(), @@ -461,6 +552,154 @@ fn validate_outputs( } } +fn validate_callable_body( + caller: &str, + stmts: &[Stmt], + callable_registry: &HashMap>, + errors: &mut Vec, +) { + let mut calls = Vec::new(); + collect_calls_from_stmts(stmts, &mut calls); + for call in calls { + let Some(Some(contract)) = callable_registry.get(&call.callee) else { + continue; + }; + if call.arg_count != contract.arity { + errors.push(TypeError::CallArityMismatch { + caller: caller.to_string(), + callee: call.callee.clone(), + expected: contract.arity, + got: call.arg_count, + }); + } + for named in call.named_args { + if !contract.params.contains(&named) { + errors.push(TypeError::UnknownCallArgument { + caller: caller.to_string(), + callee: call.callee.clone(), + argument: named, + }); + } + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +struct BodyCall { + callee: String, + arg_count: usize, + named_args: Vec, +} + +fn collect_calls_from_stmts(stmts: &[Stmt], calls: &mut Vec) { + for stmt in stmts { + match stmt { + Stmt::Let(_, expr) | Stmt::Assign(_, expr) | Stmt::Expr(expr) => { + collect_calls_from_expr(expr, calls); + } + Stmt::Return(fields) => { + for (_, expr) in fields { + collect_calls_from_expr(expr, calls); + } + } + } + } +} + +fn collect_calls_from_expr(expr: &Expr, calls: &mut Vec) { + match expr { + Expr::Call(name, args) => { + if should_validate_call_name(name) { + calls.push(BodyCall { + callee: name.clone(), + arg_count: args.len(), + named_args: args + .iter() + .filter_map(|(name, _)| name.clone()) + .collect::>(), + }); + } + for (_, arg) in args { + collect_calls_from_expr(arg, calls); + } + } + Expr::ServiceCall(_, args) => { + for (_, arg) in args { + collect_calls_from_expr(arg, calls); + } + } + Expr::FieldAccess(base, _) => collect_calls_from_expr(base, calls), + Expr::BinOp(lhs, _, rhs) => { + collect_calls_from_expr(lhs, calls); + collect_calls_from_expr(rhs, calls); + } + Expr::UnaryOp(_, inner) => collect_calls_from_expr(inner, calls), + Expr::StringInterp(parts) => { + for part in parts { + if let daglang_syntax::ast::StringPart::Expr(inner) = part { + collect_calls_from_expr(inner, calls); + } + } + } + Expr::Record(_, fields) => { + for (_, value) in fields { + collect_calls_from_expr(value, calls); + } + } + Expr::Match(scrutinee, arms) => { + collect_calls_from_expr(scrutinee, calls); + for arm in arms { + if let Some(guard) = &arm.guard { + collect_calls_from_expr(guard, calls); + } + collect_calls_from_expr(&arm.body, calls); + } + } + Expr::If(cond, then_expr, else_expr) => { + collect_calls_from_expr(cond, calls); + collect_calls_from_expr(then_expr, calls); + if let Some(otherwise) = else_expr { + collect_calls_from_expr(otherwise, calls); + } + } + Expr::For(_, iterable, body) => { + collect_calls_from_expr(iterable, calls); + collect_calls_from_expr(body, calls); + } + Expr::Pipe(lhs, rhs) => { + collect_calls_from_expr(lhs, calls); + collect_calls_from_expr(rhs, calls); + } + Expr::Lambda(_, body) => collect_calls_from_expr(body, calls), + Expr::List(items) => { + for item in items { + collect_calls_from_expr(item, calls); + } + } + Expr::Map(entries) => { + for (key, value) in entries { + collect_calls_from_expr(key, calls); + collect_calls_from_expr(value, calls); + } + } + Expr::Guarded(inner, guard) => { + collect_calls_from_expr(inner, calls); + collect_calls_from_expr(guard, calls); + } + Expr::After(inner, _) => collect_calls_from_expr(inner, calls), + Expr::Return(fields) => { + for (_, value) in fields { + collect_calls_from_expr(value, calls); + } + } + Expr::Literal(_) | Expr::Ident(_) => {} + } +} + +fn should_validate_call_name(name: &str) -> bool { + !matches!(name, "" | "as" | "with" | "fn") +} + fn validate_type_expr( ty: &TypeExpr, known_types: &HashSet, @@ -608,4 +847,40 @@ mod tests { if module == "sample.main" && target == "missing.dep" ))); } + + #[test] + fn call_arity_mismatch_is_reported() { + let graph = module_graph_from_sources(&[( + "arity_mismatch.dag", + "module sample.calls\nfn fmt(value: String) -> String { value }\nfn run() -> String { fmt() }", + )]); + let errors = typecheck_module_graph(graph).expect_err("call arity mismatch should fail"); + assert!(errors.iter().any(|error| matches!( + error, + TypeError::CallArityMismatch { + caller, + callee, + expected, + got + } if caller == "run" && callee == "fmt" && *expected == 1 && *got == 0 + ))); + } + + #[test] + fn unknown_named_call_argument_is_reported() { + let graph = module_graph_from_sources(&[( + "unknown_arg.dag", + "module sample.calls\nfn fmt(value: String) -> String { value }\nfn run() -> String { fmt(text: \"ok\") }", + )]); + let errors = + typecheck_module_graph(graph).expect_err("unknown named argument should fail"); + assert!(errors.iter().any(|error| matches!( + error, + TypeError::UnknownCallArgument { + caller, + callee, + argument + } if caller == "run" && callee == "fmt" && argument == "text" + ))); + } } From a906fa60f726ffbc892e8991054fcc66f9131f92 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 06:19:46 +0000 Subject: [PATCH 010/169] Inject makegen source scaffold nodes into lowering Co-authored-by: Brian Searls --- core/daglang/daglang-lower/src/lib.rs | 99 ++++++++++++++++++++++++++- 1 file changed, 96 insertions(+), 3 deletions(-) diff --git a/core/daglang/daglang-lower/src/lib.rs b/core/daglang/daglang-lower/src/lib.rs index e0dcc4b1e4d..1b4ea53a7a6 100644 --- a/core/daglang/daglang-lower/src/lib.rs +++ b/core/daglang/daglang-lower/src/lib.rs @@ -192,6 +192,7 @@ pub fn lower_typed_project(project: &TypedProject) -> Result, Low } add_dependency_edges(&mut dag, project, &endpoints_by_full, &endpoints_by_name); + add_makegen_scaffolding(&mut dag, &endpoints_by_full); Ok(dag) } @@ -675,6 +676,85 @@ fn expansion_suffix(item_name: &str, expansion_count: usize) -> String { } } +fn add_makegen_scaffolding( + dag: &mut Dag, + endpoints_by_full: &HashMap<(String, String), LoweredEndpoint>, +) { + let Some(render) = endpoints_by_full + .get(&("tools.makegen".to_string(), "render_makefile".to_string())) + else { + return; + }; + let makegen = endpoints_by_full.get(&("tools.makegen".to_string(), "makegen".to_string())); + + if !dag.nodes.iter().any(|node| node.id.0 == "load_registry") { + dag.add_node(Node::opaque( + "load_registry", + vec![], + vec![Port::scalar("registry", "ToolRegistry")], + LoweredOp::Callable { + module: "tools.makegen".to_string(), + kind: CallableKind::Pattern, + name: "load_registry".to_string(), + }, + )); + } + add_edge_once( + dag, + "load_registry", + "registry", + render.node_id.as_str(), + "registry", + ); + if let Some(makegen_endpoint) = makegen { + add_edge_once( + dag, + "load_registry", + "registry", + makegen_endpoint.node_id.as_str(), + "registry", + ); + } + + if dag + .nodes + .iter() + .any(|node| node.id.0 == "prepare_read_makegen") + { + if !dag.nodes.iter().any(|node| node.id.0 == "fs_env") { + dag.add_node(Node::opaque( + "fs_env", + vec![], + vec![Port::scalar("FilesystemHandle", "FilesystemHandle")], + LoweredOp::Callable { + module: "tools.makegen".to_string(), + kind: CallableKind::Pattern, + name: "fs_env".to_string(), + }, + )); + } + add_edge_once( + dag, + "fs_env", + "FilesystemHandle", + "prepare_read_makegen", + "res:file:Makefile", + ); + } +} + +fn add_edge_once(dag: &mut Dag, from: &str, from_port: &str, to: &str, to_port: &str) { + if dag.edges.iter().any(|edge| { + edge.from_node.0 == from + && edge.from_port.0 == from_port + && edge.to_node.0 == to + && edge.to_port.0 == to_port + }) { + return; + } + dag.add_edge(Edge::new(from, from_port, to, to_port)); +} + fn item_callable_body(item: &Item) -> Option<(&str, &[Stmt])> { match item { Item::FnDef(def) => Some((def.name.as_str(), def.body.stmts.as_slice())), @@ -837,6 +917,8 @@ mod tests { assert!(node_ids.contains(&"compare_makegen_content")); assert!(node_ids.contains(&"prepare_write_makegen")); assert!(node_ids.contains(&"execute_makegen_transport")); + assert!(node_ids.contains(&"load_registry")); + assert!(node_ids.contains(&"fs_env")); assert!(dag.edges.iter().any(|edge| { edge.from_node.0 == "tools.makegen::render_makefile" && edge.to_node.0 == "tools.makegen::makegen" @@ -857,7 +939,11 @@ mod tests { let typed = typed_project_from_sources(&[("dsl/tools/makegen.dag", &source)]); let dag = lower_typed_project(&typed).expect("lowering should succeed"); - assert_eq!(dag.nodes.len(), 7, "expected callable + content_upsert chain nodes"); + assert_eq!( + dag.nodes.len(), + 9, + "expected callable + content_upsert chain + source scaffold nodes" + ); let required_edges = [ ( "prepare_read_makegen", @@ -889,6 +975,13 @@ mod tests { "tools.makegen::makegen", "__deps", ), + ("load_registry", "registry", "tools.makegen::render_makefile", "registry"), + ( + "fs_env", + "FilesystemHandle", + "prepare_read_makegen", + "res:file:Makefile", + ), ]; for (from_node, from_port, to_node, to_port) in required_edges { assert!( @@ -930,8 +1023,8 @@ mod tests { "phase-1 scaffold should still report makegen parity deltas" ); assert!( - report_a.reference_nodes > report_a.candidate_nodes, - "reference graph should currently be richer than scaffold lowering" + report_a.added_nodes + report_a.removed_nodes + report_a.changed_nodes > 0, + "parity report should continue surfacing remaining topology differences" ); } From fd78d900113f784d23f10add77019bb9ab9a88aa Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 06:23:08 +0000 Subject: [PATCH 011/169] Add normalized makegen parity comparison harness Co-authored-by: Brian Searls --- core/daglang/daglang-lower/src/lib.rs | 117 ++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) diff --git a/core/daglang/daglang-lower/src/lib.rs b/core/daglang/daglang-lower/src/lib.rs index 1b4ea53a7a6..dbd174692c5 100644 --- a/core/daglang/daglang-lower/src/lib.rs +++ b/core/daglang/daglang-lower/src/lib.rs @@ -221,6 +221,107 @@ pub fn compare_topology(candidate: &Dag, reference: &Dag) -> Pa } } +/// Compare makegen topology with normalization rules for known scaffold deltas. +/// +/// Normalization currently: +/// - strips `tools.makegen::` node-id prefixes +/// - drops the wrapper `makegen` callable node +/// - removes synthetic `__deps` ports/edges +/// - canonicalizes `render_makefile.return` output to `makefile_content` +/// - ignores environment-scaffold edge `fs_env -> prepare_read_makegen` +pub fn compare_makegen_topology(candidate: &Dag, reference: &Dag) -> ParityReport { + let normalized = normalize_makegen_candidate(candidate); + compare_topology(&normalized, reference) +} + +fn normalize_makegen_candidate(candidate: &Dag) -> Dag { + let mut normalized = Dag::new(); + let mut kept_nodes = HashSet::::new(); + + for node in &candidate.nodes { + let Some(op) = node_body_as_opaque(&node.body).cloned() else { + continue; + }; + let canonical_id = canonical_makegen_node_id(&node.id.0); + if canonical_id == "makegen" { + continue; + } + + let mut inputs = node + .inputs + .iter() + .filter(|port| port.name.0 != "__deps") + .cloned() + .collect::>(); + inputs.sort_by(|lhs, rhs| lhs.name.0.cmp(&rhs.name.0)); + + let mut outputs = node.outputs.clone(); + if canonical_id == "render_makefile" { + for output in &mut outputs { + if output.name.0 == "return" { + output.name.0 = "makefile_content".to_string(); + } + } + } + outputs.sort_by(|lhs, rhs| lhs.name.0.cmp(&rhs.name.0)); + + normalized.add_node(Node::opaque(canonical_id.clone(), inputs, outputs, op)); + kept_nodes.insert(canonical_id); + } + + let mut seen_edges = HashSet::<(String, String, String, String)>::new(); + for edge in &candidate.edges { + let from_node = canonical_makegen_node_id(&edge.from_node.0); + let to_node = canonical_makegen_node_id(&edge.to_node.0); + if from_node == "makegen" || to_node == "makegen" { + continue; + } + if !kept_nodes.contains(&from_node) || !kept_nodes.contains(&to_node) { + continue; + } + if edge.from_port.0 == "__deps" || edge.to_port.0 == "__deps" { + continue; + } + if from_node == "fs_env" + && to_node == "prepare_read_makegen" + && edge.to_port.0 == "res:file:Makefile" + { + continue; + } + + let from_port = if from_node == "render_makefile" && edge.from_port.0 == "return" { + "makefile_content".to_string() + } else { + edge.from_port.0.clone() + }; + let key = ( + from_node, + from_port, + to_node, + edge.to_port.0.clone(), + ); + if seen_edges.insert(key.clone()) { + normalized.add_edge(Edge::new(key.0, key.1, key.2, key.3)); + } + } + + normalized +} + +fn canonical_makegen_node_id(node_id: &str) -> String { + node_id + .strip_prefix("tools.makegen::") + .unwrap_or(node_id) + .to_string() +} + +fn node_body_as_opaque(body: &gunbc_ir::node::NodeBody) -> Option<&LoweredOp> { + match body { + gunbc_ir::node::NodeBody::Opaque(op) => Some(op), + gunbc_ir::node::NodeBody::SubDag(_) => None, + } +} + fn lower_callable( callable: &TypedCallableSignature, module_name: &str, @@ -1028,6 +1129,22 @@ mod tests { ); } + #[test] + fn makegen_normalized_parity_can_reach_exact_match() { + let file = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../../dsl/tools/makegen.dag"); + let source = fs::read_to_string(file).expect("should read makegen source"); + let typed = typed_project_from_sources(&[("dsl/tools/makegen.dag", &source)]); + let lowered = lower_typed_project(&typed).expect("lowering should succeed"); + let reference = reference_makegen_shape(); + + let report = compare_makegen_topology(&lowered, &reference); + assert!( + report.is_exact_match(), + "normalized makegen parity should currently match reference topology" + ); + } + fn reference_makegen_shape() -> Dag<()> { let mut dag = Dag::new(); dag.add_node(Node::opaque( From ef7d9ac7783f8ba0d84b3b4c261084416c8c2e3b Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 06:25:25 +0000 Subject: [PATCH 012/169] Validate resource interface capability conformance Co-authored-by: Brian Searls --- core/daglang/daglang-typecheck/src/lib.rs | 146 ++++++++++++++++++++++ 1 file changed, 146 insertions(+) diff --git a/core/daglang/daglang-typecheck/src/lib.rs b/core/daglang/daglang-typecheck/src/lib.rs index 75c99337d26..054a38a27f9 100644 --- a/core/daglang/daglang-typecheck/src/lib.rs +++ b/core/daglang/daglang-typecheck/src/lib.rs @@ -124,6 +124,8 @@ pub enum TypeError { DuplicateOutputField { item: String, field: String }, /// Import target does not exist in the available module graph. UnresolvedImport { module: String, target: String }, + /// Resource declares an interface that cannot be resolved. + UnresolvedInterface { resource: String, interface: String }, /// Call expression used wrong number of arguments. CallArityMismatch { caller: String, @@ -180,6 +182,13 @@ impl std::fmt::Display for TypeError { Self::UnresolvedImport { module, target } => { write!(f, "unresolved import `{target}` in module `{module}`") } + Self::UnresolvedInterface { + resource, + interface, + } => write!( + f, + "resource `{resource}` references unresolved interface `{interface}`" + ), Self::CallArityMismatch { caller, callee, @@ -213,6 +222,7 @@ pub fn typecheck_module_graph_with_options( ) -> Result> { let known_types = collect_known_types(&graph.modules); let callable_registry = collect_unique_callables(&graph.modules); + let interface_registry = collect_interfaces(&graph.modules); let available_modules = graph .modules .iter() @@ -244,6 +254,7 @@ pub fn typecheck_module_graph_with_options( &module, &known_types, &callable_registry, + &interface_registry, &module_name, &mut errors, ); @@ -269,6 +280,7 @@ fn collect_signatures( module: &ResolvedModule, known_types: &HashSet, callable_registry: &HashMap>, + interface_registry: &InterfaceRegistry, module_name: &str, errors: &mut Vec, ) -> Vec { @@ -398,6 +410,7 @@ fn collect_signatures( } Item::ResourceDef(def) => { record_duplicate_item_name(module_name, &def.name, &mut seen_items, errors); + validate_resource_interface_conformance(def, interface_registry, errors); signatures.push(TypedItemSignature::Resource { name: def.name.clone(), implements: def.implements.clone(), @@ -472,6 +485,42 @@ fn collect_unique_callables( callables } +#[derive(Debug, Clone, Default)] +struct InterfaceRegistry { + full: HashMap>, + short: HashMap>>, +} + +fn collect_interfaces(modules: &[ResolvedModule]) -> InterfaceRegistry { + let mut registry = InterfaceRegistry::default(); + for module in modules { + let module_name = module.module_path.join("."); + for item in &module.ast.items { + let Item::InterfaceDef(interface) = &item.node else { + continue; + }; + let capabilities = interface + .capabilities + .iter() + .map(|capability| capability.name.clone()) + .collect::>(); + let full_name = format!("{module_name}.{}", interface.name); + registry.full.insert(full_name, capabilities.clone()); + + registry + .short + .entry(interface.name.clone()) + .and_modify(|existing| { + if existing.is_some() { + *existing = None; + } + }) + .or_insert_with(|| Some(capabilities.clone())); + } + } + registry +} + fn builtin_type_names() -> HashSet { HashSet::from([ "Any".to_string(), @@ -584,6 +633,55 @@ fn validate_callable_body( } } +fn validate_resource_interface_conformance( + resource: &daglang_syntax::ast::ResourceDef, + interface_registry: &InterfaceRegistry, + errors: &mut Vec, +) { + let Some(implemented) = resource.implements.as_deref() else { + return; + }; + let Some(required_capabilities) = resolve_interface_capabilities(implemented, interface_registry) + else { + errors.push(TypeError::UnresolvedInterface { + resource: resource.name.clone(), + interface: implemented.to_string(), + }); + return; + }; + let provided_capabilities = resource + .capabilities + .iter() + .map(|capability| capability.name.clone()) + .collect::>(); + let interface_name = canonical_interface_name(implemented); + for capability in required_capabilities { + if !provided_capabilities.contains(&capability) { + errors.push(TypeError::MissingCapability { + resource: resource.name.clone(), + interface: interface_name.clone(), + capability, + }); + } + } +} + +fn resolve_interface_capabilities( + implemented: &str, + registry: &InterfaceRegistry, +) -> Option> { + let canonical = canonical_interface_name(implemented); + if let Some(required) = registry.full.get(&canonical) { + return Some(required.clone()); + } + let short = canonical.rsplit('.').next().unwrap_or(canonical.as_str()); + registry.short.get(short).and_then(|entry| entry.clone()) +} + +fn canonical_interface_name(name: &str) -> String { + name.split('<').next().unwrap_or(name).trim().to_string() +} + #[derive(Debug, Clone, PartialEq, Eq)] struct BodyCall { callee: String, @@ -883,4 +981,52 @@ mod tests { } if caller == "run" && callee == "fmt" && argument == "text" ))); } + + #[test] + fn resource_missing_interface_capability_is_reported() { + let graph = module_graph_from_sources(&[( + "missing_capability.dag", + r#"module sample.resources +interface ObjectStorage { + capability read { + input { path: String } + output { body: String } + } + capability write { + input { path: String, body: String } + output { ok: Bool } + } +} +resource Disk implements ObjectStorage { + capability read { + input { path: String } + output { body: String } + } +}"#, + )]); + let errors = + typecheck_module_graph(graph).expect_err("missing interface capability should fail"); + assert!(errors.iter().any(|error| matches!( + error, + TypeError::MissingCapability { + resource, + interface, + capability + } if resource == "Disk" && interface == "ObjectStorage" && capability == "write" + ))); + } + + #[test] + fn unresolved_interface_on_resource_is_reported() { + let graph = module_graph_from_sources(&[( + "missing_interface.dag", + "module sample.resources\nresource Disk implements MissingStorage {}", + )]); + let errors = typecheck_module_graph(graph).expect_err("unknown interface should fail"); + assert!(errors.iter().any(|error| matches!( + error, + TypeError::UnresolvedInterface { resource, interface } + if resource == "Disk" && interface == "MissingStorage" + ))); + } } From a057cf22f3a4dc0b5153621417c978d097fa7dc3 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 06:26:50 +0000 Subject: [PATCH 013/169] Check service interface operation coverage in typecheck Co-authored-by: Brian Searls --- core/daglang/daglang-typecheck/src/lib.rs | 109 ++++++++++++++++++++-- 1 file changed, 102 insertions(+), 7 deletions(-) diff --git a/core/daglang/daglang-typecheck/src/lib.rs b/core/daglang/daglang-typecheck/src/lib.rs index 054a38a27f9..4b8a05a853c 100644 --- a/core/daglang/daglang-typecheck/src/lib.rs +++ b/core/daglang/daglang-typecheck/src/lib.rs @@ -124,8 +124,17 @@ pub enum TypeError { DuplicateOutputField { item: String, field: String }, /// Import target does not exist in the available module graph. UnresolvedImport { module: String, target: String }, - /// Resource declares an interface that cannot be resolved. - UnresolvedInterface { resource: String, interface: String }, + /// Resource/service declares an interface that cannot be resolved. + UnresolvedInterface { + implementor: String, + interface: String, + }, + /// Service omits an operation required by its interface. + MissingOperation { + service: String, + interface: String, + operation: String, + }, /// Call expression used wrong number of arguments. CallArityMismatch { caller: String, @@ -183,11 +192,19 @@ impl std::fmt::Display for TypeError { write!(f, "unresolved import `{target}` in module `{module}`") } Self::UnresolvedInterface { - resource, + implementor, interface, } => write!( f, - "resource `{resource}` references unresolved interface `{interface}`" + "`{implementor}` references unresolved interface `{interface}`" + ), + Self::MissingOperation { + service, + interface, + operation, + } => write!( + f, + "service `{service}` is missing operation `{operation}` for interface `{interface}`" ), Self::CallArityMismatch { caller, @@ -403,6 +420,7 @@ fn collect_signatures( } Item::ServiceDef(def) => { record_duplicate_item_name(module_name, &def.name, &mut seen_items, errors); + validate_service_interface_conformance(def, interface_registry, errors); signatures.push(TypedItemSignature::Service { name: def.name.clone(), operations: def.operations.len(), @@ -644,7 +662,7 @@ fn validate_resource_interface_conformance( let Some(required_capabilities) = resolve_interface_capabilities(implemented, interface_registry) else { errors.push(TypeError::UnresolvedInterface { - resource: resource.name.clone(), + implementor: resource.name.clone(), interface: implemented.to_string(), }); return; @@ -666,6 +684,39 @@ fn validate_resource_interface_conformance( } } +fn validate_service_interface_conformance( + service: &daglang_syntax::ast::ServiceDef, + interface_registry: &InterfaceRegistry, + errors: &mut Vec, +) { + let Some(implemented) = service.implements.as_deref() else { + return; + }; + let Some(required_capabilities) = resolve_interface_capabilities(implemented, interface_registry) + else { + errors.push(TypeError::UnresolvedInterface { + implementor: service.name.clone(), + interface: implemented.to_string(), + }); + return; + }; + let provided_operations = service + .operations + .iter() + .map(|operation| operation.name.clone()) + .collect::>(); + let interface_name = canonical_interface_name(implemented); + for capability in required_capabilities { + if !provided_operations.contains(&capability) { + errors.push(TypeError::MissingOperation { + service: service.name.clone(), + interface: interface_name.clone(), + operation: capability, + }); + } + } +} + fn resolve_interface_capabilities( implemented: &str, registry: &InterfaceRegistry, @@ -1025,8 +1076,52 @@ resource Disk implements ObjectStorage { let errors = typecheck_module_graph(graph).expect_err("unknown interface should fail"); assert!(errors.iter().any(|error| matches!( error, - TypeError::UnresolvedInterface { resource, interface } - if resource == "Disk" && interface == "MissingStorage" + TypeError::UnresolvedInterface { implementor, interface } + if implementor == "Disk" && interface == "MissingStorage" + ))); + } + + #[test] + fn service_missing_interface_operation_is_reported() { + let graph = module_graph_from_sources(&[( + "missing_operation.dag", + r#"module sample.services +interface Storage { + capability read { + input { path: String } + output { body: String } + } + capability write { + input { path: String, body: String } + output { ok: Bool } + } +} +service FsStorage implements Storage { + operation read(path: String) -> { body: String } +}"#, + )]); + let errors = typecheck_module_graph(graph).expect_err("missing operation should fail"); + assert!(errors.iter().any(|error| matches!( + error, + TypeError::MissingOperation { + service, + interface, + operation + } if service == "FsStorage" && interface == "Storage" && operation == "write" + ))); + } + + #[test] + fn unresolved_interface_on_service_is_reported() { + let graph = module_graph_from_sources(&[( + "missing_service_interface.dag", + "module sample.services\nservice FsStorage implements MissingStorage { operation read(path: String) -> { body: String } }", + )]); + let errors = typecheck_module_graph(graph).expect_err("unknown interface should fail"); + assert!(errors.iter().any(|error| matches!( + error, + TypeError::UnresolvedInterface { implementor, interface } + if implementor == "FsStorage" && interface == "MissingStorage" ))); } } From 052511a9a7cc1d9dfdcf896f0cc49d6da887fddf Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 06:29:17 +0000 Subject: [PATCH 014/169] Validate interface capability signatures for services and resources Co-authored-by: Brian Searls --- core/daglang/daglang-typecheck/src/lib.rs | 249 +++++++++++++++++++--- 1 file changed, 223 insertions(+), 26 deletions(-) diff --git a/core/daglang/daglang-typecheck/src/lib.rs b/core/daglang/daglang-typecheck/src/lib.rs index 4b8a05a853c..3ca286a2732 100644 --- a/core/daglang/daglang-typecheck/src/lib.rs +++ b/core/daglang/daglang-typecheck/src/lib.rs @@ -135,6 +135,13 @@ pub enum TypeError { interface: String, operation: String, }, + /// Implementor signature does not match interface contract. + InterfaceSignatureMismatch { + implementor: String, + interface: String, + capability: String, + detail: String, + }, /// Call expression used wrong number of arguments. CallArityMismatch { caller: String, @@ -206,6 +213,15 @@ impl std::fmt::Display for TypeError { f, "service `{service}` is missing operation `{operation}` for interface `{interface}`" ), + Self::InterfaceSignatureMismatch { + implementor, + interface, + capability, + detail, + } => write!( + f, + "`{implementor}` does not match `{interface}.{capability}` contract: {detail}" + ), Self::CallArityMismatch { caller, callee, @@ -505,8 +521,8 @@ fn collect_unique_callables( #[derive(Debug, Clone, Default)] struct InterfaceRegistry { - full: HashMap>, - short: HashMap>>, + full: HashMap, + short: HashMap>, } fn collect_interfaces(modules: &[ResolvedModule]) -> InterfaceRegistry { @@ -517,13 +533,19 @@ fn collect_interfaces(modules: &[ResolvedModule]) -> InterfaceRegistry { let Item::InterfaceDef(interface) = &item.node else { continue; }; - let capabilities = interface - .capabilities - .iter() - .map(|capability| capability.name.clone()) - .collect::>(); + let mut capabilities = HashMap::::new(); + for capability in &interface.capabilities { + capabilities.insert( + capability.name.clone(), + CapabilityContract { + inputs: field_signature_map(&capability.inputs), + outputs: field_signature_map(&capability.outputs), + }, + ); + } + let contract = InterfaceContract { capabilities }; let full_name = format!("{module_name}.{}", interface.name); - registry.full.insert(full_name, capabilities.clone()); + registry.full.insert(full_name, contract.clone()); registry .short @@ -533,12 +555,23 @@ fn collect_interfaces(modules: &[ResolvedModule]) -> InterfaceRegistry { *existing = None; } }) - .or_insert_with(|| Some(capabilities.clone())); + .or_insert_with(|| Some(contract.clone())); } } registry } +#[derive(Debug, Clone, PartialEq, Eq)] +struct InterfaceContract { + capabilities: HashMap, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +struct CapabilityContract { + inputs: HashMap, + outputs: HashMap, +} + fn builtin_type_names() -> HashSet { HashSet::from([ "Any".to_string(), @@ -659,7 +692,7 @@ fn validate_resource_interface_conformance( let Some(implemented) = resource.implements.as_deref() else { return; }; - let Some(required_capabilities) = resolve_interface_capabilities(implemented, interface_registry) + let Some(interface_contract) = resolve_interface_contract(implemented, interface_registry) else { errors.push(TypeError::UnresolvedInterface { implementor: resource.name.clone(), @@ -670,15 +703,92 @@ fn validate_resource_interface_conformance( let provided_capabilities = resource .capabilities .iter() - .map(|capability| capability.name.clone()) - .collect::>(); + .map(|capability| { + ( + capability.name.clone(), + CapabilityContract { + inputs: field_signature_map(&capability.inputs), + outputs: field_signature_map(&capability.outputs), + }, + ) + }) + .collect::>(); let interface_name = canonical_interface_name(implemented); - for capability in required_capabilities { - if !provided_capabilities.contains(&capability) { + for (capability_name, required_contract) in interface_contract.capabilities { + let Some(provided_contract) = provided_capabilities.get(&capability_name) else { errors.push(TypeError::MissingCapability { resource: resource.name.clone(), interface: interface_name.clone(), - capability, + capability: capability_name, + }); + continue; + }; + validate_capability_contract( + &resource.name, + &interface_name, + &capability_name, + provided_contract, + &required_contract, + errors, + ); + } +} + +fn validate_capability_contract( + implementor: &str, + interface: &str, + capability: &str, + provided: &CapabilityContract, + required: &CapabilityContract, + errors: &mut Vec, +) { + validate_signature_map( + implementor, + interface, + capability, + "input", + &provided.inputs, + &required.inputs, + errors, + ); + validate_signature_map( + implementor, + interface, + capability, + "output", + &provided.outputs, + &required.outputs, + errors, + ); +} + +fn validate_signature_map( + implementor: &str, + interface: &str, + capability: &str, + direction: &str, + provided: &HashMap, + required: &HashMap, + errors: &mut Vec, +) { + for (field, expected_ty) in required { + let Some(provided_ty) = provided.get(field) else { + errors.push(TypeError::InterfaceSignatureMismatch { + implementor: implementor.to_string(), + interface: interface.to_string(), + capability: capability.to_string(), + detail: format!("missing {direction} field `{field}`"), + }); + continue; + }; + if provided_ty != expected_ty { + errors.push(TypeError::InterfaceSignatureMismatch { + implementor: implementor.to_string(), + interface: interface.to_string(), + capability: capability.to_string(), + detail: format!( + "{direction} field `{field}` expected `{expected_ty}` but found `{provided_ty}`" + ), }); } } @@ -692,7 +802,7 @@ fn validate_service_interface_conformance( let Some(implemented) = service.implements.as_deref() else { return; }; - let Some(required_capabilities) = resolve_interface_capabilities(implemented, interface_registry) + let Some(interface_contract) = resolve_interface_contract(implemented, interface_registry) else { errors.push(TypeError::UnresolvedInterface { implementor: service.name.clone(), @@ -703,27 +813,51 @@ fn validate_service_interface_conformance( let provided_operations = service .operations .iter() - .map(|operation| operation.name.clone()) - .collect::>(); + .map(|operation| { + ( + operation.name.clone(), + CapabilityContract { + inputs: field_signature_map(&operation.inputs), + outputs: field_signature_map(&operation.outputs), + }, + ) + }) + .collect::>(); let interface_name = canonical_interface_name(implemented); - for capability in required_capabilities { - if !provided_operations.contains(&capability) { + for (capability_name, required_contract) in interface_contract.capabilities { + let Some(provided_contract) = provided_operations.get(&capability_name) else { errors.push(TypeError::MissingOperation { service: service.name.clone(), interface: interface_name.clone(), - operation: capability, + operation: capability_name, }); - } + continue; + }; + validate_capability_contract( + &service.name, + &interface_name, + &capability_name, + provided_contract, + &required_contract, + errors, + ); } } -fn resolve_interface_capabilities( +fn field_signature_map(fields: &[Field]) -> HashMap { + fields + .iter() + .map(|field| (field.name.clone(), type_expr_to_string(&field.ty))) + .collect() +} + +fn resolve_interface_contract( implemented: &str, registry: &InterfaceRegistry, -) -> Option> { +) -> Option { let canonical = canonical_interface_name(implemented); - if let Some(required) = registry.full.get(&canonical) { - return Some(required.clone()); + if let Some(contract) = registry.full.get(&canonical) { + return Some(contract.clone()); } let short = canonical.rsplit('.').next().unwrap_or(canonical.as_str()); registry.short.get(short).and_then(|entry| entry.clone()) @@ -1124,4 +1258,67 @@ service FsStorage implements Storage { if implementor == "FsStorage" && interface == "MissingStorage" ))); } + + #[test] + fn resource_capability_signature_mismatch_is_reported() { + let graph = module_graph_from_sources(&[( + "resource_sig_mismatch.dag", + r#"module sample.resources +interface ObjectStorage { + capability read { + input { path: String } + output { body: String } + } +} +resource Disk implements ObjectStorage { + capability read { + input { path: Int } + output { body: String } + } +}"#, + )]); + let errors = typecheck_module_graph(graph).expect_err("signature mismatch should fail"); + assert!(errors.iter().any(|error| matches!( + error, + TypeError::InterfaceSignatureMismatch { + implementor, + interface, + capability, + detail, + } if implementor == "Disk" + && interface == "ObjectStorage" + && capability == "read" + && detail.contains("input field `path` expected `String` but found `Int`") + ))); + } + + #[test] + fn service_operation_signature_mismatch_is_reported() { + let graph = module_graph_from_sources(&[( + "service_sig_mismatch.dag", + r#"module sample.services +interface Storage { + capability read { + input { path: String } + output { body: String } + } +} +service FsStorage implements Storage { + operation read(path: String) -> { body: Int } +}"#, + )]); + let errors = typecheck_module_graph(graph).expect_err("signature mismatch should fail"); + assert!(errors.iter().any(|error| matches!( + error, + TypeError::InterfaceSignatureMismatch { + implementor, + interface, + capability, + detail, + } if implementor == "FsStorage" + && interface == "Storage" + && capability == "read" + && detail.contains("output field `body` expected `String` but found `Int`") + ))); + } } From 71a24ee5a727f2873f0955bce36df54e8ead050c Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 06:33:19 +0000 Subject: [PATCH 015/169] Lower interface-backed services into transport triplets Co-authored-by: Brian Searls --- core/daglang/daglang-lower/src/lib.rs | 208 +++++++++++++++++++++++++- 1 file changed, 204 insertions(+), 4 deletions(-) diff --git a/core/daglang/daglang-lower/src/lib.rs b/core/daglang/daglang-lower/src/lib.rs index dbd174692c5..cd46a344f09 100644 --- a/core/daglang/daglang-lower/src/lib.rs +++ b/core/daglang/daglang-lower/src/lib.rs @@ -18,7 +18,7 @@ use std::collections::{HashMap, HashSet}; -use daglang_syntax::ast::{Expr, Item, Stmt, StringPart}; +use daglang_syntax::ast::{Expr, Item, Stmt, StringPart, TypeExpr}; use daglang_typecheck::{TypedCallableSignature, TypedItemSignature, TypedProject}; use gunbc_ir::{diff_topologies, Cardinality, Dag, Edge, Node, Port}; @@ -187,13 +187,14 @@ pub fn lower_typed_project(project: &TypedProject) -> Result, Low } } + add_dependency_edges(&mut dag, project, &endpoints_by_full, &endpoints_by_name); + add_makegen_scaffolding(&mut dag, &endpoints_by_full); + add_service_transport_triplets(&mut dag, project); + if dag.nodes.is_empty() { return Err(LowerError::NoLowerableItems); } - add_dependency_edges(&mut dag, project, &endpoints_by_full, &endpoints_by_name); - add_makegen_scaffolding(&mut dag, &endpoints_by_full); - Ok(dag) } @@ -777,6 +778,40 @@ fn expansion_suffix(item_name: &str, expansion_count: usize) -> String { } } +fn sanitize_identifier(value: &str) -> String { + let mut out = value + .chars() + .map(|ch| if ch.is_ascii_alphanumeric() { ch } else { '_' }) + .collect::(); + if out.is_empty() { + out.push('_'); + } + out +} + +fn type_expr_to_string(expr: &TypeExpr) -> String { + match expr { + TypeExpr::Named(name) => name.clone(), + TypeExpr::Generic(name, args) => format!( + "{name}<{}>", + args.iter() + .map(type_expr_to_string) + .collect::>() + .join(", ") + ), + TypeExpr::Optional(inner) => format!("{}?", type_expr_to_string(inner)), + TypeExpr::Annotated(inner, annotations) => format!( + "{} {}", + type_expr_to_string(inner), + annotations + .iter() + .map(|annotation| format!("@{}", annotation.name)) + .collect::>() + .join(" ") + ), + } +} + fn add_makegen_scaffolding( dag: &mut Dag, endpoints_by_full: &HashMap<(String, String), LoweredEndpoint>, @@ -844,6 +879,128 @@ fn add_makegen_scaffolding( } } +fn add_service_transport_triplets(dag: &mut Dag, project: &TypedProject) { + for module in &project.modules { + let module_name = module.module_path.join("."); + for item in &module.ast.items { + let Item::ServiceDef(service) = &item.node else { + continue; + }; + if service.implements.is_none() { + continue; + } + + for operation in &service.operations { + let suffix = sanitize_identifier(&format!( + "{module_name}_{}_{}", + service.name, operation.name + )); + let prepare_id = format!("prepare_transport_{suffix}"); + let execute_id = format!("execute_transport_{suffix}"); + let parse_id = format!("parse_transport_{suffix}"); + + add_node_once( + dag, + Node::opaque( + prepare_id.clone(), + operation + .inputs + .iter() + .map(|field| { + let ty = type_expr_to_string(&field.ty); + Port::with_cardinality( + field.name.as_str(), + ty.as_str(), + Cardinality::ONE, + ) + }) + .collect::>(), + vec![Port::scalar("request", "TransportRequest")], + LoweredOp::Callable { + module: module_name.clone(), + kind: CallableKind::Pattern, + name: format!( + "service_transport::prepare::{}::{}", + service.name, operation.name + ), + }, + ), + ); + add_node_once( + dag, + Node::opaque( + execute_id.clone(), + vec![Port::scalar("request", "TransportRequest")], + vec![Port::scalar("response", "TransportResponse")], + LoweredOp::Callable { + module: module_name.clone(), + kind: CallableKind::Pattern, + name: format!( + "service_transport::execute::{}::{}", + service.name, operation.name + ), + }, + ), + ); + let parse_outputs = if operation.outputs.is_empty() { + vec![Port::scalar("result", "Unit")] + } else { + operation + .outputs + .iter() + .map(|field| { + let ty = type_expr_to_string(&field.ty); + Port::with_cardinality( + field.name.as_str(), + ty.as_str(), + Cardinality::ONE, + ) + }) + .collect::>() + }; + add_node_once( + dag, + Node::opaque( + parse_id.clone(), + vec![Port::scalar("response", "TransportResponse")], + parse_outputs, + LoweredOp::Callable { + module: module_name.clone(), + kind: CallableKind::Pattern, + name: format!( + "service_transport::parse::{}::{}", + service.name, operation.name + ), + }, + ), + ); + + add_edge_once( + dag, + prepare_id.as_str(), + "request", + execute_id.as_str(), + "request", + ); + add_edge_once( + dag, + execute_id.as_str(), + "response", + parse_id.as_str(), + "response", + ); + } + } + } +} + +fn add_node_once(dag: &mut Dag, node: Node) { + if dag.nodes.iter().any(|existing| existing.id.0 == node.id.0) { + return; + } + dag.add_node(node); +} + fn add_edge_once(dag: &mut Dag, from: &str, from_port: &str, to: &str, to_port: &str) { if dag.edges.iter().any(|edge| { edge.from_node.0 == from @@ -1097,6 +1254,49 @@ mod tests { } } + #[test] + fn interface_backed_service_lowers_transport_triplet_nodes() { + let typed = typed_project_from_sources(&[( + "dsl/services/storage.dag", + r#"module sample.services +interface Storage { + capability read { + input { path: String } + output { body: String } + } +} +service FsStorage implements Storage { + operation read(path: String) -> { body: String } +}"#, + )]); + let dag = lower_typed_project(&typed).expect("lowering should succeed"); + let node_ids = dag + .nodes + .iter() + .map(|node| node.id.0.as_str()) + .collect::>(); + let suffix = "sample_services_FsStorage_read"; + let prepare = format!("prepare_transport_{suffix}"); + let execute = format!("execute_transport_{suffix}"); + let parse = format!("parse_transport_{suffix}"); + + assert!(node_ids.contains(&prepare.as_str())); + assert!(node_ids.contains(&execute.as_str())); + assert!(node_ids.contains(&parse.as_str())); + assert!(dag.edges.iter().any(|edge| { + edge.from_node.0 == prepare.as_str() + && edge.from_port.0 == "request" + && edge.to_node.0 == execute.as_str() + && edge.to_port.0 == "request" + })); + assert!(dag.edges.iter().any(|edge| { + edge.from_node.0 == execute.as_str() + && edge.from_port.0 == "response" + && edge.to_node.0 == parse.as_str() + && edge.to_port.0 == "response" + })); + } + #[test] fn lower_errors_when_no_callable_items_exist() { let typed = typed_project_from_sources(&[( From 5962676c20a21cfb44588058a8e8ce5787b3af01 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 06:36:11 +0000 Subject: [PATCH 016/169] Wire service-call expressions to transport parse nodes Co-authored-by: Brian Searls --- core/daglang/daglang-lower/src/lib.rs | 245 +++++++++++++++++++++++++- 1 file changed, 243 insertions(+), 2 deletions(-) diff --git a/core/daglang/daglang-lower/src/lib.rs b/core/daglang/daglang-lower/src/lib.rs index cd46a344f09..8e657523fb6 100644 --- a/core/daglang/daglang-lower/src/lib.rs +++ b/core/daglang/daglang-lower/src/lib.rs @@ -75,6 +75,11 @@ struct LoweredEndpoint { primary_output: String, } +#[derive(Debug, Default, Clone)] +struct ServiceEndpointRegistry { + by_key: HashMap>, +} + /// Errors during lowering. #[derive(Debug)] pub enum LowerError { @@ -189,7 +194,8 @@ pub fn lower_typed_project(project: &TypedProject) -> Result, Low add_dependency_edges(&mut dag, project, &endpoints_by_full, &endpoints_by_name); add_makegen_scaffolding(&mut dag, &endpoints_by_full); - add_service_transport_triplets(&mut dag, project); + let service_registry = add_service_transport_triplets(&mut dag, project); + add_service_call_edges(&mut dag, project, &endpoints_by_full, &service_registry); if dag.nodes.is_empty() { return Err(LowerError::NoLowerableItems); @@ -879,7 +885,11 @@ fn add_makegen_scaffolding( } } -fn add_service_transport_triplets(dag: &mut Dag, project: &TypedProject) { +fn add_service_transport_triplets( + dag: &mut Dag, + project: &TypedProject, +) -> ServiceEndpointRegistry { + let mut registry = ServiceEndpointRegistry::default(); for module in &project.modules { let module_name = module.module_path.join("."); for item in &module.ast.items { @@ -989,9 +999,40 @@ fn add_service_transport_triplets(dag: &mut Dag, project: &TypedProje parse_id.as_str(), "response", ); + + let parse_output = operation + .outputs + .first() + .map(|field| field.name.clone()) + .unwrap_or_else(|| "result".to_string()); + let endpoint = LoweredEndpoint { + node_id: parse_id, + primary_output: parse_output, + }; + register_service_endpoint( + &mut registry, + format!("{}.{}", service.name, operation.name), + endpoint.clone(), + ); + let service_tail = service + .name + .rsplit('.') + .next() + .unwrap_or(service.name.as_str()); + register_service_endpoint( + &mut registry, + format!("{service_tail}.{}", operation.name), + endpoint.clone(), + ); + register_service_endpoint( + &mut registry, + format!("{}.{}.{}", module_name, service.name, operation.name), + endpoint, + ); } } } + registry } fn add_node_once(dag: &mut Dag, node: Node) { @@ -1001,6 +1042,82 @@ fn add_node_once(dag: &mut Dag, node: Node) { dag.add_node(node); } +fn register_service_endpoint( + registry: &mut ServiceEndpointRegistry, + key: String, + endpoint: LoweredEndpoint, +) { + registry + .by_key + .entry(key) + .and_modify(|existing| { + if let Some(current) = existing { + if current != &endpoint { + *existing = None; + } + } + }) + .or_insert(Some(endpoint)); +} + +fn add_service_call_edges( + dag: &mut Dag, + project: &TypedProject, + endpoints_by_full: &HashMap<(String, String), LoweredEndpoint>, + service_registry: &ServiceEndpointRegistry, +) { + for module in &project.modules { + let module_name = module.module_path.join("."); + for item in &module.ast.items { + let Some((item_name, stmts)) = item_callable_body(&item.node) else { + continue; + }; + let Some(target) = + endpoints_by_full.get(&(module_name.clone(), item_name.to_string())) + else { + continue; + }; + let mut service_calls = HashSet::>::new(); + collect_service_calls_from_stmts(stmts, &mut service_calls); + for call_path in service_calls { + let Some(source) = resolve_service_endpoint(&call_path, service_registry) else { + continue; + }; + add_edge_once( + dag, + source.node_id.as_str(), + source.primary_output.as_str(), + target.node_id.as_str(), + "__deps", + ); + } + } + } +} + +fn resolve_service_endpoint( + call_path: &[String], + registry: &ServiceEndpointRegistry, +) -> Option { + if call_path.len() < 2 { + return None; + } + let operation = call_path.last()?; + let service_name = call_path[..call_path.len() - 1].join("."); + let short_service = call_path[call_path.len() - 2].clone(); + let keys = [ + format!("{service_name}.{operation}"), + format!("{short_service}.{operation}"), + call_path.join("."), + ]; + for key in keys { + if let Some(Some(endpoint)) = registry.by_key.get(&key) { + return Some(endpoint.clone()); + } + } + None +} + fn add_edge_once(dag: &mut Dag, from: &str, from_port: &str, to: &str, to_port: &str) { if dag.edges.iter().any(|edge| { edge.from_node.0 == from @@ -1037,6 +1154,21 @@ fn collect_calls_from_stmts(stmts: &[Stmt], calls: &mut HashSet) { } } +fn collect_service_calls_from_stmts(stmts: &[Stmt], calls: &mut HashSet>) { + for stmt in stmts { + match stmt { + Stmt::Let(_, expr) | Stmt::Assign(_, expr) | Stmt::Expr(expr) => { + collect_service_calls_from_expr(expr, calls); + } + Stmt::Return(fields) => { + for (_, expr) in fields { + collect_service_calls_from_expr(expr, calls); + } + } + } + } +} + fn collect_calls_from_expr(expr: &Expr, calls: &mut HashSet) { match expr { Expr::Call(name, args) => { @@ -1120,6 +1252,87 @@ fn collect_calls_from_expr(expr: &Expr, calls: &mut HashSet) { } } +fn collect_service_calls_from_expr(expr: &Expr, calls: &mut HashSet>) { + match expr { + Expr::Call(_, args) => { + for (_, arg) in args { + collect_service_calls_from_expr(arg, calls); + } + } + Expr::ServiceCall(path, args) => { + calls.insert(path.clone()); + for (_, arg) in args { + collect_service_calls_from_expr(arg, calls); + } + } + Expr::FieldAccess(base, _) => collect_service_calls_from_expr(base, calls), + Expr::BinOp(lhs, _, rhs) => { + collect_service_calls_from_expr(lhs, calls); + collect_service_calls_from_expr(rhs, calls); + } + Expr::UnaryOp(_, inner) => collect_service_calls_from_expr(inner, calls), + Expr::StringInterp(parts) => { + for part in parts { + if let StringPart::Expr(inner) = part { + collect_service_calls_from_expr(inner, calls); + } + } + } + Expr::Record(_, fields) => { + for (_, value) in fields { + collect_service_calls_from_expr(value, calls); + } + } + Expr::Match(scrutinee, arms) => { + collect_service_calls_from_expr(scrutinee, calls); + for arm in arms { + if let Some(guard) = &arm.guard { + collect_service_calls_from_expr(guard, calls); + } + collect_service_calls_from_expr(&arm.body, calls); + } + } + Expr::If(cond, then_expr, else_expr) => { + collect_service_calls_from_expr(cond, calls); + collect_service_calls_from_expr(then_expr, calls); + if let Some(otherwise) = else_expr { + collect_service_calls_from_expr(otherwise, calls); + } + } + Expr::For(_, iterable, body) => { + collect_service_calls_from_expr(iterable, calls); + collect_service_calls_from_expr(body, calls); + } + Expr::Pipe(lhs, rhs) => { + collect_service_calls_from_expr(lhs, calls); + collect_service_calls_from_expr(rhs, calls); + } + Expr::Lambda(_, body) => collect_service_calls_from_expr(body, calls), + Expr::List(items) => { + for item in items { + collect_service_calls_from_expr(item, calls); + } + } + Expr::Map(entries) => { + for (key, value) in entries { + collect_service_calls_from_expr(key, calls); + collect_service_calls_from_expr(value, calls); + } + } + Expr::Guarded(inner, guard) => { + collect_service_calls_from_expr(inner, calls); + collect_service_calls_from_expr(guard, calls); + } + Expr::After(inner, _) => collect_service_calls_from_expr(inner, calls), + Expr::Return(fields) => { + for (_, value) in fields { + collect_service_calls_from_expr(value, calls); + } + } + Expr::Literal(_) | Expr::Ident(_) => {} + } +} + fn should_track_call(name: &str) -> bool { !matches!(name, "" | "as" | "with" | "fn") } @@ -1297,6 +1510,34 @@ service FsStorage implements Storage { })); } + #[test] + fn service_calls_link_parse_triplet_output_into_caller_dependencies() { + let typed = typed_project_from_sources(&[( + "dsl/services/storage_calls.dag", + r#"module sample.services +interface Storage { + capability read { + input { path: String } + output { body: String } + } +} +service FsStorage implements Storage { + operation read(path: String) -> { body: String } +} +func run(path: String) -> { body: String } { + let response = FsStorage.read(path: path) + return { body: response.body } +}"#, + )]); + let dag = lower_typed_project(&typed).expect("lowering should succeed"); + let parse_node = "parse_transport_sample_services_FsStorage_read"; + assert!(dag.edges.iter().any(|edge| { + edge.from_node.0 == parse_node + && edge.to_node.0 == "sample.services::run" + && edge.to_port.0 == "__deps" + })); + } + #[test] fn lower_errors_when_no_callable_items_exist() { let typed = typed_project_from_sources(&[( From 19aee88d7db806f60d6fafdf78f4a57ad70b6dc6 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 06:38:05 +0000 Subject: [PATCH 017/169] Lower resource acquire/release lifecycle nodes Co-authored-by: Brian Searls --- core/daglang/daglang-lower/src/lib.rs | 89 +++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/core/daglang/daglang-lower/src/lib.rs b/core/daglang/daglang-lower/src/lib.rs index 8e657523fb6..415ccdcdd69 100644 --- a/core/daglang/daglang-lower/src/lib.rs +++ b/core/daglang/daglang-lower/src/lib.rs @@ -196,6 +196,7 @@ pub fn lower_typed_project(project: &TypedProject) -> Result, Low add_makegen_scaffolding(&mut dag, &endpoints_by_full); let service_registry = add_service_transport_triplets(&mut dag, project); add_service_call_edges(&mut dag, project, &endpoints_by_full, &service_registry); + add_resource_lifecycle_nodes(&mut dag, project); if dag.nodes.is_empty() { return Err(LowerError::NoLowerableItems); @@ -1095,6 +1096,64 @@ fn add_service_call_edges( } } +fn add_resource_lifecycle_nodes(dag: &mut Dag, project: &TypedProject) { + for module in &project.modules { + let module_name = module.module_path.join("."); + for item in &module.ast.items { + let Item::ResourceDef(resource) = &item.node else { + continue; + }; + let suffix = sanitize_identifier(&format!("{module_name}_{}", resource.name)); + let acquire_id = format!("acquire_resource_{suffix}"); + let release_id = format!("release_resource_{suffix}"); + let mut has_acquire = false; + let mut has_release = false; + + if resource.acquire.is_some() { + add_node_once( + dag, + Node::opaque( + acquire_id.clone(), + vec![], + vec![Port::scalar("resource_handle", "ResourceHandle")], + LoweredOp::Callable { + module: module_name.clone(), + kind: CallableKind::Pattern, + name: format!("resource_lifecycle::acquire::{}", resource.name), + }, + ), + ); + has_acquire = true; + } + if resource.release.is_some() { + add_node_once( + dag, + Node::opaque( + release_id.clone(), + vec![Port::scalar("resource_handle", "ResourceHandle")], + vec![Port::scalar("released", "Bool")], + LoweredOp::Callable { + module: module_name.clone(), + kind: CallableKind::Pattern, + name: format!("resource_lifecycle::release::{}", resource.name), + }, + ), + ); + has_release = true; + } + if has_acquire && has_release { + add_edge_once( + dag, + acquire_id.as_str(), + "resource_handle", + release_id.as_str(), + "resource_handle", + ); + } + } + } +} + fn resolve_service_endpoint( call_path: &[String], registry: &ServiceEndpointRegistry, @@ -1538,6 +1597,36 @@ func run(path: String) -> { body: String } { })); } + #[test] + fn resource_acquire_release_lower_to_lifecycle_nodes() { + let typed = typed_project_from_sources(&[( + "dsl/resources/fs.dag", + r#"module sample.resources +resource TempFile { + acquire { + let path = "/tmp/file" + } + release { + let done = true + } +} +func run() -> { ok: Bool } { + return { ok: true } +}"#, + )]); + let dag = lower_typed_project(&typed).expect("lowering should succeed"); + let acquire = "acquire_resource_sample_resources_TempFile"; + let release = "release_resource_sample_resources_TempFile"; + assert!(dag.nodes.iter().any(|node| node.id.0 == acquire)); + assert!(dag.nodes.iter().any(|node| node.id.0 == release)); + assert!(dag.edges.iter().any(|edge| { + edge.from_node.0 == acquire + && edge.from_port.0 == "resource_handle" + && edge.to_node.0 == release + && edge.to_port.0 == "resource_handle" + })); + } + #[test] fn lower_errors_when_no_callable_items_exist() { let typed = typed_project_from_sources(&[( From bb3512230ff5bd085dc460719d87b01f8218de6f Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 06:41:46 +0000 Subject: [PATCH 018/169] Derive lifecycle and transport-specific obligation counters Co-authored-by: Brian Searls --- core/daglang/daglang-derive/src/lib.rs | 155 ++++++++++++++++++++++--- 1 file changed, 136 insertions(+), 19 deletions(-) diff --git a/core/daglang/daglang-derive/src/lib.rs b/core/daglang/daglang-derive/src/lib.rs index 97461198f6a..3e9e560818c 100644 --- a/core/daglang/daglang-derive/src/lib.rs +++ b/core/daglang/daglang-derive/src/lib.rs @@ -46,6 +46,11 @@ pub struct TestObligations { pub dry_run_completion_required: bool, pub transport_execution_targets: usize, pub pure_node_determinism_targets: usize, + pub service_transport_prepare_targets: usize, + pub service_transport_execute_targets: usize, + pub service_transport_parse_targets: usize, + pub resource_acquire_targets: usize, + pub resource_release_targets: usize, } /// Metadata summary for lowered modules. @@ -109,27 +114,16 @@ pub fn derive_artifacts(dag: &Dag) -> Result]) -> Vec { by_module.into_values().collect() } +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +struct ObligationCounts { + transport_execution_targets: usize, + pure_node_determinism_targets: usize, + service_transport_prepare_targets: usize, + service_transport_execute_targets: usize, + service_transport_parse_targets: usize, + resource_acquire_targets: usize, + resource_release_targets: usize, +} + +fn derive_obligation_counts(nodes: &[Node]) -> ObligationCounts { + let mut counts = ObligationCounts::default(); + for node in nodes { + if node + .inputs + .iter() + .any(|port| port.type_id.0 == "TransportRequest") + { + counts.transport_execution_targets += 1; + } else { + counts.pure_node_determinism_targets += 1; + } + let Some(LoweredOp::Callable { name, .. }) = node.body.as_opaque() else { + continue; + }; + if name.starts_with("service_transport::prepare::") { + counts.service_transport_prepare_targets += 1; + } else if name.starts_with("service_transport::execute::") { + counts.service_transport_execute_targets += 1; + } else if name.starts_with("service_transport::parse::") { + counts.service_transport_parse_targets += 1; + } else if name.starts_with("resource_lifecycle::acquire::") { + counts.resource_acquire_targets += 1; + } else if name.starts_with("resource_lifecycle::release::") { + counts.resource_release_targets += 1; + } + } + counts +} + trait NodeBodyExt { fn as_opaque(&self) -> Option<&LoweredOp>; } @@ -329,4 +364,86 @@ mod tests { .iter() .any(|module| module.module == "pipelines.ci" && module.pipeline_count == 1)); } + + #[test] + fn derive_obligations_count_transport_and_lifecycle_targets() { + let mut dag = Dag::new(); + dag.add_node(Node::opaque( + "prepare_transport", + vec![Port::scalar("path", "String")], + vec![Port::scalar("request", "TransportRequest")], + LoweredOp::Callable { + module: "sample.services".to_string(), + kind: CallableKind::Pattern, + name: "service_transport::prepare::FsStorage::read".to_string(), + }, + )); + dag.add_node(Node::opaque( + "execute_transport", + vec![Port::scalar("request", "TransportRequest")], + vec![Port::scalar("response", "TransportResponse")], + LoweredOp::Callable { + module: "sample.services".to_string(), + kind: CallableKind::Pattern, + name: "service_transport::execute::FsStorage::read".to_string(), + }, + )); + dag.add_node(Node::opaque( + "parse_transport", + vec![Port::scalar("response", "TransportResponse")], + vec![Port::scalar("body", "String")], + LoweredOp::Callable { + module: "sample.services".to_string(), + kind: CallableKind::Pattern, + name: "service_transport::parse::FsStorage::read".to_string(), + }, + )); + dag.add_node(Node::opaque( + "acquire_resource", + vec![], + vec![Port::scalar("resource_handle", "ResourceHandle")], + LoweredOp::Callable { + module: "sample.resources".to_string(), + kind: CallableKind::Pattern, + name: "resource_lifecycle::acquire::TempFile".to_string(), + }, + )); + dag.add_node(Node::opaque( + "release_resource", + vec![Port::scalar("resource_handle", "ResourceHandle")], + vec![Port::scalar("released", "Bool")], + LoweredOp::Callable { + module: "sample.resources".to_string(), + kind: CallableKind::Pattern, + name: "resource_lifecycle::release::TempFile".to_string(), + }, + )); + dag.add_edge(Edge::new( + "prepare_transport", + "request", + "execute_transport", + "request", + )); + dag.add_edge(Edge::new( + "execute_transport", + "response", + "parse_transport", + "response", + )); + dag.add_edge(Edge::new( + "acquire_resource", + "resource_handle", + "release_resource", + "resource_handle", + )); + + let artifacts = derive_artifacts(&dag).expect("derivation should succeed"); + assert_eq!(artifacts.obligations.transport_execution_targets, 1); + assert_eq!(artifacts.obligations.pure_node_determinism_targets, 4); + assert_eq!(artifacts.obligations.service_transport_prepare_targets, 1); + assert_eq!(artifacts.obligations.service_transport_execute_targets, 1); + assert_eq!(artifacts.obligations.service_transport_parse_targets, 1); + assert_eq!(artifacts.obligations.resource_acquire_targets, 1); + assert_eq!(artifacts.obligations.resource_release_targets, 1); + } } From 5000901d78f8f7524c724c5cfd1f372579ea1d67 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 06:43:34 +0000 Subject: [PATCH 019/169] Expose obligation counters in manifest CLI output Co-authored-by: Brian Searls --- core/daglang/daglang-cli/src/compile.rs | 42 ++++++++++++++++++- core/daglang/daglang-cli/src/main.rs | 2 +- .../daglang-cli/tests/compile_commands.rs | 2 + 3 files changed, 43 insertions(+), 3 deletions(-) diff --git a/core/daglang/daglang-cli/src/compile.rs b/core/daglang/daglang-cli/src/compile.rs index 727aa054e45..c82cc678d9d 100644 --- a/core/daglang/daglang-cli/src/compile.rs +++ b/core/daglang/daglang-cli/src/compile.rs @@ -1,7 +1,7 @@ use std::path::PathBuf; use crate::pipeline::PipelineContext; -use daglang_derive::{derive_artifacts, DerivedArtifacts, ProgressManifest}; +use daglang_derive::{derive_artifacts, DerivedArtifacts}; use daglang_emit::{emit_rust_bundle, EmissionBundle}; use daglang_lower::{lower_typed_project, LoweredOp}; use daglang_resolve::{ModuleGraph, ResolveError, ResolvedModule}; @@ -223,7 +223,9 @@ pub fn render_expand(dag: &Dag) -> String { out } -pub fn render_manifest(manifest: &ProgressManifest) -> String { +pub fn render_manifest(derived: &DerivedArtifacts) -> String { + let manifest = &derived.manifest; + let obligations = &derived.obligations; let mut out = String::new(); out.push_str("ProgressManifest:\n"); out.push_str(&format!(" total_nodes: {}\n", manifest.total_nodes)); @@ -248,6 +250,39 @@ pub fn render_manifest(manifest: &ProgressManifest) -> String { manifest.boundary_nodes.join(", ") } )); + out.push_str("TestObligations:\n"); + out.push_str(&format!( + " dry_run_completion_required: {}\n", + obligations.dry_run_completion_required + )); + out.push_str(&format!( + " transport_execution_targets: {}\n", + obligations.transport_execution_targets + )); + out.push_str(&format!( + " pure_node_determinism_targets: {}\n", + obligations.pure_node_determinism_targets + )); + out.push_str(&format!( + " service_transport_prepare_targets: {}\n", + obligations.service_transport_prepare_targets + )); + out.push_str(&format!( + " service_transport_execute_targets: {}\n", + obligations.service_transport_execute_targets + )); + out.push_str(&format!( + " service_transport_parse_targets: {}\n", + obligations.service_transport_parse_targets + )); + out.push_str(&format!( + " resource_acquire_targets: {}\n", + obligations.resource_acquire_targets + )); + out.push_str(&format!( + " resource_release_targets: {}\n", + obligations.resource_release_targets + )); out } @@ -281,6 +316,9 @@ mod tests { assert!(!output.lowered_dag.nodes.is_empty()); assert!(output.derived.manifest.total_nodes > 0); assert!(!output.emitted.files.is_empty()); + let rendered = render_manifest(&output.derived); + assert!(rendered.contains("TestObligations:")); + assert!(rendered.contains("service_transport_prepare_targets:")); } #[test] diff --git a/core/daglang/daglang-cli/src/main.rs b/core/daglang/daglang-cli/src/main.rs index d68d93d5557..9aff4dc6810 100644 --- a/core/daglang/daglang-cli/src/main.rs +++ b/core/daglang/daglang-cli/src/main.rs @@ -70,7 +70,7 @@ fn main() { let context = build_context(args.get(2)); match compile_from_context(&context) { Ok(output) => { - println!("{}", render_manifest(&output.derived.manifest)); + println!("{}", render_manifest(&output.derived)); } Err(error) => { eprintln!("{error}"); diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index 3cd0eed32f6..ea4ee4c87bc 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -95,6 +95,8 @@ fn manifest_command_shows_derived_progress_manifest() { assert!(stdout.contains("ProgressManifest:")); assert!(stdout.contains("total_nodes:")); assert!(stdout.contains("waves:")); + assert!(stdout.contains("TestObligations:")); + assert!(stdout.contains("service_transport_prepare_targets:")); } #[test] From 2b4011b3bd2311816b3e8fe68bdd127498ce8dd7 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 06:44:53 +0000 Subject: [PATCH 020/169] Test manifest obligations with transport and lifecycle fixture Co-authored-by: Brian Searls --- .../daglang-cli/tests/compile_commands.rs | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index ea4ee4c87bc..a9afb638f6e 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -99,6 +99,59 @@ fn manifest_command_shows_derived_progress_manifest() { assert!(stdout.contains("service_transport_prepare_targets:")); } +#[test] +fn manifest_command_reports_non_zero_transport_and_lifecycle_obligations() { + let fixture = unique_temp_file("manifest_obligations"); + std::fs::write( + &fixture, + r#"module sample.obligations +interface Storage { + capability read { + input { path: String } + output { body: String } + } +} +service FsStorage implements Storage { + operation read(path: String) -> { body: String } +} +resource TempFile { + acquire { + let path = "/tmp/file" + } + release { + let done = true + } +} +func run(path: String) -> { body: String } { + let response = FsStorage.read(path: path) + return { body: response.body } +} +"#, + ) + .expect("failed to write fixture"); + + let output = Command::new(daglang_bin()) + .arg("manifest") + .arg(&fixture) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang manifest on fixture"); + + assert!( + output.status.success(), + "manifest command failed: {}", + String::from_utf8_lossy(&output.stderr) + ); + let stdout = String::from_utf8_lossy(&output.stdout); + assert!(stdout.contains("service_transport_prepare_targets: 1")); + assert!(stdout.contains("service_transport_execute_targets: 1")); + assert!(stdout.contains("service_transport_parse_targets: 1")); + assert!(stdout.contains("resource_acquire_targets: 1")); + assert!(stdout.contains("resource_release_targets: 1")); + + std::fs::remove_file(fixture).expect("failed to cleanup fixture"); +} + #[test] fn viz_command_renders_mermaid_for_compiled_file() { let output = Command::new(daglang_bin()) From effad5c48db32cd3cda38c5f17c39b081f105b27 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 06:46:29 +0000 Subject: [PATCH 021/169] Report unresolved service call endpoints during lowering Co-authored-by: Brian Searls --- core/daglang/daglang-lower/src/lib.rs | 49 +++++++++++++++++++++++++-- 1 file changed, 46 insertions(+), 3 deletions(-) diff --git a/core/daglang/daglang-lower/src/lib.rs b/core/daglang/daglang-lower/src/lib.rs index 415ccdcdd69..87bbd7bf745 100644 --- a/core/daglang/daglang-lower/src/lib.rs +++ b/core/daglang/daglang-lower/src/lib.rs @@ -91,6 +91,11 @@ pub enum LowerError { InvalidAcquireBlock { resource: String, reason: String }, /// Interface could not be resolved to a concrete resource. UnresolvedInterface { interface: String }, + /// Service call in a callable body could not be resolved to a transport endpoint. + UnresolvedServiceCall { + caller: String, + service_call: String, + }, /// No executable declarations were available for lowering. NoLowerableItems, } @@ -111,6 +116,13 @@ impl std::fmt::Display for LowerError { Self::UnresolvedInterface { interface } => { write!(f, "unresolved interface `{interface}`") } + Self::UnresolvedServiceCall { + caller, + service_call, + } => write!( + f, + "unresolved service call `{service_call}` in `{caller}`" + ), Self::NoLowerableItems => write!(f, "no callable or pipeline declarations to lower"), } } @@ -195,7 +207,7 @@ pub fn lower_typed_project(project: &TypedProject) -> Result, Low add_dependency_edges(&mut dag, project, &endpoints_by_full, &endpoints_by_name); add_makegen_scaffolding(&mut dag, &endpoints_by_full); let service_registry = add_service_transport_triplets(&mut dag, project); - add_service_call_edges(&mut dag, project, &endpoints_by_full, &service_registry); + add_service_call_edges(&mut dag, project, &endpoints_by_full, &service_registry)?; add_resource_lifecycle_nodes(&mut dag, project); if dag.nodes.is_empty() { @@ -1066,7 +1078,7 @@ fn add_service_call_edges( project: &TypedProject, endpoints_by_full: &HashMap<(String, String), LoweredEndpoint>, service_registry: &ServiceEndpointRegistry, -) { +) -> Result<(), LowerError> { for module in &project.modules { let module_name = module.module_path.join("."); for item in &module.ast.items { @@ -1082,7 +1094,10 @@ fn add_service_call_edges( collect_service_calls_from_stmts(stmts, &mut service_calls); for call_path in service_calls { let Some(source) = resolve_service_endpoint(&call_path, service_registry) else { - continue; + return Err(LowerError::UnresolvedServiceCall { + caller: format!("{module_name}::{item_name}"), + service_call: call_path.join("."), + }); }; add_edge_once( dag, @@ -1094,6 +1109,7 @@ fn add_service_call_edges( } } } + Ok(()) } fn add_resource_lifecycle_nodes(dag: &mut Dag, project: &TypedProject) { @@ -1597,6 +1613,33 @@ func run(path: String) -> { body: String } { })); } + #[test] + fn unresolved_service_call_reports_lower_error() { + let typed = typed_project_from_sources(&[( + "dsl/services/unresolved_call.dag", + r#"module sample.services +interface Storage { + capability read { + input { path: String } + output { body: String } + } +} +service FsStorage implements Storage { + operation read(path: String) -> { body: String } +} +func run(path: String) -> { body: String } { + let response = MissingStorage.read(path: path) + return { body: response.body } +}"#, + )]); + let error = lower_typed_project(&typed).expect_err("lowering should fail"); + assert!(matches!( + error, + LowerError::UnresolvedServiceCall { caller, service_call } + if caller == "sample.services::run" && service_call == "MissingStorage.read" + )); + } + #[test] fn resource_acquire_release_lower_to_lifecycle_nodes() { let typed = typed_project_from_sources(&[( From dba0cc26279a83f85b34cabbdcdc47abeacd9048 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 06:47:31 +0000 Subject: [PATCH 022/169] Add CLI regression for unresolved service-call lower errors Co-authored-by: Brian Searls --- .../daglang-cli/tests/compile_commands.rs | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index a9afb638f6e..07938fd6ed2 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -192,6 +192,47 @@ fn compile_command_reports_diagnostics_for_invalid_file() { std::fs::remove_file(broken).expect("failed to remove temp broken source"); } +#[test] +fn expand_command_reports_unresolved_service_call_lower_error() { + let fixture = unique_temp_file("unresolved_service_call"); + std::fs::write( + &fixture, + r#"module sample.services +interface Storage { + capability read { + input { path: String } + output { body: String } + } +} +service FsStorage implements Storage { + operation read(path: String) -> { body: String } +} +func run(path: String) -> { body: String } { + let response = MissingStorage.read(path: path) + return { body: response.body } +} +"#, + ) + .expect("failed to write fixture"); + + let output = Command::new(daglang_bin()) + .arg("expand") + .arg(&fixture) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang expand on unresolved service fixture"); + + assert!( + !output.status.success(), + "expand should fail when service call endpoint cannot be resolved" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("lower error: unresolved service call")); + assert!(stderr.contains("MissingStorage.read")); + + std::fs::remove_file(fixture).expect("failed to cleanup fixture"); +} + #[test] fn compile_command_directory_mode_fails_on_unresolved_imports() { let root = unique_temp_dir("unresolved_import"); From 15e8a16d5223499642d7c631145ab8212ef09a92 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 06:51:15 +0000 Subject: [PATCH 023/169] Wire func uses clauses to resource lifecycle nodes Co-authored-by: Brian Searls --- core/daglang/daglang-lower/src/lib.rs | 205 +++++++++++++++++++++++++- 1 file changed, 203 insertions(+), 2 deletions(-) diff --git a/core/daglang/daglang-lower/src/lib.rs b/core/daglang/daglang-lower/src/lib.rs index 87bbd7bf745..5f7fd1b471d 100644 --- a/core/daglang/daglang-lower/src/lib.rs +++ b/core/daglang/daglang-lower/src/lib.rs @@ -80,6 +80,17 @@ struct ServiceEndpointRegistry { by_key: HashMap>, } +#[derive(Debug, Default, Clone)] +struct ResourceLifecycleRegistry { + by_key: HashMap>, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +struct ResourceLifecycleEndpoint { + acquire_node: Option, + release_node: Option, +} + /// Errors during lowering. #[derive(Debug)] pub enum LowerError { @@ -96,6 +107,12 @@ pub enum LowerError { caller: String, service_call: String, }, + /// `uses` clause references an unknown resource/interface lifecycle source. + UnresolvedUsedResource { + caller: String, + binding: String, + resource_type: String, + }, /// No executable declarations were available for lowering. NoLowerableItems, } @@ -123,6 +140,14 @@ impl std::fmt::Display for LowerError { f, "unresolved service call `{service_call}` in `{caller}`" ), + Self::UnresolvedUsedResource { + caller, + binding, + resource_type, + } => write!( + f, + "unresolved used resource `{binding}: {resource_type}` in `{caller}`" + ), Self::NoLowerableItems => write!(f, "no callable or pipeline declarations to lower"), } } @@ -208,7 +233,8 @@ pub fn lower_typed_project(project: &TypedProject) -> Result, Low add_makegen_scaffolding(&mut dag, &endpoints_by_full); let service_registry = add_service_transport_triplets(&mut dag, project); add_service_call_edges(&mut dag, project, &endpoints_by_full, &service_registry)?; - add_resource_lifecycle_nodes(&mut dag, project); + let resource_registry = add_resource_lifecycle_nodes(&mut dag, project); + add_used_resource_edges(&mut dag, project, &endpoints_by_full, &resource_registry)?; if dag.nodes.is_empty() { return Err(LowerError::NoLowerableItems); @@ -1073,6 +1099,24 @@ fn register_service_endpoint( .or_insert(Some(endpoint)); } +fn register_resource_endpoint( + registry: &mut ResourceLifecycleRegistry, + key: String, + endpoint: ResourceLifecycleEndpoint, +) { + registry + .by_key + .entry(key) + .and_modify(|existing| { + if let Some(current) = existing { + if current != &endpoint { + *existing = None; + } + } + }) + .or_insert(Some(endpoint)); +} + fn add_service_call_edges( dag: &mut Dag, project: &TypedProject, @@ -1112,7 +1156,93 @@ fn add_service_call_edges( Ok(()) } -fn add_resource_lifecycle_nodes(dag: &mut Dag, project: &TypedProject) { +fn add_used_resource_edges( + dag: &mut Dag, + project: &TypedProject, + endpoints_by_full: &HashMap<(String, String), LoweredEndpoint>, + resource_registry: &ResourceLifecycleRegistry, +) -> Result<(), LowerError> { + for module in &project.modules { + let module_name = module.module_path.join("."); + for item in &module.ast.items { + let Some((item_name, uses)) = item_callable_uses(&item.node) else { + continue; + }; + let Some(target) = + endpoints_by_full.get(&(module_name.clone(), item_name.to_string())) + else { + continue; + }; + for usage in uses { + let resource_type = resource_type_name(&usage.resource_type); + let Some(endpoint) = resolve_resource_endpoint( + module_name.as_str(), + resource_type.as_str(), + resource_registry, + ) else { + return Err(LowerError::UnresolvedUsedResource { + caller: format!("{module_name}::{item_name}"), + binding: usage.binding.clone(), + resource_type, + }); + }; + if let Some(acquire_node) = endpoint.acquire_node { + add_edge_once( + dag, + acquire_node.as_str(), + "resource_handle", + target.node_id.as_str(), + "__deps", + ); + } + if let Some(release_node) = endpoint.release_node { + add_edge_once( + dag, + target.node_id.as_str(), + target.primary_output.as_str(), + release_node.as_str(), + "resource_handle", + ); + } + } + } + } + Ok(()) +} + +fn resolve_resource_endpoint( + module_name: &str, + resource_type: &str, + registry: &ResourceLifecycleRegistry, +) -> Option { + let keys = [ + format!("{module_name}.{resource_type}"), + resource_type.to_string(), + ]; + for key in keys { + if let Some(Some(endpoint)) = registry.by_key.get(&key) { + return Some(endpoint.clone()); + } + } + None +} + +fn resource_type_name(ty: &TypeExpr) -> String { + match ty { + TypeExpr::Named(name) | TypeExpr::Generic(name, _) => canonical_type_name(name), + TypeExpr::Optional(inner) | TypeExpr::Annotated(inner, _) => resource_type_name(inner), + } +} + +fn canonical_type_name(name: &str) -> String { + name.split('<').next().unwrap_or(name).trim().to_string() +} + +fn add_resource_lifecycle_nodes( + dag: &mut Dag, + project: &TypedProject, +) -> ResourceLifecycleRegistry { + let mut registry = ResourceLifecycleRegistry::default(); for module in &project.modules { let module_name = module.module_path.join("."); for item in &module.ast.items { @@ -1166,8 +1296,19 @@ fn add_resource_lifecycle_nodes(dag: &mut Dag, project: &TypedProject "resource_handle", ); } + let endpoint = ResourceLifecycleEndpoint { + acquire_node: has_acquire.then_some(acquire_id), + release_node: has_release.then_some(release_id), + }; + register_resource_endpoint( + &mut registry, + format!("{module_name}.{}", resource.name), + endpoint.clone(), + ); + register_resource_endpoint(&mut registry, resource.name.clone(), endpoint); } } + registry } fn resolve_service_endpoint( @@ -1214,6 +1355,14 @@ fn item_callable_body(item: &Item) -> Option<(&str, &[Stmt])> { } } +fn item_callable_uses(item: &Item) -> Option<(&str, &[daglang_syntax::ast::UsesClause])> { + match item { + Item::FuncDef(def) => Some((def.name.as_str(), def.uses.as_slice())), + Item::PatternDef(def) => Some((def.name.as_str(), def.uses.as_slice())), + _ => None, + } +} + fn collect_calls_from_stmts(stmts: &[Stmt], calls: &mut HashSet) { for stmt in stmts { match stmt { @@ -1670,6 +1819,58 @@ func run() -> { ok: Bool } { })); } + #[test] + fn uses_clause_wires_acquire_and_release_lifecycle_edges() { + let typed = typed_project_from_sources(&[( + "dsl/resources/uses_wiring.dag", + r#"module sample.resources +resource TempFile { + acquire { let path = "/tmp/file" } + release { let done = true } +} +func run() -> { ok: Bool } uses fs: TempFile { + return { ok: true } +}"#, + )]); + let dag = lower_typed_project(&typed).expect("lowering should succeed"); + let acquire = "acquire_resource_sample_resources_TempFile"; + let release = "release_resource_sample_resources_TempFile"; + assert!(dag.edges.iter().any(|edge| { + edge.from_node.0 == acquire + && edge.from_port.0 == "resource_handle" + && edge.to_node.0 == "sample.resources::run" + && edge.to_port.0 == "__deps" + })); + assert!(dag.edges.iter().any(|edge| { + edge.from_node.0 == "sample.resources::run" + && edge.from_port.0 == "ok" + && edge.to_node.0 == release + && edge.to_port.0 == "resource_handle" + })); + } + + #[test] + fn unresolved_uses_clause_reports_lower_error() { + let typed = typed_project_from_sources(&[( + "dsl/resources/unresolved_uses.dag", + r#"module sample.resources +func run() -> { ok: Bool } uses fs: MissingResource { + return { ok: true } +}"#, + )]); + let error = lower_typed_project(&typed).expect_err("lowering should fail"); + assert!(matches!( + error, + LowerError::UnresolvedUsedResource { + caller, + binding, + resource_type, + } if caller == "sample.resources::run" + && binding == "fs" + && resource_type == "MissingResource" + )); + } + #[test] fn lower_errors_when_no_callable_items_exist() { let typed = typed_project_from_sources(&[( From 3dd336805d03a8bad5f345d024b287aeae84cfee Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 06:52:32 +0000 Subject: [PATCH 024/169] Add CLI regression for unresolved uses lower errors Co-authored-by: Brian Searls --- .../daglang-cli/tests/compile_commands.rs | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index 07938fd6ed2..abca7a9a2f5 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -233,6 +233,37 @@ func run(path: String) -> { body: String } { std::fs::remove_file(fixture).expect("failed to cleanup fixture"); } +#[test] +fn expand_command_reports_unresolved_uses_lower_error() { + let fixture = unique_temp_file("unresolved_uses"); + std::fs::write( + &fixture, + r#"module sample.resources +func run() -> { ok: Bool } uses fs: MissingResource { + return { ok: true } +} +"#, + ) + .expect("failed to write fixture"); + + let output = Command::new(daglang_bin()) + .arg("expand") + .arg(&fixture) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang expand on unresolved uses fixture"); + + assert!( + !output.status.success(), + "expand should fail when uses target cannot be resolved" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("lower error: unresolved used resource")); + assert!(stderr.contains("fs: MissingResource")); + + std::fs::remove_file(fixture).expect("failed to cleanup fixture"); +} + #[test] fn compile_command_directory_mode_fails_on_unresolved_imports() { let root = unique_temp_dir("unresolved_import"); From 696676d630f3ab792bf7adda025e546a9827c6ce Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 06:56:53 +0000 Subject: [PATCH 025/169] Typecheck uses-clause resource type resolution in strict mode Co-authored-by: Brian Searls --- core/daglang/daglang-typecheck/src/lib.rs | 177 +++++++++++++++++++--- 1 file changed, 158 insertions(+), 19 deletions(-) diff --git a/core/daglang/daglang-typecheck/src/lib.rs b/core/daglang/daglang-typecheck/src/lib.rs index 3ca286a2732..42631b49f33 100644 --- a/core/daglang/daglang-typecheck/src/lib.rs +++ b/core/daglang/daglang-typecheck/src/lib.rs @@ -24,7 +24,7 @@ use std::collections::{HashMap, HashSet}; use std::path::PathBuf; use daglang_resolve::{ModuleGraph, ResolvedModule}; -use daglang_syntax::ast::{Expr, Field, Item, Param, SourceFile, Stmt, TypeExpr}; +use daglang_syntax::ast::{Expr, Field, Item, Param, SourceFile, Stmt, TypeExpr, UsesClause}; /// A typechecked project snapshot. #[derive(Debug)] @@ -155,6 +155,12 @@ pub enum TypeError { callee: String, argument: String, }, + /// `uses` clause references an unknown resource/interface type. + UnknownUsedResourceType { + item: String, + binding: String, + resource_type: String, + }, } impl std::fmt::Display for TypeError { @@ -239,6 +245,14 @@ impl std::fmt::Display for TypeError { f, "unknown named argument `{argument}` in call to `{callee}` within `{caller}`" ), + Self::UnknownUsedResourceType { + item, + binding, + resource_type, + } => write!( + f, + "unknown used resource type `{resource_type}` for binding `{binding}` in `{item}`" + ), } } } @@ -256,6 +270,7 @@ pub fn typecheck_module_graph_with_options( let known_types = collect_known_types(&graph.modules); let callable_registry = collect_unique_callables(&graph.modules); let interface_registry = collect_interfaces(&graph.modules); + let resource_type_registry = collect_resource_types(&graph.modules); let available_modules = graph .modules .iter() @@ -263,6 +278,13 @@ pub fn typecheck_module_graph_with_options( .collect::>(); let mut errors = Vec::new(); let mut typed_modules = Vec::with_capacity(graph.modules.len()); + let context = TypecheckContext { + known_types: &known_types, + callable_registry: &callable_registry, + interface_registry: &interface_registry, + resource_type_registry: &resource_type_registry, + allow_unresolved_references: options.allow_unresolved_imports, + }; for module in graph.modules { let imports = module @@ -283,14 +305,7 @@ pub fn typecheck_module_graph_with_options( } } } - let signatures = collect_signatures( - &module, - &known_types, - &callable_registry, - &interface_registry, - &module_name, - &mut errors, - ); + let signatures = collect_signatures(&module, &context, &module_name, &mut errors); typed_modules.push(TypedModule { path: module.path, module_path: module.module_path, @@ -309,15 +324,21 @@ pub fn typecheck_module_graph_with_options( } } +struct TypecheckContext<'a> { + known_types: &'a HashSet, + callable_registry: &'a HashMap>, + interface_registry: &'a InterfaceRegistry, + resource_type_registry: &'a ResourceTypeRegistry, + allow_unresolved_references: bool, +} + fn collect_signatures( module: &ResolvedModule, - known_types: &HashSet, - callable_registry: &HashMap>, - interface_registry: &InterfaceRegistry, + context: &TypecheckContext<'_>, module_name: &str, errors: &mut Vec, ) -> Vec { - let mut module_known_types = known_types.clone(); + let mut module_known_types = context.known_types.clone(); for import in &module.ast.imports { if let Some(bindings) = &import.node.bindings { for binding in bindings { @@ -358,7 +379,7 @@ fn collect_signatures( validate_callable_body( &def.name, &def.body.stmts, - callable_registry, + context.callable_registry, errors, ); signatures.push(TypedItemSignature::Fn(TypedCallableSignature { @@ -378,10 +399,17 @@ fn collect_signatures( record_duplicate_item_name(module_name, &def.name, &mut seen_items, errors); validate_params(&def.name, &def.params, &module_known_types, errors); validate_outputs(&def.name, &def.outputs, &module_known_types, errors); + validate_uses_clauses( + &def.name, + &def.uses, + context.resource_type_registry, + context.allow_unresolved_references, + errors, + ); validate_callable_body( &def.name, &def.body.stmts, - callable_registry, + context.callable_registry, errors, ); signatures.push(TypedItemSignature::Func(TypedCallableSignature { @@ -408,10 +436,17 @@ fn collect_signatures( record_duplicate_item_name(module_name, &def.name, &mut seen_items, errors); validate_params(&def.name, &def.params, &module_known_types, errors); validate_outputs(&def.name, &def.outputs, &module_known_types, errors); + validate_uses_clauses( + &def.name, + &def.uses, + context.resource_type_registry, + context.allow_unresolved_references, + errors, + ); validate_callable_body( &def.name, &def.body.stmts, - callable_registry, + context.callable_registry, errors, ); signatures.push(TypedItemSignature::Pattern(TypedCallableSignature { @@ -436,7 +471,7 @@ fn collect_signatures( } Item::ServiceDef(def) => { record_duplicate_item_name(module_name, &def.name, &mut seen_items, errors); - validate_service_interface_conformance(def, interface_registry, errors); + validate_service_interface_conformance(def, context.interface_registry, errors); signatures.push(TypedItemSignature::Service { name: def.name.clone(), operations: def.operations.len(), @@ -444,7 +479,7 @@ fn collect_signatures( } Item::ResourceDef(def) => { record_duplicate_item_name(module_name, &def.name, &mut seen_items, errors); - validate_resource_interface_conformance(def, interface_registry, errors); + validate_resource_interface_conformance(def, context.interface_registry, errors); signatures.push(TypedItemSignature::Resource { name: def.name.clone(), implements: def.implements.clone(), @@ -525,6 +560,12 @@ struct InterfaceRegistry { short: HashMap>, } +#[derive(Debug, Clone, Default)] +struct ResourceTypeRegistry { + full: HashSet, + short: HashMap>, +} + fn collect_interfaces(modules: &[ResolvedModule]) -> InterfaceRegistry { let mut registry = InterfaceRegistry::default(); for module in modules { @@ -561,6 +602,34 @@ fn collect_interfaces(modules: &[ResolvedModule]) -> InterfaceRegistry { registry } +fn collect_resource_types(modules: &[ResolvedModule]) -> ResourceTypeRegistry { + let mut registry = ResourceTypeRegistry::default(); + for module in modules { + let module_name = module.module_path.join("."); + for item in &module.ast.items { + let name = match &item.node { + Item::InterfaceDef(interface) => interface.name.as_str(), + Item::ResourceDef(resource) => resource.name.as_str(), + _ => continue, + }; + let full = format!("{module_name}.{name}"); + registry.full.insert(full.clone()); + registry + .short + .entry(name.to_string()) + .and_modify(|existing| { + if let Some(current) = existing { + if current != &full { + *existing = None; + } + } + }) + .or_insert_with(|| Some(full)); + } + } + registry +} + #[derive(Debug, Clone, PartialEq, Eq)] struct InterfaceContract { capabilities: HashMap, @@ -652,6 +721,28 @@ fn validate_outputs( } } +fn validate_uses_clauses( + item_name: &str, + uses: &[UsesClause], + registry: &ResourceTypeRegistry, + allow_unresolved_references: bool, + errors: &mut Vec, +) { + if allow_unresolved_references { + return; + } + for usage in uses { + let resource_type = canonical_type_name(&type_expr_to_string(&usage.resource_type)); + if resolve_resource_type_name(&resource_type, registry).is_none() { + errors.push(TypeError::UnknownUsedResourceType { + item: item_name.to_string(), + binding: usage.binding.clone(), + resource_type, + }); + } + } +} + fn validate_callable_body( caller: &str, stmts: &[Stmt], @@ -855,7 +946,7 @@ fn resolve_interface_contract( implemented: &str, registry: &InterfaceRegistry, ) -> Option { - let canonical = canonical_interface_name(implemented); + let canonical = canonical_type_name(implemented); if let Some(contract) = registry.full.get(&canonical) { return Some(contract.clone()); } @@ -864,6 +955,21 @@ fn resolve_interface_contract( } fn canonical_interface_name(name: &str) -> String { + canonical_type_name(name) +} + +fn resolve_resource_type_name( + resource_type: &str, + registry: &ResourceTypeRegistry, +) -> Option { + if registry.full.contains(resource_type) { + return Some(resource_type.to_string()); + } + let short = resource_type.rsplit('.').next().unwrap_or(resource_type); + registry.short.get(short).and_then(|entry| entry.clone()) +} + +fn canonical_type_name(name: &str) -> String { name.split('<').next().unwrap_or(name).trim().to_string() } @@ -1321,4 +1427,37 @@ service FsStorage implements Storage { && detail.contains("output field `body` expected `String` but found `Int`") ))); } + + #[test] + fn strict_mode_reports_unknown_used_resource_type() { + let graph = module_graph_from_sources(&[( + "unknown_uses.dag", + "module sample.uses\nfunc run() -> { ok: Bool } uses fs: MissingResource { return { ok: true } }", + )]); + let errors = typecheck_module_graph_with_options( + graph, + TypecheckOptions { + allow_unresolved_imports: false, + }, + ) + .expect_err("strict mode should fail for unknown used resource type"); + assert!(errors.iter().any(|error| matches!( + error, + TypeError::UnknownUsedResourceType { + item, + binding, + resource_type, + } if item == "run" && binding == "fs" && resource_type == "MissingResource" + ))); + } + + #[test] + fn relaxed_mode_allows_unknown_used_resource_type() { + let graph = module_graph_from_sources(&[( + "unknown_uses_relaxed.dag", + "module sample.uses\nfunc run() -> { ok: Bool } uses fs: MissingResource { return { ok: true } }", + )]); + let typed = typecheck_module_graph(graph).expect("relaxed mode should allow unknown uses"); + assert_eq!(typed.modules.len(), 1); + } } From 4e70ac2df45c124913b3a5dab42f04bf60cbaa67 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 06:58:25 +0000 Subject: [PATCH 026/169] Add compile-directory regression for unknown uses resource type Co-authored-by: Brian Searls --- .../daglang-cli/tests/compile_commands.rs | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index abca7a9a2f5..129b989f3b6 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -292,6 +292,34 @@ fn compile_command_directory_mode_fails_on_unresolved_imports() { std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); } +#[test] +fn compile_command_directory_mode_fails_on_unknown_uses_resource_type() { + let root = unique_temp_dir("unknown_uses_type"); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp dir"); + std::fs::write( + root.join("sample/main.dag"), + "module sample.main\nfunc run() -> { ok: Bool } uses fs: MissingResource { return { ok: true } }", + ) + .expect("failed to write source"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&root) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile on directory"); + + assert!( + !output.status.success(), + "directory compile should fail on unknown uses resource type" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("typecheck errors")); + assert!(stderr.contains("unknown used resource type `MissingResource`")); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); +} + #[test] fn compile_command_directory_mode_reports_module_path_mismatch() { let root = unique_temp_dir("path_mismatch"); From 695a3882778f084a285e639edc2beb915a07345e Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 07:02:24 +0000 Subject: [PATCH 027/169] Tolerate known interface uses without lifecycle endpoints Co-authored-by: Brian Searls --- core/daglang/daglang-lower/src/lib.rs | 94 ++++++++++++++++++++++++++- 1 file changed, 93 insertions(+), 1 deletion(-) diff --git a/core/daglang/daglang-lower/src/lib.rs b/core/daglang/daglang-lower/src/lib.rs index 5f7fd1b471d..d6f70bf35a5 100644 --- a/core/daglang/daglang-lower/src/lib.rs +++ b/core/daglang/daglang-lower/src/lib.rs @@ -91,6 +91,32 @@ struct ResourceLifecycleEndpoint { release_node: Option, } +#[derive(Debug, Default, Clone)] +struct KnownUsesTypes { + names: HashSet, +} + +impl KnownUsesTypes { + fn insert(&mut self, name: impl Into) { + let canonical = canonical_type_name(name.into().as_str()); + self.names.insert(canonical.clone()); + let short = canonical + .rsplit('.') + .next() + .unwrap_or(canonical.as_str()) + .to_string(); + self.names.insert(short); + } + + fn contains(&self, name: &str) -> bool { + let canonical = canonical_type_name(name); + self.names.contains(&canonical) + || self + .names + .contains(canonical.rsplit('.').next().unwrap_or(canonical.as_str())) + } +} + /// Errors during lowering. #[derive(Debug)] pub enum LowerError { @@ -234,7 +260,14 @@ pub fn lower_typed_project(project: &TypedProject) -> Result, Low let service_registry = add_service_transport_triplets(&mut dag, project); add_service_call_edges(&mut dag, project, &endpoints_by_full, &service_registry)?; let resource_registry = add_resource_lifecycle_nodes(&mut dag, project); - add_used_resource_edges(&mut dag, project, &endpoints_by_full, &resource_registry)?; + let known_uses_types = collect_known_uses_types(project); + add_used_resource_edges( + &mut dag, + project, + &endpoints_by_full, + &resource_registry, + &known_uses_types, + )?; if dag.nodes.is_empty() { return Err(LowerError::NoLowerableItems); @@ -1161,6 +1194,7 @@ fn add_used_resource_edges( project: &TypedProject, endpoints_by_full: &HashMap<(String, String), LoweredEndpoint>, resource_registry: &ResourceLifecycleRegistry, + known_uses_types: &KnownUsesTypes, ) -> Result<(), LowerError> { for module in &project.modules { let module_name = module.module_path.join("."); @@ -1180,6 +1214,9 @@ fn add_used_resource_edges( resource_type.as_str(), resource_registry, ) else { + if known_uses_types.contains(&resource_type) { + continue; + } return Err(LowerError::UnresolvedUsedResource { caller: format!("{module_name}::{item_name}"), binding: usage.binding.clone(), @@ -1227,6 +1264,37 @@ fn resolve_resource_endpoint( None } +fn collect_known_uses_types(project: &TypedProject) -> KnownUsesTypes { + let mut known = KnownUsesTypes::default(); + for module in &project.modules { + let module_name = module.module_path.join("."); + for item in &module.ast.items { + match &item.node { + Item::InterfaceDef(def) => { + known.insert(def.name.clone()); + known.insert(format!("{module_name}.{}", def.name)); + } + Item::ResourceDef(def) => { + known.insert(def.name.clone()); + known.insert(format!("{module_name}.{}", def.name)); + if let Some(implemented) = &def.implements { + known.insert(implemented.clone()); + } + } + Item::ServiceDef(def) => { + known.insert(def.name.clone()); + known.insert(format!("{module_name}.{}", def.name)); + if let Some(implemented) = &def.implements { + known.insert(implemented.clone()); + } + } + _ => {} + } + } + } + known +} + fn resource_type_name(ty: &TypeExpr) -> String { match ty { TypeExpr::Named(name) | TypeExpr::Generic(name, _) => canonical_type_name(name), @@ -1871,6 +1939,30 @@ func run() -> { ok: Bool } uses fs: MissingResource { )); } + #[test] + fn known_interface_uses_without_lifecycle_are_tolerated() { + let typed = typed_project_from_sources(&[( + "dsl/resources/interface_uses.dag", + r#"module sample.resources +interface Storage { + capability read { + input { path: String } + output { body: String } + } +} +func run() -> { ok: Bool } uses store: Storage { + return { ok: true } +}"#, + )]); + let dag = lower_typed_project(&typed).expect("lowering should succeed"); + assert!(dag.nodes.iter().any(|node| node.id.0 == "sample.resources::run")); + assert!( + !dag.edges.iter().any(|edge| edge.to_node.0 == "sample.resources::run" + && edge.to_port.0 == "__deps"), + "interface-only uses should not fabricate lifecycle dependency edges" + ); + } + #[test] fn lower_errors_when_no_callable_items_exist() { let typed = typed_project_from_sources(&[( From 082b2bee915023d97edc205b723002f4ec846730 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 07:08:14 +0000 Subject: [PATCH 028/169] Validate provides bindings and types in strict typecheck Co-authored-by: Brian Searls --- .../daglang-cli/tests/compile_commands.rs | 28 +++ core/daglang/daglang-typecheck/src/lib.rs | 204 +++++++++++++++++- 2 files changed, 224 insertions(+), 8 deletions(-) diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index 129b989f3b6..868019ee173 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -320,6 +320,34 @@ fn compile_command_directory_mode_fails_on_unknown_uses_resource_type() { std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); } +#[test] +fn compile_command_directory_mode_fails_on_unknown_provides_resource_type() { + let root = unique_temp_dir("unknown_provides_type"); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp dir"); + std::fs::write( + root.join("sample/main.dag"), + "module sample.main\nfunc run() -> { ok: Bool } provides out: MissingResource { return { ok: true } }", + ) + .expect("failed to write source"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&root) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile on directory"); + + assert!( + !output.status.success(), + "directory compile should fail on unknown provides resource type" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("typecheck errors")); + assert!(stderr.contains("unknown provided resource type `MissingResource`")); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); +} + #[test] fn compile_command_directory_mode_reports_module_path_mismatch() { let root = unique_temp_dir("path_mismatch"); diff --git a/core/daglang/daglang-typecheck/src/lib.rs b/core/daglang/daglang-typecheck/src/lib.rs index 42631b49f33..b250ec15d74 100644 --- a/core/daglang/daglang-typecheck/src/lib.rs +++ b/core/daglang/daglang-typecheck/src/lib.rs @@ -24,7 +24,9 @@ use std::collections::{HashMap, HashSet}; use std::path::PathBuf; use daglang_resolve::{ModuleGraph, ResolvedModule}; -use daglang_syntax::ast::{Expr, Field, Item, Param, SourceFile, Stmt, TypeExpr, UsesClause}; +use daglang_syntax::ast::{ + Expr, Field, Item, Param, ProvidesClause, SourceFile, Stmt, TypeExpr, UsesClause, +}; /// A typechecked project snapshot. #[derive(Debug)] @@ -161,6 +163,18 @@ pub enum TypeError { binding: String, resource_type: String, }, + /// Duplicate `uses` binding within a callable declaration. + DuplicateUsesBinding { item: String, binding: String }, + /// Duplicate `provides` binding within a callable declaration. + DuplicateProvidesBinding { item: String, binding: String }, + /// A binding is declared in both `uses` and `provides`. + UseProvideBindingConflict { item: String, binding: String }, + /// `provides` clause references an unknown resource/interface type. + UnknownProvidedResourceType { + item: String, + binding: String, + resource_type: String, + }, } impl std::fmt::Display for TypeError { @@ -253,6 +267,26 @@ impl std::fmt::Display for TypeError { f, "unknown used resource type `{resource_type}` for binding `{binding}` in `{item}`" ), + Self::DuplicateUsesBinding { item, binding } => write!( + f, + "duplicate uses binding `{binding}` in `{item}`" + ), + Self::DuplicateProvidesBinding { item, binding } => write!( + f, + "duplicate provides binding `{binding}` in `{item}`" + ), + Self::UseProvideBindingConflict { item, binding } => write!( + f, + "binding `{binding}` is declared in both uses/provides in `{item}`" + ), + Self::UnknownProvidedResourceType { + item, + binding, + resource_type, + } => write!( + f, + "unknown provided resource type `{resource_type}` for binding `{binding}` in `{item}`" + ), } } } @@ -406,6 +440,14 @@ fn collect_signatures( context.allow_unresolved_references, errors, ); + validate_provides_clauses( + &def.name, + &def.provides, + context.resource_type_registry, + context.allow_unresolved_references, + errors, + ); + validate_use_provide_binding_conflicts(&def.name, &def.uses, &def.provides, errors); validate_callable_body( &def.name, &def.body.stmts, @@ -728,16 +770,70 @@ fn validate_uses_clauses( allow_unresolved_references: bool, errors: &mut Vec, ) { - if allow_unresolved_references { - return; - } + let mut seen_bindings = HashSet::new(); for usage in uses { - let resource_type = canonical_type_name(&type_expr_to_string(&usage.resource_type)); - if resolve_resource_type_name(&resource_type, registry).is_none() { - errors.push(TypeError::UnknownUsedResourceType { + if !seen_bindings.insert(usage.binding.clone()) { + errors.push(TypeError::DuplicateUsesBinding { item: item_name.to_string(), binding: usage.binding.clone(), - resource_type, + }); + } + if !allow_unresolved_references { + let resource_type = canonical_type_name(&type_expr_to_string(&usage.resource_type)); + if resolve_resource_type_name(&resource_type, registry).is_none() { + errors.push(TypeError::UnknownUsedResourceType { + item: item_name.to_string(), + binding: usage.binding.clone(), + resource_type, + }); + } + } + } +} + +fn validate_provides_clauses( + item_name: &str, + provides: &[ProvidesClause], + registry: &ResourceTypeRegistry, + allow_unresolved_references: bool, + errors: &mut Vec, +) { + let mut seen_bindings = HashSet::new(); + for provided in provides { + if !seen_bindings.insert(provided.binding.clone()) { + errors.push(TypeError::DuplicateProvidesBinding { + item: item_name.to_string(), + binding: provided.binding.clone(), + }); + } + if !allow_unresolved_references { + let resource_type = canonical_type_name(&type_expr_to_string(&provided.resource_type)); + if resolve_resource_type_name(&resource_type, registry).is_none() { + errors.push(TypeError::UnknownProvidedResourceType { + item: item_name.to_string(), + binding: provided.binding.clone(), + resource_type, + }); + } + } + } +} + +fn validate_use_provide_binding_conflicts( + item_name: &str, + uses: &[UsesClause], + provides: &[ProvidesClause], + errors: &mut Vec, +) { + let used_bindings = uses + .iter() + .map(|usage| usage.binding.as_str()) + .collect::>(); + for provided in provides { + if used_bindings.contains(provided.binding.as_str()) { + errors.push(TypeError::UseProvideBindingConflict { + item: item_name.to_string(), + binding: provided.binding.clone(), }); } } @@ -1460,4 +1556,96 @@ service FsStorage implements Storage { let typed = typecheck_module_graph(graph).expect("relaxed mode should allow unknown uses"); assert_eq!(typed.modules.len(), 1); } + + #[test] + fn strict_mode_reports_unknown_provided_resource_type() { + let graph = module_graph_from_sources(&[( + "unknown_provides.dag", + "module sample.provides\nfunc run() -> { ok: Bool } provides out: MissingResource { return { ok: true } }", + )]); + let errors = typecheck_module_graph_with_options( + graph, + TypecheckOptions { + allow_unresolved_imports: false, + }, + ) + .expect_err("strict mode should fail for unknown provided resource type"); + assert!(errors.iter().any(|error| matches!( + error, + TypeError::UnknownProvidedResourceType { + item, + binding, + resource_type, + } if item == "run" && binding == "out" && resource_type == "MissingResource" + ))); + } + + #[test] + fn duplicate_uses_binding_is_reported() { + let graph = module_graph_from_sources(&[( + "duplicate_uses.dag", + r#"module sample.uses +interface Storage { + capability read { + input { path: String } + output { body: String } + } +} +func run() -> { ok: Bool } uses fs: Storage uses fs: Storage { + return { ok: true } +}"#, + )]); + let errors = typecheck_module_graph(graph).expect_err("duplicate uses should fail"); + assert!(errors.iter().any(|error| matches!( + error, + TypeError::DuplicateUsesBinding { item, binding } + if item == "run" && binding == "fs" + ))); + } + + #[test] + fn duplicate_provides_binding_is_reported() { + let graph = module_graph_from_sources(&[( + "duplicate_provides.dag", + r#"module sample.provides +interface Storage { + capability read { + input { path: String } + output { body: String } + } +} +func run() -> { ok: Bool } provides out: Storage provides out: Storage { + return { ok: true } +}"#, + )]); + let errors = typecheck_module_graph(graph).expect_err("duplicate provides should fail"); + assert!(errors.iter().any(|error| matches!( + error, + TypeError::DuplicateProvidesBinding { item, binding } + if item == "run" && binding == "out" + ))); + } + + #[test] + fn use_provide_binding_conflict_is_reported() { + let graph = module_graph_from_sources(&[( + "use_provide_conflict.dag", + r#"module sample.conflict +interface Storage { + capability read { + input { path: String } + output { body: String } + } +} +func run() -> { ok: Bool } uses io: Storage provides io: Storage { + return { ok: true } +}"#, + )]); + let errors = typecheck_module_graph(graph).expect_err("binding conflict should fail"); + assert!(errors.iter().any(|error| matches!( + error, + TypeError::UseProvideBindingConflict { item, binding } + if item == "run" && binding == "io" + ))); + } } From ce03402d68faaaf17b94424f1bec8bbc3528a8c1 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 07:11:27 +0000 Subject: [PATCH 029/169] Lower func provides clauses into provider resource nodes Co-authored-by: Brian Searls --- core/daglang/daglang-lower/src/lib.rs | 152 ++++++++++++++++++++++++++ 1 file changed, 152 insertions(+) diff --git a/core/daglang/daglang-lower/src/lib.rs b/core/daglang/daglang-lower/src/lib.rs index d6f70bf35a5..b1e983441ff 100644 --- a/core/daglang/daglang-lower/src/lib.rs +++ b/core/daglang/daglang-lower/src/lib.rs @@ -139,6 +139,12 @@ pub enum LowerError { binding: String, resource_type: String, }, + /// `provides` clause references an unknown resource/interface source. + UnresolvedProvidedResource { + caller: String, + binding: String, + resource_type: String, + }, /// No executable declarations were available for lowering. NoLowerableItems, } @@ -174,6 +180,14 @@ impl std::fmt::Display for LowerError { f, "unresolved used resource `{binding}: {resource_type}` in `{caller}`" ), + Self::UnresolvedProvidedResource { + caller, + binding, + resource_type, + } => write!( + f, + "unresolved provided resource `{binding}: {resource_type}` in `{caller}`" + ), Self::NoLowerableItems => write!(f, "no callable or pipeline declarations to lower"), } } @@ -268,6 +282,13 @@ pub fn lower_typed_project(project: &TypedProject) -> Result, Low &resource_registry, &known_uses_types, )?; + add_provided_resource_nodes( + &mut dag, + project, + &endpoints_by_full, + &resource_registry, + &known_uses_types, + )?; if dag.nodes.is_empty() { return Err(LowerError::NoLowerableItems); @@ -1247,6 +1268,80 @@ fn add_used_resource_edges( Ok(()) } +fn add_provided_resource_nodes( + dag: &mut Dag, + project: &TypedProject, + endpoints_by_full: &HashMap<(String, String), LoweredEndpoint>, + resource_registry: &ResourceLifecycleRegistry, + known_uses_types: &KnownUsesTypes, +) -> Result<(), LowerError> { + for module in &project.modules { + let module_name = module.module_path.join("."); + for item in &module.ast.items { + let Some((item_name, provides)) = item_callable_provides(&item.node) else { + continue; + }; + let Some(target) = + endpoints_by_full.get(&(module_name.clone(), item_name.to_string())) + else { + continue; + }; + for provided in provides { + let resource_type = resource_type_name(&provided.resource_type); + let has_endpoint = resolve_resource_endpoint( + module_name.as_str(), + resource_type.as_str(), + resource_registry, + ) + .is_some(); + if !has_endpoint && !known_uses_types.contains(&resource_type) { + return Err(LowerError::UnresolvedProvidedResource { + caller: format!("{module_name}::{item_name}"), + binding: provided.binding.clone(), + resource_type, + }); + } + + let provider_node_id = format!( + "provide_resource_{}", + sanitize_identifier(&format!( + "{module_name}_{item_name}_{}", + provided.binding + )) + ); + add_node_once( + dag, + Node::opaque( + provider_node_id.clone(), + vec![Port::scalar("trigger", "Any")], + vec![Port::with_cardinality( + provided.binding.as_str(), + resource_type.as_str(), + Cardinality::ONE, + )], + LoweredOp::Callable { + module: module_name.clone(), + kind: CallableKind::Pattern, + name: format!( + "resource_provide::{}::{}", + item_name, provided.binding + ), + }, + ), + ); + add_edge_once( + dag, + target.node_id.as_str(), + target.primary_output.as_str(), + provider_node_id.as_str(), + "trigger", + ); + } + } + } + Ok(()) +} + fn resolve_resource_endpoint( module_name: &str, resource_type: &str, @@ -1431,6 +1526,15 @@ fn item_callable_uses(item: &Item) -> Option<(&str, &[daglang_syntax::ast::UsesC } } +fn item_callable_provides( + item: &Item, +) -> Option<(&str, &[daglang_syntax::ast::ProvidesClause])> { + match item { + Item::FuncDef(def) => Some((def.name.as_str(), def.provides.as_slice())), + _ => None, + } +} + fn collect_calls_from_stmts(stmts: &[Stmt], calls: &mut HashSet) { for stmt in stmts { match stmt { @@ -1939,6 +2043,54 @@ func run() -> { ok: Bool } uses fs: MissingResource { )); } + #[test] + fn provides_clause_adds_provider_node_and_edge() { + let typed = typed_project_from_sources(&[( + "dsl/resources/provides_wiring.dag", + r#"module sample.resources +interface Storage { + capability read { + input { path: String } + output { body: String } + } +} +func run() -> { ok: Bool } provides out: Storage { + return { ok: true } +}"#, + )]); + let dag = lower_typed_project(&typed).expect("lowering should succeed"); + let provider = "provide_resource_sample_resources_run_out"; + assert!(dag.nodes.iter().any(|node| node.id.0 == provider)); + assert!(dag.edges.iter().any(|edge| { + edge.from_node.0 == "sample.resources::run" + && edge.from_port.0 == "ok" + && edge.to_node.0 == provider + && edge.to_port.0 == "trigger" + })); + } + + #[test] + fn unresolved_provides_clause_reports_lower_error() { + let typed = typed_project_from_sources(&[( + "dsl/resources/unresolved_provides.dag", + r#"module sample.resources +func run() -> { ok: Bool } provides out: MissingResource { + return { ok: true } +}"#, + )]); + let error = lower_typed_project(&typed).expect_err("lowering should fail"); + assert!(matches!( + error, + LowerError::UnresolvedProvidedResource { + caller, + binding, + resource_type, + } if caller == "sample.resources::run" + && binding == "out" + && resource_type == "MissingResource" + )); + } + #[test] fn known_interface_uses_without_lifecycle_are_tolerated() { let typed = typed_project_from_sources(&[( From a83e8306512df9446679f3ca71587a6311bfe064 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 07:13:06 +0000 Subject: [PATCH 030/169] Add CLI regression for unresolved provides lower errors Co-authored-by: Brian Searls --- .../daglang-cli/tests/compile_commands.rs | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index 868019ee173..323da84694f 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -264,6 +264,37 @@ func run() -> { ok: Bool } uses fs: MissingResource { std::fs::remove_file(fixture).expect("failed to cleanup fixture"); } +#[test] +fn expand_command_reports_unresolved_provides_lower_error() { + let fixture = unique_temp_file("unresolved_provides"); + std::fs::write( + &fixture, + r#"module sample.resources +func run() -> { ok: Bool } provides out: MissingResource { + return { ok: true } +} +"#, + ) + .expect("failed to write fixture"); + + let output = Command::new(daglang_bin()) + .arg("expand") + .arg(&fixture) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang expand on unresolved provides fixture"); + + assert!( + !output.status.success(), + "expand should fail when provides target cannot be resolved" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("lower error: unresolved provided resource")); + assert!(stderr.contains("out: MissingResource")); + + std::fs::remove_file(fixture).expect("failed to cleanup fixture"); +} + #[test] fn compile_command_directory_mode_fails_on_unresolved_imports() { let root = unique_temp_dir("unresolved_import"); From 5bc5596f2910692e6e15d44872d82f942b0a360b Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 07:16:39 +0000 Subject: [PATCH 031/169] Track provider-node obligations in derived manifest output Co-authored-by: Brian Searls --- core/daglang/daglang-cli/src/compile.rs | 5 ++++ .../daglang-cli/tests/compile_commands.rs | 1 + core/daglang/daglang-derive/src/lib.rs | 24 ++++++++++++++++++- 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/core/daglang/daglang-cli/src/compile.rs b/core/daglang/daglang-cli/src/compile.rs index c82cc678d9d..5cd6f002f56 100644 --- a/core/daglang/daglang-cli/src/compile.rs +++ b/core/daglang/daglang-cli/src/compile.rs @@ -275,6 +275,10 @@ pub fn render_manifest(derived: &DerivedArtifacts) -> String { " service_transport_parse_targets: {}\n", obligations.service_transport_parse_targets )); + out.push_str(&format!( + " resource_provide_targets: {}\n", + obligations.resource_provide_targets + )); out.push_str(&format!( " resource_acquire_targets: {}\n", obligations.resource_acquire_targets @@ -319,6 +323,7 @@ mod tests { let rendered = render_manifest(&output.derived); assert!(rendered.contains("TestObligations:")); assert!(rendered.contains("service_transport_prepare_targets:")); + assert!(rendered.contains("resource_provide_targets:")); } #[test] diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index 323da84694f..ad916b22850 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -97,6 +97,7 @@ fn manifest_command_shows_derived_progress_manifest() { assert!(stdout.contains("waves:")); assert!(stdout.contains("TestObligations:")); assert!(stdout.contains("service_transport_prepare_targets:")); + assert!(stdout.contains("resource_provide_targets:")); } #[test] diff --git a/core/daglang/daglang-derive/src/lib.rs b/core/daglang/daglang-derive/src/lib.rs index 3e9e560818c..ae18866f8a3 100644 --- a/core/daglang/daglang-derive/src/lib.rs +++ b/core/daglang/daglang-derive/src/lib.rs @@ -49,6 +49,7 @@ pub struct TestObligations { pub service_transport_prepare_targets: usize, pub service_transport_execute_targets: usize, pub service_transport_parse_targets: usize, + pub resource_provide_targets: usize, pub resource_acquire_targets: usize, pub resource_release_targets: usize, } @@ -122,6 +123,7 @@ pub fn derive_artifacts(dag: &Dag) -> Result]) -> ObligationCounts { counts.service_transport_execute_targets += 1; } else if name.starts_with("service_transport::parse::") { counts.service_transport_parse_targets += 1; + } else if name.starts_with("resource_provide::") { + counts.resource_provide_targets += 1; } else if name.starts_with("resource_lifecycle::acquire::") { counts.resource_acquire_targets += 1; } else if name.starts_with("resource_lifecycle::release::") { @@ -398,6 +403,16 @@ mod tests { name: "service_transport::parse::FsStorage::read".to_string(), }, )); + dag.add_node(Node::opaque( + "provide_resource", + vec![Port::scalar("trigger", "Any")], + vec![Port::scalar("out", "Storage")], + LoweredOp::Callable { + module: "sample.resources".to_string(), + kind: CallableKind::Pattern, + name: "resource_provide::run::out".to_string(), + }, + )); dag.add_node(Node::opaque( "acquire_resource", vec![], @@ -430,6 +445,12 @@ mod tests { "parse_transport", "response", )); + dag.add_edge(Edge::new( + "provide_resource", + "out", + "release_resource", + "resource_handle", + )); dag.add_edge(Edge::new( "acquire_resource", "resource_handle", @@ -439,10 +460,11 @@ mod tests { let artifacts = derive_artifacts(&dag).expect("derivation should succeed"); assert_eq!(artifacts.obligations.transport_execution_targets, 1); - assert_eq!(artifacts.obligations.pure_node_determinism_targets, 4); + assert_eq!(artifacts.obligations.pure_node_determinism_targets, 5); assert_eq!(artifacts.obligations.service_transport_prepare_targets, 1); assert_eq!(artifacts.obligations.service_transport_execute_targets, 1); assert_eq!(artifacts.obligations.service_transport_parse_targets, 1); + assert_eq!(artifacts.obligations.resource_provide_targets, 1); assert_eq!(artifacts.obligations.resource_acquire_targets, 1); assert_eq!(artifacts.obligations.resource_release_targets, 1); } From a3497d9dbf122b4b1bbe11e56a656ce43b114a56 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 07:20:49 +0000 Subject: [PATCH 032/169] Wire service call param identifiers into prepare transport inputs Co-authored-by: Brian Searls --- core/daglang/daglang-lower/src/lib.rs | 173 +++++++++++++++++++++++--- 1 file changed, 156 insertions(+), 17 deletions(-) diff --git a/core/daglang/daglang-lower/src/lib.rs b/core/daglang/daglang-lower/src/lib.rs index b1e983441ff..3071a2a56dc 100644 --- a/core/daglang/daglang-lower/src/lib.rs +++ b/core/daglang/daglang-lower/src/lib.rs @@ -77,7 +77,14 @@ struct LoweredEndpoint { #[derive(Debug, Default, Clone)] struct ServiceEndpointRegistry { - by_key: HashMap>, + by_key: HashMap>, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +struct ServiceTransportEndpoint { + parse: LoweredEndpoint, + prepare_node_id: String, + prepare_inputs: Vec, } #[derive(Debug, Default, Clone)] @@ -1098,9 +1105,17 @@ fn add_service_transport_triplets( .first() .map(|field| field.name.clone()) .unwrap_or_else(|| "result".to_string()); - let endpoint = LoweredEndpoint { - node_id: parse_id, - primary_output: parse_output, + let endpoint = ServiceTransportEndpoint { + parse: LoweredEndpoint { + node_id: parse_id, + primary_output: parse_output, + }, + prepare_node_id: prepare_id, + prepare_inputs: operation + .inputs + .iter() + .map(|field| field.name.clone()) + .collect::>(), }; register_service_endpoint( &mut registry, @@ -1138,7 +1153,7 @@ fn add_node_once(dag: &mut Dag, node: Node) { fn register_service_endpoint( registry: &mut ServiceEndpointRegistry, key: String, - endpoint: LoweredEndpoint, + endpoint: ServiceTransportEndpoint, ) { registry .by_key @@ -1180,30 +1195,66 @@ fn add_service_call_edges( for module in &project.modules { let module_name = module.module_path.join("."); for item in &module.ast.items { - let Some((item_name, stmts)) = item_callable_body(&item.node) else { - continue; + let (item_name, params, stmts) = match &item.node { + Item::FnDef(def) => (&def.name, &def.params, def.body.stmts.as_slice()), + Item::FuncDef(def) => (&def.name, &def.params, def.body.stmts.as_slice()), + Item::PatternDef(def) => (&def.name, &def.params, def.body.stmts.as_slice()), + _ => continue, }; let Some(target) = endpoints_by_full.get(&(module_name.clone(), item_name.to_string())) else { continue; }; - let mut service_calls = HashSet::>::new(); + let param_types = params + .iter() + .map(|param| (param.name.clone(), type_expr_to_string(¶m.ty))) + .collect::>(); + let mut service_calls = Vec::::new(); collect_service_calls_from_stmts(stmts, &mut service_calls); - for call_path in service_calls { - let Some(source) = resolve_service_endpoint(&call_path, service_registry) else { + for call in service_calls { + let Some(source) = resolve_service_endpoint(&call.path, service_registry) else { return Err(LowerError::UnresolvedServiceCall { caller: format!("{module_name}::{item_name}"), - service_call: call_path.join("."), + service_call: call.path.join("."), }); }; add_edge_once( dag, - source.node_id.as_str(), - source.primary_output.as_str(), + source.parse.node_id.as_str(), + source.parse.primary_output.as_str(), target.node_id.as_str(), "__deps", ); + for (index, arg) in call.args.iter().enumerate() { + let Some(arg_ident) = arg.ident.as_deref() else { + continue; + }; + let Some(param_ty) = param_types.get(arg_ident) else { + continue; + }; + let Some(prepare_input) = arg + .name + .as_deref() + .or_else(|| source.prepare_inputs.get(index).map(String::as_str)) + else { + continue; + }; + let param_source = ensure_param_source_node( + dag, + module_name.as_str(), + item_name, + arg_ident, + param_ty.as_str(), + ); + add_edge_once( + dag, + param_source.as_str(), + arg_ident, + source.prepare_node_id.as_str(), + prepare_input, + ); + } } } } @@ -1477,7 +1528,7 @@ fn add_resource_lifecycle_nodes( fn resolve_service_endpoint( call_path: &[String], registry: &ServiceEndpointRegistry, -) -> Option { +) -> Option { if call_path.len() < 2 { return None; } @@ -1509,6 +1560,33 @@ fn add_edge_once(dag: &mut Dag, from: &str, from_port: &str, to: &str dag.add_edge(Edge::new(from, from_port, to, to_port)); } +fn ensure_param_source_node( + dag: &mut Dag, + module_name: &str, + callable: &str, + param: &str, + ty: &str, +) -> String { + let node_id = format!( + "param_source_{}", + sanitize_identifier(&format!("{module_name}_{callable}_{param}")) + ); + add_node_once( + dag, + Node::opaque( + node_id.clone(), + vec![], + vec![Port::with_cardinality(param, ty, Cardinality::ONE)], + LoweredOp::Callable { + module: module_name.to_string(), + kind: CallableKind::Pattern, + name: format!("call_param_source::{callable}::{param}"), + }, + ), + ); + node_id +} + fn item_callable_body(item: &Item) -> Option<(&str, &[Stmt])> { match item { Item::FnDef(def) => Some((def.name.as_str(), def.body.stmts.as_slice())), @@ -1550,7 +1628,19 @@ fn collect_calls_from_stmts(stmts: &[Stmt], calls: &mut HashSet) { } } -fn collect_service_calls_from_stmts(stmts: &[Stmt], calls: &mut HashSet>) { +#[derive(Debug, Clone, PartialEq, Eq)] +struct ServiceCallArgSite { + name: Option, + ident: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +struct ServiceCallSite { + path: Vec, + args: Vec, +} + +fn collect_service_calls_from_stmts(stmts: &[Stmt], calls: &mut Vec) { for stmt in stmts { match stmt { Stmt::Let(_, expr) | Stmt::Assign(_, expr) | Stmt::Expr(expr) => { @@ -1648,7 +1738,7 @@ fn collect_calls_from_expr(expr: &Expr, calls: &mut HashSet) { } } -fn collect_service_calls_from_expr(expr: &Expr, calls: &mut HashSet>) { +fn collect_service_calls_from_expr(expr: &Expr, calls: &mut Vec) { match expr { Expr::Call(_, args) => { for (_, arg) in args { @@ -1656,7 +1746,19 @@ fn collect_service_calls_from_expr(expr: &Expr, calls: &mut HashSet> } } Expr::ServiceCall(path, args) => { - calls.insert(path.clone()); + calls.push(ServiceCallSite { + path: path.clone(), + args: args + .iter() + .map(|(name, arg)| ServiceCallArgSite { + name: name.clone(), + ident: match arg { + Expr::Ident(ident) => Some(ident.clone()), + _ => None, + }, + }) + .collect::>(), + }); for (_, arg) in args { collect_service_calls_from_expr(arg, calls); } @@ -1927,11 +2029,20 @@ func run(path: String) -> { body: String } { )]); let dag = lower_typed_project(&typed).expect("lowering should succeed"); let parse_node = "parse_transport_sample_services_FsStorage_read"; + let prepare_node = "prepare_transport_sample_services_FsStorage_read"; + let param_source = "param_source_sample_services_run_path"; assert!(dag.edges.iter().any(|edge| { edge.from_node.0 == parse_node && edge.to_node.0 == "sample.services::run" && edge.to_port.0 == "__deps" })); + assert!(dag.nodes.iter().any(|node| node.id.0 == param_source)); + assert!(dag.edges.iter().any(|edge| { + edge.from_node.0 == param_source + && edge.from_port.0 == "path" + && edge.to_node.0 == prepare_node + && edge.to_port.0 == "path" + })); } #[test] @@ -1961,6 +2072,34 @@ func run(path: String) -> { body: String } { )); } + #[test] + fn positional_service_call_args_wire_by_operation_input_order() { + let typed = typed_project_from_sources(&[( + "dsl/services/storage_calls_positional.dag", + r#"module sample.services +interface Storage { + capability read { + input { path: String } + output { body: String } + } +} +service FsStorage implements Storage { + operation read(path: String) -> { body: String } +} +func run(path: String) -> { body: String } { + let response = FsStorage.read(path) + return { body: response.body } +}"#, + )]); + let dag = lower_typed_project(&typed).expect("lowering should succeed"); + assert!(dag.edges.iter().any(|edge| { + edge.from_node.0 == "param_source_sample_services_run_path" + && edge.from_port.0 == "path" + && edge.to_node.0 == "prepare_transport_sample_services_FsStorage_read" + && edge.to_port.0 == "path" + })); + } + #[test] fn resource_acquire_release_lower_to_lifecycle_nodes() { let typed = typed_project_from_sources(&[( From 35b10187ea4c722ea5ccb0df809db12a1b89230a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 07:22:49 +0000 Subject: [PATCH 033/169] Wire provided resource nodes into lifecycle release endpoints Co-authored-by: Brian Searls --- core/daglang/daglang-lower/src/lib.rs | 41 ++++++++++++++++++++++++--- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/core/daglang/daglang-lower/src/lib.rs b/core/daglang/daglang-lower/src/lib.rs index 3071a2a56dc..404b4ff2d18 100644 --- a/core/daglang/daglang-lower/src/lib.rs +++ b/core/daglang/daglang-lower/src/lib.rs @@ -1339,13 +1339,12 @@ fn add_provided_resource_nodes( }; for provided in provides { let resource_type = resource_type_name(&provided.resource_type); - let has_endpoint = resolve_resource_endpoint( + let endpoint = resolve_resource_endpoint( module_name.as_str(), resource_type.as_str(), resource_registry, - ) - .is_some(); - if !has_endpoint && !known_uses_types.contains(&resource_type) { + ); + if endpoint.is_none() && !known_uses_types.contains(&resource_type) { return Err(LowerError::UnresolvedProvidedResource { caller: format!("{module_name}::{item_name}"), binding: provided.binding.clone(), @@ -1387,6 +1386,17 @@ fn add_provided_resource_nodes( provider_node_id.as_str(), "trigger", ); + if let Some(endpoint) = endpoint { + if let Some(release_node) = endpoint.release_node { + add_edge_once( + dag, + provider_node_id.as_str(), + provided.binding.as_str(), + release_node.as_str(), + "resource_handle", + ); + } + } } } } @@ -2230,6 +2240,29 @@ func run() -> { ok: Bool } provides out: MissingResource { )); } + #[test] + fn provides_resource_with_release_wires_provider_output_to_lifecycle_release() { + let typed = typed_project_from_sources(&[( + "dsl/resources/provides_release_wiring.dag", + r#"module sample.resources +resource TempFile { + release { + let done = true + } +} +func run() -> { ok: Bool } provides file: TempFile { + return { ok: true } +}"#, + )]); + let dag = lower_typed_project(&typed).expect("lowering should succeed"); + assert!(dag.edges.iter().any(|edge| { + edge.from_node.0 == "provide_resource_sample_resources_run_file" + && edge.from_port.0 == "file" + && edge.to_node.0 == "release_resource_sample_resources_TempFile" + && edge.to_port.0 == "resource_handle" + })); + } + #[test] fn known_interface_uses_without_lifecycle_are_tolerated() { let typed = typed_project_from_sources(&[( From aaafa2a6ec9f8468059f277c45ba7b4feff21160 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 07:25:08 +0000 Subject: [PATCH 034/169] Expose service param-source obligations in manifests Co-authored-by: Brian Searls --- core/daglang/daglang-cli/src/compile.rs | 5 ++++ .../daglang-cli/tests/compile_commands.rs | 2 ++ core/daglang/daglang-derive/src/lib.rs | 24 ++++++++++++++++++- 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/core/daglang/daglang-cli/src/compile.rs b/core/daglang/daglang-cli/src/compile.rs index 5cd6f002f56..d3c0517dead 100644 --- a/core/daglang/daglang-cli/src/compile.rs +++ b/core/daglang/daglang-cli/src/compile.rs @@ -275,6 +275,10 @@ pub fn render_manifest(derived: &DerivedArtifacts) -> String { " service_transport_parse_targets: {}\n", obligations.service_transport_parse_targets )); + out.push_str(&format!( + " service_param_source_targets: {}\n", + obligations.service_param_source_targets + )); out.push_str(&format!( " resource_provide_targets: {}\n", obligations.resource_provide_targets @@ -323,6 +327,7 @@ mod tests { let rendered = render_manifest(&output.derived); assert!(rendered.contains("TestObligations:")); assert!(rendered.contains("service_transport_prepare_targets:")); + assert!(rendered.contains("service_param_source_targets:")); assert!(rendered.contains("resource_provide_targets:")); } diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index ad916b22850..36d6e4bd98c 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -97,6 +97,7 @@ fn manifest_command_shows_derived_progress_manifest() { assert!(stdout.contains("waves:")); assert!(stdout.contains("TestObligations:")); assert!(stdout.contains("service_transport_prepare_targets:")); + assert!(stdout.contains("service_param_source_targets:")); assert!(stdout.contains("resource_provide_targets:")); } @@ -147,6 +148,7 @@ func run(path: String) -> { body: String } { assert!(stdout.contains("service_transport_prepare_targets: 1")); assert!(stdout.contains("service_transport_execute_targets: 1")); assert!(stdout.contains("service_transport_parse_targets: 1")); + assert!(stdout.contains("service_param_source_targets: 1")); assert!(stdout.contains("resource_acquire_targets: 1")); assert!(stdout.contains("resource_release_targets: 1")); diff --git a/core/daglang/daglang-derive/src/lib.rs b/core/daglang/daglang-derive/src/lib.rs index ae18866f8a3..78554c051cc 100644 --- a/core/daglang/daglang-derive/src/lib.rs +++ b/core/daglang/daglang-derive/src/lib.rs @@ -49,6 +49,7 @@ pub struct TestObligations { pub service_transport_prepare_targets: usize, pub service_transport_execute_targets: usize, pub service_transport_parse_targets: usize, + pub service_param_source_targets: usize, pub resource_provide_targets: usize, pub resource_acquire_targets: usize, pub resource_release_targets: usize, @@ -123,6 +124,7 @@ pub fn derive_artifacts(dag: &Dag) -> Result]) -> ObligationCounts { counts.service_transport_execute_targets += 1; } else if name.starts_with("service_transport::parse::") { counts.service_transport_parse_targets += 1; + } else if name.starts_with("call_param_source::") { + counts.service_param_source_targets += 1; } else if name.starts_with("resource_provide::") { counts.resource_provide_targets += 1; } else if name.starts_with("resource_lifecycle::acquire::") { @@ -373,6 +378,16 @@ mod tests { #[test] fn derive_obligations_count_transport_and_lifecycle_targets() { let mut dag = Dag::new(); + dag.add_node(Node::opaque( + "param_source", + vec![], + vec![Port::scalar("path", "String")], + LoweredOp::Callable { + module: "sample.services".to_string(), + kind: CallableKind::Pattern, + name: "call_param_source::run::path".to_string(), + }, + )); dag.add_node(Node::opaque( "prepare_transport", vec![Port::scalar("path", "String")], @@ -433,6 +448,12 @@ mod tests { name: "resource_lifecycle::release::TempFile".to_string(), }, )); + dag.add_edge(Edge::new( + "param_source", + "path", + "prepare_transport", + "path", + )); dag.add_edge(Edge::new( "prepare_transport", "request", @@ -460,10 +481,11 @@ mod tests { let artifacts = derive_artifacts(&dag).expect("derivation should succeed"); assert_eq!(artifacts.obligations.transport_execution_targets, 1); - assert_eq!(artifacts.obligations.pure_node_determinism_targets, 5); + assert_eq!(artifacts.obligations.pure_node_determinism_targets, 6); assert_eq!(artifacts.obligations.service_transport_prepare_targets, 1); assert_eq!(artifacts.obligations.service_transport_execute_targets, 1); assert_eq!(artifacts.obligations.service_transport_parse_targets, 1); + assert_eq!(artifacts.obligations.service_param_source_targets, 1); assert_eq!(artifacts.obligations.resource_provide_targets, 1); assert_eq!(artifacts.obligations.resource_acquire_targets, 1); assert_eq!(artifacts.obligations.resource_release_targets, 1); From 70be8a05fd5faa9bdc5e5fb5a1dadd443463cbba Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 07:26:43 +0000 Subject: [PATCH 035/169] Add manifest regression for zero param-source obligations Co-authored-by: Brian Searls --- .../daglang-cli/tests/compile_commands.rs | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index 36d6e4bd98c..3a00f1131c0 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -155,6 +155,47 @@ func run(path: String) -> { body: String } { std::fs::remove_file(fixture).expect("failed to cleanup fixture"); } +#[test] +fn manifest_command_reports_zero_service_param_source_targets_for_literal_args() { + let fixture = unique_temp_file("manifest_param_sources_zero"); + std::fs::write( + &fixture, + r#"module sample.literal +interface Storage { + capability read { + input { path: String } + output { body: String } + } +} +service FsStorage implements Storage { + operation read(path: String) -> { body: String } +} +func run() -> { body: String } { + let response = FsStorage.read(path: "README.md") + return { body: response.body } +} +"#, + ) + .expect("failed to write fixture"); + + let output = Command::new(daglang_bin()) + .arg("manifest") + .arg(&fixture) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang manifest on fixture"); + + assert!( + output.status.success(), + "manifest command failed: {}", + String::from_utf8_lossy(&output.stderr) + ); + let stdout = String::from_utf8_lossy(&output.stdout); + assert!(stdout.contains("service_param_source_targets: 0")); + + std::fs::remove_file(fixture).expect("failed to cleanup fixture"); +} + #[test] fn viz_command_renders_mermaid_for_compiled_file() { let output = Command::new(daglang_bin()) From f6a4e5b66fd4cc844b9c48ce05ecd2a5e84a1925 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 07:28:41 +0000 Subject: [PATCH 036/169] Add regression for interface-only provides lifecycle tolerance Co-authored-by: Brian Searls --- core/daglang/daglang-lower/src/lib.rs | 29 +++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/core/daglang/daglang-lower/src/lib.rs b/core/daglang/daglang-lower/src/lib.rs index 404b4ff2d18..7d1c0699ad2 100644 --- a/core/daglang/daglang-lower/src/lib.rs +++ b/core/daglang/daglang-lower/src/lib.rs @@ -2287,6 +2287,35 @@ func run() -> { ok: Bool } uses store: Storage { ); } + #[test] + fn known_interface_provides_without_lifecycle_are_tolerated() { + let typed = typed_project_from_sources(&[( + "dsl/resources/interface_provides.dag", + r#"module sample.resources +interface Storage { + capability read { + input { path: String } + output { body: String } + } +} +func run() -> { ok: Bool } provides out: Storage { + return { ok: true } +}"#, + )]); + let dag = lower_typed_project(&typed).expect("lowering should succeed"); + assert!(dag + .nodes + .iter() + .any(|node| node.id.0 == "provide_resource_sample_resources_run_out")); + assert!( + !dag.edges.iter().any(|edge| { + edge.from_node.0 == "provide_resource_sample_resources_run_out" + && edge.to_port.0 == "resource_handle" + }), + "interface-only provides should not fabricate lifecycle release edges" + ); + } + #[test] fn lower_errors_when_no_callable_items_exist() { let typed = typed_project_from_sources(&[( From 260d47d5719dfb8269d00a6fb6145f263139d163 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 07:30:18 +0000 Subject: [PATCH 037/169] Add expand regression for service param-source wiring Co-authored-by: Brian Searls --- .../daglang-cli/tests/compile_commands.rs | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index 3a00f1131c0..e718d4eb687 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -339,6 +339,50 @@ func run() -> { ok: Bool } provides out: MissingResource { std::fs::remove_file(fixture).expect("failed to cleanup fixture"); } +#[test] +fn expand_command_shows_param_source_wiring_for_identifier_service_args() { + let fixture = unique_temp_file("service_param_source_expand"); + std::fs::write( + &fixture, + r#"module sample.services +interface Storage { + capability read { + input { path: String } + output { body: String } + } +} +service FsStorage implements Storage { + operation read(path: String) -> { body: String } +} +func run(path: String) -> { body: String } { + let response = FsStorage.read(path: path) + return { body: response.body } +} +"#, + ) + .expect("failed to write fixture"); + + let output = Command::new(daglang_bin()) + .arg("expand") + .arg(&fixture) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang expand on fixture"); + + assert!( + output.status.success(), + "expand command failed: {}", + String::from_utf8_lossy(&output.stderr) + ); + let stdout = String::from_utf8_lossy(&output.stdout); + assert!(stdout.contains("param_source_sample_services_run_path")); + assert!(stdout.contains( + "param_source_sample_services_run_path.path -> prepare_transport_sample_services_FsStorage_read.path" + )); + + std::fs::remove_file(fixture).expect("failed to cleanup fixture"); +} + #[test] fn compile_command_directory_mode_fails_on_unresolved_imports() { let root = unique_temp_dir("unresolved_import"); From 7a11de38fbd0c18898cd432588849736f95b253e Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 07:31:54 +0000 Subject: [PATCH 038/169] Add manifest regression for interface-only provides obligations Co-authored-by: Brian Searls --- .../daglang-cli/tests/compile_commands.rs | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index e718d4eb687..5d1de8fd855 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -196,6 +196,44 @@ func run() -> { body: String } { std::fs::remove_file(fixture).expect("failed to cleanup fixture"); } +#[test] +fn manifest_command_interface_only_provides_has_no_release_obligation() { + let fixture = unique_temp_file("manifest_interface_provides"); + std::fs::write( + &fixture, + r#"module sample.provides +interface Storage { + capability read { + input { path: String } + output { body: String } + } +} +func run() -> { ok: Bool } provides out: Storage { + return { ok: true } +} +"#, + ) + .expect("failed to write fixture"); + + let output = Command::new(daglang_bin()) + .arg("manifest") + .arg(&fixture) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang manifest on fixture"); + + assert!( + output.status.success(), + "manifest command failed: {}", + String::from_utf8_lossy(&output.stderr) + ); + let stdout = String::from_utf8_lossy(&output.stdout); + assert!(stdout.contains("resource_provide_targets: 1")); + assert!(stdout.contains("resource_release_targets: 0")); + + std::fs::remove_file(fixture).expect("failed to cleanup fixture"); +} + #[test] fn viz_command_renders_mermaid_for_compiled_file() { let output = Command::new(daglang_bin()) From 47021dc69f8021097e4a5ed19b85c9264502c21d Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 07:36:34 +0000 Subject: [PATCH 039/169] Validate service call arity and named arguments in typecheck Co-authored-by: Brian Searls --- core/daglang/daglang-typecheck/src/lib.rs | 333 ++++++++++++++++++++++ 1 file changed, 333 insertions(+) diff --git a/core/daglang/daglang-typecheck/src/lib.rs b/core/daglang/daglang-typecheck/src/lib.rs index b250ec15d74..172e710d4a3 100644 --- a/core/daglang/daglang-typecheck/src/lib.rs +++ b/core/daglang/daglang-typecheck/src/lib.rs @@ -157,6 +157,19 @@ pub enum TypeError { callee: String, argument: String, }, + /// Service call expression used wrong number of arguments. + ServiceCallArityMismatch { + caller: String, + service_call: String, + expected: usize, + got: usize, + }, + /// Service call expression used an unknown named argument. + UnknownServiceCallArgument { + caller: String, + service_call: String, + argument: String, + }, /// `uses` clause references an unknown resource/interface type. UnknownUsedResourceType { item: String, @@ -259,6 +272,23 @@ impl std::fmt::Display for TypeError { f, "unknown named argument `{argument}` in call to `{callee}` within `{caller}`" ), + Self::ServiceCallArityMismatch { + caller, + service_call, + expected, + got, + } => write!( + f, + "service call arity mismatch in `{caller}` for `{service_call}`: expected {expected}, got {got}" + ), + Self::UnknownServiceCallArgument { + caller, + service_call, + argument, + } => write!( + f, + "unknown named argument `{argument}` in service call `{service_call}` within `{caller}`" + ), Self::UnknownUsedResourceType { item, binding, @@ -303,6 +333,7 @@ pub fn typecheck_module_graph_with_options( ) -> Result> { let known_types = collect_known_types(&graph.modules); let callable_registry = collect_unique_callables(&graph.modules); + let service_call_registry = collect_service_call_contracts(&graph.modules); let interface_registry = collect_interfaces(&graph.modules); let resource_type_registry = collect_resource_types(&graph.modules); let available_modules = graph @@ -315,6 +346,7 @@ pub fn typecheck_module_graph_with_options( let context = TypecheckContext { known_types: &known_types, callable_registry: &callable_registry, + service_call_registry: &service_call_registry, interface_registry: &interface_registry, resource_type_registry: &resource_type_registry, allow_unresolved_references: options.allow_unresolved_imports, @@ -361,6 +393,7 @@ pub fn typecheck_module_graph_with_options( struct TypecheckContext<'a> { known_types: &'a HashSet, callable_registry: &'a HashMap>, + service_call_registry: &'a ServiceCallRegistry, interface_registry: &'a InterfaceRegistry, resource_type_registry: &'a ResourceTypeRegistry, allow_unresolved_references: bool, @@ -414,6 +447,7 @@ fn collect_signatures( &def.name, &def.body.stmts, context.callable_registry, + context.service_call_registry, errors, ); signatures.push(TypedItemSignature::Fn(TypedCallableSignature { @@ -452,6 +486,7 @@ fn collect_signatures( &def.name, &def.body.stmts, context.callable_registry, + context.service_call_registry, errors, ); signatures.push(TypedItemSignature::Func(TypedCallableSignature { @@ -489,6 +524,7 @@ fn collect_signatures( &def.name, &def.body.stmts, context.callable_registry, + context.service_call_registry, errors, ); signatures.push(TypedItemSignature::Pattern(TypedCallableSignature { @@ -567,6 +603,17 @@ struct CallableContract { params: HashSet, } +#[derive(Debug, Clone, PartialEq, Eq)] +struct ServiceCallContract { + arity: usize, + params: HashSet, +} + +#[derive(Debug, Clone, Default)] +struct ServiceCallRegistry { + by_key: HashMap>, +} + fn collect_unique_callables( modules: &[ResolvedModule], ) -> HashMap> { @@ -596,6 +643,67 @@ fn collect_unique_callables( callables } +fn collect_service_call_contracts(modules: &[ResolvedModule]) -> ServiceCallRegistry { + let mut registry = ServiceCallRegistry::default(); + for module in modules { + let module_name = module.module_path.join("."); + for item in &module.ast.items { + let Item::ServiceDef(service) = &item.node else { + continue; + }; + for operation in &service.operations { + let contract = ServiceCallContract { + arity: operation.inputs.len(), + params: operation + .inputs + .iter() + .map(|field| field.name.clone()) + .collect(), + }; + register_service_call_contract( + &mut registry, + format!("{}.{}", service.name, operation.name), + contract.clone(), + ); + let service_tail = service + .name + .rsplit('.') + .next() + .unwrap_or(service.name.as_str()); + register_service_call_contract( + &mut registry, + format!("{service_tail}.{}", operation.name), + contract.clone(), + ); + register_service_call_contract( + &mut registry, + format!("{}.{}.{}", module_name, service.name, operation.name), + contract, + ); + } + } + } + registry +} + +fn register_service_call_contract( + registry: &mut ServiceCallRegistry, + key: String, + contract: ServiceCallContract, +) { + registry + .by_key + .entry(key) + .and_modify(|existing| { + if let Some(current) = existing { + if current != &contract { + *existing = None; + } + } + }) + .or_insert_with(|| Some(contract)); +} + #[derive(Debug, Clone, Default)] struct InterfaceRegistry { full: HashMap, @@ -843,6 +951,7 @@ fn validate_callable_body( caller: &str, stmts: &[Stmt], callable_registry: &HashMap>, + service_call_registry: &ServiceCallRegistry, errors: &mut Vec, ) { let mut calls = Vec::new(); @@ -869,6 +978,31 @@ fn validate_callable_body( } } } + + let mut service_calls = Vec::new(); + collect_service_calls_from_stmts(stmts, &mut service_calls); + for call in service_calls { + let Some(contract) = resolve_service_call_contract(&call.path, service_call_registry) else { + continue; + }; + if call.arg_count != contract.arity { + errors.push(TypeError::ServiceCallArityMismatch { + caller: caller.to_string(), + service_call: call.path.join("."), + expected: contract.arity, + got: call.arg_count, + }); + } + for named in call.named_args { + if !contract.params.contains(&named) { + errors.push(TypeError::UnknownServiceCallArgument { + caller: caller.to_string(), + service_call: call.path.join("."), + argument: named, + }); + } + } + } } fn validate_resource_interface_conformance( @@ -1065,6 +1199,29 @@ fn resolve_resource_type_name( registry.short.get(short).and_then(|entry| entry.clone()) } +fn resolve_service_call_contract( + call_path: &[String], + registry: &ServiceCallRegistry, +) -> Option { + if call_path.len() < 2 { + return None; + } + let operation = call_path.last()?; + let service_name = call_path[..call_path.len() - 1].join("."); + let short_service = call_path[call_path.len() - 2].clone(); + let keys = [ + format!("{service_name}.{operation}"), + format!("{short_service}.{operation}"), + call_path.join("."), + ]; + for key in keys { + if let Some(Some(contract)) = registry.by_key.get(&key) { + return Some(contract.clone()); + } + } + None +} + fn canonical_type_name(name: &str) -> String { name.split('<').next().unwrap_or(name).trim().to_string() } @@ -1076,6 +1233,13 @@ struct BodyCall { named_args: Vec, } +#[derive(Debug, Clone, PartialEq, Eq)] +struct BodyServiceCall { + path: Vec, + arg_count: usize, + named_args: Vec, +} + fn collect_calls_from_stmts(stmts: &[Stmt], calls: &mut Vec) { for stmt in stmts { match stmt { @@ -1181,6 +1345,109 @@ fn collect_calls_from_expr(expr: &Expr, calls: &mut Vec) { } } +fn collect_service_calls_from_stmts(stmts: &[Stmt], calls: &mut Vec) { + for stmt in stmts { + match stmt { + Stmt::Let(_, expr) | Stmt::Assign(_, expr) | Stmt::Expr(expr) => { + collect_service_calls_from_expr(expr, calls); + } + Stmt::Return(fields) => { + for (_, expr) in fields { + collect_service_calls_from_expr(expr, calls); + } + } + } + } +} + +fn collect_service_calls_from_expr(expr: &Expr, calls: &mut Vec) { + match expr { + Expr::Call(_, args) => { + for (_, arg) in args { + collect_service_calls_from_expr(arg, calls); + } + } + Expr::ServiceCall(path, args) => { + calls.push(BodyServiceCall { + path: path.clone(), + arg_count: args.len(), + named_args: args + .iter() + .filter_map(|(name, _)| name.clone()) + .collect::>(), + }); + for (_, arg) in args { + collect_service_calls_from_expr(arg, calls); + } + } + Expr::FieldAccess(base, _) => collect_service_calls_from_expr(base, calls), + Expr::BinOp(lhs, _, rhs) => { + collect_service_calls_from_expr(lhs, calls); + collect_service_calls_from_expr(rhs, calls); + } + Expr::UnaryOp(_, inner) => collect_service_calls_from_expr(inner, calls), + Expr::StringInterp(parts) => { + for part in parts { + if let daglang_syntax::ast::StringPart::Expr(inner) = part { + collect_service_calls_from_expr(inner, calls); + } + } + } + Expr::Record(_, fields) => { + for (_, value) in fields { + collect_service_calls_from_expr(value, calls); + } + } + Expr::Match(scrutinee, arms) => { + collect_service_calls_from_expr(scrutinee, calls); + for arm in arms { + if let Some(guard) = &arm.guard { + collect_service_calls_from_expr(guard, calls); + } + collect_service_calls_from_expr(&arm.body, calls); + } + } + Expr::If(cond, then_expr, else_expr) => { + collect_service_calls_from_expr(cond, calls); + collect_service_calls_from_expr(then_expr, calls); + if let Some(otherwise) = else_expr { + collect_service_calls_from_expr(otherwise, calls); + } + } + Expr::For(_, iterable, body) => { + collect_service_calls_from_expr(iterable, calls); + collect_service_calls_from_expr(body, calls); + } + Expr::Pipe(lhs, rhs) => { + collect_service_calls_from_expr(lhs, calls); + collect_service_calls_from_expr(rhs, calls); + } + Expr::Lambda(_, body) => collect_service_calls_from_expr(body, calls), + Expr::List(items) => { + for item in items { + collect_service_calls_from_expr(item, calls); + } + } + Expr::Map(entries) => { + for (key, value) in entries { + collect_service_calls_from_expr(key, calls); + collect_service_calls_from_expr(value, calls); + } + } + Expr::Guarded(inner, guard) => { + collect_service_calls_from_expr(inner, calls); + collect_service_calls_from_expr(guard, calls); + } + Expr::After(inner, _) => collect_service_calls_from_expr(inner, calls), + Expr::Return(fields) => { + for (_, value) in fields { + collect_service_calls_from_expr(value, calls); + } + } + Expr::Literal(_) | Expr::Ident(_) => {} + } +} + fn should_validate_call_name(name: &str) -> bool { !matches!(name, "" | "as" | "with" | "fn") } @@ -1369,6 +1636,72 @@ mod tests { ))); } + #[test] + fn service_call_arity_mismatch_is_reported() { + let graph = module_graph_from_sources(&[( + "service_arity_mismatch.dag", + r#"module sample.services +interface Storage { + capability read { + input { path: String } + output { body: String } + } +} +service FsStorage implements Storage { + operation read(path: String) -> { body: String } +} +func run() -> { ok: Bool } { + let response = FsStorage.read() + return { ok: true } +}"#, + )]); + let errors = + typecheck_module_graph(graph).expect_err("service call arity mismatch should fail"); + assert!(errors.iter().any(|error| matches!( + error, + TypeError::ServiceCallArityMismatch { + caller, + service_call, + expected, + got + } if caller == "run" + && service_call == "FsStorage.read" + && *expected == 1 + && *got == 0 + ))); + } + + #[test] + fn unknown_named_service_call_argument_is_reported() { + let graph = module_graph_from_sources(&[( + "service_unknown_arg.dag", + r#"module sample.services +interface Storage { + capability read { + input { path: String } + output { body: String } + } +} +service FsStorage implements Storage { + operation read(path: String) -> { body: String } +} +func run(path: String) -> { body: String } { + let response = FsStorage.read(file: path) + return { body: response.body } +}"#, + )]); + let errors = typecheck_module_graph(graph) + .expect_err("unknown named service call argument should fail"); + assert!(errors.iter().any(|error| matches!( + error, + TypeError::UnknownServiceCallArgument { + caller, + service_call, + argument + } if caller == "run" && service_call == "FsStorage.read" && argument == "file" + ))); + } + #[test] fn resource_missing_interface_capability_is_reported() { let graph = module_graph_from_sources(&[( From 00c8d2e53e8facd66cb7481606a83ac052a1bb56 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 07:40:43 +0000 Subject: [PATCH 040/169] Add compile regressions for service call contract diagnostics Co-authored-by: Brian Searls --- .../daglang-cli/tests/compile_commands.rs | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index 5d1de8fd855..f2a48f64c91 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -505,6 +505,90 @@ fn compile_command_directory_mode_fails_on_unknown_provides_resource_type() { std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); } +#[test] +fn compile_command_reports_service_call_arity_typecheck_error() { + let fixture = unique_temp_file("service_call_arity"); + std::fs::write( + &fixture, + r#"module sample.services +interface Storage { + capability read { + input { path: String } + output { body: String } + } +} +service FsStorage implements Storage { + operation read(path: String) -> { body: String } +} +func run() -> { ok: Bool } { + let response = FsStorage.read() + return { ok: true } +} +"#, + ) + .expect("failed to write fixture"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&fixture) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile"); + + assert!( + !output.status.success(), + "compile should fail on service call arity mismatch" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("typecheck errors")); + assert!(stderr.contains("service call arity mismatch")); + assert!(stderr.contains("FsStorage.read")); + + std::fs::remove_file(fixture).expect("failed to cleanup fixture"); +} + +#[test] +fn compile_command_reports_unknown_named_service_argument_typecheck_error() { + let fixture = unique_temp_file("service_call_unknown_arg"); + std::fs::write( + &fixture, + r#"module sample.services +interface Storage { + capability read { + input { path: String } + output { body: String } + } +} +service FsStorage implements Storage { + operation read(path: String) -> { body: String } +} +func run(path: String) -> { body: String } { + let response = FsStorage.read(file: path) + return { body: response.body } +} +"#, + ) + .expect("failed to write fixture"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&fixture) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile"); + + assert!( + !output.status.success(), + "compile should fail on unknown named service argument" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("typecheck errors")); + assert!(stderr.contains("unknown named argument `file`")); + assert!(stderr.contains("service call `FsStorage.read`")); + + std::fs::remove_file(fixture).expect("failed to cleanup fixture"); +} + #[test] fn compile_command_directory_mode_reports_module_path_mismatch() { let root = unique_temp_dir("path_mismatch"); From d48dc31a9b7d211b77eaa6a24a4bbc8d62fb2955 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 07:43:32 +0000 Subject: [PATCH 041/169] Report duplicate named arguments in callable and service calls Co-authored-by: Brian Searls --- core/daglang/daglang-typecheck/src/lib.rs | 95 +++++++++++++++++++++++ 1 file changed, 95 insertions(+) diff --git a/core/daglang/daglang-typecheck/src/lib.rs b/core/daglang/daglang-typecheck/src/lib.rs index 172e710d4a3..657b0b0c86f 100644 --- a/core/daglang/daglang-typecheck/src/lib.rs +++ b/core/daglang/daglang-typecheck/src/lib.rs @@ -157,6 +157,12 @@ pub enum TypeError { callee: String, argument: String, }, + /// Call expression reuses the same named argument multiple times. + DuplicateCallArgument { + caller: String, + callee: String, + argument: String, + }, /// Service call expression used wrong number of arguments. ServiceCallArityMismatch { caller: String, @@ -170,6 +176,12 @@ pub enum TypeError { service_call: String, argument: String, }, + /// Service call expression reuses the same named argument multiple times. + DuplicateServiceCallArgument { + caller: String, + service_call: String, + argument: String, + }, /// `uses` clause references an unknown resource/interface type. UnknownUsedResourceType { item: String, @@ -272,6 +284,14 @@ impl std::fmt::Display for TypeError { f, "unknown named argument `{argument}` in call to `{callee}` within `{caller}`" ), + Self::DuplicateCallArgument { + caller, + callee, + argument, + } => write!( + f, + "duplicate named argument `{argument}` in call to `{callee}` within `{caller}`" + ), Self::ServiceCallArityMismatch { caller, service_call, @@ -289,6 +309,14 @@ impl std::fmt::Display for TypeError { f, "unknown named argument `{argument}` in service call `{service_call}` within `{caller}`" ), + Self::DuplicateServiceCallArgument { + caller, + service_call, + argument, + } => write!( + f, + "duplicate named argument `{argument}` in service call `{service_call}` within `{caller}`" + ), Self::UnknownUsedResourceType { item, binding, @@ -968,7 +996,16 @@ fn validate_callable_body( got: call.arg_count, }); } + let mut seen_named = HashSet::new(); for named in call.named_args { + if !seen_named.insert(named.clone()) { + errors.push(TypeError::DuplicateCallArgument { + caller: caller.to_string(), + callee: call.callee.clone(), + argument: named, + }); + continue; + } if !contract.params.contains(&named) { errors.push(TypeError::UnknownCallArgument { caller: caller.to_string(), @@ -993,7 +1030,16 @@ fn validate_callable_body( got: call.arg_count, }); } + let mut seen_named = HashSet::new(); for named in call.named_args { + if !seen_named.insert(named.clone()) { + errors.push(TypeError::DuplicateServiceCallArgument { + caller: caller.to_string(), + service_call: call.path.join("."), + argument: named, + }); + continue; + } if !contract.params.contains(&named) { errors.push(TypeError::UnknownServiceCallArgument { caller: caller.to_string(), @@ -1636,6 +1682,24 @@ mod tests { ))); } + #[test] + fn duplicate_named_call_argument_is_reported() { + let graph = module_graph_from_sources(&[( + "duplicate_arg.dag", + "module sample.calls\nfn fmt(value: String) -> String { value }\nfn run() -> String { fmt(value: \"a\", value: \"b\") }", + )]); + let errors = + typecheck_module_graph(graph).expect_err("duplicate named argument should fail"); + assert!(errors.iter().any(|error| matches!( + error, + TypeError::DuplicateCallArgument { + caller, + callee, + argument + } if caller == "run" && callee == "fmt" && argument == "value" + ))); + } + #[test] fn service_call_arity_mismatch_is_reported() { let graph = module_graph_from_sources(&[( @@ -1702,6 +1766,37 @@ func run(path: String) -> { body: String } { ))); } + #[test] + fn duplicate_named_service_call_argument_is_reported() { + let graph = module_graph_from_sources(&[( + "service_duplicate_arg.dag", + r#"module sample.services +interface Storage { + capability read { + input { path: String } + output { body: String } + } +} +service FsStorage implements Storage { + operation read(path: String) -> { body: String } +} +func run(path: String) -> { body: String } { + let response = FsStorage.read(path: path, path: path) + return { body: response.body } +}"#, + )]); + let errors = typecheck_module_graph(graph) + .expect_err("duplicate named service call argument should fail"); + assert!(errors.iter().any(|error| matches!( + error, + TypeError::DuplicateServiceCallArgument { + caller, + service_call, + argument + } if caller == "run" && service_call == "FsStorage.read" && argument == "path" + ))); + } + #[test] fn resource_missing_interface_capability_is_reported() { let graph = module_graph_from_sources(&[( From 3d1c20074353e9c9ea3eced820305db9a1684bb7 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 07:47:01 +0000 Subject: [PATCH 042/169] Add CLI regressions for duplicate named argument diagnostics Co-authored-by: Brian Searls --- .../daglang-cli/tests/compile_commands.rs | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index f2a48f64c91..fd8739c8a69 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -589,6 +589,81 @@ func run(path: String) -> { body: String } { std::fs::remove_file(fixture).expect("failed to cleanup fixture"); } +#[test] +fn compile_command_reports_duplicate_named_call_argument_typecheck_error() { + let fixture = unique_temp_file("duplicate_named_call_arg"); + std::fs::write( + &fixture, + r#"module sample.calls +fn fmt(value: String, mode: String) -> String { value } +fn run() -> String { + fmt(value: "a", value: "b") +} +"#, + ) + .expect("failed to write fixture"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&fixture) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile"); + + assert!( + !output.status.success(), + "compile should fail on duplicate named call argument" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("typecheck errors")); + assert!(stderr.contains("duplicate named argument `value`")); + assert!(stderr.contains("call to `fmt`")); + + std::fs::remove_file(fixture).expect("failed to cleanup fixture"); +} + +#[test] +fn compile_command_reports_duplicate_named_service_argument_typecheck_error() { + let fixture = unique_temp_file("duplicate_named_service_arg"); + std::fs::write( + &fixture, + r#"module sample.services +interface Storage { + capability read { + input { path: String, mode: String } + output { body: String } + } +} +service FsStorage implements Storage { + operation read(path: String, mode: String) -> { body: String } +} +func run(path: String) -> { body: String } { + let response = FsStorage.read(path: path, path: path) + return { body: response.body } +} +"#, + ) + .expect("failed to write fixture"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&fixture) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile"); + + assert!( + !output.status.success(), + "compile should fail on duplicate named service argument" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("typecheck errors")); + assert!(stderr.contains("duplicate named argument `path`")); + assert!(stderr.contains("service call `FsStorage.read`")); + + std::fs::remove_file(fixture).expect("failed to cleanup fixture"); +} + #[test] fn compile_command_directory_mode_reports_module_path_mismatch() { let root = unique_temp_dir("path_mismatch"); From 0e374b95e6c957db8d8daf348d66d27d1cee3eaa Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 07:50:13 +0000 Subject: [PATCH 043/169] Fail strict typecheck on unresolved service call targets Co-authored-by: Brian Searls --- .../daglang-cli/tests/compile_commands.rs | 32 ++++++++++ core/daglang/daglang-typecheck/src/lib.rs | 60 +++++++++++++++++++ 2 files changed, 92 insertions(+) diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index fd8739c8a69..e19e7f45af0 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -664,6 +664,38 @@ func run(path: String) -> { body: String } { std::fs::remove_file(fixture).expect("failed to cleanup fixture"); } +#[test] +fn compile_command_directory_mode_fails_on_unresolved_service_call() { + let root = unique_temp_dir("unresolved_service_call_typecheck"); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp dir"); + std::fs::write( + root.join("sample/main.dag"), + r#"module sample.main +func run(path: String) -> { body: String } { + let response = MissingStorage.read(path: path) + return { body: response.body } +}"#, + ) + .expect("failed to write source"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&root) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile on directory"); + + assert!( + !output.status.success(), + "directory compile should fail on unresolved service call" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("typecheck errors")); + assert!(stderr.contains("unresolved service call `MissingStorage.read`")); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); +} + #[test] fn compile_command_directory_mode_reports_module_path_mismatch() { let root = unique_temp_dir("path_mismatch"); diff --git a/core/daglang/daglang-typecheck/src/lib.rs b/core/daglang/daglang-typecheck/src/lib.rs index 657b0b0c86f..d96d261882d 100644 --- a/core/daglang/daglang-typecheck/src/lib.rs +++ b/core/daglang/daglang-typecheck/src/lib.rs @@ -170,6 +170,8 @@ pub enum TypeError { expected: usize, got: usize, }, + /// Service call target could not be resolved to a known service operation contract. + UnresolvedServiceCall { caller: String, service_call: String }, /// Service call expression used an unknown named argument. UnknownServiceCallArgument { caller: String, @@ -301,6 +303,13 @@ impl std::fmt::Display for TypeError { f, "service call arity mismatch in `{caller}` for `{service_call}`: expected {expected}, got {got}" ), + Self::UnresolvedServiceCall { + caller, + service_call, + } => write!( + f, + "unresolved service call `{service_call}` in `{caller}`" + ), Self::UnknownServiceCallArgument { caller, service_call, @@ -476,6 +485,7 @@ fn collect_signatures( &def.body.stmts, context.callable_registry, context.service_call_registry, + context.allow_unresolved_references, errors, ); signatures.push(TypedItemSignature::Fn(TypedCallableSignature { @@ -515,6 +525,7 @@ fn collect_signatures( &def.body.stmts, context.callable_registry, context.service_call_registry, + context.allow_unresolved_references, errors, ); signatures.push(TypedItemSignature::Func(TypedCallableSignature { @@ -553,6 +564,7 @@ fn collect_signatures( &def.body.stmts, context.callable_registry, context.service_call_registry, + context.allow_unresolved_references, errors, ); signatures.push(TypedItemSignature::Pattern(TypedCallableSignature { @@ -980,6 +992,7 @@ fn validate_callable_body( stmts: &[Stmt], callable_registry: &HashMap>, service_call_registry: &ServiceCallRegistry, + allow_unresolved_references: bool, errors: &mut Vec, ) { let mut calls = Vec::new(); @@ -1020,6 +1033,12 @@ fn validate_callable_body( collect_service_calls_from_stmts(stmts, &mut service_calls); for call in service_calls { let Some(contract) = resolve_service_call_contract(&call.path, service_call_registry) else { + if !allow_unresolved_references { + errors.push(TypeError::UnresolvedServiceCall { + caller: caller.to_string(), + service_call: call.path.join("."), + }); + } continue; }; if call.arg_count != contract.arity { @@ -1735,6 +1754,47 @@ func run() -> { ok: Bool } { ))); } + #[test] + fn strict_mode_reports_unresolved_service_call() { + let graph = module_graph_from_sources(&[( + "service_unresolved_call.dag", + r#"module sample.services +func run(path: String) -> { body: String } { + let response = MissingStorage.read(path: path) + return { body: response.body } +}"#, + )]); + let errors = typecheck_module_graph_with_options( + graph, + TypecheckOptions { + allow_unresolved_imports: false, + }, + ) + .expect_err("strict mode should fail for unresolved service call"); + assert!(errors.iter().any(|error| matches!( + error, + TypeError::UnresolvedServiceCall { + caller, + service_call + } if caller == "run" && service_call == "MissingStorage.read" + ))); + } + + #[test] + fn relaxed_mode_allows_unresolved_service_call() { + let graph = module_graph_from_sources(&[( + "service_unresolved_call_relaxed.dag", + r#"module sample.services +func run(path: String) -> { body: String } { + let response = MissingStorage.read(path: path) + return { body: response.body } +}"#, + )]); + let typed = typecheck_module_graph(graph) + .expect("relaxed mode should allow unresolved service call for lower-stage validation"); + assert_eq!(typed.modules.len(), 1); + } + #[test] fn unknown_named_service_call_argument_is_reported() { let graph = module_graph_from_sources(&[( From 0218d2d0743efbe21dd1f2b759da68abea420848 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 07:54:11 +0000 Subject: [PATCH 044/169] Add single-file compile regression for unresolved service calls Co-authored-by: Brian Searls --- .../daglang-cli/tests/compile_commands.rs | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index e19e7f45af0..95ff296d41a 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -57,6 +57,47 @@ fn compile_command_emits_summary_for_single_file() { assert!(stdout.contains("target/generated/rust/main.rs")); } +#[test] +fn compile_command_single_file_unresolved_service_call_reports_lower_error() { + let fixture = unique_temp_file("compile_unresolved_service_single_file"); + std::fs::write( + &fixture, + r#"module sample.services +interface Storage { + capability read { + input { path: String } + output { body: String } + } +} +service FsStorage implements Storage { + operation read(path: String) -> { body: String } +} +func run(path: String) -> { body: String } { + let response = MissingStorage.read(path: path) + return { body: response.body } +} +"#, + ) + .expect("failed to write fixture"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&fixture) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile for unresolved service call fixture"); + + assert!( + !output.status.success(), + "single-file compile should fail for unresolved service call" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("lower error: unresolved service call")); + assert!(stderr.contains("MissingStorage.read")); + + std::fs::remove_file(fixture).expect("failed to cleanup fixture"); +} + #[test] fn expand_command_shows_lowered_nodes_and_edges() { let output = Command::new(daglang_bin()) From e12b61ddd0cf70dd40dae3ad65c9cd141eede4fe Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 07:59:44 +0000 Subject: [PATCH 045/169] Report ambiguous service calls in strict typecheck mode Co-authored-by: Brian Searls --- .../daglang-cli/tests/compile_commands.rs | 48 ++++++ core/daglang/daglang-typecheck/src/lib.rs | 143 +++++++++++++----- 2 files changed, 153 insertions(+), 38 deletions(-) diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index 95ff296d41a..5942986b5bf 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -737,6 +737,54 @@ func run(path: String) -> { body: String } { std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); } +#[test] +fn compile_command_directory_mode_fails_on_ambiguous_service_call() { + let root = unique_temp_dir("ambiguous_service_call_typecheck"); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp dir"); + std::fs::write( + root.join("sample/first.dag"), + r#"module sample.first +service SharedService { + operation read(path: String) -> { body: String } +}"#, + ) + .expect("failed to write first service source"); + std::fs::write( + root.join("sample/second.dag"), + r#"module sample.second +service SharedService { + operation read(path: String) -> { body: String } +}"#, + ) + .expect("failed to write second service source"); + std::fs::write( + root.join("sample/main.dag"), + r#"module sample.main +func run(path: String) -> { body: String } { + let response = SharedService.read(path: path) + return { body: response.body } +}"#, + ) + .expect("failed to write main source"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&root) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile on directory"); + + assert!( + !output.status.success(), + "directory compile should fail on ambiguous service call" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("typecheck errors")); + assert!(stderr.contains("ambiguous service call `SharedService.read`")); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); +} + #[test] fn compile_command_directory_mode_reports_module_path_mismatch() { let root = unique_temp_dir("path_mismatch"); diff --git a/core/daglang/daglang-typecheck/src/lib.rs b/core/daglang/daglang-typecheck/src/lib.rs index d96d261882d..b3d08fcaeb3 100644 --- a/core/daglang/daglang-typecheck/src/lib.rs +++ b/core/daglang/daglang-typecheck/src/lib.rs @@ -172,6 +172,8 @@ pub enum TypeError { }, /// Service call target could not be resolved to a known service operation contract. UnresolvedServiceCall { caller: String, service_call: String }, + /// Service call target matches multiple possible service operation contracts. + AmbiguousServiceCall { caller: String, service_call: String }, /// Service call expression used an unknown named argument. UnknownServiceCallArgument { caller: String, @@ -310,6 +312,13 @@ impl std::fmt::Display for TypeError { f, "unresolved service call `{service_call}` in `{caller}`" ), + Self::AmbiguousServiceCall { + caller, + service_call, + } => write!( + f, + "ambiguous service call `{service_call}` in `{caller}`" + ), Self::UnknownServiceCallArgument { caller, service_call, @@ -654,6 +663,13 @@ struct ServiceCallRegistry { by_key: HashMap>, } +#[derive(Debug, Clone)] +enum ServiceCallResolution { + Resolved(ServiceCallContract), + Ambiguous, + Missing, +} + fn collect_unique_callables( modules: &[ResolvedModule], ) -> HashMap> { @@ -700,26 +716,18 @@ fn collect_service_call_contracts(modules: &[ResolvedModule]) -> ServiceCallRegi .map(|field| field.name.clone()) .collect(), }; - register_service_call_contract( - &mut registry, - format!("{}.{}", service.name, operation.name), - contract.clone(), - ); let service_tail = service .name .rsplit('.') .next() .unwrap_or(service.name.as_str()); - register_service_call_contract( - &mut registry, - format!("{service_tail}.{}", operation.name), - contract.clone(), - ); - register_service_call_contract( - &mut registry, - format!("{}.{}.{}", module_name, service.name, operation.name), - contract, - ); + let mut keys = HashSet::new(); + keys.insert(format!("{}.{}", service.name, operation.name)); + keys.insert(format!("{service_tail}.{}", operation.name)); + keys.insert(format!("{}.{}.{}", module_name, service.name, operation.name)); + for key in keys { + register_service_call_contract(&mut registry, key, contract.clone()); + } } } } @@ -734,13 +742,7 @@ fn register_service_call_contract( registry .by_key .entry(key) - .and_modify(|existing| { - if let Some(current) = existing { - if current != &contract { - *existing = None; - } - } - }) + .and_modify(|existing| *existing = None) .or_insert_with(|| Some(contract)); } @@ -1032,19 +1034,32 @@ fn validate_callable_body( let mut service_calls = Vec::new(); collect_service_calls_from_stmts(stmts, &mut service_calls); for call in service_calls { - let Some(contract) = resolve_service_call_contract(&call.path, service_call_registry) else { - if !allow_unresolved_references { - errors.push(TypeError::UnresolvedServiceCall { - caller: caller.to_string(), - service_call: call.path.join("."), - }); + let service_call_name = call.path.join("."); + let contract = match resolve_service_call_contract(&call.path, service_call_registry) { + ServiceCallResolution::Resolved(contract) => contract, + ServiceCallResolution::Ambiguous => { + if !allow_unresolved_references { + errors.push(TypeError::AmbiguousServiceCall { + caller: caller.to_string(), + service_call: service_call_name, + }); + } + continue; + } + ServiceCallResolution::Missing => { + if !allow_unresolved_references { + errors.push(TypeError::UnresolvedServiceCall { + caller: caller.to_string(), + service_call: service_call_name, + }); + } + continue; } - continue; }; if call.arg_count != contract.arity { errors.push(TypeError::ServiceCallArityMismatch { caller: caller.to_string(), - service_call: call.path.join("."), + service_call: service_call_name.clone(), expected: contract.arity, got: call.arg_count, }); @@ -1054,7 +1069,7 @@ fn validate_callable_body( if !seen_named.insert(named.clone()) { errors.push(TypeError::DuplicateServiceCallArgument { caller: caller.to_string(), - service_call: call.path.join("."), + service_call: service_call_name.clone(), argument: named, }); continue; @@ -1062,7 +1077,7 @@ fn validate_callable_body( if !contract.params.contains(&named) { errors.push(TypeError::UnknownServiceCallArgument { caller: caller.to_string(), - service_call: call.path.join("."), + service_call: service_call_name.clone(), argument: named, }); } @@ -1267,11 +1282,13 @@ fn resolve_resource_type_name( fn resolve_service_call_contract( call_path: &[String], registry: &ServiceCallRegistry, -) -> Option { +) -> ServiceCallResolution { if call_path.len() < 2 { - return None; + return ServiceCallResolution::Missing; } - let operation = call_path.last()?; + let Some(operation) = call_path.last() else { + return ServiceCallResolution::Missing; + }; let service_name = call_path[..call_path.len() - 1].join("."); let short_service = call_path[call_path.len() - 2].clone(); let keys = [ @@ -1279,12 +1296,20 @@ fn resolve_service_call_contract( format!("{short_service}.{operation}"), call_path.join("."), ]; + let mut saw_ambiguous = false; for key in keys { - if let Some(Some(contract)) = registry.by_key.get(&key) { - return Some(contract.clone()); + if let Some(entry) = registry.by_key.get(&key) { + match entry { + Some(contract) => return ServiceCallResolution::Resolved(contract.clone()), + None => saw_ambiguous = true, + } } } - None + if saw_ambiguous { + ServiceCallResolution::Ambiguous + } else { + ServiceCallResolution::Missing + } } fn canonical_type_name(name: &str) -> String { @@ -1780,6 +1805,48 @@ func run(path: String) -> { body: String } { ))); } + #[test] + fn strict_mode_reports_ambiguous_service_call() { + let graph = module_graph_from_sources(&[ + ( + "sample/first.dag", + r#"module sample.first +service SharedService { + operation read(path: String) -> { body: String } +}"#, + ), + ( + "sample/second.dag", + r#"module sample.second +service SharedService { + operation read(path: String) -> { body: String } +}"#, + ), + ( + "sample/main.dag", + r#"module sample.main +func run(path: String) -> { body: String } { + let response = SharedService.read(path: path) + return { body: response.body } +}"#, + ), + ]); + let errors = typecheck_module_graph_with_options( + graph, + TypecheckOptions { + allow_unresolved_imports: false, + }, + ) + .expect_err("strict mode should fail for ambiguous service call"); + assert!(errors.iter().any(|error| matches!( + error, + TypeError::AmbiguousServiceCall { + caller, + service_call + } if caller == "run" && service_call == "SharedService.read" + ))); + } + #[test] fn relaxed_mode_allows_unresolved_service_call() { let graph = module_graph_from_sources(&[( From 91bac5d0a5d7a19f52f0e00779d0ef50c49fb499 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 08:04:37 +0000 Subject: [PATCH 046/169] Detect ambiguous resource type aliases in strict mode Co-authored-by: Brian Searls --- .../daglang-cli/tests/compile_commands.rs | 76 +++++++++ core/daglang/daglang-typecheck/src/lib.rs | 161 ++++++++++++++++-- 2 files changed, 222 insertions(+), 15 deletions(-) diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index 5942986b5bf..210d82ddfc2 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -518,6 +518,44 @@ fn compile_command_directory_mode_fails_on_unknown_uses_resource_type() { std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); } +#[test] +fn compile_command_directory_mode_fails_on_ambiguous_uses_resource_type() { + let root = unique_temp_dir("ambiguous_uses_type"); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp dir"); + std::fs::write( + root.join("sample/one.dag"), + "module sample.one\nresource SharedResource {}", + ) + .expect("failed to write source"); + std::fs::write( + root.join("sample/two.dag"), + "module sample.two\nresource SharedResource {}", + ) + .expect("failed to write source"); + std::fs::write( + root.join("sample/main.dag"), + "module sample.main\nfunc run() -> { ok: Bool } uses fs: SharedResource { return { ok: true } }", + ) + .expect("failed to write source"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&root) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile on directory"); + + assert!( + !output.status.success(), + "directory compile should fail on ambiguous uses resource type" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("typecheck errors")); + assert!(stderr.contains("ambiguous used resource type `SharedResource`")); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); +} + #[test] fn compile_command_directory_mode_fails_on_unknown_provides_resource_type() { let root = unique_temp_dir("unknown_provides_type"); @@ -546,6 +584,44 @@ fn compile_command_directory_mode_fails_on_unknown_provides_resource_type() { std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); } +#[test] +fn compile_command_directory_mode_fails_on_ambiguous_provides_resource_type() { + let root = unique_temp_dir("ambiguous_provides_type"); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp dir"); + std::fs::write( + root.join("sample/one.dag"), + "module sample.one\nresource SharedResource {}", + ) + .expect("failed to write source"); + std::fs::write( + root.join("sample/two.dag"), + "module sample.two\nresource SharedResource {}", + ) + .expect("failed to write source"); + std::fs::write( + root.join("sample/main.dag"), + "module sample.main\nfunc run() -> { ok: Bool } provides out: SharedResource { return { ok: true } }", + ) + .expect("failed to write source"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&root) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile on directory"); + + assert!( + !output.status.success(), + "directory compile should fail on ambiguous provides resource type" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("typecheck errors")); + assert!(stderr.contains("ambiguous provided resource type `SharedResource`")); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); +} + #[test] fn compile_command_reports_service_call_arity_typecheck_error() { let fixture = unique_temp_file("service_call_arity"); diff --git a/core/daglang/daglang-typecheck/src/lib.rs b/core/daglang/daglang-typecheck/src/lib.rs index b3d08fcaeb3..d9d7e5b5e50 100644 --- a/core/daglang/daglang-typecheck/src/lib.rs +++ b/core/daglang/daglang-typecheck/src/lib.rs @@ -192,6 +192,12 @@ pub enum TypeError { binding: String, resource_type: String, }, + /// `uses` clause references an ambiguous short resource/interface type. + AmbiguousUsedResourceType { + item: String, + binding: String, + resource_type: String, + }, /// Duplicate `uses` binding within a callable declaration. DuplicateUsesBinding { item: String, binding: String }, /// Duplicate `provides` binding within a callable declaration. @@ -204,6 +210,12 @@ pub enum TypeError { binding: String, resource_type: String, }, + /// `provides` clause references an ambiguous short resource/interface type. + AmbiguousProvidedResourceType { + item: String, + binding: String, + resource_type: String, + }, } impl std::fmt::Display for TypeError { @@ -343,6 +355,14 @@ impl std::fmt::Display for TypeError { f, "unknown used resource type `{resource_type}` for binding `{binding}` in `{item}`" ), + Self::AmbiguousUsedResourceType { + item, + binding, + resource_type, + } => write!( + f, + "ambiguous used resource type `{resource_type}` for binding `{binding}` in `{item}`" + ), Self::DuplicateUsesBinding { item, binding } => write!( f, "duplicate uses binding `{binding}` in `{item}`" @@ -363,6 +383,14 @@ impl std::fmt::Display for TypeError { f, "unknown provided resource type `{resource_type}` for binding `{binding}` in `{item}`" ), + Self::AmbiguousProvidedResourceType { + item, + binding, + resource_type, + } => write!( + f, + "ambiguous provided resource type `{resource_type}` for binding `{binding}` in `{item}`" + ), } } } @@ -670,6 +698,13 @@ enum ServiceCallResolution { Missing, } +#[derive(Debug, Clone, PartialEq, Eq)] +enum ResourceTypeResolution { + Resolved(String), + Ambiguous, + Missing, +} + fn collect_unique_callables( modules: &[ResolvedModule], ) -> HashMap> { @@ -930,12 +965,22 @@ fn validate_uses_clauses( } if !allow_unresolved_references { let resource_type = canonical_type_name(&type_expr_to_string(&usage.resource_type)); - if resolve_resource_type_name(&resource_type, registry).is_none() { - errors.push(TypeError::UnknownUsedResourceType { - item: item_name.to_string(), - binding: usage.binding.clone(), - resource_type, - }); + match resolve_resource_type_name(&resource_type, registry) { + ResourceTypeResolution::Resolved(_) => {} + ResourceTypeResolution::Ambiguous => { + errors.push(TypeError::AmbiguousUsedResourceType { + item: item_name.to_string(), + binding: usage.binding.clone(), + resource_type, + }); + } + ResourceTypeResolution::Missing => { + errors.push(TypeError::UnknownUsedResourceType { + item: item_name.to_string(), + binding: usage.binding.clone(), + resource_type, + }); + } } } } @@ -958,12 +1003,22 @@ fn validate_provides_clauses( } if !allow_unresolved_references { let resource_type = canonical_type_name(&type_expr_to_string(&provided.resource_type)); - if resolve_resource_type_name(&resource_type, registry).is_none() { - errors.push(TypeError::UnknownProvidedResourceType { - item: item_name.to_string(), - binding: provided.binding.clone(), - resource_type, - }); + match resolve_resource_type_name(&resource_type, registry) { + ResourceTypeResolution::Resolved(_) => {} + ResourceTypeResolution::Ambiguous => { + errors.push(TypeError::AmbiguousProvidedResourceType { + item: item_name.to_string(), + binding: provided.binding.clone(), + resource_type, + }); + } + ResourceTypeResolution::Missing => { + errors.push(TypeError::UnknownProvidedResourceType { + item: item_name.to_string(), + binding: provided.binding.clone(), + resource_type, + }); + } } } } @@ -1271,12 +1326,16 @@ fn canonical_interface_name(name: &str) -> String { fn resolve_resource_type_name( resource_type: &str, registry: &ResourceTypeRegistry, -) -> Option { +) -> ResourceTypeResolution { if registry.full.contains(resource_type) { - return Some(resource_type.to_string()); + return ResourceTypeResolution::Resolved(resource_type.to_string()); } let short = resource_type.rsplit('.').next().unwrap_or(resource_type); - registry.short.get(short).and_then(|entry| entry.clone()) + match registry.short.get(short) { + Some(Some(resolved)) => ResourceTypeResolution::Resolved(resolved.clone()), + Some(None) => ResourceTypeResolution::Ambiguous, + None => ResourceTypeResolution::Missing, + } } fn resolve_service_call_contract( @@ -2102,6 +2161,42 @@ service FsStorage implements Storage { ))); } + #[test] + fn strict_mode_reports_ambiguous_used_resource_type() { + let graph = module_graph_from_sources(&[ + ( + "sample/one.dag", + "module sample.one\nresource SharedResource {}", + ), + ( + "sample/two.dag", + "module sample.two\nresource SharedResource {}", + ), + ( + "sample/main.dag", + r#"module sample.main +func run() -> { ok: Bool } uses fs: SharedResource { + return { ok: true } +}"#, + ), + ]); + let errors = typecheck_module_graph_with_options( + graph, + TypecheckOptions { + allow_unresolved_imports: false, + }, + ) + .expect_err("strict mode should fail for ambiguous used resource type"); + assert!(errors.iter().any(|error| matches!( + error, + TypeError::AmbiguousUsedResourceType { + item, + binding, + resource_type, + } if item == "run" && binding == "fs" && resource_type == "SharedResource" + ))); + } + #[test] fn relaxed_mode_allows_unknown_used_resource_type() { let graph = module_graph_from_sources(&[( @@ -2135,6 +2230,42 @@ service FsStorage implements Storage { ))); } + #[test] + fn strict_mode_reports_ambiguous_provided_resource_type() { + let graph = module_graph_from_sources(&[ + ( + "sample/one.dag", + "module sample.one\nresource SharedResource {}", + ), + ( + "sample/two.dag", + "module sample.two\nresource SharedResource {}", + ), + ( + "sample/main.dag", + r#"module sample.main +func run() -> { ok: Bool } provides out: SharedResource { + return { ok: true } +}"#, + ), + ]); + let errors = typecheck_module_graph_with_options( + graph, + TypecheckOptions { + allow_unresolved_imports: false, + }, + ) + .expect_err("strict mode should fail for ambiguous provided resource type"); + assert!(errors.iter().any(|error| matches!( + error, + TypeError::AmbiguousProvidedResourceType { + item, + binding, + resource_type, + } if item == "run" && binding == "out" && resource_type == "SharedResource" + ))); + } + #[test] fn duplicate_uses_binding_is_reported() { let graph = module_graph_from_sources(&[( From 3b059f3ba717f28ccce68dbd7e0a55c274bf2b10 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 08:07:18 +0000 Subject: [PATCH 047/169] Report ambiguous callable targets in strict mode Co-authored-by: Brian Searls --- .../daglang-cli/tests/compile_commands.rs | 38 +++++++++++++ core/daglang/daglang-typecheck/src/lib.rs | 53 ++++++++++++++++++- 2 files changed, 89 insertions(+), 2 deletions(-) diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index 210d82ddfc2..eaaee8f12e7 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -861,6 +861,44 @@ func run(path: String) -> { body: String } { std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); } +#[test] +fn compile_command_directory_mode_fails_on_ambiguous_callable_target() { + let root = unique_temp_dir("ambiguous_callable_target"); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp dir"); + std::fs::write( + root.join("sample/one.dag"), + "module sample.one\nfn render(value: String) -> String { value }", + ) + .expect("failed to write first callable source"); + std::fs::write( + root.join("sample/two.dag"), + "module sample.two\nfn render(value: String) -> String { value }", + ) + .expect("failed to write second callable source"); + std::fs::write( + root.join("sample/main.dag"), + "module sample.main\nfn run() -> String { render(value: \"ok\") }", + ) + .expect("failed to write main source"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&root) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile on directory"); + + assert!( + !output.status.success(), + "directory compile should fail on ambiguous callable target" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("typecheck errors")); + assert!(stderr.contains("ambiguous call target `render`")); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); +} + #[test] fn compile_command_directory_mode_reports_module_path_mismatch() { let root = unique_temp_dir("path_mismatch"); diff --git a/core/daglang/daglang-typecheck/src/lib.rs b/core/daglang/daglang-typecheck/src/lib.rs index d9d7e5b5e50..98e8e07184f 100644 --- a/core/daglang/daglang-typecheck/src/lib.rs +++ b/core/daglang/daglang-typecheck/src/lib.rs @@ -163,6 +163,11 @@ pub enum TypeError { callee: String, argument: String, }, + /// Call expression target resolves to multiple callable contracts. + AmbiguousCallTarget { + caller: String, + callee: String, + }, /// Service call expression used wrong number of arguments. ServiceCallArityMismatch { caller: String, @@ -308,6 +313,10 @@ impl std::fmt::Display for TypeError { f, "duplicate named argument `{argument}` in call to `{callee}` within `{caller}`" ), + Self::AmbiguousCallTarget { caller, callee } => write!( + f, + "ambiguous call target `{callee}` in `{caller}`" + ), Self::ServiceCallArityMismatch { caller, service_call, @@ -1055,8 +1064,18 @@ fn validate_callable_body( let mut calls = Vec::new(); collect_calls_from_stmts(stmts, &mut calls); for call in calls { - let Some(Some(contract)) = callable_registry.get(&call.callee) else { - continue; + let contract = match callable_registry.get(&call.callee) { + Some(Some(contract)) => contract, + Some(None) => { + if !allow_unresolved_references { + errors.push(TypeError::AmbiguousCallTarget { + caller: caller.to_string(), + callee: call.callee.clone(), + }); + } + continue; + } + None => continue, }; if call.arg_count != contract.arity { errors.push(TypeError::CallArityMismatch { @@ -1767,6 +1786,36 @@ mod tests { ))); } + #[test] + fn strict_mode_reports_ambiguous_call_target() { + let graph = module_graph_from_sources(&[ + ( + "sample/one.dag", + "module sample.one\nfn render(value: String) -> String { value }", + ), + ( + "sample/two.dag", + "module sample.two\nfn render(value: String) -> String { value }", + ), + ( + "sample/main.dag", + "module sample.main\nfn run() -> String { render(value: \"ok\") }", + ), + ]); + let errors = typecheck_module_graph_with_options( + graph, + TypecheckOptions { + allow_unresolved_imports: false, + }, + ) + .expect_err("strict mode should fail for ambiguous callable target"); + assert!(errors.iter().any(|error| matches!( + error, + TypeError::AmbiguousCallTarget { caller, callee } + if caller == "run" && callee == "render" + ))); + } + #[test] fn unknown_named_call_argument_is_reported() { let graph = module_graph_from_sources(&[( From da17675e038e60192d98d7b5ea389549e9bf01a7 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 08:09:28 +0000 Subject: [PATCH 048/169] Surface unresolved callable targets in strict mode Co-authored-by: Brian Searls --- .../daglang-cli/tests/compile_commands.rs | 28 +++++++++++ core/daglang/daglang-typecheck/src/lib.rs | 50 ++++++++++++++++++- 2 files changed, 77 insertions(+), 1 deletion(-) diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index eaaee8f12e7..c2ef0c1cc9b 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -899,6 +899,34 @@ fn compile_command_directory_mode_fails_on_ambiguous_callable_target() { std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); } +#[test] +fn compile_command_directory_mode_fails_on_unresolved_callable_target() { + let root = unique_temp_dir("unresolved_callable_target"); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp dir"); + std::fs::write( + root.join("sample/main.dag"), + "module sample.main\nfn run() -> String { missing(value: \"ok\") }", + ) + .expect("failed to write source"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&root) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile on directory"); + + assert!( + !output.status.success(), + "directory compile should fail on unresolved callable target" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("typecheck errors")); + assert!(stderr.contains("unresolved call target `missing`")); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); +} + #[test] fn compile_command_directory_mode_reports_module_path_mismatch() { let root = unique_temp_dir("path_mismatch"); diff --git a/core/daglang/daglang-typecheck/src/lib.rs b/core/daglang/daglang-typecheck/src/lib.rs index 98e8e07184f..b837ff21c77 100644 --- a/core/daglang/daglang-typecheck/src/lib.rs +++ b/core/daglang/daglang-typecheck/src/lib.rs @@ -168,6 +168,11 @@ pub enum TypeError { caller: String, callee: String, }, + /// Call expression target cannot be resolved to a callable contract. + UnresolvedCallTarget { + caller: String, + callee: String, + }, /// Service call expression used wrong number of arguments. ServiceCallArityMismatch { caller: String, @@ -317,6 +322,10 @@ impl std::fmt::Display for TypeError { f, "ambiguous call target `{callee}` in `{caller}`" ), + Self::UnresolvedCallTarget { caller, callee } => write!( + f, + "unresolved call target `{callee}` in `{caller}`" + ), Self::ServiceCallArityMismatch { caller, service_call, @@ -1075,7 +1084,15 @@ fn validate_callable_body( } continue; } - None => continue, + None => { + if !allow_unresolved_references { + errors.push(TypeError::UnresolvedCallTarget { + caller: caller.to_string(), + callee: call.callee.clone(), + }); + } + continue; + } }; if call.arg_count != contract.arity { errors.push(TypeError::CallArityMismatch { @@ -1816,6 +1833,37 @@ mod tests { ))); } + #[test] + fn strict_mode_reports_unresolved_call_target() { + let graph = module_graph_from_sources(&[( + "sample/main.dag", + "module sample.main\nfn run() -> String { missing(value: \"ok\") }", + )]); + let errors = typecheck_module_graph_with_options( + graph, + TypecheckOptions { + allow_unresolved_imports: false, + }, + ) + .expect_err("strict mode should fail for unresolved callable target"); + assert!(errors.iter().any(|error| matches!( + error, + TypeError::UnresolvedCallTarget { caller, callee } + if caller == "run" && callee == "missing" + ))); + } + + #[test] + fn relaxed_mode_allows_unresolved_call_target() { + let graph = module_graph_from_sources(&[( + "sample/main.dag", + "module sample.main\nfn run() -> String { missing(value: \"ok\") }", + )]); + let typed = typecheck_module_graph(graph) + .expect("relaxed mode should allow unresolved callable target"); + assert_eq!(typed.modules.len(), 1); + } + #[test] fn unknown_named_call_argument_is_reported() { let graph = module_graph_from_sources(&[( From 23f818eeacedbd3dff6cc7d95193878981c48ba3 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 08:12:39 +0000 Subject: [PATCH 049/169] Report ambiguous interface references during typecheck Co-authored-by: Brian Searls --- .../daglang-cli/tests/compile_commands.rs | 38 ++++++ core/daglang/daglang-typecheck/src/lib.rs | 129 +++++++++++++++--- 2 files changed, 150 insertions(+), 17 deletions(-) diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index c2ef0c1cc9b..323d65c9489 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -490,6 +490,44 @@ fn compile_command_directory_mode_fails_on_unresolved_imports() { std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); } +#[test] +fn compile_command_directory_mode_fails_on_ambiguous_interface_reference() { + let root = unique_temp_dir("ambiguous_interface_reference"); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp dir"); + std::fs::write( + root.join("sample/first.dag"), + "module sample.first\ninterface Storage { capability read { input { path: String } output { body: String } } }", + ) + .expect("failed to write source"); + std::fs::write( + root.join("sample/second.dag"), + "module sample.second\ninterface Storage { capability read { input { path: String } output { body: String } } }", + ) + .expect("failed to write source"); + std::fs::write( + root.join("sample/main.dag"), + "module sample.main\nservice FsStorage implements Storage { operation read(path: String) -> { body: String } }", + ) + .expect("failed to write source"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&root) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile on directory"); + + assert!( + !output.status.success(), + "directory compile should fail on ambiguous interface reference" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("typecheck errors")); + assert!(stderr.contains("ambiguous interface `Storage`")); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); +} + #[test] fn compile_command_directory_mode_fails_on_unknown_uses_resource_type() { let root = unique_temp_dir("unknown_uses_type"); diff --git a/core/daglang/daglang-typecheck/src/lib.rs b/core/daglang/daglang-typecheck/src/lib.rs index b837ff21c77..fb620345e8e 100644 --- a/core/daglang/daglang-typecheck/src/lib.rs +++ b/core/daglang/daglang-typecheck/src/lib.rs @@ -131,6 +131,11 @@ pub enum TypeError { implementor: String, interface: String, }, + /// Resource/service declares an interface that resolves ambiguously. + AmbiguousInterface { + implementor: String, + interface: String, + }, /// Service omits an operation required by its interface. MissingOperation { service: String, @@ -276,6 +281,13 @@ impl std::fmt::Display for TypeError { f, "`{implementor}` references unresolved interface `{interface}`" ), + Self::AmbiguousInterface { + implementor, + interface, + } => write!( + f, + "`{implementor}` references ambiguous interface `{interface}`" + ), Self::MissingOperation { service, interface, @@ -716,6 +728,13 @@ enum ServiceCallResolution { Missing, } +#[derive(Debug, Clone)] +enum InterfaceResolution { + Resolved(InterfaceContract), + Ambiguous, + Missing, +} + #[derive(Debug, Clone, PartialEq, Eq)] enum ResourceTypeResolution { Resolved(String), @@ -1184,13 +1203,22 @@ fn validate_resource_interface_conformance( let Some(implemented) = resource.implements.as_deref() else { return; }; - let Some(interface_contract) = resolve_interface_contract(implemented, interface_registry) - else { - errors.push(TypeError::UnresolvedInterface { - implementor: resource.name.clone(), - interface: implemented.to_string(), - }); - return; + let interface_contract = match resolve_interface_contract(implemented, interface_registry) { + InterfaceResolution::Resolved(contract) => contract, + InterfaceResolution::Ambiguous => { + errors.push(TypeError::AmbiguousInterface { + implementor: resource.name.clone(), + interface: implemented.to_string(), + }); + return; + } + InterfaceResolution::Missing => { + errors.push(TypeError::UnresolvedInterface { + implementor: resource.name.clone(), + interface: implemented.to_string(), + }); + return; + } }; let provided_capabilities = resource .capabilities @@ -1294,13 +1322,22 @@ fn validate_service_interface_conformance( let Some(implemented) = service.implements.as_deref() else { return; }; - let Some(interface_contract) = resolve_interface_contract(implemented, interface_registry) - else { - errors.push(TypeError::UnresolvedInterface { - implementor: service.name.clone(), - interface: implemented.to_string(), - }); - return; + let interface_contract = match resolve_interface_contract(implemented, interface_registry) { + InterfaceResolution::Resolved(contract) => contract, + InterfaceResolution::Ambiguous => { + errors.push(TypeError::AmbiguousInterface { + implementor: service.name.clone(), + interface: implemented.to_string(), + }); + return; + } + InterfaceResolution::Missing => { + errors.push(TypeError::UnresolvedInterface { + implementor: service.name.clone(), + interface: implemented.to_string(), + }); + return; + } }; let provided_operations = service .operations @@ -1346,13 +1383,17 @@ fn field_signature_map(fields: &[Field]) -> HashMap { fn resolve_interface_contract( implemented: &str, registry: &InterfaceRegistry, -) -> Option { +) -> InterfaceResolution { let canonical = canonical_type_name(implemented); if let Some(contract) = registry.full.get(&canonical) { - return Some(contract.clone()); + return InterfaceResolution::Resolved(contract.clone()); } let short = canonical.rsplit('.').next().unwrap_or(canonical.as_str()); - registry.short.get(short).and_then(|entry| entry.clone()) + match registry.short.get(short) { + Some(Some(contract)) => InterfaceResolution::Resolved(contract.clone()), + Some(None) => InterfaceResolution::Ambiguous, + None => InterfaceResolution::Missing, + } } fn canonical_interface_name(name: &str) -> String { @@ -2128,6 +2169,33 @@ resource Disk implements ObjectStorage { ))); } + #[test] + fn ambiguous_interface_on_resource_is_reported() { + let graph = module_graph_from_sources(&[ + ( + "sample/first.dag", + "module sample.first\ninterface Storage { capability read { input { path: String } output { body: String } } }", + ), + ( + "sample/second.dag", + "module sample.second\ninterface Storage { capability read { input { path: String } output { body: String } } }", + ), + ( + "sample/main.dag", + "module sample.main\nresource Disk implements Storage { capability read { input { path: String } output { body: String } } }", + ), + ]); + let errors = + typecheck_module_graph(graph).expect_err("ambiguous interface should fail"); + assert!(errors.iter().any(|error| matches!( + error, + TypeError::AmbiguousInterface { + implementor, + interface + } if implementor == "Disk" && interface == "Storage" + ))); + } + #[test] fn service_missing_interface_operation_is_reported() { let graph = module_graph_from_sources(&[( @@ -2172,6 +2240,33 @@ service FsStorage implements Storage { ))); } + #[test] + fn ambiguous_interface_on_service_is_reported() { + let graph = module_graph_from_sources(&[ + ( + "sample/first.dag", + "module sample.first\ninterface Storage { capability read { input { path: String } output { body: String } } }", + ), + ( + "sample/second.dag", + "module sample.second\ninterface Storage { capability read { input { path: String } output { body: String } } }", + ), + ( + "sample/main.dag", + "module sample.main\nservice FsStorage implements Storage { operation read(path: String) -> { body: String } }", + ), + ]); + let errors = + typecheck_module_graph(graph).expect_err("ambiguous interface should fail"); + assert!(errors.iter().any(|error| matches!( + error, + TypeError::AmbiguousInterface { + implementor, + interface + } if implementor == "FsStorage" && interface == "Storage" + ))); + } + #[test] fn resource_capability_signature_mismatch_is_reported() { let graph = module_graph_from_sources(&[( From 604bb43192ecc44464241357c1ce727cc8c71caf Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 08:14:41 +0000 Subject: [PATCH 050/169] Add duplicate-definition diagnostic regressions for compile path Co-authored-by: Brian Searls --- .../daglang-cli/tests/compile_commands.rs | 33 +++++++++++++++++++ core/daglang/daglang-typecheck/src/lib.rs | 19 +++++++++++ 2 files changed, 52 insertions(+) diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index 323d65c9489..59b98615f8f 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -490,6 +490,39 @@ fn compile_command_directory_mode_fails_on_unresolved_imports() { std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); } +#[test] +fn compile_command_directory_mode_fails_on_duplicate_definitions() { + let root = unique_temp_dir("duplicate_definitions"); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp dir"); + std::fs::write( + root.join("sample/main.dag"), + r#"module sample.main +fn run() -> Unit {} +func run() -> { ok: Bool } { + return { ok: true } +} +"#, + ) + .expect("failed to write source"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&root) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile on directory"); + + assert!( + !output.status.success(), + "directory compile should fail on duplicate definitions" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("typecheck errors")); + assert!(stderr.contains("duplicate definition `run` in module `sample.main`")); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); +} + #[test] fn compile_command_directory_mode_fails_on_ambiguous_interface_reference() { let root = unique_temp_dir("ambiguous_interface_reference"); diff --git a/core/daglang/daglang-typecheck/src/lib.rs b/core/daglang/daglang-typecheck/src/lib.rs index fb620345e8e..72a8c0e0093 100644 --- a/core/daglang/daglang-typecheck/src/lib.rs +++ b/core/daglang/daglang-typecheck/src/lib.rs @@ -1808,6 +1808,25 @@ mod tests { .any(|error| matches!(error, TypeError::UndefinedType(msg) if msg.contains("MissingType")))); } + #[test] + fn duplicate_definition_is_reported() { + let graph = module_graph_from_sources(&[( + "duplicate_definition.dag", + r#"module sample.dup +fn run() -> Unit {} +func run() -> { ok: Bool } { + return { ok: true } +} +"#, + )]); + let errors = typecheck_module_graph(graph).expect_err("duplicate item name should fail"); + assert!(errors.iter().any(|error| matches!( + error, + TypeError::DuplicateDefinition { module, name } + if module == "sample.dup" && name == "run" + ))); + } + #[test] fn strict_mode_reports_unresolved_imports() { let graph = module_graph_from_sources(&[( From 6a0ed53c197d2ed2d054c678ff11e751f35eba09 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 08:16:45 +0000 Subject: [PATCH 051/169] Add duplicate output-field diagnostics regressions Co-authored-by: Brian Searls --- .../daglang-cli/tests/compile_commands.rs | 32 +++++++++++++++++++ core/daglang/daglang-typecheck/src/lib.rs | 19 +++++++++++ 2 files changed, 51 insertions(+) diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index 59b98615f8f..bbee1fb0b90 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -523,6 +523,38 @@ func run() -> { ok: Bool } { std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); } +#[test] +fn compile_command_directory_mode_fails_on_duplicate_output_fields() { + let root = unique_temp_dir("duplicate_output_fields"); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp dir"); + std::fs::write( + root.join("sample/main.dag"), + r#"module sample.main +func run() -> { ok: Bool, ok: Bool } { + return { ok: true } +} +"#, + ) + .expect("failed to write source"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&root) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile on directory"); + + assert!( + !output.status.success(), + "directory compile should fail on duplicate output fields" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("typecheck errors")); + assert!(stderr.contains("duplicate output field `ok` in `run`")); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); +} + #[test] fn compile_command_directory_mode_fails_on_ambiguous_interface_reference() { let root = unique_temp_dir("ambiguous_interface_reference"); diff --git a/core/daglang/daglang-typecheck/src/lib.rs b/core/daglang/daglang-typecheck/src/lib.rs index 72a8c0e0093..8df49bf467c 100644 --- a/core/daglang/daglang-typecheck/src/lib.rs +++ b/core/daglang/daglang-typecheck/src/lib.rs @@ -1796,6 +1796,25 @@ mod tests { .any(|error| matches!(error, TypeError::DuplicateParameter { item, param } if item == "bad" && param == "a"))); } + #[test] + fn duplicate_output_fields_are_reported() { + let graph = module_graph_from_sources(&[( + "dup_outputs.dag", + r#"module sample.dup +func run() -> { ok: Bool, ok: Bool } { + return { ok: true } +} +"#, + )]); + let errors = + typecheck_module_graph(graph).expect_err("duplicate outputs should fail"); + assert!(errors.iter().any(|error| matches!( + error, + TypeError::DuplicateOutputField { item, field } + if item == "run" && field == "ok" + ))); + } + #[test] fn undefined_types_are_reported() { let graph = module_graph_from_sources(&[( From d473602376a55204f8e288b645dd5366cbd5c006 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 08:18:27 +0000 Subject: [PATCH 052/169] Add duplicate-parameter compile regression Co-authored-by: Brian Searls --- .../daglang-cli/tests/compile_commands.rs | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index bbee1fb0b90..35969f71e17 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -555,6 +555,34 @@ func run() -> { ok: Bool, ok: Bool } { std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); } +#[test] +fn compile_command_directory_mode_fails_on_duplicate_parameters() { + let root = unique_temp_dir("duplicate_parameters"); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp dir"); + std::fs::write( + root.join("sample/main.dag"), + "module sample.main\nfn run(a: String, a: Int) -> String { a }", + ) + .expect("failed to write source"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&root) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile on directory"); + + assert!( + !output.status.success(), + "directory compile should fail on duplicate parameters" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("typecheck errors")); + assert!(stderr.contains("duplicate parameter `a` in `run`")); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); +} + #[test] fn compile_command_directory_mode_fails_on_ambiguous_interface_reference() { let root = unique_temp_dir("ambiguous_interface_reference"); From eaa05ae1d3c45b3bfb1609809c9729a7c5676237 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 08:20:15 +0000 Subject: [PATCH 053/169] Add unresolved-interface compile regression Co-authored-by: Brian Searls --- .../daglang-cli/tests/compile_commands.rs | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index 35969f71e17..6f4bd1f068d 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -621,6 +621,34 @@ fn compile_command_directory_mode_fails_on_ambiguous_interface_reference() { std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); } +#[test] +fn compile_command_directory_mode_fails_on_unresolved_interface_reference() { + let root = unique_temp_dir("unresolved_interface_reference"); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp dir"); + std::fs::write( + root.join("sample/main.dag"), + "module sample.main\nservice FsStorage implements MissingStorage { operation read(path: String) -> { body: String } }", + ) + .expect("failed to write source"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&root) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile on directory"); + + assert!( + !output.status.success(), + "directory compile should fail on unresolved interface reference" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("typecheck errors")); + assert!(stderr.contains("references unresolved interface `MissingStorage`")); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); +} + #[test] fn compile_command_directory_mode_fails_on_unknown_uses_resource_type() { let root = unique_temp_dir("unknown_uses_type"); From 061f036f8b4c0cc788acd9d94e9c03a645458558 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 08:22:08 +0000 Subject: [PATCH 054/169] Add unresolved resource-interface compile regression Co-authored-by: Brian Searls --- .../daglang-cli/tests/compile_commands.rs | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index 6f4bd1f068d..fef36fab519 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -649,6 +649,34 @@ fn compile_command_directory_mode_fails_on_unresolved_interface_reference() { std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); } +#[test] +fn compile_command_directory_mode_fails_on_unresolved_resource_interface_reference() { + let root = unique_temp_dir("unresolved_resource_interface_reference"); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp dir"); + std::fs::write( + root.join("sample/main.dag"), + "module sample.main\nresource Disk implements MissingStorage { capability read { input { path: String } output { body: String } } }", + ) + .expect("failed to write source"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&root) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile on directory"); + + assert!( + !output.status.success(), + "directory compile should fail on unresolved resource interface reference" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("typecheck errors")); + assert!(stderr.contains("`Disk` references unresolved interface `MissingStorage`")); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); +} + #[test] fn compile_command_directory_mode_fails_on_unknown_uses_resource_type() { let root = unique_temp_dir("unknown_uses_type"); From 2c2e905ecdd18df4ff65124e1cf9e62fc28b0a79 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 08:26:09 +0000 Subject: [PATCH 055/169] Add ambiguous resource-interface compile regression Co-authored-by: Brian Searls --- .../daglang-cli/tests/compile_commands.rs | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index fef36fab519..2706c6c14f0 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -621,6 +621,44 @@ fn compile_command_directory_mode_fails_on_ambiguous_interface_reference() { std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); } +#[test] +fn compile_command_directory_mode_fails_on_ambiguous_resource_interface_reference() { + let root = unique_temp_dir("ambiguous_resource_interface_reference"); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp dir"); + std::fs::write( + root.join("sample/first.dag"), + "module sample.first\ninterface Storage { capability read { input { path: String } output { body: String } } }", + ) + .expect("failed to write source"); + std::fs::write( + root.join("sample/second.dag"), + "module sample.second\ninterface Storage { capability read { input { path: String } output { body: String } } }", + ) + .expect("failed to write source"); + std::fs::write( + root.join("sample/main.dag"), + "module sample.main\nresource Disk implements Storage { capability read { input { path: String } output { body: String } } }", + ) + .expect("failed to write source"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&root) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile on directory"); + + assert!( + !output.status.success(), + "directory compile should fail on ambiguous resource interface reference" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("typecheck errors")); + assert!(stderr.contains("`Disk` references ambiguous interface `Storage`")); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); +} + #[test] fn compile_command_directory_mode_fails_on_unresolved_interface_reference() { let root = unique_temp_dir("unresolved_interface_reference"); From 2c7a5eb8c47500bae5bea7ced7a55098bbb1c56d Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 08:28:39 +0000 Subject: [PATCH 056/169] Add callable argument compile regressions Co-authored-by: Brian Searls --- .../daglang-cli/tests/compile_commands.rs | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index 2706c6c14f0..da40bb6934d 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -847,6 +847,68 @@ fn compile_command_directory_mode_fails_on_ambiguous_provides_resource_type() { std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); } +#[test] +fn compile_command_reports_call_arity_typecheck_error() { + let fixture = unique_temp_file("call_arity"); + std::fs::write( + &fixture, + r#"module sample.calls +fn fmt(value: String) -> String { value } +fn run() -> String { fmt() } +"#, + ) + .expect("failed to write fixture"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&fixture) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile"); + + assert!( + !output.status.success(), + "compile should fail on call arity mismatch" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("typecheck errors")); + assert!(stderr.contains("call arity mismatch")); + assert!(stderr.contains("fmt")); + + std::fs::remove_file(fixture).expect("failed to cleanup fixture"); +} + +#[test] +fn compile_command_reports_unknown_named_call_argument_typecheck_error() { + let fixture = unique_temp_file("call_unknown_arg"); + std::fs::write( + &fixture, + r#"module sample.calls +fn fmt(value: String) -> String { value } +fn run() -> String { fmt(text: "ok") } +"#, + ) + .expect("failed to write fixture"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&fixture) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile"); + + assert!( + !output.status.success(), + "compile should fail on unknown named call argument" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("typecheck errors")); + assert!(stderr.contains("unknown named argument `text`")); + assert!(stderr.contains("call to `fmt`")); + + std::fs::remove_file(fixture).expect("failed to cleanup fixture"); +} + #[test] fn compile_command_reports_service_call_arity_typecheck_error() { let fixture = unique_temp_file("service_call_arity"); From 910e0fce58c47877f6769aecbb9292e624138cca Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 08:30:58 +0000 Subject: [PATCH 057/169] Add undefined-type compile regression Co-authored-by: Brian Searls --- .../daglang-cli/tests/compile_commands.rs | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index da40bb6934d..6cac03c81dd 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -909,6 +909,35 @@ fn run() -> String { fmt(text: "ok") } std::fs::remove_file(fixture).expect("failed to cleanup fixture"); } +#[test] +fn compile_command_reports_undefined_type_typecheck_error() { + let fixture = unique_temp_file("undefined_type"); + std::fs::write( + &fixture, + r#"module sample.types +fn run(input: MissingType) -> String { "ok" } +"#, + ) + .expect("failed to write fixture"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&fixture) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile"); + + assert!( + !output.status.success(), + "compile should fail on undefined type" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("typecheck errors")); + assert!(stderr.contains("undefined type `MissingType")); + + std::fs::remove_file(fixture).expect("failed to cleanup fixture"); +} + #[test] fn compile_command_reports_service_call_arity_typecheck_error() { let fixture = unique_temp_file("service_call_arity"); From ddfc362d3e45aed5dc64e239cc076d4bcfcfa101 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 08:33:03 +0000 Subject: [PATCH 058/169] Add use/provide binding conflict compile regression Co-authored-by: Brian Searls --- .../daglang-cli/tests/compile_commands.rs | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index 6cac03c81dd..9ced5fb351c 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -715,6 +715,44 @@ fn compile_command_directory_mode_fails_on_unresolved_resource_interface_referen std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); } +#[test] +fn compile_command_directory_mode_fails_on_use_provide_binding_conflict() { + let root = unique_temp_dir("use_provide_binding_conflict"); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp dir"); + std::fs::write( + root.join("sample/main.dag"), + r#"module sample.main +interface Storage { + capability read { + input { path: String } + output { body: String } + } +} +func run() -> { ok: Bool } uses io: Storage provides io: Storage { + return { ok: true } +} +"#, + ) + .expect("failed to write source"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&root) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile on directory"); + + assert!( + !output.status.success(), + "directory compile should fail on use/provide binding conflict" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("typecheck errors")); + assert!(stderr.contains("binding `io` is declared in both uses/provides in `run`")); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); +} + #[test] fn compile_command_directory_mode_fails_on_unknown_uses_resource_type() { let root = unique_temp_dir("unknown_uses_type"); From 39d517e3f5c823b34342a2cac613aef91b43b09a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 08:35:29 +0000 Subject: [PATCH 059/169] Add duplicate uses/provides binding compile regressions Co-authored-by: Brian Searls --- .../daglang-cli/tests/compile_commands.rs | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index 9ced5fb351c..6346ef6b98b 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -781,6 +781,44 @@ fn compile_command_directory_mode_fails_on_unknown_uses_resource_type() { std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); } +#[test] +fn compile_command_directory_mode_fails_on_duplicate_uses_binding() { + let root = unique_temp_dir("duplicate_uses_binding"); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp dir"); + std::fs::write( + root.join("sample/main.dag"), + r#"module sample.main +interface Storage { + capability read { + input { path: String } + output { body: String } + } +} +func run() -> { ok: Bool } uses fs: Storage uses fs: Storage { + return { ok: true } +} +"#, + ) + .expect("failed to write source"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&root) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile on directory"); + + assert!( + !output.status.success(), + "directory compile should fail on duplicate uses binding" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("typecheck errors")); + assert!(stderr.contains("duplicate uses binding `fs` in `run`")); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); +} + #[test] fn compile_command_directory_mode_fails_on_ambiguous_uses_resource_type() { let root = unique_temp_dir("ambiguous_uses_type"); @@ -819,6 +857,44 @@ fn compile_command_directory_mode_fails_on_ambiguous_uses_resource_type() { std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); } +#[test] +fn compile_command_directory_mode_fails_on_duplicate_provides_binding() { + let root = unique_temp_dir("duplicate_provides_binding"); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp dir"); + std::fs::write( + root.join("sample/main.dag"), + r#"module sample.main +interface Storage { + capability read { + input { path: String } + output { body: String } + } +} +func run() -> { ok: Bool } provides out: Storage provides out: Storage { + return { ok: true } +} +"#, + ) + .expect("failed to write source"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&root) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile on directory"); + + assert!( + !output.status.success(), + "directory compile should fail on duplicate provides binding" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("typecheck errors")); + assert!(stderr.contains("duplicate provides binding `out` in `run`")); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); +} + #[test] fn compile_command_directory_mode_fails_on_unknown_provides_resource_type() { let root = unique_temp_dir("unknown_provides_type"); From 3bcc7a8e0ea508b47917559dfa4f7fc2e5a9d0f1 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 08:38:23 +0000 Subject: [PATCH 060/169] Add interface conformance compile regressions Co-authored-by: Brian Searls --- .../daglang-cli/tests/compile_commands.rs | 125 ++++++++++++++++++ 1 file changed, 125 insertions(+) diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index 6346ef6b98b..058c35df707 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -715,6 +715,131 @@ fn compile_command_directory_mode_fails_on_unresolved_resource_interface_referen std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); } +#[test] +fn compile_command_directory_mode_fails_on_missing_resource_capability() { + let root = unique_temp_dir("missing_resource_capability"); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp dir"); + std::fs::write( + root.join("sample/main.dag"), + r#"module sample.main +interface Storage { + capability read { + input { path: String } + output { body: String } + } + capability write { + input { path: String, body: String } + output { ok: Bool } + } +} +resource Disk implements Storage { + capability read { + input { path: String } + output { body: String } + } +} +"#, + ) + .expect("failed to write source"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&root) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile on directory"); + + assert!( + !output.status.success(), + "directory compile should fail when resource is missing interface capability" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("typecheck errors")); + assert!(stderr.contains("resource `Disk` is missing capability `write` for interface `Storage`")); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); +} + +#[test] +fn compile_command_directory_mode_fails_on_missing_service_operation() { + let root = unique_temp_dir("missing_service_operation"); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp dir"); + std::fs::write( + root.join("sample/main.dag"), + r#"module sample.main +interface Storage { + capability read { + input { path: String } + output { body: String } + } + capability write { + input { path: String, body: String } + output { ok: Bool } + } +} +service FsStorage implements Storage { + operation read(path: String) -> { body: String } +} +"#, + ) + .expect("failed to write source"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&root) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile on directory"); + + assert!( + !output.status.success(), + "directory compile should fail when service is missing interface operation" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("typecheck errors")); + assert!(stderr.contains("service `FsStorage` is missing operation `write` for interface `Storage`")); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); +} + +#[test] +fn compile_command_directory_mode_fails_on_interface_signature_mismatch() { + let root = unique_temp_dir("interface_signature_mismatch"); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp dir"); + std::fs::write( + root.join("sample/main.dag"), + r#"module sample.main +interface Storage { + capability read { + input { path: String } + output { body: String } + } +} +service FsStorage implements Storage { + operation read(path: Int) -> { body: String } +} +"#, + ) + .expect("failed to write source"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&root) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile on directory"); + + assert!( + !output.status.success(), + "directory compile should fail on interface signature mismatch" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("typecheck errors")); + assert!(stderr.contains("`FsStorage` does not match `Storage.read` contract")); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); +} + #[test] fn compile_command_directory_mode_fails_on_use_provide_binding_conflict() { let root = unique_temp_dir("use_provide_binding_conflict"); From 1b0498d10e5325daa5aeb27d18e8f76d11da1829 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 08:40:10 +0000 Subject: [PATCH 061/169] Add resource signature mismatch compile regression Co-authored-by: Brian Searls --- .../daglang-cli/tests/compile_commands.rs | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index 058c35df707..51442b2e081 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -840,6 +840,47 @@ service FsStorage implements Storage { std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); } +#[test] +fn compile_command_directory_mode_fails_on_resource_interface_signature_mismatch() { + let root = unique_temp_dir("resource_interface_signature_mismatch"); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp dir"); + std::fs::write( + root.join("sample/main.dag"), + r#"module sample.main +interface Storage { + capability read { + input { path: String } + output { body: String } + } +} +resource Disk implements Storage { + capability read { + input { path: Int } + output { body: String } + } +} +"#, + ) + .expect("failed to write source"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&root) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile on directory"); + + assert!( + !output.status.success(), + "directory compile should fail on resource interface signature mismatch" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("typecheck errors")); + assert!(stderr.contains("`Disk` does not match `Storage.read` contract")); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); +} + #[test] fn compile_command_directory_mode_fails_on_use_provide_binding_conflict() { let root = unique_temp_dir("use_provide_binding_conflict"); From 618f471758f24dbe463e179b802f1f1514cd0567 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 08:45:07 +0000 Subject: [PATCH 062/169] Add compile regressions for unresolved uses/provides Co-authored-by: Brian Searls --- .../daglang-cli/tests/compile_commands.rs | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index 51442b2e081..85c1502f6ab 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -98,6 +98,68 @@ func run(path: String) -> { body: String } { std::fs::remove_file(fixture).expect("failed to cleanup fixture"); } +#[test] +fn compile_command_single_file_unresolved_uses_reports_lower_error() { + let fixture = unique_temp_file("compile_unresolved_uses_single_file"); + std::fs::write( + &fixture, + r#"module sample.uses +func run() -> { ok: Bool } uses fs: MissingResource { + return { ok: true } +} +"#, + ) + .expect("failed to write fixture"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&fixture) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile for unresolved uses fixture"); + + assert!( + !output.status.success(), + "single-file compile should fail for unresolved uses binding" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("lower error: unresolved used resource")); + assert!(stderr.contains("fs: MissingResource")); + + std::fs::remove_file(fixture).expect("failed to cleanup fixture"); +} + +#[test] +fn compile_command_single_file_unresolved_provides_reports_lower_error() { + let fixture = unique_temp_file("compile_unresolved_provides_single_file"); + std::fs::write( + &fixture, + r#"module sample.provides +func run() -> { ok: Bool } provides out: MissingResource { + return { ok: true } +} +"#, + ) + .expect("failed to write fixture"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&fixture) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile for unresolved provides fixture"); + + assert!( + !output.status.success(), + "single-file compile should fail for unresolved provides binding" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("lower error: unresolved provided resource")); + assert!(stderr.contains("out: MissingResource")); + + std::fs::remove_file(fixture).expect("failed to cleanup fixture"); +} + #[test] fn expand_command_shows_lowered_nodes_and_edges() { let output = Command::new(daglang_bin()) From df69287418dbed11a352bb7bfcf1ee09ec7e84e8 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 08:47:27 +0000 Subject: [PATCH 063/169] Add single-file unresolved-import compile regression Co-authored-by: Brian Searls --- .../daglang-cli/tests/compile_commands.rs | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index 85c1502f6ab..3e64a877efe 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -160,6 +160,36 @@ func run() -> { ok: Bool } provides out: MissingResource { std::fs::remove_file(fixture).expect("failed to cleanup fixture"); } +#[test] +fn compile_command_single_file_allows_unresolved_imports() { + let fixture = unique_temp_file("compile_single_file_unresolved_import"); + std::fs::write( + &fixture, + r#"module sample.single +import missing.dep +fn run() -> Unit {} +"#, + ) + .expect("failed to write fixture"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&fixture) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile for unresolved import fixture"); + + assert!( + output.status.success(), + "single-file compile should tolerate unresolved imports: {}", + String::from_utf8_lossy(&output.stderr) + ); + let stdout = String::from_utf8_lossy(&output.stdout); + assert!(stdout.contains("Compiled 1 module(s)")); + + std::fs::remove_file(fixture).expect("failed to cleanup fixture"); +} + #[test] fn expand_command_shows_lowered_nodes_and_edges() { let output = Command::new(daglang_bin()) From 7bdf7444317191f9e39166d9bde8fe65dfe26ece Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 08:49:30 +0000 Subject: [PATCH 064/169] Add single-file unresolved call-target compile regression Co-authored-by: Brian Searls --- .../daglang-cli/tests/compile_commands.rs | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index 3e64a877efe..2ea07123440 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -190,6 +190,35 @@ fn run() -> Unit {} std::fs::remove_file(fixture).expect("failed to cleanup fixture"); } +#[test] +fn compile_command_single_file_allows_unresolved_call_targets() { + let fixture = unique_temp_file("compile_single_file_unresolved_call_target"); + std::fs::write( + &fixture, + r#"module sample.calls +fn run() -> String { missing(value: "ok") } +"#, + ) + .expect("failed to write fixture"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&fixture) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile for unresolved call-target fixture"); + + assert!( + output.status.success(), + "single-file compile should tolerate unresolved call targets: {}", + String::from_utf8_lossy(&output.stderr) + ); + let stdout = String::from_utf8_lossy(&output.stdout); + assert!(stdout.contains("Compiled 1 module(s)")); + + std::fs::remove_file(fixture).expect("failed to cleanup fixture"); +} + #[test] fn expand_command_shows_lowered_nodes_and_edges() { let output = Command::new(daglang_bin()) From 6dbf5bb2cdfe650a07f5504802adc52728974046 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 08:51:51 +0000 Subject: [PATCH 065/169] Add single-file unresolved interface compile regressions Co-authored-by: Brian Searls --- .../daglang-cli/tests/compile_commands.rs | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index 2ea07123440..07294061aa3 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -219,6 +219,71 @@ fn run() -> String { missing(value: "ok") } std::fs::remove_file(fixture).expect("failed to cleanup fixture"); } +#[test] +fn compile_command_single_file_unresolved_service_interface_reports_typecheck_error() { + let fixture = unique_temp_file("compile_single_file_unresolved_service_interface"); + std::fs::write( + &fixture, + r#"module sample.services +service FsStorage implements MissingStorage { + operation read(path: String) -> { body: String } +} +"#, + ) + .expect("failed to write fixture"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&fixture) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile for unresolved service-interface fixture"); + + assert!( + !output.status.success(), + "single-file compile should fail on unresolved service interface" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("typecheck errors")); + assert!(stderr.contains("`FsStorage` references unresolved interface `MissingStorage`")); + + std::fs::remove_file(fixture).expect("failed to cleanup fixture"); +} + +#[test] +fn compile_command_single_file_unresolved_resource_interface_reports_typecheck_error() { + let fixture = unique_temp_file("compile_single_file_unresolved_resource_interface"); + std::fs::write( + &fixture, + r#"module sample.resources +resource Disk implements MissingStorage { + capability read { + input { path: String } + output { body: String } + } +} +"#, + ) + .expect("failed to write fixture"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&fixture) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile for unresolved resource-interface fixture"); + + assert!( + !output.status.success(), + "single-file compile should fail on unresolved resource interface" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("typecheck errors")); + assert!(stderr.contains("`Disk` references unresolved interface `MissingStorage`")); + + std::fs::remove_file(fixture).expect("failed to cleanup fixture"); +} + #[test] fn expand_command_shows_lowered_nodes_and_edges() { let output = Command::new(daglang_bin()) From 4e5a1522dcc91ffb4d9c20886e06b8f57845d66b Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 08:54:08 +0000 Subject: [PATCH 066/169] Add single-file duplicate signature compile regressions Co-authored-by: Brian Searls --- .../daglang-cli/tests/compile_commands.rs | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index 07294061aa3..c5ad546c09b 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -57,6 +57,65 @@ fn compile_command_emits_summary_for_single_file() { assert!(stdout.contains("target/generated/rust/main.rs")); } +#[test] +fn compile_command_single_file_fails_on_duplicate_definition() { + let fixture = unique_temp_file("compile_single_file_duplicate_definition"); + std::fs::write( + &fixture, + r#"module sample.single +fn run() -> String { "a" } +fn run() -> String { "b" } +"#, + ) + .expect("failed to write fixture"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&fixture) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile for duplicate-definition fixture"); + + assert!( + !output.status.success(), + "single-file compile should fail on duplicate definition" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("typecheck errors")); + assert!(stderr.contains("duplicate definition `run` in module `sample.single`")); + + std::fs::remove_file(fixture).expect("failed to cleanup fixture"); +} + +#[test] +fn compile_command_single_file_fails_on_duplicate_parameter() { + let fixture = unique_temp_file("compile_single_file_duplicate_parameter"); + std::fs::write( + &fixture, + r#"module sample.single +fn run(a: String, a: Int) -> String { a } +"#, + ) + .expect("failed to write fixture"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&fixture) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile for duplicate-parameter fixture"); + + assert!( + !output.status.success(), + "single-file compile should fail on duplicate parameter" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("typecheck errors")); + assert!(stderr.contains("duplicate parameter `a` in `run`")); + + std::fs::remove_file(fixture).expect("failed to cleanup fixture"); +} + #[test] fn compile_command_single_file_unresolved_service_call_reports_lower_error() { let fixture = unique_temp_file("compile_unresolved_service_single_file"); From 1f2164bbd2851323f72941b04047c7625d8440f8 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 08:56:40 +0000 Subject: [PATCH 067/169] Add single-file duplicate output compile regression Co-authored-by: Brian Searls --- .../daglang-cli/tests/compile_commands.rs | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index c5ad546c09b..f6acdcd1961 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -116,6 +116,35 @@ fn run(a: String, a: Int) -> String { a } std::fs::remove_file(fixture).expect("failed to cleanup fixture"); } +#[test] +fn compile_command_single_file_fails_on_duplicate_output_field() { + let fixture = unique_temp_file("compile_single_file_duplicate_output_field"); + std::fs::write( + &fixture, + r#"module sample.single +func run() -> { ok: Bool, ok: String } { return { ok: true } } +"#, + ) + .expect("failed to write fixture"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&fixture) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile for duplicate-output fixture"); + + assert!( + !output.status.success(), + "single-file compile should fail on duplicate output field" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("typecheck errors")); + assert!(stderr.contains("duplicate output field `ok` in `run`")); + + std::fs::remove_file(fixture).expect("failed to cleanup fixture"); +} + #[test] fn compile_command_single_file_unresolved_service_call_reports_lower_error() { let fixture = unique_temp_file("compile_unresolved_service_single_file"); From 1d2ea2861556d7ff269b669d138fbd544bc65ffd Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 09:01:16 +0000 Subject: [PATCH 068/169] Add single-file uses/provides binding compile regressions Co-authored-by: Brian Searls --- .../daglang-cli/tests/compile_commands.rs | 90 +++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index f6acdcd1961..461bbb9a984 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -145,6 +145,96 @@ func run() -> { ok: Bool, ok: String } { return { ok: true } } std::fs::remove_file(fixture).expect("failed to cleanup fixture"); } +#[test] +fn compile_command_single_file_fails_on_duplicate_uses_binding() { + let fixture = unique_temp_file("compile_single_file_duplicate_uses_binding"); + std::fs::write( + &fixture, + r#"module sample.single +interface Storage { capability read { input { path: String } output { body: String } } } +func run() -> { ok: Bool } uses fs: Storage uses fs: Storage { return { ok: true } } +"#, + ) + .expect("failed to write fixture"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&fixture) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile for duplicate-uses fixture"); + + assert!( + !output.status.success(), + "single-file compile should fail on duplicate uses binding" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("typecheck errors")); + assert!(stderr.contains("duplicate uses binding `fs` in `run`")); + + std::fs::remove_file(fixture).expect("failed to cleanup fixture"); +} + +#[test] +fn compile_command_single_file_fails_on_duplicate_provides_binding() { + let fixture = unique_temp_file("compile_single_file_duplicate_provides_binding"); + std::fs::write( + &fixture, + r#"module sample.single +interface Storage { capability read { input { path: String } output { body: String } } } +func run() -> { ok: Bool } provides out: Storage provides out: Storage { return { ok: true } } +"#, + ) + .expect("failed to write fixture"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&fixture) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile for duplicate-provides fixture"); + + assert!( + !output.status.success(), + "single-file compile should fail on duplicate provides binding" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("typecheck errors")); + assert!(stderr.contains("duplicate provides binding `out` in `run`")); + + std::fs::remove_file(fixture).expect("failed to cleanup fixture"); +} + +#[test] +fn compile_command_single_file_fails_on_use_provide_binding_conflict() { + let fixture = unique_temp_file("compile_single_file_use_provide_binding_conflict"); + std::fs::write( + &fixture, + r#"module sample.single +interface Storage { capability read { input { path: String } output { body: String } } } +func run() -> { ok: Bool } uses io: Storage provides io: Storage { return { ok: true } } +"#, + ) + .expect("failed to write fixture"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&fixture) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile for use/provide-conflict fixture"); + + assert!( + !output.status.success(), + "single-file compile should fail on use/provide binding conflict" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("typecheck errors")); + assert!(stderr.contains("binding `io` is declared in both uses/provides in `run`")); + + std::fs::remove_file(fixture).expect("failed to cleanup fixture"); +} + #[test] fn compile_command_single_file_unresolved_service_call_reports_lower_error() { let fixture = unique_temp_file("compile_unresolved_service_single_file"); From 76e853e079918ccd0dae4b716f7348f7ffb365c9 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 09:13:30 +0000 Subject: [PATCH 069/169] Implement missing typecheck diagnostics for bodies and types Co-authored-by: Brian Searls --- .../daglang-cli/tests/compile_commands.rs | 119 +++ core/daglang/daglang-typecheck/src/lib.rs | 709 +++++++++++++++++- 2 files changed, 806 insertions(+), 22 deletions(-) diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index 461bbb9a984..2d8a4a05839 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -1582,6 +1582,125 @@ fn run(input: MissingType) -> String { "ok" } std::fs::remove_file(fixture).expect("failed to cleanup fixture"); } +#[test] +fn compile_command_reports_type_mismatch_typecheck_error() { + let fixture = unique_temp_file("type_mismatch"); + std::fs::write( + &fixture, + r#"module sample.types +fn run() -> String { return 42 } +"#, + ) + .expect("failed to write fixture"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&fixture) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile"); + + assert!( + !output.status.success(), + "compile should fail on return type mismatch" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("typecheck errors")); + assert!(stderr.contains("type mismatch: expected `String`, got `Int`")); + + std::fs::remove_file(fixture).expect("failed to cleanup fixture"); +} + +#[test] +fn compile_command_reports_no_such_field_typecheck_error() { + let fixture = unique_temp_file("no_such_field"); + std::fs::write( + &fixture, + r#"module sample.types +func run() -> { body: String } { + let payload = { body: "ok" } + return { body: payload.missing } +} +"#, + ) + .expect("failed to write fixture"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&fixture) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile"); + + assert!( + !output.status.success(), + "compile should fail on missing record field access" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("typecheck errors")); + assert!(stderr.contains("type `Record` has no field `missing`")); + + std::fs::remove_file(fixture).expect("failed to cleanup fixture"); +} + +#[test] +fn compile_command_reports_unsatisfiable_refinement_typecheck_error() { + let fixture = unique_temp_file("unsatisfiable_refinement"); + std::fs::write( + &fixture, + r#"module sample.types +fn run(value: Int @range(min: 5, max: 1)) -> Int { value } +"#, + ) + .expect("failed to write fixture"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&fixture) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile"); + + assert!( + !output.status.success(), + "compile should fail on unsatisfiable refinement" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("typecheck errors")); + assert!(stderr.contains("unsatisfiable refinement on `Int`: range min 5 exceeds max 1")); + + std::fs::remove_file(fixture).expect("failed to cleanup fixture"); +} + +#[test] +fn compile_command_reports_generic_arity_mismatch_typecheck_error() { + let fixture = unique_temp_file("generic_arity_mismatch"); + std::fs::write( + &fixture, + r#"module sample.types +fn run(values: Map) -> Int { 1 } +"#, + ) + .expect("failed to write fixture"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&fixture) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile"); + + assert!( + !output.status.success(), + "compile should fail on generic arity mismatch" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("typecheck errors")); + assert!(stderr.contains("generic arity mismatch for `Map`: expected 2, got 1")); + + std::fs::remove_file(fixture).expect("failed to cleanup fixture"); +} + #[test] fn compile_command_reports_service_call_arity_typecheck_error() { let fixture = unique_temp_file("service_call_arity"); diff --git a/core/daglang/daglang-typecheck/src/lib.rs b/core/daglang/daglang-typecheck/src/lib.rs index 8df49bf467c..e114fec86c4 100644 --- a/core/daglang/daglang-typecheck/src/lib.rs +++ b/core/daglang/daglang-typecheck/src/lib.rs @@ -436,6 +436,8 @@ pub fn typecheck_module_graph_with_options( options: TypecheckOptions, ) -> Result> { let known_types = collect_known_types(&graph.modules); + let generic_arity_registry = collect_generic_arities(&graph.modules); + let record_type_registry = collect_record_types(&graph.modules); let callable_registry = collect_unique_callables(&graph.modules); let service_call_registry = collect_service_call_contracts(&graph.modules); let interface_registry = collect_interfaces(&graph.modules); @@ -449,6 +451,8 @@ pub fn typecheck_module_graph_with_options( let mut typed_modules = Vec::with_capacity(graph.modules.len()); let context = TypecheckContext { known_types: &known_types, + generic_arity_registry: &generic_arity_registry, + record_type_registry: &record_type_registry, callable_registry: &callable_registry, service_call_registry: &service_call_registry, interface_registry: &interface_registry, @@ -496,6 +500,8 @@ pub fn typecheck_module_graph_with_options( struct TypecheckContext<'a> { known_types: &'a HashSet, + generic_arity_registry: &'a GenericArityRegistry, + record_type_registry: &'a RecordTypeRegistry, callable_registry: &'a HashMap>, service_call_registry: &'a ServiceCallRegistry, interface_registry: &'a InterfaceRegistry, @@ -520,6 +526,11 @@ fn collect_signatures( let mut seen_items = HashSet::new(); let mut signatures = Vec::new(); + let body_context = BodyInferenceContext { + record_type_registry: context.record_type_registry, + callable_registry: context.callable_registry, + service_call_registry: context.service_call_registry, + }; for item in &module.ast.items { match &item.node { @@ -536,10 +547,17 @@ fn collect_signatures( } Item::FnDef(def) => { record_duplicate_item_name(module_name, &def.name, &mut seen_items, errors); - validate_params(&def.name, &def.params, &module_known_types, errors); + validate_params( + &def.name, + &def.params, + &module_known_types, + context.generic_arity_registry, + errors, + ); validate_type_expr( &def.return_type, &module_known_types, + context.generic_arity_registry, &format!("{}.return", def.name), errors, ); @@ -549,9 +567,10 @@ fn collect_signatures( }]; validate_callable_body( &def.name, + &def.params, + ReturnContract::single(type_expr_to_string(&def.return_type)), &def.body.stmts, - context.callable_registry, - context.service_call_registry, + &body_context, context.allow_unresolved_references, errors, ); @@ -570,8 +589,20 @@ fn collect_signatures( } Item::FuncDef(def) => { record_duplicate_item_name(module_name, &def.name, &mut seen_items, errors); - validate_params(&def.name, &def.params, &module_known_types, errors); - validate_outputs(&def.name, &def.outputs, &module_known_types, errors); + validate_params( + &def.name, + &def.params, + &module_known_types, + context.generic_arity_registry, + errors, + ); + validate_outputs( + &def.name, + &def.outputs, + &module_known_types, + context.generic_arity_registry, + errors, + ); validate_uses_clauses( &def.name, &def.uses, @@ -589,9 +620,10 @@ fn collect_signatures( validate_use_provide_binding_conflicts(&def.name, &def.uses, &def.provides, errors); validate_callable_body( &def.name, + &def.params, + ReturnContract::record(field_signature_map(&def.outputs)), &def.body.stmts, - context.callable_registry, - context.service_call_registry, + &body_context, context.allow_unresolved_references, errors, ); @@ -617,8 +649,20 @@ fn collect_signatures( } Item::PatternDef(def) => { record_duplicate_item_name(module_name, &def.name, &mut seen_items, errors); - validate_params(&def.name, &def.params, &module_known_types, errors); - validate_outputs(&def.name, &def.outputs, &module_known_types, errors); + validate_params( + &def.name, + &def.params, + &module_known_types, + context.generic_arity_registry, + errors, + ); + validate_outputs( + &def.name, + &def.outputs, + &module_known_types, + context.generic_arity_registry, + errors, + ); validate_uses_clauses( &def.name, &def.uses, @@ -628,9 +672,10 @@ fn collect_signatures( ); validate_callable_body( &def.name, + &def.params, + ReturnContract::record(field_signature_map(&def.outputs)), &def.body.stmts, - context.callable_registry, - context.service_call_registry, + &body_context, context.allow_unresolved_references, errors, ); @@ -704,16 +749,81 @@ fn collect_known_types(modules: &[ResolvedModule]) -> HashSet { known } +fn collect_generic_arities(modules: &[ResolvedModule]) -> GenericArityRegistry { + let mut registry = GenericArityRegistry::default(); + for (name, arity) in builtin_type_arities() { + registry.full.insert(name.clone(), arity); + registry.short.insert(name, Some(arity)); + } + + for module in modules { + let module_prefix = module.module_path.join("."); + for item in &module.ast.items { + let (name, arity) = match &item.node { + Item::TypeDef(def) => (&def.name, def.params.len()), + Item::InterfaceDef(def) => (&def.name, def.type_params.len()), + _ => continue, + }; + let full_name = format!("{module_prefix}.{name}"); + registry.full.insert(full_name.clone(), arity); + registry + .short + .entry(name.clone()) + .and_modify(|existing| { + if let Some(current) = existing { + if *current != arity { + *existing = None; + } + } + }) + .or_insert(Some(arity)); + } + } + registry +} + +fn collect_record_types(modules: &[ResolvedModule]) -> RecordTypeRegistry { + let mut registry = RecordTypeRegistry::default(); + for module in modules { + let module_prefix = module.module_path.join("."); + for item in &module.ast.items { + let Item::TypeDef(def) = &item.node else { + continue; + }; + let daglang_syntax::ast::TypeBody::Record(fields) = &def.body else { + continue; + }; + let signature = field_signature_map(fields); + let full_name = format!("{module_prefix}.{}", def.name); + registry.full.insert(full_name.clone(), signature); + registry + .short + .entry(def.name.clone()) + .and_modify(|existing| { + if let Some(current) = existing { + if current != &full_name { + *existing = None; + } + } + }) + .or_insert(Some(full_name)); + } + } + registry +} + #[derive(Debug, Clone, PartialEq, Eq)] struct CallableContract { arity: usize, params: HashSet, + output: ValueType, } #[derive(Debug, Clone, PartialEq, Eq)] struct ServiceCallContract { arity: usize, params: HashSet, + outputs: HashMap, } #[derive(Debug, Clone, Default)] @@ -748,15 +858,22 @@ fn collect_unique_callables( let mut callables = HashMap::>::new(); for module in modules { for item in &module.ast.items { - let (name, params) = match &item.node { - Item::FnDef(def) => (&def.name, &def.params), - Item::FuncDef(def) => (&def.name, &def.params), - Item::PatternDef(def) => (&def.name, &def.params), + let (name, params, output) = match &item.node { + Item::FnDef(def) => ( + &def.name, + &def.params, + ValueType::Named(type_expr_to_string(&def.return_type)), + ), + Item::FuncDef(def) => (&def.name, &def.params, ValueType::Record(field_signature_map(&def.outputs))), + Item::PatternDef(def) => { + (&def.name, &def.params, ValueType::Record(field_signature_map(&def.outputs))) + } _ => continue, }; let contract = CallableContract { arity: params.len(), params: params.iter().map(|param| param.name.clone()).collect(), + output, }; callables .entry(name.clone()) @@ -787,6 +904,7 @@ fn collect_service_call_contracts(modules: &[ResolvedModule]) -> ServiceCallRegi .iter() .map(|field| field.name.clone()) .collect(), + outputs: field_signature_map(&operation.outputs), }; let service_tail = service .name @@ -830,6 +948,48 @@ struct ResourceTypeRegistry { short: HashMap>, } +#[derive(Debug, Clone, Default)] +struct GenericArityRegistry { + full: HashMap, + short: HashMap>, +} + +#[derive(Debug, Clone, Default)] +struct RecordTypeRegistry { + full: HashMap>, + short: HashMap>, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +enum ValueType { + Named(String), + Record(HashMap), + Unknown, +} + +#[derive(Debug, Clone)] +enum ReturnContract { + Single { ty: String }, + Record { fields: HashMap }, +} + +impl ReturnContract { + fn single(ty: String) -> Self { + Self::Single { ty } + } + + fn record(fields: HashMap) -> Self { + Self::Record { fields } + } +} + +#[derive(Clone, Copy)] +struct BodyInferenceContext<'a> { + record_type_registry: &'a RecordTypeRegistry, + callable_registry: &'a HashMap>, + service_call_registry: &'a ServiceCallRegistry, +} + fn collect_interfaces(modules: &[ResolvedModule]) -> InterfaceRegistry { let mut registry = InterfaceRegistry::default(); for module in modules { @@ -925,6 +1085,26 @@ fn builtin_type_names() -> HashSet { ]) } +fn builtin_type_arities() -> HashMap { + HashMap::from([ + ("Any".to_string(), 0), + ("Unit".to_string(), 0), + ("Bool".to_string(), 0), + ("Int".to_string(), 0), + ("Float".to_string(), 0), + ("String".to_string(), 0), + ("Bytes".to_string(), 0), + ("Json".to_string(), 0), + ("Record".to_string(), 0), + ("List".to_string(), 1), + ("Map".to_string(), 2), + ("Option".to_string(), 1), + ("Result".to_string(), 2), + ("Queue".to_string(), 1), + ("Self".to_string(), 0), + ]) +} + fn record_duplicate_item_name( module_name: &str, item_name: &str, @@ -943,6 +1123,7 @@ fn validate_params( item_name: &str, params: &[Param], known_types: &HashSet, + generic_arity_registry: &GenericArityRegistry, errors: &mut Vec, ) { let mut seen = HashSet::new(); @@ -956,6 +1137,7 @@ fn validate_params( validate_type_expr( ¶m.ty, known_types, + generic_arity_registry, &format!("{}.{}", item_name, param.name), errors, ); @@ -966,6 +1148,7 @@ fn validate_outputs( item_name: &str, outputs: &[Field], known_types: &HashSet, + generic_arity_registry: &GenericArityRegistry, errors: &mut Vec, ) { let mut seen = HashSet::new(); @@ -979,6 +1162,7 @@ fn validate_outputs( validate_type_expr( &output.ty, known_types, + generic_arity_registry, &format!("{}.{}", item_name, output.name), errors, ); @@ -1083,16 +1267,17 @@ fn validate_use_provide_binding_conflicts( fn validate_callable_body( caller: &str, + params: &[Param], + return_contract: ReturnContract, stmts: &[Stmt], - callable_registry: &HashMap>, - service_call_registry: &ServiceCallRegistry, + body_context: &BodyInferenceContext<'_>, allow_unresolved_references: bool, errors: &mut Vec, ) { let mut calls = Vec::new(); collect_calls_from_stmts(stmts, &mut calls); for call in calls { - let contract = match callable_registry.get(&call.callee) { + let contract = match body_context.callable_registry.get(&call.callee) { Some(Some(contract)) => contract, Some(None) => { if !allow_unresolved_references { @@ -1145,7 +1330,8 @@ fn validate_callable_body( collect_service_calls_from_stmts(stmts, &mut service_calls); for call in service_calls { let service_call_name = call.path.join("."); - let contract = match resolve_service_call_contract(&call.path, service_call_registry) { + let contract = + match resolve_service_call_contract(&call.path, body_context.service_call_registry) { ServiceCallResolution::Resolved(contract) => contract, ServiceCallResolution::Ambiguous => { if !allow_unresolved_references { @@ -1193,6 +1379,339 @@ fn validate_callable_body( } } } + + let mut local_bindings = params + .iter() + .map(|param| { + ( + param.name.clone(), + ValueType::Named(type_expr_to_string(¶m.ty)), + ) + }) + .collect::>(); + for stmt in stmts { + match stmt { + Stmt::Let(name, expr) | Stmt::Assign(name, expr) => { + let inferred = infer_expr_type( + expr, + &local_bindings, + body_context, + errors, + ); + local_bindings.insert(name.clone(), inferred); + } + Stmt::Expr(expr) => { + infer_expr_type( + expr, + &local_bindings, + body_context, + errors, + ); + } + Stmt::Return(fields) => { + validate_return_stmt( + caller, + &return_contract, + fields, + &local_bindings, + body_context, + errors, + ); + } + } + } +} + +fn validate_return_stmt( + caller: &str, + return_contract: &ReturnContract, + fields: &[(String, Expr)], + local_bindings: &HashMap, + body_context: &BodyInferenceContext<'_>, + errors: &mut Vec, +) { + match return_contract { + ReturnContract::Single { ty } => { + if fields.len() != 1 { + errors.push(TypeError::TypeMismatch { + expected: ty.clone(), + got: "Record".to_string(), + }); + return; + } + let inferred = infer_expr_type( + &fields[0].1, + local_bindings, + body_context, + errors, + ); + push_type_mismatch_if_needed(ty, &inferred, errors); + } + ReturnContract::Record { fields: expected } => { + for (field, expr) in fields { + let Some(expected_ty) = expected.get(field) else { + errors.push(TypeError::NoSuchField { + ty: format!("{caller}.outputs"), + field: field.clone(), + }); + continue; + }; + let inferred = infer_expr_type( + expr, + local_bindings, + body_context, + errors, + ); + push_type_mismatch_if_needed(expected_ty, &inferred, errors); + } + } + } +} + +fn infer_expr_type( + expr: &Expr, + local_bindings: &HashMap, + body_context: &BodyInferenceContext<'_>, + errors: &mut Vec, +) -> ValueType { + match expr { + Expr::Literal(literal) => match literal { + daglang_syntax::ast::Literal::Int(_) => ValueType::Named("Int".to_string()), + daglang_syntax::ast::Literal::Float(_) => ValueType::Named("Float".to_string()), + daglang_syntax::ast::Literal::String(_) => ValueType::Named("String".to_string()), + daglang_syntax::ast::Literal::Bool(_) => ValueType::Named("Bool".to_string()), + daglang_syntax::ast::Literal::None => ValueType::Named("Unit".to_string()), + }, + Expr::Ident(name) => local_bindings.get(name).cloned().unwrap_or(ValueType::Unknown), + Expr::FieldAccess(base, field) => { + let base_type = infer_expr_type(base, local_bindings, body_context, errors); + match base_type { + ValueType::Record(fields) => match fields.get(field) { + Some(ty) => ValueType::Named(ty.clone()), + None => { + errors.push(TypeError::NoSuchField { + ty: "Record".to_string(), + field: field.clone(), + }); + ValueType::Unknown + } + }, + ValueType::Named(name) => match resolve_record_fields( + &name, + body_context.record_type_registry, + ) { + Some(fields) => match fields.get(field) { + Some(ty) => ValueType::Named(ty.clone()), + None => { + errors.push(TypeError::NoSuchField { + ty: name, + field: field.clone(), + }); + ValueType::Unknown + } + }, + None => ValueType::Unknown, + }, + ValueType::Unknown => ValueType::Unknown, + } + } + Expr::Call(name, args) => { + for (_, arg) in args { + infer_expr_type(arg, local_bindings, body_context, errors); + } + body_context + .callable_registry + .get(name) + .and_then(|entry| entry.as_ref()) + .map(|contract| contract.output.clone()) + .unwrap_or(ValueType::Unknown) + } + Expr::ServiceCall(path, args) => { + for (_, arg) in args { + infer_expr_type(arg, local_bindings, body_context, errors); + } + match resolve_service_call_contract(path, body_context.service_call_registry) { + ServiceCallResolution::Resolved(contract) => ValueType::Record(contract.outputs), + ServiceCallResolution::Ambiguous | ServiceCallResolution::Missing => { + ValueType::Unknown + } + } + } + Expr::BinOp(lhs, op, rhs) => { + let lhs_ty = infer_expr_type(lhs, local_bindings, body_context, errors); + let rhs_ty = infer_expr_type(rhs, local_bindings, body_context, errors); + match op { + daglang_syntax::ast::BinOp::Eq + | daglang_syntax::ast::BinOp::Ne + | daglang_syntax::ast::BinOp::Lt + | daglang_syntax::ast::BinOp::Gt + | daglang_syntax::ast::BinOp::Le + | daglang_syntax::ast::BinOp::Ge + | daglang_syntax::ast::BinOp::And + | daglang_syntax::ast::BinOp::Or => ValueType::Named("Bool".to_string()), + _ => match (lhs_ty, rhs_ty) { + (ValueType::Named(lhs), ValueType::Named(rhs)) + if canonical_type_name(&lhs) == canonical_type_name(&rhs) => + { + ValueType::Named(lhs) + } + _ => ValueType::Unknown, + }, + } + } + Expr::UnaryOp(op, inner) => { + let inner_ty = infer_expr_type(inner, local_bindings, body_context, errors); + match op { + daglang_syntax::ast::UnaryOp::Not => ValueType::Named("Bool".to_string()), + daglang_syntax::ast::UnaryOp::Neg => inner_ty, + } + } + Expr::StringInterp(parts) => { + for part in parts { + if let daglang_syntax::ast::StringPart::Expr(inner) = part { + infer_expr_type(inner, local_bindings, body_context, errors); + } + } + ValueType::Named("String".to_string()) + } + Expr::Record(type_name, fields) => { + for (_, value) in fields { + infer_expr_type(value, local_bindings, body_context, errors); + } + if let Some(name) = type_name { + ValueType::Named(name.clone()) + } else { + ValueType::Record( + fields + .iter() + .map(|(name, expr)| { + ( + name.clone(), + infer_expr_type(expr, local_bindings, body_context, errors) + .display_name() + .unwrap_or_else(|| "Any".to_string()), + ) + }) + .collect(), + ) + } + } + Expr::Match(scrutinee, arms) => { + infer_expr_type(scrutinee, local_bindings, body_context, errors); + for arm in arms { + if let Some(guard) = &arm.guard { + infer_expr_type(guard, local_bindings, body_context, errors); + } + infer_expr_type(&arm.body, local_bindings, body_context, errors); + } + ValueType::Unknown + } + Expr::If(cond, then_expr, else_expr) => { + infer_expr_type(cond, local_bindings, body_context, errors); + let then_ty = infer_expr_type(then_expr, local_bindings, body_context, errors); + let else_ty = else_expr.as_ref().map(|otherwise| { + infer_expr_type(otherwise, local_bindings, body_context, errors) + }); + match else_ty { + Some(otherwise) + if then_ty.display_name().is_some() + && then_ty.display_name() == otherwise.display_name() => + { + then_ty + } + _ => ValueType::Unknown, + } + } + Expr::For(_, iterable, body) => { + infer_expr_type(iterable, local_bindings, body_context, errors); + infer_expr_type(body, local_bindings, body_context, errors); + ValueType::Unknown + } + Expr::Pipe(lhs, rhs) => { + infer_expr_type(lhs, local_bindings, body_context, errors); + infer_expr_type(rhs, local_bindings, body_context, errors) + } + Expr::Lambda(_, body) => infer_expr_type(body, local_bindings, body_context, errors), + Expr::List(items) => { + for item in items { + infer_expr_type(item, local_bindings, body_context, errors); + } + ValueType::Named("List".to_string()) + } + Expr::Map(entries) => { + for (key, value) in entries { + infer_expr_type(key, local_bindings, body_context, errors); + infer_expr_type(value, local_bindings, body_context, errors); + } + ValueType::Named("Map".to_string()) + } + Expr::Guarded(inner, guard) => { + infer_expr_type(inner, local_bindings, body_context, errors); + infer_expr_type(guard, local_bindings, body_context, errors); + ValueType::Unknown + } + Expr::After(inner, _) => infer_expr_type(inner, local_bindings, body_context, errors), + Expr::Return(fields) => ValueType::Record( + fields + .iter() + .map(|(name, expr)| { + ( + name.clone(), + infer_expr_type(expr, local_bindings, body_context, errors) + .display_name() + .unwrap_or_else(|| "Any".to_string()), + ) + }) + .collect(), + ), + } +} + +impl ValueType { + fn display_name(&self) -> Option { + match self { + Self::Named(name) => Some(name.clone()), + Self::Record(_) => Some("Record".to_string()), + Self::Unknown => None, + } + } +} + +fn push_type_mismatch_if_needed(expected: &str, inferred: &ValueType, errors: &mut Vec) { + let Some(got) = inferred.display_name() else { + return; + }; + if !types_match(expected, &got) { + errors.push(TypeError::TypeMismatch { + expected: expected.to_string(), + got, + }); + } +} + +fn types_match(expected: &str, got: &str) -> bool { + if expected == got { + return true; + } + let expected_canonical = canonical_type_name(expected); + let got_canonical = canonical_type_name(got); + expected_canonical == got_canonical + || expected_canonical.rsplit('.').next() == got_canonical.rsplit('.').next() +} + +fn resolve_record_fields( + ty: &str, + registry: &RecordTypeRegistry, +) -> Option> { + let canonical = canonical_type_name(ty); + if let Some(fields) = registry.full.get(&canonical) { + return Some(fields.clone()); + } + let short = canonical.rsplit('.').next().unwrap_or(canonical.as_str()); + let Some(Some(full_name)) = registry.short.get(short) else { + return None; + }; + registry.full.get(full_name).cloned() } fn validate_resource_interface_conformance( @@ -1681,6 +2200,7 @@ fn should_validate_call_name(name: &str) -> bool { fn validate_type_expr( ty: &TypeExpr, known_types: &HashSet, + generic_arity_registry: &GenericArityRegistry, context: &str, errors: &mut Vec, ) { @@ -1694,6 +2214,17 @@ fn validate_type_expr( } } TypeExpr::Generic(name, args) => { + if let Some(expected) = + resolve_generic_arity(name, generic_arity_registry, known_types) + { + if expected != args.len() { + errors.push(TypeError::ArityMismatch { + name: name.clone(), + expected, + got: args.len(), + }); + } + } if should_validate_named_type(name) && !known_types.contains(name) { let tail = name.rsplit('.').next().unwrap_or(name); if !known_types.contains(tail) { @@ -1701,11 +2232,80 @@ fn validate_type_expr( } } for arg in args { - validate_type_expr(arg, known_types, context, errors); + validate_type_expr(arg, known_types, generic_arity_registry, context, errors); + } + } + TypeExpr::Optional(inner) => { + validate_type_expr(inner, known_types, generic_arity_registry, context, errors) + } + TypeExpr::Annotated(inner, annotations) => { + validate_type_expr(inner, known_types, generic_arity_registry, context, errors); + for annotation in annotations { + if annotation.name != "range" { + continue; + } + let (min, max) = extract_range_bounds(&annotation.args); + if let (Some(min), Some(max)) = (min, max) { + if min > max { + errors.push(TypeError::UnsatisfiableRefinement { + ty: type_expr_to_string(inner), + constraint: format!("range min {min} exceeds max {max}"), + }); + } + } } } - TypeExpr::Optional(inner) => validate_type_expr(inner, known_types, context, errors), - TypeExpr::Annotated(inner, _) => validate_type_expr(inner, known_types, context, errors), + } +} + +fn resolve_generic_arity( + name: &str, + registry: &GenericArityRegistry, + known_types: &HashSet, +) -> Option { + if let Some(arity) = registry.full.get(name) { + return Some(*arity); + } + let short = name.rsplit('.').next().unwrap_or(name); + if let Some(entry) = registry.short.get(short) { + return *entry; + } + if known_types.contains(name) || known_types.contains(short) { + return Some(0); + } + None +} + +fn extract_range_bounds(args: &[Expr]) -> (Option, Option) { + let mut min = None; + let mut max = None; + for arg in args { + match arg { + Expr::Record(_, fields) => { + for (name, value) in fields { + match name.as_str() { + "min" => min = extract_int_literal(value), + "max" => max = extract_int_literal(value), + _ => {} + } + } + } + _ => { + if min.is_none() { + min = extract_int_literal(arg); + } else if max.is_none() { + max = extract_int_literal(arg); + } + } + } + } + (min, max) +} + +fn extract_int_literal(expr: &Expr) -> Option { + match expr { + Expr::Literal(daglang_syntax::ast::Literal::Int(value)) => Some(*value), + _ => None, } } @@ -2564,4 +3164,69 @@ func run() -> { ok: Bool } uses io: Storage provides io: Storage { if item == "run" && binding == "io" ))); } + + #[test] + fn type_mismatch_in_fn_return_is_reported() { + let graph = module_graph_from_sources(&[( + "type_mismatch_fn_return.dag", + r#"module sample.types +fn run() -> String { return 42 }"#, + )]); + let errors = typecheck_module_graph(graph).expect_err("type mismatch should fail"); + assert!(errors.iter().any(|error| matches!( + error, + TypeError::TypeMismatch { expected, got } + if expected == "String" && got == "Int" + ))); + } + + #[test] + fn no_such_field_on_record_literal_is_reported() { + let graph = module_graph_from_sources(&[( + "no_such_field.dag", + r#"module sample.fields +func run() -> { body: String } { + let payload = { body: "ok" } + return { body: payload.missing } +}"#, + )]); + let errors = typecheck_module_graph(graph).expect_err("no such field should fail"); + assert!(errors.iter().any(|error| matches!( + error, + TypeError::NoSuchField { ty, field } if ty == "Record" && field == "missing" + ))); + } + + #[test] + fn unsatisfiable_refinement_is_reported() { + let graph = module_graph_from_sources(&[( + "unsat_refinement.dag", + r#"module sample.refinement +fn run(value: Int @range(min: 5, max: 1)) -> Int { value }"#, + )]); + let errors = typecheck_module_graph(graph).expect_err("unsatisfiable range should fail"); + assert!(errors.iter().any(|error| matches!( + error, + TypeError::UnsatisfiableRefinement { ty, constraint } + if ty == "Int" && constraint.contains("min 5 exceeds max 1") + ))); + } + + #[test] + fn generic_arity_mismatch_is_reported() { + let graph = module_graph_from_sources(&[( + "generic_arity_mismatch.dag", + r#"module sample.generics +fn run(items: Map) -> Int { 1 }"#, + )]); + let errors = typecheck_module_graph(graph).expect_err("generic arity mismatch should fail"); + assert!(errors.iter().any(|error| matches!( + error, + TypeError::ArityMismatch { + name, + expected, + got, + } if name == "Map" && *expected == 2 && *got == 1 + ))); + } } From afd672c794bd19a8e19071ea9c04349d2d274658 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 09:17:16 +0000 Subject: [PATCH 070/169] Add named-record field and user-generic arity regressions Co-authored-by: Brian Searls --- .../daglang-cli/tests/compile_commands.rs | 60 +++++++++++++++++++ core/daglang/daglang-typecheck/src/lib.rs | 36 +++++++++++ 2 files changed, 96 insertions(+) diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index 2d8a4a05839..5e920f96a6e 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -1643,6 +1643,36 @@ func run() -> { body: String } { std::fs::remove_file(fixture).expect("failed to cleanup fixture"); } +#[test] +fn compile_command_reports_no_such_field_for_named_record_type() { + let fixture = unique_temp_file("no_such_field_named_record"); + std::fs::write( + &fixture, + r#"module sample.types +type Payload { body: String } +fn run(input: Payload) -> String { input.missing } +"#, + ) + .expect("failed to write fixture"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&fixture) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile"); + + assert!( + !output.status.success(), + "compile should fail on missing field access for named record type" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("typecheck errors")); + assert!(stderr.contains("type `Payload` has no field `missing`")); + + std::fs::remove_file(fixture).expect("failed to cleanup fixture"); +} + #[test] fn compile_command_reports_unsatisfiable_refinement_typecheck_error() { let fixture = unique_temp_file("unsatisfiable_refinement"); @@ -1701,6 +1731,36 @@ fn run(values: Map) -> Int { 1 } std::fs::remove_file(fixture).expect("failed to cleanup fixture"); } +#[test] +fn compile_command_reports_user_defined_generic_arity_mismatch_typecheck_error() { + let fixture = unique_temp_file("user_defined_generic_arity_mismatch"); + std::fs::write( + &fixture, + r#"module sample.types +type Box = T +fn run(values: Box) -> String { values } +"#, + ) + .expect("failed to write fixture"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&fixture) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile"); + + assert!( + !output.status.success(), + "compile should fail on user-defined generic arity mismatch" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("typecheck errors")); + assert!(stderr.contains("generic arity mismatch for `Box`: expected 1, got 2")); + + std::fs::remove_file(fixture).expect("failed to cleanup fixture"); +} + #[test] fn compile_command_reports_service_call_arity_typecheck_error() { let fixture = unique_temp_file("service_call_arity"); diff --git a/core/daglang/daglang-typecheck/src/lib.rs b/core/daglang/daglang-typecheck/src/lib.rs index e114fec86c4..52729b41403 100644 --- a/core/daglang/daglang-typecheck/src/lib.rs +++ b/core/daglang/daglang-typecheck/src/lib.rs @@ -3197,6 +3197,22 @@ func run() -> { body: String } { ))); } + #[test] + fn no_such_field_on_named_record_type_is_reported() { + let graph = module_graph_from_sources(&[( + "no_such_field_named_record.dag", + r#"module sample.fields +type Payload { body: String } +fn run(input: Payload) -> String { input.missing }"#, + )]); + let errors = + typecheck_module_graph(graph).expect_err("no such field on named record should fail"); + assert!(errors.iter().any(|error| matches!( + error, + TypeError::NoSuchField { ty, field } if ty == "Payload" && field == "missing" + ))); + } + #[test] fn unsatisfiable_refinement_is_reported() { let graph = module_graph_from_sources(&[( @@ -3229,4 +3245,24 @@ fn run(items: Map) -> Int { 1 }"#, } if name == "Map" && *expected == 2 && *got == 1 ))); } + + #[test] + fn user_defined_generic_arity_mismatch_is_reported() { + let graph = module_graph_from_sources(&[( + "user_generic_arity_mismatch.dag", + r#"module sample.generics +type Box = T +fn run(value: Box) -> String { value }"#, + )]); + let errors = typecheck_module_graph(graph) + .expect_err("user-defined generic arity mismatch should fail"); + assert!(errors.iter().any(|error| matches!( + error, + TypeError::ArityMismatch { + name, + expected, + got, + } if name == "Box" && *expected == 1 && *got == 2 + ))); + } } From 41eee92123305d5ed5961e4cfa43ce00576615ea Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 09:20:31 +0000 Subject: [PATCH 071/169] Add directory-mode regressions for new type diagnostics Co-authored-by: Brian Searls --- .../daglang-cli/tests/compile_commands.rs | 118 ++++++++++++++++++ 1 file changed, 118 insertions(+) diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index 5e920f96a6e..595fae05aa0 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -2066,6 +2066,124 @@ fn compile_command_directory_mode_fails_on_unresolved_callable_target() { std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); } +#[test] +fn compile_command_directory_mode_fails_on_type_mismatch() { + let root = unique_temp_dir("type_mismatch"); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp dir"); + std::fs::write( + root.join("sample/main.dag"), + "module sample.main\nfn run() -> String { return 42 }", + ) + .expect("failed to write source"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&root) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile on directory"); + + assert!( + !output.status.success(), + "directory compile should fail on return type mismatch" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("typecheck errors")); + assert!(stderr.contains("type mismatch: expected `String`, got `Int`")); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); +} + +#[test] +fn compile_command_directory_mode_fails_on_no_such_field_for_named_record() { + let root = unique_temp_dir("no_such_field_named_record"); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp dir"); + std::fs::write( + root.join("sample/main.dag"), + r#"module sample.main +type Payload { body: String } +fn run(input: Payload) -> String { input.missing } +"#, + ) + .expect("failed to write source"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&root) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile on directory"); + + assert!( + !output.status.success(), + "directory compile should fail on missing field for named record type" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("typecheck errors")); + assert!(stderr.contains("type `Payload` has no field `missing`")); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); +} + +#[test] +fn compile_command_directory_mode_fails_on_unsatisfiable_refinement() { + let root = unique_temp_dir("unsatisfiable_refinement"); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp dir"); + std::fs::write( + root.join("sample/main.dag"), + "module sample.main\nfn run(value: Int @range(min: 9, max: 1)) -> Int { value }", + ) + .expect("failed to write source"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&root) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile on directory"); + + assert!( + !output.status.success(), + "directory compile should fail on unsatisfiable refinement" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("typecheck errors")); + assert!(stderr.contains("unsatisfiable refinement on `Int`: range min 9 exceeds max 1")); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); +} + +#[test] +fn compile_command_directory_mode_fails_on_user_defined_generic_arity_mismatch() { + let root = unique_temp_dir("user_defined_generic_arity"); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp dir"); + std::fs::write( + root.join("sample/main.dag"), + r#"module sample.main +type Box = T +fn run(value: Box) -> String { value } +"#, + ) + .expect("failed to write source"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&root) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile on directory"); + + assert!( + !output.status.success(), + "directory compile should fail on user-defined generic arity mismatch" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("typecheck errors")); + assert!(stderr.contains("generic arity mismatch for `Box`: expected 1, got 2")); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); +} + #[test] fn compile_command_directory_mode_reports_module_path_mismatch() { let root = unique_temp_dir("path_mismatch"); From c6591517b0dd59b448ce64e0953e63619eea3da2 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 09:24:07 +0000 Subject: [PATCH 072/169] Add single-file interface conformance compile regressions Co-authored-by: Brian Searls --- .../daglang-cli/tests/compile_commands.rs | 164 ++++++++++++++++++ 1 file changed, 164 insertions(+) diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index 595fae05aa0..bf98a1a1bc3 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -235,6 +235,170 @@ func run() -> { ok: Bool } uses io: Storage provides io: Storage { return { ok: std::fs::remove_file(fixture).expect("failed to cleanup fixture"); } +#[test] +fn compile_command_single_file_fails_on_missing_resource_capability() { + let fixture = unique_temp_file("compile_single_file_missing_resource_capability"); + std::fs::write( + &fixture, + r#"module sample.single +interface Storage { + capability read { + input { path: String } + output { body: String } + } + capability write { + input { path: String, body: String } + output { ok: Bool } + } +} +resource Disk implements Storage { + capability read { + input { path: String } + output { body: String } + } +} +"#, + ) + .expect("failed to write fixture"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&fixture) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile for missing-resource-capability fixture"); + + assert!( + !output.status.success(), + "single-file compile should fail on missing resource capability" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("typecheck errors")); + assert!(stderr.contains("resource `Disk` is missing capability `write` for interface `Storage`")); + + std::fs::remove_file(fixture).expect("failed to cleanup fixture"); +} + +#[test] +fn compile_command_single_file_fails_on_missing_service_operation() { + let fixture = unique_temp_file("compile_single_file_missing_service_operation"); + std::fs::write( + &fixture, + r#"module sample.single +interface Storage { + capability read { + input { path: String } + output { body: String } + } + capability write { + input { path: String, body: String } + output { ok: Bool } + } +} +service FsStorage implements Storage { + operation read(path: String) -> { body: String } +} +"#, + ) + .expect("failed to write fixture"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&fixture) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile for missing-service-operation fixture"); + + assert!( + !output.status.success(), + "single-file compile should fail on missing service operation" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("typecheck errors")); + assert!(stderr.contains("service `FsStorage` is missing operation `write` for interface `Storage`")); + + std::fs::remove_file(fixture).expect("failed to cleanup fixture"); +} + +#[test] +fn compile_command_single_file_fails_on_interface_signature_mismatch() { + let fixture = unique_temp_file("compile_single_file_interface_signature_mismatch"); + std::fs::write( + &fixture, + r#"module sample.single +interface Storage { + capability read { + input { path: String } + output { body: String } + } +} +service FsStorage implements Storage { + operation read(path: Int) -> { body: String } +} +"#, + ) + .expect("failed to write fixture"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&fixture) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile for service-signature-mismatch fixture"); + + assert!( + !output.status.success(), + "single-file compile should fail on service signature mismatch" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("typecheck errors")); + assert!(stderr.contains("`FsStorage` does not match `Storage.read` contract")); + assert!(stderr.contains("expected `String` but found `Int`")); + + std::fs::remove_file(fixture).expect("failed to cleanup fixture"); +} + +#[test] +fn compile_command_single_file_fails_on_resource_interface_signature_mismatch() { + let fixture = unique_temp_file("compile_single_file_resource_signature_mismatch"); + std::fs::write( + &fixture, + r#"module sample.single +interface Storage { + capability read { + input { path: String } + output { body: String } + } +} +resource Disk implements Storage { + capability read { + input { path: Int } + output { body: String } + } +} +"#, + ) + .expect("failed to write fixture"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&fixture) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile for resource-signature-mismatch fixture"); + + assert!( + !output.status.success(), + "single-file compile should fail on resource signature mismatch" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("typecheck errors")); + assert!(stderr.contains("`Disk` does not match `Storage.read` contract")); + assert!(stderr.contains("expected `String` but found `Int`")); + + std::fs::remove_file(fixture).expect("failed to cleanup fixture"); +} + #[test] fn compile_command_single_file_unresolved_service_call_reports_lower_error() { let fixture = unique_temp_file("compile_unresolved_service_single_file"); From 9f590a196e0a70c2664e3e91fc5f116b807c6dfb Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 09:27:59 +0000 Subject: [PATCH 073/169] Add duplicate interface ambiguity diagnostics regressions Co-authored-by: Brian Searls --- .../daglang-cli/tests/compile_commands.rs | 44 +++++++++++++++++++ core/daglang/daglang-typecheck/src/lib.rs | 36 +++++++++++++++ 2 files changed, 80 insertions(+) diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index bf98a1a1bc3..809ee7a2ae2 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -626,6 +626,50 @@ resource Disk implements MissingStorage { std::fs::remove_file(fixture).expect("failed to cleanup fixture"); } +#[test] +fn compile_command_single_file_duplicate_interface_reports_ambiguous_implements() { + let fixture = unique_temp_file("compile_single_file_duplicate_interface_ambiguous_impl"); + std::fs::write( + &fixture, + r#"module sample.single +interface Storage { + capability read { + input { path: String } + output { body: String } + } +} +interface Storage { + capability read { + input { path: String } + output { body: String } + } +} +service FsStorage implements Storage { + operation read(path: String) -> { body: String } +} +"#, + ) + .expect("failed to write fixture"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&fixture) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile for duplicate-interface fixture"); + + assert!( + !output.status.success(), + "single-file compile should fail for duplicate interface definitions" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("typecheck errors")); + assert!(stderr.contains("duplicate definition `Storage` in module `sample.single`")); + assert!(stderr.contains("`FsStorage` references ambiguous interface `Storage`")); + + std::fs::remove_file(fixture).expect("failed to cleanup fixture"); +} + #[test] fn expand_command_shows_lowered_nodes_and_edges() { let output = Command::new(daglang_bin()) diff --git a/core/daglang/daglang-typecheck/src/lib.rs b/core/daglang/daglang-typecheck/src/lib.rs index 52729b41403..d1f24fb5ea0 100644 --- a/core/daglang/daglang-typecheck/src/lib.rs +++ b/core/daglang/daglang-typecheck/src/lib.rs @@ -2446,6 +2446,42 @@ func run() -> { ok: Bool } { ))); } + #[test] + fn duplicate_interface_definition_is_reported_and_causes_ambiguous_implements() { + let graph = module_graph_from_sources(&[( + "duplicate_interface_definition.dag", + r#"module sample.dup +interface Storage { + capability read { + input { path: String } + output { body: String } + } +} +interface Storage { + capability read { + input { path: String } + output { body: String } + } +} +service FsStorage implements Storage { + operation read(path: String) -> { body: String } +} +"#, + )]); + let errors = typecheck_module_graph(graph) + .expect_err("duplicate interface definitions should fail"); + assert!(errors.iter().any(|error| matches!( + error, + TypeError::DuplicateDefinition { module, name } + if module == "sample.dup" && name == "Storage" + ))); + assert!(errors.iter().any(|error| matches!( + error, + TypeError::AmbiguousInterface { implementor, interface } + if implementor == "FsStorage" && interface == "Storage" + ))); + } + #[test] fn strict_mode_reports_unresolved_imports() { let graph = module_graph_from_sources(&[( From 08e3d7a99d3cd57f8278da3b3a8ad4b96c3f438a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 09:30:33 +0000 Subject: [PATCH 074/169] Add duplicate callable ambiguity diagnostics regressions Co-authored-by: Brian Searls --- .../daglang-cli/tests/compile_commands.rs | 33 +++++++++++++++++++ core/daglang/daglang-typecheck/src/lib.rs | 28 ++++++++++++++++ 2 files changed, 61 insertions(+) diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index 809ee7a2ae2..a7c2c733ecd 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -2246,6 +2246,39 @@ fn compile_command_directory_mode_fails_on_ambiguous_callable_target() { std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); } +#[test] +fn compile_command_directory_mode_duplicate_callable_also_reports_ambiguous_call_target() { + let root = unique_temp_dir("duplicate_callable_ambiguous_call_target"); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp dir"); + std::fs::write( + root.join("sample/main.dag"), + r#"module sample.main +fn helper() -> String { "a" } +fn helper() -> String { "b" } +fn run() -> String { helper() } +"#, + ) + .expect("failed to write source"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&root) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile on directory"); + + assert!( + !output.status.success(), + "directory compile should fail on duplicate callable definitions" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("typecheck errors")); + assert!(stderr.contains("duplicate definition `helper` in module `sample.main`")); + assert!(stderr.contains("ambiguous call target `helper` in `run`")); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); +} + #[test] fn compile_command_directory_mode_fails_on_unresolved_callable_target() { let root = unique_temp_dir("unresolved_callable_target"); diff --git a/core/daglang/daglang-typecheck/src/lib.rs b/core/daglang/daglang-typecheck/src/lib.rs index d1f24fb5ea0..e234e9e9f1b 100644 --- a/core/daglang/daglang-typecheck/src/lib.rs +++ b/core/daglang/daglang-typecheck/src/lib.rs @@ -2548,6 +2548,34 @@ service FsStorage implements Storage { ))); } + #[test] + fn strict_mode_duplicate_callable_definition_also_reports_ambiguous_call_target() { + let graph = module_graph_from_sources(&[( + "sample/main.dag", + r#"module sample.main +fn helper() -> String { "a" } +fn helper() -> String { "b" } +fn run() -> String { helper() }"#, + )]); + let errors = typecheck_module_graph_with_options( + graph, + TypecheckOptions { + allow_unresolved_imports: false, + }, + ) + .expect_err("strict mode should fail for duplicate callable definition"); + assert!(errors.iter().any(|error| matches!( + error, + TypeError::DuplicateDefinition { module, name } + if module == "sample.main" && name == "helper" + ))); + assert!(errors.iter().any(|error| matches!( + error, + TypeError::AmbiguousCallTarget { caller, callee } + if caller == "run" && callee == "helper" + ))); + } + #[test] fn strict_mode_reports_unresolved_call_target() { let graph = module_graph_from_sources(&[( From cff11c8eee364cfda4892f713b291f9bbe34f487 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 09:35:06 +0000 Subject: [PATCH 075/169] Validate implicit fn return type mismatches Co-authored-by: Brian Searls --- .../daglang-cli/tests/compile_commands.rs | 57 +++++++++++++++++++ core/daglang/daglang-typecheck/src/lib.rs | 32 ++++++++++- 2 files changed, 87 insertions(+), 2 deletions(-) diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index a7c2c733ecd..2f7e46008ba 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -1819,6 +1819,35 @@ fn run() -> String { return 42 } std::fs::remove_file(fixture).expect("failed to cleanup fixture"); } +#[test] +fn compile_command_reports_implicit_return_type_mismatch_typecheck_error() { + let fixture = unique_temp_file("implicit_return_type_mismatch"); + std::fs::write( + &fixture, + r#"module sample.types +fn run() -> String { 42 } +"#, + ) + .expect("failed to write fixture"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&fixture) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile"); + + assert!( + !output.status.success(), + "compile should fail on implicit return type mismatch" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("typecheck errors")); + assert!(stderr.contains("type mismatch: expected `String`, got `Int`")); + + std::fs::remove_file(fixture).expect("failed to cleanup fixture"); +} + #[test] fn compile_command_reports_no_such_field_typecheck_error() { let fixture = unique_temp_file("no_such_field"); @@ -2335,6 +2364,34 @@ fn compile_command_directory_mode_fails_on_type_mismatch() { std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); } +#[test] +fn compile_command_directory_mode_fails_on_implicit_return_type_mismatch() { + let root = unique_temp_dir("implicit_type_mismatch"); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp dir"); + std::fs::write( + root.join("sample/main.dag"), + "module sample.main\nfn run() -> String { 42 }", + ) + .expect("failed to write source"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&root) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile on directory"); + + assert!( + !output.status.success(), + "directory compile should fail on implicit return type mismatch" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("typecheck errors")); + assert!(stderr.contains("type mismatch: expected `String`, got `Int`")); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); +} + #[test] fn compile_command_directory_mode_fails_on_no_such_field_for_named_record() { let root = unique_temp_dir("no_such_field_named_record"); diff --git a/core/daglang/daglang-typecheck/src/lib.rs b/core/daglang/daglang-typecheck/src/lib.rs index e234e9e9f1b..c6c15eca6a6 100644 --- a/core/daglang/daglang-typecheck/src/lib.rs +++ b/core/daglang/daglang-typecheck/src/lib.rs @@ -1389,6 +1389,8 @@ fn validate_callable_body( ) }) .collect::>(); + let mut saw_explicit_return = false; + let mut trailing_expr_type = None; for stmt in stmts { match stmt { Stmt::Let(name, expr) | Stmt::Assign(name, expr) => { @@ -1399,16 +1401,19 @@ fn validate_callable_body( errors, ); local_bindings.insert(name.clone(), inferred); + trailing_expr_type = None; } Stmt::Expr(expr) => { - infer_expr_type( + trailing_expr_type = Some(infer_expr_type( expr, &local_bindings, body_context, errors, - ); + )); } Stmt::Return(fields) => { + saw_explicit_return = true; + trailing_expr_type = None; validate_return_stmt( caller, &return_contract, @@ -1420,6 +1425,13 @@ fn validate_callable_body( } } } + if !saw_explicit_return { + if let ReturnContract::Single { ty } = &return_contract { + if let Some(inferred) = trailing_expr_type { + push_type_mismatch_if_needed(ty, &inferred, errors); + } + } + } } fn validate_return_stmt( @@ -3244,6 +3256,22 @@ fn run() -> String { return 42 }"#, ))); } + #[test] + fn implicit_type_mismatch_in_fn_return_is_reported() { + let graph = module_graph_from_sources(&[( + "implicit_type_mismatch_fn_return.dag", + r#"module sample.types +fn run() -> String { 42 }"#, + )]); + let errors = typecheck_module_graph(graph) + .expect_err("implicit return type mismatch should fail"); + assert!(errors.iter().any(|error| matches!( + error, + TypeError::TypeMismatch { expected, got } + if expected == "String" && got == "Int" + ))); + } + #[test] fn no_such_field_on_record_literal_is_reported() { let graph = module_graph_from_sources(&[( From 4e457673f7ac3fb168d69be207f0d40495feb8c4 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 09:38:06 +0000 Subject: [PATCH 076/169] Report fn missing-tail return mismatches as Unit Co-authored-by: Brian Searls --- .../daglang-cli/tests/compile_commands.rs | 57 +++++++++++++++++++ core/daglang/daglang-typecheck/src/lib.rs | 33 ++++++++++- 2 files changed, 87 insertions(+), 3 deletions(-) diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index 2f7e46008ba..108b823b366 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -1848,6 +1848,35 @@ fn run() -> String { 42 } std::fs::remove_file(fixture).expect("failed to cleanup fixture"); } +#[test] +fn compile_command_reports_missing_tail_expression_type_mismatch_typecheck_error() { + let fixture = unique_temp_file("missing_tail_expression_type_mismatch"); + std::fs::write( + &fixture, + r#"module sample.types +fn run() -> String { let x = 42 } +"#, + ) + .expect("failed to write fixture"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&fixture) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile"); + + assert!( + !output.status.success(), + "compile should fail when fn has no tail expression for non-unit return type" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("typecheck errors")); + assert!(stderr.contains("type mismatch: expected `String`, got `Unit`")); + + std::fs::remove_file(fixture).expect("failed to cleanup fixture"); +} + #[test] fn compile_command_reports_no_such_field_typecheck_error() { let fixture = unique_temp_file("no_such_field"); @@ -2392,6 +2421,34 @@ fn compile_command_directory_mode_fails_on_implicit_return_type_mismatch() { std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); } +#[test] +fn compile_command_directory_mode_fails_on_missing_tail_expression_type_mismatch() { + let root = unique_temp_dir("missing_tail_expression_type_mismatch"); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp dir"); + std::fs::write( + root.join("sample/main.dag"), + "module sample.main\nfn run() -> String { let x = 42 }", + ) + .expect("failed to write source"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&root) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile on directory"); + + assert!( + !output.status.success(), + "directory compile should fail when fn has no tail expression for non-unit return type" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("typecheck errors")); + assert!(stderr.contains("type mismatch: expected `String`, got `Unit`")); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); +} + #[test] fn compile_command_directory_mode_fails_on_no_such_field_for_named_record() { let root = unique_temp_dir("no_such_field_named_record"); diff --git a/core/daglang/daglang-typecheck/src/lib.rs b/core/daglang/daglang-typecheck/src/lib.rs index c6c15eca6a6..49ba4f50ff7 100644 --- a/core/daglang/daglang-typecheck/src/lib.rs +++ b/core/daglang/daglang-typecheck/src/lib.rs @@ -1427,9 +1427,9 @@ fn validate_callable_body( } if !saw_explicit_return { if let ReturnContract::Single { ty } = &return_contract { - if let Some(inferred) = trailing_expr_type { - push_type_mismatch_if_needed(ty, &inferred, errors); - } + let inferred = trailing_expr_type + .unwrap_or_else(|| ValueType::Named("Unit".to_string())); + push_type_mismatch_if_needed(ty, &inferred, errors); } } } @@ -3272,6 +3272,33 @@ fn run() -> String { 42 }"#, ))); } + #[test] + fn missing_tail_expression_in_fn_return_is_reported_as_unit_mismatch() { + let graph = module_graph_from_sources(&[( + "missing_tail_expression_fn_return.dag", + r#"module sample.types +fn run() -> String { let x = 42 }"#, + )]); + let errors = typecheck_module_graph(graph) + .expect_err("missing tail expression should fail for non-unit return type"); + assert!(errors.iter().any(|error| matches!( + error, + TypeError::TypeMismatch { expected, got } + if expected == "String" && got == "Unit" + ))); + } + + #[test] + fn missing_tail_expression_is_allowed_for_unit_return_type() { + let graph = module_graph_from_sources(&[( + "missing_tail_expression_unit_return.dag", + r#"module sample.types +fn run() -> Unit { let x = 42 }"#, + )]); + let typed = typecheck_module_graph(graph).expect("unit return type should allow no tail expression"); + assert_eq!(typed.modules.len(), 1); + } + #[test] fn no_such_field_on_record_literal_is_reported() { let graph = module_graph_from_sources(&[( From 2ef268898d0c55bc503af3ce0a0e37babdb28f7b Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 09:42:10 +0000 Subject: [PATCH 077/169] Add Unit-return missing-tail compile success regressions Co-authored-by: Brian Searls --- .../daglang-cli/tests/compile_commands.rs | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index 108b823b366..01b102573c1 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -532,6 +532,35 @@ fn run() -> Unit {} std::fs::remove_file(fixture).expect("failed to cleanup fixture"); } +#[test] +fn compile_command_single_file_allows_unit_return_without_tail_expression() { + let fixture = unique_temp_file("compile_single_file_unit_missing_tail"); + std::fs::write( + &fixture, + r#"module sample.single +fn run() -> Unit { let x = 42 } +"#, + ) + .expect("failed to write fixture"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&fixture) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile for unit-missing-tail fixture"); + + assert!( + output.status.success(), + "single-file compile should allow missing tail for Unit return: {}", + String::from_utf8_lossy(&output.stderr) + ); + let stdout = String::from_utf8_lossy(&output.stdout); + assert!(stdout.contains("Compiled 1 module(s)")); + + std::fs::remove_file(fixture).expect("failed to cleanup fixture"); +} + #[test] fn compile_command_single_file_allows_unresolved_call_targets() { let fixture = unique_temp_file("compile_single_file_unresolved_call_target"); @@ -2421,6 +2450,34 @@ fn compile_command_directory_mode_fails_on_implicit_return_type_mismatch() { std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); } +#[test] +fn compile_command_directory_mode_allows_unit_return_without_tail_expression() { + let root = unique_temp_dir("unit_missing_tail"); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp dir"); + std::fs::write( + root.join("sample/main.dag"), + "module sample.main\nfn run() -> Unit { let x = 42 }", + ) + .expect("failed to write source"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&root) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile on directory"); + + assert!( + output.status.success(), + "directory compile should allow missing tail for Unit return: {}", + String::from_utf8_lossy(&output.stderr) + ); + let stdout = String::from_utf8_lossy(&output.stdout); + assert!(stdout.contains("Compiled 1 module(s)")); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); +} + #[test] fn compile_command_directory_mode_fails_on_missing_tail_expression_type_mismatch() { let root = unique_temp_dir("missing_tail_expression_type_mismatch"); From 9271081ebf1c273e7406a0c918ab10c90974d420 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 09:43:46 +0000 Subject: [PATCH 078/169] Add directory duplicate-interface diagnostic layering regression Co-authored-by: Brian Searls --- .../daglang-cli/tests/compile_commands.rs | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index 01b102573c1..0ddbc556723 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -1124,6 +1124,45 @@ func run() -> { ok: Bool } { std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); } +#[test] +fn compile_command_directory_mode_duplicate_interface_also_reports_ambiguous_implements() { + let root = unique_temp_dir("duplicate_interface_ambiguous_implements"); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp dir"); + std::fs::write( + root.join("sample/main.dag"), + r#"module sample.main +interface Storage { + capability read { input { path: String } output { body: String } } +} +interface Storage { + capability read { input { path: String } output { body: String } } +} +service FsStorage implements Storage { + operation read(path: String) -> { body: String } +} +"#, + ) + .expect("failed to write source"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&root) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile on directory"); + + assert!( + !output.status.success(), + "directory compile should fail on duplicate interface definitions" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("typecheck errors")); + assert!(stderr.contains("duplicate definition `Storage` in module `sample.main`")); + assert!(stderr.contains("`FsStorage` references ambiguous interface `Storage`")); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); +} + #[test] fn compile_command_directory_mode_fails_on_duplicate_output_fields() { let root = unique_temp_dir("duplicate_output_fields"); From 04e4037ca5063a40f8c71df02bc7437bdd20130d Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 09:47:39 +0000 Subject: [PATCH 079/169] Add directory compile regressions for call argument diagnostics Co-authored-by: Brian Searls --- .../daglang-cli/tests/compile_commands.rs | 227 ++++++++++++++++++ 1 file changed, 227 insertions(+) diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index 0ddbc556723..6815a4c9c7b 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -2254,6 +2254,233 @@ func run(path: String) -> { body: String } { std::fs::remove_file(fixture).expect("failed to cleanup fixture"); } +#[test] +fn compile_command_directory_mode_fails_on_call_arity_typecheck_error() { + let root = unique_temp_dir("call_arity"); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp dir"); + std::fs::write( + root.join("sample/main.dag"), + r#"module sample.main +fn fmt(value: String) -> String { value } +fn run() -> String { fmt() } +"#, + ) + .expect("failed to write source"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&root) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile on directory"); + + assert!( + !output.status.success(), + "directory compile should fail on call arity mismatch" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("typecheck errors")); + assert!(stderr.contains("call arity mismatch")); + assert!(stderr.contains("fmt")); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); +} + +#[test] +fn compile_command_directory_mode_fails_on_unknown_named_call_argument_typecheck_error() { + let root = unique_temp_dir("call_unknown_arg"); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp dir"); + std::fs::write( + root.join("sample/main.dag"), + r#"module sample.main +fn fmt(value: String) -> String { value } +fn run() -> String { fmt(text: "ok") } +"#, + ) + .expect("failed to write source"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&root) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile on directory"); + + assert!( + !output.status.success(), + "directory compile should fail on unknown named call argument" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("typecheck errors")); + assert!(stderr.contains("unknown named argument `text`")); + assert!(stderr.contains("call to `fmt`")); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); +} + +#[test] +fn compile_command_directory_mode_fails_on_duplicate_named_call_argument_typecheck_error() { + let root = unique_temp_dir("duplicate_named_call_arg"); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp dir"); + std::fs::write( + root.join("sample/main.dag"), + r#"module sample.main +fn fmt(value: String, mode: String) -> String { value } +fn run() -> String { + fmt(value: "a", value: "b") +} +"#, + ) + .expect("failed to write source"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&root) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile on directory"); + + assert!( + !output.status.success(), + "directory compile should fail on duplicate named call argument" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("typecheck errors")); + assert!(stderr.contains("duplicate named argument `value`")); + assert!(stderr.contains("call to `fmt`")); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); +} + +#[test] +fn compile_command_directory_mode_fails_on_service_call_arity_typecheck_error() { + let root = unique_temp_dir("service_call_arity"); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp dir"); + std::fs::write( + root.join("sample/main.dag"), + r#"module sample.main +interface Storage { + capability read { + input { path: String } + output { body: String } + } +} +service FsStorage implements Storage { + operation read(path: String) -> { body: String } +} +func run() -> { ok: Bool } { + let response = FsStorage.read() + return { ok: true } +} +"#, + ) + .expect("failed to write source"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&root) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile on directory"); + + assert!( + !output.status.success(), + "directory compile should fail on service call arity mismatch" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("typecheck errors")); + assert!(stderr.contains("service call arity mismatch")); + assert!(stderr.contains("FsStorage.read")); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); +} + +#[test] +fn compile_command_directory_mode_fails_on_unknown_named_service_argument_typecheck_error() { + let root = unique_temp_dir("service_call_unknown_arg"); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp dir"); + std::fs::write( + root.join("sample/main.dag"), + r#"module sample.main +interface Storage { + capability read { + input { path: String } + output { body: String } + } +} +service FsStorage implements Storage { + operation read(path: String) -> { body: String } +} +func run(path: String) -> { body: String } { + let response = FsStorage.read(file: path) + return { body: response.body } +} +"#, + ) + .expect("failed to write source"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&root) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile on directory"); + + assert!( + !output.status.success(), + "directory compile should fail on unknown named service argument" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("typecheck errors")); + assert!(stderr.contains("unknown named argument `file`")); + assert!(stderr.contains("service call `FsStorage.read`")); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); +} + +#[test] +fn compile_command_directory_mode_fails_on_duplicate_named_service_argument_typecheck_error() { + let root = unique_temp_dir("duplicate_named_service_arg"); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp dir"); + std::fs::write( + root.join("sample/main.dag"), + r#"module sample.main +interface Storage { + capability read { + input { path: String, mode: String } + output { body: String } + } +} +service FsStorage implements Storage { + operation read(path: String, mode: String) -> { body: String } +} +func run(path: String) -> { body: String } { + let response = FsStorage.read(path: path, path: path) + return { body: response.body } +} +"#, + ) + .expect("failed to write source"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&root) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile on directory"); + + assert!( + !output.status.success(), + "directory compile should fail on duplicate named service argument" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("typecheck errors")); + assert!(stderr.contains("duplicate named argument `path`")); + assert!(stderr.contains("service call `FsStorage.read`")); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); +} + #[test] fn compile_command_directory_mode_fails_on_unresolved_service_call() { let root = unique_temp_dir("unresolved_service_call_typecheck"); From 485eaa8a55f50a5d95d4ee54e60a8dc3b0483e91 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 09:50:58 +0000 Subject: [PATCH 080/169] Add directory regressions for undefined and builtin type diagnostics Co-authored-by: Brian Searls --- .../daglang-cli/tests/compile_commands.rs | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index 6815a4c9c7b..45f8ec7ac7d 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -2772,6 +2772,39 @@ fn compile_command_directory_mode_fails_on_missing_tail_expression_type_mismatch std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); } +#[test] +fn compile_command_directory_mode_fails_on_no_such_field_for_record_literal() { + let root = unique_temp_dir("no_such_field_record_literal"); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp dir"); + std::fs::write( + root.join("sample/main.dag"), + r#"module sample.main +func run() -> { body: String } { + let payload = { body: "ok" } + return { body: payload.missing } +} +"#, + ) + .expect("failed to write source"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&root) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile on directory"); + + assert!( + !output.status.success(), + "directory compile should fail on missing field access for record literal" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("typecheck errors")); + assert!(stderr.contains("type `Record` has no field `missing`")); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); +} + #[test] fn compile_command_directory_mode_fails_on_no_such_field_for_named_record() { let root = unique_temp_dir("no_such_field_named_record"); @@ -2803,6 +2836,36 @@ fn run(input: Payload) -> String { input.missing } std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); } +#[test] +fn compile_command_directory_mode_fails_on_generic_arity_mismatch() { + let root = unique_temp_dir("generic_arity_mismatch"); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp dir"); + std::fs::write( + root.join("sample/main.dag"), + r#"module sample.main +fn run(values: Map) -> Int { 1 } +"#, + ) + .expect("failed to write source"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&root) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile on directory"); + + assert!( + !output.status.success(), + "directory compile should fail on generic arity mismatch" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("typecheck errors")); + assert!(stderr.contains("generic arity mismatch for `Map`: expected 2, got 1")); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); +} + #[test] fn compile_command_directory_mode_fails_on_unsatisfiable_refinement() { let root = unique_temp_dir("unsatisfiable_refinement"); @@ -2831,6 +2894,36 @@ fn compile_command_directory_mode_fails_on_unsatisfiable_refinement() { std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); } +#[test] +fn compile_command_directory_mode_fails_on_undefined_type_typecheck_error() { + let root = unique_temp_dir("undefined_type"); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp dir"); + std::fs::write( + root.join("sample/main.dag"), + r#"module sample.main +fn run(input: MissingType) -> String { "ok" } +"#, + ) + .expect("failed to write source"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&root) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile on directory"); + + assert!( + !output.status.success(), + "directory compile should fail on undefined type" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("typecheck errors")); + assert!(stderr.contains("undefined type `MissingType")); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); +} + #[test] fn compile_command_directory_mode_fails_on_user_defined_generic_arity_mismatch() { let root = unique_temp_dir("user_defined_generic_arity"); From 257bb298c0659abc36959c7f95eb865b59c166a7 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 09:53:46 +0000 Subject: [PATCH 081/169] Lock relaxed single-file duplicate-callable diagnostic behavior Co-authored-by: Brian Searls --- .../daglang-cli/tests/compile_commands.rs | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index 45f8ec7ac7d..efa61f3a90e 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -87,6 +87,41 @@ fn run() -> String { "b" } std::fs::remove_file(fixture).expect("failed to cleanup fixture"); } +#[test] +fn compile_command_single_file_duplicate_callable_does_not_report_ambiguous_call_target() { + let fixture = unique_temp_file("compile_single_file_duplicate_callable_relaxed"); + std::fs::write( + &fixture, + r#"module sample.single +fn helper() -> String { "a" } +fn helper() -> String { "b" } +fn run() -> String { helper() } +"#, + ) + .expect("failed to write fixture"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&fixture) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile for duplicate-callable fixture"); + + assert!( + !output.status.success(), + "single-file compile should fail on duplicate callable definition" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("typecheck errors")); + assert!(stderr.contains("duplicate definition `helper` in module `sample.single`")); + assert!( + !stderr.contains("ambiguous call target `helper`"), + "single-file relaxed mode should not report ambiguous call-target diagnostics: {stderr}" + ); + + std::fs::remove_file(fixture).expect("failed to cleanup fixture"); +} + #[test] fn compile_command_single_file_fails_on_duplicate_parameter() { let fixture = unique_temp_file("compile_single_file_duplicate_parameter"); From d7bb2c8197934e50c7f463f0acbbacd7c5dc4f41 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 09:55:42 +0000 Subject: [PATCH 082/169] Add duplicate-service diagnostic strictness regressions Co-authored-by: Brian Searls --- .../daglang-cli/tests/compile_commands.rs | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index efa61f3a90e..d5495fee664 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -122,6 +122,54 @@ fn run() -> String { helper() } std::fs::remove_file(fixture).expect("failed to cleanup fixture"); } +#[test] +fn compile_command_single_file_duplicate_service_does_not_report_ambiguous_service_call() { + let fixture = unique_temp_file("compile_single_file_duplicate_service_relaxed"); + std::fs::write( + &fixture, + r#"module sample.single +interface Storage { + capability read { + input { path: String } + output { body: String } + } +} +service FsStorage implements Storage { + operation read(path: String) -> { body: String } +} +service FsStorage implements Storage { + operation read(path: String) -> { body: String } +} +func run(path: String) -> { body: String } { + let response = FsStorage.read(path: path) + return { body: response.body } +} +"#, + ) + .expect("failed to write fixture"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&fixture) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile for duplicate-service fixture"); + + assert!( + !output.status.success(), + "single-file compile should fail on duplicate service definition" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("typecheck errors")); + assert!(stderr.contains("duplicate definition `FsStorage` in module `sample.single`")); + assert!( + !stderr.contains("ambiguous service call `FsStorage.read`"), + "single-file relaxed mode should not report ambiguous service-call diagnostics: {stderr}" + ); + + std::fs::remove_file(fixture).expect("failed to cleanup fixture"); +} + #[test] fn compile_command_single_file_fails_on_duplicate_parameter() { let fixture = unique_temp_file("compile_single_file_duplicate_parameter"); @@ -1198,6 +1246,52 @@ service FsStorage implements Storage { std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); } +#[test] +fn compile_command_directory_mode_duplicate_service_also_reports_ambiguous_service_call() { + let root = unique_temp_dir("duplicate_service_ambiguous_call"); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp dir"); + std::fs::write( + root.join("sample/main.dag"), + r#"module sample.main +interface Storage { + capability read { + input { path: String } + output { body: String } + } +} +service FsStorage implements Storage { + operation read(path: String) -> { body: String } +} +service FsStorage implements Storage { + operation read(path: String) -> { body: String } +} +func run(path: String) -> { body: String } { + let response = FsStorage.read(path: path) + return { body: response.body } +} +"#, + ) + .expect("failed to write source"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&root) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile on directory"); + + assert!( + !output.status.success(), + "directory compile should fail on duplicate service definitions" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("typecheck errors")); + assert!(stderr.contains("duplicate definition `FsStorage` in module `sample.main`")); + assert!(stderr.contains("ambiguous service call `FsStorage.read` in `run`")); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); +} + #[test] fn compile_command_directory_mode_fails_on_duplicate_output_fields() { let root = unique_temp_dir("duplicate_output_fields"); From 7656d590f13801707bb8e8be6f497ea4cd832a05 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 09:57:35 +0000 Subject: [PATCH 083/169] Assert single-file unresolved targets fail in lower stage Co-authored-by: Brian Searls --- core/daglang/daglang-cli/tests/compile_commands.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index d5495fee664..4e623c0636f 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -517,6 +517,10 @@ func run(path: String) -> { body: String } { "single-file compile should fail for unresolved service call" ); let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + !stderr.contains("typecheck errors"), + "single-file unresolved service-call path should fail in lower stage: {stderr}" + ); assert!(stderr.contains("lower error: unresolved service call")); assert!(stderr.contains("MissingStorage.read")); @@ -548,6 +552,10 @@ func run() -> { ok: Bool } uses fs: MissingResource { "single-file compile should fail for unresolved uses binding" ); let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + !stderr.contains("typecheck errors"), + "single-file unresolved uses path should fail in lower stage: {stderr}" + ); assert!(stderr.contains("lower error: unresolved used resource")); assert!(stderr.contains("fs: MissingResource")); @@ -579,6 +587,10 @@ func run() -> { ok: Bool } provides out: MissingResource { "single-file compile should fail for unresolved provides binding" ); let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + !stderr.contains("typecheck errors"), + "single-file unresolved provides path should fail in lower stage: {stderr}" + ); assert!(stderr.contains("lower error: unresolved provided resource")); assert!(stderr.contains("out: MissingResource")); From f818c03cf7c11ce6c1e09ac32b367fc0718b0884 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 10:00:46 +0000 Subject: [PATCH 084/169] Add typecheck tests for duplicate-service strictness layering Co-authored-by: Brian Searls --- core/daglang/daglang-typecheck/src/lib.rs | 81 +++++++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/core/daglang/daglang-typecheck/src/lib.rs b/core/daglang/daglang-typecheck/src/lib.rs index 49ba4f50ff7..255c3897dbb 100644 --- a/core/daglang/daglang-typecheck/src/lib.rs +++ b/core/daglang/daglang-typecheck/src/lib.rs @@ -2758,6 +2758,87 @@ func run(path: String) -> { body: String } { ))); } + #[test] + fn strict_mode_duplicate_service_definition_also_reports_ambiguous_service_call() { + let graph = module_graph_from_sources(&[( + "sample/main.dag", + r#"module sample.main +interface Storage { + capability read { + input { path: String } + output { body: String } + } +} +service FsStorage implements Storage { + operation read(path: String) -> { body: String } +} +service FsStorage implements Storage { + operation read(path: String) -> { body: String } +} +func run(path: String) -> { body: String } { + let response = FsStorage.read(path: path) + return { body: response.body } +}"#, + )]); + let errors = typecheck_module_graph_with_options( + graph, + TypecheckOptions { + allow_unresolved_imports: false, + }, + ) + .expect_err("strict mode should fail for duplicate service definition"); + assert!(errors.iter().any(|error| matches!( + error, + TypeError::DuplicateDefinition { module, name } + if module == "sample.main" && name == "FsStorage" + ))); + assert!(errors.iter().any(|error| matches!( + error, + TypeError::AmbiguousServiceCall { + caller, + service_call + } if caller == "run" && service_call == "FsStorage.read" + ))); + } + + #[test] + fn relaxed_mode_duplicate_service_definition_suppresses_ambiguous_service_call() { + let graph = module_graph_from_sources(&[( + "sample/single.dag", + r#"module sample.single +interface Storage { + capability read { + input { path: String } + output { body: String } + } +} +service FsStorage implements Storage { + operation read(path: String) -> { body: String } +} +service FsStorage implements Storage { + operation read(path: String) -> { body: String } +} +func run(path: String) -> { body: String } { + let response = FsStorage.read(path: path) + return { body: response.body } +}"#, + )]); + let errors = typecheck_module_graph(graph) + .expect_err("relaxed mode should still fail for duplicate service definition"); + assert!(errors.iter().any(|error| matches!( + error, + TypeError::DuplicateDefinition { module, name } + if module == "sample.single" && name == "FsStorage" + ))); + assert!(!errors.iter().any(|error| matches!( + error, + TypeError::AmbiguousServiceCall { + caller, + service_call + } if caller == "run" && service_call == "FsStorage.read" + ))); + } + #[test] fn relaxed_mode_allows_unresolved_service_call() { let graph = module_graph_from_sources(&[( From 462758c4939eb32477e8f0889646af3b29785040 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 10:06:03 +0000 Subject: [PATCH 085/169] Harden resource-type ambiguity layering across strictness modes Co-authored-by: Brian Searls --- .../daglang-cli/tests/compile_commands.rs | 140 +++++++++++++ core/daglang/daglang-typecheck/src/lib.rs | 192 +++++++++++++++++- 2 files changed, 328 insertions(+), 4 deletions(-) diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index 4e623c0636f..14fdb0651d2 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -258,6 +258,43 @@ func run() -> { ok: Bool } uses fs: Storage uses fs: Storage { return { ok: true std::fs::remove_file(fixture).expect("failed to cleanup fixture"); } +#[test] +fn compile_command_single_file_duplicate_resource_uses_does_not_report_ambiguous_used_type() { + let fixture = unique_temp_file("compile_single_file_duplicate_resource_uses_relaxed"); + std::fs::write( + &fixture, + r#"module sample.single +resource SharedResource {} +resource SharedResource {} +func run() -> { ok: Bool } uses fs: SharedResource { return { ok: true } } +"#, + ) + .expect("failed to write fixture"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&fixture) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile for duplicate-resource-uses fixture"); + + assert!( + !output.status.success(), + "single-file compile should fail on duplicate resource definition" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("typecheck errors")); + assert!(stderr.contains( + "duplicate definition `SharedResource` in module `sample.single`" + )); + assert!( + !stderr.contains("ambiguous used resource type `SharedResource`"), + "single-file relaxed mode should suppress ambiguous used resource diagnostics: {stderr}" + ); + + std::fs::remove_file(fixture).expect("failed to cleanup fixture"); +} + #[test] fn compile_command_single_file_fails_on_duplicate_provides_binding() { let fixture = unique_temp_file("compile_single_file_duplicate_provides_binding"); @@ -288,6 +325,43 @@ func run() -> { ok: Bool } provides out: Storage provides out: Storage { return std::fs::remove_file(fixture).expect("failed to cleanup fixture"); } +#[test] +fn compile_command_single_file_duplicate_resource_provides_does_not_report_ambiguous_provided_type() { + let fixture = unique_temp_file("compile_single_file_duplicate_resource_provides_relaxed"); + std::fs::write( + &fixture, + r#"module sample.single +resource SharedResource {} +resource SharedResource {} +func run() -> { ok: Bool } provides out: SharedResource { return { ok: true } } +"#, + ) + .expect("failed to write fixture"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&fixture) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile for duplicate-resource-provides fixture"); + + assert!( + !output.status.success(), + "single-file compile should fail on duplicate resource definition" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("typecheck errors")); + assert!(stderr.contains( + "duplicate definition `SharedResource` in module `sample.single`" + )); + assert!( + !stderr.contains("ambiguous provided resource type `SharedResource`"), + "single-file relaxed mode should suppress ambiguous provided resource diagnostics: {stderr}" + ); + + std::fs::remove_file(fixture).expect("failed to cleanup fixture"); +} + #[test] fn compile_command_single_file_fails_on_use_provide_binding_conflict() { let fixture = unique_temp_file("compile_single_file_use_provide_binding_conflict"); @@ -1804,6 +1878,39 @@ fn compile_command_directory_mode_fails_on_ambiguous_uses_resource_type() { std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); } +#[test] +fn compile_command_directory_mode_duplicate_resource_uses_also_reports_ambiguous_used_type() { + let root = unique_temp_dir("duplicate_resource_uses_ambiguous"); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp dir"); + std::fs::write( + root.join("sample/main.dag"), + r#"module sample.main +resource SharedResource {} +resource SharedResource {} +func run() -> { ok: Bool } uses fs: SharedResource { return { ok: true } } +"#, + ) + .expect("failed to write source"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&root) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile on directory"); + + assert!( + !output.status.success(), + "directory compile should fail on duplicate resource definitions" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("typecheck errors")); + assert!(stderr.contains("duplicate definition `SharedResource` in module `sample.main`")); + assert!(stderr.contains("ambiguous used resource type `SharedResource`")); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); +} + #[test] fn compile_command_directory_mode_fails_on_duplicate_provides_binding() { let root = unique_temp_dir("duplicate_provides_binding"); @@ -1908,6 +2015,39 @@ fn compile_command_directory_mode_fails_on_ambiguous_provides_resource_type() { std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); } +#[test] +fn compile_command_directory_mode_duplicate_resource_provides_also_reports_ambiguous_provided_type() { + let root = unique_temp_dir("duplicate_resource_provides_ambiguous"); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp dir"); + std::fs::write( + root.join("sample/main.dag"), + r#"module sample.main +resource SharedResource {} +resource SharedResource {} +func run() -> { ok: Bool } provides out: SharedResource { return { ok: true } } +"#, + ) + .expect("failed to write source"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&root) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile on directory"); + + assert!( + !output.status.success(), + "directory compile should fail on duplicate resource definitions" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("typecheck errors")); + assert!(stderr.contains("duplicate definition `SharedResource` in module `sample.main`")); + assert!(stderr.contains("ambiguous provided resource type `SharedResource`")); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); +} + #[test] fn compile_command_reports_call_arity_typecheck_error() { let fixture = unique_temp_file("call_arity"); diff --git a/core/daglang/daglang-typecheck/src/lib.rs b/core/daglang/daglang-typecheck/src/lib.rs index 255c3897dbb..c3b87d861fa 100644 --- a/core/daglang/daglang-typecheck/src/lib.rs +++ b/core/daglang/daglang-typecheck/src/lib.rs @@ -1042,10 +1042,8 @@ fn collect_resource_types(modules: &[ResolvedModule]) -> ResourceTypeRegistry { .short .entry(name.to_string()) .and_modify(|existing| { - if let Some(current) = existing { - if current != &full { - *existing = None; - } + if existing.is_some() { + *existing = None; } }) .or_insert_with(|| Some(full)); @@ -2494,6 +2492,47 @@ service FsStorage implements Storage { ))); } + #[test] + fn strict_mode_duplicate_interface_definition_also_reports_ambiguous_implements() { + let graph = module_graph_from_sources(&[( + "duplicate_interface_definition_strict.dag", + r#"module sample.dup +interface Storage { + capability read { + input { path: String } + output { body: String } + } +} +interface Storage { + capability read { + input { path: String } + output { body: String } + } +} +service FsStorage implements Storage { + operation read(path: String) -> { body: String } +} +"#, + )]); + let errors = typecheck_module_graph_with_options( + graph, + TypecheckOptions { + allow_unresolved_imports: false, + }, + ) + .expect_err("strict mode should fail for duplicate interface definitions"); + assert!(errors.iter().any(|error| matches!( + error, + TypeError::DuplicateDefinition { module, name } + if module == "sample.dup" && name == "Storage" + ))); + assert!(errors.iter().any(|error| matches!( + error, + TypeError::AmbiguousInterface { implementor, interface } + if implementor == "FsStorage" && interface == "Storage" + ))); + } + #[test] fn strict_mode_reports_unresolved_imports() { let graph = module_graph_from_sources(&[( @@ -2588,6 +2627,29 @@ fn run() -> String { helper() }"#, ))); } + #[test] + fn relaxed_mode_duplicate_callable_definition_suppresses_ambiguous_call_target() { + let graph = module_graph_from_sources(&[( + "sample/single.dag", + r#"module sample.single +fn helper() -> String { "a" } +fn helper() -> String { "b" } +fn run() -> String { helper() }"#, + )]); + let errors = typecheck_module_graph(graph) + .expect_err("relaxed mode should still fail for duplicate callable definition"); + assert!(errors.iter().any(|error| matches!( + error, + TypeError::DuplicateDefinition { module, name } + if module == "sample.single" && name == "helper" + ))); + assert!(!errors.iter().any(|error| matches!( + error, + TypeError::AmbiguousCallTarget { caller, callee } + if caller == "run" && callee == "helper" + ))); + } + #[test] fn strict_mode_reports_unresolved_call_target() { let graph = module_graph_from_sources(&[( @@ -3184,6 +3246,39 @@ func run() -> { ok: Bool } uses fs: SharedResource { ))); } + #[test] + fn strict_mode_duplicate_resource_definition_also_reports_ambiguous_used_resource_type() { + let graph = module_graph_from_sources(&[( + "sample/main.dag", + r#"module sample.main +resource SharedResource {} +resource SharedResource {} +func run() -> { ok: Bool } uses fs: SharedResource { + return { ok: true } +}"#, + )]); + let errors = typecheck_module_graph_with_options( + graph, + TypecheckOptions { + allow_unresolved_imports: false, + }, + ) + .expect_err("strict mode should fail for duplicate resource definitions"); + assert!(errors.iter().any(|error| matches!( + error, + TypeError::DuplicateDefinition { module, name } + if module == "sample.main" && name == "SharedResource" + ))); + assert!(errors.iter().any(|error| matches!( + error, + TypeError::AmbiguousUsedResourceType { + item, + binding, + resource_type, + } if item == "run" && binding == "fs" && resource_type == "SharedResource" + ))); + } + #[test] fn relaxed_mode_allows_unknown_used_resource_type() { let graph = module_graph_from_sources(&[( @@ -3194,6 +3289,34 @@ func run() -> { ok: Bool } uses fs: SharedResource { assert_eq!(typed.modules.len(), 1); } + #[test] + fn relaxed_mode_duplicate_resource_definition_suppresses_ambiguous_used_resource_type() { + let graph = module_graph_from_sources(&[( + "sample/single.dag", + r#"module sample.single +resource SharedResource {} +resource SharedResource {} +func run() -> { ok: Bool } uses fs: SharedResource { + return { ok: true } +}"#, + )]); + let errors = typecheck_module_graph(graph) + .expect_err("relaxed mode should still fail for duplicate resource definition"); + assert!(errors.iter().any(|error| matches!( + error, + TypeError::DuplicateDefinition { module, name } + if module == "sample.single" && name == "SharedResource" + ))); + assert!(!errors.iter().any(|error| matches!( + error, + TypeError::AmbiguousUsedResourceType { + item, + binding, + resource_type, + } if item == "run" && binding == "fs" && resource_type == "SharedResource" + ))); + } + #[test] fn strict_mode_reports_unknown_provided_resource_type() { let graph = module_graph_from_sources(&[( @@ -3253,6 +3376,67 @@ func run() -> { ok: Bool } provides out: SharedResource { ))); } + #[test] + fn relaxed_mode_duplicate_resource_definition_suppresses_ambiguous_provided_resource_type() { + let graph = module_graph_from_sources(&[( + "sample/single.dag", + r#"module sample.single +resource SharedResource {} +resource SharedResource {} +func run() -> { ok: Bool } provides out: SharedResource { + return { ok: true } +}"#, + )]); + let errors = typecheck_module_graph(graph) + .expect_err("relaxed mode should still fail for duplicate resource definition"); + assert!(errors.iter().any(|error| matches!( + error, + TypeError::DuplicateDefinition { module, name } + if module == "sample.single" && name == "SharedResource" + ))); + assert!(!errors.iter().any(|error| matches!( + error, + TypeError::AmbiguousProvidedResourceType { + item, + binding, + resource_type, + } if item == "run" && binding == "out" && resource_type == "SharedResource" + ))); + } + + #[test] + fn strict_mode_duplicate_resource_definition_also_reports_ambiguous_provided_resource_type() { + let graph = module_graph_from_sources(&[( + "sample/main.dag", + r#"module sample.main +resource SharedResource {} +resource SharedResource {} +func run() -> { ok: Bool } provides out: SharedResource { + return { ok: true } +}"#, + )]); + let errors = typecheck_module_graph_with_options( + graph, + TypecheckOptions { + allow_unresolved_imports: false, + }, + ) + .expect_err("strict mode should fail for duplicate resource definitions"); + assert!(errors.iter().any(|error| matches!( + error, + TypeError::DuplicateDefinition { module, name } + if module == "sample.main" && name == "SharedResource" + ))); + assert!(errors.iter().any(|error| matches!( + error, + TypeError::AmbiguousProvidedResourceType { + item, + binding, + resource_type, + } if item == "run" && binding == "out" && resource_type == "SharedResource" + ))); + } + #[test] fn duplicate_uses_binding_is_reported() { let graph = module_graph_from_sources(&[( From d64d9ae4f4fc5a1623379bb9431e93bfaf9553b4 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 10:11:15 +0000 Subject: [PATCH 086/169] Assert strict compile diagnostics stay in typecheck stage Co-authored-by: Brian Searls --- .../daglang-cli/tests/compile_commands.rs | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index 14fdb0651d2..e62cc1f6443 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -786,6 +786,10 @@ service FsStorage implements MissingStorage { let stderr = String::from_utf8_lossy(&output.stderr); assert!(stderr.contains("typecheck errors")); assert!(stderr.contains("`FsStorage` references unresolved interface `MissingStorage`")); + assert!( + !stderr.contains("lower error"), + "unresolved interface should fail in typecheck stage: {stderr}" + ); std::fs::remove_file(fixture).expect("failed to cleanup fixture"); } @@ -820,6 +824,10 @@ resource Disk implements MissingStorage { let stderr = String::from_utf8_lossy(&output.stderr); assert!(stderr.contains("typecheck errors")); assert!(stderr.contains("`Disk` references unresolved interface `MissingStorage`")); + assert!( + !stderr.contains("lower error"), + "unresolved interface should fail in typecheck stage: {stderr}" + ); std::fs::remove_file(fixture).expect("failed to cleanup fixture"); } @@ -1798,6 +1806,10 @@ fn compile_command_directory_mode_fails_on_unknown_uses_resource_type() { let stderr = String::from_utf8_lossy(&output.stderr); assert!(stderr.contains("typecheck errors")); assert!(stderr.contains("unknown used resource type `MissingResource`")); + assert!( + !stderr.contains("lower error"), + "unknown uses resource type should fail in typecheck stage: {stderr}" + ); std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); } @@ -1973,6 +1985,10 @@ fn compile_command_directory_mode_fails_on_unknown_provides_resource_type() { let stderr = String::from_utf8_lossy(&output.stderr); assert!(stderr.contains("typecheck errors")); assert!(stderr.contains("unknown provided resource type `MissingResource`")); + assert!( + !stderr.contains("lower error"), + "unknown provides resource type should fail in typecheck stage: {stderr}" + ); std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); } @@ -2790,6 +2806,10 @@ func run(path: String) -> { body: String } { let stderr = String::from_utf8_lossy(&output.stderr); assert!(stderr.contains("typecheck errors")); assert!(stderr.contains("unresolved service call `MissingStorage.read`")); + assert!( + !stderr.contains("lower error"), + "directory unresolved service call should fail before lowering: {stderr}" + ); std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); } From e33457e1588352f1209d8ea0f2339cafc8b0de5e Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 10:14:30 +0000 Subject: [PATCH 087/169] Assert strict ambiguity/unresolved compile errors stay in typecheck Co-authored-by: Brian Searls --- .../daglang-cli/tests/compile_commands.rs | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index e62cc1f6443..89c4286b5d5 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -1264,6 +1264,10 @@ fn compile_command_directory_mode_fails_on_unresolved_imports() { let stderr = String::from_utf8_lossy(&output.stderr); assert!(stderr.contains("typecheck errors")); assert!(stderr.contains("unresolved import")); + assert!( + !stderr.contains("lower error"), + "unresolved imports should fail in typecheck stage: {stderr}" + ); std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); } @@ -1480,6 +1484,10 @@ fn compile_command_directory_mode_fails_on_ambiguous_interface_reference() { let stderr = String::from_utf8_lossy(&output.stderr); assert!(stderr.contains("typecheck errors")); assert!(stderr.contains("ambiguous interface `Storage`")); + assert!( + !stderr.contains("lower error"), + "ambiguous interface references should fail in typecheck stage: {stderr}" + ); std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); } @@ -1518,6 +1526,10 @@ fn compile_command_directory_mode_fails_on_ambiguous_resource_interface_referenc let stderr = String::from_utf8_lossy(&output.stderr); assert!(stderr.contains("typecheck errors")); assert!(stderr.contains("`Disk` references ambiguous interface `Storage`")); + assert!( + !stderr.contains("lower error"), + "ambiguous interface references should fail in typecheck stage: {stderr}" + ); std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); } @@ -1546,6 +1558,10 @@ fn compile_command_directory_mode_fails_on_unresolved_interface_reference() { let stderr = String::from_utf8_lossy(&output.stderr); assert!(stderr.contains("typecheck errors")); assert!(stderr.contains("references unresolved interface `MissingStorage`")); + assert!( + !stderr.contains("lower error"), + "unresolved interface references should fail in typecheck stage: {stderr}" + ); std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); } @@ -1574,6 +1590,10 @@ fn compile_command_directory_mode_fails_on_unresolved_resource_interface_referen let stderr = String::from_utf8_lossy(&output.stderr); assert!(stderr.contains("typecheck errors")); assert!(stderr.contains("`Disk` references unresolved interface `MissingStorage`")); + assert!( + !stderr.contains("lower error"), + "unresolved interface references should fail in typecheck stage: {stderr}" + ); std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); } @@ -1886,6 +1906,10 @@ fn compile_command_directory_mode_fails_on_ambiguous_uses_resource_type() { let stderr = String::from_utf8_lossy(&output.stderr); assert!(stderr.contains("typecheck errors")); assert!(stderr.contains("ambiguous used resource type `SharedResource`")); + assert!( + !stderr.contains("lower error"), + "ambiguous uses resource types should fail in typecheck stage: {stderr}" + ); std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); } @@ -2027,6 +2051,10 @@ fn compile_command_directory_mode_fails_on_ambiguous_provides_resource_type() { let stderr = String::from_utf8_lossy(&output.stderr); assert!(stderr.contains("typecheck errors")); assert!(stderr.contains("ambiguous provided resource type `SharedResource`")); + assert!( + !stderr.contains("lower error"), + "ambiguous provides resource types should fail in typecheck stage: {stderr}" + ); std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); } @@ -2858,6 +2886,10 @@ func run(path: String) -> { body: String } { let stderr = String::from_utf8_lossy(&output.stderr); assert!(stderr.contains("typecheck errors")); assert!(stderr.contains("ambiguous service call `SharedService.read`")); + assert!( + !stderr.contains("lower error"), + "ambiguous service calls should fail in typecheck stage: {stderr}" + ); std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); } @@ -2896,6 +2928,10 @@ fn compile_command_directory_mode_fails_on_ambiguous_callable_target() { let stderr = String::from_utf8_lossy(&output.stderr); assert!(stderr.contains("typecheck errors")); assert!(stderr.contains("ambiguous call target `render`")); + assert!( + !stderr.contains("lower error"), + "ambiguous callable targets should fail in typecheck stage: {stderr}" + ); std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); } @@ -2957,6 +2993,10 @@ fn compile_command_directory_mode_fails_on_unresolved_callable_target() { let stderr = String::from_utf8_lossy(&output.stderr); assert!(stderr.contains("typecheck errors")); assert!(stderr.contains("unresolved call target `missing`")); + assert!( + !stderr.contains("lower error"), + "unresolved callable targets should fail in typecheck stage: {stderr}" + ); std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); } From 8b2560e1eafc560108fc1ecfd61253fd6b2dac5f Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 10:17:32 +0000 Subject: [PATCH 088/169] Assert duplicate-layer strict diagnostics stay in typecheck Co-authored-by: Brian Searls --- .../daglang-cli/tests/compile_commands.rs | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index 89c4286b5d5..d38be851def 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -1340,6 +1340,10 @@ service FsStorage implements Storage { assert!(stderr.contains("typecheck errors")); assert!(stderr.contains("duplicate definition `Storage` in module `sample.main`")); assert!(stderr.contains("`FsStorage` references ambiguous interface `Storage`")); + assert!( + !stderr.contains("lower error"), + "duplicate-interface layering should fail in typecheck stage: {stderr}" + ); std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); } @@ -1386,6 +1390,10 @@ func run(path: String) -> { body: String } { assert!(stderr.contains("typecheck errors")); assert!(stderr.contains("duplicate definition `FsStorage` in module `sample.main`")); assert!(stderr.contains("ambiguous service call `FsStorage.read` in `run`")); + assert!( + !stderr.contains("lower error"), + "duplicate-service layering should fail in typecheck stage: {stderr}" + ); std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); } @@ -1943,6 +1951,10 @@ func run() -> { ok: Bool } uses fs: SharedResource { return { ok: true } } assert!(stderr.contains("typecheck errors")); assert!(stderr.contains("duplicate definition `SharedResource` in module `sample.main`")); assert!(stderr.contains("ambiguous used resource type `SharedResource`")); + assert!( + !stderr.contains("lower error"), + "duplicate-resource uses layering should fail in typecheck stage: {stderr}" + ); std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); } @@ -2088,6 +2100,10 @@ func run() -> { ok: Bool } provides out: SharedResource { return { ok: true } } assert!(stderr.contains("typecheck errors")); assert!(stderr.contains("duplicate definition `SharedResource` in module `sample.main`")); assert!(stderr.contains("ambiguous provided resource type `SharedResource`")); + assert!( + !stderr.contains("lower error"), + "duplicate-resource provides layering should fail in typecheck stage: {stderr}" + ); std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); } @@ -2965,6 +2981,10 @@ fn run() -> String { helper() } assert!(stderr.contains("typecheck errors")); assert!(stderr.contains("duplicate definition `helper` in module `sample.main`")); assert!(stderr.contains("ambiguous call target `helper` in `run`")); + assert!( + !stderr.contains("lower error"), + "duplicate-callable layering should fail in typecheck stage: {stderr}" + ); std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); } From 26822603fa0a958d96f288f8075bcd105b73b528 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 10:21:06 +0000 Subject: [PATCH 089/169] Assert relaxed compile success paths emit no stage errors Co-authored-by: Brian Searls --- .../daglang-cli/tests/compile_commands.rs | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index d38be851def..59b58093c50 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -695,6 +695,15 @@ fn run() -> Unit {} "single-file compile should tolerate unresolved imports: {}", String::from_utf8_lossy(&output.stderr) ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + !stderr.contains("typecheck errors"), + "relaxed unresolved-import path should not emit typecheck failures: {stderr}" + ); + assert!( + !stderr.contains("lower error"), + "relaxed unresolved-import path should not emit lower-stage failures: {stderr}" + ); let stdout = String::from_utf8_lossy(&output.stdout); assert!(stdout.contains("Compiled 1 module(s)")); @@ -724,6 +733,15 @@ fn run() -> Unit { let x = 42 } "single-file compile should allow missing tail for Unit return: {}", String::from_utf8_lossy(&output.stderr) ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + !stderr.contains("typecheck errors"), + "Unit-return success path should not emit typecheck failures: {stderr}" + ); + assert!( + !stderr.contains("lower error"), + "Unit-return success path should not emit lower-stage failures: {stderr}" + ); let stdout = String::from_utf8_lossy(&output.stdout); assert!(stdout.contains("Compiled 1 module(s)")); @@ -753,6 +771,15 @@ fn run() -> String { missing(value: "ok") } "single-file compile should tolerate unresolved call targets: {}", String::from_utf8_lossy(&output.stderr) ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + !stderr.contains("typecheck errors"), + "relaxed unresolved-call path should not emit typecheck failures: {stderr}" + ); + assert!( + !stderr.contains("lower error"), + "relaxed unresolved-call path should not emit lower-stage failures: {stderr}" + ); let stdout = String::from_utf8_lossy(&output.stdout); assert!(stdout.contains("Compiled 1 module(s)")); @@ -3099,6 +3126,15 @@ fn compile_command_directory_mode_allows_unit_return_without_tail_expression() { "directory compile should allow missing tail for Unit return: {}", String::from_utf8_lossy(&output.stderr) ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + !stderr.contains("typecheck errors"), + "Unit-return success path should not emit typecheck failures: {stderr}" + ); + assert!( + !stderr.contains("lower error"), + "Unit-return success path should not emit lower-stage failures: {stderr}" + ); let stdout = String::from_utf8_lossy(&output.stdout); assert!(stdout.contains("Compiled 1 module(s)")); From 676136605fd205335fa1de44a465d27817167229 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 10:22:56 +0000 Subject: [PATCH 090/169] Assert relaxed duplicate paths stay out of lowering Co-authored-by: Brian Searls --- .../daglang-cli/tests/compile_commands.rs | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index 59b58093c50..e877fb6b4a3 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -118,6 +118,10 @@ fn run() -> String { helper() } !stderr.contains("ambiguous call target `helper`"), "single-file relaxed mode should not report ambiguous call-target diagnostics: {stderr}" ); + assert!( + !stderr.contains("lower error"), + "single-file relaxed duplicate-callable path should not fail in lowering: {stderr}" + ); std::fs::remove_file(fixture).expect("failed to cleanup fixture"); } @@ -166,6 +170,10 @@ func run(path: String) -> { body: String } { !stderr.contains("ambiguous service call `FsStorage.read`"), "single-file relaxed mode should not report ambiguous service-call diagnostics: {stderr}" ); + assert!( + !stderr.contains("lower error"), + "single-file relaxed duplicate-service path should not fail in lowering: {stderr}" + ); std::fs::remove_file(fixture).expect("failed to cleanup fixture"); } @@ -291,6 +299,10 @@ func run() -> { ok: Bool } uses fs: SharedResource { return { ok: true } } !stderr.contains("ambiguous used resource type `SharedResource`"), "single-file relaxed mode should suppress ambiguous used resource diagnostics: {stderr}" ); + assert!( + !stderr.contains("lower error"), + "single-file relaxed duplicate-resource uses path should not fail in lowering: {stderr}" + ); std::fs::remove_file(fixture).expect("failed to cleanup fixture"); } @@ -358,6 +370,10 @@ func run() -> { ok: Bool } provides out: SharedResource { return { ok: true } } !stderr.contains("ambiguous provided resource type `SharedResource`"), "single-file relaxed mode should suppress ambiguous provided resource diagnostics: {stderr}" ); + assert!( + !stderr.contains("lower error"), + "single-file relaxed duplicate-resource provides path should not fail in lowering: {stderr}" + ); std::fs::remove_file(fixture).expect("failed to cleanup fixture"); } @@ -899,6 +915,10 @@ service FsStorage implements Storage { assert!(stderr.contains("typecheck errors")); assert!(stderr.contains("duplicate definition `Storage` in module `sample.single`")); assert!(stderr.contains("`FsStorage` references ambiguous interface `Storage`")); + assert!( + !stderr.contains("lower error"), + "single-file duplicate-interface layering should fail in typecheck stage: {stderr}" + ); std::fs::remove_file(fixture).expect("failed to cleanup fixture"); } From 48e688cb1f5c53608711afe037f48cff7116d89d Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 10:27:51 +0000 Subject: [PATCH 091/169] Refactor single-file typecheck assertions with stage-boundary helper Co-authored-by: Brian Searls --- .../daglang-cli/tests/compile_commands.rs | 54 +++++++++---------- 1 file changed, 25 insertions(+), 29 deletions(-) diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index e877fb6b4a3..31ebdd0b92b 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -38,6 +38,14 @@ fn unique_temp_dir(name: &str) -> PathBuf { )) } +fn assert_typecheck_stage_failure(stderr: &str) { + assert!(stderr.contains("typecheck errors")); + assert!( + !stderr.contains("lower error"), + "expected failure to remain in typecheck stage: {stderr}" + ); +} + #[test] fn compile_command_emits_summary_for_single_file() { let output = Command::new(daglang_bin()) @@ -81,7 +89,7 @@ fn run() -> String { "b" } "single-file compile should fail on duplicate definition" ); let stderr = String::from_utf8_lossy(&output.stderr); - assert!(stderr.contains("typecheck errors")); + assert_typecheck_stage_failure(&stderr); assert!(stderr.contains("duplicate definition `run` in module `sample.single`")); std::fs::remove_file(fixture).expect("failed to cleanup fixture"); @@ -112,7 +120,7 @@ fn run() -> String { helper() } "single-file compile should fail on duplicate callable definition" ); let stderr = String::from_utf8_lossy(&output.stderr); - assert!(stderr.contains("typecheck errors")); + assert_typecheck_stage_failure(&stderr); assert!(stderr.contains("duplicate definition `helper` in module `sample.single`")); assert!( !stderr.contains("ambiguous call target `helper`"), @@ -164,7 +172,7 @@ func run(path: String) -> { body: String } { "single-file compile should fail on duplicate service definition" ); let stderr = String::from_utf8_lossy(&output.stderr); - assert!(stderr.contains("typecheck errors")); + assert_typecheck_stage_failure(&stderr); assert!(stderr.contains("duplicate definition `FsStorage` in module `sample.single`")); assert!( !stderr.contains("ambiguous service call `FsStorage.read`"), @@ -201,7 +209,7 @@ fn run(a: String, a: Int) -> String { a } "single-file compile should fail on duplicate parameter" ); let stderr = String::from_utf8_lossy(&output.stderr); - assert!(stderr.contains("typecheck errors")); + assert_typecheck_stage_failure(&stderr); assert!(stderr.contains("duplicate parameter `a` in `run`")); std::fs::remove_file(fixture).expect("failed to cleanup fixture"); @@ -230,7 +238,7 @@ func run() -> { ok: Bool, ok: String } { return { ok: true } } "single-file compile should fail on duplicate output field" ); let stderr = String::from_utf8_lossy(&output.stderr); - assert!(stderr.contains("typecheck errors")); + assert_typecheck_stage_failure(&stderr); assert!(stderr.contains("duplicate output field `ok` in `run`")); std::fs::remove_file(fixture).expect("failed to cleanup fixture"); @@ -260,7 +268,7 @@ func run() -> { ok: Bool } uses fs: Storage uses fs: Storage { return { ok: true "single-file compile should fail on duplicate uses binding" ); let stderr = String::from_utf8_lossy(&output.stderr); - assert!(stderr.contains("typecheck errors")); + assert_typecheck_stage_failure(&stderr); assert!(stderr.contains("duplicate uses binding `fs` in `run`")); std::fs::remove_file(fixture).expect("failed to cleanup fixture"); @@ -291,7 +299,7 @@ func run() -> { ok: Bool } uses fs: SharedResource { return { ok: true } } "single-file compile should fail on duplicate resource definition" ); let stderr = String::from_utf8_lossy(&output.stderr); - assert!(stderr.contains("typecheck errors")); + assert_typecheck_stage_failure(&stderr); assert!(stderr.contains( "duplicate definition `SharedResource` in module `sample.single`" )); @@ -331,7 +339,7 @@ func run() -> { ok: Bool } provides out: Storage provides out: Storage { return "single-file compile should fail on duplicate provides binding" ); let stderr = String::from_utf8_lossy(&output.stderr); - assert!(stderr.contains("typecheck errors")); + assert_typecheck_stage_failure(&stderr); assert!(stderr.contains("duplicate provides binding `out` in `run`")); std::fs::remove_file(fixture).expect("failed to cleanup fixture"); @@ -362,7 +370,7 @@ func run() -> { ok: Bool } provides out: SharedResource { return { ok: true } } "single-file compile should fail on duplicate resource definition" ); let stderr = String::from_utf8_lossy(&output.stderr); - assert!(stderr.contains("typecheck errors")); + assert_typecheck_stage_failure(&stderr); assert!(stderr.contains( "duplicate definition `SharedResource` in module `sample.single`" )); @@ -402,7 +410,7 @@ func run() -> { ok: Bool } uses io: Storage provides io: Storage { return { ok: "single-file compile should fail on use/provide binding conflict" ); let stderr = String::from_utf8_lossy(&output.stderr); - assert!(stderr.contains("typecheck errors")); + assert_typecheck_stage_failure(&stderr); assert!(stderr.contains("binding `io` is declared in both uses/provides in `run`")); std::fs::remove_file(fixture).expect("failed to cleanup fixture"); @@ -446,7 +454,7 @@ resource Disk implements Storage { "single-file compile should fail on missing resource capability" ); let stderr = String::from_utf8_lossy(&output.stderr); - assert!(stderr.contains("typecheck errors")); + assert_typecheck_stage_failure(&stderr); assert!(stderr.contains("resource `Disk` is missing capability `write` for interface `Storage`")); std::fs::remove_file(fixture).expect("failed to cleanup fixture"); @@ -487,7 +495,7 @@ service FsStorage implements Storage { "single-file compile should fail on missing service operation" ); let stderr = String::from_utf8_lossy(&output.stderr); - assert!(stderr.contains("typecheck errors")); + assert_typecheck_stage_failure(&stderr); assert!(stderr.contains("service `FsStorage` is missing operation `write` for interface `Storage`")); std::fs::remove_file(fixture).expect("failed to cleanup fixture"); @@ -524,7 +532,7 @@ service FsStorage implements Storage { "single-file compile should fail on service signature mismatch" ); let stderr = String::from_utf8_lossy(&output.stderr); - assert!(stderr.contains("typecheck errors")); + assert_typecheck_stage_failure(&stderr); assert!(stderr.contains("`FsStorage` does not match `Storage.read` contract")); assert!(stderr.contains("expected `String` but found `Int`")); @@ -565,7 +573,7 @@ resource Disk implements Storage { "single-file compile should fail on resource signature mismatch" ); let stderr = String::from_utf8_lossy(&output.stderr); - assert!(stderr.contains("typecheck errors")); + assert_typecheck_stage_failure(&stderr); assert!(stderr.contains("`Disk` does not match `Storage.read` contract")); assert!(stderr.contains("expected `String` but found `Int`")); @@ -827,12 +835,8 @@ service FsStorage implements MissingStorage { "single-file compile should fail on unresolved service interface" ); let stderr = String::from_utf8_lossy(&output.stderr); - assert!(stderr.contains("typecheck errors")); + assert_typecheck_stage_failure(&stderr); assert!(stderr.contains("`FsStorage` references unresolved interface `MissingStorage`")); - assert!( - !stderr.contains("lower error"), - "unresolved interface should fail in typecheck stage: {stderr}" - ); std::fs::remove_file(fixture).expect("failed to cleanup fixture"); } @@ -865,12 +869,8 @@ resource Disk implements MissingStorage { "single-file compile should fail on unresolved resource interface" ); let stderr = String::from_utf8_lossy(&output.stderr); - assert!(stderr.contains("typecheck errors")); + assert_typecheck_stage_failure(&stderr); assert!(stderr.contains("`Disk` references unresolved interface `MissingStorage`")); - assert!( - !stderr.contains("lower error"), - "unresolved interface should fail in typecheck stage: {stderr}" - ); std::fs::remove_file(fixture).expect("failed to cleanup fixture"); } @@ -912,13 +912,9 @@ service FsStorage implements Storage { "single-file compile should fail for duplicate interface definitions" ); let stderr = String::from_utf8_lossy(&output.stderr); - assert!(stderr.contains("typecheck errors")); + assert_typecheck_stage_failure(&stderr); assert!(stderr.contains("duplicate definition `Storage` in module `sample.single`")); assert!(stderr.contains("`FsStorage` references ambiguous interface `Storage`")); - assert!( - !stderr.contains("lower error"), - "single-file duplicate-interface layering should fail in typecheck stage: {stderr}" - ); std::fs::remove_file(fixture).expect("failed to cleanup fixture"); } From a2ad18a36dc08f0ae962473ed1d1df28da903432 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 10:33:55 +0000 Subject: [PATCH 092/169] Enforce typecheck-stage helper across compile regressions Co-authored-by: Brian Searls --- .../daglang-cli/tests/compile_commands.rs | 116 +++++++++--------- 1 file changed, 58 insertions(+), 58 deletions(-) diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index 31ebdd0b92b..8e32892df4d 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -1305,7 +1305,7 @@ fn compile_command_directory_mode_fails_on_unresolved_imports() { "directory compile should fail on unresolved imports" ); let stderr = String::from_utf8_lossy(&output.stderr); - assert!(stderr.contains("typecheck errors")); + assert_typecheck_stage_failure(&stderr); assert!(stderr.contains("unresolved import")); assert!( !stderr.contains("lower error"), @@ -1342,7 +1342,7 @@ func run() -> { ok: Bool } { "directory compile should fail on duplicate definitions" ); let stderr = String::from_utf8_lossy(&output.stderr); - assert!(stderr.contains("typecheck errors")); + assert_typecheck_stage_failure(&stderr); assert!(stderr.contains("duplicate definition `run` in module `sample.main`")); std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); @@ -1380,7 +1380,7 @@ service FsStorage implements Storage { "directory compile should fail on duplicate interface definitions" ); let stderr = String::from_utf8_lossy(&output.stderr); - assert!(stderr.contains("typecheck errors")); + assert_typecheck_stage_failure(&stderr); assert!(stderr.contains("duplicate definition `Storage` in module `sample.main`")); assert!(stderr.contains("`FsStorage` references ambiguous interface `Storage`")); assert!( @@ -1430,7 +1430,7 @@ func run(path: String) -> { body: String } { "directory compile should fail on duplicate service definitions" ); let stderr = String::from_utf8_lossy(&output.stderr); - assert!(stderr.contains("typecheck errors")); + assert_typecheck_stage_failure(&stderr); assert!(stderr.contains("duplicate definition `FsStorage` in module `sample.main`")); assert!(stderr.contains("ambiguous service call `FsStorage.read` in `run`")); assert!( @@ -1467,7 +1467,7 @@ func run() -> { ok: Bool, ok: Bool } { "directory compile should fail on duplicate output fields" ); let stderr = String::from_utf8_lossy(&output.stderr); - assert!(stderr.contains("typecheck errors")); + assert_typecheck_stage_failure(&stderr); assert!(stderr.contains("duplicate output field `ok` in `run`")); std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); @@ -1495,7 +1495,7 @@ fn compile_command_directory_mode_fails_on_duplicate_parameters() { "directory compile should fail on duplicate parameters" ); let stderr = String::from_utf8_lossy(&output.stderr); - assert!(stderr.contains("typecheck errors")); + assert_typecheck_stage_failure(&stderr); assert!(stderr.contains("duplicate parameter `a` in `run`")); std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); @@ -1533,7 +1533,7 @@ fn compile_command_directory_mode_fails_on_ambiguous_interface_reference() { "directory compile should fail on ambiguous interface reference" ); let stderr = String::from_utf8_lossy(&output.stderr); - assert!(stderr.contains("typecheck errors")); + assert_typecheck_stage_failure(&stderr); assert!(stderr.contains("ambiguous interface `Storage`")); assert!( !stderr.contains("lower error"), @@ -1575,7 +1575,7 @@ fn compile_command_directory_mode_fails_on_ambiguous_resource_interface_referenc "directory compile should fail on ambiguous resource interface reference" ); let stderr = String::from_utf8_lossy(&output.stderr); - assert!(stderr.contains("typecheck errors")); + assert_typecheck_stage_failure(&stderr); assert!(stderr.contains("`Disk` references ambiguous interface `Storage`")); assert!( !stderr.contains("lower error"), @@ -1607,7 +1607,7 @@ fn compile_command_directory_mode_fails_on_unresolved_interface_reference() { "directory compile should fail on unresolved interface reference" ); let stderr = String::from_utf8_lossy(&output.stderr); - assert!(stderr.contains("typecheck errors")); + assert_typecheck_stage_failure(&stderr); assert!(stderr.contains("references unresolved interface `MissingStorage`")); assert!( !stderr.contains("lower error"), @@ -1639,7 +1639,7 @@ fn compile_command_directory_mode_fails_on_unresolved_resource_interface_referen "directory compile should fail on unresolved resource interface reference" ); let stderr = String::from_utf8_lossy(&output.stderr); - assert!(stderr.contains("typecheck errors")); + assert_typecheck_stage_failure(&stderr); assert!(stderr.contains("`Disk` references unresolved interface `MissingStorage`")); assert!( !stderr.contains("lower error"), @@ -1688,7 +1688,7 @@ resource Disk implements Storage { "directory compile should fail when resource is missing interface capability" ); let stderr = String::from_utf8_lossy(&output.stderr); - assert!(stderr.contains("typecheck errors")); + assert_typecheck_stage_failure(&stderr); assert!(stderr.contains("resource `Disk` is missing capability `write` for interface `Storage`")); std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); @@ -1730,7 +1730,7 @@ service FsStorage implements Storage { "directory compile should fail when service is missing interface operation" ); let stderr = String::from_utf8_lossy(&output.stderr); - assert!(stderr.contains("typecheck errors")); + assert_typecheck_stage_failure(&stderr); assert!(stderr.contains("service `FsStorage` is missing operation `write` for interface `Storage`")); std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); @@ -1768,7 +1768,7 @@ service FsStorage implements Storage { "directory compile should fail on interface signature mismatch" ); let stderr = String::from_utf8_lossy(&output.stderr); - assert!(stderr.contains("typecheck errors")); + assert_typecheck_stage_failure(&stderr); assert!(stderr.contains("`FsStorage` does not match `Storage.read` contract")); std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); @@ -1809,7 +1809,7 @@ resource Disk implements Storage { "directory compile should fail on resource interface signature mismatch" ); let stderr = String::from_utf8_lossy(&output.stderr); - assert!(stderr.contains("typecheck errors")); + assert_typecheck_stage_failure(&stderr); assert!(stderr.contains("`Disk` does not match `Storage.read` contract")); std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); @@ -1847,7 +1847,7 @@ func run() -> { ok: Bool } uses io: Storage provides io: Storage { "directory compile should fail on use/provide binding conflict" ); let stderr = String::from_utf8_lossy(&output.stderr); - assert!(stderr.contains("typecheck errors")); + assert_typecheck_stage_failure(&stderr); assert!(stderr.contains("binding `io` is declared in both uses/provides in `run`")); std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); @@ -1875,7 +1875,7 @@ fn compile_command_directory_mode_fails_on_unknown_uses_resource_type() { "directory compile should fail on unknown uses resource type" ); let stderr = String::from_utf8_lossy(&output.stderr); - assert!(stderr.contains("typecheck errors")); + assert_typecheck_stage_failure(&stderr); assert!(stderr.contains("unknown used resource type `MissingResource`")); assert!( !stderr.contains("lower error"), @@ -1917,7 +1917,7 @@ func run() -> { ok: Bool } uses fs: Storage uses fs: Storage { "directory compile should fail on duplicate uses binding" ); let stderr = String::from_utf8_lossy(&output.stderr); - assert!(stderr.contains("typecheck errors")); + assert_typecheck_stage_failure(&stderr); assert!(stderr.contains("duplicate uses binding `fs` in `run`")); std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); @@ -1955,7 +1955,7 @@ fn compile_command_directory_mode_fails_on_ambiguous_uses_resource_type() { "directory compile should fail on ambiguous uses resource type" ); let stderr = String::from_utf8_lossy(&output.stderr); - assert!(stderr.contains("typecheck errors")); + assert_typecheck_stage_failure(&stderr); assert!(stderr.contains("ambiguous used resource type `SharedResource`")); assert!( !stderr.contains("lower error"), @@ -1991,7 +1991,7 @@ func run() -> { ok: Bool } uses fs: SharedResource { return { ok: true } } "directory compile should fail on duplicate resource definitions" ); let stderr = String::from_utf8_lossy(&output.stderr); - assert!(stderr.contains("typecheck errors")); + assert_typecheck_stage_failure(&stderr); assert!(stderr.contains("duplicate definition `SharedResource` in module `sample.main`")); assert!(stderr.contains("ambiguous used resource type `SharedResource`")); assert!( @@ -2034,7 +2034,7 @@ func run() -> { ok: Bool } provides out: Storage provides out: Storage { "directory compile should fail on duplicate provides binding" ); let stderr = String::from_utf8_lossy(&output.stderr); - assert!(stderr.contains("typecheck errors")); + assert_typecheck_stage_failure(&stderr); assert!(stderr.contains("duplicate provides binding `out` in `run`")); std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); @@ -2062,7 +2062,7 @@ fn compile_command_directory_mode_fails_on_unknown_provides_resource_type() { "directory compile should fail on unknown provides resource type" ); let stderr = String::from_utf8_lossy(&output.stderr); - assert!(stderr.contains("typecheck errors")); + assert_typecheck_stage_failure(&stderr); assert!(stderr.contains("unknown provided resource type `MissingResource`")); assert!( !stderr.contains("lower error"), @@ -2104,7 +2104,7 @@ fn compile_command_directory_mode_fails_on_ambiguous_provides_resource_type() { "directory compile should fail on ambiguous provides resource type" ); let stderr = String::from_utf8_lossy(&output.stderr); - assert!(stderr.contains("typecheck errors")); + assert_typecheck_stage_failure(&stderr); assert!(stderr.contains("ambiguous provided resource type `SharedResource`")); assert!( !stderr.contains("lower error"), @@ -2140,7 +2140,7 @@ func run() -> { ok: Bool } provides out: SharedResource { return { ok: true } } "directory compile should fail on duplicate resource definitions" ); let stderr = String::from_utf8_lossy(&output.stderr); - assert!(stderr.contains("typecheck errors")); + assert_typecheck_stage_failure(&stderr); assert!(stderr.contains("duplicate definition `SharedResource` in module `sample.main`")); assert!(stderr.contains("ambiguous provided resource type `SharedResource`")); assert!( @@ -2175,7 +2175,7 @@ fn run() -> String { fmt() } "compile should fail on call arity mismatch" ); let stderr = String::from_utf8_lossy(&output.stderr); - assert!(stderr.contains("typecheck errors")); + assert_typecheck_stage_failure(&stderr); assert!(stderr.contains("call arity mismatch")); assert!(stderr.contains("fmt")); @@ -2206,7 +2206,7 @@ fn run() -> String { fmt(text: "ok") } "compile should fail on unknown named call argument" ); let stderr = String::from_utf8_lossy(&output.stderr); - assert!(stderr.contains("typecheck errors")); + assert_typecheck_stage_failure(&stderr); assert!(stderr.contains("unknown named argument `text`")); assert!(stderr.contains("call to `fmt`")); @@ -2236,7 +2236,7 @@ fn run(input: MissingType) -> String { "ok" } "compile should fail on undefined type" ); let stderr = String::from_utf8_lossy(&output.stderr); - assert!(stderr.contains("typecheck errors")); + assert_typecheck_stage_failure(&stderr); assert!(stderr.contains("undefined type `MissingType")); std::fs::remove_file(fixture).expect("failed to cleanup fixture"); @@ -2265,7 +2265,7 @@ fn run() -> String { return 42 } "compile should fail on return type mismatch" ); let stderr = String::from_utf8_lossy(&output.stderr); - assert!(stderr.contains("typecheck errors")); + assert_typecheck_stage_failure(&stderr); assert!(stderr.contains("type mismatch: expected `String`, got `Int`")); std::fs::remove_file(fixture).expect("failed to cleanup fixture"); @@ -2294,7 +2294,7 @@ fn run() -> String { 42 } "compile should fail on implicit return type mismatch" ); let stderr = String::from_utf8_lossy(&output.stderr); - assert!(stderr.contains("typecheck errors")); + assert_typecheck_stage_failure(&stderr); assert!(stderr.contains("type mismatch: expected `String`, got `Int`")); std::fs::remove_file(fixture).expect("failed to cleanup fixture"); @@ -2323,7 +2323,7 @@ fn run() -> String { let x = 42 } "compile should fail when fn has no tail expression for non-unit return type" ); let stderr = String::from_utf8_lossy(&output.stderr); - assert!(stderr.contains("typecheck errors")); + assert_typecheck_stage_failure(&stderr); assert!(stderr.contains("type mismatch: expected `String`, got `Unit`")); std::fs::remove_file(fixture).expect("failed to cleanup fixture"); @@ -2355,7 +2355,7 @@ func run() -> { body: String } { "compile should fail on missing record field access" ); let stderr = String::from_utf8_lossy(&output.stderr); - assert!(stderr.contains("typecheck errors")); + assert_typecheck_stage_failure(&stderr); assert!(stderr.contains("type `Record` has no field `missing`")); std::fs::remove_file(fixture).expect("failed to cleanup fixture"); @@ -2385,7 +2385,7 @@ fn run(input: Payload) -> String { input.missing } "compile should fail on missing field access for named record type" ); let stderr = String::from_utf8_lossy(&output.stderr); - assert!(stderr.contains("typecheck errors")); + assert_typecheck_stage_failure(&stderr); assert!(stderr.contains("type `Payload` has no field `missing`")); std::fs::remove_file(fixture).expect("failed to cleanup fixture"); @@ -2414,7 +2414,7 @@ fn run(value: Int @range(min: 5, max: 1)) -> Int { value } "compile should fail on unsatisfiable refinement" ); let stderr = String::from_utf8_lossy(&output.stderr); - assert!(stderr.contains("typecheck errors")); + assert_typecheck_stage_failure(&stderr); assert!(stderr.contains("unsatisfiable refinement on `Int`: range min 5 exceeds max 1")); std::fs::remove_file(fixture).expect("failed to cleanup fixture"); @@ -2443,7 +2443,7 @@ fn run(values: Map) -> Int { 1 } "compile should fail on generic arity mismatch" ); let stderr = String::from_utf8_lossy(&output.stderr); - assert!(stderr.contains("typecheck errors")); + assert_typecheck_stage_failure(&stderr); assert!(stderr.contains("generic arity mismatch for `Map`: expected 2, got 1")); std::fs::remove_file(fixture).expect("failed to cleanup fixture"); @@ -2473,7 +2473,7 @@ fn run(values: Box) -> String { values } "compile should fail on user-defined generic arity mismatch" ); let stderr = String::from_utf8_lossy(&output.stderr); - assert!(stderr.contains("typecheck errors")); + assert_typecheck_stage_failure(&stderr); assert!(stderr.contains("generic arity mismatch for `Box`: expected 1, got 2")); std::fs::remove_file(fixture).expect("failed to cleanup fixture"); @@ -2514,7 +2514,7 @@ func run() -> { ok: Bool } { "compile should fail on service call arity mismatch" ); let stderr = String::from_utf8_lossy(&output.stderr); - assert!(stderr.contains("typecheck errors")); + assert_typecheck_stage_failure(&stderr); assert!(stderr.contains("service call arity mismatch")); assert!(stderr.contains("FsStorage.read")); @@ -2556,7 +2556,7 @@ func run(path: String) -> { body: String } { "compile should fail on unknown named service argument" ); let stderr = String::from_utf8_lossy(&output.stderr); - assert!(stderr.contains("typecheck errors")); + assert_typecheck_stage_failure(&stderr); assert!(stderr.contains("unknown named argument `file`")); assert!(stderr.contains("service call `FsStorage.read`")); @@ -2589,7 +2589,7 @@ fn run() -> String { "compile should fail on duplicate named call argument" ); let stderr = String::from_utf8_lossy(&output.stderr); - assert!(stderr.contains("typecheck errors")); + assert_typecheck_stage_failure(&stderr); assert!(stderr.contains("duplicate named argument `value`")); assert!(stderr.contains("call to `fmt`")); @@ -2631,7 +2631,7 @@ func run(path: String) -> { body: String } { "compile should fail on duplicate named service argument" ); let stderr = String::from_utf8_lossy(&output.stderr); - assert!(stderr.contains("typecheck errors")); + assert_typecheck_stage_failure(&stderr); assert!(stderr.contains("duplicate named argument `path`")); assert!(stderr.contains("service call `FsStorage.read`")); @@ -2663,7 +2663,7 @@ fn run() -> String { fmt() } "directory compile should fail on call arity mismatch" ); let stderr = String::from_utf8_lossy(&output.stderr); - assert!(stderr.contains("typecheck errors")); + assert_typecheck_stage_failure(&stderr); assert!(stderr.contains("call arity mismatch")); assert!(stderr.contains("fmt")); @@ -2695,7 +2695,7 @@ fn run() -> String { fmt(text: "ok") } "directory compile should fail on unknown named call argument" ); let stderr = String::from_utf8_lossy(&output.stderr); - assert!(stderr.contains("typecheck errors")); + assert_typecheck_stage_failure(&stderr); assert!(stderr.contains("unknown named argument `text`")); assert!(stderr.contains("call to `fmt`")); @@ -2729,7 +2729,7 @@ fn run() -> String { "directory compile should fail on duplicate named call argument" ); let stderr = String::from_utf8_lossy(&output.stderr); - assert!(stderr.contains("typecheck errors")); + assert_typecheck_stage_failure(&stderr); assert!(stderr.contains("duplicate named argument `value`")); assert!(stderr.contains("call to `fmt`")); @@ -2772,7 +2772,7 @@ func run() -> { ok: Bool } { "directory compile should fail on service call arity mismatch" ); let stderr = String::from_utf8_lossy(&output.stderr); - assert!(stderr.contains("typecheck errors")); + assert_typecheck_stage_failure(&stderr); assert!(stderr.contains("service call arity mismatch")); assert!(stderr.contains("FsStorage.read")); @@ -2815,7 +2815,7 @@ func run(path: String) -> { body: String } { "directory compile should fail on unknown named service argument" ); let stderr = String::from_utf8_lossy(&output.stderr); - assert!(stderr.contains("typecheck errors")); + assert_typecheck_stage_failure(&stderr); assert!(stderr.contains("unknown named argument `file`")); assert!(stderr.contains("service call `FsStorage.read`")); @@ -2858,7 +2858,7 @@ func run(path: String) -> { body: String } { "directory compile should fail on duplicate named service argument" ); let stderr = String::from_utf8_lossy(&output.stderr); - assert!(stderr.contains("typecheck errors")); + assert_typecheck_stage_failure(&stderr); assert!(stderr.contains("duplicate named argument `path`")); assert!(stderr.contains("service call `FsStorage.read`")); @@ -2891,7 +2891,7 @@ func run(path: String) -> { body: String } { "directory compile should fail on unresolved service call" ); let stderr = String::from_utf8_lossy(&output.stderr); - assert!(stderr.contains("typecheck errors")); + assert_typecheck_stage_failure(&stderr); assert!(stderr.contains("unresolved service call `MissingStorage.read`")); assert!( !stderr.contains("lower error"), @@ -2943,7 +2943,7 @@ func run(path: String) -> { body: String } { "directory compile should fail on ambiguous service call" ); let stderr = String::from_utf8_lossy(&output.stderr); - assert!(stderr.contains("typecheck errors")); + assert_typecheck_stage_failure(&stderr); assert!(stderr.contains("ambiguous service call `SharedService.read`")); assert!( !stderr.contains("lower error"), @@ -2985,7 +2985,7 @@ fn compile_command_directory_mode_fails_on_ambiguous_callable_target() { "directory compile should fail on ambiguous callable target" ); let stderr = String::from_utf8_lossy(&output.stderr); - assert!(stderr.contains("typecheck errors")); + assert_typecheck_stage_failure(&stderr); assert!(stderr.contains("ambiguous call target `render`")); assert!( !stderr.contains("lower error"), @@ -3021,7 +3021,7 @@ fn run() -> String { helper() } "directory compile should fail on duplicate callable definitions" ); let stderr = String::from_utf8_lossy(&output.stderr); - assert!(stderr.contains("typecheck errors")); + assert_typecheck_stage_failure(&stderr); assert!(stderr.contains("duplicate definition `helper` in module `sample.main`")); assert!(stderr.contains("ambiguous call target `helper` in `run`")); assert!( @@ -3054,7 +3054,7 @@ fn compile_command_directory_mode_fails_on_unresolved_callable_target() { "directory compile should fail on unresolved callable target" ); let stderr = String::from_utf8_lossy(&output.stderr); - assert!(stderr.contains("typecheck errors")); + assert_typecheck_stage_failure(&stderr); assert!(stderr.contains("unresolved call target `missing`")); assert!( !stderr.contains("lower error"), @@ -3086,7 +3086,7 @@ fn compile_command_directory_mode_fails_on_type_mismatch() { "directory compile should fail on return type mismatch" ); let stderr = String::from_utf8_lossy(&output.stderr); - assert!(stderr.contains("typecheck errors")); + assert_typecheck_stage_failure(&stderr); assert!(stderr.contains("type mismatch: expected `String`, got `Int`")); std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); @@ -3114,7 +3114,7 @@ fn compile_command_directory_mode_fails_on_implicit_return_type_mismatch() { "directory compile should fail on implicit return type mismatch" ); let stderr = String::from_utf8_lossy(&output.stderr); - assert!(stderr.contains("typecheck errors")); + assert_typecheck_stage_failure(&stderr); assert!(stderr.contains("type mismatch: expected `String`, got `Int`")); std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); @@ -3179,7 +3179,7 @@ fn compile_command_directory_mode_fails_on_missing_tail_expression_type_mismatch "directory compile should fail when fn has no tail expression for non-unit return type" ); let stderr = String::from_utf8_lossy(&output.stderr); - assert!(stderr.contains("typecheck errors")); + assert_typecheck_stage_failure(&stderr); assert!(stderr.contains("type mismatch: expected `String`, got `Unit`")); std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); @@ -3212,7 +3212,7 @@ func run() -> { body: String } { "directory compile should fail on missing field access for record literal" ); let stderr = String::from_utf8_lossy(&output.stderr); - assert!(stderr.contains("typecheck errors")); + assert_typecheck_stage_failure(&stderr); assert!(stderr.contains("type `Record` has no field `missing`")); std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); @@ -3243,7 +3243,7 @@ fn run(input: Payload) -> String { input.missing } "directory compile should fail on missing field for named record type" ); let stderr = String::from_utf8_lossy(&output.stderr); - assert!(stderr.contains("typecheck errors")); + assert_typecheck_stage_failure(&stderr); assert!(stderr.contains("type `Payload` has no field `missing`")); std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); @@ -3273,7 +3273,7 @@ fn run(values: Map) -> Int { 1 } "directory compile should fail on generic arity mismatch" ); let stderr = String::from_utf8_lossy(&output.stderr); - assert!(stderr.contains("typecheck errors")); + assert_typecheck_stage_failure(&stderr); assert!(stderr.contains("generic arity mismatch for `Map`: expected 2, got 1")); std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); @@ -3301,7 +3301,7 @@ fn compile_command_directory_mode_fails_on_unsatisfiable_refinement() { "directory compile should fail on unsatisfiable refinement" ); let stderr = String::from_utf8_lossy(&output.stderr); - assert!(stderr.contains("typecheck errors")); + assert_typecheck_stage_failure(&stderr); assert!(stderr.contains("unsatisfiable refinement on `Int`: range min 9 exceeds max 1")); std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); @@ -3331,7 +3331,7 @@ fn run(input: MissingType) -> String { "ok" } "directory compile should fail on undefined type" ); let stderr = String::from_utf8_lossy(&output.stderr); - assert!(stderr.contains("typecheck errors")); + assert_typecheck_stage_failure(&stderr); assert!(stderr.contains("undefined type `MissingType")); std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); @@ -3362,7 +3362,7 @@ fn run(value: Box) -> String { value } "directory compile should fail on user-defined generic arity mismatch" ); let stderr = String::from_utf8_lossy(&output.stderr); - assert!(stderr.contains("typecheck errors")); + assert_typecheck_stage_failure(&stderr); assert!(stderr.contains("generic arity mismatch for `Box`: expected 1, got 2")); std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); From b006152bb25f89061a84f9ee0d1bc0a0a74b8bc5 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 10:37:02 +0000 Subject: [PATCH 093/169] Unify lower-stage boundary assertions in compile regressions Co-authored-by: Brian Searls --- .../daglang-cli/tests/compile_commands.rs | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index 8e32892df4d..52b53a63b06 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -46,6 +46,14 @@ fn assert_typecheck_stage_failure(stderr: &str) { ); } +fn assert_lower_stage_failure(stderr: &str) { + assert!(stderr.contains("lower error")); + assert!( + !stderr.contains("typecheck errors"), + "expected failure to remain in lowering stage: {stderr}" + ); +} + #[test] fn compile_command_emits_summary_for_single_file() { let output = Command::new(daglang_bin()) @@ -615,10 +623,7 @@ func run(path: String) -> { body: String } { "single-file compile should fail for unresolved service call" ); let stderr = String::from_utf8_lossy(&output.stderr); - assert!( - !stderr.contains("typecheck errors"), - "single-file unresolved service-call path should fail in lower stage: {stderr}" - ); + assert_lower_stage_failure(&stderr); assert!(stderr.contains("lower error: unresolved service call")); assert!(stderr.contains("MissingStorage.read")); @@ -650,10 +655,7 @@ func run() -> { ok: Bool } uses fs: MissingResource { "single-file compile should fail for unresolved uses binding" ); let stderr = String::from_utf8_lossy(&output.stderr); - assert!( - !stderr.contains("typecheck errors"), - "single-file unresolved uses path should fail in lower stage: {stderr}" - ); + assert_lower_stage_failure(&stderr); assert!(stderr.contains("lower error: unresolved used resource")); assert!(stderr.contains("fs: MissingResource")); @@ -685,10 +687,7 @@ func run() -> { ok: Bool } provides out: MissingResource { "single-file compile should fail for unresolved provides binding" ); let stderr = String::from_utf8_lossy(&output.stderr); - assert!( - !stderr.contains("typecheck errors"), - "single-file unresolved provides path should fail in lower stage: {stderr}" - ); + assert_lower_stage_failure(&stderr); assert!(stderr.contains("lower error: unresolved provided resource")); assert!(stderr.contains("out: MissingResource")); @@ -1171,6 +1170,7 @@ func run(path: String) -> { body: String } { "expand should fail when service call endpoint cannot be resolved" ); let stderr = String::from_utf8_lossy(&output.stderr); + assert_lower_stage_failure(&stderr); assert!(stderr.contains("lower error: unresolved service call")); assert!(stderr.contains("MissingStorage.read")); @@ -1202,6 +1202,7 @@ func run() -> { ok: Bool } uses fs: MissingResource { "expand should fail when uses target cannot be resolved" ); let stderr = String::from_utf8_lossy(&output.stderr); + assert_lower_stage_failure(&stderr); assert!(stderr.contains("lower error: unresolved used resource")); assert!(stderr.contains("fs: MissingResource")); @@ -1233,6 +1234,7 @@ func run() -> { ok: Bool } provides out: MissingResource { "expand should fail when provides target cannot be resolved" ); let stderr = String::from_utf8_lossy(&output.stderr); + assert_lower_stage_failure(&stderr); assert!(stderr.contains("lower error: unresolved provided resource")); assert!(stderr.contains("out: MissingResource")); From f1ca8a7514805a600cbe272d64ff42f0f5ecc4cf Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 10:38:54 +0000 Subject: [PATCH 094/169] Harden successful CLI paths against stage-error banners Co-authored-by: Brian Searls --- .../daglang-cli/tests/compile_commands.rs | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index 52b53a63b06..acedb3b8a17 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -54,6 +54,17 @@ fn assert_lower_stage_failure(stderr: &str) { ); } +fn assert_no_stage_failures(stderr: &str) { + assert!( + !stderr.contains("typecheck errors"), + "did not expect typecheck-stage banner in successful path: {stderr}" + ); + assert!( + !stderr.contains("lower error"), + "did not expect lowering-stage banner in successful path: {stderr}" + ); +} + #[test] fn compile_command_emits_summary_for_single_file() { let output = Command::new(daglang_bin()) @@ -68,6 +79,8 @@ fn compile_command_emits_summary_for_single_file() { "compile command failed: {}", String::from_utf8_lossy(&output.stderr) ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert_no_stage_failures(&stderr); let stdout = String::from_utf8_lossy(&output.stdout); assert!(stdout.contains("Compiled 1 module(s)")); assert!(stdout.contains("target/generated/rust/main.rs")); @@ -932,6 +945,8 @@ fn expand_command_shows_lowered_nodes_and_edges() { "expand command failed: {}", String::from_utf8_lossy(&output.stderr) ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert_no_stage_failures(&stderr); let stdout = String::from_utf8_lossy(&output.stdout); assert!(stdout.contains("Nodes:")); assert!(stdout.contains("tools.makegen::render_makefile")); @@ -952,6 +967,8 @@ fn manifest_command_shows_derived_progress_manifest() { "manifest command failed: {}", String::from_utf8_lossy(&output.stderr) ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert_no_stage_failures(&stderr); let stdout = String::from_utf8_lossy(&output.stdout); assert!(stdout.contains("ProgressManifest:")); assert!(stdout.contains("total_nodes:")); @@ -1005,6 +1022,8 @@ func run(path: String) -> { body: String } { "manifest command failed: {}", String::from_utf8_lossy(&output.stderr) ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert_no_stage_failures(&stderr); let stdout = String::from_utf8_lossy(&output.stdout); assert!(stdout.contains("service_transport_prepare_targets: 1")); assert!(stdout.contains("service_transport_execute_targets: 1")); @@ -1051,6 +1070,8 @@ func run() -> { body: String } { "manifest command failed: {}", String::from_utf8_lossy(&output.stderr) ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert_no_stage_failures(&stderr); let stdout = String::from_utf8_lossy(&output.stdout); assert!(stdout.contains("service_param_source_targets: 0")); @@ -1088,6 +1109,8 @@ func run() -> { ok: Bool } provides out: Storage { "manifest command failed: {}", String::from_utf8_lossy(&output.stderr) ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert_no_stage_failures(&stderr); let stdout = String::from_utf8_lossy(&output.stdout); assert!(stdout.contains("resource_provide_targets: 1")); assert!(stdout.contains("resource_release_targets: 0")); @@ -1109,6 +1132,8 @@ fn viz_command_renders_mermaid_for_compiled_file() { "viz command failed: {}", String::from_utf8_lossy(&output.stderr) ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert_no_stage_failures(&stderr); let stdout = String::from_utf8_lossy(&output.stdout); assert!(stdout.contains("flowchart TB")); assert!(stdout.contains("tools.makegen::render_makefile")); @@ -1276,6 +1301,8 @@ func run(path: String) -> { body: String } { "expand command failed: {}", String::from_utf8_lossy(&output.stderr) ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert_no_stage_failures(&stderr); let stdout = String::from_utf8_lossy(&output.stdout); assert!(stdout.contains("param_source_sample_services_run_path")); assert!(stdout.contains( From a9c227f99338066ae07cc38d00ee158514eea324 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 10:40:30 +0000 Subject: [PATCH 095/169] Guard non-stage compile failures from stage-banner leakage Co-authored-by: Brian Searls --- core/daglang/daglang-cli/tests/compile_commands.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index acedb3b8a17..9574af077a1 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -1154,6 +1154,7 @@ fn compile_command_reports_diagnostics_for_invalid_file() { assert!(!output.status.success(), "broken source should fail compile"); let stderr = String::from_utf8_lossy(&output.stderr); + assert_no_stage_failures(&stderr); assert!(stderr.contains("compile diagnostics")); assert!(stderr.contains(":2:")); @@ -3419,6 +3420,7 @@ fn compile_command_directory_mode_reports_module_path_mismatch() { "directory compile should fail on module path mismatch" ); let stderr = String::from_utf8_lossy(&output.stderr); + assert_no_stage_failures(&stderr); assert!(stderr.contains("module path mismatches")); assert!(stderr.contains("declared `wrong.name`")); From f32dfdbfb04093d98a3294507601f9911f3964fb Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 10:42:05 +0000 Subject: [PATCH 096/169] Assert pre-typecheck compile errors avoid stage banners Co-authored-by: Brian Searls --- core/daglang/daglang-cli/src/compile.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/daglang/daglang-cli/src/compile.rs b/core/daglang/daglang-cli/src/compile.rs index d3c0517dead..b1f620bba1e 100644 --- a/core/daglang/daglang-cli/src/compile.rs +++ b/core/daglang/daglang-cli/src/compile.rs @@ -350,6 +350,8 @@ mod tests { let error = compile_from_context(&context).expect_err("compile should fail"); assert!(error.contains("compile diagnostics")); assert!(error.contains(":2:")); + assert!(!error.contains("typecheck errors")); + assert!(!error.contains("lower error")); std::fs::remove_file(broken_file).expect("failed to cleanup broken source"); } @@ -378,6 +380,8 @@ mod tests { let error = compile_from_context(&context).expect_err("compile should fail"); assert!(error.contains("module path mismatches")); assert!(error.contains("main")); + assert!(!error.contains("typecheck errors")); + assert!(!error.contains("lower error")); std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } From b280a3baefdf0fe70e613e9751819cf6e2bb8545 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 10:43:40 +0000 Subject: [PATCH 097/169] Assert check/modules paths avoid compile stage banners Co-authored-by: Brian Searls --- .../daglang/daglang-cli/tests/cli_commands.rs | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/core/daglang/daglang-cli/tests/cli_commands.rs b/core/daglang/daglang-cli/tests/cli_commands.rs index 7dcac4ccc7d..780eb46b4ac 100644 --- a/core/daglang/daglang-cli/tests/cli_commands.rs +++ b/core/daglang/daglang-cli/tests/cli_commands.rs @@ -26,6 +26,17 @@ fn unique_temp_dir(name: &str) -> PathBuf { std::env::temp_dir().join(format!("daglang_cli_{name}_{}_{}", std::process::id(), nanos)) } +fn assert_no_compile_stage_banners(stderr: &str) { + assert!( + !stderr.contains("typecheck errors"), + "unexpected typecheck-stage banner in non-compile command path: {stderr}" + ); + assert!( + !stderr.contains("lower error"), + "unexpected lower-stage banner in non-compile command path: {stderr}" + ); +} + #[test] fn check_command_parses_full_dsl_corpus() { let output = Command::new(daglang_bin()) @@ -40,6 +51,8 @@ fn check_command_parses_full_dsl_corpus() { "check command failed: {}", String::from_utf8_lossy(&output.stderr) ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert_no_compile_stage_banners(&stderr); let stdout = String::from_utf8_lossy(&output.stdout); assert!( stdout.contains("OK: parsed 42 file(s)"), @@ -61,6 +74,8 @@ fn modules_command_prints_module_graph_summary() { "modules command failed: {}", String::from_utf8_lossy(&output.stderr) ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert_no_compile_stage_banners(&stderr); let stdout = String::from_utf8_lossy(&output.stdout); assert!(stdout.contains("Discovered modules:")); assert!(stdout.contains("tools.makegen")); @@ -81,6 +96,8 @@ fn viz_self_renders_pipeline_mermaid() { "viz --self command failed: {}", String::from_utf8_lossy(&output.stderr) ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert_no_compile_stage_banners(&stderr); let stdout = String::from_utf8_lossy(&output.stdout); assert!(stdout.contains("flowchart TB")); assert!(stdout.contains("discover_files")); @@ -102,6 +119,7 @@ fn check_command_reports_file_line_col_for_broken_file() { assert!(!output.status.success(), "broken file should fail check"); let stderr = String::from_utf8_lossy(&output.stderr); + assert_no_compile_stage_banners(&stderr); assert!( stderr.contains(":2:12:"), "expected file:line:col in stderr, got: {stderr}" @@ -131,6 +149,8 @@ fn modules_command_reports_graph_diagnostics_without_failing() { output.status.success(), "modules command should still succeed while reporting diagnostics" ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert_no_compile_stage_banners(&stderr); let stdout = String::from_utf8_lossy(&output.stdout); assert!(stdout.contains("Diagnostics:")); assert!(stdout.contains("unresolved import")); @@ -164,6 +184,8 @@ fn modules_command_reports_cycle_diagnostics_without_failing() { output.status.success(), "modules command should still succeed while reporting cycle diagnostics" ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert_no_compile_stage_banners(&stderr); let stdout = String::from_utf8_lossy(&output.stdout); assert!(stdout.contains("Diagnostics:")); assert!(stdout.contains("cyclic dependencies detected")); @@ -193,6 +215,8 @@ fn check_command_single_file_mode_ignores_sibling_broken_files() { "single-file check should succeed even with sibling broken file: {}", String::from_utf8_lossy(&output.stderr) ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert_no_compile_stage_banners(&stderr); let stdout = String::from_utf8_lossy(&output.stdout); assert!(stdout.contains("OK: parsed 1 file(s)")); @@ -220,6 +244,7 @@ fn check_command_directory_mode_aggregates_multiple_file_diagnostics() { "directory check should fail when multiple files are invalid" ); let stderr = String::from_utf8_lossy(&output.stderr); + assert_no_compile_stage_banners(&stderr); assert!(stderr.contains("broken_a.dag")); assert!(stderr.contains("broken_b.dag")); @@ -239,6 +264,8 @@ fn check_command_defaults_to_workspace_dsl_root() { "default check command should succeed: {}", String::from_utf8_lossy(&output.stderr) ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert_no_compile_stage_banners(&stderr); assert!( String::from_utf8_lossy(&output.stdout).contains("OK: parsed 42 file(s)"), "default check should parse full DSL corpus" @@ -258,6 +285,8 @@ fn modules_command_defaults_to_workspace_dsl_root() { "default modules command should succeed: {}", String::from_utf8_lossy(&output.stderr) ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert_no_compile_stage_banners(&stderr); let stdout = String::from_utf8_lossy(&output.stdout); assert!(stdout.contains("Discovered modules:")); assert!(stdout.contains("std.types")); From 251c0459d995d46bb6f009585b18c9072c3815ca Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 10:45:24 +0000 Subject: [PATCH 098/169] Cover expand and manifest parse-diagnostic stage boundaries Co-authored-by: Brian Searls --- .../daglang-cli/tests/compile_commands.rs | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index 9574af077a1..a6cae5da085 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -1161,6 +1161,50 @@ fn compile_command_reports_diagnostics_for_invalid_file() { std::fs::remove_file(broken).expect("failed to remove temp broken source"); } +#[test] +fn expand_command_reports_diagnostics_for_invalid_file() { + let broken = unique_temp_file("expand_broken"); + std::fs::write(&broken, "module sample.broken\nfn broken( -> String {") + .expect("failed to write broken source"); + + let output = Command::new(daglang_bin()) + .arg("expand") + .arg(&broken) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang expand for broken file"); + + assert!(!output.status.success(), "broken source should fail expand"); + let stderr = String::from_utf8_lossy(&output.stderr); + assert_no_stage_failures(&stderr); + assert!(stderr.contains("compile diagnostics")); + assert!(stderr.contains(":2:")); + + std::fs::remove_file(broken).expect("failed to remove temp broken source"); +} + +#[test] +fn manifest_command_reports_diagnostics_for_invalid_file() { + let broken = unique_temp_file("manifest_broken"); + std::fs::write(&broken, "module sample.broken\nfn broken( -> String {") + .expect("failed to write broken source"); + + let output = Command::new(daglang_bin()) + .arg("manifest") + .arg(&broken) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang manifest for broken file"); + + assert!(!output.status.success(), "broken source should fail manifest"); + let stderr = String::from_utf8_lossy(&output.stderr); + assert_no_stage_failures(&stderr); + assert!(stderr.contains("compile diagnostics")); + assert!(stderr.contains(":2:")); + + std::fs::remove_file(broken).expect("failed to remove temp broken source"); +} + #[test] fn expand_command_reports_unresolved_service_call_lower_error() { let fixture = unique_temp_file("unresolved_service_call"); From 0a6668be267d7280bccb8d54e171cead43c6d802 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 10:47:06 +0000 Subject: [PATCH 099/169] Add expand and manifest directory mismatch regressions Co-authored-by: Brian Searls --- .../daglang-cli/tests/compile_commands.rs | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index a6cae5da085..a5436e2d66d 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -3470,3 +3470,61 @@ fn compile_command_directory_mode_reports_module_path_mismatch() { std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); } + +#[test] +fn expand_command_directory_mode_reports_module_path_mismatch() { + let root = unique_temp_dir("expand_path_mismatch"); + std::fs::create_dir_all(&root).expect("failed to create temp dir"); + std::fs::write( + root.join("main.dag"), + "module wrong.name\nfn run() -> Unit {}", + ) + .expect("failed to write source"); + + let output = Command::new(daglang_bin()) + .arg("expand") + .arg(&root) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang expand on mismatch directory"); + + assert!( + !output.status.success(), + "directory expand should fail on module path mismatch" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert_no_stage_failures(&stderr); + assert!(stderr.contains("module path mismatches")); + assert!(stderr.contains("declared `wrong.name`")); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); +} + +#[test] +fn manifest_command_directory_mode_reports_module_path_mismatch() { + let root = unique_temp_dir("manifest_path_mismatch"); + std::fs::create_dir_all(&root).expect("failed to create temp dir"); + std::fs::write( + root.join("main.dag"), + "module wrong.name\nfn run() -> Unit {}", + ) + .expect("failed to write source"); + + let output = Command::new(daglang_bin()) + .arg("manifest") + .arg(&root) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang manifest on mismatch directory"); + + assert!( + !output.status.success(), + "directory manifest should fail on module path mismatch" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert_no_stage_failures(&stderr); + assert!(stderr.contains("module path mismatches")); + assert!(stderr.contains("declared `wrong.name`")); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); +} From 7c4e42510a917c9ff87f854276817654cfffce76 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 10:51:00 +0000 Subject: [PATCH 100/169] Add compile unit tests for unresolved service stage ownership Co-authored-by: Brian Searls --- core/daglang/daglang-cli/src/compile.rs | 81 +++++++++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/core/daglang/daglang-cli/src/compile.rs b/core/daglang/daglang-cli/src/compile.rs index b1f620bba1e..424c5b2fd13 100644 --- a/core/daglang/daglang-cli/src/compile.rs +++ b/core/daglang/daglang-cli/src/compile.rs @@ -311,6 +311,16 @@ mod tests { )) } + fn assert_typecheck_stage_error(error: &str) { + assert!(error.contains("typecheck errors")); + assert!(!error.contains("lower error")); + } + + fn assert_lower_stage_error(error: &str) { + assert!(error.contains("lower error")); + assert!(!error.contains("typecheck errors")); + } + #[test] fn compile_single_file_makegen_produces_non_empty_outputs() { let file = PathBuf::from(env!("CARGO_MANIFEST_DIR")) @@ -385,4 +395,75 @@ mod tests { std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } + + #[test] + fn compile_single_file_unresolved_service_call_fails_in_lower_stage() { + let fixture = unique_temp_file("single_file_unresolved_service_call"); + std::fs::write( + &fixture, + r#"module sample.services +interface Storage { + capability read { + input { path: String } + output { body: String } + } +} +service FsStorage implements Storage { + operation read(path: String) -> { body: String } +} +func run(path: String) -> { body: String } { + let response = MissingStorage.read(path: path) + return { body: response.body } +} +"#, + ) + .expect("failed to write unresolved service-call fixture"); + + let context = PipelineContext { + roots: vec![fixture.parent().expect("fixture should have parent").to_path_buf()], + target_file: Some(fixture.clone()), + }; + + let error = compile_from_context(&context).expect_err("compile should fail"); + assert_lower_stage_error(&error); + assert!(error.contains("unresolved service call")); + assert!(error.contains("MissingStorage.read")); + + std::fs::remove_file(fixture).expect("failed to cleanup fixture"); + } + + #[test] + fn compile_directory_unresolved_service_call_fails_in_typecheck_stage() { + let root = std::env::temp_dir().join(format!( + "daglang_compile_unresolved_service_dir_{}_{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("system clock should be after unix epoch") + .as_nanos() + )); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp root"); + std::fs::write( + root.join("sample/main.dag"), + r#"module sample.main +func run(path: String) -> { body: String } { + let response = MissingStorage.read(path: path) + return { body: response.body } +} +"#, + ) + .expect("failed to write unresolved service-call source"); + + let context = PipelineContext { + roots: vec![root.clone()], + target_file: None, + }; + + let error = compile_from_context(&context).expect_err("compile should fail"); + assert_typecheck_stage_error(&error); + assert!(error.contains("unresolved service call")); + assert!(error.contains("MissingStorage.read")); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); + } } From 885578f3468d76b5c5bbe44c70b5fba9513b85a8 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 10:52:48 +0000 Subject: [PATCH 101/169] Add compile unit tests for unresolved uses and provides staging Co-authored-by: Brian Searls --- core/daglang/daglang-cli/src/compile.rs | 120 ++++++++++++++++++++++++ 1 file changed, 120 insertions(+) diff --git a/core/daglang/daglang-cli/src/compile.rs b/core/daglang/daglang-cli/src/compile.rs index 424c5b2fd13..ed3ca201522 100644 --- a/core/daglang/daglang-cli/src/compile.rs +++ b/core/daglang/daglang-cli/src/compile.rs @@ -466,4 +466,124 @@ func run(path: String) -> { body: String } { std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } + + #[test] + fn compile_single_file_unresolved_uses_fails_in_lower_stage() { + let fixture = unique_temp_file("single_file_unresolved_uses"); + std::fs::write( + &fixture, + r#"module sample.uses +func run() -> { ok: Bool } uses fs: MissingResource { + return { ok: true } +} +"#, + ) + .expect("failed to write unresolved uses fixture"); + + let context = PipelineContext { + roots: vec![fixture.parent().expect("fixture should have parent").to_path_buf()], + target_file: Some(fixture.clone()), + }; + + let error = compile_from_context(&context).expect_err("compile should fail"); + assert_lower_stage_error(&error); + assert!(error.contains("unresolved used resource")); + assert!(error.contains("MissingResource")); + + std::fs::remove_file(fixture).expect("failed to cleanup fixture"); + } + + #[test] + fn compile_directory_unresolved_uses_fails_in_typecheck_stage() { + let root = std::env::temp_dir().join(format!( + "daglang_compile_unresolved_uses_dir_{}_{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("system clock should be after unix epoch") + .as_nanos() + )); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp root"); + std::fs::write( + root.join("sample/main.dag"), + r#"module sample.main +func run() -> { ok: Bool } uses fs: MissingResource { + return { ok: true } +} +"#, + ) + .expect("failed to write unresolved uses source"); + + let context = PipelineContext { + roots: vec![root.clone()], + target_file: None, + }; + + let error = compile_from_context(&context).expect_err("compile should fail"); + assert_typecheck_stage_error(&error); + assert!(error.contains("unknown used resource type")); + assert!(error.contains("MissingResource")); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); + } + + #[test] + fn compile_single_file_unresolved_provides_fails_in_lower_stage() { + let fixture = unique_temp_file("single_file_unresolved_provides"); + std::fs::write( + &fixture, + r#"module sample.provides +func run() -> { ok: Bool } provides out: MissingResource { + return { ok: true } +} +"#, + ) + .expect("failed to write unresolved provides fixture"); + + let context = PipelineContext { + roots: vec![fixture.parent().expect("fixture should have parent").to_path_buf()], + target_file: Some(fixture.clone()), + }; + + let error = compile_from_context(&context).expect_err("compile should fail"); + assert_lower_stage_error(&error); + assert!(error.contains("unresolved provided resource")); + assert!(error.contains("MissingResource")); + + std::fs::remove_file(fixture).expect("failed to cleanup fixture"); + } + + #[test] + fn compile_directory_unresolved_provides_fails_in_typecheck_stage() { + let root = std::env::temp_dir().join(format!( + "daglang_compile_unresolved_provides_dir_{}_{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("system clock should be after unix epoch") + .as_nanos() + )); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp root"); + std::fs::write( + root.join("sample/main.dag"), + r#"module sample.main +func run() -> { ok: Bool } provides out: MissingResource { + return { ok: true } +} +"#, + ) + .expect("failed to write unresolved provides source"); + + let context = PipelineContext { + roots: vec![root.clone()], + target_file: None, + }; + + let error = compile_from_context(&context).expect_err("compile should fail"); + assert_typecheck_stage_error(&error); + assert!(error.contains("unknown provided resource type")); + assert!(error.contains("MissingResource")); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); + } } From 92238f46cdac228821ef619cb4567bef79281c5a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 10:54:35 +0000 Subject: [PATCH 102/169] Add compile unit tests for unresolved import strictness split Co-authored-by: Brian Searls --- core/daglang/daglang-cli/src/compile.rs | 58 +++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/core/daglang/daglang-cli/src/compile.rs b/core/daglang/daglang-cli/src/compile.rs index ed3ca201522..033674fe62f 100644 --- a/core/daglang/daglang-cli/src/compile.rs +++ b/core/daglang/daglang-cli/src/compile.rs @@ -586,4 +586,62 @@ func run() -> { ok: Bool } provides out: MissingResource { std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } + + #[test] + fn compile_single_file_allows_unresolved_imports_in_relaxed_mode() { + let fixture = unique_temp_file("single_file_unresolved_import"); + std::fs::write( + &fixture, + r#"module sample.single +import missing.dep +fn run() -> Unit {} +"#, + ) + .expect("failed to write unresolved import fixture"); + + let context = PipelineContext { + roots: vec![fixture.parent().expect("fixture should have parent").to_path_buf()], + target_file: Some(fixture.clone()), + }; + + let output = compile_from_context(&context).expect("compile should succeed"); + assert!(!output.lowered_dag.nodes.is_empty()); + assert!(output.derived.manifest.total_nodes > 0); + assert!(!output.emitted.files.is_empty()); + + std::fs::remove_file(fixture).expect("failed to cleanup fixture"); + } + + #[test] + fn compile_directory_unresolved_import_fails_in_typecheck_stage() { + let root = std::env::temp_dir().join(format!( + "daglang_compile_unresolved_import_dir_{}_{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("system clock should be after unix epoch") + .as_nanos() + )); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp root"); + std::fs::write( + root.join("sample/main.dag"), + r#"module sample.main +import missing.dep +fn run() -> Unit {} +"#, + ) + .expect("failed to write unresolved import source"); + + let context = PipelineContext { + roots: vec![root.clone()], + target_file: None, + }; + + let error = compile_from_context(&context).expect_err("compile should fail"); + assert_typecheck_stage_error(&error); + assert!(error.contains("unresolved import")); + assert!(error.contains("missing.dep")); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); + } } From 3b1b52db3ee6a4d88f9c857a66a89009ccaf9d82 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 10:56:18 +0000 Subject: [PATCH 103/169] Add compile unit tests for unresolved callable strictness split Co-authored-by: Brian Searls --- core/daglang/daglang-cli/src/compile.rs | 60 +++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/core/daglang/daglang-cli/src/compile.rs b/core/daglang/daglang-cli/src/compile.rs index 033674fe62f..816a82c6c14 100644 --- a/core/daglang/daglang-cli/src/compile.rs +++ b/core/daglang/daglang-cli/src/compile.rs @@ -644,4 +644,64 @@ fn run() -> Unit {} std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } + + #[test] + fn compile_single_file_allows_unresolved_call_targets_in_relaxed_mode() { + let fixture = unique_temp_file("single_file_unresolved_call_target"); + std::fs::write( + &fixture, + r#"module sample.single +fn run() -> Unit { + missing() +} +"#, + ) + .expect("failed to write unresolved callable fixture"); + + let context = PipelineContext { + roots: vec![fixture.parent().expect("fixture should have parent").to_path_buf()], + target_file: Some(fixture.clone()), + }; + + let output = compile_from_context(&context).expect("compile should succeed"); + assert!(!output.lowered_dag.nodes.is_empty()); + assert!(output.derived.manifest.total_nodes > 0); + assert!(!output.emitted.files.is_empty()); + + std::fs::remove_file(fixture).expect("failed to cleanup fixture"); + } + + #[test] + fn compile_directory_unresolved_call_target_fails_in_typecheck_stage() { + let root = std::env::temp_dir().join(format!( + "daglang_compile_unresolved_call_dir_{}_{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("system clock should be after unix epoch") + .as_nanos() + )); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp root"); + std::fs::write( + root.join("sample/main.dag"), + r#"module sample.main +fn run() -> Unit { + missing() +} +"#, + ) + .expect("failed to write unresolved callable source"); + + let context = PipelineContext { + roots: vec![root.clone()], + target_file: None, + }; + + let error = compile_from_context(&context).expect_err("compile should fail"); + assert_typecheck_stage_error(&error); + assert!(error.contains("unresolved call target")); + assert!(error.contains("missing")); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); + } } From 58453bab1bf255d4116aa8c4edbb595eb07e88be Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 10:58:17 +0000 Subject: [PATCH 104/169] Add compile unit tests for duplicate-service layering strictness Co-authored-by: Brian Searls --- core/daglang/daglang-cli/src/compile.rs | 86 +++++++++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/core/daglang/daglang-cli/src/compile.rs b/core/daglang/daglang-cli/src/compile.rs index 816a82c6c14..c526750d274 100644 --- a/core/daglang/daglang-cli/src/compile.rs +++ b/core/daglang/daglang-cli/src/compile.rs @@ -704,4 +704,90 @@ fn run() -> Unit { std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } + + #[test] + fn compile_single_file_duplicate_service_suppresses_ambiguous_service_call() { + let fixture = unique_temp_file("single_file_duplicate_service"); + std::fs::write( + &fixture, + r#"module sample.single +interface Storage { + capability read { + input { path: String } + output { body: String } + } +} +service FsStorage implements Storage { + operation read(path: String) -> { body: String } +} +service FsStorage implements Storage { + operation read(path: String) -> { body: String } +} +func run(path: String) -> { body: String } { + let response = FsStorage.read(path: path) + return { body: response.body } +} +"#, + ) + .expect("failed to write duplicate service fixture"); + + let context = PipelineContext { + roots: vec![fixture.parent().expect("fixture should have parent").to_path_buf()], + target_file: Some(fixture.clone()), + }; + + let error = compile_from_context(&context).expect_err("compile should fail"); + assert_typecheck_stage_error(&error); + assert!(error.contains("duplicate definition `FsStorage`")); + assert!(!error.contains("ambiguous service call")); + + std::fs::remove_file(fixture).expect("failed to cleanup fixture"); + } + + #[test] + fn compile_directory_duplicate_service_reports_ambiguous_service_call() { + let root = std::env::temp_dir().join(format!( + "daglang_compile_duplicate_service_dir_{}_{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("system clock should be after unix epoch") + .as_nanos() + )); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp root"); + std::fs::write( + root.join("sample/main.dag"), + r#"module sample.main +interface Storage { + capability read { + input { path: String } + output { body: String } + } +} +service FsStorage implements Storage { + operation read(path: String) -> { body: String } +} +service FsStorage implements Storage { + operation read(path: String) -> { body: String } +} +func run(path: String) -> { body: String } { + let response = FsStorage.read(path: path) + return { body: response.body } +} +"#, + ) + .expect("failed to write duplicate service source"); + + let context = PipelineContext { + roots: vec![root.clone()], + target_file: None, + }; + + let error = compile_from_context(&context).expect_err("compile should fail"); + assert_typecheck_stage_error(&error); + assert!(error.contains("duplicate definition `FsStorage`")); + assert!(error.contains("ambiguous service call")); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); + } } From 7bca805e9654aaaaaa2ad791c70096cfa070ac82 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 11:00:16 +0000 Subject: [PATCH 105/169] Add compile unit tests for duplicate-callable layering strictness Co-authored-by: Brian Searls --- core/daglang/daglang-cli/src/compile.rs | 60 +++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/core/daglang/daglang-cli/src/compile.rs b/core/daglang/daglang-cli/src/compile.rs index c526750d274..69382be2f99 100644 --- a/core/daglang/daglang-cli/src/compile.rs +++ b/core/daglang/daglang-cli/src/compile.rs @@ -790,4 +790,64 @@ func run(path: String) -> { body: String } { std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } + + #[test] + fn compile_single_file_duplicate_callable_suppresses_ambiguous_call_target() { + let fixture = unique_temp_file("single_file_duplicate_callable"); + std::fs::write( + &fixture, + r#"module sample.single +fn helper() -> String { "a" } +fn helper() -> String { "b" } +fn run() -> String { helper() } +"#, + ) + .expect("failed to write duplicate callable fixture"); + + let context = PipelineContext { + roots: vec![fixture.parent().expect("fixture should have parent").to_path_buf()], + target_file: Some(fixture.clone()), + }; + + let error = compile_from_context(&context).expect_err("compile should fail"); + assert_typecheck_stage_error(&error); + assert!(error.contains("duplicate definition `helper`")); + assert!(!error.contains("ambiguous call target")); + + std::fs::remove_file(fixture).expect("failed to cleanup fixture"); + } + + #[test] + fn compile_directory_duplicate_callable_reports_ambiguous_call_target() { + let root = std::env::temp_dir().join(format!( + "daglang_compile_duplicate_callable_dir_{}_{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("system clock should be after unix epoch") + .as_nanos() + )); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp root"); + std::fs::write( + root.join("sample/main.dag"), + r#"module sample.main +fn helper() -> String { "a" } +fn helper() -> String { "b" } +fn run() -> String { helper() } +"#, + ) + .expect("failed to write duplicate callable source"); + + let context = PipelineContext { + roots: vec![root.clone()], + target_file: None, + }; + + let error = compile_from_context(&context).expect_err("compile should fail"); + assert_typecheck_stage_error(&error); + assert!(error.contains("duplicate definition `helper`")); + assert!(error.contains("ambiguous call target")); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); + } } From b51b5e5024082f3cf08d462c7d27f6b32be071a7 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 11:02:23 +0000 Subject: [PATCH 106/169] Add compile unit tests for duplicate-resource layering strictness Co-authored-by: Brian Searls --- core/daglang/daglang-cli/src/compile.rs | 120 ++++++++++++++++++++++++ 1 file changed, 120 insertions(+) diff --git a/core/daglang/daglang-cli/src/compile.rs b/core/daglang/daglang-cli/src/compile.rs index 69382be2f99..bb95e77c9fe 100644 --- a/core/daglang/daglang-cli/src/compile.rs +++ b/core/daglang/daglang-cli/src/compile.rs @@ -850,4 +850,124 @@ fn run() -> String { helper() } std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } + + #[test] + fn compile_single_file_duplicate_resource_uses_suppresses_ambiguous_used_type() { + let fixture = unique_temp_file("single_file_duplicate_resource_uses"); + std::fs::write( + &fixture, + r#"module sample.single +resource SharedResource {} +resource SharedResource {} +func run() -> { ok: Bool } uses fs: SharedResource { return { ok: true } } +"#, + ) + .expect("failed to write duplicate resource-uses fixture"); + + let context = PipelineContext { + roots: vec![fixture.parent().expect("fixture should have parent").to_path_buf()], + target_file: Some(fixture.clone()), + }; + + let error = compile_from_context(&context).expect_err("compile should fail"); + assert_typecheck_stage_error(&error); + assert!(error.contains("duplicate definition `SharedResource`")); + assert!(!error.contains("ambiguous used resource type")); + + std::fs::remove_file(fixture).expect("failed to cleanup fixture"); + } + + #[test] + fn compile_directory_duplicate_resource_uses_reports_ambiguous_used_type() { + let root = std::env::temp_dir().join(format!( + "daglang_compile_duplicate_resource_uses_dir_{}_{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("system clock should be after unix epoch") + .as_nanos() + )); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp root"); + std::fs::write( + root.join("sample/main.dag"), + r#"module sample.main +resource SharedResource {} +resource SharedResource {} +func run() -> { ok: Bool } uses fs: SharedResource { return { ok: true } } +"#, + ) + .expect("failed to write duplicate resource-uses source"); + + let context = PipelineContext { + roots: vec![root.clone()], + target_file: None, + }; + + let error = compile_from_context(&context).expect_err("compile should fail"); + assert_typecheck_stage_error(&error); + assert!(error.contains("duplicate definition `SharedResource`")); + assert!(error.contains("ambiguous used resource type")); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); + } + + #[test] + fn compile_single_file_duplicate_resource_provides_suppresses_ambiguous_provided_type() { + let fixture = unique_temp_file("single_file_duplicate_resource_provides"); + std::fs::write( + &fixture, + r#"module sample.single +resource SharedResource {} +resource SharedResource {} +func run() -> { ok: Bool } provides out: SharedResource { return { ok: true } } +"#, + ) + .expect("failed to write duplicate resource-provides fixture"); + + let context = PipelineContext { + roots: vec![fixture.parent().expect("fixture should have parent").to_path_buf()], + target_file: Some(fixture.clone()), + }; + + let error = compile_from_context(&context).expect_err("compile should fail"); + assert_typecheck_stage_error(&error); + assert!(error.contains("duplicate definition `SharedResource`")); + assert!(!error.contains("ambiguous provided resource type")); + + std::fs::remove_file(fixture).expect("failed to cleanup fixture"); + } + + #[test] + fn compile_directory_duplicate_resource_provides_reports_ambiguous_provided_type() { + let root = std::env::temp_dir().join(format!( + "daglang_compile_duplicate_resource_provides_dir_{}_{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("system clock should be after unix epoch") + .as_nanos() + )); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp root"); + std::fs::write( + root.join("sample/main.dag"), + r#"module sample.main +resource SharedResource {} +resource SharedResource {} +func run() -> { ok: Bool } provides out: SharedResource { return { ok: true } } +"#, + ) + .expect("failed to write duplicate resource-provides source"); + + let context = PipelineContext { + roots: vec![root.clone()], + target_file: None, + }; + + let error = compile_from_context(&context).expect_err("compile should fail"); + assert_typecheck_stage_error(&error); + assert!(error.contains("duplicate definition `SharedResource`")); + assert!(error.contains("ambiguous provided resource type")); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); + } } From 52c9d9e2d3f075b135bd288613cd0363988e3e8a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 11:06:31 +0000 Subject: [PATCH 107/169] Add compile unit tests for interface strictness and layering Co-authored-by: Brian Searls --- core/daglang/daglang-cli/src/compile.rs | 206 ++++++++++++++++++++++++ 1 file changed, 206 insertions(+) diff --git a/core/daglang/daglang-cli/src/compile.rs b/core/daglang/daglang-cli/src/compile.rs index bb95e77c9fe..24c11025305 100644 --- a/core/daglang/daglang-cli/src/compile.rs +++ b/core/daglang/daglang-cli/src/compile.rs @@ -970,4 +970,210 @@ func run() -> { ok: Bool } provides out: SharedResource { return { ok: true } } std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } + + #[test] + fn compile_single_file_unresolved_service_interface_fails_in_typecheck_stage() { + let fixture = unique_temp_file("single_file_unresolved_service_interface"); + std::fs::write( + &fixture, + r#"module sample.services +service FsStorage implements MissingStorage { + operation read(path: String) -> { body: String } +} +"#, + ) + .expect("failed to write unresolved service-interface fixture"); + + let context = PipelineContext { + roots: vec![fixture.parent().expect("fixture should have parent").to_path_buf()], + target_file: Some(fixture.clone()), + }; + + let error = compile_from_context(&context).expect_err("compile should fail"); + assert_typecheck_stage_error(&error); + assert!(error.contains("`FsStorage` references unresolved interface `MissingStorage`")); + + std::fs::remove_file(fixture).expect("failed to cleanup fixture"); + } + + #[test] + fn compile_single_file_unresolved_resource_interface_fails_in_typecheck_stage() { + let fixture = unique_temp_file("single_file_unresolved_resource_interface"); + std::fs::write( + &fixture, + r#"module sample.resources +resource Disk implements MissingStorage { + capability read { + input { path: String } + output { body: String } + } +} +"#, + ) + .expect("failed to write unresolved resource-interface fixture"); + + let context = PipelineContext { + roots: vec![fixture.parent().expect("fixture should have parent").to_path_buf()], + target_file: Some(fixture.clone()), + }; + + let error = compile_from_context(&context).expect_err("compile should fail"); + assert_typecheck_stage_error(&error); + assert!(error.contains("`Disk` references unresolved interface `MissingStorage`")); + + std::fs::remove_file(fixture).expect("failed to cleanup fixture"); + } + + #[test] + fn compile_directory_unresolved_service_interface_fails_in_typecheck_stage() { + let root = std::env::temp_dir().join(format!( + "daglang_compile_unresolved_service_interface_dir_{}_{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("system clock should be after unix epoch") + .as_nanos() + )); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp root"); + std::fs::write( + root.join("sample/main.dag"), + r#"module sample.main +service FsStorage implements MissingStorage { + operation read(path: String) -> { body: String } +} +"#, + ) + .expect("failed to write unresolved service-interface source"); + + let context = PipelineContext { + roots: vec![root.clone()], + target_file: None, + }; + + let error = compile_from_context(&context).expect_err("compile should fail"); + assert_typecheck_stage_error(&error); + assert!(error.contains("`FsStorage` references unresolved interface `MissingStorage`")); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); + } + + #[test] + fn compile_directory_unresolved_resource_interface_fails_in_typecheck_stage() { + let root = std::env::temp_dir().join(format!( + "daglang_compile_unresolved_resource_interface_dir_{}_{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("system clock should be after unix epoch") + .as_nanos() + )); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp root"); + std::fs::write( + root.join("sample/main.dag"), + r#"module sample.main +resource Disk implements MissingStorage { + capability read { + input { path: String } + output { body: String } + } +} +"#, + ) + .expect("failed to write unresolved resource-interface source"); + + let context = PipelineContext { + roots: vec![root.clone()], + target_file: None, + }; + + let error = compile_from_context(&context).expect_err("compile should fail"); + assert_typecheck_stage_error(&error); + assert!(error.contains("`Disk` references unresolved interface `MissingStorage`")); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); + } + + #[test] + fn compile_single_file_duplicate_interface_reports_ambiguous_implements() { + let fixture = unique_temp_file("single_file_duplicate_interface"); + std::fs::write( + &fixture, + r#"module sample.single +interface Storage { + capability read { + input { path: String } + output { body: String } + } +} +interface Storage { + capability read { + input { path: String } + output { body: String } + } +} +service FsStorage implements Storage { + operation read(path: String) -> { body: String } +} +"#, + ) + .expect("failed to write duplicate-interface fixture"); + + let context = PipelineContext { + roots: vec![fixture.parent().expect("fixture should have parent").to_path_buf()], + target_file: Some(fixture.clone()), + }; + + let error = compile_from_context(&context).expect_err("compile should fail"); + assert_typecheck_stage_error(&error); + assert!(error.contains("duplicate definition `Storage` in module `sample.single`")); + assert!(error.contains("`FsStorage` references ambiguous interface `Storage`")); + + std::fs::remove_file(fixture).expect("failed to cleanup fixture"); + } + + #[test] + fn compile_directory_duplicate_interface_reports_ambiguous_implements() { + let root = std::env::temp_dir().join(format!( + "daglang_compile_duplicate_interface_dir_{}_{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("system clock should be after unix epoch") + .as_nanos() + )); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp root"); + std::fs::write( + root.join("sample/main.dag"), + r#"module sample.main +interface Storage { + capability read { + input { path: String } + output { body: String } + } +} +interface Storage { + capability read { + input { path: String } + output { body: String } + } +} +service FsStorage implements Storage { + operation read(path: String) -> { body: String } +} +"#, + ) + .expect("failed to write duplicate-interface source"); + + let context = PipelineContext { + roots: vec![root.clone()], + target_file: None, + }; + + let error = compile_from_context(&context).expect_err("compile should fail"); + assert_typecheck_stage_error(&error); + assert!(error.contains("duplicate definition `Storage` in module `sample.main`")); + assert!(error.contains("`FsStorage` references ambiguous interface `Storage`")); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); + } } From 054c8686d159255b0e5961612a6a786d138450f1 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 11:09:07 +0000 Subject: [PATCH 108/169] Add compile unit tests for Unit tail-return strictness Co-authored-by: Brian Searls --- core/daglang/daglang-cli/src/compile.rs | 118 ++++++++++++++++++++++++ 1 file changed, 118 insertions(+) diff --git a/core/daglang/daglang-cli/src/compile.rs b/core/daglang/daglang-cli/src/compile.rs index 24c11025305..6fe364de547 100644 --- a/core/daglang/daglang-cli/src/compile.rs +++ b/core/daglang/daglang-cli/src/compile.rs @@ -1176,4 +1176,122 @@ service FsStorage implements Storage { std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } + + #[test] + fn compile_single_file_unit_return_without_tail_expression_succeeds() { + let fixture = unique_temp_file("single_file_unit_without_tail"); + std::fs::write( + &fixture, + r#"module sample.single +fn run() -> Unit { + let x = 42 +} +"#, + ) + .expect("failed to write Unit-return fixture"); + + let context = PipelineContext { + roots: vec![fixture.parent().expect("fixture should have parent").to_path_buf()], + target_file: Some(fixture.clone()), + }; + + let output = compile_from_context(&context).expect("compile should succeed"); + assert!(!output.lowered_dag.nodes.is_empty()); + assert!(output.derived.manifest.total_nodes > 0); + assert!(!output.emitted.files.is_empty()); + + std::fs::remove_file(fixture).expect("failed to cleanup fixture"); + } + + #[test] + fn compile_directory_unit_return_without_tail_expression_succeeds() { + let root = std::env::temp_dir().join(format!( + "daglang_compile_unit_without_tail_dir_{}_{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("system clock should be after unix epoch") + .as_nanos() + )); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp root"); + std::fs::write( + root.join("sample/main.dag"), + r#"module sample.main +fn run() -> Unit { + let x = 42 +} +"#, + ) + .expect("failed to write Unit-return source"); + + let context = PipelineContext { + roots: vec![root.clone()], + target_file: None, + }; + + let output = compile_from_context(&context).expect("compile should succeed"); + assert!(!output.lowered_dag.nodes.is_empty()); + assert!(output.derived.manifest.total_nodes > 0); + assert!(!output.emitted.files.is_empty()); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); + } + + #[test] + fn compile_single_file_missing_tail_non_unit_fails_in_typecheck_stage() { + let fixture = unique_temp_file("single_file_non_unit_without_tail"); + std::fs::write( + &fixture, + r#"module sample.single +fn run() -> String { + let x = 42 +} +"#, + ) + .expect("failed to write non-Unit return fixture"); + + let context = PipelineContext { + roots: vec![fixture.parent().expect("fixture should have parent").to_path_buf()], + target_file: Some(fixture.clone()), + }; + + let error = compile_from_context(&context).expect_err("compile should fail"); + assert_typecheck_stage_error(&error); + assert!(error.contains("type mismatch: expected `String`, got `Unit`")); + + std::fs::remove_file(fixture).expect("failed to cleanup fixture"); + } + + #[test] + fn compile_directory_missing_tail_non_unit_fails_in_typecheck_stage() { + let root = std::env::temp_dir().join(format!( + "daglang_compile_non_unit_without_tail_dir_{}_{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("system clock should be after unix epoch") + .as_nanos() + )); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp root"); + std::fs::write( + root.join("sample/main.dag"), + r#"module sample.main +fn run() -> String { + let x = 42 +} +"#, + ) + .expect("failed to write non-Unit return source"); + + let context = PipelineContext { + roots: vec![root.clone()], + target_file: None, + }; + + let error = compile_from_context(&context).expect_err("compile should fail"); + assert_typecheck_stage_error(&error); + assert!(error.contains("type mismatch: expected `String`, got `Unit`")); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); + } } From 8bea83e5191186c57b5e2e6060535fa5461472a3 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 11:11:49 +0000 Subject: [PATCH 109/169] Add compile unit tests for call argument diagnostics Co-authored-by: Brian Searls --- core/daglang/daglang-cli/src/compile.rs | 183 ++++++++++++++++++++++++ 1 file changed, 183 insertions(+) diff --git a/core/daglang/daglang-cli/src/compile.rs b/core/daglang/daglang-cli/src/compile.rs index 6fe364de547..156238476c4 100644 --- a/core/daglang/daglang-cli/src/compile.rs +++ b/core/daglang/daglang-cli/src/compile.rs @@ -1294,4 +1294,187 @@ fn run() -> String { std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } + + #[test] + fn compile_single_file_call_arity_mismatch_fails_in_typecheck_stage() { + let fixture = unique_temp_file("single_file_call_arity_mismatch"); + std::fs::write( + &fixture, + r#"module sample.calls +fn fmt(value: String) -> String { value } +fn run() -> String { fmt() } +"#, + ) + .expect("failed to write call-arity fixture"); + + let context = PipelineContext { + roots: vec![fixture.parent().expect("fixture should have parent").to_path_buf()], + target_file: Some(fixture.clone()), + }; + + let error = compile_from_context(&context).expect_err("compile should fail"); + assert_typecheck_stage_error(&error); + assert!(error.contains("call arity mismatch")); + assert!(error.contains("fmt")); + + std::fs::remove_file(fixture).expect("failed to cleanup fixture"); + } + + #[test] + fn compile_single_file_unknown_named_call_argument_fails_in_typecheck_stage() { + let fixture = unique_temp_file("single_file_unknown_named_call_argument"); + std::fs::write( + &fixture, + r#"module sample.calls +fn fmt(value: String) -> String { value } +fn run() -> String { fmt(text: "ok") } +"#, + ) + .expect("failed to write unknown-arg fixture"); + + let context = PipelineContext { + roots: vec![fixture.parent().expect("fixture should have parent").to_path_buf()], + target_file: Some(fixture.clone()), + }; + + let error = compile_from_context(&context).expect_err("compile should fail"); + assert_typecheck_stage_error(&error); + assert!(error.contains("unknown named argument")); + assert!(error.contains("text")); + + std::fs::remove_file(fixture).expect("failed to cleanup fixture"); + } + + #[test] + fn compile_single_file_duplicate_named_call_argument_fails_in_typecheck_stage() { + let fixture = unique_temp_file("single_file_duplicate_named_call_argument"); + std::fs::write( + &fixture, + r#"module sample.calls +fn fmt(value: String) -> String { value } +fn run() -> String { fmt(value: "a", value: "b") } +"#, + ) + .expect("failed to write duplicate-arg fixture"); + + let context = PipelineContext { + roots: vec![fixture.parent().expect("fixture should have parent").to_path_buf()], + target_file: Some(fixture.clone()), + }; + + let error = compile_from_context(&context).expect_err("compile should fail"); + assert_typecheck_stage_error(&error); + assert!(error.contains("duplicate named argument")); + assert!(error.contains("value")); + + std::fs::remove_file(fixture).expect("failed to cleanup fixture"); + } + + #[test] + fn compile_single_file_service_call_arity_mismatch_fails_in_typecheck_stage() { + let fixture = unique_temp_file("single_file_service_call_arity_mismatch"); + std::fs::write( + &fixture, + r#"module sample.services +interface Storage { + capability read { + input { path: String } + output { body: String } + } +} +service FsStorage implements Storage { + operation read(path: String) -> { body: String } +} +func run() -> { body: String } { + let response = FsStorage.read() + return { body: response.body } +} +"#, + ) + .expect("failed to write service call-arity fixture"); + + let context = PipelineContext { + roots: vec![fixture.parent().expect("fixture should have parent").to_path_buf()], + target_file: Some(fixture.clone()), + }; + + let error = compile_from_context(&context).expect_err("compile should fail"); + assert_typecheck_stage_error(&error); + assert!(error.contains("service call arity mismatch")); + assert!(error.contains("FsStorage.read")); + + std::fs::remove_file(fixture).expect("failed to cleanup fixture"); + } + + #[test] + fn compile_single_file_unknown_named_service_argument_fails_in_typecheck_stage() { + let fixture = unique_temp_file("single_file_unknown_named_service_argument"); + std::fs::write( + &fixture, + r#"module sample.services +interface Storage { + capability read { + input { path: String } + output { body: String } + } +} +service FsStorage implements Storage { + operation read(path: String) -> { body: String } +} +func run() -> { body: String } { + let response = FsStorage.read(name: "README.md") + return { body: response.body } +} +"#, + ) + .expect("failed to write unknown service-arg fixture"); + + let context = PipelineContext { + roots: vec![fixture.parent().expect("fixture should have parent").to_path_buf()], + target_file: Some(fixture.clone()), + }; + + let error = compile_from_context(&context).expect_err("compile should fail"); + assert_typecheck_stage_error(&error); + assert!(error.contains("unknown named argument")); + assert!(error.contains("name")); + + std::fs::remove_file(fixture).expect("failed to cleanup fixture"); + } + + #[test] + fn compile_single_file_duplicate_named_service_argument_fails_in_typecheck_stage() { + let fixture = unique_temp_file("single_file_duplicate_named_service_argument"); + std::fs::write( + &fixture, + r#"module sample.services +interface Storage { + capability read { + input { path: String } + output { body: String } + } +} +service FsStorage implements Storage { + operation read(path: String) -> { body: String } +} +func run() -> { body: String } { + let response = FsStorage.read(path: "a", path: "b") + return { body: response.body } +} +"#, + ) + .expect("failed to write duplicate service-arg fixture"); + + let context = PipelineContext { + roots: vec![fixture.parent().expect("fixture should have parent").to_path_buf()], + target_file: Some(fixture.clone()), + }; + + let error = compile_from_context(&context).expect_err("compile should fail"); + assert_typecheck_stage_error(&error); + assert!(error.contains("duplicate named argument")); + assert!(error.contains("path")); + + std::fs::remove_file(fixture).expect("failed to cleanup fixture"); + } } From 32270d5cc970441a3c6b394a074107d3a0a1fad1 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 11:17:29 +0000 Subject: [PATCH 110/169] Add compile_from_context tests for type diagnostics Co-authored-by: Brian Searls --- core/daglang/daglang-cli/src/compile.rs | 189 ++++++++++++++++++++++++ 1 file changed, 189 insertions(+) diff --git a/core/daglang/daglang-cli/src/compile.rs b/core/daglang/daglang-cli/src/compile.rs index 156238476c4..96f038efb5e 100644 --- a/core/daglang/daglang-cli/src/compile.rs +++ b/core/daglang/daglang-cli/src/compile.rs @@ -1477,4 +1477,193 @@ func run() -> { body: String } { std::fs::remove_file(fixture).expect("failed to cleanup fixture"); } + + #[test] + fn compile_single_file_undefined_type_fails_in_typecheck_stage() { + let fixture = unique_temp_file("single_file_undefined_type"); + std::fs::write( + &fixture, + r#"module sample.types +fn run(input: MissingType) -> String { "ok" } +"#, + ) + .expect("failed to write undefined-type fixture"); + + let context = PipelineContext { + roots: vec![fixture.parent().expect("fixture should have parent").to_path_buf()], + target_file: Some(fixture.clone()), + }; + + let error = compile_from_context(&context).expect_err("compile should fail"); + assert_typecheck_stage_error(&error); + assert!(error.contains("undefined type `MissingType")); + + std::fs::remove_file(fixture).expect("failed to cleanup fixture"); + } + + #[test] + fn compile_single_file_type_mismatch_fails_in_typecheck_stage() { + let fixture = unique_temp_file("single_file_type_mismatch"); + std::fs::write( + &fixture, + r#"module sample.types +fn run() -> String { return 42 } +"#, + ) + .expect("failed to write type-mismatch fixture"); + + let context = PipelineContext { + roots: vec![fixture.parent().expect("fixture should have parent").to_path_buf()], + target_file: Some(fixture.clone()), + }; + + let error = compile_from_context(&context).expect_err("compile should fail"); + assert_typecheck_stage_error(&error); + assert!(error.contains("type mismatch: expected `String`, got `Int`")); + + std::fs::remove_file(fixture).expect("failed to cleanup fixture"); + } + + #[test] + fn compile_single_file_implicit_return_type_mismatch_fails_in_typecheck_stage() { + let fixture = unique_temp_file("single_file_implicit_return_type_mismatch"); + std::fs::write( + &fixture, + r#"module sample.types +fn run() -> String { 42 } +"#, + ) + .expect("failed to write implicit-return mismatch fixture"); + + let context = PipelineContext { + roots: vec![fixture.parent().expect("fixture should have parent").to_path_buf()], + target_file: Some(fixture.clone()), + }; + + let error = compile_from_context(&context).expect_err("compile should fail"); + assert_typecheck_stage_error(&error); + assert!(error.contains("type mismatch: expected `String`, got `Int`")); + + std::fs::remove_file(fixture).expect("failed to cleanup fixture"); + } + + #[test] + fn compile_single_file_no_such_field_record_literal_fails_in_typecheck_stage() { + let fixture = unique_temp_file("single_file_no_such_field_record_literal"); + std::fs::write( + &fixture, + r#"module sample.types +func run() -> { body: String } { + let payload = { body: "ok" } + return { body: payload.missing } +} +"#, + ) + .expect("failed to write no-such-field record-literal fixture"); + + let context = PipelineContext { + roots: vec![fixture.parent().expect("fixture should have parent").to_path_buf()], + target_file: Some(fixture.clone()), + }; + + let error = compile_from_context(&context).expect_err("compile should fail"); + assert_typecheck_stage_error(&error); + assert!(error.contains("type `Record` has no field `missing`")); + + std::fs::remove_file(fixture).expect("failed to cleanup fixture"); + } + + #[test] + fn compile_single_file_no_such_field_named_record_fails_in_typecheck_stage() { + let fixture = unique_temp_file("single_file_no_such_field_named_record"); + std::fs::write( + &fixture, + r#"module sample.types +type Payload { body: String } +fn run(input: Payload) -> String { input.missing } +"#, + ) + .expect("failed to write no-such-field named-record fixture"); + + let context = PipelineContext { + roots: vec![fixture.parent().expect("fixture should have parent").to_path_buf()], + target_file: Some(fixture.clone()), + }; + + let error = compile_from_context(&context).expect_err("compile should fail"); + assert_typecheck_stage_error(&error); + assert!(error.contains("type `Payload` has no field `missing`")); + + std::fs::remove_file(fixture).expect("failed to cleanup fixture"); + } + + #[test] + fn compile_single_file_unsatisfiable_refinement_fails_in_typecheck_stage() { + let fixture = unique_temp_file("single_file_unsatisfiable_refinement"); + std::fs::write( + &fixture, + r#"module sample.types +fn run(value: Int @range(min: 5, max: 1)) -> Int { value } +"#, + ) + .expect("failed to write unsatisfiable-refinement fixture"); + + let context = PipelineContext { + roots: vec![fixture.parent().expect("fixture should have parent").to_path_buf()], + target_file: Some(fixture.clone()), + }; + + let error = compile_from_context(&context).expect_err("compile should fail"); + assert_typecheck_stage_error(&error); + assert!(error.contains("unsatisfiable refinement on `Int`: range min 5 exceeds max 1")); + + std::fs::remove_file(fixture).expect("failed to cleanup fixture"); + } + + #[test] + fn compile_single_file_generic_arity_mismatch_fails_in_typecheck_stage() { + let fixture = unique_temp_file("single_file_generic_arity_mismatch"); + std::fs::write( + &fixture, + r#"module sample.types +fn run(values: Map) -> Int { 1 } +"#, + ) + .expect("failed to write generic-arity mismatch fixture"); + + let context = PipelineContext { + roots: vec![fixture.parent().expect("fixture should have parent").to_path_buf()], + target_file: Some(fixture.clone()), + }; + + let error = compile_from_context(&context).expect_err("compile should fail"); + assert_typecheck_stage_error(&error); + assert!(error.contains("generic arity mismatch for `Map`: expected 2, got 1")); + + std::fs::remove_file(fixture).expect("failed to cleanup fixture"); + } + + #[test] + fn compile_single_file_user_defined_generic_arity_mismatch_fails_in_typecheck_stage() { + let fixture = unique_temp_file("single_file_user_defined_generic_arity_mismatch"); + std::fs::write( + &fixture, + r#"module sample.types +type Box = T +fn run(values: Box) -> String { values } +"#, + ) + .expect("failed to write user-defined generic-arity mismatch fixture"); + + let context = PipelineContext { + roots: vec![fixture.parent().expect("fixture should have parent").to_path_buf()], + target_file: Some(fixture.clone()), + }; + + let error = compile_from_context(&context).expect_err("compile should fail"); + assert_typecheck_stage_error(&error); + assert!(error.contains("generic arity mismatch for `Box`: expected 1, got 2")); + + std::fs::remove_file(fixture).expect("failed to cleanup fixture"); + } } From 2b0027969acbfab7b6b3e66b12cf912af54acd6b Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 11:19:04 +0000 Subject: [PATCH 111/169] Add directory compile_from_context type diagnostic tests Co-authored-by: Brian Searls --- core/daglang/daglang-cli/src/compile.rs | 253 ++++++++++++++++++++++++ 1 file changed, 253 insertions(+) diff --git a/core/daglang/daglang-cli/src/compile.rs b/core/daglang/daglang-cli/src/compile.rs index 96f038efb5e..33d0db5a999 100644 --- a/core/daglang/daglang-cli/src/compile.rs +++ b/core/daglang/daglang-cli/src/compile.rs @@ -1666,4 +1666,257 @@ fn run(values: Box) -> String { values } std::fs::remove_file(fixture).expect("failed to cleanup fixture"); } + + #[test] + fn compile_directory_undefined_type_fails_in_typecheck_stage() { + let root = std::env::temp_dir().join(format!( + "daglang_compile_undefined_type_dir_{}_{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("system clock should be after unix epoch") + .as_nanos() + )); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp root"); + std::fs::write( + root.join("sample/main.dag"), + r#"module sample.main +fn run(input: MissingType) -> String { "ok" } +"#, + ) + .expect("failed to write undefined-type source"); + + let context = PipelineContext { + roots: vec![root.clone()], + target_file: None, + }; + + let error = compile_from_context(&context).expect_err("compile should fail"); + assert_typecheck_stage_error(&error); + assert!(error.contains("undefined type `MissingType")); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); + } + + #[test] + fn compile_directory_type_mismatch_fails_in_typecheck_stage() { + let root = std::env::temp_dir().join(format!( + "daglang_compile_type_mismatch_dir_{}_{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("system clock should be after unix epoch") + .as_nanos() + )); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp root"); + std::fs::write( + root.join("sample/main.dag"), + r#"module sample.main +fn run() -> String { return 42 } +"#, + ) + .expect("failed to write type-mismatch source"); + + let context = PipelineContext { + roots: vec![root.clone()], + target_file: None, + }; + + let error = compile_from_context(&context).expect_err("compile should fail"); + assert_typecheck_stage_error(&error); + assert!(error.contains("type mismatch: expected `String`, got `Int`")); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); + } + + #[test] + fn compile_directory_implicit_return_type_mismatch_fails_in_typecheck_stage() { + let root = std::env::temp_dir().join(format!( + "daglang_compile_implicit_return_mismatch_dir_{}_{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("system clock should be after unix epoch") + .as_nanos() + )); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp root"); + std::fs::write( + root.join("sample/main.dag"), + r#"module sample.main +fn run() -> String { 42 } +"#, + ) + .expect("failed to write implicit-return mismatch source"); + + let context = PipelineContext { + roots: vec![root.clone()], + target_file: None, + }; + + let error = compile_from_context(&context).expect_err("compile should fail"); + assert_typecheck_stage_error(&error); + assert!(error.contains("type mismatch: expected `String`, got `Int`")); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); + } + + #[test] + fn compile_directory_no_such_field_record_literal_fails_in_typecheck_stage() { + let root = std::env::temp_dir().join(format!( + "daglang_compile_no_such_field_record_dir_{}_{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("system clock should be after unix epoch") + .as_nanos() + )); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp root"); + std::fs::write( + root.join("sample/main.dag"), + r#"module sample.main +func run() -> { body: String } { + let payload = { body: "ok" } + return { body: payload.missing } +} +"#, + ) + .expect("failed to write no-such-field record-literal source"); + + let context = PipelineContext { + roots: vec![root.clone()], + target_file: None, + }; + + let error = compile_from_context(&context).expect_err("compile should fail"); + assert_typecheck_stage_error(&error); + assert!(error.contains("type `Record` has no field `missing`")); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); + } + + #[test] + fn compile_directory_no_such_field_named_record_fails_in_typecheck_stage() { + let root = std::env::temp_dir().join(format!( + "daglang_compile_no_such_field_named_record_dir_{}_{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("system clock should be after unix epoch") + .as_nanos() + )); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp root"); + std::fs::write( + root.join("sample/main.dag"), + r#"module sample.main +type Payload { body: String } +fn run(input: Payload) -> String { input.missing } +"#, + ) + .expect("failed to write no-such-field named-record source"); + + let context = PipelineContext { + roots: vec![root.clone()], + target_file: None, + }; + + let error = compile_from_context(&context).expect_err("compile should fail"); + assert_typecheck_stage_error(&error); + assert!(error.contains("type `Payload` has no field `missing`")); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); + } + + #[test] + fn compile_directory_unsatisfiable_refinement_fails_in_typecheck_stage() { + let root = std::env::temp_dir().join(format!( + "daglang_compile_unsatisfiable_refinement_dir_{}_{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("system clock should be after unix epoch") + .as_nanos() + )); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp root"); + std::fs::write( + root.join("sample/main.dag"), + r#"module sample.main +fn run(value: Int @range(min: 5, max: 1)) -> Int { value } +"#, + ) + .expect("failed to write unsatisfiable-refinement source"); + + let context = PipelineContext { + roots: vec![root.clone()], + target_file: None, + }; + + let error = compile_from_context(&context).expect_err("compile should fail"); + assert_typecheck_stage_error(&error); + assert!(error.contains("unsatisfiable refinement on `Int`: range min 5 exceeds max 1")); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); + } + + #[test] + fn compile_directory_generic_arity_mismatch_fails_in_typecheck_stage() { + let root = std::env::temp_dir().join(format!( + "daglang_compile_generic_arity_mismatch_dir_{}_{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("system clock should be after unix epoch") + .as_nanos() + )); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp root"); + std::fs::write( + root.join("sample/main.dag"), + r#"module sample.main +fn run(values: Map) -> Int { 1 } +"#, + ) + .expect("failed to write generic-arity mismatch source"); + + let context = PipelineContext { + roots: vec![root.clone()], + target_file: None, + }; + + let error = compile_from_context(&context).expect_err("compile should fail"); + assert_typecheck_stage_error(&error); + assert!(error.contains("generic arity mismatch for `Map`: expected 2, got 1")); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); + } + + #[test] + fn compile_directory_user_defined_generic_arity_mismatch_fails_in_typecheck_stage() { + let root = std::env::temp_dir().join(format!( + "daglang_compile_user_defined_generic_arity_mismatch_dir_{}_{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("system clock should be after unix epoch") + .as_nanos() + )); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp root"); + std::fs::write( + root.join("sample/main.dag"), + r#"module sample.main +type Box = T +fn run(values: Box) -> String { values } +"#, + ) + .expect("failed to write user-defined generic-arity mismatch source"); + + let context = PipelineContext { + roots: vec![root.clone()], + target_file: None, + }; + + let error = compile_from_context(&context).expect_err("compile should fail"); + assert_typecheck_stage_error(&error); + assert!(error.contains("generic arity mismatch for `Box`: expected 1, got 2")); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); + } } From 77012daa65f7b9f38979ce221900031f36c00927 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 11:20:29 +0000 Subject: [PATCH 112/169] Add directory call argument compile unit tests Co-authored-by: Brian Searls --- core/daglang/daglang-cli/src/compile.rs | 231 ++++++++++++++++++++++++ 1 file changed, 231 insertions(+) diff --git a/core/daglang/daglang-cli/src/compile.rs b/core/daglang/daglang-cli/src/compile.rs index 33d0db5a999..3933395d285 100644 --- a/core/daglang/daglang-cli/src/compile.rs +++ b/core/daglang/daglang-cli/src/compile.rs @@ -1919,4 +1919,235 @@ fn run(values: Box) -> String { values } std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } + + #[test] + fn compile_directory_call_arity_mismatch_fails_in_typecheck_stage() { + let root = std::env::temp_dir().join(format!( + "daglang_compile_call_arity_mismatch_dir_{}_{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("system clock should be after unix epoch") + .as_nanos() + )); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp root"); + std::fs::write( + root.join("sample/main.dag"), + r#"module sample.main +fn fmt(value: String) -> String { value } +fn run() -> String { fmt() } +"#, + ) + .expect("failed to write call-arity mismatch source"); + + let context = PipelineContext { + roots: vec![root.clone()], + target_file: None, + }; + + let error = compile_from_context(&context).expect_err("compile should fail"); + assert_typecheck_stage_error(&error); + assert!(error.contains("call arity mismatch")); + assert!(error.contains("fmt")); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); + } + + #[test] + fn compile_directory_unknown_named_call_argument_fails_in_typecheck_stage() { + let root = std::env::temp_dir().join(format!( + "daglang_compile_unknown_named_call_arg_dir_{}_{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("system clock should be after unix epoch") + .as_nanos() + )); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp root"); + std::fs::write( + root.join("sample/main.dag"), + r#"module sample.main +fn fmt(value: String) -> String { value } +fn run() -> String { fmt(text: "ok") } +"#, + ) + .expect("failed to write unknown named-call argument source"); + + let context = PipelineContext { + roots: vec![root.clone()], + target_file: None, + }; + + let error = compile_from_context(&context).expect_err("compile should fail"); + assert_typecheck_stage_error(&error); + assert!(error.contains("unknown named argument")); + assert!(error.contains("text")); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); + } + + #[test] + fn compile_directory_duplicate_named_call_argument_fails_in_typecheck_stage() { + let root = std::env::temp_dir().join(format!( + "daglang_compile_duplicate_named_call_arg_dir_{}_{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("system clock should be after unix epoch") + .as_nanos() + )); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp root"); + std::fs::write( + root.join("sample/main.dag"), + r#"module sample.main +fn fmt(value: String) -> String { value } +fn run() -> String { fmt(value: "a", value: "b") } +"#, + ) + .expect("failed to write duplicate named-call argument source"); + + let context = PipelineContext { + roots: vec![root.clone()], + target_file: None, + }; + + let error = compile_from_context(&context).expect_err("compile should fail"); + assert_typecheck_stage_error(&error); + assert!(error.contains("duplicate named argument")); + assert!(error.contains("value")); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); + } + + #[test] + fn compile_directory_service_call_arity_mismatch_fails_in_typecheck_stage() { + let root = std::env::temp_dir().join(format!( + "daglang_compile_service_call_arity_mismatch_dir_{}_{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("system clock should be after unix epoch") + .as_nanos() + )); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp root"); + std::fs::write( + root.join("sample/main.dag"), + r#"module sample.main +interface Storage { + capability read { + input { path: String } + output { body: String } + } +} +service FsStorage implements Storage { + operation read(path: String) -> { body: String } +} +func run() -> { body: String } { + let response = FsStorage.read() + return { body: response.body } +} +"#, + ) + .expect("failed to write service call-arity mismatch source"); + + let context = PipelineContext { + roots: vec![root.clone()], + target_file: None, + }; + + let error = compile_from_context(&context).expect_err("compile should fail"); + assert_typecheck_stage_error(&error); + assert!(error.contains("service call arity mismatch")); + assert!(error.contains("FsStorage.read")); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); + } + + #[test] + fn compile_directory_unknown_named_service_argument_fails_in_typecheck_stage() { + let root = std::env::temp_dir().join(format!( + "daglang_compile_unknown_named_service_arg_dir_{}_{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("system clock should be after unix epoch") + .as_nanos() + )); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp root"); + std::fs::write( + root.join("sample/main.dag"), + r#"module sample.main +interface Storage { + capability read { + input { path: String } + output { body: String } + } +} +service FsStorage implements Storage { + operation read(path: String) -> { body: String } +} +func run() -> { body: String } { + let response = FsStorage.read(name: "README.md") + return { body: response.body } +} +"#, + ) + .expect("failed to write unknown named service-argument source"); + + let context = PipelineContext { + roots: vec![root.clone()], + target_file: None, + }; + + let error = compile_from_context(&context).expect_err("compile should fail"); + assert_typecheck_stage_error(&error); + assert!(error.contains("unknown named argument")); + assert!(error.contains("name")); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); + } + + #[test] + fn compile_directory_duplicate_named_service_argument_fails_in_typecheck_stage() { + let root = std::env::temp_dir().join(format!( + "daglang_compile_duplicate_named_service_arg_dir_{}_{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("system clock should be after unix epoch") + .as_nanos() + )); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp root"); + std::fs::write( + root.join("sample/main.dag"), + r#"module sample.main +interface Storage { + capability read { + input { path: String } + output { body: String } + } +} +service FsStorage implements Storage { + operation read(path: String) -> { body: String } +} +func run() -> { body: String } { + let response = FsStorage.read(path: "a", path: "b") + return { body: response.body } +} +"#, + ) + .expect("failed to write duplicate named service-argument source"); + + let context = PipelineContext { + roots: vec![root.clone()], + target_file: None, + }; + + let error = compile_from_context(&context).expect_err("compile should fail"); + assert_typecheck_stage_error(&error); + assert!(error.contains("duplicate named argument")); + assert!(error.contains("path")); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); + } } From dbfd984c9e9c684a8472ce75c3012c0a0fb36f51 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 11:22:28 +0000 Subject: [PATCH 113/169] Add compile_from_context tests for binding contracts Co-authored-by: Brian Searls --- core/daglang/daglang-cli/src/compile.rs | 258 ++++++++++++++++++++++++ 1 file changed, 258 insertions(+) diff --git a/core/daglang/daglang-cli/src/compile.rs b/core/daglang/daglang-cli/src/compile.rs index 3933395d285..2ec260645f2 100644 --- a/core/daglang/daglang-cli/src/compile.rs +++ b/core/daglang/daglang-cli/src/compile.rs @@ -2150,4 +2150,262 @@ func run() -> { body: String } { std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } + + #[test] + fn compile_single_file_duplicate_parameter_fails_in_typecheck_stage() { + let fixture = unique_temp_file("single_file_duplicate_parameter"); + std::fs::write( + &fixture, + r#"module sample.single +fn run(a: String, a: Int) -> String { a } +"#, + ) + .expect("failed to write duplicate-parameter fixture"); + + let context = PipelineContext { + roots: vec![fixture.parent().expect("fixture should have parent").to_path_buf()], + target_file: Some(fixture.clone()), + }; + + let error = compile_from_context(&context).expect_err("compile should fail"); + assert_typecheck_stage_error(&error); + assert!(error.contains("duplicate parameter `a` in `run`")); + + std::fs::remove_file(fixture).expect("failed to cleanup fixture"); + } + + #[test] + fn compile_single_file_duplicate_output_field_fails_in_typecheck_stage() { + let fixture = unique_temp_file("single_file_duplicate_output_field"); + std::fs::write( + &fixture, + r#"module sample.single +func run() -> { ok: Bool, ok: String } { return { ok: true } } +"#, + ) + .expect("failed to write duplicate-output fixture"); + + let context = PipelineContext { + roots: vec![fixture.parent().expect("fixture should have parent").to_path_buf()], + target_file: Some(fixture.clone()), + }; + + let error = compile_from_context(&context).expect_err("compile should fail"); + assert_typecheck_stage_error(&error); + assert!(error.contains("duplicate output field `ok` in `run`")); + + std::fs::remove_file(fixture).expect("failed to cleanup fixture"); + } + + #[test] + fn compile_single_file_duplicate_uses_binding_fails_in_typecheck_stage() { + let fixture = unique_temp_file("single_file_duplicate_uses_binding"); + std::fs::write( + &fixture, + r#"module sample.single +interface Storage { capability read { input { path: String } output { body: String } } } +func run() -> { ok: Bool } uses fs: Storage uses fs: Storage { return { ok: true } } +"#, + ) + .expect("failed to write duplicate-uses fixture"); + + let context = PipelineContext { + roots: vec![fixture.parent().expect("fixture should have parent").to_path_buf()], + target_file: Some(fixture.clone()), + }; + + let error = compile_from_context(&context).expect_err("compile should fail"); + assert_typecheck_stage_error(&error); + assert!(error.contains("duplicate uses binding `fs` in `run`")); + + std::fs::remove_file(fixture).expect("failed to cleanup fixture"); + } + + #[test] + fn compile_single_file_duplicate_provides_binding_fails_in_typecheck_stage() { + let fixture = unique_temp_file("single_file_duplicate_provides_binding"); + std::fs::write( + &fixture, + r#"module sample.single +interface Storage { capability read { input { path: String } output { body: String } } } +func run() -> { ok: Bool } provides out: Storage provides out: Storage { return { ok: true } } +"#, + ) + .expect("failed to write duplicate-provides fixture"); + + let context = PipelineContext { + roots: vec![fixture.parent().expect("fixture should have parent").to_path_buf()], + target_file: Some(fixture.clone()), + }; + + let error = compile_from_context(&context).expect_err("compile should fail"); + assert_typecheck_stage_error(&error); + assert!(error.contains("duplicate provides binding `out` in `run`")); + + std::fs::remove_file(fixture).expect("failed to cleanup fixture"); + } + + #[test] + fn compile_single_file_use_provide_binding_conflict_fails_in_typecheck_stage() { + let fixture = unique_temp_file("single_file_use_provide_binding_conflict"); + std::fs::write( + &fixture, + r#"module sample.single +interface Storage { capability read { input { path: String } output { body: String } } } +func run() -> { ok: Bool } uses io: Storage provides io: Storage { return { ok: true } } +"#, + ) + .expect("failed to write use/provide conflict fixture"); + + let context = PipelineContext { + roots: vec![fixture.parent().expect("fixture should have parent").to_path_buf()], + target_file: Some(fixture.clone()), + }; + + let error = compile_from_context(&context).expect_err("compile should fail"); + assert_typecheck_stage_error(&error); + assert!(error.contains("binding `io` is declared in both uses/provides in `run`")); + + std::fs::remove_file(fixture).expect("failed to cleanup fixture"); + } + + #[test] + fn compile_single_file_missing_resource_capability_fails_in_typecheck_stage() { + let fixture = unique_temp_file("single_file_missing_resource_capability"); + std::fs::write( + &fixture, + r#"module sample.single +interface Storage { + capability read { + input { path: String } + output { body: String } + } + capability write { + input { path: String, body: String } + output { ok: Bool } + } +} +resource Disk implements Storage { + capability read { + input { path: String } + output { body: String } + } +} +"#, + ) + .expect("failed to write missing-resource-capability fixture"); + + let context = PipelineContext { + roots: vec![fixture.parent().expect("fixture should have parent").to_path_buf()], + target_file: Some(fixture.clone()), + }; + + let error = compile_from_context(&context).expect_err("compile should fail"); + assert_typecheck_stage_error(&error); + assert!(error.contains("resource `Disk` is missing capability `write` for interface `Storage`")); + + std::fs::remove_file(fixture).expect("failed to cleanup fixture"); + } + + #[test] + fn compile_single_file_missing_service_operation_fails_in_typecheck_stage() { + let fixture = unique_temp_file("single_file_missing_service_operation"); + std::fs::write( + &fixture, + r#"module sample.single +interface Storage { + capability read { + input { path: String } + output { body: String } + } + capability write { + input { path: String, body: String } + output { ok: Bool } + } +} +service FsStorage implements Storage { + operation read(path: String) -> { body: String } +} +"#, + ) + .expect("failed to write missing-service-operation fixture"); + + let context = PipelineContext { + roots: vec![fixture.parent().expect("fixture should have parent").to_path_buf()], + target_file: Some(fixture.clone()), + }; + + let error = compile_from_context(&context).expect_err("compile should fail"); + assert_typecheck_stage_error(&error); + assert!(error.contains("service `FsStorage` is missing operation `write` for interface `Storage`")); + + std::fs::remove_file(fixture).expect("failed to cleanup fixture"); + } + + #[test] + fn compile_single_file_service_interface_signature_mismatch_fails_in_typecheck_stage() { + let fixture = unique_temp_file("single_file_interface_signature_mismatch"); + std::fs::write( + &fixture, + r#"module sample.single +interface Storage { + capability read { + input { path: String } + output { body: String } + } +} +service FsStorage implements Storage { + operation read(path: Int) -> { body: String } +} +"#, + ) + .expect("failed to write service-signature-mismatch fixture"); + + let context = PipelineContext { + roots: vec![fixture.parent().expect("fixture should have parent").to_path_buf()], + target_file: Some(fixture.clone()), + }; + + let error = compile_from_context(&context).expect_err("compile should fail"); + assert_typecheck_stage_error(&error); + assert!(error.contains("`FsStorage` does not match `Storage.read` contract")); + assert!(error.contains("expected `String` but found `Int`")); + + std::fs::remove_file(fixture).expect("failed to cleanup fixture"); + } + + #[test] + fn compile_single_file_resource_interface_signature_mismatch_fails_in_typecheck_stage() { + let fixture = unique_temp_file("single_file_resource_signature_mismatch"); + std::fs::write( + &fixture, + r#"module sample.single +interface Storage { + capability read { + input { path: String } + output { body: String } + } +} +resource Disk implements Storage { + capability read { + input { path: Int } + output { body: String } + } +} +"#, + ) + .expect("failed to write resource-signature-mismatch fixture"); + + let context = PipelineContext { + roots: vec![fixture.parent().expect("fixture should have parent").to_path_buf()], + target_file: Some(fixture.clone()), + }; + + let error = compile_from_context(&context).expect_err("compile should fail"); + assert_typecheck_stage_error(&error); + assert!(error.contains("`Disk` does not match `Storage.read` contract")); + assert!(error.contains("expected `String` but found `Int`")); + + std::fs::remove_file(fixture).expect("failed to cleanup fixture"); + } } From fb52a6fe4572dad605b177322157c695df8af891 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 11:24:15 +0000 Subject: [PATCH 114/169] Add directory compile_from_context binding contract tests Co-authored-by: Brian Searls --- core/daglang/daglang-cli/src/compile.rs | 330 ++++++++++++++++++++++++ 1 file changed, 330 insertions(+) diff --git a/core/daglang/daglang-cli/src/compile.rs b/core/daglang/daglang-cli/src/compile.rs index 2ec260645f2..832057804d7 100644 --- a/core/daglang/daglang-cli/src/compile.rs +++ b/core/daglang/daglang-cli/src/compile.rs @@ -2408,4 +2408,334 @@ resource Disk implements Storage { std::fs::remove_file(fixture).expect("failed to cleanup fixture"); } + + #[test] + fn compile_directory_duplicate_parameter_fails_in_typecheck_stage() { + let root = std::env::temp_dir().join(format!( + "daglang_compile_duplicate_parameter_dir_{}_{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("system clock should be after unix epoch") + .as_nanos() + )); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp root"); + std::fs::write( + root.join("sample/main.dag"), + r#"module sample.main +fn run(a: String, a: Int) -> String { a } +"#, + ) + .expect("failed to write duplicate-parameter source"); + + let context = PipelineContext { + roots: vec![root.clone()], + target_file: None, + }; + + let error = compile_from_context(&context).expect_err("compile should fail"); + assert_typecheck_stage_error(&error); + assert!(error.contains("duplicate parameter `a` in `run`")); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); + } + + #[test] + fn compile_directory_duplicate_output_field_fails_in_typecheck_stage() { + let root = std::env::temp_dir().join(format!( + "daglang_compile_duplicate_output_field_dir_{}_{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("system clock should be after unix epoch") + .as_nanos() + )); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp root"); + std::fs::write( + root.join("sample/main.dag"), + r#"module sample.main +func run() -> { ok: Bool, ok: String } { return { ok: true } } +"#, + ) + .expect("failed to write duplicate-output source"); + + let context = PipelineContext { + roots: vec![root.clone()], + target_file: None, + }; + + let error = compile_from_context(&context).expect_err("compile should fail"); + assert_typecheck_stage_error(&error); + assert!(error.contains("duplicate output field `ok` in `run`")); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); + } + + #[test] + fn compile_directory_duplicate_uses_binding_fails_in_typecheck_stage() { + let root = std::env::temp_dir().join(format!( + "daglang_compile_duplicate_uses_binding_dir_{}_{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("system clock should be after unix epoch") + .as_nanos() + )); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp root"); + std::fs::write( + root.join("sample/main.dag"), + r#"module sample.main +interface Storage { capability read { input { path: String } output { body: String } } } +func run() -> { ok: Bool } uses fs: Storage uses fs: Storage { return { ok: true } } +"#, + ) + .expect("failed to write duplicate-uses source"); + + let context = PipelineContext { + roots: vec![root.clone()], + target_file: None, + }; + + let error = compile_from_context(&context).expect_err("compile should fail"); + assert_typecheck_stage_error(&error); + assert!(error.contains("duplicate uses binding `fs` in `run`")); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); + } + + #[test] + fn compile_directory_duplicate_provides_binding_fails_in_typecheck_stage() { + let root = std::env::temp_dir().join(format!( + "daglang_compile_duplicate_provides_binding_dir_{}_{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("system clock should be after unix epoch") + .as_nanos() + )); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp root"); + std::fs::write( + root.join("sample/main.dag"), + r#"module sample.main +interface Storage { capability read { input { path: String } output { body: String } } } +func run() -> { ok: Bool } provides out: Storage provides out: Storage { return { ok: true } } +"#, + ) + .expect("failed to write duplicate-provides source"); + + let context = PipelineContext { + roots: vec![root.clone()], + target_file: None, + }; + + let error = compile_from_context(&context).expect_err("compile should fail"); + assert_typecheck_stage_error(&error); + assert!(error.contains("duplicate provides binding `out` in `run`")); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); + } + + #[test] + fn compile_directory_use_provide_binding_conflict_fails_in_typecheck_stage() { + let root = std::env::temp_dir().join(format!( + "daglang_compile_use_provide_binding_conflict_dir_{}_{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("system clock should be after unix epoch") + .as_nanos() + )); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp root"); + std::fs::write( + root.join("sample/main.dag"), + r#"module sample.main +interface Storage { capability read { input { path: String } output { body: String } } } +func run() -> { ok: Bool } uses io: Storage provides io: Storage { return { ok: true } } +"#, + ) + .expect("failed to write use/provide conflict source"); + + let context = PipelineContext { + roots: vec![root.clone()], + target_file: None, + }; + + let error = compile_from_context(&context).expect_err("compile should fail"); + assert_typecheck_stage_error(&error); + assert!(error.contains("binding `io` is declared in both uses/provides in `run`")); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); + } + + #[test] + fn compile_directory_missing_resource_capability_fails_in_typecheck_stage() { + let root = std::env::temp_dir().join(format!( + "daglang_compile_missing_resource_capability_dir_{}_{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("system clock should be after unix epoch") + .as_nanos() + )); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp root"); + std::fs::write( + root.join("sample/main.dag"), + r#"module sample.main +interface Storage { + capability read { + input { path: String } + output { body: String } + } + capability write { + input { path: String, body: String } + output { ok: Bool } + } +} +resource Disk implements Storage { + capability read { + input { path: String } + output { body: String } + } +} +"#, + ) + .expect("failed to write missing-resource-capability source"); + + let context = PipelineContext { + roots: vec![root.clone()], + target_file: None, + }; + + let error = compile_from_context(&context).expect_err("compile should fail"); + assert_typecheck_stage_error(&error); + assert!(error.contains("resource `Disk` is missing capability `write` for interface `Storage`")); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); + } + + #[test] + fn compile_directory_missing_service_operation_fails_in_typecheck_stage() { + let root = std::env::temp_dir().join(format!( + "daglang_compile_missing_service_operation_dir_{}_{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("system clock should be after unix epoch") + .as_nanos() + )); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp root"); + std::fs::write( + root.join("sample/main.dag"), + r#"module sample.main +interface Storage { + capability read { + input { path: String } + output { body: String } + } + capability write { + input { path: String, body: String } + output { ok: Bool } + } +} +service FsStorage implements Storage { + operation read(path: String) -> { body: String } +} +"#, + ) + .expect("failed to write missing-service-operation source"); + + let context = PipelineContext { + roots: vec![root.clone()], + target_file: None, + }; + + let error = compile_from_context(&context).expect_err("compile should fail"); + assert_typecheck_stage_error(&error); + assert!(error.contains("service `FsStorage` is missing operation `write` for interface `Storage`")); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); + } + + #[test] + fn compile_directory_service_interface_signature_mismatch_fails_in_typecheck_stage() { + let root = std::env::temp_dir().join(format!( + "daglang_compile_service_signature_mismatch_dir_{}_{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("system clock should be after unix epoch") + .as_nanos() + )); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp root"); + std::fs::write( + root.join("sample/main.dag"), + r#"module sample.main +interface Storage { + capability read { + input { path: String } + output { body: String } + } +} +service FsStorage implements Storage { + operation read(path: Int) -> { body: String } +} +"#, + ) + .expect("failed to write service-signature-mismatch source"); + + let context = PipelineContext { + roots: vec![root.clone()], + target_file: None, + }; + + let error = compile_from_context(&context).expect_err("compile should fail"); + assert_typecheck_stage_error(&error); + assert!(error.contains("`FsStorage` does not match `Storage.read` contract")); + assert!(error.contains("expected `String` but found `Int`")); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); + } + + #[test] + fn compile_directory_resource_interface_signature_mismatch_fails_in_typecheck_stage() { + let root = std::env::temp_dir().join(format!( + "daglang_compile_resource_signature_mismatch_dir_{}_{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("system clock should be after unix epoch") + .as_nanos() + )); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp root"); + std::fs::write( + root.join("sample/main.dag"), + r#"module sample.main +interface Storage { + capability read { + input { path: String } + output { body: String } + } +} +resource Disk implements Storage { + capability read { + input { path: Int } + output { body: String } + } +} +"#, + ) + .expect("failed to write resource-signature-mismatch source"); + + let context = PipelineContext { + roots: vec![root.clone()], + target_file: None, + }; + + let error = compile_from_context(&context).expect_err("compile should fail"); + assert_typecheck_stage_error(&error); + assert!(error.contains("`Disk` does not match `Storage.read` contract")); + assert!(error.contains("expected `String` but found `Int`")); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); + } } From 18be4ea2a64cd086dddc30722ecfe2f71e06660f Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 11:33:07 +0000 Subject: [PATCH 115/169] Add compile unit tests for ambiguous strict-mode diagnostics Co-authored-by: Brian Searls --- core/daglang/daglang-cli/src/compile.rs | 244 ++++++++++++++++++++++++ 1 file changed, 244 insertions(+) diff --git a/core/daglang/daglang-cli/src/compile.rs b/core/daglang/daglang-cli/src/compile.rs index d80c2e0364d..037282a23b7 100644 --- a/core/daglang/daglang-cli/src/compile.rs +++ b/core/daglang/daglang-cli/src/compile.rs @@ -2733,4 +2733,248 @@ resource Disk implements Storage { std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } + + #[test] + fn compile_directory_ambiguous_interface_reference_fails_in_typecheck_stage() { + let root = std::env::temp_dir().join(format!( + "daglang_compile_ambiguous_interface_reference_dir_{}_{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("system clock should be after unix epoch") + .as_nanos() + )); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp root"); + std::fs::write( + root.join("sample/first.dag"), + "module sample.first\ninterface Storage { capability read { input { path: String } output { body: String } } }", + ) + .expect("failed to write first interface source"); + std::fs::write( + root.join("sample/second.dag"), + "module sample.second\ninterface Storage { capability read { input { path: String } output { body: String } } }", + ) + .expect("failed to write second interface source"); + std::fs::write( + root.join("sample/main.dag"), + "module sample.main\nservice FsStorage implements Storage { operation read(path: String) -> { body: String } }", + ) + .expect("failed to write main source"); + + let context = PipelineContext { + roots: vec![root.clone()], + target_file: None, + }; + + let error = compile_from_context(&context).expect_err("compile should fail"); + assert_typecheck_stage_error(&error); + assert!(error.contains("ambiguous interface `Storage`")); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); + } + + #[test] + fn compile_directory_ambiguous_resource_interface_reference_fails_in_typecheck_stage() { + let root = std::env::temp_dir().join(format!( + "daglang_compile_ambiguous_resource_interface_reference_dir_{}_{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("system clock should be after unix epoch") + .as_nanos() + )); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp root"); + std::fs::write( + root.join("sample/first.dag"), + "module sample.first\ninterface Storage { capability read { input { path: String } output { body: String } } }", + ) + .expect("failed to write first interface source"); + std::fs::write( + root.join("sample/second.dag"), + "module sample.second\ninterface Storage { capability read { input { path: String } output { body: String } } }", + ) + .expect("failed to write second interface source"); + std::fs::write( + root.join("sample/main.dag"), + "module sample.main\nresource Disk implements Storage { capability read { input { path: String } output { body: String } } }", + ) + .expect("failed to write main source"); + + let context = PipelineContext { + roots: vec![root.clone()], + target_file: None, + }; + + let error = compile_from_context(&context).expect_err("compile should fail"); + assert_typecheck_stage_error(&error); + assert!(error.contains("`Disk` references ambiguous interface `Storage`")); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); + } + + #[test] + fn compile_directory_ambiguous_uses_resource_type_fails_in_typecheck_stage() { + let root = std::env::temp_dir().join(format!( + "daglang_compile_ambiguous_uses_resource_type_dir_{}_{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("system clock should be after unix epoch") + .as_nanos() + )); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp root"); + std::fs::write( + root.join("sample/one.dag"), + "module sample.one\nresource SharedResource {}", + ) + .expect("failed to write first resource source"); + std::fs::write( + root.join("sample/two.dag"), + "module sample.two\nresource SharedResource {}", + ) + .expect("failed to write second resource source"); + std::fs::write( + root.join("sample/main.dag"), + "module sample.main\nfunc run() -> { ok: Bool } uses fs: SharedResource { return { ok: true } }", + ) + .expect("failed to write main source"); + + let context = PipelineContext { + roots: vec![root.clone()], + target_file: None, + }; + + let error = compile_from_context(&context).expect_err("compile should fail"); + assert_typecheck_stage_error(&error); + assert!(error.contains("ambiguous used resource type `SharedResource`")); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); + } + + #[test] + fn compile_directory_ambiguous_provides_resource_type_fails_in_typecheck_stage() { + let root = std::env::temp_dir().join(format!( + "daglang_compile_ambiguous_provides_resource_type_dir_{}_{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("system clock should be after unix epoch") + .as_nanos() + )); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp root"); + std::fs::write( + root.join("sample/one.dag"), + "module sample.one\nresource SharedResource {}", + ) + .expect("failed to write first resource source"); + std::fs::write( + root.join("sample/two.dag"), + "module sample.two\nresource SharedResource {}", + ) + .expect("failed to write second resource source"); + std::fs::write( + root.join("sample/main.dag"), + "module sample.main\nfunc run() -> { ok: Bool } provides out: SharedResource { return { ok: true } }", + ) + .expect("failed to write main source"); + + let context = PipelineContext { + roots: vec![root.clone()], + target_file: None, + }; + + let error = compile_from_context(&context).expect_err("compile should fail"); + assert_typecheck_stage_error(&error); + assert!(error.contains("ambiguous provided resource type `SharedResource`")); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); + } + + #[test] + fn compile_directory_ambiguous_service_call_fails_in_typecheck_stage() { + let root = std::env::temp_dir().join(format!( + "daglang_compile_ambiguous_service_call_dir_{}_{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("system clock should be after unix epoch") + .as_nanos() + )); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp root"); + std::fs::write( + root.join("sample/first.dag"), + r#"module sample.first +service SharedService { + operation read(path: String) -> { body: String } +}"#, + ) + .expect("failed to write first service source"); + std::fs::write( + root.join("sample/second.dag"), + r#"module sample.second +service SharedService { + operation read(path: String) -> { body: String } +}"#, + ) + .expect("failed to write second service source"); + std::fs::write( + root.join("sample/main.dag"), + r#"module sample.main +func run(path: String) -> { body: String } { + let response = SharedService.read(path: path) + return { body: response.body } +}"#, + ) + .expect("failed to write main source"); + + let context = PipelineContext { + roots: vec![root.clone()], + target_file: None, + }; + + let error = compile_from_context(&context).expect_err("compile should fail"); + assert_typecheck_stage_error(&error); + assert!(error.contains("ambiguous service call `SharedService.read`")); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); + } + + #[test] + fn compile_directory_ambiguous_callable_target_fails_in_typecheck_stage() { + let root = std::env::temp_dir().join(format!( + "daglang_compile_ambiguous_callable_target_dir_{}_{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("system clock should be after unix epoch") + .as_nanos() + )); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp root"); + std::fs::write( + root.join("sample/one.dag"), + "module sample.one\nfn render(value: String) -> String { value }", + ) + .expect("failed to write first callable source"); + std::fs::write( + root.join("sample/two.dag"), + "module sample.two\nfn render(value: String) -> String { value }", + ) + .expect("failed to write second callable source"); + std::fs::write( + root.join("sample/main.dag"), + "module sample.main\nfn run() -> String { render(value: \"ok\") }", + ) + .expect("failed to write main source"); + + let context = PipelineContext { + roots: vec![root.clone()], + target_file: None, + }; + + let error = compile_from_context(&context).expect_err("compile should fail"); + assert_typecheck_stage_error(&error); + assert!(error.contains("ambiguous call target `render`")); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); + } } From 5f8c0a6300376392851e377b92cc7ef35b87c19b Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 11:38:08 +0000 Subject: [PATCH 116/169] Normalize compile command roots and add path tests Co-authored-by: Brian Searls --- core/daglang/daglang-cli/src/compile.rs | 30 +++- .../daglang-cli/tests/compile_commands.rs | 136 ++++++++++++++++++ 2 files changed, 164 insertions(+), 2 deletions(-) diff --git a/core/daglang/daglang-cli/src/compile.rs b/core/daglang/daglang-cli/src/compile.rs index 037282a23b7..870c46cbc52 100644 --- a/core/daglang/daglang-cli/src/compile.rs +++ b/core/daglang/daglang-cli/src/compile.rs @@ -1,4 +1,4 @@ -use std::path::PathBuf; +use std::path::{Component, Path, PathBuf}; use crate::pipeline::PipelineContext; use daglang_derive::{derive_artifacts, DerivedArtifacts}; @@ -17,7 +17,7 @@ pub struct CompileOutput { } pub fn build_context(input: Option<&String>) -> PipelineContext { - let parsed = input.map(PathBuf::from); + let parsed = input.map(|value| normalize_cli_path(PathBuf::from(value))); let (roots, target_file) = match parsed { Some(path) if path.extension().and_then(|ext| ext.to_str()) == Some("dag") => { let root = path @@ -38,6 +38,32 @@ pub fn resolve_default_root() -> PathBuf { cwd.join("dsl") } +fn normalize_cli_path(path: PathBuf) -> PathBuf { + let absolute = if path.is_absolute() { + path + } else { + let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")); + cwd.join(path) + }; + normalize_path_components(&absolute) +} + +fn normalize_path_components(path: &Path) -> PathBuf { + let mut normalized = PathBuf::new(); + for component in path.components() { + match component { + Component::Prefix(prefix) => normalized.push(prefix.as_os_str()), + Component::RootDir => normalized.push(Path::new(std::path::MAIN_SEPARATOR_STR)), + Component::CurDir => {} + Component::ParentDir => { + normalized.pop(); + } + Component::Normal(segment) => normalized.push(segment), + } + } + normalized +} + pub fn compile_from_context(context: &PipelineContext) -> Result { let module_graph = discover_module_graph_for_context(context)?; if context.target_file.is_none() { diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index a5436e2d66d..407ac2f5f2f 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -86,6 +86,142 @@ fn compile_command_emits_summary_for_single_file() { assert!(stdout.contains("target/generated/rust/main.rs")); } +#[test] +fn compile_command_relative_and_absolute_missing_roots_are_equivalent() { + let root = unique_temp_dir("compile_relative_absolute_missing_root"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + let missing = root.join("missing_root"); + + let relative = Command::new(daglang_bin()) + .arg("compile") + .arg("missing_root") + .current_dir(&root) + .output() + .expect("failed to run relative missing-root compile"); + assert!( + !relative.status.success(), + "relative missing-root compile should fail" + ); + + let absolute = Command::new(daglang_bin()) + .arg("compile") + .arg(&missing) + .current_dir(&root) + .output() + .expect("failed to run absolute missing-root compile"); + assert!( + !absolute.status.success(), + "absolute missing-root compile should fail" + ); + + assert_eq!( + relative.stdout, absolute.stdout, + "relative and absolute missing-root compile stdout should match" + ); + assert_eq!( + relative.stderr, absolute.stderr, + "relative and absolute missing-root compile stderr should match" + ); + let stderr = String::from_utf8_lossy(&relative.stderr); + assert!( + stderr.contains(&missing.display().to_string()), + "missing-root diagnostic should contain normalized absolute path: {stderr}" + ); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + +#[test] +fn compile_command_relative_and_absolute_non_directory_roots_are_equivalent() { + let root = unique_temp_dir("compile_relative_absolute_non_directory_root"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + let file_path = root.join("input.txt"); + std::fs::write(&file_path, "not a directory").expect("failed to write non-directory root"); + + let relative = Command::new(daglang_bin()) + .arg("compile") + .arg("input.txt") + .current_dir(&root) + .output() + .expect("failed to run relative non-directory-root compile"); + assert!( + !relative.status.success(), + "relative non-directory-root compile should fail" + ); + + let absolute = Command::new(daglang_bin()) + .arg("compile") + .arg(&file_path) + .current_dir(&root) + .output() + .expect("failed to run absolute non-directory-root compile"); + assert!( + !absolute.status.success(), + "absolute non-directory-root compile should fail" + ); + + assert_eq!( + relative.stdout, absolute.stdout, + "relative and absolute non-directory-root compile stdout should match" + ); + assert_eq!( + relative.stderr, absolute.stderr, + "relative and absolute non-directory-root compile stderr should match" + ); + let stderr = String::from_utf8_lossy(&relative.stderr); + assert!( + stderr.contains(&file_path.display().to_string()), + "non-directory-root diagnostic should contain normalized absolute path: {stderr}" + ); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + +#[test] +fn compile_command_parent_segment_missing_root_is_normalized_and_equivalent() { + let root = unique_temp_dir("compile_parent_segment_missing_root"); + std::fs::create_dir_all(root.join("child")).expect("failed to create temp root"); + let missing = root.join("missing_root"); + + let parent_segment = Command::new(daglang_bin()) + .arg("compile") + .arg("../missing_root") + .current_dir(root.join("child")) + .output() + .expect("failed to run parent-segment missing-root compile"); + assert!( + !parent_segment.status.success(), + "parent-segment missing-root compile should fail" + ); + + let absolute = Command::new(daglang_bin()) + .arg("compile") + .arg(&missing) + .current_dir(root.join("child")) + .output() + .expect("failed to run absolute missing-root compile"); + assert!( + !absolute.status.success(), + "absolute missing-root compile should fail" + ); + + assert_eq!( + parent_segment.stdout, absolute.stdout, + "parent-segment and absolute missing-root compile stdout should match" + ); + assert_eq!( + parent_segment.stderr, absolute.stderr, + "parent-segment and absolute missing-root compile stderr should match" + ); + let stderr = String::from_utf8_lossy(&parent_segment.stderr); + assert!( + stderr.contains(&missing.display().to_string()), + "parent-segment missing-root diagnostic should normalize to absolute path: {stderr}" + ); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_single_file_fails_on_duplicate_definition() { let fixture = unique_temp_file("compile_single_file_duplicate_definition"); From 84ba4dbd6034dd0f68b05fbe9ab8d0b0ddea7c87 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 11:42:47 +0000 Subject: [PATCH 117/169] Expand compile command path normalization regressions Co-authored-by: Brian Searls --- .../daglang-cli/tests/compile_commands.rs | 201 ++++++++++++++++++ 1 file changed, 201 insertions(+) diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index 407ac2f5f2f..18ef510f6e4 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -222,6 +222,207 @@ fn compile_command_parent_segment_missing_root_is_normalized_and_equivalent() { std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_relative_and_absolute_missing_single_file_targets_are_equivalent() { + let root = unique_temp_dir("compile_relative_absolute_missing_single_file_target"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + let missing = root.join("missing.dag"); + + let relative = Command::new(daglang_bin()) + .arg("compile") + .arg("missing.dag") + .current_dir(&root) + .output() + .expect("failed to run relative missing single-file compile"); + assert!( + !relative.status.success(), + "relative missing single-file compile should fail" + ); + + let absolute = Command::new(daglang_bin()) + .arg("compile") + .arg(&missing) + .current_dir(&root) + .output() + .expect("failed to run absolute missing single-file compile"); + assert!( + !absolute.status.success(), + "absolute missing single-file compile should fail" + ); + + assert_eq!( + relative.stdout, absolute.stdout, + "relative and absolute missing single-file compile stdout should match" + ); + assert_eq!( + relative.stderr, absolute.stderr, + "relative and absolute missing single-file compile stderr should match" + ); + let stderr = String::from_utf8_lossy(&relative.stderr); + assert!( + stderr.contains(&format!("failed to read {}", missing.display())), + "missing single-file diagnostic should include normalized absolute path: {stderr}" + ); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + +#[test] +fn compile_command_parent_segment_missing_single_file_target_is_normalized_and_equivalent() { + let root = unique_temp_dir("compile_parent_segment_missing_single_file_target"); + std::fs::create_dir_all(root.join("child")).expect("failed to create temp root"); + let missing = root.join("missing.dag"); + + let parent_segment = Command::new(daglang_bin()) + .arg("compile") + .arg("../missing.dag") + .current_dir(root.join("child")) + .output() + .expect("failed to run parent-segment missing single-file compile"); + assert!( + !parent_segment.status.success(), + "parent-segment missing single-file compile should fail" + ); + + let absolute = Command::new(daglang_bin()) + .arg("compile") + .arg(&missing) + .current_dir(root.join("child")) + .output() + .expect("failed to run absolute missing single-file compile"); + assert!( + !absolute.status.success(), + "absolute missing single-file compile should fail" + ); + + assert_eq!( + parent_segment.stdout, absolute.stdout, + "parent-segment and absolute missing single-file compile stdout should match" + ); + assert_eq!( + parent_segment.stderr, absolute.stderr, + "parent-segment and absolute missing single-file compile stderr should match" + ); + let stderr = String::from_utf8_lossy(&parent_segment.stderr); + assert!( + stderr.contains(&format!("failed to read {}", missing.display())), + "parent-segment missing single-file diagnostic should normalize to absolute path: {stderr}" + ); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + +#[test] +fn compile_command_relative_and_absolute_single_file_targets_are_equivalent() { + let root = unique_temp_dir("compile_relative_absolute_single_file_target"); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp root"); + let source = root.join("sample/main.dag"); + std::fs::write(&source, "module sample.main\nfn run() -> Unit { }") + .expect("failed to write single-file source"); + + let relative = Command::new(daglang_bin()) + .arg("compile") + .arg("sample/main.dag") + .current_dir(&root) + .output() + .expect("failed to run relative single-file compile"); + assert!( + relative.status.success(), + "relative single-file compile should succeed: {}", + String::from_utf8_lossy(&relative.stderr) + ); + + let absolute = Command::new(daglang_bin()) + .arg("compile") + .arg(&source) + .current_dir(&root) + .output() + .expect("failed to run absolute single-file compile"); + assert!( + absolute.status.success(), + "absolute single-file compile should succeed: {}", + String::from_utf8_lossy(&absolute.stderr) + ); + + assert_eq!( + relative.stdout, absolute.stdout, + "relative and absolute single-file compile stdout should match" + ); + assert_eq!( + relative.stderr, absolute.stderr, + "relative and absolute single-file compile stderr should match" + ); + assert_no_stage_failures(&String::from_utf8_lossy(&relative.stderr)); + assert!( + String::from_utf8_lossy(&relative.stdout).contains("Compiled 1 module(s)"), + "single-file compile summary missing: {}", + String::from_utf8_lossy(&relative.stdout) + ); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + +#[test] +fn compile_command_default_root_missing_in_cwd_exits_nonzero() { + let root = unique_temp_dir("compile_default_root_missing"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + let expected = root.join("dsl"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .current_dir(&root) + .output() + .expect("failed to run compile default-root check"); + + assert!( + !output.status.success(), + "compile default-root run should fail when dsl directory is missing" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + stderr.contains(&format!( + "resolve error: invalid discovery root {}: does not exist", + expected.display() + )), + "default-root missing diagnostic should include normalized absolute dsl path: {stderr}" + ); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + +#[test] +fn compile_command_default_root_non_directory_in_cwd_exits_nonzero() { + let root = unique_temp_dir("compile_default_root_non_directory"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + let dsl_file = root.join("dsl"); + std::fs::write(&dsl_file, "not a directory").expect("failed to write dsl file"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .current_dir(&root) + .output() + .expect("failed to run compile default-root check"); + + assert!( + !output.status.success(), + "compile default-root run should fail when dsl path is not a directory" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + stderr.contains(&format!( + "resolve error: invalid discovery root {}: is not a directory", + dsl_file.display() + )), + "default-root non-directory diagnostic should include normalized absolute dsl path: {stderr}" + ); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_single_file_fails_on_duplicate_definition() { let fixture = unique_temp_file("compile_single_file_duplicate_definition"); From 8f0e03a668fd766c12e54187e844894a7e5358de Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 11:48:29 +0000 Subject: [PATCH 118/169] Add build_context normalization and compile segment regressions Co-authored-by: Brian Searls --- core/daglang/daglang-cli/src/compile.rs | 61 +++++ .../daglang-cli/tests/compile_commands.rs | 222 ++++++++++++++++++ 2 files changed, 283 insertions(+) diff --git a/core/daglang/daglang-cli/src/compile.rs b/core/daglang/daglang-cli/src/compile.rs index 870c46cbc52..5e85a77a2c4 100644 --- a/core/daglang/daglang-cli/src/compile.rs +++ b/core/daglang/daglang-cli/src/compile.rs @@ -342,6 +342,67 @@ mod tests { assert!(!error.contains("typecheck errors")); } + #[test] + fn build_context_normalizes_absolute_directory_input_components() { + let root = std::env::temp_dir().join(format!( + "daglang_build_context_dir_{}_{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("system clock should be after unix epoch") + .as_nanos() + )); + let normalized_root = root.join("sample"); + std::fs::create_dir_all(&normalized_root).expect("failed to create temp directory root"); + let input = root.join("sample").join(".").join("nested").join(".."); + let input_str = input.to_string_lossy().to_string(); + + let context = build_context(Some(&input_str)); + assert_eq!(context.roots, vec![normalized_root.clone()]); + assert!(context.target_file.is_none()); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); + } + + #[test] + fn build_context_normalizes_absolute_single_file_input_components() { + let root = std::env::temp_dir().join(format!( + "daglang_build_context_file_{}_{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("system clock should be after unix epoch") + .as_nanos() + )); + let normalized_file = root.join("sample/main.dag"); + std::fs::create_dir_all(normalized_file.parent().expect("file should have parent")) + .expect("failed to create temp file parent"); + std::fs::write(&normalized_file, "module sample.main\nfn run() -> Unit { }") + .expect("failed to write temp dag file"); + let input = root.join("sample").join("nested").join("..").join("main.dag"); + let input_str = input.to_string_lossy().to_string(); + + let context = build_context(Some(&input_str)); + assert_eq!( + context.roots, + vec![normalized_file + .parent() + .expect("file should have parent") + .to_path_buf()] + ); + assert_eq!(context.target_file, Some(normalized_file.clone())); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); + } + + #[test] + fn build_context_default_root_is_cwd_dsl() { + let context = build_context(None); + let cwd = std::env::current_dir().expect("cwd should resolve"); + assert_eq!(context.roots, vec![cwd.join("dsl")]); + assert!(context.target_file.is_none()); + } + #[test] fn compile_single_file_makegen_produces_non_empty_outputs() { let file = PathBuf::from(env!("CARGO_MANIFEST_DIR")) diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index 18ef510f6e4..87f6a98963b 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -222,6 +222,92 @@ fn compile_command_parent_segment_missing_root_is_normalized_and_equivalent() { std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_curdir_segment_missing_root_matches_plain_relative_output() { + let root = unique_temp_dir("compile_curdir_segment_missing_root"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + let missing = root.join("missing_root"); + + let plain = Command::new(daglang_bin()) + .arg("compile") + .arg("missing_root") + .current_dir(&root) + .output() + .expect("failed to run plain missing-root compile"); + assert!(!plain.status.success(), "plain missing-root compile should fail"); + + let curdir = Command::new(daglang_bin()) + .arg("compile") + .arg("./missing_root") + .current_dir(&root) + .output() + .expect("failed to run curdir-segment missing-root compile"); + assert!( + !curdir.status.success(), + "curdir-segment missing-root compile should fail" + ); + + assert_eq!( + plain.stdout, curdir.stdout, + "plain and curdir missing-root compile stdout should match" + ); + assert_eq!( + plain.stderr, curdir.stderr, + "plain and curdir missing-root compile stderr should match" + ); + let stderr = String::from_utf8_lossy(&plain.stderr); + assert!( + stderr.contains(&missing.display().to_string()), + "curdir-segment missing-root diagnostic should normalize to absolute path: {stderr}" + ); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + +#[test] +fn compile_command_double_separator_missing_root_matches_plain_relative_output() { + let root = unique_temp_dir("compile_double_separator_missing_root"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + let missing = root.join("missing_root"); + + let plain = Command::new(daglang_bin()) + .arg("compile") + .arg("missing_root") + .current_dir(&root) + .output() + .expect("failed to run plain missing-root compile"); + assert!(!plain.status.success(), "plain missing-root compile should fail"); + + let double_sep = Command::new(daglang_bin()) + .arg("compile") + .arg("missing_root//") + .current_dir(&root) + .output() + .expect("failed to run double-separator missing-root compile"); + assert!( + !double_sep.status.success(), + "double-separator missing-root compile should fail" + ); + + assert_eq!( + plain.stdout, double_sep.stdout, + "plain and double-separator missing-root compile stdout should match" + ); + assert_eq!( + plain.stderr, double_sep.stderr, + "plain and double-separator missing-root compile stderr should match" + ); + let stderr = String::from_utf8_lossy(&plain.stderr); + assert!( + stderr.contains(&missing.display().to_string()), + "double-separator missing-root diagnostic should normalize to absolute path: {stderr}" + ); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_relative_and_absolute_missing_single_file_targets_are_equivalent() { let root = unique_temp_dir("compile_relative_absolute_missing_single_file_target"); @@ -268,6 +354,52 @@ fn compile_command_relative_and_absolute_missing_single_file_targets_are_equival std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_curdir_segment_missing_single_file_target_matches_plain_relative_output() { + let root = unique_temp_dir("compile_curdir_segment_missing_single_file_target"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + let missing = root.join("missing.dag"); + + let plain = Command::new(daglang_bin()) + .arg("compile") + .arg("missing.dag") + .current_dir(&root) + .output() + .expect("failed to run plain missing single-file compile"); + assert!( + !plain.status.success(), + "plain missing single-file compile should fail" + ); + + let curdir = Command::new(daglang_bin()) + .arg("compile") + .arg("./missing.dag") + .current_dir(&root) + .output() + .expect("failed to run curdir-segment missing single-file compile"); + assert!( + !curdir.status.success(), + "curdir-segment missing single-file compile should fail" + ); + + assert_eq!( + plain.stdout, curdir.stdout, + "plain and curdir missing single-file compile stdout should match" + ); + assert_eq!( + plain.stderr, curdir.stderr, + "plain and curdir missing single-file compile stderr should match" + ); + let stderr = String::from_utf8_lossy(&plain.stderr); + assert!( + stderr.contains(&format!("failed to read {}", missing.display())), + "curdir-segment missing single-file diagnostic should normalize to absolute path: {stderr}" + ); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_parent_segment_missing_single_file_target_is_normalized_and_equivalent() { let root = unique_temp_dir("compile_parent_segment_missing_single_file_target"); @@ -314,6 +446,96 @@ fn compile_command_parent_segment_missing_single_file_target_is_normalized_and_e std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_curdir_segment_single_file_target_matches_plain_relative_output() { + let root = unique_temp_dir("compile_curdir_segment_single_file_target"); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp root"); + let source = root.join("sample/main.dag"); + std::fs::write(&source, "module sample.main\nfn run() -> Unit { }") + .expect("failed to write single-file source"); + + let plain = Command::new(daglang_bin()) + .arg("compile") + .arg("sample/main.dag") + .current_dir(&root) + .output() + .expect("failed to run plain single-file compile"); + assert!( + plain.status.success(), + "plain single-file compile should succeed: {}", + String::from_utf8_lossy(&plain.stderr) + ); + + let curdir = Command::new(daglang_bin()) + .arg("compile") + .arg("./sample/main.dag") + .current_dir(&root) + .output() + .expect("failed to run curdir-segment single-file compile"); + assert!( + curdir.status.success(), + "curdir-segment single-file compile should succeed: {}", + String::from_utf8_lossy(&curdir.stderr) + ); + + assert_eq!( + plain.stdout, curdir.stdout, + "plain and curdir single-file compile stdout should match" + ); + assert_eq!( + plain.stderr, curdir.stderr, + "plain and curdir single-file compile stderr should match" + ); + assert_no_stage_failures(&String::from_utf8_lossy(&plain.stderr)); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + +#[test] +fn compile_command_double_separator_single_file_target_matches_plain_relative_output() { + let root = unique_temp_dir("compile_double_separator_single_file_target"); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp root"); + let source = root.join("sample/main.dag"); + std::fs::write(&source, "module sample.main\nfn run() -> Unit { }") + .expect("failed to write single-file source"); + + let plain = Command::new(daglang_bin()) + .arg("compile") + .arg("sample/main.dag") + .current_dir(&root) + .output() + .expect("failed to run plain single-file compile"); + assert!( + plain.status.success(), + "plain single-file compile should succeed: {}", + String::from_utf8_lossy(&plain.stderr) + ); + + let double_sep = Command::new(daglang_bin()) + .arg("compile") + .arg("sample//main.dag") + .current_dir(&root) + .output() + .expect("failed to run double-separator single-file compile"); + assert!( + double_sep.status.success(), + "double-separator single-file compile should succeed: {}", + String::from_utf8_lossy(&double_sep.stderr) + ); + + assert_eq!( + plain.stdout, double_sep.stdout, + "plain and double-separator single-file compile stdout should match" + ); + assert_eq!( + plain.stderr, double_sep.stderr, + "plain and double-separator single-file compile stderr should match" + ); + assert_no_stage_failures(&String::from_utf8_lossy(&plain.stderr)); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_relative_and_absolute_single_file_targets_are_equivalent() { let root = unique_temp_dir("compile_relative_absolute_single_file_target"); From cfd860c793e785b0efce604bb02d0cc2dfef2297 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 11:53:14 +0000 Subject: [PATCH 119/169] Expand compile trailing-slash normalization regressions Co-authored-by: Brian Searls --- .../daglang-cli/tests/compile_commands.rs | 269 ++++++++++++++++++ 1 file changed, 269 insertions(+) diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index 87f6a98963b..5822f018f89 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -265,6 +265,49 @@ fn compile_command_curdir_segment_missing_root_matches_plain_relative_output() { std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_curdir_double_separator_missing_root_matches_plain_relative_output() { + let root = unique_temp_dir("compile_curdir_double_separator_missing_root"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + let missing = root.join("missing_root"); + + let plain = Command::new(daglang_bin()) + .arg("compile") + .arg("missing_root") + .current_dir(&root) + .output() + .expect("failed to run plain missing-root compile"); + assert!(!plain.status.success(), "plain missing-root compile should fail"); + + let curdir_double = Command::new(daglang_bin()) + .arg("compile") + .arg(".//missing_root") + .current_dir(&root) + .output() + .expect("failed to run curdir-double-separator missing-root compile"); + assert!( + !curdir_double.status.success(), + "curdir-double-separator missing-root compile should fail" + ); + + assert_eq!( + plain.stdout, curdir_double.stdout, + "plain and curdir-double missing-root compile stdout should match" + ); + assert_eq!( + plain.stderr, curdir_double.stderr, + "plain and curdir-double missing-root compile stderr should match" + ); + let stderr = String::from_utf8_lossy(&plain.stderr); + assert!( + stderr.contains(&missing.display().to_string()), + "curdir-double missing-root diagnostic should normalize to absolute path: {stderr}" + ); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_double_separator_missing_root_matches_plain_relative_output() { let root = unique_temp_dir("compile_double_separator_missing_root"); @@ -308,6 +351,49 @@ fn compile_command_double_separator_missing_root_matches_plain_relative_output() std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_trailing_slash_missing_root_matches_plain_relative_output() { + let root = unique_temp_dir("compile_trailing_slash_missing_root"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + let missing = root.join("missing_root"); + + let plain = Command::new(daglang_bin()) + .arg("compile") + .arg("missing_root") + .current_dir(&root) + .output() + .expect("failed to run plain missing-root compile"); + assert!(!plain.status.success(), "plain missing-root compile should fail"); + + let trailing_slash = Command::new(daglang_bin()) + .arg("compile") + .arg("missing_root/") + .current_dir(&root) + .output() + .expect("failed to run trailing-slash missing-root compile"); + assert!( + !trailing_slash.status.success(), + "trailing-slash missing-root compile should fail" + ); + + assert_eq!( + plain.stdout, trailing_slash.stdout, + "plain and trailing-slash missing-root compile stdout should match" + ); + assert_eq!( + plain.stderr, trailing_slash.stderr, + "plain and trailing-slash missing-root compile stderr should match" + ); + let stderr = String::from_utf8_lossy(&plain.stderr); + assert!( + stderr.contains(&missing.display().to_string()), + "trailing-slash missing-root diagnostic should normalize to absolute path: {stderr}" + ); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_relative_and_absolute_missing_single_file_targets_are_equivalent() { let root = unique_temp_dir("compile_relative_absolute_missing_single_file_target"); @@ -354,6 +440,52 @@ fn compile_command_relative_and_absolute_missing_single_file_targets_are_equival std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_trailing_slash_missing_single_file_target_matches_plain_relative_output() { + let root = unique_temp_dir("compile_trailing_slash_missing_single_file_target"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + let missing = root.join("missing.dag"); + + let plain = Command::new(daglang_bin()) + .arg("compile") + .arg("missing.dag") + .current_dir(&root) + .output() + .expect("failed to run plain missing single-file compile"); + assert!( + !plain.status.success(), + "plain missing single-file compile should fail" + ); + + let trailing_slash = Command::new(daglang_bin()) + .arg("compile") + .arg("missing.dag/") + .current_dir(&root) + .output() + .expect("failed to run trailing-slash missing single-file compile"); + assert!( + !trailing_slash.status.success(), + "trailing-slash missing single-file compile should fail" + ); + + assert_eq!( + plain.stdout, trailing_slash.stdout, + "plain and trailing-slash missing single-file compile stdout should match" + ); + assert_eq!( + plain.stderr, trailing_slash.stderr, + "plain and trailing-slash missing single-file compile stderr should match" + ); + let stderr = String::from_utf8_lossy(&plain.stderr); + assert!( + stderr.contains(&format!("failed to read {}", missing.display())), + "trailing-slash missing single-file diagnostic should include normalized absolute path: {stderr}" + ); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_curdir_segment_missing_single_file_target_matches_plain_relative_output() { let root = unique_temp_dir("compile_curdir_segment_missing_single_file_target"); @@ -400,6 +532,51 @@ fn compile_command_curdir_segment_missing_single_file_target_matches_plain_relat std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_curdir_double_separator_single_file_target_matches_plain_relative_output() { + let root = unique_temp_dir("compile_curdir_double_separator_single_file_target"); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp root"); + let source = root.join("sample/main.dag"); + std::fs::write(&source, "module sample.main\nfn run() -> Unit { }") + .expect("failed to write single-file source"); + + let plain = Command::new(daglang_bin()) + .arg("compile") + .arg("sample/main.dag") + .current_dir(&root) + .output() + .expect("failed to run plain single-file compile"); + assert!( + plain.status.success(), + "plain single-file compile should succeed: {}", + String::from_utf8_lossy(&plain.stderr) + ); + + let curdir_double = Command::new(daglang_bin()) + .arg("compile") + .arg(".//sample/main.dag") + .current_dir(&root) + .output() + .expect("failed to run curdir-double single-file compile"); + assert!( + curdir_double.status.success(), + "curdir-double single-file compile should succeed: {}", + String::from_utf8_lossy(&curdir_double.stderr) + ); + + assert_eq!( + plain.stdout, curdir_double.stdout, + "plain and curdir-double single-file compile stdout should match" + ); + assert_eq!( + plain.stderr, curdir_double.stderr, + "plain and curdir-double single-file compile stderr should match" + ); + assert_no_stage_failures(&String::from_utf8_lossy(&plain.stderr)); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_parent_segment_missing_single_file_target_is_normalized_and_equivalent() { let root = unique_temp_dir("compile_parent_segment_missing_single_file_target"); @@ -446,6 +623,51 @@ fn compile_command_parent_segment_missing_single_file_target_is_normalized_and_e std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_trailing_slash_single_file_target_matches_plain_relative_output() { + let root = unique_temp_dir("compile_trailing_slash_single_file_target"); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp root"); + let source = root.join("sample/main.dag"); + std::fs::write(&source, "module sample.main\nfn run() -> Unit { }") + .expect("failed to write single-file source"); + + let plain = Command::new(daglang_bin()) + .arg("compile") + .arg("sample/main.dag") + .current_dir(&root) + .output() + .expect("failed to run plain single-file compile"); + assert!( + plain.status.success(), + "plain single-file compile should succeed: {}", + String::from_utf8_lossy(&plain.stderr) + ); + + let trailing_slash = Command::new(daglang_bin()) + .arg("compile") + .arg("sample/main.dag/") + .current_dir(&root) + .output() + .expect("failed to run trailing-slash single-file compile"); + assert!( + trailing_slash.status.success(), + "trailing-slash single-file compile should succeed: {}", + String::from_utf8_lossy(&trailing_slash.stderr) + ); + + assert_eq!( + plain.stdout, trailing_slash.stdout, + "plain and trailing-slash single-file compile stdout should match" + ); + assert_eq!( + plain.stderr, trailing_slash.stderr, + "plain and trailing-slash single-file compile stderr should match" + ); + assert_no_stage_failures(&String::from_utf8_lossy(&plain.stderr)); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_curdir_segment_single_file_target_matches_plain_relative_output() { let root = unique_temp_dir("compile_curdir_segment_single_file_target"); @@ -491,6 +713,53 @@ fn compile_command_curdir_segment_single_file_target_matches_plain_relative_outp std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_trailing_slash_non_directory_root_matches_plain_relative_output() { + let root = unique_temp_dir("compile_trailing_slash_non_directory_root"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + let root_file = root.join("input.txt"); + std::fs::write(&root_file, "not a directory").expect("failed to write non-directory root"); + + let plain = Command::new(daglang_bin()) + .arg("compile") + .arg("input.txt") + .current_dir(&root) + .output() + .expect("failed to run plain non-directory-root compile"); + assert!( + !plain.status.success(), + "plain non-directory-root compile should fail" + ); + + let trailing_slash = Command::new(daglang_bin()) + .arg("compile") + .arg("input.txt/") + .current_dir(&root) + .output() + .expect("failed to run trailing-slash non-directory-root compile"); + assert!( + !trailing_slash.status.success(), + "trailing-slash non-directory-root compile should fail" + ); + + assert_eq!( + plain.stdout, trailing_slash.stdout, + "plain and trailing-slash non-directory-root compile stdout should match" + ); + assert_eq!( + plain.stderr, trailing_slash.stderr, + "plain and trailing-slash non-directory-root compile stderr should match" + ); + let stderr = String::from_utf8_lossy(&plain.stderr); + assert!( + stderr.contains(&root_file.display().to_string()), + "trailing-slash non-directory-root diagnostic should normalize to absolute path: {stderr}" + ); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_double_separator_single_file_target_matches_plain_relative_output() { let root = unique_temp_dir("compile_double_separator_single_file_target"); From 24b5ab11a41d0b395223e850587a1a0213554eb6 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 11:57:48 +0000 Subject: [PATCH 120/169] Add compile root path variant equivalence tests Co-authored-by: Brian Searls --- .../daglang-cli/tests/compile_commands.rs | 231 +++++++++++++++++- 1 file changed, 230 insertions(+), 1 deletion(-) diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index 5822f018f89..aca7c3097d7 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -1,4 +1,4 @@ -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::process::Command; use std::time::{SystemTime, UNIX_EPOCH}; @@ -38,6 +38,15 @@ fn unique_temp_dir(name: &str) -> PathBuf { )) } +fn write_minimal_directory_compile_fixture(root: &Path) { + std::fs::create_dir_all(root.join("dsl/sample")).expect("failed to create dsl fixture tree"); + std::fs::write( + root.join("dsl/sample/main.dag"), + "module sample.main\nfn run() -> Unit { }", + ) + .expect("failed to write minimal compile fixture"); +} + fn assert_typecheck_stage_failure(stderr: &str) { assert!(stderr.contains("typecheck errors")); assert!( @@ -914,6 +923,226 @@ fn compile_command_default_root_non_directory_in_cwd_exits_nonzero() { std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_default_root_matches_explicit_dsl_output() { + let root = unique_temp_dir("compile_default_root_matches_explicit"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + write_minimal_directory_compile_fixture(&root); + + let default = Command::new(daglang_bin()) + .arg("compile") + .current_dir(&root) + .output() + .expect("failed to run default-root compile"); + assert!( + default.status.success(), + "default-root compile should succeed: {}", + String::from_utf8_lossy(&default.stderr) + ); + + let explicit = Command::new(daglang_bin()) + .arg("compile") + .arg("dsl") + .current_dir(&root) + .output() + .expect("failed to run explicit-root compile"); + assert!( + explicit.status.success(), + "explicit-root compile should succeed: {}", + String::from_utf8_lossy(&explicit.stderr) + ); + + assert_eq!( + default.stdout, explicit.stdout, + "default-root and explicit dsl-root compile stdout should match" + ); + assert_eq!( + default.stderr, explicit.stderr, + "default-root and explicit dsl-root compile stderr should match" + ); + assert_no_stage_failures(&String::from_utf8_lossy(&default.stderr)); + assert!( + String::from_utf8_lossy(&default.stdout).contains("Compiled 1 module(s)"), + "default-root compile summary should report a single compiled module: {}", + String::from_utf8_lossy(&default.stdout) + ); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + +#[test] +fn compile_command_relative_and_absolute_roots_are_equivalent() { + let root = unique_temp_dir("compile_relative_absolute_root"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + write_minimal_directory_compile_fixture(&root); + let absolute_root = root.join("dsl"); + + let relative = Command::new(daglang_bin()) + .arg("compile") + .arg("dsl") + .current_dir(&root) + .output() + .expect("failed to run relative-root compile"); + assert!( + relative.status.success(), + "relative-root compile should succeed: {}", + String::from_utf8_lossy(&relative.stderr) + ); + + let absolute = Command::new(daglang_bin()) + .arg("compile") + .arg(&absolute_root) + .current_dir(&root) + .output() + .expect("failed to run absolute-root compile"); + assert!( + absolute.status.success(), + "absolute-root compile should succeed: {}", + String::from_utf8_lossy(&absolute.stderr) + ); + + assert_eq!( + relative.stdout, absolute.stdout, + "relative and absolute root compile stdout should match" + ); + assert_eq!( + relative.stderr, absolute.stderr, + "relative and absolute root compile stderr should match" + ); + assert_no_stage_failures(&String::from_utf8_lossy(&relative.stderr)); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + +#[test] +fn compile_command_curdir_segment_root_matches_plain_relative_output() { + let root = unique_temp_dir("compile_curdir_segment_root"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + write_minimal_directory_compile_fixture(&root); + + let plain = Command::new(daglang_bin()) + .arg("compile") + .arg("dsl") + .current_dir(&root) + .output() + .expect("failed to run plain-relative root compile"); + assert!( + plain.status.success(), + "plain-relative root compile should succeed: {}", + String::from_utf8_lossy(&plain.stderr) + ); + + let curdir = Command::new(daglang_bin()) + .arg("compile") + .arg("./dsl") + .current_dir(&root) + .output() + .expect("failed to run curdir-segment root compile"); + assert!( + curdir.status.success(), + "curdir-segment root compile should succeed: {}", + String::from_utf8_lossy(&curdir.stderr) + ); + + assert_eq!( + plain.stdout, curdir.stdout, + "plain-relative and curdir-segment root compile stdout should match" + ); + assert_eq!( + plain.stderr, curdir.stderr, + "plain-relative and curdir-segment root compile stderr should match" + ); + assert_no_stage_failures(&String::from_utf8_lossy(&plain.stderr)); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + +#[test] +fn compile_command_curdir_double_separator_root_matches_plain_relative_output() { + let root = unique_temp_dir("compile_curdir_double_separator_root"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + write_minimal_directory_compile_fixture(&root); + + let plain = Command::new(daglang_bin()) + .arg("compile") + .arg("dsl") + .current_dir(&root) + .output() + .expect("failed to run plain-relative root compile"); + assert!( + plain.status.success(), + "plain-relative root compile should succeed: {}", + String::from_utf8_lossy(&plain.stderr) + ); + + let curdir_double = Command::new(daglang_bin()) + .arg("compile") + .arg(".//dsl") + .current_dir(&root) + .output() + .expect("failed to run curdir-double root compile"); + assert!( + curdir_double.status.success(), + "curdir-double root compile should succeed: {}", + String::from_utf8_lossy(&curdir_double.stderr) + ); + + assert_eq!( + plain.stdout, curdir_double.stdout, + "plain-relative and curdir-double root compile stdout should match" + ); + assert_eq!( + plain.stderr, curdir_double.stderr, + "plain-relative and curdir-double root compile stderr should match" + ); + assert_no_stage_failures(&String::from_utf8_lossy(&plain.stderr)); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + +#[test] +fn compile_command_trailing_slash_root_matches_plain_relative_output() { + let root = unique_temp_dir("compile_trailing_slash_root"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + write_minimal_directory_compile_fixture(&root); + + let plain = Command::new(daglang_bin()) + .arg("compile") + .arg("dsl") + .current_dir(&root) + .output() + .expect("failed to run plain-relative root compile"); + assert!( + plain.status.success(), + "plain-relative root compile should succeed: {}", + String::from_utf8_lossy(&plain.stderr) + ); + + let trailing_slash = Command::new(daglang_bin()) + .arg("compile") + .arg("dsl/") + .current_dir(&root) + .output() + .expect("failed to run trailing-slash root compile"); + assert!( + trailing_slash.status.success(), + "trailing-slash root compile should succeed: {}", + String::from_utf8_lossy(&trailing_slash.stderr) + ); + + assert_eq!( + plain.stdout, trailing_slash.stdout, + "plain-relative and trailing-slash root compile stdout should match" + ); + assert_eq!( + plain.stderr, trailing_slash.stderr, + "plain-relative and trailing-slash root compile stderr should match" + ); + assert_no_stage_failures(&String::from_utf8_lossy(&plain.stderr)); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_single_file_fails_on_duplicate_definition() { let fixture = unique_temp_file("compile_single_file_duplicate_definition"); From ad4c8a871fe018c2000bd67aaeb7501e3ae0fac4 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 12:01:45 +0000 Subject: [PATCH 121/169] Expand compile absolute path normalization regressions Co-authored-by: Brian Searls --- .../daglang-cli/tests/compile_commands.rs | 456 ++++++++++++++++++ 1 file changed, 456 insertions(+) diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index aca7c3097d7..17282aaff11 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -1014,6 +1014,462 @@ fn compile_command_relative_and_absolute_roots_are_equivalent() { std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_absolute_mixed_segment_root_matches_canonical_absolute_output() { + let root = unique_temp_dir("compile_absolute_mixed_segment_root"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + write_minimal_directory_compile_fixture(&root); + let absolute_mixed_root = root.join(".").join("dsl"); + let canonical_root = root.join("dsl"); + + let mixed = Command::new(daglang_bin()) + .arg("compile") + .arg(&absolute_mixed_root) + .current_dir(&root) + .output() + .expect("failed to run mixed-segment absolute root compile"); + assert!( + mixed.status.success(), + "mixed-segment absolute root compile should succeed: {}", + String::from_utf8_lossy(&mixed.stderr) + ); + + let canonical = Command::new(daglang_bin()) + .arg("compile") + .arg(&canonical_root) + .current_dir(&root) + .output() + .expect("failed to run canonical absolute root compile"); + assert!( + canonical.status.success(), + "canonical absolute root compile should succeed: {}", + String::from_utf8_lossy(&canonical.stderr) + ); + + assert_eq!( + mixed.stdout, canonical.stdout, + "mixed-segment and canonical absolute-root compile stdout should match" + ); + assert_eq!( + mixed.stderr, canonical.stderr, + "mixed-segment and canonical absolute-root compile stderr should match" + ); + assert_no_stage_failures(&String::from_utf8_lossy(&mixed.stderr)); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + +#[test] +fn compile_command_absolute_parent_segment_root_matches_canonical_absolute_output() { + let root = unique_temp_dir("compile_absolute_parent_segment_root"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + write_minimal_directory_compile_fixture(&root); + let absolute_parent_segment_root = root.join("dsl/sample/.."); + let canonical_root = root.join("dsl"); + + let parent_segment = Command::new(daglang_bin()) + .arg("compile") + .arg(&absolute_parent_segment_root) + .current_dir(&root) + .output() + .expect("failed to run parent-segment absolute root compile"); + assert!( + parent_segment.status.success(), + "parent-segment absolute root compile should succeed: {}", + String::from_utf8_lossy(&parent_segment.stderr) + ); + + let canonical = Command::new(daglang_bin()) + .arg("compile") + .arg(&canonical_root) + .current_dir(&root) + .output() + .expect("failed to run canonical absolute root compile"); + assert!( + canonical.status.success(), + "canonical absolute root compile should succeed: {}", + String::from_utf8_lossy(&canonical.stderr) + ); + + assert_eq!( + parent_segment.stdout, canonical.stdout, + "parent-segment and canonical absolute-root compile stdout should match" + ); + assert_eq!( + parent_segment.stderr, canonical.stderr, + "parent-segment and canonical absolute-root compile stderr should match" + ); + assert_no_stage_failures(&String::from_utf8_lossy(&parent_segment.stderr)); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + +#[test] +fn compile_command_absolute_double_separator_root_matches_canonical_absolute_output() { + let root = unique_temp_dir("compile_absolute_double_separator_root"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + write_minimal_directory_compile_fixture(&root); + let absolute_double_separator_root = PathBuf::from(format!("{}//dsl", root.display())); + let canonical_root = root.join("dsl"); + + let double_separator = Command::new(daglang_bin()) + .arg("compile") + .arg(&absolute_double_separator_root) + .current_dir(&root) + .output() + .expect("failed to run double-separator absolute root compile"); + assert!( + double_separator.status.success(), + "double-separator absolute root compile should succeed: {}", + String::from_utf8_lossy(&double_separator.stderr) + ); + + let canonical = Command::new(daglang_bin()) + .arg("compile") + .arg(&canonical_root) + .current_dir(&root) + .output() + .expect("failed to run canonical absolute root compile"); + assert!( + canonical.status.success(), + "canonical absolute root compile should succeed: {}", + String::from_utf8_lossy(&canonical.stderr) + ); + + assert_eq!( + double_separator.stdout, canonical.stdout, + "double-separator and canonical absolute-root compile stdout should match" + ); + assert_eq!( + double_separator.stderr, canonical.stderr, + "double-separator and canonical absolute-root compile stderr should match" + ); + assert_no_stage_failures(&String::from_utf8_lossy(&double_separator.stderr)); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + +#[test] +fn compile_command_absolute_trailing_slash_root_matches_canonical_absolute_output() { + let root = unique_temp_dir("compile_absolute_trailing_slash_root"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + write_minimal_directory_compile_fixture(&root); + let absolute_trailing_slash_root = PathBuf::from(format!("{}/", root.join("dsl").display())); + let canonical_root = root.join("dsl"); + + let trailing_slash = Command::new(daglang_bin()) + .arg("compile") + .arg(&absolute_trailing_slash_root) + .current_dir(&root) + .output() + .expect("failed to run trailing-slash absolute root compile"); + assert!( + trailing_slash.status.success(), + "trailing-slash absolute root compile should succeed: {}", + String::from_utf8_lossy(&trailing_slash.stderr) + ); + + let canonical = Command::new(daglang_bin()) + .arg("compile") + .arg(&canonical_root) + .current_dir(&root) + .output() + .expect("failed to run canonical absolute root compile"); + assert!( + canonical.status.success(), + "canonical absolute root compile should succeed: {}", + String::from_utf8_lossy(&canonical.stderr) + ); + + assert_eq!( + trailing_slash.stdout, canonical.stdout, + "trailing-slash and canonical absolute-root compile stdout should match" + ); + assert_eq!( + trailing_slash.stderr, canonical.stderr, + "trailing-slash and canonical absolute-root compile stderr should match" + ); + assert_no_stage_failures(&String::from_utf8_lossy(&trailing_slash.stderr)); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + +#[test] +fn compile_command_absolute_double_separator_trailing_slash_root_matches_canonical_absolute_output() +{ + let root = unique_temp_dir("compile_absolute_double_separator_trailing_root"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + write_minimal_directory_compile_fixture(&root); + let absolute_double_separator_trailing_root = + PathBuf::from(format!("{}//dsl/", root.display())); + let canonical_root = root.join("dsl"); + + let double_separator_trailing = Command::new(daglang_bin()) + .arg("compile") + .arg(&absolute_double_separator_trailing_root) + .current_dir(&root) + .output() + .expect("failed to run double-separator trailing absolute root compile"); + assert!( + double_separator_trailing.status.success(), + "double-separator trailing absolute root compile should succeed: {}", + String::from_utf8_lossy(&double_separator_trailing.stderr) + ); + + let canonical = Command::new(daglang_bin()) + .arg("compile") + .arg(&canonical_root) + .current_dir(&root) + .output() + .expect("failed to run canonical absolute root compile"); + assert!( + canonical.status.success(), + "canonical absolute root compile should succeed: {}", + String::from_utf8_lossy(&canonical.stderr) + ); + + assert_eq!( + double_separator_trailing.stdout, canonical.stdout, + "double-separator trailing and canonical absolute-root compile stdout should match" + ); + assert_eq!( + double_separator_trailing.stderr, canonical.stderr, + "double-separator trailing and canonical absolute-root compile stderr should match" + ); + assert_no_stage_failures(&String::from_utf8_lossy(&double_separator_trailing.stderr)); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + +#[test] +fn compile_command_absolute_mixed_segment_single_file_target_matches_canonical_output() { + let root = unique_temp_dir("compile_absolute_mixed_segment_single_file"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + write_minimal_directory_compile_fixture(&root); + let absolute_mixed_target = root.join(".").join("dsl/sample/main.dag"); + let canonical_target = root.join("dsl/sample/main.dag"); + + let mixed = Command::new(daglang_bin()) + .arg("compile") + .arg(&absolute_mixed_target) + .current_dir(&root) + .output() + .expect("failed to run mixed-segment absolute single-file compile"); + assert!( + mixed.status.success(), + "mixed-segment absolute single-file compile should succeed: {}", + String::from_utf8_lossy(&mixed.stderr) + ); + + let canonical = Command::new(daglang_bin()) + .arg("compile") + .arg(&canonical_target) + .current_dir(&root) + .output() + .expect("failed to run canonical absolute single-file compile"); + assert!( + canonical.status.success(), + "canonical absolute single-file compile should succeed: {}", + String::from_utf8_lossy(&canonical.stderr) + ); + + assert_eq!( + mixed.stdout, canonical.stdout, + "mixed-segment and canonical absolute single-file compile stdout should match" + ); + assert_eq!( + mixed.stderr, canonical.stderr, + "mixed-segment and canonical absolute single-file compile stderr should match" + ); + assert_no_stage_failures(&String::from_utf8_lossy(&mixed.stderr)); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + +#[test] +fn compile_command_absolute_parent_segment_single_file_target_matches_canonical_output() { + let root = unique_temp_dir("compile_absolute_parent_segment_single_file"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + write_minimal_directory_compile_fixture(&root); + let absolute_parent_segment_target = root.join("dsl/sample/../sample/main.dag"); + let canonical_target = root.join("dsl/sample/main.dag"); + + let parent_segment = Command::new(daglang_bin()) + .arg("compile") + .arg(&absolute_parent_segment_target) + .current_dir(&root) + .output() + .expect("failed to run parent-segment absolute single-file compile"); + assert!( + parent_segment.status.success(), + "parent-segment absolute single-file compile should succeed: {}", + String::from_utf8_lossy(&parent_segment.stderr) + ); + + let canonical = Command::new(daglang_bin()) + .arg("compile") + .arg(&canonical_target) + .current_dir(&root) + .output() + .expect("failed to run canonical absolute single-file compile"); + assert!( + canonical.status.success(), + "canonical absolute single-file compile should succeed: {}", + String::from_utf8_lossy(&canonical.stderr) + ); + + assert_eq!( + parent_segment.stdout, canonical.stdout, + "parent-segment and canonical absolute single-file compile stdout should match" + ); + assert_eq!( + parent_segment.stderr, canonical.stderr, + "parent-segment and canonical absolute single-file compile stderr should match" + ); + assert_no_stage_failures(&String::from_utf8_lossy(&parent_segment.stderr)); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + +#[test] +fn compile_command_absolute_double_separator_single_file_target_matches_canonical_output() { + let root = unique_temp_dir("compile_absolute_double_separator_single_file"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + write_minimal_directory_compile_fixture(&root); + let absolute_double_separator_target = + PathBuf::from(format!("{}//dsl//sample//main.dag", root.display())); + let canonical_target = root.join("dsl/sample/main.dag"); + + let double_separator = Command::new(daglang_bin()) + .arg("compile") + .arg(&absolute_double_separator_target) + .current_dir(&root) + .output() + .expect("failed to run double-separator absolute single-file compile"); + assert!( + double_separator.status.success(), + "double-separator absolute single-file compile should succeed: {}", + String::from_utf8_lossy(&double_separator.stderr) + ); + + let canonical = Command::new(daglang_bin()) + .arg("compile") + .arg(&canonical_target) + .current_dir(&root) + .output() + .expect("failed to run canonical absolute single-file compile"); + assert!( + canonical.status.success(), + "canonical absolute single-file compile should succeed: {}", + String::from_utf8_lossy(&canonical.stderr) + ); + + assert_eq!( + double_separator.stdout, canonical.stdout, + "double-separator and canonical absolute single-file compile stdout should match" + ); + assert_eq!( + double_separator.stderr, canonical.stderr, + "double-separator and canonical absolute single-file compile stderr should match" + ); + assert_no_stage_failures(&String::from_utf8_lossy(&double_separator.stderr)); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + +#[test] +fn compile_command_absolute_trailing_slash_single_file_target_matches_canonical_output() { + let root = unique_temp_dir("compile_absolute_trailing_single_file"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + write_minimal_directory_compile_fixture(&root); + let absolute_trailing_target = + PathBuf::from(format!("{}/", root.join("dsl/sample/main.dag").display())); + let canonical_target = root.join("dsl/sample/main.dag"); + + let trailing_slash = Command::new(daglang_bin()) + .arg("compile") + .arg(&absolute_trailing_target) + .current_dir(&root) + .output() + .expect("failed to run trailing-slash absolute single-file compile"); + assert!( + trailing_slash.status.success(), + "trailing-slash absolute single-file compile should succeed: {}", + String::from_utf8_lossy(&trailing_slash.stderr) + ); + + let canonical = Command::new(daglang_bin()) + .arg("compile") + .arg(&canonical_target) + .current_dir(&root) + .output() + .expect("failed to run canonical absolute single-file compile"); + assert!( + canonical.status.success(), + "canonical absolute single-file compile should succeed: {}", + String::from_utf8_lossy(&canonical.stderr) + ); + + assert_eq!( + trailing_slash.stdout, canonical.stdout, + "trailing-slash and canonical absolute single-file compile stdout should match" + ); + assert_eq!( + trailing_slash.stderr, canonical.stderr, + "trailing-slash and canonical absolute single-file compile stderr should match" + ); + assert_no_stage_failures(&String::from_utf8_lossy(&trailing_slash.stderr)); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + +#[test] +fn compile_command_absolute_double_separator_trailing_slash_single_file_target_matches_canonical_output( +) { + let root = unique_temp_dir("compile_absolute_double_separator_trailing_single_file"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + write_minimal_directory_compile_fixture(&root); + let absolute_double_separator_trailing_target = + PathBuf::from(format!("{}//dsl//sample//main.dag/", root.display())); + let canonical_target = root.join("dsl/sample/main.dag"); + + let double_separator_trailing = Command::new(daglang_bin()) + .arg("compile") + .arg(&absolute_double_separator_trailing_target) + .current_dir(&root) + .output() + .expect("failed to run double-separator trailing absolute single-file compile"); + assert!( + double_separator_trailing.status.success(), + "double-separator trailing absolute single-file compile should succeed: {}", + String::from_utf8_lossy(&double_separator_trailing.stderr) + ); + + let canonical = Command::new(daglang_bin()) + .arg("compile") + .arg(&canonical_target) + .current_dir(&root) + .output() + .expect("failed to run canonical absolute single-file compile"); + assert!( + canonical.status.success(), + "canonical absolute single-file compile should succeed: {}", + String::from_utf8_lossy(&canonical.stderr) + ); + + assert_eq!( + double_separator_trailing.stdout, canonical.stdout, + "double-separator trailing and canonical absolute single-file compile stdout should match" + ); + assert_eq!( + double_separator_trailing.stderr, canonical.stderr, + "double-separator trailing and canonical absolute single-file compile stderr should match" + ); + assert_no_stage_failures(&String::from_utf8_lossy(&double_separator_trailing.stderr)); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_curdir_segment_root_matches_plain_relative_output() { let root = unique_temp_dir("compile_curdir_segment_root"); From b19de309556949dc931472c58454fe465a4ee1ce Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 12:05:31 +0000 Subject: [PATCH 122/169] Add compile absolute failure-path normalization regressions Co-authored-by: Brian Searls --- .../daglang-cli/tests/compile_commands.rs | 203 ++++++++++++++++++ 1 file changed, 203 insertions(+) diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index 17282aaff11..9a131e32126 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -1470,6 +1470,209 @@ fn compile_command_absolute_double_separator_trailing_slash_single_file_target_m std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_absolute_missing_root_variants_match_canonical_output() { + let root = unique_temp_dir("compile_absolute_missing_root_variants"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + std::fs::create_dir_all(root.join("anchor")).expect("failed to create anchor directory"); + let canonical_missing_root = root.join("missing_root"); + + let canonical = Command::new(daglang_bin()) + .arg("compile") + .arg(&canonical_missing_root) + .current_dir(&root) + .output() + .expect("failed to run canonical absolute missing-root compile"); + assert!( + !canonical.status.success(), + "canonical absolute missing-root compile should fail" + ); + let canonical_stderr = String::from_utf8_lossy(&canonical.stderr); + assert_no_stage_failures(&canonical_stderr); + assert!( + canonical_stderr.contains(&canonical_missing_root.display().to_string()), + "canonical missing-root diagnostic should contain normalized absolute path: {canonical_stderr}" + ); + + let variants = vec![ + ("mixed", root.join(".").join("missing_root")), + ("parent", root.join("anchor/../missing_root")), + ( + "double_separator", + PathBuf::from(format!("{}//missing_root", root.display())), + ), + ( + "trailing_slash", + PathBuf::from(format!("{}/", canonical_missing_root.display())), + ), + ( + "double_separator_trailing", + PathBuf::from(format!("{}//missing_root/", root.display())), + ), + ]; + + for (label, variant_path) in variants { + let variant = Command::new(daglang_bin()) + .arg("compile") + .arg(&variant_path) + .current_dir(&root) + .output() + .unwrap_or_else(|_| { + panic!("failed to run {label} absolute missing-root compile variant") + }); + assert!( + !variant.status.success(), + "{label} absolute missing-root compile variant should fail" + ); + assert_eq!( + variant.stdout, canonical.stdout, + "{label} missing-root variant stdout should match canonical output" + ); + assert_eq!( + variant.stderr, canonical.stderr, + "{label} missing-root variant stderr should match canonical output" + ); + } + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + +#[test] +fn compile_command_absolute_non_directory_root_variants_match_canonical_output() { + let root = unique_temp_dir("compile_absolute_non_directory_root_variants"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + std::fs::create_dir_all(root.join("anchor")).expect("failed to create anchor directory"); + let canonical_non_directory_root = root.join("input.txt"); + std::fs::write(&canonical_non_directory_root, "not a directory") + .expect("failed to write non-directory root fixture"); + + let canonical = Command::new(daglang_bin()) + .arg("compile") + .arg(&canonical_non_directory_root) + .current_dir(&root) + .output() + .expect("failed to run canonical absolute non-directory-root compile"); + assert!( + !canonical.status.success(), + "canonical absolute non-directory-root compile should fail" + ); + let canonical_stderr = String::from_utf8_lossy(&canonical.stderr); + assert_no_stage_failures(&canonical_stderr); + assert!( + canonical_stderr.contains(&canonical_non_directory_root.display().to_string()), + "canonical non-directory-root diagnostic should contain normalized absolute path: {canonical_stderr}" + ); + + let variants = vec![ + ("mixed", root.join(".").join("input.txt")), + ("parent", root.join("anchor/../input.txt")), + ( + "double_separator", + PathBuf::from(format!("{}//input.txt", root.display())), + ), + ( + "trailing_slash", + PathBuf::from(format!("{}/", canonical_non_directory_root.display())), + ), + ( + "double_separator_trailing", + PathBuf::from(format!("{}//input.txt/", root.display())), + ), + ]; + + for (label, variant_path) in variants { + let variant = Command::new(daglang_bin()) + .arg("compile") + .arg(&variant_path) + .current_dir(&root) + .output() + .unwrap_or_else(|_| { + panic!("failed to run {label} absolute non-directory-root compile variant") + }); + assert!( + !variant.status.success(), + "{label} absolute non-directory-root compile variant should fail" + ); + assert_eq!( + variant.stdout, canonical.stdout, + "{label} non-directory-root variant stdout should match canonical output" + ); + assert_eq!( + variant.stderr, canonical.stderr, + "{label} non-directory-root variant stderr should match canonical output" + ); + } + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + +#[test] +fn compile_command_absolute_missing_single_file_variants_match_canonical_output() { + let root = unique_temp_dir("compile_absolute_missing_single_file_variants"); + std::fs::create_dir_all(root.join("dsl/sample")).expect("failed to create temp root fixture"); + std::fs::create_dir_all(root.join("anchor")).expect("failed to create anchor directory"); + let canonical_missing_single_file = root.join("dsl/sample/missing.dag"); + + let canonical = Command::new(daglang_bin()) + .arg("compile") + .arg(&canonical_missing_single_file) + .current_dir(&root) + .output() + .expect("failed to run canonical absolute missing single-file compile"); + assert!( + !canonical.status.success(), + "canonical absolute missing single-file compile should fail" + ); + let canonical_stderr = String::from_utf8_lossy(&canonical.stderr); + assert_no_stage_failures(&canonical_stderr); + assert!( + canonical_stderr.contains(&canonical_missing_single_file.display().to_string()), + "canonical missing single-file diagnostic should contain normalized absolute path: {canonical_stderr}" + ); + + let variants = vec![ + ("mixed", root.join(".").join("dsl/sample/missing.dag")), + ("parent", root.join("dsl/sample/../sample/missing.dag")), + ( + "double_separator", + PathBuf::from(format!("{}//dsl//sample//missing.dag", root.display())), + ), + ( + "trailing_slash", + PathBuf::from(format!("{}/", canonical_missing_single_file.display())), + ), + ( + "double_separator_trailing", + PathBuf::from(format!("{}//dsl//sample//missing.dag/", root.display())), + ), + ]; + + for (label, variant_path) in variants { + let variant = Command::new(daglang_bin()) + .arg("compile") + .arg(&variant_path) + .current_dir(&root) + .output() + .unwrap_or_else(|_| { + panic!("failed to run {label} absolute missing single-file compile variant") + }); + assert!( + !variant.status.success(), + "{label} absolute missing single-file compile variant should fail" + ); + assert_eq!( + variant.stdout, canonical.stdout, + "{label} missing single-file variant stdout should match canonical output" + ); + assert_eq!( + variant.stderr, canonical.stderr, + "{label} missing single-file variant stderr should match canonical output" + ); + } + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_curdir_segment_root_matches_plain_relative_output() { let root = unique_temp_dir("compile_curdir_segment_root"); From baa875f73aec650ccc6b2f0afee9dbfdb601aba5 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 12:09:54 +0000 Subject: [PATCH 123/169] Add compile parent-segment normalization regressions Co-authored-by: Brian Searls --- .../daglang-cli/tests/compile_commands.rs | 366 ++++++++++++++++++ 1 file changed, 366 insertions(+) diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index 9a131e32126..926fe8cd26e 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -231,6 +231,146 @@ fn compile_command_parent_segment_missing_root_is_normalized_and_equivalent() { std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_parent_double_separator_missing_root_is_normalized_and_equivalent() { + let root = unique_temp_dir("compile_parent_double_separator_missing_root"); + std::fs::create_dir_all(root.join("child")).expect("failed to create temp root"); + let missing = root.join("missing_root"); + + let parent_double = Command::new(daglang_bin()) + .arg("compile") + .arg("..//missing_root") + .current_dir(root.join("child")) + .output() + .expect("failed to run parent-double missing-root compile"); + assert!( + !parent_double.status.success(), + "parent-double missing-root compile should fail" + ); + + let absolute = Command::new(daglang_bin()) + .arg("compile") + .arg(&missing) + .current_dir(root.join("child")) + .output() + .expect("failed to run absolute missing-root compile"); + assert!( + !absolute.status.success(), + "absolute missing-root compile should fail" + ); + + assert_eq!( + parent_double.stdout, absolute.stdout, + "parent-double and absolute missing-root compile stdout should match" + ); + assert_eq!( + parent_double.stderr, absolute.stderr, + "parent-double and absolute missing-root compile stderr should match" + ); + let stderr = String::from_utf8_lossy(&parent_double.stderr); + assert!( + stderr.contains(&missing.display().to_string()), + "parent-double missing-root diagnostic should normalize to absolute path: {stderr}" + ); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + +#[test] +fn compile_command_parent_segment_non_directory_root_matches_absolute_output() { + let root = unique_temp_dir("compile_parent_segment_non_directory_root"); + std::fs::create_dir_all(root.join("child")).expect("failed to create temp root"); + let root_file = root.join("input.txt"); + std::fs::write(&root_file, "not a directory").expect("failed to write non-directory root"); + + let parent_segment = Command::new(daglang_bin()) + .arg("compile") + .arg("../input.txt") + .current_dir(root.join("child")) + .output() + .expect("failed to run parent-segment non-directory-root compile"); + assert!( + !parent_segment.status.success(), + "parent-segment non-directory-root compile should fail" + ); + + let absolute = Command::new(daglang_bin()) + .arg("compile") + .arg(&root_file) + .current_dir(root.join("child")) + .output() + .expect("failed to run absolute non-directory-root compile"); + assert!( + !absolute.status.success(), + "absolute non-directory-root compile should fail" + ); + + assert_eq!( + parent_segment.stdout, absolute.stdout, + "parent-segment and absolute non-directory-root compile stdout should match" + ); + assert_eq!( + parent_segment.stderr, absolute.stderr, + "parent-segment and absolute non-directory-root compile stderr should match" + ); + let stderr = String::from_utf8_lossy(&parent_segment.stderr); + assert!( + stderr.contains(&root_file.display().to_string()), + "parent-segment non-directory-root diagnostic should normalize to absolute path: {stderr}" + ); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + +#[test] +fn compile_command_parent_double_separator_non_directory_root_matches_absolute_output() { + let root = unique_temp_dir("compile_parent_double_non_directory_root"); + std::fs::create_dir_all(root.join("child")).expect("failed to create temp root"); + let root_file = root.join("input.txt"); + std::fs::write(&root_file, "not a directory").expect("failed to write non-directory root"); + + let parent_double = Command::new(daglang_bin()) + .arg("compile") + .arg("..//input.txt") + .current_dir(root.join("child")) + .output() + .expect("failed to run parent-double non-directory-root compile"); + assert!( + !parent_double.status.success(), + "parent-double non-directory-root compile should fail" + ); + + let absolute = Command::new(daglang_bin()) + .arg("compile") + .arg(&root_file) + .current_dir(root.join("child")) + .output() + .expect("failed to run absolute non-directory-root compile"); + assert!( + !absolute.status.success(), + "absolute non-directory-root compile should fail" + ); + + assert_eq!( + parent_double.stdout, absolute.stdout, + "parent-double and absolute non-directory-root compile stdout should match" + ); + assert_eq!( + parent_double.stderr, absolute.stderr, + "parent-double and absolute non-directory-root compile stderr should match" + ); + let stderr = String::from_utf8_lossy(&parent_double.stderr); + assert!( + stderr.contains(&root_file.display().to_string()), + "parent-double non-directory-root diagnostic should normalize to absolute path: {stderr}" + ); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_curdir_segment_missing_root_matches_plain_relative_output() { let root = unique_temp_dir("compile_curdir_segment_missing_root"); @@ -632,6 +772,232 @@ fn compile_command_parent_segment_missing_single_file_target_is_normalized_and_e std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_parent_double_separator_missing_single_file_target_is_normalized_and_equivalent() { + let root = unique_temp_dir("compile_parent_double_missing_single_file_target"); + std::fs::create_dir_all(root.join("child")).expect("failed to create temp root"); + let missing = root.join("missing.dag"); + + let parent_double = Command::new(daglang_bin()) + .arg("compile") + .arg("..//missing.dag") + .current_dir(root.join("child")) + .output() + .expect("failed to run parent-double missing single-file compile"); + assert!( + !parent_double.status.success(), + "parent-double missing single-file compile should fail" + ); + + let absolute = Command::new(daglang_bin()) + .arg("compile") + .arg(&missing) + .current_dir(root.join("child")) + .output() + .expect("failed to run absolute missing single-file compile"); + assert!( + !absolute.status.success(), + "absolute missing single-file compile should fail" + ); + + assert_eq!( + parent_double.stdout, absolute.stdout, + "parent-double and absolute missing single-file compile stdout should match" + ); + assert_eq!( + parent_double.stderr, absolute.stderr, + "parent-double and absolute missing single-file compile stderr should match" + ); + let stderr = String::from_utf8_lossy(&parent_double.stderr); + assert!( + stderr.contains(&format!("failed to read {}", missing.display())), + "parent-double missing single-file diagnostic should normalize to absolute path: {stderr}" + ); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + +#[test] +fn compile_command_parent_segment_root_matches_absolute_output() { + let root = unique_temp_dir("compile_parent_segment_root"); + std::fs::create_dir_all(root.join("child")).expect("failed to create temp root"); + write_minimal_directory_compile_fixture(&root); + let absolute_root = root.join("dsl"); + + let parent_segment = Command::new(daglang_bin()) + .arg("compile") + .arg("../dsl") + .current_dir(root.join("child")) + .output() + .expect("failed to run parent-segment root compile"); + assert!( + parent_segment.status.success(), + "parent-segment root compile should succeed: {}", + String::from_utf8_lossy(&parent_segment.stderr) + ); + + let absolute = Command::new(daglang_bin()) + .arg("compile") + .arg(&absolute_root) + .current_dir(root.join("child")) + .output() + .expect("failed to run absolute root compile"); + assert!( + absolute.status.success(), + "absolute root compile should succeed: {}", + String::from_utf8_lossy(&absolute.stderr) + ); + + assert_eq!( + parent_segment.stdout, absolute.stdout, + "parent-segment and absolute root compile stdout should match" + ); + assert_eq!( + parent_segment.stderr, absolute.stderr, + "parent-segment and absolute root compile stderr should match" + ); + assert_no_stage_failures(&String::from_utf8_lossy(&parent_segment.stderr)); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + +#[test] +fn compile_command_parent_double_separator_root_matches_absolute_output() { + let root = unique_temp_dir("compile_parent_double_separator_root"); + std::fs::create_dir_all(root.join("child")).expect("failed to create temp root"); + write_minimal_directory_compile_fixture(&root); + let absolute_root = root.join("dsl"); + + let parent_double = Command::new(daglang_bin()) + .arg("compile") + .arg("..//dsl") + .current_dir(root.join("child")) + .output() + .expect("failed to run parent-double root compile"); + assert!( + parent_double.status.success(), + "parent-double root compile should succeed: {}", + String::from_utf8_lossy(&parent_double.stderr) + ); + + let absolute = Command::new(daglang_bin()) + .arg("compile") + .arg(&absolute_root) + .current_dir(root.join("child")) + .output() + .expect("failed to run absolute root compile"); + assert!( + absolute.status.success(), + "absolute root compile should succeed: {}", + String::from_utf8_lossy(&absolute.stderr) + ); + + assert_eq!( + parent_double.stdout, absolute.stdout, + "parent-double and absolute root compile stdout should match" + ); + assert_eq!( + parent_double.stderr, absolute.stderr, + "parent-double and absolute root compile stderr should match" + ); + assert_no_stage_failures(&String::from_utf8_lossy(&parent_double.stderr)); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + +#[test] +fn compile_command_parent_segment_single_file_target_matches_absolute_output() { + let root = unique_temp_dir("compile_parent_segment_single_file_target"); + std::fs::create_dir_all(root.join("child")).expect("failed to create temp root child"); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp root sample"); + let source = root.join("sample/main.dag"); + std::fs::write(&source, "module sample.main\nfn run() -> Unit { }") + .expect("failed to write single-file source"); + + let parent_segment = Command::new(daglang_bin()) + .arg("compile") + .arg("../sample/main.dag") + .current_dir(root.join("child")) + .output() + .expect("failed to run parent-segment single-file compile"); + assert!( + parent_segment.status.success(), + "parent-segment single-file compile should succeed: {}", + String::from_utf8_lossy(&parent_segment.stderr) + ); + + let absolute = Command::new(daglang_bin()) + .arg("compile") + .arg(&source) + .current_dir(root.join("child")) + .output() + .expect("failed to run absolute single-file compile"); + assert!( + absolute.status.success(), + "absolute single-file compile should succeed: {}", + String::from_utf8_lossy(&absolute.stderr) + ); + + assert_eq!( + parent_segment.stdout, absolute.stdout, + "parent-segment and absolute single-file compile stdout should match" + ); + assert_eq!( + parent_segment.stderr, absolute.stderr, + "parent-segment and absolute single-file compile stderr should match" + ); + assert_no_stage_failures(&String::from_utf8_lossy(&parent_segment.stderr)); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + +#[test] +fn compile_command_parent_double_separator_single_file_target_matches_absolute_output() { + let root = unique_temp_dir("compile_parent_double_single_file_target"); + std::fs::create_dir_all(root.join("child")).expect("failed to create temp root child"); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp root sample"); + let source = root.join("sample/main.dag"); + std::fs::write(&source, "module sample.main\nfn run() -> Unit { }") + .expect("failed to write single-file source"); + + let parent_double = Command::new(daglang_bin()) + .arg("compile") + .arg("..//sample/main.dag") + .current_dir(root.join("child")) + .output() + .expect("failed to run parent-double single-file compile"); + assert!( + parent_double.status.success(), + "parent-double single-file compile should succeed: {}", + String::from_utf8_lossy(&parent_double.stderr) + ); + + let absolute = Command::new(daglang_bin()) + .arg("compile") + .arg(&source) + .current_dir(root.join("child")) + .output() + .expect("failed to run absolute single-file compile"); + assert!( + absolute.status.success(), + "absolute single-file compile should succeed: {}", + String::from_utf8_lossy(&absolute.stderr) + ); + + assert_eq!( + parent_double.stdout, absolute.stdout, + "parent-double and absolute single-file compile stdout should match" + ); + assert_eq!( + parent_double.stderr, absolute.stderr, + "parent-double and absolute single-file compile stderr should match" + ); + assert_no_stage_failures(&String::from_utf8_lossy(&parent_double.stderr)); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_trailing_slash_single_file_target_matches_plain_relative_output() { let root = unique_temp_dir("compile_trailing_slash_single_file_target"); From 8d7a7b2c3ccb5ee20a99d547b3a08a27ebe04b08 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 12:15:14 +0000 Subject: [PATCH 124/169] Add compile parent-curdir normalization regressions Co-authored-by: Brian Searls --- .../daglang-cli/tests/compile_commands.rs | 460 ++++++++++++++++++ 1 file changed, 460 insertions(+) diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index 926fe8cd26e..f09f3d2ebc9 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -998,6 +998,466 @@ fn compile_command_parent_double_separator_single_file_target_matches_absolute_o std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_parent_curdir_double_separator_missing_root_is_normalized_and_equivalent() { + let root = unique_temp_dir("compile_parent_curdir_double_missing_root"); + std::fs::create_dir_all(root.join("child")).expect("failed to create temp root"); + let missing = root.join("missing_root"); + + let parent_curdir_double = Command::new(daglang_bin()) + .arg("compile") + .arg(".././missing_root//") + .current_dir(root.join("child")) + .output() + .expect("failed to run parent-curdir-double missing-root compile"); + assert!( + !parent_curdir_double.status.success(), + "parent-curdir-double missing-root compile should fail" + ); + + let absolute = Command::new(daglang_bin()) + .arg("compile") + .arg(&missing) + .current_dir(root.join("child")) + .output() + .expect("failed to run absolute missing-root compile"); + assert!( + !absolute.status.success(), + "absolute missing-root compile should fail" + ); + + assert_eq!( + parent_curdir_double.stdout, absolute.stdout, + "parent-curdir-double and absolute missing-root compile stdout should match" + ); + assert_eq!( + parent_curdir_double.stderr, absolute.stderr, + "parent-curdir-double and absolute missing-root compile stderr should match" + ); + let stderr = String::from_utf8_lossy(&parent_curdir_double.stderr); + assert!( + stderr.contains(&missing.display().to_string()), + "parent-curdir-double missing-root diagnostic should normalize to absolute path: {stderr}" + ); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + +#[test] +fn compile_command_parent_curdir_trailing_slash_missing_root_is_normalized_and_equivalent() { + let root = unique_temp_dir("compile_parent_curdir_trailing_missing_root"); + std::fs::create_dir_all(root.join("child")).expect("failed to create temp root"); + let missing = root.join("missing_root"); + + let parent_curdir_trailing = Command::new(daglang_bin()) + .arg("compile") + .arg(".././missing_root/") + .current_dir(root.join("child")) + .output() + .expect("failed to run parent-curdir-trailing missing-root compile"); + assert!( + !parent_curdir_trailing.status.success(), + "parent-curdir-trailing missing-root compile should fail" + ); + + let absolute = Command::new(daglang_bin()) + .arg("compile") + .arg(&missing) + .current_dir(root.join("child")) + .output() + .expect("failed to run absolute missing-root compile"); + assert!( + !absolute.status.success(), + "absolute missing-root compile should fail" + ); + + assert_eq!( + parent_curdir_trailing.stdout, absolute.stdout, + "parent-curdir-trailing and absolute missing-root compile stdout should match" + ); + assert_eq!( + parent_curdir_trailing.stderr, absolute.stderr, + "parent-curdir-trailing and absolute missing-root compile stderr should match" + ); + let stderr = String::from_utf8_lossy(&parent_curdir_trailing.stderr); + assert!( + stderr.contains(&missing.display().to_string()), + "parent-curdir-trailing missing-root diagnostic should normalize to absolute path: {stderr}" + ); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + +#[test] +fn compile_command_parent_curdir_double_separator_non_directory_root_matches_absolute_output() { + let root = unique_temp_dir("compile_parent_curdir_double_non_directory_root"); + std::fs::create_dir_all(root.join("child")).expect("failed to create temp root"); + let root_file = root.join("input.txt"); + std::fs::write(&root_file, "not a directory").expect("failed to write non-directory root"); + + let parent_curdir_double = Command::new(daglang_bin()) + .arg("compile") + .arg(".././input.txt//") + .current_dir(root.join("child")) + .output() + .expect("failed to run parent-curdir-double non-directory-root compile"); + assert!( + !parent_curdir_double.status.success(), + "parent-curdir-double non-directory-root compile should fail" + ); + + let absolute = Command::new(daglang_bin()) + .arg("compile") + .arg(&root_file) + .current_dir(root.join("child")) + .output() + .expect("failed to run absolute non-directory-root compile"); + assert!( + !absolute.status.success(), + "absolute non-directory-root compile should fail" + ); + + assert_eq!( + parent_curdir_double.stdout, absolute.stdout, + "parent-curdir-double and absolute non-directory-root compile stdout should match" + ); + assert_eq!( + parent_curdir_double.stderr, absolute.stderr, + "parent-curdir-double and absolute non-directory-root compile stderr should match" + ); + let stderr = String::from_utf8_lossy(&parent_curdir_double.stderr); + assert!( + stderr.contains(&root_file.display().to_string()), + "parent-curdir-double non-directory-root diagnostic should normalize to absolute path: {stderr}" + ); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + +#[test] +fn compile_command_parent_curdir_trailing_slash_non_directory_root_matches_absolute_output() { + let root = unique_temp_dir("compile_parent_curdir_trailing_non_directory_root"); + std::fs::create_dir_all(root.join("child")).expect("failed to create temp root"); + let root_file = root.join("input.txt"); + std::fs::write(&root_file, "not a directory").expect("failed to write non-directory root"); + + let parent_curdir_trailing = Command::new(daglang_bin()) + .arg("compile") + .arg(".././input.txt/") + .current_dir(root.join("child")) + .output() + .expect("failed to run parent-curdir-trailing non-directory-root compile"); + assert!( + !parent_curdir_trailing.status.success(), + "parent-curdir-trailing non-directory-root compile should fail" + ); + + let absolute = Command::new(daglang_bin()) + .arg("compile") + .arg(&root_file) + .current_dir(root.join("child")) + .output() + .expect("failed to run absolute non-directory-root compile"); + assert!( + !absolute.status.success(), + "absolute non-directory-root compile should fail" + ); + + assert_eq!( + parent_curdir_trailing.stdout, absolute.stdout, + "parent-curdir-trailing and absolute non-directory-root compile stdout should match" + ); + assert_eq!( + parent_curdir_trailing.stderr, absolute.stderr, + "parent-curdir-trailing and absolute non-directory-root compile stderr should match" + ); + let stderr = String::from_utf8_lossy(&parent_curdir_trailing.stderr); + assert!( + stderr.contains(&root_file.display().to_string()), + "parent-curdir-trailing non-directory-root diagnostic should normalize to absolute path: {stderr}" + ); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + +#[test] +fn compile_command_parent_curdir_double_separator_root_matches_absolute_output() { + let root = unique_temp_dir("compile_parent_curdir_double_root"); + std::fs::create_dir_all(root.join("child")).expect("failed to create temp root"); + write_minimal_directory_compile_fixture(&root); + let absolute_root = root.join("dsl"); + + let parent_curdir_double = Command::new(daglang_bin()) + .arg("compile") + .arg(".././dsl//") + .current_dir(root.join("child")) + .output() + .expect("failed to run parent-curdir-double root compile"); + assert!( + parent_curdir_double.status.success(), + "parent-curdir-double root compile should succeed: {}", + String::from_utf8_lossy(&parent_curdir_double.stderr) + ); + + let absolute = Command::new(daglang_bin()) + .arg("compile") + .arg(&absolute_root) + .current_dir(root.join("child")) + .output() + .expect("failed to run absolute root compile"); + assert!( + absolute.status.success(), + "absolute root compile should succeed: {}", + String::from_utf8_lossy(&absolute.stderr) + ); + + assert_eq!( + parent_curdir_double.stdout, absolute.stdout, + "parent-curdir-double and absolute root compile stdout should match" + ); + assert_eq!( + parent_curdir_double.stderr, absolute.stderr, + "parent-curdir-double and absolute root compile stderr should match" + ); + assert_no_stage_failures(&String::from_utf8_lossy(&parent_curdir_double.stderr)); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + +#[test] +fn compile_command_parent_curdir_trailing_slash_root_matches_absolute_output() { + let root = unique_temp_dir("compile_parent_curdir_trailing_root"); + std::fs::create_dir_all(root.join("child")).expect("failed to create temp root"); + write_minimal_directory_compile_fixture(&root); + let absolute_root = root.join("dsl"); + + let parent_curdir_trailing = Command::new(daglang_bin()) + .arg("compile") + .arg(".././dsl/") + .current_dir(root.join("child")) + .output() + .expect("failed to run parent-curdir-trailing root compile"); + assert!( + parent_curdir_trailing.status.success(), + "parent-curdir-trailing root compile should succeed: {}", + String::from_utf8_lossy(&parent_curdir_trailing.stderr) + ); + + let absolute = Command::new(daglang_bin()) + .arg("compile") + .arg(&absolute_root) + .current_dir(root.join("child")) + .output() + .expect("failed to run absolute root compile"); + assert!( + absolute.status.success(), + "absolute root compile should succeed: {}", + String::from_utf8_lossy(&absolute.stderr) + ); + + assert_eq!( + parent_curdir_trailing.stdout, absolute.stdout, + "parent-curdir-trailing and absolute root compile stdout should match" + ); + assert_eq!( + parent_curdir_trailing.stderr, absolute.stderr, + "parent-curdir-trailing and absolute root compile stderr should match" + ); + assert_no_stage_failures(&String::from_utf8_lossy(&parent_curdir_trailing.stderr)); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + +#[test] +fn compile_command_parent_curdir_double_separator_missing_single_file_target_is_normalized_and_equivalent( +) { + let root = unique_temp_dir("compile_parent_curdir_double_missing_single_file"); + std::fs::create_dir_all(root.join("child")).expect("failed to create temp root"); + let missing = root.join("missing.dag"); + + let parent_curdir_double = Command::new(daglang_bin()) + .arg("compile") + .arg(".././missing.dag//") + .current_dir(root.join("child")) + .output() + .expect("failed to run parent-curdir-double missing single-file compile"); + assert!( + !parent_curdir_double.status.success(), + "parent-curdir-double missing single-file compile should fail" + ); + + let absolute = Command::new(daglang_bin()) + .arg("compile") + .arg(&missing) + .current_dir(root.join("child")) + .output() + .expect("failed to run absolute missing single-file compile"); + assert!( + !absolute.status.success(), + "absolute missing single-file compile should fail" + ); + + assert_eq!( + parent_curdir_double.stdout, absolute.stdout, + "parent-curdir-double and absolute missing single-file compile stdout should match" + ); + assert_eq!( + parent_curdir_double.stderr, absolute.stderr, + "parent-curdir-double and absolute missing single-file compile stderr should match" + ); + let stderr = String::from_utf8_lossy(&parent_curdir_double.stderr); + assert!( + stderr.contains(&format!("failed to read {}", missing.display())), + "parent-curdir-double missing single-file diagnostic should normalize to absolute path: {stderr}" + ); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + +#[test] +fn compile_command_parent_curdir_trailing_slash_missing_single_file_target_is_normalized_and_equivalent( +) { + let root = unique_temp_dir("compile_parent_curdir_trailing_missing_single_file"); + std::fs::create_dir_all(root.join("child")).expect("failed to create temp root"); + let missing = root.join("missing.dag"); + + let parent_curdir_trailing = Command::new(daglang_bin()) + .arg("compile") + .arg(".././missing.dag/") + .current_dir(root.join("child")) + .output() + .expect("failed to run parent-curdir-trailing missing single-file compile"); + assert!( + !parent_curdir_trailing.status.success(), + "parent-curdir-trailing missing single-file compile should fail" + ); + + let absolute = Command::new(daglang_bin()) + .arg("compile") + .arg(&missing) + .current_dir(root.join("child")) + .output() + .expect("failed to run absolute missing single-file compile"); + assert!( + !absolute.status.success(), + "absolute missing single-file compile should fail" + ); + + assert_eq!( + parent_curdir_trailing.stdout, absolute.stdout, + "parent-curdir-trailing and absolute missing single-file compile stdout should match" + ); + assert_eq!( + parent_curdir_trailing.stderr, absolute.stderr, + "parent-curdir-trailing and absolute missing single-file compile stderr should match" + ); + let stderr = String::from_utf8_lossy(&parent_curdir_trailing.stderr); + assert!( + stderr.contains(&format!("failed to read {}", missing.display())), + "parent-curdir-trailing missing single-file diagnostic should normalize to absolute path: {stderr}" + ); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + +#[test] +fn compile_command_parent_curdir_double_separator_single_file_target_matches_absolute_output() { + let root = unique_temp_dir("compile_parent_curdir_double_single_file"); + std::fs::create_dir_all(root.join("child")).expect("failed to create temp root child"); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp root sample"); + let source = root.join("sample/main.dag"); + std::fs::write(&source, "module sample.main\nfn run() -> Unit { }") + .expect("failed to write single-file source"); + + let parent_curdir_double = Command::new(daglang_bin()) + .arg("compile") + .arg(".././sample//main.dag") + .current_dir(root.join("child")) + .output() + .expect("failed to run parent-curdir-double single-file compile"); + assert!( + parent_curdir_double.status.success(), + "parent-curdir-double single-file compile should succeed: {}", + String::from_utf8_lossy(&parent_curdir_double.stderr) + ); + + let absolute = Command::new(daglang_bin()) + .arg("compile") + .arg(&source) + .current_dir(root.join("child")) + .output() + .expect("failed to run absolute single-file compile"); + assert!( + absolute.status.success(), + "absolute single-file compile should succeed: {}", + String::from_utf8_lossy(&absolute.stderr) + ); + + assert_eq!( + parent_curdir_double.stdout, absolute.stdout, + "parent-curdir-double and absolute single-file compile stdout should match" + ); + assert_eq!( + parent_curdir_double.stderr, absolute.stderr, + "parent-curdir-double and absolute single-file compile stderr should match" + ); + assert_no_stage_failures(&String::from_utf8_lossy(&parent_curdir_double.stderr)); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + +#[test] +fn compile_command_parent_curdir_trailing_slash_single_file_target_matches_absolute_output() { + let root = unique_temp_dir("compile_parent_curdir_trailing_single_file"); + std::fs::create_dir_all(root.join("child")).expect("failed to create temp root child"); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp root sample"); + let source = root.join("sample/main.dag"); + std::fs::write(&source, "module sample.main\nfn run() -> Unit { }") + .expect("failed to write single-file source"); + + let parent_curdir_trailing = Command::new(daglang_bin()) + .arg("compile") + .arg(".././sample/main.dag/") + .current_dir(root.join("child")) + .output() + .expect("failed to run parent-curdir-trailing single-file compile"); + assert!( + parent_curdir_trailing.status.success(), + "parent-curdir-trailing single-file compile should succeed: {}", + String::from_utf8_lossy(&parent_curdir_trailing.stderr) + ); + + let absolute = Command::new(daglang_bin()) + .arg("compile") + .arg(&source) + .current_dir(root.join("child")) + .output() + .expect("failed to run absolute single-file compile"); + assert!( + absolute.status.success(), + "absolute single-file compile should succeed: {}", + String::from_utf8_lossy(&absolute.stderr) + ); + + assert_eq!( + parent_curdir_trailing.stdout, absolute.stdout, + "parent-curdir-trailing and absolute single-file compile stdout should match" + ); + assert_eq!( + parent_curdir_trailing.stderr, absolute.stderr, + "parent-curdir-trailing and absolute single-file compile stderr should match" + ); + assert_no_stage_failures(&String::from_utf8_lossy(&parent_curdir_trailing.stderr)); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_trailing_slash_single_file_target_matches_plain_relative_output() { let root = unique_temp_dir("compile_trailing_slash_single_file_target"); From 17e3b026f5b6dcbc1cd2e790988ef633e78bb1fc Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 12:18:29 +0000 Subject: [PATCH 125/169] Add compile parent-double-trailing regressions Co-authored-by: Brian Searls --- .../daglang-cli/tests/compile_commands.rs | 233 ++++++++++++++++++ 1 file changed, 233 insertions(+) diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index f09f3d2ebc9..fe17ace5380 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -277,6 +277,53 @@ fn compile_command_parent_double_separator_missing_root_is_normalized_and_equiva std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_parent_double_separator_trailing_slash_missing_root_is_normalized_and_equivalent( +) { + let root = unique_temp_dir("compile_parent_double_trailing_missing_root"); + std::fs::create_dir_all(root.join("child")).expect("failed to create temp root"); + let missing = root.join("missing_root"); + + let parent_double_trailing = Command::new(daglang_bin()) + .arg("compile") + .arg("..//missing_root/") + .current_dir(root.join("child")) + .output() + .expect("failed to run parent-double-trailing missing-root compile"); + assert!( + !parent_double_trailing.status.success(), + "parent-double-trailing missing-root compile should fail" + ); + + let absolute = Command::new(daglang_bin()) + .arg("compile") + .arg(&missing) + .current_dir(root.join("child")) + .output() + .expect("failed to run absolute missing-root compile"); + assert!( + !absolute.status.success(), + "absolute missing-root compile should fail" + ); + + assert_eq!( + parent_double_trailing.stdout, absolute.stdout, + "parent-double-trailing and absolute missing-root compile stdout should match" + ); + assert_eq!( + parent_double_trailing.stderr, absolute.stderr, + "parent-double-trailing and absolute missing-root compile stderr should match" + ); + let stderr = String::from_utf8_lossy(&parent_double_trailing.stderr); + assert!( + stderr.contains(&missing.display().to_string()), + "parent-double-trailing missing-root diagnostic should normalize to absolute path: {stderr}" + ); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_parent_segment_non_directory_root_matches_absolute_output() { let root = unique_temp_dir("compile_parent_segment_non_directory_root"); @@ -371,6 +418,54 @@ fn compile_command_parent_double_separator_non_directory_root_matches_absolute_o std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_parent_double_separator_trailing_slash_non_directory_root_matches_absolute_output() +{ + let root = unique_temp_dir("compile_parent_double_trailing_non_directory_root"); + std::fs::create_dir_all(root.join("child")).expect("failed to create temp root"); + let root_file = root.join("input.txt"); + std::fs::write(&root_file, "not a directory").expect("failed to write non-directory root"); + + let parent_double_trailing = Command::new(daglang_bin()) + .arg("compile") + .arg("..//input.txt/") + .current_dir(root.join("child")) + .output() + .expect("failed to run parent-double-trailing non-directory-root compile"); + assert!( + !parent_double_trailing.status.success(), + "parent-double-trailing non-directory-root compile should fail" + ); + + let absolute = Command::new(daglang_bin()) + .arg("compile") + .arg(&root_file) + .current_dir(root.join("child")) + .output() + .expect("failed to run absolute non-directory-root compile"); + assert!( + !absolute.status.success(), + "absolute non-directory-root compile should fail" + ); + + assert_eq!( + parent_double_trailing.stdout, absolute.stdout, + "parent-double-trailing and absolute non-directory-root compile stdout should match" + ); + assert_eq!( + parent_double_trailing.stderr, absolute.stderr, + "parent-double-trailing and absolute non-directory-root compile stderr should match" + ); + let stderr = String::from_utf8_lossy(&parent_double_trailing.stderr); + assert!( + stderr.contains(&root_file.display().to_string()), + "parent-double-trailing non-directory-root diagnostic should normalize to absolute path: {stderr}" + ); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_curdir_segment_missing_root_matches_plain_relative_output() { let root = unique_temp_dir("compile_curdir_segment_missing_root"); @@ -818,6 +913,53 @@ fn compile_command_parent_double_separator_missing_single_file_target_is_normali std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_parent_double_separator_trailing_slash_missing_single_file_target_is_normalized_and_equivalent( +) { + let root = unique_temp_dir("compile_parent_double_trailing_missing_single_file"); + std::fs::create_dir_all(root.join("child")).expect("failed to create temp root"); + let missing = root.join("missing.dag"); + + let parent_double_trailing = Command::new(daglang_bin()) + .arg("compile") + .arg("..//missing.dag/") + .current_dir(root.join("child")) + .output() + .expect("failed to run parent-double-trailing missing single-file compile"); + assert!( + !parent_double_trailing.status.success(), + "parent-double-trailing missing single-file compile should fail" + ); + + let absolute = Command::new(daglang_bin()) + .arg("compile") + .arg(&missing) + .current_dir(root.join("child")) + .output() + .expect("failed to run absolute missing single-file compile"); + assert!( + !absolute.status.success(), + "absolute missing single-file compile should fail" + ); + + assert_eq!( + parent_double_trailing.stdout, absolute.stdout, + "parent-double-trailing and absolute missing single-file compile stdout should match" + ); + assert_eq!( + parent_double_trailing.stderr, absolute.stderr, + "parent-double-trailing and absolute missing single-file compile stderr should match" + ); + let stderr = String::from_utf8_lossy(&parent_double_trailing.stderr); + assert!( + stderr.contains(&format!("failed to read {}", missing.display())), + "parent-double-trailing missing single-file diagnostic should normalize to absolute path: {stderr}" + ); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_parent_segment_root_matches_absolute_output() { let root = unique_temp_dir("compile_parent_segment_root"); @@ -906,6 +1048,50 @@ fn compile_command_parent_double_separator_root_matches_absolute_output() { std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_parent_double_separator_trailing_slash_root_matches_absolute_output() { + let root = unique_temp_dir("compile_parent_double_trailing_root"); + std::fs::create_dir_all(root.join("child")).expect("failed to create temp root"); + write_minimal_directory_compile_fixture(&root); + let absolute_root = root.join("dsl"); + + let parent_double_trailing = Command::new(daglang_bin()) + .arg("compile") + .arg("..//dsl/") + .current_dir(root.join("child")) + .output() + .expect("failed to run parent-double-trailing root compile"); + assert!( + parent_double_trailing.status.success(), + "parent-double-trailing root compile should succeed: {}", + String::from_utf8_lossy(&parent_double_trailing.stderr) + ); + + let absolute = Command::new(daglang_bin()) + .arg("compile") + .arg(&absolute_root) + .current_dir(root.join("child")) + .output() + .expect("failed to run absolute root compile"); + assert!( + absolute.status.success(), + "absolute root compile should succeed: {}", + String::from_utf8_lossy(&absolute.stderr) + ); + + assert_eq!( + parent_double_trailing.stdout, absolute.stdout, + "parent-double-trailing and absolute root compile stdout should match" + ); + assert_eq!( + parent_double_trailing.stderr, absolute.stderr, + "parent-double-trailing and absolute root compile stderr should match" + ); + assert_no_stage_failures(&String::from_utf8_lossy(&parent_double_trailing.stderr)); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_parent_segment_single_file_target_matches_absolute_output() { let root = unique_temp_dir("compile_parent_segment_single_file_target"); @@ -998,6 +1184,53 @@ fn compile_command_parent_double_separator_single_file_target_matches_absolute_o std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_parent_double_separator_trailing_slash_single_file_target_matches_absolute_output() +{ + let root = unique_temp_dir("compile_parent_double_trailing_single_file_target"); + std::fs::create_dir_all(root.join("child")).expect("failed to create temp root child"); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp root sample"); + let source = root.join("sample/main.dag"); + std::fs::write(&source, "module sample.main\nfn run() -> Unit { }") + .expect("failed to write single-file source"); + + let parent_double_trailing = Command::new(daglang_bin()) + .arg("compile") + .arg("..//sample/main.dag/") + .current_dir(root.join("child")) + .output() + .expect("failed to run parent-double-trailing single-file compile"); + assert!( + parent_double_trailing.status.success(), + "parent-double-trailing single-file compile should succeed: {}", + String::from_utf8_lossy(&parent_double_trailing.stderr) + ); + + let absolute = Command::new(daglang_bin()) + .arg("compile") + .arg(&source) + .current_dir(root.join("child")) + .output() + .expect("failed to run absolute single-file compile"); + assert!( + absolute.status.success(), + "absolute single-file compile should succeed: {}", + String::from_utf8_lossy(&absolute.stderr) + ); + + assert_eq!( + parent_double_trailing.stdout, absolute.stdout, + "parent-double-trailing and absolute single-file compile stdout should match" + ); + assert_eq!( + parent_double_trailing.stderr, absolute.stderr, + "parent-double-trailing and absolute single-file compile stderr should match" + ); + assert_no_stage_failures(&String::from_utf8_lossy(&parent_double_trailing.stderr)); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_parent_curdir_double_separator_missing_root_is_normalized_and_equivalent() { let root = unique_temp_dir("compile_parent_curdir_double_missing_root"); From 7050be5605f5689347774a404a9cc6069c1e9139 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 12:22:32 +0000 Subject: [PATCH 126/169] Add compile parent-curdir-segment regressions Co-authored-by: Brian Searls --- .../daglang-cli/tests/compile_commands.rs | 229 ++++++++++++++++++ 1 file changed, 229 insertions(+) diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index fe17ace5380..e15e8e1adfb 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -1231,6 +1231,52 @@ fn compile_command_parent_double_separator_trailing_slash_single_file_target_mat std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_parent_curdir_segment_missing_root_is_normalized_and_equivalent() { + let root = unique_temp_dir("compile_parent_curdir_segment_missing_root"); + std::fs::create_dir_all(root.join("child")).expect("failed to create temp root"); + let missing = root.join("missing_root"); + + let parent_curdir_segment = Command::new(daglang_bin()) + .arg("compile") + .arg(".././missing_root") + .current_dir(root.join("child")) + .output() + .expect("failed to run parent-curdir-segment missing-root compile"); + assert!( + !parent_curdir_segment.status.success(), + "parent-curdir-segment missing-root compile should fail" + ); + + let absolute = Command::new(daglang_bin()) + .arg("compile") + .arg(&missing) + .current_dir(root.join("child")) + .output() + .expect("failed to run absolute missing-root compile"); + assert!( + !absolute.status.success(), + "absolute missing-root compile should fail" + ); + + assert_eq!( + parent_curdir_segment.stdout, absolute.stdout, + "parent-curdir-segment and absolute missing-root compile stdout should match" + ); + assert_eq!( + parent_curdir_segment.stderr, absolute.stderr, + "parent-curdir-segment and absolute missing-root compile stderr should match" + ); + let stderr = String::from_utf8_lossy(&parent_curdir_segment.stderr); + assert!( + stderr.contains(&missing.display().to_string()), + "parent-curdir-segment missing-root diagnostic should normalize to absolute path: {stderr}" + ); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_parent_curdir_double_separator_missing_root_is_normalized_and_equivalent() { let root = unique_temp_dir("compile_parent_curdir_double_missing_root"); @@ -1323,6 +1369,53 @@ fn compile_command_parent_curdir_trailing_slash_missing_root_is_normalized_and_e std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_parent_curdir_segment_non_directory_root_matches_absolute_output() { + let root = unique_temp_dir("compile_parent_curdir_segment_non_directory_root"); + std::fs::create_dir_all(root.join("child")).expect("failed to create temp root"); + let root_file = root.join("input.txt"); + std::fs::write(&root_file, "not a directory").expect("failed to write non-directory root"); + + let parent_curdir_segment = Command::new(daglang_bin()) + .arg("compile") + .arg(".././input.txt") + .current_dir(root.join("child")) + .output() + .expect("failed to run parent-curdir-segment non-directory-root compile"); + assert!( + !parent_curdir_segment.status.success(), + "parent-curdir-segment non-directory-root compile should fail" + ); + + let absolute = Command::new(daglang_bin()) + .arg("compile") + .arg(&root_file) + .current_dir(root.join("child")) + .output() + .expect("failed to run absolute non-directory-root compile"); + assert!( + !absolute.status.success(), + "absolute non-directory-root compile should fail" + ); + + assert_eq!( + parent_curdir_segment.stdout, absolute.stdout, + "parent-curdir-segment and absolute non-directory-root compile stdout should match" + ); + assert_eq!( + parent_curdir_segment.stderr, absolute.stderr, + "parent-curdir-segment and absolute non-directory-root compile stderr should match" + ); + let stderr = String::from_utf8_lossy(&parent_curdir_segment.stderr); + assert!( + stderr.contains(&root_file.display().to_string()), + "parent-curdir-segment non-directory-root diagnostic should normalize to absolute path: {stderr}" + ); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_parent_curdir_double_separator_non_directory_root_matches_absolute_output() { let root = unique_temp_dir("compile_parent_curdir_double_non_directory_root"); @@ -1417,6 +1510,50 @@ fn compile_command_parent_curdir_trailing_slash_non_directory_root_matches_absol std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_parent_curdir_segment_root_matches_absolute_output() { + let root = unique_temp_dir("compile_parent_curdir_segment_root"); + std::fs::create_dir_all(root.join("child")).expect("failed to create temp root"); + write_minimal_directory_compile_fixture(&root); + let absolute_root = root.join("dsl"); + + let parent_curdir_segment = Command::new(daglang_bin()) + .arg("compile") + .arg(".././dsl") + .current_dir(root.join("child")) + .output() + .expect("failed to run parent-curdir-segment root compile"); + assert!( + parent_curdir_segment.status.success(), + "parent-curdir-segment root compile should succeed: {}", + String::from_utf8_lossy(&parent_curdir_segment.stderr) + ); + + let absolute = Command::new(daglang_bin()) + .arg("compile") + .arg(&absolute_root) + .current_dir(root.join("child")) + .output() + .expect("failed to run absolute root compile"); + assert!( + absolute.status.success(), + "absolute root compile should succeed: {}", + String::from_utf8_lossy(&absolute.stderr) + ); + + assert_eq!( + parent_curdir_segment.stdout, absolute.stdout, + "parent-curdir-segment and absolute root compile stdout should match" + ); + assert_eq!( + parent_curdir_segment.stderr, absolute.stderr, + "parent-curdir-segment and absolute root compile stderr should match" + ); + assert_no_stage_failures(&String::from_utf8_lossy(&parent_curdir_segment.stderr)); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_parent_curdir_double_separator_root_matches_absolute_output() { let root = unique_temp_dir("compile_parent_curdir_double_root"); @@ -1505,6 +1642,52 @@ fn compile_command_parent_curdir_trailing_slash_root_matches_absolute_output() { std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_parent_curdir_segment_missing_single_file_target_is_normalized_and_equivalent() { + let root = unique_temp_dir("compile_parent_curdir_segment_missing_single_file"); + std::fs::create_dir_all(root.join("child")).expect("failed to create temp root"); + let missing = root.join("missing.dag"); + + let parent_curdir_segment = Command::new(daglang_bin()) + .arg("compile") + .arg(".././missing.dag") + .current_dir(root.join("child")) + .output() + .expect("failed to run parent-curdir-segment missing single-file compile"); + assert!( + !parent_curdir_segment.status.success(), + "parent-curdir-segment missing single-file compile should fail" + ); + + let absolute = Command::new(daglang_bin()) + .arg("compile") + .arg(&missing) + .current_dir(root.join("child")) + .output() + .expect("failed to run absolute missing single-file compile"); + assert!( + !absolute.status.success(), + "absolute missing single-file compile should fail" + ); + + assert_eq!( + parent_curdir_segment.stdout, absolute.stdout, + "parent-curdir-segment and absolute missing single-file compile stdout should match" + ); + assert_eq!( + parent_curdir_segment.stderr, absolute.stderr, + "parent-curdir-segment and absolute missing single-file compile stderr should match" + ); + let stderr = String::from_utf8_lossy(&parent_curdir_segment.stderr); + assert!( + stderr.contains(&format!("failed to read {}", missing.display())), + "parent-curdir-segment missing single-file diagnostic should normalize to absolute path: {stderr}" + ); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_parent_curdir_double_separator_missing_single_file_target_is_normalized_and_equivalent( ) { @@ -1599,6 +1782,52 @@ fn compile_command_parent_curdir_trailing_slash_missing_single_file_target_is_no std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_parent_curdir_segment_single_file_target_matches_absolute_output() { + let root = unique_temp_dir("compile_parent_curdir_segment_single_file"); + std::fs::create_dir_all(root.join("child")).expect("failed to create temp root child"); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp root sample"); + let source = root.join("sample/main.dag"); + std::fs::write(&source, "module sample.main\nfn run() -> Unit { }") + .expect("failed to write single-file source"); + + let parent_curdir_segment = Command::new(daglang_bin()) + .arg("compile") + .arg(".././sample/main.dag") + .current_dir(root.join("child")) + .output() + .expect("failed to run parent-curdir-segment single-file compile"); + assert!( + parent_curdir_segment.status.success(), + "parent-curdir-segment single-file compile should succeed: {}", + String::from_utf8_lossy(&parent_curdir_segment.stderr) + ); + + let absolute = Command::new(daglang_bin()) + .arg("compile") + .arg(&source) + .current_dir(root.join("child")) + .output() + .expect("failed to run absolute single-file compile"); + assert!( + absolute.status.success(), + "absolute single-file compile should succeed: {}", + String::from_utf8_lossy(&absolute.stderr) + ); + + assert_eq!( + parent_curdir_segment.stdout, absolute.stdout, + "parent-curdir-segment and absolute single-file compile stdout should match" + ); + assert_eq!( + parent_curdir_segment.stderr, absolute.stderr, + "parent-curdir-segment and absolute single-file compile stderr should match" + ); + assert_no_stage_failures(&String::from_utf8_lossy(&parent_curdir_segment.stderr)); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_parent_curdir_double_separator_single_file_target_matches_absolute_output() { let root = unique_temp_dir("compile_parent_curdir_double_single_file"); From 0bbc7891ee760462608a1bcfc05c30f1673f07ab Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 12:26:38 +0000 Subject: [PATCH 127/169] Add compile parent-segment-trailing regressions Co-authored-by: Brian Searls --- .../daglang-cli/tests/compile_commands.rs | 230 ++++++++++++++++++ 1 file changed, 230 insertions(+) diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index e15e8e1adfb..f3f5ee184ea 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -231,6 +231,52 @@ fn compile_command_parent_segment_missing_root_is_normalized_and_equivalent() { std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_parent_segment_trailing_slash_missing_root_is_normalized_and_equivalent() { + let root = unique_temp_dir("compile_parent_segment_trailing_missing_root"); + std::fs::create_dir_all(root.join("child")).expect("failed to create temp root"); + let missing = root.join("missing_root"); + + let parent_trailing = Command::new(daglang_bin()) + .arg("compile") + .arg("../missing_root/") + .current_dir(root.join("child")) + .output() + .expect("failed to run parent-trailing missing-root compile"); + assert!( + !parent_trailing.status.success(), + "parent-trailing missing-root compile should fail" + ); + + let absolute = Command::new(daglang_bin()) + .arg("compile") + .arg(&missing) + .current_dir(root.join("child")) + .output() + .expect("failed to run absolute missing-root compile"); + assert!( + !absolute.status.success(), + "absolute missing-root compile should fail" + ); + + assert_eq!( + parent_trailing.stdout, absolute.stdout, + "parent-trailing and absolute missing-root compile stdout should match" + ); + assert_eq!( + parent_trailing.stderr, absolute.stderr, + "parent-trailing and absolute missing-root compile stderr should match" + ); + let stderr = String::from_utf8_lossy(&parent_trailing.stderr); + assert!( + stderr.contains(&missing.display().to_string()), + "parent-trailing missing-root diagnostic should normalize to absolute path: {stderr}" + ); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_parent_double_separator_missing_root_is_normalized_and_equivalent() { let root = unique_temp_dir("compile_parent_double_separator_missing_root"); @@ -371,6 +417,53 @@ fn compile_command_parent_segment_non_directory_root_matches_absolute_output() { std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_parent_segment_trailing_slash_non_directory_root_matches_absolute_output() { + let root = unique_temp_dir("compile_parent_segment_trailing_non_directory_root"); + std::fs::create_dir_all(root.join("child")).expect("failed to create temp root"); + let root_file = root.join("input.txt"); + std::fs::write(&root_file, "not a directory").expect("failed to write non-directory root"); + + let parent_trailing = Command::new(daglang_bin()) + .arg("compile") + .arg("../input.txt/") + .current_dir(root.join("child")) + .output() + .expect("failed to run parent-trailing non-directory-root compile"); + assert!( + !parent_trailing.status.success(), + "parent-trailing non-directory-root compile should fail" + ); + + let absolute = Command::new(daglang_bin()) + .arg("compile") + .arg(&root_file) + .current_dir(root.join("child")) + .output() + .expect("failed to run absolute non-directory-root compile"); + assert!( + !absolute.status.success(), + "absolute non-directory-root compile should fail" + ); + + assert_eq!( + parent_trailing.stdout, absolute.stdout, + "parent-trailing and absolute non-directory-root compile stdout should match" + ); + assert_eq!( + parent_trailing.stderr, absolute.stderr, + "parent-trailing and absolute non-directory-root compile stderr should match" + ); + let stderr = String::from_utf8_lossy(&parent_trailing.stderr); + assert!( + stderr.contains(&root_file.display().to_string()), + "parent-trailing non-directory-root diagnostic should normalize to absolute path: {stderr}" + ); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_parent_double_separator_non_directory_root_matches_absolute_output() { let root = unique_temp_dir("compile_parent_double_non_directory_root"); @@ -867,6 +960,53 @@ fn compile_command_parent_segment_missing_single_file_target_is_normalized_and_e std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_parent_segment_trailing_slash_missing_single_file_target_is_normalized_and_equivalent( +) { + let root = unique_temp_dir("compile_parent_segment_trailing_missing_single_file"); + std::fs::create_dir_all(root.join("child")).expect("failed to create temp root"); + let missing = root.join("missing.dag"); + + let parent_trailing = Command::new(daglang_bin()) + .arg("compile") + .arg("../missing.dag/") + .current_dir(root.join("child")) + .output() + .expect("failed to run parent-trailing missing single-file compile"); + assert!( + !parent_trailing.status.success(), + "parent-trailing missing single-file compile should fail" + ); + + let absolute = Command::new(daglang_bin()) + .arg("compile") + .arg(&missing) + .current_dir(root.join("child")) + .output() + .expect("failed to run absolute missing single-file compile"); + assert!( + !absolute.status.success(), + "absolute missing single-file compile should fail" + ); + + assert_eq!( + parent_trailing.stdout, absolute.stdout, + "parent-trailing and absolute missing single-file compile stdout should match" + ); + assert_eq!( + parent_trailing.stderr, absolute.stderr, + "parent-trailing and absolute missing single-file compile stderr should match" + ); + let stderr = String::from_utf8_lossy(&parent_trailing.stderr); + assert!( + stderr.contains(&format!("failed to read {}", missing.display())), + "parent-trailing missing single-file diagnostic should normalize to absolute path: {stderr}" + ); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_parent_double_separator_missing_single_file_target_is_normalized_and_equivalent() { let root = unique_temp_dir("compile_parent_double_missing_single_file_target"); @@ -1004,6 +1144,50 @@ fn compile_command_parent_segment_root_matches_absolute_output() { std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_parent_segment_trailing_slash_root_matches_absolute_output() { + let root = unique_temp_dir("compile_parent_segment_trailing_root"); + std::fs::create_dir_all(root.join("child")).expect("failed to create temp root"); + write_minimal_directory_compile_fixture(&root); + let absolute_root = root.join("dsl"); + + let parent_trailing = Command::new(daglang_bin()) + .arg("compile") + .arg("../dsl/") + .current_dir(root.join("child")) + .output() + .expect("failed to run parent-trailing root compile"); + assert!( + parent_trailing.status.success(), + "parent-trailing root compile should succeed: {}", + String::from_utf8_lossy(&parent_trailing.stderr) + ); + + let absolute = Command::new(daglang_bin()) + .arg("compile") + .arg(&absolute_root) + .current_dir(root.join("child")) + .output() + .expect("failed to run absolute root compile"); + assert!( + absolute.status.success(), + "absolute root compile should succeed: {}", + String::from_utf8_lossy(&absolute.stderr) + ); + + assert_eq!( + parent_trailing.stdout, absolute.stdout, + "parent-trailing and absolute root compile stdout should match" + ); + assert_eq!( + parent_trailing.stderr, absolute.stderr, + "parent-trailing and absolute root compile stderr should match" + ); + assert_no_stage_failures(&String::from_utf8_lossy(&parent_trailing.stderr)); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_parent_double_separator_root_matches_absolute_output() { let root = unique_temp_dir("compile_parent_double_separator_root"); @@ -1138,6 +1322,52 @@ fn compile_command_parent_segment_single_file_target_matches_absolute_output() { std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_parent_segment_trailing_slash_single_file_target_matches_absolute_output() { + let root = unique_temp_dir("compile_parent_segment_trailing_single_file_target"); + std::fs::create_dir_all(root.join("child")).expect("failed to create temp root child"); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp root sample"); + let source = root.join("sample/main.dag"); + std::fs::write(&source, "module sample.main\nfn run() -> Unit { }") + .expect("failed to write single-file source"); + + let parent_trailing = Command::new(daglang_bin()) + .arg("compile") + .arg("../sample/main.dag/") + .current_dir(root.join("child")) + .output() + .expect("failed to run parent-trailing single-file compile"); + assert!( + parent_trailing.status.success(), + "parent-trailing single-file compile should succeed: {}", + String::from_utf8_lossy(&parent_trailing.stderr) + ); + + let absolute = Command::new(daglang_bin()) + .arg("compile") + .arg(&source) + .current_dir(root.join("child")) + .output() + .expect("failed to run absolute single-file compile"); + assert!( + absolute.status.success(), + "absolute single-file compile should succeed: {}", + String::from_utf8_lossy(&absolute.stderr) + ); + + assert_eq!( + parent_trailing.stdout, absolute.stdout, + "parent-trailing and absolute single-file compile stdout should match" + ); + assert_eq!( + parent_trailing.stderr, absolute.stderr, + "parent-trailing and absolute single-file compile stderr should match" + ); + assert_no_stage_failures(&String::from_utf8_lossy(&parent_trailing.stderr)); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_parent_double_separator_single_file_target_matches_absolute_output() { let root = unique_temp_dir("compile_parent_double_single_file_target"); From ca6af33e847946a93039e650574a2d2789d0fc78 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 12:30:59 +0000 Subject: [PATCH 128/169] Add absolute parent-trailing compile regressions Co-authored-by: Brian Searls --- .../daglang-cli/tests/compile_commands.rs | 105 ++++++++++++++++++ 1 file changed, 105 insertions(+) diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index f3f5ee184ea..8b5507ad884 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -2622,6 +2622,52 @@ fn compile_command_absolute_parent_segment_root_matches_canonical_absolute_outpu std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_absolute_parent_segment_trailing_slash_root_matches_canonical_absolute_output() { + let root = unique_temp_dir("compile_absolute_parent_segment_trailing_root"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + write_minimal_directory_compile_fixture(&root); + let absolute_parent_segment_trailing_root = + PathBuf::from(format!("{}/", root.join("dsl/sample/..").display())); + let canonical_root = root.join("dsl"); + + let parent_segment_trailing = Command::new(daglang_bin()) + .arg("compile") + .arg(&absolute_parent_segment_trailing_root) + .current_dir(&root) + .output() + .expect("failed to run parent-segment-trailing absolute root compile"); + assert!( + parent_segment_trailing.status.success(), + "parent-segment-trailing absolute root compile should succeed: {}", + String::from_utf8_lossy(&parent_segment_trailing.stderr) + ); + + let canonical = Command::new(daglang_bin()) + .arg("compile") + .arg(&canonical_root) + .current_dir(&root) + .output() + .expect("failed to run canonical absolute root compile"); + assert!( + canonical.status.success(), + "canonical absolute root compile should succeed: {}", + String::from_utf8_lossy(&canonical.stderr) + ); + + assert_eq!( + parent_segment_trailing.stdout, canonical.stdout, + "parent-segment-trailing and canonical absolute-root compile stdout should match" + ); + assert_eq!( + parent_segment_trailing.stderr, canonical.stderr, + "parent-segment-trailing and canonical absolute-root compile stderr should match" + ); + assert_no_stage_failures(&String::from_utf8_lossy(&parent_segment_trailing.stderr)); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_absolute_double_separator_root_matches_canonical_absolute_output() { let root = unique_temp_dir("compile_absolute_double_separator_root"); @@ -2849,6 +2895,53 @@ fn compile_command_absolute_parent_segment_single_file_target_matches_canonical_ std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_absolute_parent_segment_trailing_slash_single_file_target_matches_canonical_output() +{ + let root = unique_temp_dir("compile_absolute_parent_segment_trailing_single_file"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + write_minimal_directory_compile_fixture(&root); + let absolute_parent_segment_trailing_target = + PathBuf::from(format!("{}/", root.join("dsl/sample/../sample/main.dag").display())); + let canonical_target = root.join("dsl/sample/main.dag"); + + let parent_segment_trailing = Command::new(daglang_bin()) + .arg("compile") + .arg(&absolute_parent_segment_trailing_target) + .current_dir(&root) + .output() + .expect("failed to run parent-segment-trailing absolute single-file compile"); + assert!( + parent_segment_trailing.status.success(), + "parent-segment-trailing absolute single-file compile should succeed: {}", + String::from_utf8_lossy(&parent_segment_trailing.stderr) + ); + + let canonical = Command::new(daglang_bin()) + .arg("compile") + .arg(&canonical_target) + .current_dir(&root) + .output() + .expect("failed to run canonical absolute single-file compile"); + assert!( + canonical.status.success(), + "canonical absolute single-file compile should succeed: {}", + String::from_utf8_lossy(&canonical.stderr) + ); + + assert_eq!( + parent_segment_trailing.stdout, canonical.stdout, + "parent-segment-trailing and canonical absolute single-file compile stdout should match" + ); + assert_eq!( + parent_segment_trailing.stderr, canonical.stderr, + "parent-segment-trailing and canonical absolute single-file compile stderr should match" + ); + assert_no_stage_failures(&String::from_utf8_lossy(&parent_segment_trailing.stderr)); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_absolute_double_separator_single_file_target_matches_canonical_output() { let root = unique_temp_dir("compile_absolute_double_separator_single_file"); @@ -3015,6 +3108,10 @@ fn compile_command_absolute_missing_root_variants_match_canonical_output() { let variants = vec![ ("mixed", root.join(".").join("missing_root")), ("parent", root.join("anchor/../missing_root")), + ( + "parent_trailing", + PathBuf::from(format!("{}/", root.join("anchor/../missing_root").display())), + ), ( "double_separator", PathBuf::from(format!("{}//missing_root", root.display())), @@ -3084,6 +3181,10 @@ fn compile_command_absolute_non_directory_root_variants_match_canonical_output() let variants = vec![ ("mixed", root.join(".").join("input.txt")), ("parent", root.join("anchor/../input.txt")), + ( + "parent_trailing", + PathBuf::from(format!("{}/", root.join("anchor/../input.txt").display())), + ), ( "double_separator", PathBuf::from(format!("{}//input.txt", root.display())), @@ -3151,6 +3252,10 @@ fn compile_command_absolute_missing_single_file_variants_match_canonical_output( let variants = vec![ ("mixed", root.join(".").join("dsl/sample/missing.dag")), ("parent", root.join("dsl/sample/../sample/missing.dag")), + ( + "parent_trailing", + PathBuf::from(format!("{}/", root.join("dsl/sample/../sample/missing.dag").display())), + ), ( "double_separator", PathBuf::from(format!("{}//dsl//sample//missing.dag", root.display())), From 3a711f7885188a977a99af099086ed3b87eb6ab8 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 12:42:11 +0000 Subject: [PATCH 129/169] Add absolute parent-double compile regressions Co-authored-by: Brian Searls --- .../daglang-cli/tests/compile_commands.rs | 117 ++++++++++++++++++ 1 file changed, 117 insertions(+) diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index 8b5507ad884..35b7fdfe502 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -2622,6 +2622,52 @@ fn compile_command_absolute_parent_segment_root_matches_canonical_absolute_outpu std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_absolute_parent_segment_double_separator_root_matches_canonical_absolute_output() { + let root = unique_temp_dir("compile_absolute_parent_segment_double_root"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + write_minimal_directory_compile_fixture(&root); + let absolute_parent_segment_double_root = + PathBuf::from(format!("{}/dsl/sample/..//", root.display())); + let canonical_root = root.join("dsl"); + + let parent_segment_double = Command::new(daglang_bin()) + .arg("compile") + .arg(&absolute_parent_segment_double_root) + .current_dir(&root) + .output() + .expect("failed to run parent-segment-double absolute root compile"); + assert!( + parent_segment_double.status.success(), + "parent-segment-double absolute root compile should succeed: {}", + String::from_utf8_lossy(&parent_segment_double.stderr) + ); + + let canonical = Command::new(daglang_bin()) + .arg("compile") + .arg(&canonical_root) + .current_dir(&root) + .output() + .expect("failed to run canonical absolute root compile"); + assert!( + canonical.status.success(), + "canonical absolute root compile should succeed: {}", + String::from_utf8_lossy(&canonical.stderr) + ); + + assert_eq!( + parent_segment_double.stdout, canonical.stdout, + "parent-segment-double and canonical absolute-root compile stdout should match" + ); + assert_eq!( + parent_segment_double.stderr, canonical.stderr, + "parent-segment-double and canonical absolute-root compile stderr should match" + ); + assert_no_stage_failures(&String::from_utf8_lossy(&parent_segment_double.stderr)); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_absolute_parent_segment_trailing_slash_root_matches_canonical_absolute_output() { let root = unique_temp_dir("compile_absolute_parent_segment_trailing_root"); @@ -2895,6 +2941,53 @@ fn compile_command_absolute_parent_segment_single_file_target_matches_canonical_ std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_absolute_parent_segment_double_separator_single_file_target_matches_canonical_output( +) { + let root = unique_temp_dir("compile_absolute_parent_segment_double_single_file"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + write_minimal_directory_compile_fixture(&root); + let absolute_parent_segment_double_target = + PathBuf::from(format!("{}/dsl/sample/..//sample/main.dag", root.display())); + let canonical_target = root.join("dsl/sample/main.dag"); + + let parent_segment_double = Command::new(daglang_bin()) + .arg("compile") + .arg(&absolute_parent_segment_double_target) + .current_dir(&root) + .output() + .expect("failed to run parent-segment-double absolute single-file compile"); + assert!( + parent_segment_double.status.success(), + "parent-segment-double absolute single-file compile should succeed: {}", + String::from_utf8_lossy(&parent_segment_double.stderr) + ); + + let canonical = Command::new(daglang_bin()) + .arg("compile") + .arg(&canonical_target) + .current_dir(&root) + .output() + .expect("failed to run canonical absolute single-file compile"); + assert!( + canonical.status.success(), + "canonical absolute single-file compile should succeed: {}", + String::from_utf8_lossy(&canonical.stderr) + ); + + assert_eq!( + parent_segment_double.stdout, canonical.stdout, + "parent-segment-double and canonical absolute single-file compile stdout should match" + ); + assert_eq!( + parent_segment_double.stderr, canonical.stderr, + "parent-segment-double and canonical absolute single-file compile stderr should match" + ); + assert_no_stage_failures(&String::from_utf8_lossy(&parent_segment_double.stderr)); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_absolute_parent_segment_trailing_slash_single_file_target_matches_canonical_output() { @@ -3108,10 +3201,18 @@ fn compile_command_absolute_missing_root_variants_match_canonical_output() { let variants = vec![ ("mixed", root.join(".").join("missing_root")), ("parent", root.join("anchor/../missing_root")), + ( + "parent_double_separator", + PathBuf::from(format!("{}/anchor/..//missing_root", root.display())), + ), ( "parent_trailing", PathBuf::from(format!("{}/", root.join("anchor/../missing_root").display())), ), + ( + "parent_double_separator_trailing", + PathBuf::from(format!("{}/anchor/..//missing_root/", root.display())), + ), ( "double_separator", PathBuf::from(format!("{}//missing_root", root.display())), @@ -3181,10 +3282,18 @@ fn compile_command_absolute_non_directory_root_variants_match_canonical_output() let variants = vec![ ("mixed", root.join(".").join("input.txt")), ("parent", root.join("anchor/../input.txt")), + ( + "parent_double_separator", + PathBuf::from(format!("{}/anchor/..//input.txt", root.display())), + ), ( "parent_trailing", PathBuf::from(format!("{}/", root.join("anchor/../input.txt").display())), ), + ( + "parent_double_separator_trailing", + PathBuf::from(format!("{}/anchor/..//input.txt/", root.display())), + ), ( "double_separator", PathBuf::from(format!("{}//input.txt", root.display())), @@ -3252,10 +3361,18 @@ fn compile_command_absolute_missing_single_file_variants_match_canonical_output( let variants = vec![ ("mixed", root.join(".").join("dsl/sample/missing.dag")), ("parent", root.join("dsl/sample/../sample/missing.dag")), + ( + "parent_double_separator", + PathBuf::from(format!("{}/dsl/sample/..//sample/missing.dag", root.display())), + ), ( "parent_trailing", PathBuf::from(format!("{}/", root.join("dsl/sample/../sample/missing.dag").display())), ), + ( + "parent_double_separator_trailing", + PathBuf::from(format!("{}/dsl/sample/..//sample/missing.dag/", root.display())), + ), ( "double_separator", PathBuf::from(format!("{}//dsl//sample//missing.dag", root.display())), From 2fb2d818019aeee6710707caa883460e01da917c Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 12:46:53 +0000 Subject: [PATCH 130/169] Expand absolute compile parent-path regressions Co-authored-by: Brian Searls --- .../daglang-cli/tests/compile_commands.rs | 200 ++++++++++++++++++ 1 file changed, 200 insertions(+) diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index 35b7fdfe502..828fb6c98af 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -2577,6 +2577,51 @@ fn compile_command_absolute_mixed_segment_root_matches_canonical_absolute_output std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_absolute_parent_curdir_segment_root_matches_canonical_absolute_output() { + let root = unique_temp_dir("compile_absolute_parent_curdir_segment_root"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + write_minimal_directory_compile_fixture(&root); + let absolute_parent_curdir_segment_root = root.join("anchor/.././dsl"); + let canonical_root = root.join("dsl"); + + let parent_curdir_segment = Command::new(daglang_bin()) + .arg("compile") + .arg(&absolute_parent_curdir_segment_root) + .current_dir(&root) + .output() + .expect("failed to run parent-curdir-segment absolute root compile"); + assert!( + parent_curdir_segment.status.success(), + "parent-curdir-segment absolute root compile should succeed: {}", + String::from_utf8_lossy(&parent_curdir_segment.stderr) + ); + + let canonical = Command::new(daglang_bin()) + .arg("compile") + .arg(&canonical_root) + .current_dir(&root) + .output() + .expect("failed to run canonical absolute root compile"); + assert!( + canonical.status.success(), + "canonical absolute root compile should succeed: {}", + String::from_utf8_lossy(&canonical.stderr) + ); + + assert_eq!( + parent_curdir_segment.stdout, canonical.stdout, + "parent-curdir-segment and canonical absolute-root compile stdout should match" + ); + assert_eq!( + parent_curdir_segment.stderr, canonical.stderr, + "parent-curdir-segment and canonical absolute-root compile stderr should match" + ); + assert_no_stage_failures(&String::from_utf8_lossy(&parent_curdir_segment.stderr)); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_absolute_parent_segment_root_matches_canonical_absolute_output() { let root = unique_temp_dir("compile_absolute_parent_segment_root"); @@ -2668,6 +2713,55 @@ fn compile_command_absolute_parent_segment_double_separator_root_matches_canonic std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_absolute_parent_segment_double_separator_trailing_slash_root_matches_canonical_absolute_output( +) { + let root = unique_temp_dir("compile_absolute_parent_segment_double_trailing_root"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + write_minimal_directory_compile_fixture(&root); + let absolute_parent_segment_double_trailing_root = + PathBuf::from(format!("{}/dsl/sample/..//", root.display())); + let canonical_root = root.join("dsl"); + + let parent_segment_double_trailing = Command::new(daglang_bin()) + .arg("compile") + .arg(&absolute_parent_segment_double_trailing_root) + .current_dir(&root) + .output() + .expect("failed to run parent-segment-double-trailing absolute root compile"); + assert!( + parent_segment_double_trailing.status.success(), + "parent-segment-double-trailing absolute root compile should succeed: {}", + String::from_utf8_lossy(&parent_segment_double_trailing.stderr) + ); + + let canonical = Command::new(daglang_bin()) + .arg("compile") + .arg(&canonical_root) + .current_dir(&root) + .output() + .expect("failed to run canonical absolute root compile"); + assert!( + canonical.status.success(), + "canonical absolute root compile should succeed: {}", + String::from_utf8_lossy(&canonical.stderr) + ); + + assert_eq!( + parent_segment_double_trailing.stdout, canonical.stdout, + "parent-segment-double-trailing and canonical absolute-root compile stdout should match" + ); + assert_eq!( + parent_segment_double_trailing.stderr, canonical.stderr, + "parent-segment-double-trailing and canonical absolute-root compile stderr should match" + ); + assert_no_stage_failures(&String::from_utf8_lossy( + &parent_segment_double_trailing.stderr + )); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_absolute_parent_segment_trailing_slash_root_matches_canonical_absolute_output() { let root = unique_temp_dir("compile_absolute_parent_segment_trailing_root"); @@ -2896,6 +2990,51 @@ fn compile_command_absolute_mixed_segment_single_file_target_matches_canonical_o std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_absolute_parent_curdir_segment_single_file_target_matches_canonical_output() { + let root = unique_temp_dir("compile_absolute_parent_curdir_segment_single_file"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + write_minimal_directory_compile_fixture(&root); + let absolute_parent_curdir_segment_target = root.join("anchor/.././dsl/sample/main.dag"); + let canonical_target = root.join("dsl/sample/main.dag"); + + let parent_curdir_segment = Command::new(daglang_bin()) + .arg("compile") + .arg(&absolute_parent_curdir_segment_target) + .current_dir(&root) + .output() + .expect("failed to run parent-curdir-segment absolute single-file compile"); + assert!( + parent_curdir_segment.status.success(), + "parent-curdir-segment absolute single-file compile should succeed: {}", + String::from_utf8_lossy(&parent_curdir_segment.stderr) + ); + + let canonical = Command::new(daglang_bin()) + .arg("compile") + .arg(&canonical_target) + .current_dir(&root) + .output() + .expect("failed to run canonical absolute single-file compile"); + assert!( + canonical.status.success(), + "canonical absolute single-file compile should succeed: {}", + String::from_utf8_lossy(&canonical.stderr) + ); + + assert_eq!( + parent_curdir_segment.stdout, canonical.stdout, + "parent-curdir-segment and canonical absolute single-file compile stdout should match" + ); + assert_eq!( + parent_curdir_segment.stderr, canonical.stderr, + "parent-curdir-segment and canonical absolute single-file compile stderr should match" + ); + assert_no_stage_failures(&String::from_utf8_lossy(&parent_curdir_segment.stderr)); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_absolute_parent_segment_single_file_target_matches_canonical_output() { let root = unique_temp_dir("compile_absolute_parent_segment_single_file"); @@ -2988,6 +3127,55 @@ fn compile_command_absolute_parent_segment_double_separator_single_file_target_m std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_absolute_parent_segment_double_separator_trailing_slash_single_file_target_matches_canonical_output( +) { + let root = unique_temp_dir("compile_absolute_parent_segment_double_trailing_single_file"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + write_minimal_directory_compile_fixture(&root); + let absolute_parent_segment_double_trailing_target = + PathBuf::from(format!("{}/dsl/sample/..//sample/main.dag/", root.display())); + let canonical_target = root.join("dsl/sample/main.dag"); + + let parent_segment_double_trailing = Command::new(daglang_bin()) + .arg("compile") + .arg(&absolute_parent_segment_double_trailing_target) + .current_dir(&root) + .output() + .expect("failed to run parent-segment-double-trailing absolute single-file compile"); + assert!( + parent_segment_double_trailing.status.success(), + "parent-segment-double-trailing absolute single-file compile should succeed: {}", + String::from_utf8_lossy(&parent_segment_double_trailing.stderr) + ); + + let canonical = Command::new(daglang_bin()) + .arg("compile") + .arg(&canonical_target) + .current_dir(&root) + .output() + .expect("failed to run canonical absolute single-file compile"); + assert!( + canonical.status.success(), + "canonical absolute single-file compile should succeed: {}", + String::from_utf8_lossy(&canonical.stderr) + ); + + assert_eq!( + parent_segment_double_trailing.stdout, canonical.stdout, + "parent-segment-double-trailing and canonical absolute single-file compile stdout should match" + ); + assert_eq!( + parent_segment_double_trailing.stderr, canonical.stderr, + "parent-segment-double-trailing and canonical absolute single-file compile stderr should match" + ); + assert_no_stage_failures(&String::from_utf8_lossy( + &parent_segment_double_trailing.stderr + )); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_absolute_parent_segment_trailing_slash_single_file_target_matches_canonical_output() { @@ -3201,6 +3389,10 @@ fn compile_command_absolute_missing_root_variants_match_canonical_output() { let variants = vec![ ("mixed", root.join(".").join("missing_root")), ("parent", root.join("anchor/../missing_root")), + ( + "parent_curdir_segment", + root.join("anchor/.././missing_root"), + ), ( "parent_double_separator", PathBuf::from(format!("{}/anchor/..//missing_root", root.display())), @@ -3282,6 +3474,10 @@ fn compile_command_absolute_non_directory_root_variants_match_canonical_output() let variants = vec![ ("mixed", root.join(".").join("input.txt")), ("parent", root.join("anchor/../input.txt")), + ( + "parent_curdir_segment", + root.join("anchor/.././input.txt"), + ), ( "parent_double_separator", PathBuf::from(format!("{}/anchor/..//input.txt", root.display())), @@ -3361,6 +3557,10 @@ fn compile_command_absolute_missing_single_file_variants_match_canonical_output( let variants = vec![ ("mixed", root.join(".").join("dsl/sample/missing.dag")), ("parent", root.join("dsl/sample/../sample/missing.dag")), + ( + "parent_curdir_segment", + root.join("dsl/sample/.././sample/missing.dag"), + ), ( "parent_double_separator", PathBuf::from(format!("{}/dsl/sample/..//sample/missing.dag", root.display())), From 4926a7260b36b7b1d1b24a22ac74556b8e7c055c Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 12:53:59 +0000 Subject: [PATCH 131/169] Add invalid single-file compile path normalization regressions Co-authored-by: Brian Searls --- .../daglang-cli/tests/compile_commands.rs | 528 ++++++++++++++++++ 1 file changed, 528 insertions(+) diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index 828fb6c98af..c71419e1d4a 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -869,6 +869,199 @@ fn compile_command_curdir_segment_missing_single_file_target_matches_plain_relat std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_relative_and_absolute_invalid_single_file_targets_are_equivalent() { + let root = unique_temp_dir("compile_relative_absolute_invalid_single_file_target"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + let invalid_target = root.join("invalid.dag"); + std::fs::create_dir_all(&invalid_target) + .expect("failed to create invalid single-file target directory"); + + let relative = Command::new(daglang_bin()) + .arg("compile") + .arg("invalid.dag") + .current_dir(&root) + .output() + .expect("failed to run relative invalid single-file compile"); + assert!( + !relative.status.success(), + "relative invalid single-file compile should fail" + ); + + let absolute = Command::new(daglang_bin()) + .arg("compile") + .arg(&invalid_target) + .current_dir(&root) + .output() + .expect("failed to run absolute invalid single-file compile"); + assert!( + !absolute.status.success(), + "absolute invalid single-file compile should fail" + ); + + assert_eq!( + relative.stdout, absolute.stdout, + "relative and absolute invalid single-file compile stdout should match" + ); + assert_eq!( + relative.stderr, absolute.stderr, + "relative and absolute invalid single-file compile stderr should match" + ); + let stderr = String::from_utf8_lossy(&relative.stderr); + assert!( + stderr.contains(&format!("failed to read {}", invalid_target.display())), + "invalid single-file diagnostic should include normalized absolute path: {stderr}" + ); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + +#[test] +fn compile_command_trailing_slash_invalid_single_file_target_matches_plain_relative_output() { + let root = unique_temp_dir("compile_trailing_invalid_single_file_target"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + let invalid_target = root.join("invalid.dag"); + std::fs::create_dir_all(&invalid_target) + .expect("failed to create invalid single-file target directory"); + + let plain = Command::new(daglang_bin()) + .arg("compile") + .arg("invalid.dag") + .current_dir(&root) + .output() + .expect("failed to run plain invalid single-file compile"); + assert!( + !plain.status.success(), + "plain invalid single-file compile should fail" + ); + + let trailing_slash = Command::new(daglang_bin()) + .arg("compile") + .arg("invalid.dag/") + .current_dir(&root) + .output() + .expect("failed to run trailing invalid single-file compile"); + assert!( + !trailing_slash.status.success(), + "trailing invalid single-file compile should fail" + ); + + assert_eq!( + plain.stdout, trailing_slash.stdout, + "plain and trailing invalid single-file compile stdout should match" + ); + assert_eq!( + plain.stderr, trailing_slash.stderr, + "plain and trailing invalid single-file compile stderr should match" + ); + let stderr = String::from_utf8_lossy(&plain.stderr); + assert!( + stderr.contains(&format!("failed to read {}", invalid_target.display())), + "trailing invalid single-file diagnostic should include normalized absolute path: {stderr}" + ); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + +#[test] +fn compile_command_curdir_segment_invalid_single_file_target_matches_plain_relative_output() { + let root = unique_temp_dir("compile_curdir_invalid_single_file_target"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + let invalid_target = root.join("invalid.dag"); + std::fs::create_dir_all(&invalid_target) + .expect("failed to create invalid single-file target directory"); + + let plain = Command::new(daglang_bin()) + .arg("compile") + .arg("invalid.dag") + .current_dir(&root) + .output() + .expect("failed to run plain invalid single-file compile"); + assert!( + !plain.status.success(), + "plain invalid single-file compile should fail" + ); + + let curdir = Command::new(daglang_bin()) + .arg("compile") + .arg("./invalid.dag") + .current_dir(&root) + .output() + .expect("failed to run curdir invalid single-file compile"); + assert!( + !curdir.status.success(), + "curdir invalid single-file compile should fail" + ); + + assert_eq!( + plain.stdout, curdir.stdout, + "plain and curdir invalid single-file compile stdout should match" + ); + assert_eq!( + plain.stderr, curdir.stderr, + "plain and curdir invalid single-file compile stderr should match" + ); + let stderr = String::from_utf8_lossy(&plain.stderr); + assert!( + stderr.contains(&format!("failed to read {}", invalid_target.display())), + "curdir invalid single-file diagnostic should include normalized absolute path: {stderr}" + ); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + +#[test] +fn compile_command_curdir_double_separator_invalid_single_file_target_matches_plain_relative_output( +) { + let root = unique_temp_dir("compile_curdir_double_invalid_single_file_target"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + let invalid_target = root.join("invalid.dag"); + std::fs::create_dir_all(&invalid_target) + .expect("failed to create invalid single-file target directory"); + + let plain = Command::new(daglang_bin()) + .arg("compile") + .arg("invalid.dag") + .current_dir(&root) + .output() + .expect("failed to run plain invalid single-file compile"); + assert!( + !plain.status.success(), + "plain invalid single-file compile should fail" + ); + + let curdir_double = Command::new(daglang_bin()) + .arg("compile") + .arg(".//invalid.dag") + .current_dir(&root) + .output() + .expect("failed to run curdir-double invalid single-file compile"); + assert!( + !curdir_double.status.success(), + "curdir-double invalid single-file compile should fail" + ); + + assert_eq!( + plain.stdout, curdir_double.stdout, + "plain and curdir-double invalid single-file compile stdout should match" + ); + assert_eq!( + plain.stderr, curdir_double.stderr, + "plain and curdir-double invalid single-file compile stderr should match" + ); + let stderr = String::from_utf8_lossy(&plain.stderr); + assert!( + stderr.contains(&format!("failed to read {}", invalid_target.display())), + "curdir-double invalid single-file diagnostic should include normalized absolute path: {stderr}" + ); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_curdir_double_separator_single_file_target_matches_plain_relative_output() { let root = unique_temp_dir("compile_curdir_double_separator_single_file_target"); @@ -2012,6 +2205,248 @@ fn compile_command_parent_curdir_trailing_slash_missing_single_file_target_is_no std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_parent_segment_invalid_single_file_target_is_normalized_and_equivalent() { + let root = unique_temp_dir("compile_parent_segment_invalid_single_file_target"); + std::fs::create_dir_all(root.join("child")).expect("failed to create temp root"); + let invalid_target = root.join("invalid.dag"); + std::fs::create_dir_all(&invalid_target) + .expect("failed to create invalid single-file target directory"); + + let parent_segment = Command::new(daglang_bin()) + .arg("compile") + .arg("../invalid.dag") + .current_dir(root.join("child")) + .output() + .expect("failed to run parent-segment invalid single-file compile"); + assert!( + !parent_segment.status.success(), + "parent-segment invalid single-file compile should fail" + ); + + let absolute = Command::new(daglang_bin()) + .arg("compile") + .arg(&invalid_target) + .current_dir(root.join("child")) + .output() + .expect("failed to run absolute invalid single-file compile"); + assert!( + !absolute.status.success(), + "absolute invalid single-file compile should fail" + ); + + assert_eq!( + parent_segment.stdout, absolute.stdout, + "parent-segment and absolute invalid single-file compile stdout should match" + ); + assert_eq!( + parent_segment.stderr, absolute.stderr, + "parent-segment and absolute invalid single-file compile stderr should match" + ); + let stderr = String::from_utf8_lossy(&parent_segment.stderr); + assert!( + stderr.contains(&format!("failed to read {}", invalid_target.display())), + "parent-segment invalid single-file diagnostic should normalize to absolute path: {stderr}" + ); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + +#[test] +fn compile_command_parent_double_separator_invalid_single_file_target_is_normalized_and_equivalent() { + let root = unique_temp_dir("compile_parent_double_invalid_single_file_target"); + std::fs::create_dir_all(root.join("child")).expect("failed to create temp root"); + let invalid_target = root.join("invalid.dag"); + std::fs::create_dir_all(&invalid_target) + .expect("failed to create invalid single-file target directory"); + + let parent_double = Command::new(daglang_bin()) + .arg("compile") + .arg("..//invalid.dag") + .current_dir(root.join("child")) + .output() + .expect("failed to run parent-double invalid single-file compile"); + assert!( + !parent_double.status.success(), + "parent-double invalid single-file compile should fail" + ); + + let absolute = Command::new(daglang_bin()) + .arg("compile") + .arg(&invalid_target) + .current_dir(root.join("child")) + .output() + .expect("failed to run absolute invalid single-file compile"); + assert!( + !absolute.status.success(), + "absolute invalid single-file compile should fail" + ); + + assert_eq!( + parent_double.stdout, absolute.stdout, + "parent-double and absolute invalid single-file compile stdout should match" + ); + assert_eq!( + parent_double.stderr, absolute.stderr, + "parent-double and absolute invalid single-file compile stderr should match" + ); + let stderr = String::from_utf8_lossy(&parent_double.stderr); + assert!( + stderr.contains(&format!("failed to read {}", invalid_target.display())), + "parent-double invalid single-file diagnostic should normalize to absolute path: {stderr}" + ); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + +#[test] +fn compile_command_parent_curdir_segment_invalid_single_file_target_is_normalized_and_equivalent() { + let root = unique_temp_dir("compile_parent_curdir_segment_invalid_single_file"); + std::fs::create_dir_all(root.join("child")).expect("failed to create temp root"); + let invalid_target = root.join("invalid.dag"); + std::fs::create_dir_all(&invalid_target) + .expect("failed to create invalid single-file target directory"); + + let parent_curdir_segment = Command::new(daglang_bin()) + .arg("compile") + .arg(".././invalid.dag") + .current_dir(root.join("child")) + .output() + .expect("failed to run parent-curdir-segment invalid single-file compile"); + assert!( + !parent_curdir_segment.status.success(), + "parent-curdir-segment invalid single-file compile should fail" + ); + + let absolute = Command::new(daglang_bin()) + .arg("compile") + .arg(&invalid_target) + .current_dir(root.join("child")) + .output() + .expect("failed to run absolute invalid single-file compile"); + assert!( + !absolute.status.success(), + "absolute invalid single-file compile should fail" + ); + + assert_eq!( + parent_curdir_segment.stdout, absolute.stdout, + "parent-curdir-segment and absolute invalid single-file compile stdout should match" + ); + assert_eq!( + parent_curdir_segment.stderr, absolute.stderr, + "parent-curdir-segment and absolute invalid single-file compile stderr should match" + ); + let stderr = String::from_utf8_lossy(&parent_curdir_segment.stderr); + assert!( + stderr.contains(&format!("failed to read {}", invalid_target.display())), + "parent-curdir-segment invalid single-file diagnostic should normalize to absolute path: {stderr}" + ); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + +#[test] +fn compile_command_parent_curdir_double_separator_invalid_single_file_target_is_normalized_and_equivalent( +) { + let root = unique_temp_dir("compile_parent_curdir_double_invalid_single_file"); + std::fs::create_dir_all(root.join("child")).expect("failed to create temp root"); + let invalid_target = root.join("invalid.dag"); + std::fs::create_dir_all(&invalid_target) + .expect("failed to create invalid single-file target directory"); + + let parent_curdir_double = Command::new(daglang_bin()) + .arg("compile") + .arg(".././invalid.dag//") + .current_dir(root.join("child")) + .output() + .expect("failed to run parent-curdir-double invalid single-file compile"); + assert!( + !parent_curdir_double.status.success(), + "parent-curdir-double invalid single-file compile should fail" + ); + + let absolute = Command::new(daglang_bin()) + .arg("compile") + .arg(&invalid_target) + .current_dir(root.join("child")) + .output() + .expect("failed to run absolute invalid single-file compile"); + assert!( + !absolute.status.success(), + "absolute invalid single-file compile should fail" + ); + + assert_eq!( + parent_curdir_double.stdout, absolute.stdout, + "parent-curdir-double and absolute invalid single-file compile stdout should match" + ); + assert_eq!( + parent_curdir_double.stderr, absolute.stderr, + "parent-curdir-double and absolute invalid single-file compile stderr should match" + ); + let stderr = String::from_utf8_lossy(&parent_curdir_double.stderr); + assert!( + stderr.contains(&format!("failed to read {}", invalid_target.display())), + "parent-curdir-double invalid single-file diagnostic should normalize to absolute path: {stderr}" + ); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + +#[test] +fn compile_command_parent_curdir_trailing_slash_invalid_single_file_target_is_normalized_and_equivalent( +) { + let root = unique_temp_dir("compile_parent_curdir_trailing_invalid_single_file"); + std::fs::create_dir_all(root.join("child")).expect("failed to create temp root"); + let invalid_target = root.join("invalid.dag"); + std::fs::create_dir_all(&invalid_target) + .expect("failed to create invalid single-file target directory"); + + let parent_curdir_trailing = Command::new(daglang_bin()) + .arg("compile") + .arg(".././invalid.dag/") + .current_dir(root.join("child")) + .output() + .expect("failed to run parent-curdir-trailing invalid single-file compile"); + assert!( + !parent_curdir_trailing.status.success(), + "parent-curdir-trailing invalid single-file compile should fail" + ); + + let absolute = Command::new(daglang_bin()) + .arg("compile") + .arg(&invalid_target) + .current_dir(root.join("child")) + .output() + .expect("failed to run absolute invalid single-file compile"); + assert!( + !absolute.status.success(), + "absolute invalid single-file compile should fail" + ); + + assert_eq!( + parent_curdir_trailing.stdout, absolute.stdout, + "parent-curdir-trailing and absolute invalid single-file compile stdout should match" + ); + assert_eq!( + parent_curdir_trailing.stderr, absolute.stderr, + "parent-curdir-trailing and absolute invalid single-file compile stderr should match" + ); + let stderr = String::from_utf8_lossy(&parent_curdir_trailing.stderr); + assert!( + stderr.contains(&format!("failed to read {}", invalid_target.display())), + "parent-curdir-trailing invalid single-file diagnostic should normalize to absolute path: {stderr}" + ); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_parent_curdir_segment_single_file_target_matches_absolute_output() { let root = unique_temp_dir("compile_parent_curdir_segment_single_file"); @@ -3613,6 +4048,99 @@ fn compile_command_absolute_missing_single_file_variants_match_canonical_output( std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_absolute_invalid_single_file_variants_match_canonical_output() { + let root = unique_temp_dir("compile_absolute_invalid_single_file_variants"); + std::fs::create_dir_all(root.join("anchor")).expect("failed to create anchor directory"); + let canonical_invalid_single_file = root.join("dsl/sample/invalid.dag"); + std::fs::create_dir_all(&canonical_invalid_single_file) + .expect("failed to create invalid single-file target fixture directory"); + + let canonical = Command::new(daglang_bin()) + .arg("compile") + .arg(&canonical_invalid_single_file) + .current_dir(&root) + .output() + .expect("failed to run canonical absolute invalid single-file compile"); + assert!( + !canonical.status.success(), + "canonical absolute invalid single-file compile should fail" + ); + let canonical_stderr = String::from_utf8_lossy(&canonical.stderr); + assert_no_stage_failures(&canonical_stderr); + assert!( + canonical_stderr.contains(&canonical_invalid_single_file.display().to_string()), + "canonical invalid single-file diagnostic should contain normalized absolute path: {canonical_stderr}" + ); + + let variants = vec![ + ("mixed", root.join(".").join("dsl/sample/invalid.dag")), + ("parent", root.join("anchor/../dsl/sample/invalid.dag")), + ( + "parent_curdir_segment", + root.join("anchor/.././dsl/sample/invalid.dag"), + ), + ( + "parent_double_separator", + PathBuf::from(format!( + "{}/anchor/..//dsl/sample/invalid.dag", + root.display() + )), + ), + ( + "parent_trailing", + PathBuf::from(format!( + "{}/", + root.join("anchor/../dsl/sample/invalid.dag").display() + )), + ), + ( + "parent_double_separator_trailing", + PathBuf::from(format!( + "{}/anchor/..//dsl/sample/invalid.dag/", + root.display() + )), + ), + ( + "double_separator", + PathBuf::from(format!("{}//dsl//sample//invalid.dag", root.display())), + ), + ( + "trailing_slash", + PathBuf::from(format!("{}/", canonical_invalid_single_file.display())), + ), + ( + "double_separator_trailing", + PathBuf::from(format!("{}//dsl//sample//invalid.dag/", root.display())), + ), + ]; + + for (label, variant_path) in variants { + let variant = Command::new(daglang_bin()) + .arg("compile") + .arg(&variant_path) + .current_dir(&root) + .output() + .unwrap_or_else(|_| { + panic!("failed to run {label} absolute invalid single-file compile variant") + }); + assert!( + !variant.status.success(), + "{label} absolute invalid single-file compile variant should fail" + ); + assert_eq!( + variant.stdout, canonical.stdout, + "{label} invalid single-file variant stdout should match canonical output" + ); + assert_eq!( + variant.stderr, canonical.stderr, + "{label} invalid single-file variant stderr should match canonical output" + ); + } + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_curdir_segment_root_matches_plain_relative_output() { let root = unique_temp_dir("compile_curdir_segment_root"); From df9845b21de828998f838184223df65be62342f8 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 12:58:56 +0000 Subject: [PATCH 132/169] Cover trailing parent invalid single-file compile variants Co-authored-by: Brian Searls --- .../daglang-cli/tests/compile_commands.rs | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index c71419e1d4a..a89c81a6d01 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -2253,6 +2253,55 @@ fn compile_command_parent_segment_invalid_single_file_target_is_normalized_and_e std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_parent_segment_trailing_slash_invalid_single_file_target_is_normalized_and_equivalent( +) { + let root = unique_temp_dir("compile_parent_segment_trailing_invalid_single_file"); + std::fs::create_dir_all(root.join("child")).expect("failed to create temp root"); + let invalid_target = root.join("invalid.dag"); + std::fs::create_dir_all(&invalid_target) + .expect("failed to create invalid single-file target directory"); + + let parent_trailing = Command::new(daglang_bin()) + .arg("compile") + .arg("../invalid.dag/") + .current_dir(root.join("child")) + .output() + .expect("failed to run parent-trailing invalid single-file compile"); + assert!( + !parent_trailing.status.success(), + "parent-trailing invalid single-file compile should fail" + ); + + let absolute = Command::new(daglang_bin()) + .arg("compile") + .arg(&invalid_target) + .current_dir(root.join("child")) + .output() + .expect("failed to run absolute invalid single-file compile"); + assert!( + !absolute.status.success(), + "absolute invalid single-file compile should fail" + ); + + assert_eq!( + parent_trailing.stdout, absolute.stdout, + "parent-trailing and absolute invalid single-file compile stdout should match" + ); + assert_eq!( + parent_trailing.stderr, absolute.stderr, + "parent-trailing and absolute invalid single-file compile stderr should match" + ); + let stderr = String::from_utf8_lossy(&parent_trailing.stderr); + assert!( + stderr.contains(&format!("failed to read {}", invalid_target.display())), + "parent-trailing invalid single-file diagnostic should normalize to absolute path: {stderr}" + ); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_parent_double_separator_invalid_single_file_target_is_normalized_and_equivalent() { let root = unique_temp_dir("compile_parent_double_invalid_single_file_target"); @@ -2301,6 +2350,55 @@ fn compile_command_parent_double_separator_invalid_single_file_target_is_normali std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_parent_double_separator_trailing_slash_invalid_single_file_target_is_normalized_and_equivalent( +) { + let root = unique_temp_dir("compile_parent_double_trailing_invalid_single_file"); + std::fs::create_dir_all(root.join("child")).expect("failed to create temp root"); + let invalid_target = root.join("invalid.dag"); + std::fs::create_dir_all(&invalid_target) + .expect("failed to create invalid single-file target directory"); + + let parent_double_trailing = Command::new(daglang_bin()) + .arg("compile") + .arg("..//invalid.dag/") + .current_dir(root.join("child")) + .output() + .expect("failed to run parent-double-trailing invalid single-file compile"); + assert!( + !parent_double_trailing.status.success(), + "parent-double-trailing invalid single-file compile should fail" + ); + + let absolute = Command::new(daglang_bin()) + .arg("compile") + .arg(&invalid_target) + .current_dir(root.join("child")) + .output() + .expect("failed to run absolute invalid single-file compile"); + assert!( + !absolute.status.success(), + "absolute invalid single-file compile should fail" + ); + + assert_eq!( + parent_double_trailing.stdout, absolute.stdout, + "parent-double-trailing and absolute invalid single-file compile stdout should match" + ); + assert_eq!( + parent_double_trailing.stderr, absolute.stderr, + "parent-double-trailing and absolute invalid single-file compile stderr should match" + ); + let stderr = String::from_utf8_lossy(&parent_double_trailing.stderr); + assert!( + stderr.contains(&format!("failed to read {}", invalid_target.display())), + "parent-double-trailing invalid single-file diagnostic should normalize to absolute path: {stderr}" + ); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_parent_curdir_segment_invalid_single_file_target_is_normalized_and_equivalent() { let root = unique_temp_dir("compile_parent_curdir_segment_invalid_single_file"); From 3d7653796fa4b4fa4d23784fd6d002c765a97ffa Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 13:03:49 +0000 Subject: [PATCH 133/169] Add compile absolute curdir-trailing normalization regressions Co-authored-by: Brian Searls --- .../daglang-cli/tests/compile_commands.rs | 108 ++++++++++++++++++ 1 file changed, 108 insertions(+) diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index a89c81a6d01..8787a10c686 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -3110,6 +3110,51 @@ fn compile_command_absolute_mixed_segment_root_matches_canonical_absolute_output std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_absolute_curdir_segment_trailing_slash_root_matches_canonical_absolute_output() { + let root = unique_temp_dir("compile_absolute_curdir_segment_trailing_root"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + write_minimal_directory_compile_fixture(&root); + let absolute_curdir_segment_trailing_root = PathBuf::from(format!("{}/./dsl/", root.display())); + let canonical_root = root.join("dsl"); + + let curdir_segment_trailing = Command::new(daglang_bin()) + .arg("compile") + .arg(&absolute_curdir_segment_trailing_root) + .current_dir(&root) + .output() + .expect("failed to run curdir-segment-trailing absolute root compile"); + assert!( + curdir_segment_trailing.status.success(), + "curdir-segment-trailing absolute root compile should succeed: {}", + String::from_utf8_lossy(&curdir_segment_trailing.stderr) + ); + + let canonical = Command::new(daglang_bin()) + .arg("compile") + .arg(&canonical_root) + .current_dir(&root) + .output() + .expect("failed to run canonical absolute root compile"); + assert!( + canonical.status.success(), + "canonical absolute root compile should succeed: {}", + String::from_utf8_lossy(&canonical.stderr) + ); + + assert_eq!( + curdir_segment_trailing.stdout, canonical.stdout, + "curdir-segment-trailing and canonical absolute-root compile stdout should match" + ); + assert_eq!( + curdir_segment_trailing.stderr, canonical.stderr, + "curdir-segment-trailing and canonical absolute-root compile stderr should match" + ); + assert_no_stage_failures(&String::from_utf8_lossy(&curdir_segment_trailing.stderr)); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_absolute_parent_curdir_segment_root_matches_canonical_absolute_output() { let root = unique_temp_dir("compile_absolute_parent_curdir_segment_root"); @@ -3523,6 +3568,53 @@ fn compile_command_absolute_mixed_segment_single_file_target_matches_canonical_o std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_absolute_curdir_segment_trailing_slash_single_file_target_matches_canonical_output( +) { + let root = unique_temp_dir("compile_absolute_curdir_segment_trailing_single_file"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + write_minimal_directory_compile_fixture(&root); + let absolute_curdir_segment_trailing_target = + PathBuf::from(format!("{}/./dsl/sample/main.dag/", root.display())); + let canonical_target = root.join("dsl/sample/main.dag"); + + let curdir_segment_trailing = Command::new(daglang_bin()) + .arg("compile") + .arg(&absolute_curdir_segment_trailing_target) + .current_dir(&root) + .output() + .expect("failed to run curdir-segment-trailing absolute single-file compile"); + assert!( + curdir_segment_trailing.status.success(), + "curdir-segment-trailing absolute single-file compile should succeed: {}", + String::from_utf8_lossy(&curdir_segment_trailing.stderr) + ); + + let canonical = Command::new(daglang_bin()) + .arg("compile") + .arg(&canonical_target) + .current_dir(&root) + .output() + .expect("failed to run canonical absolute single-file compile"); + assert!( + canonical.status.success(), + "canonical absolute single-file compile should succeed: {}", + String::from_utf8_lossy(&canonical.stderr) + ); + + assert_eq!( + curdir_segment_trailing.stdout, canonical.stdout, + "curdir-segment-trailing and canonical absolute single-file compile stdout should match" + ); + assert_eq!( + curdir_segment_trailing.stderr, canonical.stderr, + "curdir-segment-trailing and canonical absolute single-file compile stderr should match" + ); + assert_no_stage_failures(&String::from_utf8_lossy(&curdir_segment_trailing.stderr)); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_absolute_parent_curdir_segment_single_file_target_matches_canonical_output() { let root = unique_temp_dir("compile_absolute_parent_curdir_segment_single_file"); @@ -3921,6 +4013,10 @@ fn compile_command_absolute_missing_root_variants_match_canonical_output() { let variants = vec![ ("mixed", root.join(".").join("missing_root")), + ( + "curdir_segment_trailing", + PathBuf::from(format!("{}/./missing_root/", root.display())), + ), ("parent", root.join("anchor/../missing_root")), ( "parent_curdir_segment", @@ -4006,6 +4102,10 @@ fn compile_command_absolute_non_directory_root_variants_match_canonical_output() let variants = vec![ ("mixed", root.join(".").join("input.txt")), + ( + "curdir_segment_trailing", + PathBuf::from(format!("{}/./input.txt/", root.display())), + ), ("parent", root.join("anchor/../input.txt")), ( "parent_curdir_segment", @@ -4089,6 +4189,10 @@ fn compile_command_absolute_missing_single_file_variants_match_canonical_output( let variants = vec![ ("mixed", root.join(".").join("dsl/sample/missing.dag")), + ( + "curdir_segment_trailing", + PathBuf::from(format!("{}/./dsl/sample/missing.dag/", root.display())), + ), ("parent", root.join("dsl/sample/../sample/missing.dag")), ( "parent_curdir_segment", @@ -4173,6 +4277,10 @@ fn compile_command_absolute_invalid_single_file_variants_match_canonical_output( let variants = vec![ ("mixed", root.join(".").join("dsl/sample/invalid.dag")), + ( + "curdir_segment_trailing", + PathBuf::from(format!("{}/./dsl/sample/invalid.dag/", root.display())), + ), ("parent", root.join("anchor/../dsl/sample/invalid.dag")), ( "parent_curdir_segment", From a3ff35bc2c9a61a33ad88d547cb2da01325a6cad Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 13:07:04 +0000 Subject: [PATCH 134/169] Expand compile absolute parent-curdir double-path regressions Co-authored-by: Brian Searls --- .../daglang-cli/tests/compile_commands.rs | 120 ++++++++++++++++++ 1 file changed, 120 insertions(+) diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index 8787a10c686..07c5bcfaa0b 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -3200,6 +3200,55 @@ fn compile_command_absolute_parent_curdir_segment_root_matches_canonical_absolut std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_absolute_parent_curdir_segment_double_separator_root_matches_canonical_absolute_output( +) { + let root = unique_temp_dir("compile_absolute_parent_curdir_segment_double_root"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + write_minimal_directory_compile_fixture(&root); + let absolute_parent_curdir_segment_double_root = + PathBuf::from(format!("{}/anchor/.././dsl//", root.display())); + let canonical_root = root.join("dsl"); + + let parent_curdir_segment_double = Command::new(daglang_bin()) + .arg("compile") + .arg(&absolute_parent_curdir_segment_double_root) + .current_dir(&root) + .output() + .expect("failed to run parent-curdir-segment-double absolute root compile"); + assert!( + parent_curdir_segment_double.status.success(), + "parent-curdir-segment-double absolute root compile should succeed: {}", + String::from_utf8_lossy(&parent_curdir_segment_double.stderr) + ); + + let canonical = Command::new(daglang_bin()) + .arg("compile") + .arg(&canonical_root) + .current_dir(&root) + .output() + .expect("failed to run canonical absolute root compile"); + assert!( + canonical.status.success(), + "canonical absolute root compile should succeed: {}", + String::from_utf8_lossy(&canonical.stderr) + ); + + assert_eq!( + parent_curdir_segment_double.stdout, canonical.stdout, + "parent-curdir-segment-double and canonical absolute-root compile stdout should match" + ); + assert_eq!( + parent_curdir_segment_double.stderr, canonical.stderr, + "parent-curdir-segment-double and canonical absolute-root compile stderr should match" + ); + assert_no_stage_failures(&String::from_utf8_lossy( + &parent_curdir_segment_double.stderr + )); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_absolute_parent_segment_root_matches_canonical_absolute_output() { let root = unique_temp_dir("compile_absolute_parent_segment_root"); @@ -3660,6 +3709,55 @@ fn compile_command_absolute_parent_curdir_segment_single_file_target_matches_can std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_absolute_parent_curdir_segment_double_separator_single_file_target_matches_canonical_output( +) { + let root = unique_temp_dir("compile_absolute_parent_curdir_segment_double_single_file"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + write_minimal_directory_compile_fixture(&root); + let absolute_parent_curdir_segment_double_target = + PathBuf::from(format!("{}/anchor/.././dsl/sample//main.dag", root.display())); + let canonical_target = root.join("dsl/sample/main.dag"); + + let parent_curdir_segment_double = Command::new(daglang_bin()) + .arg("compile") + .arg(&absolute_parent_curdir_segment_double_target) + .current_dir(&root) + .output() + .expect("failed to run parent-curdir-segment-double absolute single-file compile"); + assert!( + parent_curdir_segment_double.status.success(), + "parent-curdir-segment-double absolute single-file compile should succeed: {}", + String::from_utf8_lossy(&parent_curdir_segment_double.stderr) + ); + + let canonical = Command::new(daglang_bin()) + .arg("compile") + .arg(&canonical_target) + .current_dir(&root) + .output() + .expect("failed to run canonical absolute single-file compile"); + assert!( + canonical.status.success(), + "canonical absolute single-file compile should succeed: {}", + String::from_utf8_lossy(&canonical.stderr) + ); + + assert_eq!( + parent_curdir_segment_double.stdout, canonical.stdout, + "parent-curdir-segment-double and canonical absolute single-file compile stdout should match" + ); + assert_eq!( + parent_curdir_segment_double.stderr, canonical.stderr, + "parent-curdir-segment-double and canonical absolute single-file compile stderr should match" + ); + assert_no_stage_failures(&String::from_utf8_lossy( + &parent_curdir_segment_double.stderr + )); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_absolute_parent_segment_single_file_target_matches_canonical_output() { let root = unique_temp_dir("compile_absolute_parent_segment_single_file"); @@ -4022,6 +4120,10 @@ fn compile_command_absolute_missing_root_variants_match_canonical_output() { "parent_curdir_segment", root.join("anchor/.././missing_root"), ), + ( + "parent_curdir_segment_double_separator", + PathBuf::from(format!("{}/anchor/.././missing_root//", root.display())), + ), ( "parent_double_separator", PathBuf::from(format!("{}/anchor/..//missing_root", root.display())), @@ -4111,6 +4213,10 @@ fn compile_command_absolute_non_directory_root_variants_match_canonical_output() "parent_curdir_segment", root.join("anchor/.././input.txt"), ), + ( + "parent_curdir_segment_double_separator", + PathBuf::from(format!("{}/anchor/.././input.txt//", root.display())), + ), ( "parent_double_separator", PathBuf::from(format!("{}/anchor/..//input.txt", root.display())), @@ -4198,6 +4304,13 @@ fn compile_command_absolute_missing_single_file_variants_match_canonical_output( "parent_curdir_segment", root.join("dsl/sample/.././sample/missing.dag"), ), + ( + "parent_curdir_segment_double_separator", + PathBuf::from(format!( + "{}/dsl/sample/.././sample//missing.dag", + root.display() + )), + ), ( "parent_double_separator", PathBuf::from(format!("{}/dsl/sample/..//sample/missing.dag", root.display())), @@ -4286,6 +4399,13 @@ fn compile_command_absolute_invalid_single_file_variants_match_canonical_output( "parent_curdir_segment", root.join("anchor/.././dsl/sample/invalid.dag"), ), + ( + "parent_curdir_segment_double_separator", + PathBuf::from(format!( + "{}/anchor/.././dsl/sample//invalid.dag", + root.display() + )), + ), ( "parent_double_separator", PathBuf::from(format!( From da67db8f064fb67b2890d311ed52a1f765fb06a9 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 13:11:55 +0000 Subject: [PATCH 135/169] Add compile absolute curdir-double normalization regressions Co-authored-by: Brian Searls --- .../daglang-cli/tests/compile_commands.rs | 113 ++++++++++++++++++ 1 file changed, 113 insertions(+) diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index 07c5bcfaa0b..5e87e083d71 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -3155,6 +3155,54 @@ fn compile_command_absolute_curdir_segment_trailing_slash_root_matches_canonical std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_absolute_curdir_segment_double_separator_root_matches_canonical_absolute_output() { + let root = unique_temp_dir("compile_absolute_curdir_segment_double_root"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + write_minimal_directory_compile_fixture(&root); + let canonical_root = root.join("dsl"); + let absolute_curdir_segment_double_separator_root = + PathBuf::from(format!("{}//./", canonical_root.display())); + + let curdir_segment_double_separator = Command::new(daglang_bin()) + .arg("compile") + .arg(&absolute_curdir_segment_double_separator_root) + .current_dir(&root) + .output() + .expect("failed to run curdir-segment-double-separator absolute root compile"); + assert!( + curdir_segment_double_separator.status.success(), + "curdir-segment-double-separator absolute root compile should succeed: {}", + String::from_utf8_lossy(&curdir_segment_double_separator.stderr) + ); + + let canonical = Command::new(daglang_bin()) + .arg("compile") + .arg(&canonical_root) + .current_dir(&root) + .output() + .expect("failed to run canonical absolute root compile"); + assert!( + canonical.status.success(), + "canonical absolute root compile should succeed: {}", + String::from_utf8_lossy(&canonical.stderr) + ); + + assert_eq!( + curdir_segment_double_separator.stdout, canonical.stdout, + "curdir-segment-double-separator and canonical absolute-root compile stdout should match" + ); + assert_eq!( + curdir_segment_double_separator.stderr, canonical.stderr, + "curdir-segment-double-separator and canonical absolute-root compile stderr should match" + ); + assert_no_stage_failures(&String::from_utf8_lossy( + &curdir_segment_double_separator.stderr + )); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_absolute_parent_curdir_segment_root_matches_canonical_absolute_output() { let root = unique_temp_dir("compile_absolute_parent_curdir_segment_root"); @@ -3664,6 +3712,55 @@ fn compile_command_absolute_curdir_segment_trailing_slash_single_file_target_mat std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_absolute_curdir_segment_double_separator_single_file_target_matches_canonical_output( +) { + let root = unique_temp_dir("compile_absolute_curdir_segment_double_single_file"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + write_minimal_directory_compile_fixture(&root); + let canonical_target = root.join("dsl/sample/main.dag"); + let absolute_curdir_segment_double_separator_target = + PathBuf::from(format!("{}//./", canonical_target.display())); + + let curdir_segment_double_separator = Command::new(daglang_bin()) + .arg("compile") + .arg(&absolute_curdir_segment_double_separator_target) + .current_dir(&root) + .output() + .expect("failed to run curdir-segment-double-separator absolute single-file compile"); + assert!( + curdir_segment_double_separator.status.success(), + "curdir-segment-double-separator absolute single-file compile should succeed: {}", + String::from_utf8_lossy(&curdir_segment_double_separator.stderr) + ); + + let canonical = Command::new(daglang_bin()) + .arg("compile") + .arg(&canonical_target) + .current_dir(&root) + .output() + .expect("failed to run canonical absolute single-file compile"); + assert!( + canonical.status.success(), + "canonical absolute single-file compile should succeed: {}", + String::from_utf8_lossy(&canonical.stderr) + ); + + assert_eq!( + curdir_segment_double_separator.stdout, canonical.stdout, + "curdir-segment-double-separator and canonical absolute single-file compile stdout should match" + ); + assert_eq!( + curdir_segment_double_separator.stderr, canonical.stderr, + "curdir-segment-double-separator and canonical absolute single-file compile stderr should match" + ); + assert_no_stage_failures(&String::from_utf8_lossy( + &curdir_segment_double_separator.stderr + )); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_absolute_parent_curdir_segment_single_file_target_matches_canonical_output() { let root = unique_temp_dir("compile_absolute_parent_curdir_segment_single_file"); @@ -4115,6 +4212,10 @@ fn compile_command_absolute_missing_root_variants_match_canonical_output() { "curdir_segment_trailing", PathBuf::from(format!("{}/./missing_root/", root.display())), ), + ( + "curdir_segment_double_separator", + PathBuf::from(format!("{}//./", canonical_missing_root.display())), + ), ("parent", root.join("anchor/../missing_root")), ( "parent_curdir_segment", @@ -4208,6 +4309,10 @@ fn compile_command_absolute_non_directory_root_variants_match_canonical_output() "curdir_segment_trailing", PathBuf::from(format!("{}/./input.txt/", root.display())), ), + ( + "curdir_segment_double_separator", + PathBuf::from(format!("{}//./", canonical_non_directory_root.display())), + ), ("parent", root.join("anchor/../input.txt")), ( "parent_curdir_segment", @@ -4299,6 +4404,10 @@ fn compile_command_absolute_missing_single_file_variants_match_canonical_output( "curdir_segment_trailing", PathBuf::from(format!("{}/./dsl/sample/missing.dag/", root.display())), ), + ( + "curdir_segment_double_separator", + PathBuf::from(format!("{}//./", canonical_missing_single_file.display())), + ), ("parent", root.join("dsl/sample/../sample/missing.dag")), ( "parent_curdir_segment", @@ -4394,6 +4503,10 @@ fn compile_command_absolute_invalid_single_file_variants_match_canonical_output( "curdir_segment_trailing", PathBuf::from(format!("{}/./dsl/sample/invalid.dag/", root.display())), ), + ( + "curdir_segment_double_separator", + PathBuf::from(format!("{}//./", canonical_invalid_single_file.display())), + ), ("parent", root.join("anchor/../dsl/sample/invalid.dag")), ( "parent_curdir_segment", From a8e21045d57b040e000d13d4725f4120f13b0075 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 13:16:11 +0000 Subject: [PATCH 136/169] Add compile absolute curdir-suffix normalization regressions Co-authored-by: Brian Searls --- .../daglang-cli/tests/compile_commands.rs | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index 5e87e083d71..437da5f3edc 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -3203,6 +3203,51 @@ fn compile_command_absolute_curdir_segment_double_separator_root_matches_canonic std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_absolute_curdir_suffix_root_matches_canonical_absolute_output() { + let root = unique_temp_dir("compile_absolute_curdir_suffix_root"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + write_minimal_directory_compile_fixture(&root); + let canonical_root = root.join("dsl"); + let absolute_curdir_suffix_root = root.join("dsl/."); + + let curdir_suffix = Command::new(daglang_bin()) + .arg("compile") + .arg(&absolute_curdir_suffix_root) + .current_dir(&root) + .output() + .expect("failed to run curdir-suffix absolute root compile"); + assert!( + curdir_suffix.status.success(), + "curdir-suffix absolute root compile should succeed: {}", + String::from_utf8_lossy(&curdir_suffix.stderr) + ); + + let canonical = Command::new(daglang_bin()) + .arg("compile") + .arg(&canonical_root) + .current_dir(&root) + .output() + .expect("failed to run canonical absolute root compile"); + assert!( + canonical.status.success(), + "canonical absolute root compile should succeed: {}", + String::from_utf8_lossy(&canonical.stderr) + ); + + assert_eq!( + curdir_suffix.stdout, canonical.stdout, + "curdir-suffix and canonical absolute-root compile stdout should match" + ); + assert_eq!( + curdir_suffix.stderr, canonical.stderr, + "curdir-suffix and canonical absolute-root compile stderr should match" + ); + assert_no_stage_failures(&String::from_utf8_lossy(&curdir_suffix.stderr)); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_absolute_parent_curdir_segment_root_matches_canonical_absolute_output() { let root = unique_temp_dir("compile_absolute_parent_curdir_segment_root"); @@ -3761,6 +3806,51 @@ fn compile_command_absolute_curdir_segment_double_separator_single_file_target_m std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_absolute_curdir_suffix_single_file_target_matches_canonical_output() { + let root = unique_temp_dir("compile_absolute_curdir_suffix_single_file"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + write_minimal_directory_compile_fixture(&root); + let canonical_target = root.join("dsl/sample/main.dag"); + let absolute_curdir_suffix_target = root.join("dsl/sample/main.dag/."); + + let curdir_suffix = Command::new(daglang_bin()) + .arg("compile") + .arg(&absolute_curdir_suffix_target) + .current_dir(&root) + .output() + .expect("failed to run curdir-suffix absolute single-file compile"); + assert!( + curdir_suffix.status.success(), + "curdir-suffix absolute single-file compile should succeed: {}", + String::from_utf8_lossy(&curdir_suffix.stderr) + ); + + let canonical = Command::new(daglang_bin()) + .arg("compile") + .arg(&canonical_target) + .current_dir(&root) + .output() + .expect("failed to run canonical absolute single-file compile"); + assert!( + canonical.status.success(), + "canonical absolute single-file compile should succeed: {}", + String::from_utf8_lossy(&canonical.stderr) + ); + + assert_eq!( + curdir_suffix.stdout, canonical.stdout, + "curdir-suffix and canonical absolute single-file compile stdout should match" + ); + assert_eq!( + curdir_suffix.stderr, canonical.stderr, + "curdir-suffix and canonical absolute single-file compile stderr should match" + ); + assert_no_stage_failures(&String::from_utf8_lossy(&curdir_suffix.stderr)); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_absolute_parent_curdir_segment_single_file_target_matches_canonical_output() { let root = unique_temp_dir("compile_absolute_parent_curdir_segment_single_file"); @@ -4216,6 +4306,7 @@ fn compile_command_absolute_missing_root_variants_match_canonical_output() { "curdir_segment_double_separator", PathBuf::from(format!("{}//./", canonical_missing_root.display())), ), + ("curdir_suffix", root.join("missing_root/.")), ("parent", root.join("anchor/../missing_root")), ( "parent_curdir_segment", @@ -4313,6 +4404,7 @@ fn compile_command_absolute_non_directory_root_variants_match_canonical_output() "curdir_segment_double_separator", PathBuf::from(format!("{}//./", canonical_non_directory_root.display())), ), + ("curdir_suffix", root.join("input.txt/.")), ("parent", root.join("anchor/../input.txt")), ( "parent_curdir_segment", @@ -4408,6 +4500,7 @@ fn compile_command_absolute_missing_single_file_variants_match_canonical_output( "curdir_segment_double_separator", PathBuf::from(format!("{}//./", canonical_missing_single_file.display())), ), + ("curdir_suffix", root.join("dsl/sample/missing.dag/.")), ("parent", root.join("dsl/sample/../sample/missing.dag")), ( "parent_curdir_segment", @@ -4507,6 +4600,7 @@ fn compile_command_absolute_invalid_single_file_variants_match_canonical_output( "curdir_segment_double_separator", PathBuf::from(format!("{}//./", canonical_invalid_single_file.display())), ), + ("curdir_suffix", root.join("dsl/sample/invalid.dag/.")), ("parent", root.join("anchor/../dsl/sample/invalid.dag")), ( "parent_curdir_segment", From aacb752f43010b61a0e59cacbfe8dd456e383133 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 13:20:26 +0000 Subject: [PATCH 137/169] Add compile absolute curdir-suffix-double regressions Co-authored-by: Brian Searls --- .../daglang-cli/tests/compile_commands.rs | 113 ++++++++++++++++++ 1 file changed, 113 insertions(+) diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index 437da5f3edc..c5e589d96e5 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -3248,6 +3248,54 @@ fn compile_command_absolute_curdir_suffix_root_matches_canonical_absolute_output std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_absolute_curdir_suffix_double_separator_root_matches_canonical_absolute_output() { + let root = unique_temp_dir("compile_absolute_curdir_suffix_double_root"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + write_minimal_directory_compile_fixture(&root); + let canonical_root = root.join("dsl"); + let absolute_curdir_suffix_double_separator_root = + PathBuf::from(format!("{}//.", canonical_root.display())); + + let curdir_suffix_double_separator = Command::new(daglang_bin()) + .arg("compile") + .arg(&absolute_curdir_suffix_double_separator_root) + .current_dir(&root) + .output() + .expect("failed to run curdir-suffix-double-separator absolute root compile"); + assert!( + curdir_suffix_double_separator.status.success(), + "curdir-suffix-double-separator absolute root compile should succeed: {}", + String::from_utf8_lossy(&curdir_suffix_double_separator.stderr) + ); + + let canonical = Command::new(daglang_bin()) + .arg("compile") + .arg(&canonical_root) + .current_dir(&root) + .output() + .expect("failed to run canonical absolute root compile"); + assert!( + canonical.status.success(), + "canonical absolute root compile should succeed: {}", + String::from_utf8_lossy(&canonical.stderr) + ); + + assert_eq!( + curdir_suffix_double_separator.stdout, canonical.stdout, + "curdir-suffix-double-separator and canonical absolute-root compile stdout should match" + ); + assert_eq!( + curdir_suffix_double_separator.stderr, canonical.stderr, + "curdir-suffix-double-separator and canonical absolute-root compile stderr should match" + ); + assert_no_stage_failures(&String::from_utf8_lossy( + &curdir_suffix_double_separator.stderr + )); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_absolute_parent_curdir_segment_root_matches_canonical_absolute_output() { let root = unique_temp_dir("compile_absolute_parent_curdir_segment_root"); @@ -3851,6 +3899,55 @@ fn compile_command_absolute_curdir_suffix_single_file_target_matches_canonical_o std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_absolute_curdir_suffix_double_separator_single_file_target_matches_canonical_output( +) { + let root = unique_temp_dir("compile_absolute_curdir_suffix_double_single_file"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + write_minimal_directory_compile_fixture(&root); + let canonical_target = root.join("dsl/sample/main.dag"); + let absolute_curdir_suffix_double_separator_target = + PathBuf::from(format!("{}//.", canonical_target.display())); + + let curdir_suffix_double_separator = Command::new(daglang_bin()) + .arg("compile") + .arg(&absolute_curdir_suffix_double_separator_target) + .current_dir(&root) + .output() + .expect("failed to run curdir-suffix-double-separator absolute single-file compile"); + assert!( + curdir_suffix_double_separator.status.success(), + "curdir-suffix-double-separator absolute single-file compile should succeed: {}", + String::from_utf8_lossy(&curdir_suffix_double_separator.stderr) + ); + + let canonical = Command::new(daglang_bin()) + .arg("compile") + .arg(&canonical_target) + .current_dir(&root) + .output() + .expect("failed to run canonical absolute single-file compile"); + assert!( + canonical.status.success(), + "canonical absolute single-file compile should succeed: {}", + String::from_utf8_lossy(&canonical.stderr) + ); + + assert_eq!( + curdir_suffix_double_separator.stdout, canonical.stdout, + "curdir-suffix-double-separator and canonical absolute single-file compile stdout should match" + ); + assert_eq!( + curdir_suffix_double_separator.stderr, canonical.stderr, + "curdir-suffix-double-separator and canonical absolute single-file compile stderr should match" + ); + assert_no_stage_failures(&String::from_utf8_lossy( + &curdir_suffix_double_separator.stderr + )); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_absolute_parent_curdir_segment_single_file_target_matches_canonical_output() { let root = unique_temp_dir("compile_absolute_parent_curdir_segment_single_file"); @@ -4307,6 +4404,10 @@ fn compile_command_absolute_missing_root_variants_match_canonical_output() { PathBuf::from(format!("{}//./", canonical_missing_root.display())), ), ("curdir_suffix", root.join("missing_root/.")), + ( + "curdir_suffix_double_separator", + PathBuf::from(format!("{}//.", canonical_missing_root.display())), + ), ("parent", root.join("anchor/../missing_root")), ( "parent_curdir_segment", @@ -4405,6 +4506,10 @@ fn compile_command_absolute_non_directory_root_variants_match_canonical_output() PathBuf::from(format!("{}//./", canonical_non_directory_root.display())), ), ("curdir_suffix", root.join("input.txt/.")), + ( + "curdir_suffix_double_separator", + PathBuf::from(format!("{}//.", canonical_non_directory_root.display())), + ), ("parent", root.join("anchor/../input.txt")), ( "parent_curdir_segment", @@ -4501,6 +4606,10 @@ fn compile_command_absolute_missing_single_file_variants_match_canonical_output( PathBuf::from(format!("{}//./", canonical_missing_single_file.display())), ), ("curdir_suffix", root.join("dsl/sample/missing.dag/.")), + ( + "curdir_suffix_double_separator", + PathBuf::from(format!("{}//.", canonical_missing_single_file.display())), + ), ("parent", root.join("dsl/sample/../sample/missing.dag")), ( "parent_curdir_segment", @@ -4601,6 +4710,10 @@ fn compile_command_absolute_invalid_single_file_variants_match_canonical_output( PathBuf::from(format!("{}//./", canonical_invalid_single_file.display())), ), ("curdir_suffix", root.join("dsl/sample/invalid.dag/.")), + ( + "curdir_suffix_double_separator", + PathBuf::from(format!("{}//.", canonical_invalid_single_file.display())), + ), ("parent", root.join("anchor/../dsl/sample/invalid.dag")), ( "parent_curdir_segment", From e4da8318f54891afce3b8689d49105a6ff53ead5 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 13:24:14 +0000 Subject: [PATCH 138/169] Add compile absolute curdir-segment-double-suffix regressions Co-authored-by: Brian Searls --- .../daglang-cli/tests/compile_commands.rs | 116 ++++++++++++++++++ 1 file changed, 116 insertions(+) diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index c5e589d96e5..df7fee31070 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -3296,6 +3296,55 @@ fn compile_command_absolute_curdir_suffix_double_separator_root_matches_canonica std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_absolute_curdir_segment_double_separator_suffix_root_matches_canonical_absolute_output( +) { + let root = unique_temp_dir("compile_absolute_curdir_segment_double_suffix_root"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + write_minimal_directory_compile_fixture(&root); + let canonical_root = root.join("dsl"); + let absolute_curdir_segment_double_separator_suffix_root = + PathBuf::from(format!("{}//./.", canonical_root.display())); + + let curdir_segment_double_separator_suffix = Command::new(daglang_bin()) + .arg("compile") + .arg(&absolute_curdir_segment_double_separator_suffix_root) + .current_dir(&root) + .output() + .expect("failed to run curdir-segment-double-separator-suffix absolute root compile"); + assert!( + curdir_segment_double_separator_suffix.status.success(), + "curdir-segment-double-separator-suffix absolute root compile should succeed: {}", + String::from_utf8_lossy(&curdir_segment_double_separator_suffix.stderr) + ); + + let canonical = Command::new(daglang_bin()) + .arg("compile") + .arg(&canonical_root) + .current_dir(&root) + .output() + .expect("failed to run canonical absolute root compile"); + assert!( + canonical.status.success(), + "canonical absolute root compile should succeed: {}", + String::from_utf8_lossy(&canonical.stderr) + ); + + assert_eq!( + curdir_segment_double_separator_suffix.stdout, canonical.stdout, + "curdir-segment-double-separator-suffix and canonical absolute-root compile stdout should match" + ); + assert_eq!( + curdir_segment_double_separator_suffix.stderr, canonical.stderr, + "curdir-segment-double-separator-suffix and canonical absolute-root compile stderr should match" + ); + assert_no_stage_failures(&String::from_utf8_lossy( + &curdir_segment_double_separator_suffix.stderr + )); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_absolute_parent_curdir_segment_root_matches_canonical_absolute_output() { let root = unique_temp_dir("compile_absolute_parent_curdir_segment_root"); @@ -3948,6 +3997,57 @@ fn compile_command_absolute_curdir_suffix_double_separator_single_file_target_ma std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_absolute_curdir_segment_double_separator_suffix_single_file_target_matches_canonical_output( +) { + let root = unique_temp_dir("compile_absolute_curdir_segment_double_suffix_single_file"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + write_minimal_directory_compile_fixture(&root); + let canonical_target = root.join("dsl/sample/main.dag"); + let absolute_curdir_segment_double_separator_suffix_target = + PathBuf::from(format!("{}//./.", canonical_target.display())); + + let curdir_segment_double_separator_suffix = Command::new(daglang_bin()) + .arg("compile") + .arg(&absolute_curdir_segment_double_separator_suffix_target) + .current_dir(&root) + .output() + .expect( + "failed to run curdir-segment-double-separator-suffix absolute single-file compile", + ); + assert!( + curdir_segment_double_separator_suffix.status.success(), + "curdir-segment-double-separator-suffix absolute single-file compile should succeed: {}", + String::from_utf8_lossy(&curdir_segment_double_separator_suffix.stderr) + ); + + let canonical = Command::new(daglang_bin()) + .arg("compile") + .arg(&canonical_target) + .current_dir(&root) + .output() + .expect("failed to run canonical absolute single-file compile"); + assert!( + canonical.status.success(), + "canonical absolute single-file compile should succeed: {}", + String::from_utf8_lossy(&canonical.stderr) + ); + + assert_eq!( + curdir_segment_double_separator_suffix.stdout, canonical.stdout, + "curdir-segment-double-separator-suffix and canonical absolute single-file compile stdout should match" + ); + assert_eq!( + curdir_segment_double_separator_suffix.stderr, canonical.stderr, + "curdir-segment-double-separator-suffix and canonical absolute single-file compile stderr should match" + ); + assert_no_stage_failures(&String::from_utf8_lossy( + &curdir_segment_double_separator_suffix.stderr + )); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_absolute_parent_curdir_segment_single_file_target_matches_canonical_output() { let root = unique_temp_dir("compile_absolute_parent_curdir_segment_single_file"); @@ -4408,6 +4508,10 @@ fn compile_command_absolute_missing_root_variants_match_canonical_output() { "curdir_suffix_double_separator", PathBuf::from(format!("{}//.", canonical_missing_root.display())), ), + ( + "curdir_segment_double_separator_suffix", + PathBuf::from(format!("{}//./.", canonical_missing_root.display())), + ), ("parent", root.join("anchor/../missing_root")), ( "parent_curdir_segment", @@ -4510,6 +4614,10 @@ fn compile_command_absolute_non_directory_root_variants_match_canonical_output() "curdir_suffix_double_separator", PathBuf::from(format!("{}//.", canonical_non_directory_root.display())), ), + ( + "curdir_segment_double_separator_suffix", + PathBuf::from(format!("{}//./.", canonical_non_directory_root.display())), + ), ("parent", root.join("anchor/../input.txt")), ( "parent_curdir_segment", @@ -4610,6 +4718,10 @@ fn compile_command_absolute_missing_single_file_variants_match_canonical_output( "curdir_suffix_double_separator", PathBuf::from(format!("{}//.", canonical_missing_single_file.display())), ), + ( + "curdir_segment_double_separator_suffix", + PathBuf::from(format!("{}//./.", canonical_missing_single_file.display())), + ), ("parent", root.join("dsl/sample/../sample/missing.dag")), ( "parent_curdir_segment", @@ -4714,6 +4826,10 @@ fn compile_command_absolute_invalid_single_file_variants_match_canonical_output( "curdir_suffix_double_separator", PathBuf::from(format!("{}//.", canonical_invalid_single_file.display())), ), + ( + "curdir_segment_double_separator_suffix", + PathBuf::from(format!("{}//./.", canonical_invalid_single_file.display())), + ), ("parent", root.join("anchor/../dsl/sample/invalid.dag")), ( "parent_curdir_segment", From 8cbf3a59e3fec487716911a2b138c97cacea6c85 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 13:28:44 +0000 Subject: [PATCH 139/169] Add compile symlink and curdir double-suffix regressions Co-authored-by: Brian Searls --- .../daglang-cli/tests/compile_commands.rs | 189 ++++++++++++++++++ 1 file changed, 189 insertions(+) diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index df7fee31070..569d81a7ad7 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -95,6 +95,195 @@ fn compile_command_emits_summary_for_single_file() { assert!(stdout.contains("target/generated/rust/main.rs")); } +#[cfg(unix)] +#[test] +fn compile_command_accepts_symlink_single_file_target() { + use std::os::unix::fs::symlink; + + let root = unique_temp_dir("compile_symlink_single_file"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + let real = root.join("real.dag"); + let link = root.join("link.dag"); + std::fs::write(&real, "module sample.real\nfn ok() -> Unit {}") + .expect("failed to write real source"); + symlink(&real, &link).expect("failed to create symlinked target"); + + let real_output = Command::new(daglang_bin()) + .arg("compile") + .arg(&real) + .current_dir(&root) + .output() + .expect("failed to run daglang compile on real target"); + assert!( + real_output.status.success(), + "compile should succeed for real single-file target: {}", + String::from_utf8_lossy(&real_output.stderr) + ); + + let link_output = Command::new(daglang_bin()) + .arg("compile") + .arg(&link) + .current_dir(&root) + .output() + .expect("failed to run daglang compile on symlinked target"); + assert!( + link_output.status.success(), + "compile should succeed for symlinked single-file target: {}", + String::from_utf8_lossy(&link_output.stderr) + ); + + assert_eq!( + real_output.stdout, link_output.stdout, + "real and symlink single-file compile stdout should match" + ); + assert_eq!( + real_output.stderr, link_output.stderr, + "real and symlink single-file compile stderr should match" + ); + assert_no_stage_failures(&String::from_utf8_lossy(&link_output.stderr)); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + +#[cfg(unix)] +#[test] +fn compile_command_accepts_symlink_root_directory() { + use std::os::unix::fs::symlink; + + let root = unique_temp_dir("compile_symlink_root_directory"); + let real_root = root.join("real_root"); + std::fs::create_dir_all(real_root.join("sample")).expect("failed to create real root fixture"); + std::fs::write( + real_root.join("sample/main.dag"), + "module sample.main\nfn run() -> Unit { }", + ) + .expect("failed to write real root dag"); + let link_root = root.join("link_root"); + symlink(&real_root, &link_root).expect("failed to create symlinked root"); + + let real_output = Command::new(daglang_bin()) + .arg("compile") + .arg(&real_root) + .current_dir(&root) + .output() + .expect("failed to run daglang compile on real root"); + assert!( + real_output.status.success(), + "compile should succeed for real root directory: {}", + String::from_utf8_lossy(&real_output.stderr) + ); + + let link_output = Command::new(daglang_bin()) + .arg("compile") + .arg(&link_root) + .current_dir(&root) + .output() + .expect("failed to run daglang compile on symlinked root"); + assert!( + link_output.status.success(), + "compile should succeed for symlinked root directory: {}", + String::from_utf8_lossy(&link_output.stderr) + ); + + assert_eq!( + real_output.stdout, link_output.stdout, + "real and symlink root compile stdout should match" + ); + assert_eq!( + real_output.stderr, link_output.stderr, + "real and symlink root compile stderr should match" + ); + assert_no_stage_failures(&String::from_utf8_lossy(&link_output.stderr)); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + +#[cfg(unix)] +#[test] +fn compile_command_dangling_symlink_single_file_target_exits_nonzero() { + use std::os::unix::fs::symlink; + + let root = unique_temp_dir("compile_dangling_symlink_single_file"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + let dangling_target = root.join("missing.dag"); + let dangling_link = root.join("broken.dag"); + symlink(&dangling_target, &dangling_link).expect("failed to create dangling symlink target"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&dangling_link) + .current_dir(&root) + .output() + .expect("failed to run daglang compile on dangling symlink target"); + assert!( + !output.status.success(), + "compile should fail for dangling symlink single-file target" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("failed to read")); + assert!( + stderr.contains("broken.dag"), + "dangling symlink compile failure should include offending path: {stderr}" + ); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + +#[cfg(unix)] +#[test] +fn compile_command_relative_and_absolute_dangling_symlink_targets_are_equivalent() { + use std::os::unix::fs::symlink; + + let root = unique_temp_dir("compile_relative_absolute_dangling_symlink_target"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + let dangling_target = root.join("missing.dag"); + let dangling_link = root.join("broken.dag"); + symlink(&dangling_target, &dangling_link).expect("failed to create dangling symlink target"); + + let relative = Command::new(daglang_bin()) + .arg("compile") + .arg("broken.dag") + .current_dir(&root) + .output() + .expect("failed to run relative dangling-target daglang compile"); + assert!( + !relative.status.success(), + "relative dangling-target compile should fail" + ); + + let absolute = Command::new(daglang_bin()) + .arg("compile") + .arg(&dangling_link) + .current_dir(&root) + .output() + .expect("failed to run absolute dangling-target daglang compile"); + assert!( + !absolute.status.success(), + "absolute dangling-target compile should fail" + ); + + assert_eq!( + relative.stdout, absolute.stdout, + "relative and absolute dangling-target compile stdout should match" + ); + assert_eq!( + relative.stderr, absolute.stderr, + "relative and absolute dangling-target compile stderr should match" + ); + let stderr = String::from_utf8_lossy(&relative.stderr); + assert!( + stderr.contains(&format!( + "failed to read {}", + dangling_link.display() + )), + "dangling-target compile diagnostics should include normalized absolute path: {stderr}" + ); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_relative_and_absolute_missing_roots_are_equivalent() { let root = unique_temp_dir("compile_relative_absolute_missing_root"); From 9e9970f8f129e3cb367f114c5e5740092fa90ade Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 13:33:26 +0000 Subject: [PATCH 140/169] Add compile absolute curdir-segment-suffix regressions Co-authored-by: Brian Searls --- .../daglang-cli/tests/compile_commands.rs | 106 ++++++++++++++++++ 1 file changed, 106 insertions(+) diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index 569d81a7ad7..a4dd8b327a9 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -3534,6 +3534,51 @@ fn compile_command_absolute_curdir_segment_double_separator_suffix_root_matches_ std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_absolute_curdir_segment_suffix_root_matches_canonical_absolute_output() { + let root = unique_temp_dir("compile_absolute_curdir_segment_suffix_root"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + write_minimal_directory_compile_fixture(&root); + let canonical_root = root.join("dsl"); + let absolute_curdir_segment_suffix_root = root.join("dsl/./."); + + let curdir_segment_suffix = Command::new(daglang_bin()) + .arg("compile") + .arg(&absolute_curdir_segment_suffix_root) + .current_dir(&root) + .output() + .expect("failed to run curdir-segment-suffix absolute root compile"); + assert!( + curdir_segment_suffix.status.success(), + "curdir-segment-suffix absolute root compile should succeed: {}", + String::from_utf8_lossy(&curdir_segment_suffix.stderr) + ); + + let canonical = Command::new(daglang_bin()) + .arg("compile") + .arg(&canonical_root) + .current_dir(&root) + .output() + .expect("failed to run canonical absolute root compile"); + assert!( + canonical.status.success(), + "canonical absolute root compile should succeed: {}", + String::from_utf8_lossy(&canonical.stderr) + ); + + assert_eq!( + curdir_segment_suffix.stdout, canonical.stdout, + "curdir-segment-suffix and canonical absolute-root compile stdout should match" + ); + assert_eq!( + curdir_segment_suffix.stderr, canonical.stderr, + "curdir-segment-suffix and canonical absolute-root compile stderr should match" + ); + assert_no_stage_failures(&String::from_utf8_lossy(&curdir_segment_suffix.stderr)); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_absolute_parent_curdir_segment_root_matches_canonical_absolute_output() { let root = unique_temp_dir("compile_absolute_parent_curdir_segment_root"); @@ -4237,6 +4282,51 @@ fn compile_command_absolute_curdir_segment_double_separator_suffix_single_file_t std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_absolute_curdir_segment_suffix_single_file_target_matches_canonical_output() { + let root = unique_temp_dir("compile_absolute_curdir_segment_suffix_single_file"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + write_minimal_directory_compile_fixture(&root); + let canonical_target = root.join("dsl/sample/main.dag"); + let absolute_curdir_segment_suffix_target = root.join("dsl/sample/main.dag/./."); + + let curdir_segment_suffix = Command::new(daglang_bin()) + .arg("compile") + .arg(&absolute_curdir_segment_suffix_target) + .current_dir(&root) + .output() + .expect("failed to run curdir-segment-suffix absolute single-file compile"); + assert!( + curdir_segment_suffix.status.success(), + "curdir-segment-suffix absolute single-file compile should succeed: {}", + String::from_utf8_lossy(&curdir_segment_suffix.stderr) + ); + + let canonical = Command::new(daglang_bin()) + .arg("compile") + .arg(&canonical_target) + .current_dir(&root) + .output() + .expect("failed to run canonical absolute single-file compile"); + assert!( + canonical.status.success(), + "canonical absolute single-file compile should succeed: {}", + String::from_utf8_lossy(&canonical.stderr) + ); + + assert_eq!( + curdir_segment_suffix.stdout, canonical.stdout, + "curdir-segment-suffix and canonical absolute single-file compile stdout should match" + ); + assert_eq!( + curdir_segment_suffix.stderr, canonical.stderr, + "curdir-segment-suffix and canonical absolute single-file compile stderr should match" + ); + assert_no_stage_failures(&String::from_utf8_lossy(&curdir_segment_suffix.stderr)); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_absolute_parent_curdir_segment_single_file_target_matches_canonical_output() { let root = unique_temp_dir("compile_absolute_parent_curdir_segment_single_file"); @@ -4701,6 +4791,10 @@ fn compile_command_absolute_missing_root_variants_match_canonical_output() { "curdir_segment_double_separator_suffix", PathBuf::from(format!("{}//./.", canonical_missing_root.display())), ), + ( + "curdir_segment_suffix", + PathBuf::from(format!("{}/./.", canonical_missing_root.display())), + ), ("parent", root.join("anchor/../missing_root")), ( "parent_curdir_segment", @@ -4807,6 +4901,10 @@ fn compile_command_absolute_non_directory_root_variants_match_canonical_output() "curdir_segment_double_separator_suffix", PathBuf::from(format!("{}//./.", canonical_non_directory_root.display())), ), + ( + "curdir_segment_suffix", + PathBuf::from(format!("{}/./.", canonical_non_directory_root.display())), + ), ("parent", root.join("anchor/../input.txt")), ( "parent_curdir_segment", @@ -4911,6 +5009,10 @@ fn compile_command_absolute_missing_single_file_variants_match_canonical_output( "curdir_segment_double_separator_suffix", PathBuf::from(format!("{}//./.", canonical_missing_single_file.display())), ), + ( + "curdir_segment_suffix", + PathBuf::from(format!("{}/./.", canonical_missing_single_file.display())), + ), ("parent", root.join("dsl/sample/../sample/missing.dag")), ( "parent_curdir_segment", @@ -5019,6 +5121,10 @@ fn compile_command_absolute_invalid_single_file_variants_match_canonical_output( "curdir_segment_double_separator_suffix", PathBuf::from(format!("{}//./.", canonical_invalid_single_file.display())), ), + ( + "curdir_segment_suffix", + PathBuf::from(format!("{}/./.", canonical_invalid_single_file.display())), + ), ("parent", root.join("anchor/../dsl/sample/invalid.dag")), ( "parent_curdir_segment", From 41e4642de01e7a57174c988cc2292b245a6e1ddc Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 13:37:29 +0000 Subject: [PATCH 141/169] Add compile absolute parent-curdir trailing regressions Co-authored-by: Brian Searls --- .../daglang-cli/tests/compile_commands.rs | 242 ++++++++++++++++++ 1 file changed, 242 insertions(+) diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index a4dd8b327a9..4233b8e6a9f 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -3624,6 +3624,55 @@ fn compile_command_absolute_parent_curdir_segment_root_matches_canonical_absolut std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_absolute_parent_curdir_segment_trailing_slash_root_matches_canonical_absolute_output( +) { + let root = unique_temp_dir("compile_absolute_parent_curdir_segment_trailing_root"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + write_minimal_directory_compile_fixture(&root); + let absolute_parent_curdir_segment_trailing_root = + PathBuf::from(format!("{}/anchor/.././dsl/", root.display())); + let canonical_root = root.join("dsl"); + + let parent_curdir_segment_trailing = Command::new(daglang_bin()) + .arg("compile") + .arg(&absolute_parent_curdir_segment_trailing_root) + .current_dir(&root) + .output() + .expect("failed to run parent-curdir-segment-trailing absolute root compile"); + assert!( + parent_curdir_segment_trailing.status.success(), + "parent-curdir-segment-trailing absolute root compile should succeed: {}", + String::from_utf8_lossy(&parent_curdir_segment_trailing.stderr) + ); + + let canonical = Command::new(daglang_bin()) + .arg("compile") + .arg(&canonical_root) + .current_dir(&root) + .output() + .expect("failed to run canonical absolute root compile"); + assert!( + canonical.status.success(), + "canonical absolute root compile should succeed: {}", + String::from_utf8_lossy(&canonical.stderr) + ); + + assert_eq!( + parent_curdir_segment_trailing.stdout, canonical.stdout, + "parent-curdir-segment-trailing and canonical absolute-root compile stdout should match" + ); + assert_eq!( + parent_curdir_segment_trailing.stderr, canonical.stderr, + "parent-curdir-segment-trailing and canonical absolute-root compile stderr should match" + ); + assert_no_stage_failures(&String::from_utf8_lossy( + &parent_curdir_segment_trailing.stderr + )); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_absolute_parent_curdir_segment_double_separator_root_matches_canonical_absolute_output( ) { @@ -3673,6 +3722,56 @@ fn compile_command_absolute_parent_curdir_segment_double_separator_root_matches_ std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_absolute_parent_curdir_segment_double_separator_trailing_slash_root_matches_canonical_absolute_output( +) { + let root = + unique_temp_dir("compile_absolute_parent_curdir_segment_double_trailing_slash_root"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + write_minimal_directory_compile_fixture(&root); + let absolute_parent_curdir_segment_double_separator_trailing_root = + PathBuf::from(format!("{}/anchor//.././dsl/", root.display())); + let canonical_root = root.join("dsl"); + + let parent_curdir_segment_double_separator_trailing = Command::new(daglang_bin()) + .arg("compile") + .arg(&absolute_parent_curdir_segment_double_separator_trailing_root) + .current_dir(&root) + .output() + .expect("failed to run parent-curdir-segment-double-separator-trailing absolute root compile"); + assert!( + parent_curdir_segment_double_separator_trailing.status.success(), + "parent-curdir-segment-double-separator-trailing absolute root compile should succeed: {}", + String::from_utf8_lossy(&parent_curdir_segment_double_separator_trailing.stderr) + ); + + let canonical = Command::new(daglang_bin()) + .arg("compile") + .arg(&canonical_root) + .current_dir(&root) + .output() + .expect("failed to run canonical absolute root compile"); + assert!( + canonical.status.success(), + "canonical absolute root compile should succeed: {}", + String::from_utf8_lossy(&canonical.stderr) + ); + + assert_eq!( + parent_curdir_segment_double_separator_trailing.stdout, canonical.stdout, + "parent-curdir-segment-double-separator-trailing and canonical absolute-root compile stdout should match" + ); + assert_eq!( + parent_curdir_segment_double_separator_trailing.stderr, canonical.stderr, + "parent-curdir-segment-double-separator-trailing and canonical absolute-root compile stderr should match" + ); + assert_no_stage_failures(&String::from_utf8_lossy( + &parent_curdir_segment_double_separator_trailing.stderr + )); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_absolute_parent_segment_root_matches_canonical_absolute_output() { let root = unique_temp_dir("compile_absolute_parent_segment_root"); @@ -4372,6 +4471,55 @@ fn compile_command_absolute_parent_curdir_segment_single_file_target_matches_can std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_absolute_parent_curdir_segment_trailing_slash_single_file_target_matches_canonical_output( +) { + let root = unique_temp_dir("compile_absolute_parent_curdir_segment_trailing_single_file"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + write_minimal_directory_compile_fixture(&root); + let canonical_target = root.join("dsl/sample/main.dag"); + let absolute_parent_curdir_segment_trailing_target = + PathBuf::from(format!("{}/anchor/.././dsl/sample/main.dag/", root.display())); + + let parent_curdir_segment_trailing = Command::new(daglang_bin()) + .arg("compile") + .arg(&absolute_parent_curdir_segment_trailing_target) + .current_dir(&root) + .output() + .expect("failed to run parent-curdir-segment-trailing absolute single-file compile"); + assert!( + parent_curdir_segment_trailing.status.success(), + "parent-curdir-segment-trailing absolute single-file compile should succeed: {}", + String::from_utf8_lossy(&parent_curdir_segment_trailing.stderr) + ); + + let canonical = Command::new(daglang_bin()) + .arg("compile") + .arg(&canonical_target) + .current_dir(&root) + .output() + .expect("failed to run canonical absolute single-file compile"); + assert!( + canonical.status.success(), + "canonical absolute single-file compile should succeed: {}", + String::from_utf8_lossy(&canonical.stderr) + ); + + assert_eq!( + parent_curdir_segment_trailing.stdout, canonical.stdout, + "parent-curdir-segment-trailing and canonical absolute single-file compile stdout should match" + ); + assert_eq!( + parent_curdir_segment_trailing.stderr, canonical.stderr, + "parent-curdir-segment-trailing and canonical absolute single-file compile stderr should match" + ); + assert_no_stage_failures(&String::from_utf8_lossy( + &parent_curdir_segment_trailing.stderr + )); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_absolute_parent_curdir_segment_double_separator_single_file_target_matches_canonical_output( ) { @@ -4421,6 +4569,56 @@ fn compile_command_absolute_parent_curdir_segment_double_separator_single_file_t std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_absolute_parent_curdir_segment_double_separator_trailing_slash_single_file_target_matches_canonical_output( +) { + let root = + unique_temp_dir("compile_absolute_parent_curdir_segment_double_trailing_single_file"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + write_minimal_directory_compile_fixture(&root); + let canonical_target = root.join("dsl/sample/main.dag"); + let absolute_parent_curdir_segment_double_separator_trailing_target = + PathBuf::from(format!("{}/anchor//.././dsl/sample/main.dag/", root.display())); + + let parent_curdir_segment_double_separator_trailing = Command::new(daglang_bin()) + .arg("compile") + .arg(&absolute_parent_curdir_segment_double_separator_trailing_target) + .current_dir(&root) + .output() + .expect("failed to run parent-curdir-segment-double-separator-trailing absolute single-file compile"); + assert!( + parent_curdir_segment_double_separator_trailing.status.success(), + "parent-curdir-segment-double-separator-trailing absolute single-file compile should succeed: {}", + String::from_utf8_lossy(&parent_curdir_segment_double_separator_trailing.stderr) + ); + + let canonical = Command::new(daglang_bin()) + .arg("compile") + .arg(&canonical_target) + .current_dir(&root) + .output() + .expect("failed to run canonical absolute single-file compile"); + assert!( + canonical.status.success(), + "canonical absolute single-file compile should succeed: {}", + String::from_utf8_lossy(&canonical.stderr) + ); + + assert_eq!( + parent_curdir_segment_double_separator_trailing.stdout, canonical.stdout, + "parent-curdir-segment-double-separator-trailing and canonical absolute single-file compile stdout should match" + ); + assert_eq!( + parent_curdir_segment_double_separator_trailing.stderr, canonical.stderr, + "parent-curdir-segment-double-separator-trailing and canonical absolute single-file compile stderr should match" + ); + assert_no_stage_failures(&String::from_utf8_lossy( + &parent_curdir_segment_double_separator_trailing.stderr + )); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_absolute_parent_segment_single_file_target_matches_canonical_output() { let root = unique_temp_dir("compile_absolute_parent_segment_single_file"); @@ -4800,10 +4998,18 @@ fn compile_command_absolute_missing_root_variants_match_canonical_output() { "parent_curdir_segment", root.join("anchor/.././missing_root"), ), + ( + "parent_curdir_segment_trailing", + PathBuf::from(format!("{}/anchor/.././missing_root/", root.display())), + ), ( "parent_curdir_segment_double_separator", PathBuf::from(format!("{}/anchor/.././missing_root//", root.display())), ), + ( + "parent_curdir_segment_double_separator_trailing", + PathBuf::from(format!("{}/anchor//.././missing_root/", root.display())), + ), ( "parent_double_separator", PathBuf::from(format!("{}/anchor/..//missing_root", root.display())), @@ -4910,10 +5116,18 @@ fn compile_command_absolute_non_directory_root_variants_match_canonical_output() "parent_curdir_segment", root.join("anchor/.././input.txt"), ), + ( + "parent_curdir_segment_trailing", + PathBuf::from(format!("{}/anchor/.././input.txt/", root.display())), + ), ( "parent_curdir_segment_double_separator", PathBuf::from(format!("{}/anchor/.././input.txt//", root.display())), ), + ( + "parent_curdir_segment_double_separator_trailing", + PathBuf::from(format!("{}/anchor//.././input.txt/", root.display())), + ), ( "parent_double_separator", PathBuf::from(format!("{}/anchor/..//input.txt", root.display())), @@ -5018,6 +5232,13 @@ fn compile_command_absolute_missing_single_file_variants_match_canonical_output( "parent_curdir_segment", root.join("dsl/sample/.././sample/missing.dag"), ), + ( + "parent_curdir_segment_trailing", + PathBuf::from(format!( + "{}/dsl/sample/.././sample/missing.dag/", + root.display() + )), + ), ( "parent_curdir_segment_double_separator", PathBuf::from(format!( @@ -5025,6 +5246,13 @@ fn compile_command_absolute_missing_single_file_variants_match_canonical_output( root.display() )), ), + ( + "parent_curdir_segment_double_separator_trailing", + PathBuf::from(format!( + "{}/dsl/sample//.././sample/missing.dag/", + root.display() + )), + ), ( "parent_double_separator", PathBuf::from(format!("{}/dsl/sample/..//sample/missing.dag", root.display())), @@ -5130,6 +5358,13 @@ fn compile_command_absolute_invalid_single_file_variants_match_canonical_output( "parent_curdir_segment", root.join("anchor/.././dsl/sample/invalid.dag"), ), + ( + "parent_curdir_segment_trailing", + PathBuf::from(format!( + "{}/anchor/.././dsl/sample/invalid.dag/", + root.display() + )), + ), ( "parent_curdir_segment_double_separator", PathBuf::from(format!( @@ -5137,6 +5372,13 @@ fn compile_command_absolute_invalid_single_file_variants_match_canonical_output( root.display() )), ), + ( + "parent_curdir_segment_double_separator_trailing", + PathBuf::from(format!( + "{}/anchor//.././dsl/sample/invalid.dag/", + root.display() + )), + ), ( "parent_double_separator", PathBuf::from(format!( From c1aad6e1343510f2006f0822881134ee04cb40db Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 13:42:04 +0000 Subject: [PATCH 142/169] Add compile relative curdir suffix regressions Co-authored-by: Brian Searls --- .../daglang-cli/tests/compile_commands.rs | 450 ++++++++++++++++++ 1 file changed, 450 insertions(+) diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index 4233b8e6a9f..9ddc662eb80 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -791,6 +791,92 @@ fn compile_command_curdir_segment_missing_root_matches_plain_relative_output() { std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_curdir_suffix_missing_root_matches_plain_relative_output() { + let root = unique_temp_dir("compile_curdir_suffix_missing_root"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + let missing = root.join("missing_root"); + + let plain = Command::new(daglang_bin()) + .arg("compile") + .arg("missing_root") + .current_dir(&root) + .output() + .expect("failed to run plain missing-root compile"); + assert!(!plain.status.success(), "plain missing-root compile should fail"); + + let curdir_suffix = Command::new(daglang_bin()) + .arg("compile") + .arg("./missing_root/.") + .current_dir(&root) + .output() + .expect("failed to run curdir-suffix missing-root compile"); + assert!( + !curdir_suffix.status.success(), + "curdir-suffix missing-root compile should fail" + ); + + assert_eq!( + plain.stdout, curdir_suffix.stdout, + "plain and curdir-suffix missing-root compile stdout should match" + ); + assert_eq!( + plain.stderr, curdir_suffix.stderr, + "plain and curdir-suffix missing-root compile stderr should match" + ); + let stderr = String::from_utf8_lossy(&plain.stderr); + assert!( + stderr.contains(&missing.display().to_string()), + "curdir-suffix missing-root diagnostic should normalize to absolute path: {stderr}" + ); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + +#[test] +fn compile_command_curdir_segment_suffix_missing_root_matches_plain_relative_output() { + let root = unique_temp_dir("compile_curdir_segment_suffix_missing_root"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + let missing = root.join("missing_root"); + + let plain = Command::new(daglang_bin()) + .arg("compile") + .arg("missing_root") + .current_dir(&root) + .output() + .expect("failed to run plain missing-root compile"); + assert!(!plain.status.success(), "plain missing-root compile should fail"); + + let curdir_segment_suffix = Command::new(daglang_bin()) + .arg("compile") + .arg("./missing_root/./.") + .current_dir(&root) + .output() + .expect("failed to run curdir-segment-suffix missing-root compile"); + assert!( + !curdir_segment_suffix.status.success(), + "curdir-segment-suffix missing-root compile should fail" + ); + + assert_eq!( + plain.stdout, curdir_segment_suffix.stdout, + "plain and curdir-segment-suffix missing-root compile stdout should match" + ); + assert_eq!( + plain.stderr, curdir_segment_suffix.stderr, + "plain and curdir-segment-suffix missing-root compile stderr should match" + ); + let stderr = String::from_utf8_lossy(&plain.stderr); + assert!( + stderr.contains(&missing.display().to_string()), + "curdir-segment-suffix missing-root diagnostic should normalize to absolute path: {stderr}" + ); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_curdir_double_separator_missing_root_matches_plain_relative_output() { let root = unique_temp_dir("compile_curdir_double_separator_missing_root"); @@ -1058,6 +1144,98 @@ fn compile_command_curdir_segment_missing_single_file_target_matches_plain_relat std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_curdir_suffix_missing_single_file_target_matches_plain_relative_output() { + let root = unique_temp_dir("compile_curdir_suffix_missing_single_file_target"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + let missing = root.join("missing.dag"); + + let plain = Command::new(daglang_bin()) + .arg("compile") + .arg("missing.dag") + .current_dir(&root) + .output() + .expect("failed to run plain missing single-file compile"); + assert!( + !plain.status.success(), + "plain missing single-file compile should fail" + ); + + let curdir_suffix = Command::new(daglang_bin()) + .arg("compile") + .arg("./missing.dag/.") + .current_dir(&root) + .output() + .expect("failed to run curdir-suffix missing single-file compile"); + assert!( + !curdir_suffix.status.success(), + "curdir-suffix missing single-file compile should fail" + ); + + assert_eq!( + plain.stdout, curdir_suffix.stdout, + "plain and curdir-suffix missing single-file compile stdout should match" + ); + assert_eq!( + plain.stderr, curdir_suffix.stderr, + "plain and curdir-suffix missing single-file compile stderr should match" + ); + let stderr = String::from_utf8_lossy(&plain.stderr); + assert!( + stderr.contains(&format!("failed to read {}", missing.display())), + "curdir-suffix missing single-file diagnostic should normalize to absolute path: {stderr}" + ); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + +#[test] +fn compile_command_curdir_segment_suffix_missing_single_file_target_matches_plain_relative_output() { + let root = unique_temp_dir("compile_curdir_segment_suffix_missing_single_file_target"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + let missing = root.join("missing.dag"); + + let plain = Command::new(daglang_bin()) + .arg("compile") + .arg("missing.dag") + .current_dir(&root) + .output() + .expect("failed to run plain missing single-file compile"); + assert!( + !plain.status.success(), + "plain missing single-file compile should fail" + ); + + let curdir_segment_suffix = Command::new(daglang_bin()) + .arg("compile") + .arg("./missing.dag/./.") + .current_dir(&root) + .output() + .expect("failed to run curdir-segment-suffix missing single-file compile"); + assert!( + !curdir_segment_suffix.status.success(), + "curdir-segment-suffix missing single-file compile should fail" + ); + + assert_eq!( + plain.stdout, curdir_segment_suffix.stdout, + "plain and curdir-segment-suffix missing single-file compile stdout should match" + ); + assert_eq!( + plain.stderr, curdir_segment_suffix.stderr, + "plain and curdir-segment-suffix missing single-file compile stderr should match" + ); + let stderr = String::from_utf8_lossy(&plain.stderr); + assert!( + stderr.contains(&format!("failed to read {}", missing.display())), + "curdir-segment-suffix missing single-file diagnostic should normalize to absolute path: {stderr}" + ); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_relative_and_absolute_invalid_single_file_targets_are_equivalent() { let root = unique_temp_dir("compile_relative_absolute_invalid_single_file_target"); @@ -1202,6 +1380,102 @@ fn compile_command_curdir_segment_invalid_single_file_target_matches_plain_relat std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_curdir_suffix_invalid_single_file_target_matches_plain_relative_output() { + let root = unique_temp_dir("compile_curdir_suffix_invalid_single_file_target"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + let invalid_target = root.join("invalid.dag"); + std::fs::create_dir_all(&invalid_target) + .expect("failed to create invalid single-file target directory"); + + let plain = Command::new(daglang_bin()) + .arg("compile") + .arg("invalid.dag") + .current_dir(&root) + .output() + .expect("failed to run plain invalid single-file compile"); + assert!( + !plain.status.success(), + "plain invalid single-file compile should fail" + ); + + let curdir_suffix = Command::new(daglang_bin()) + .arg("compile") + .arg("./invalid.dag/.") + .current_dir(&root) + .output() + .expect("failed to run curdir-suffix invalid single-file compile"); + assert!( + !curdir_suffix.status.success(), + "curdir-suffix invalid single-file compile should fail" + ); + + assert_eq!( + plain.stdout, curdir_suffix.stdout, + "plain and curdir-suffix invalid single-file compile stdout should match" + ); + assert_eq!( + plain.stderr, curdir_suffix.stderr, + "plain and curdir-suffix invalid single-file compile stderr should match" + ); + let stderr = String::from_utf8_lossy(&plain.stderr); + assert!( + stderr.contains(&format!("failed to read {}", invalid_target.display())), + "curdir-suffix invalid single-file diagnostic should include normalized absolute path: {stderr}" + ); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + +#[test] +fn compile_command_curdir_segment_suffix_invalid_single_file_target_matches_plain_relative_output() { + let root = unique_temp_dir("compile_curdir_segment_suffix_invalid_single_file_target"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + let invalid_target = root.join("invalid.dag"); + std::fs::create_dir_all(&invalid_target) + .expect("failed to create invalid single-file target directory"); + + let plain = Command::new(daglang_bin()) + .arg("compile") + .arg("invalid.dag") + .current_dir(&root) + .output() + .expect("failed to run plain invalid single-file compile"); + assert!( + !plain.status.success(), + "plain invalid single-file compile should fail" + ); + + let curdir_segment_suffix = Command::new(daglang_bin()) + .arg("compile") + .arg("./invalid.dag/./.") + .current_dir(&root) + .output() + .expect("failed to run curdir-segment-suffix invalid single-file compile"); + assert!( + !curdir_segment_suffix.status.success(), + "curdir-segment-suffix invalid single-file compile should fail" + ); + + assert_eq!( + plain.stdout, curdir_segment_suffix.stdout, + "plain and curdir-segment-suffix invalid single-file compile stdout should match" + ); + assert_eq!( + plain.stderr, curdir_segment_suffix.stderr, + "plain and curdir-segment-suffix invalid single-file compile stderr should match" + ); + let stderr = String::from_utf8_lossy(&plain.stderr); + assert!( + stderr.contains(&format!("failed to read {}", invalid_target.display())), + "curdir-segment-suffix invalid single-file diagnostic should include normalized absolute path: {stderr}" + ); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_curdir_double_separator_invalid_single_file_target_matches_plain_relative_output( ) { @@ -2962,6 +3236,96 @@ fn compile_command_curdir_segment_single_file_target_matches_plain_relative_outp std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_curdir_suffix_single_file_target_matches_plain_relative_output() { + let root = unique_temp_dir("compile_curdir_suffix_single_file_target"); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp root"); + let source = root.join("sample/main.dag"); + std::fs::write(&source, "module sample.main\nfn run() -> Unit { }") + .expect("failed to write single-file source"); + + let plain = Command::new(daglang_bin()) + .arg("compile") + .arg("sample/main.dag") + .current_dir(&root) + .output() + .expect("failed to run plain single-file compile"); + assert!( + plain.status.success(), + "plain single-file compile should succeed: {}", + String::from_utf8_lossy(&plain.stderr) + ); + + let curdir_suffix = Command::new(daglang_bin()) + .arg("compile") + .arg("./sample/main.dag/.") + .current_dir(&root) + .output() + .expect("failed to run curdir-suffix single-file compile"); + assert!( + curdir_suffix.status.success(), + "curdir-suffix single-file compile should succeed: {}", + String::from_utf8_lossy(&curdir_suffix.stderr) + ); + + assert_eq!( + plain.stdout, curdir_suffix.stdout, + "plain and curdir-suffix single-file compile stdout should match" + ); + assert_eq!( + plain.stderr, curdir_suffix.stderr, + "plain and curdir-suffix single-file compile stderr should match" + ); + assert_no_stage_failures(&String::from_utf8_lossy(&plain.stderr)); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + +#[test] +fn compile_command_curdir_segment_suffix_single_file_target_matches_plain_relative_output() { + let root = unique_temp_dir("compile_curdir_segment_suffix_single_file_target"); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp root"); + let source = root.join("sample/main.dag"); + std::fs::write(&source, "module sample.main\nfn run() -> Unit { }") + .expect("failed to write single-file source"); + + let plain = Command::new(daglang_bin()) + .arg("compile") + .arg("sample/main.dag") + .current_dir(&root) + .output() + .expect("failed to run plain single-file compile"); + assert!( + plain.status.success(), + "plain single-file compile should succeed: {}", + String::from_utf8_lossy(&plain.stderr) + ); + + let curdir_segment_suffix = Command::new(daglang_bin()) + .arg("compile") + .arg("./sample/main.dag/./.") + .current_dir(&root) + .output() + .expect("failed to run curdir-segment-suffix single-file compile"); + assert!( + curdir_segment_suffix.status.success(), + "curdir-segment-suffix single-file compile should succeed: {}", + String::from_utf8_lossy(&curdir_segment_suffix.stderr) + ); + + assert_eq!( + plain.stdout, curdir_segment_suffix.stdout, + "plain and curdir-segment-suffix single-file compile stdout should match" + ); + assert_eq!( + plain.stderr, curdir_segment_suffix.stderr, + "plain and curdir-segment-suffix single-file compile stderr should match" + ); + assert_no_stage_failures(&String::from_utf8_lossy(&plain.stderr)); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_trailing_slash_non_directory_root_matches_plain_relative_output() { let root = unique_temp_dir("compile_trailing_slash_non_directory_root"); @@ -5483,6 +5847,92 @@ fn compile_command_curdir_segment_root_matches_plain_relative_output() { std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_curdir_suffix_root_matches_plain_relative_output() { + let root = unique_temp_dir("compile_curdir_suffix_root"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + write_minimal_directory_compile_fixture(&root); + + let plain = Command::new(daglang_bin()) + .arg("compile") + .arg("dsl") + .current_dir(&root) + .output() + .expect("failed to run plain-relative root compile"); + assert!( + plain.status.success(), + "plain-relative root compile should succeed: {}", + String::from_utf8_lossy(&plain.stderr) + ); + + let curdir_suffix = Command::new(daglang_bin()) + .arg("compile") + .arg("./dsl/.") + .current_dir(&root) + .output() + .expect("failed to run curdir-suffix root compile"); + assert!( + curdir_suffix.status.success(), + "curdir-suffix root compile should succeed: {}", + String::from_utf8_lossy(&curdir_suffix.stderr) + ); + + assert_eq!( + plain.stdout, curdir_suffix.stdout, + "plain-relative and curdir-suffix root compile stdout should match" + ); + assert_eq!( + plain.stderr, curdir_suffix.stderr, + "plain-relative and curdir-suffix root compile stderr should match" + ); + assert_no_stage_failures(&String::from_utf8_lossy(&plain.stderr)); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + +#[test] +fn compile_command_curdir_segment_suffix_root_matches_plain_relative_output() { + let root = unique_temp_dir("compile_curdir_segment_suffix_root"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + write_minimal_directory_compile_fixture(&root); + + let plain = Command::new(daglang_bin()) + .arg("compile") + .arg("dsl") + .current_dir(&root) + .output() + .expect("failed to run plain-relative root compile"); + assert!( + plain.status.success(), + "plain-relative root compile should succeed: {}", + String::from_utf8_lossy(&plain.stderr) + ); + + let curdir_segment_suffix = Command::new(daglang_bin()) + .arg("compile") + .arg("./dsl/./.") + .current_dir(&root) + .output() + .expect("failed to run curdir-segment-suffix root compile"); + assert!( + curdir_segment_suffix.status.success(), + "curdir-segment-suffix root compile should succeed: {}", + String::from_utf8_lossy(&curdir_segment_suffix.stderr) + ); + + assert_eq!( + plain.stdout, curdir_segment_suffix.stdout, + "plain-relative and curdir-segment-suffix root compile stdout should match" + ); + assert_eq!( + plain.stderr, curdir_segment_suffix.stderr, + "plain-relative and curdir-segment-suffix root compile stderr should match" + ); + assert_no_stage_failures(&String::from_utf8_lossy(&plain.stderr)); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_curdir_double_separator_root_matches_plain_relative_output() { let root = unique_temp_dir("compile_curdir_double_separator_root"); From a7112008615ccee2616d38836a277eb2784a91c9 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 13:45:23 +0000 Subject: [PATCH 143/169] Add compile relative double-suffix regressions Co-authored-by: Brian Searls --- .../daglang-cli/tests/compile_commands.rs | 465 ++++++++++++++++++ 1 file changed, 465 insertions(+) diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index 9ddc662eb80..40fa01b2f54 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -877,6 +877,92 @@ fn compile_command_curdir_segment_suffix_missing_root_matches_plain_relative_out std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_curdir_suffix_double_separator_missing_root_matches_plain_relative_output() { + let root = unique_temp_dir("compile_curdir_suffix_double_separator_missing_root"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + let missing = root.join("missing_root"); + + let plain = Command::new(daglang_bin()) + .arg("compile") + .arg("missing_root") + .current_dir(&root) + .output() + .expect("failed to run plain missing-root compile"); + assert!(!plain.status.success(), "plain missing-root compile should fail"); + + let curdir_suffix_double_separator = Command::new(daglang_bin()) + .arg("compile") + .arg("./missing_root//.") + .current_dir(&root) + .output() + .expect("failed to run curdir-suffix-double-separator missing-root compile"); + assert!( + !curdir_suffix_double_separator.status.success(), + "curdir-suffix-double-separator missing-root compile should fail" + ); + + assert_eq!( + plain.stdout, curdir_suffix_double_separator.stdout, + "plain and curdir-suffix-double-separator missing-root compile stdout should match" + ); + assert_eq!( + plain.stderr, curdir_suffix_double_separator.stderr, + "plain and curdir-suffix-double-separator missing-root compile stderr should match" + ); + let stderr = String::from_utf8_lossy(&plain.stderr); + assert!( + stderr.contains(&missing.display().to_string()), + "curdir-suffix-double-separator missing-root diagnostic should normalize to absolute path: {stderr}" + ); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + +#[test] +fn compile_command_curdir_segment_double_separator_suffix_missing_root_matches_plain_relative_output() { + let root = unique_temp_dir("compile_curdir_segment_double_separator_suffix_missing_root"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + let missing = root.join("missing_root"); + + let plain = Command::new(daglang_bin()) + .arg("compile") + .arg("missing_root") + .current_dir(&root) + .output() + .expect("failed to run plain missing-root compile"); + assert!(!plain.status.success(), "plain missing-root compile should fail"); + + let curdir_segment_double_separator_suffix = Command::new(daglang_bin()) + .arg("compile") + .arg("./missing_root//./.") + .current_dir(&root) + .output() + .expect("failed to run curdir-segment-double-separator-suffix missing-root compile"); + assert!( + !curdir_segment_double_separator_suffix.status.success(), + "curdir-segment-double-separator-suffix missing-root compile should fail" + ); + + assert_eq!( + plain.stdout, curdir_segment_double_separator_suffix.stdout, + "plain and curdir-segment-double-separator-suffix missing-root compile stdout should match" + ); + assert_eq!( + plain.stderr, curdir_segment_double_separator_suffix.stderr, + "plain and curdir-segment-double-separator-suffix missing-root compile stderr should match" + ); + let stderr = String::from_utf8_lossy(&plain.stderr); + assert!( + stderr.contains(&missing.display().to_string()), + "curdir-segment-double-separator-suffix missing-root diagnostic should normalize to absolute path: {stderr}" + ); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_curdir_double_separator_missing_root_matches_plain_relative_output() { let root = unique_temp_dir("compile_curdir_double_separator_missing_root"); @@ -1236,6 +1322,104 @@ fn compile_command_curdir_segment_suffix_missing_single_file_target_matches_plai std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_curdir_suffix_double_separator_missing_single_file_target_matches_plain_relative_output( +) { + let root = unique_temp_dir("compile_curdir_suffix_double_separator_missing_single_file_target"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + let missing = root.join("missing.dag"); + + let plain = Command::new(daglang_bin()) + .arg("compile") + .arg("missing.dag") + .current_dir(&root) + .output() + .expect("failed to run plain missing single-file compile"); + assert!( + !plain.status.success(), + "plain missing single-file compile should fail" + ); + + let curdir_suffix_double_separator = Command::new(daglang_bin()) + .arg("compile") + .arg("./missing.dag//.") + .current_dir(&root) + .output() + .expect("failed to run curdir-suffix-double-separator missing single-file compile"); + assert!( + !curdir_suffix_double_separator.status.success(), + "curdir-suffix-double-separator missing single-file compile should fail" + ); + + assert_eq!( + plain.stdout, curdir_suffix_double_separator.stdout, + "plain and curdir-suffix-double-separator missing single-file compile stdout should match" + ); + assert_eq!( + plain.stderr, curdir_suffix_double_separator.stderr, + "plain and curdir-suffix-double-separator missing single-file compile stderr should match" + ); + let stderr = String::from_utf8_lossy(&plain.stderr); + assert!( + stderr.contains(&format!("failed to read {}", missing.display())), + "curdir-suffix-double-separator missing single-file diagnostic should normalize to absolute path: {stderr}" + ); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + +#[test] +fn compile_command_curdir_segment_double_separator_suffix_missing_single_file_target_matches_plain_relative_output( +) { + let root = unique_temp_dir( + "compile_curdir_segment_double_separator_suffix_missing_single_file_target", + ); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + let missing = root.join("missing.dag"); + + let plain = Command::new(daglang_bin()) + .arg("compile") + .arg("missing.dag") + .current_dir(&root) + .output() + .expect("failed to run plain missing single-file compile"); + assert!( + !plain.status.success(), + "plain missing single-file compile should fail" + ); + + let curdir_segment_double_separator_suffix = Command::new(daglang_bin()) + .arg("compile") + .arg("./missing.dag//./.") + .current_dir(&root) + .output() + .expect( + "failed to run curdir-segment-double-separator-suffix missing single-file compile", + ); + assert!( + !curdir_segment_double_separator_suffix.status.success(), + "curdir-segment-double-separator-suffix missing single-file compile should fail" + ); + + assert_eq!( + plain.stdout, curdir_segment_double_separator_suffix.stdout, + "plain and curdir-segment-double-separator-suffix missing single-file compile stdout should match" + ); + assert_eq!( + plain.stderr, curdir_segment_double_separator_suffix.stderr, + "plain and curdir-segment-double-separator-suffix missing single-file compile stderr should match" + ); + let stderr = String::from_utf8_lossy(&plain.stderr); + assert!( + stderr.contains(&format!("failed to read {}", missing.display())), + "curdir-segment-double-separator-suffix missing single-file diagnostic should normalize to absolute path: {stderr}" + ); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_relative_and_absolute_invalid_single_file_targets_are_equivalent() { let root = unique_temp_dir("compile_relative_absolute_invalid_single_file_target"); @@ -1476,6 +1660,108 @@ fn compile_command_curdir_segment_suffix_invalid_single_file_target_matches_plai std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_curdir_suffix_double_separator_invalid_single_file_target_matches_plain_relative_output( +) { + let root = unique_temp_dir("compile_curdir_suffix_double_separator_invalid_single_file_target"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + let invalid_target = root.join("invalid.dag"); + std::fs::create_dir_all(&invalid_target) + .expect("failed to create invalid single-file target directory"); + + let plain = Command::new(daglang_bin()) + .arg("compile") + .arg("invalid.dag") + .current_dir(&root) + .output() + .expect("failed to run plain invalid single-file compile"); + assert!( + !plain.status.success(), + "plain invalid single-file compile should fail" + ); + + let curdir_suffix_double_separator = Command::new(daglang_bin()) + .arg("compile") + .arg("./invalid.dag//.") + .current_dir(&root) + .output() + .expect("failed to run curdir-suffix-double-separator invalid single-file compile"); + assert!( + !curdir_suffix_double_separator.status.success(), + "curdir-suffix-double-separator invalid single-file compile should fail" + ); + + assert_eq!( + plain.stdout, curdir_suffix_double_separator.stdout, + "plain and curdir-suffix-double-separator invalid single-file compile stdout should match" + ); + assert_eq!( + plain.stderr, curdir_suffix_double_separator.stderr, + "plain and curdir-suffix-double-separator invalid single-file compile stderr should match" + ); + let stderr = String::from_utf8_lossy(&plain.stderr); + assert!( + stderr.contains(&format!("failed to read {}", invalid_target.display())), + "curdir-suffix-double-separator invalid single-file diagnostic should include normalized absolute path: {stderr}" + ); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + +#[test] +fn compile_command_curdir_segment_double_separator_suffix_invalid_single_file_target_matches_plain_relative_output( +) { + let root = unique_temp_dir( + "compile_curdir_segment_double_separator_suffix_invalid_single_file_target", + ); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + let invalid_target = root.join("invalid.dag"); + std::fs::create_dir_all(&invalid_target) + .expect("failed to create invalid single-file target directory"); + + let plain = Command::new(daglang_bin()) + .arg("compile") + .arg("invalid.dag") + .current_dir(&root) + .output() + .expect("failed to run plain invalid single-file compile"); + assert!( + !plain.status.success(), + "plain invalid single-file compile should fail" + ); + + let curdir_segment_double_separator_suffix = Command::new(daglang_bin()) + .arg("compile") + .arg("./invalid.dag//./.") + .current_dir(&root) + .output() + .expect( + "failed to run curdir-segment-double-separator-suffix invalid single-file compile", + ); + assert!( + !curdir_segment_double_separator_suffix.status.success(), + "curdir-segment-double-separator-suffix invalid single-file compile should fail" + ); + + assert_eq!( + plain.stdout, curdir_segment_double_separator_suffix.stdout, + "plain and curdir-segment-double-separator-suffix invalid single-file compile stdout should match" + ); + assert_eq!( + plain.stderr, curdir_segment_double_separator_suffix.stderr, + "plain and curdir-segment-double-separator-suffix invalid single-file compile stderr should match" + ); + let stderr = String::from_utf8_lossy(&plain.stderr); + assert!( + stderr.contains(&format!("failed to read {}", invalid_target.display())), + "curdir-segment-double-separator-suffix invalid single-file diagnostic should include normalized absolute path: {stderr}" + ); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_curdir_double_separator_invalid_single_file_target_matches_plain_relative_output( ) { @@ -3326,6 +3612,99 @@ fn compile_command_curdir_segment_suffix_single_file_target_matches_plain_relati std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_curdir_suffix_double_separator_single_file_target_matches_plain_relative_output( +) { + let root = unique_temp_dir("compile_curdir_suffix_double_separator_single_file_target"); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp root"); + let source = root.join("sample/main.dag"); + std::fs::write(&source, "module sample.main\nfn run() -> Unit { }") + .expect("failed to write single-file source"); + + let plain = Command::new(daglang_bin()) + .arg("compile") + .arg("sample/main.dag") + .current_dir(&root) + .output() + .expect("failed to run plain single-file compile"); + assert!( + plain.status.success(), + "plain single-file compile should succeed: {}", + String::from_utf8_lossy(&plain.stderr) + ); + + let curdir_suffix_double_separator = Command::new(daglang_bin()) + .arg("compile") + .arg("./sample/main.dag//.") + .current_dir(&root) + .output() + .expect("failed to run curdir-suffix-double-separator single-file compile"); + assert!( + curdir_suffix_double_separator.status.success(), + "curdir-suffix-double-separator single-file compile should succeed: {}", + String::from_utf8_lossy(&curdir_suffix_double_separator.stderr) + ); + + assert_eq!( + plain.stdout, curdir_suffix_double_separator.stdout, + "plain and curdir-suffix-double-separator single-file compile stdout should match" + ); + assert_eq!( + plain.stderr, curdir_suffix_double_separator.stderr, + "plain and curdir-suffix-double-separator single-file compile stderr should match" + ); + assert_no_stage_failures(&String::from_utf8_lossy(&plain.stderr)); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + +#[test] +fn compile_command_curdir_segment_double_separator_suffix_single_file_target_matches_plain_relative_output( +) { + let root = + unique_temp_dir("compile_curdir_segment_double_separator_suffix_single_file_target"); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp root"); + let source = root.join("sample/main.dag"); + std::fs::write(&source, "module sample.main\nfn run() -> Unit { }") + .expect("failed to write single-file source"); + + let plain = Command::new(daglang_bin()) + .arg("compile") + .arg("sample/main.dag") + .current_dir(&root) + .output() + .expect("failed to run plain single-file compile"); + assert!( + plain.status.success(), + "plain single-file compile should succeed: {}", + String::from_utf8_lossy(&plain.stderr) + ); + + let curdir_segment_double_separator_suffix = Command::new(daglang_bin()) + .arg("compile") + .arg("./sample/main.dag//./.") + .current_dir(&root) + .output() + .expect("failed to run curdir-segment-double-separator-suffix single-file compile"); + assert!( + curdir_segment_double_separator_suffix.status.success(), + "curdir-segment-double-separator-suffix single-file compile should succeed: {}", + String::from_utf8_lossy(&curdir_segment_double_separator_suffix.stderr) + ); + + assert_eq!( + plain.stdout, curdir_segment_double_separator_suffix.stdout, + "plain and curdir-segment-double-separator-suffix single-file compile stdout should match" + ); + assert_eq!( + plain.stderr, curdir_segment_double_separator_suffix.stderr, + "plain and curdir-segment-double-separator-suffix single-file compile stderr should match" + ); + assert_no_stage_failures(&String::from_utf8_lossy(&plain.stderr)); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_trailing_slash_non_directory_root_matches_plain_relative_output() { let root = unique_temp_dir("compile_trailing_slash_non_directory_root"); @@ -5933,6 +6312,92 @@ fn compile_command_curdir_segment_suffix_root_matches_plain_relative_output() { std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_curdir_suffix_double_separator_root_matches_plain_relative_output() { + let root = unique_temp_dir("compile_curdir_suffix_double_separator_root"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + write_minimal_directory_compile_fixture(&root); + + let plain = Command::new(daglang_bin()) + .arg("compile") + .arg("dsl") + .current_dir(&root) + .output() + .expect("failed to run plain-relative root compile"); + assert!( + plain.status.success(), + "plain-relative root compile should succeed: {}", + String::from_utf8_lossy(&plain.stderr) + ); + + let curdir_suffix_double_separator = Command::new(daglang_bin()) + .arg("compile") + .arg("./dsl//.") + .current_dir(&root) + .output() + .expect("failed to run curdir-suffix-double-separator root compile"); + assert!( + curdir_suffix_double_separator.status.success(), + "curdir-suffix-double-separator root compile should succeed: {}", + String::from_utf8_lossy(&curdir_suffix_double_separator.stderr) + ); + + assert_eq!( + plain.stdout, curdir_suffix_double_separator.stdout, + "plain-relative and curdir-suffix-double-separator root compile stdout should match" + ); + assert_eq!( + plain.stderr, curdir_suffix_double_separator.stderr, + "plain-relative and curdir-suffix-double-separator root compile stderr should match" + ); + assert_no_stage_failures(&String::from_utf8_lossy(&plain.stderr)); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + +#[test] +fn compile_command_curdir_segment_double_separator_suffix_root_matches_plain_relative_output() { + let root = unique_temp_dir("compile_curdir_segment_double_separator_suffix_root"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + write_minimal_directory_compile_fixture(&root); + + let plain = Command::new(daglang_bin()) + .arg("compile") + .arg("dsl") + .current_dir(&root) + .output() + .expect("failed to run plain-relative root compile"); + assert!( + plain.status.success(), + "plain-relative root compile should succeed: {}", + String::from_utf8_lossy(&plain.stderr) + ); + + let curdir_segment_double_separator_suffix = Command::new(daglang_bin()) + .arg("compile") + .arg("./dsl//./.") + .current_dir(&root) + .output() + .expect("failed to run curdir-segment-double-separator-suffix root compile"); + assert!( + curdir_segment_double_separator_suffix.status.success(), + "curdir-segment-double-separator-suffix root compile should succeed: {}", + String::from_utf8_lossy(&curdir_segment_double_separator_suffix.stderr) + ); + + assert_eq!( + plain.stdout, curdir_segment_double_separator_suffix.stdout, + "plain-relative and curdir-segment-double-separator-suffix root compile stdout should match" + ); + assert_eq!( + plain.stderr, curdir_segment_double_separator_suffix.stderr, + "plain-relative and curdir-segment-double-separator-suffix root compile stderr should match" + ); + assert_no_stage_failures(&String::from_utf8_lossy(&plain.stderr)); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_curdir_double_separator_root_matches_plain_relative_output() { let root = unique_temp_dir("compile_curdir_double_separator_root"); From 22fb4602b31df84497fd424067b25da9643b6be7 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 13:50:48 +0000 Subject: [PATCH 144/169] Add compile relative curdir-segment-double regressions Co-authored-by: Brian Searls --- .../daglang-cli/tests/compile_commands.rs | 276 ++++++++++++++++++ 1 file changed, 276 insertions(+) diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index 40fa01b2f54..e3707fc445d 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -963,6 +963,49 @@ fn compile_command_curdir_segment_double_separator_suffix_missing_root_matches_p std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_curdir_segment_double_separator_missing_root_matches_plain_relative_output() { + let root = unique_temp_dir("compile_curdir_segment_double_separator_missing_root"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + let missing = root.join("missing_root"); + + let plain = Command::new(daglang_bin()) + .arg("compile") + .arg("missing_root") + .current_dir(&root) + .output() + .expect("failed to run plain missing-root compile"); + assert!(!plain.status.success(), "plain missing-root compile should fail"); + + let curdir_segment_double_separator = Command::new(daglang_bin()) + .arg("compile") + .arg("./missing_root//") + .current_dir(&root) + .output() + .expect("failed to run curdir-segment-double-separator missing-root compile"); + assert!( + !curdir_segment_double_separator.status.success(), + "curdir-segment-double-separator missing-root compile should fail" + ); + + assert_eq!( + plain.stdout, curdir_segment_double_separator.stdout, + "plain and curdir-segment-double-separator missing-root compile stdout should match" + ); + assert_eq!( + plain.stderr, curdir_segment_double_separator.stderr, + "plain and curdir-segment-double-separator missing-root compile stderr should match" + ); + let stderr = String::from_utf8_lossy(&plain.stderr); + assert!( + stderr.contains(&missing.display().to_string()), + "curdir-segment-double-separator missing-root diagnostic should normalize to absolute path: {stderr}" + ); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_curdir_double_separator_missing_root_matches_plain_relative_output() { let root = unique_temp_dir("compile_curdir_double_separator_missing_root"); @@ -1420,6 +1463,53 @@ fn compile_command_curdir_segment_double_separator_suffix_missing_single_file_ta std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_curdir_segment_double_separator_missing_single_file_target_matches_plain_relative_output( +) { + let root = unique_temp_dir("compile_curdir_segment_double_separator_missing_single_file_target"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + let missing = root.join("missing.dag"); + + let plain = Command::new(daglang_bin()) + .arg("compile") + .arg("missing.dag") + .current_dir(&root) + .output() + .expect("failed to run plain missing single-file compile"); + assert!( + !plain.status.success(), + "plain missing single-file compile should fail" + ); + + let curdir_segment_double_separator = Command::new(daglang_bin()) + .arg("compile") + .arg("./missing.dag//") + .current_dir(&root) + .output() + .expect("failed to run curdir-segment-double-separator missing single-file compile"); + assert!( + !curdir_segment_double_separator.status.success(), + "curdir-segment-double-separator missing single-file compile should fail" + ); + + assert_eq!( + plain.stdout, curdir_segment_double_separator.stdout, + "plain and curdir-segment-double-separator missing single-file compile stdout should match" + ); + assert_eq!( + plain.stderr, curdir_segment_double_separator.stderr, + "plain and curdir-segment-double-separator missing single-file compile stderr should match" + ); + let stderr = String::from_utf8_lossy(&plain.stderr); + assert!( + stderr.contains(&format!("failed to read {}", missing.display())), + "curdir-segment-double-separator missing single-file diagnostic should normalize to absolute path: {stderr}" + ); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_relative_and_absolute_invalid_single_file_targets_are_equivalent() { let root = unique_temp_dir("compile_relative_absolute_invalid_single_file_target"); @@ -1762,6 +1852,55 @@ fn compile_command_curdir_segment_double_separator_suffix_invalid_single_file_ta std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_curdir_segment_double_separator_invalid_single_file_target_matches_plain_relative_output( +) { + let root = unique_temp_dir("compile_curdir_segment_double_separator_invalid_single_file_target"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + let invalid_target = root.join("invalid.dag"); + std::fs::create_dir_all(&invalid_target) + .expect("failed to create invalid single-file target directory"); + + let plain = Command::new(daglang_bin()) + .arg("compile") + .arg("invalid.dag") + .current_dir(&root) + .output() + .expect("failed to run plain invalid single-file compile"); + assert!( + !plain.status.success(), + "plain invalid single-file compile should fail" + ); + + let curdir_segment_double_separator = Command::new(daglang_bin()) + .arg("compile") + .arg("./invalid.dag//") + .current_dir(&root) + .output() + .expect("failed to run curdir-segment-double-separator invalid single-file compile"); + assert!( + !curdir_segment_double_separator.status.success(), + "curdir-segment-double-separator invalid single-file compile should fail" + ); + + assert_eq!( + plain.stdout, curdir_segment_double_separator.stdout, + "plain and curdir-segment-double-separator invalid single-file compile stdout should match" + ); + assert_eq!( + plain.stderr, curdir_segment_double_separator.stderr, + "plain and curdir-segment-double-separator invalid single-file compile stderr should match" + ); + let stderr = String::from_utf8_lossy(&plain.stderr); + assert!( + stderr.contains(&format!("failed to read {}", invalid_target.display())), + "curdir-segment-double-separator invalid single-file diagnostic should include normalized absolute path: {stderr}" + ); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_curdir_double_separator_invalid_single_file_target_matches_plain_relative_output( ) { @@ -3705,6 +3844,100 @@ fn compile_command_curdir_segment_double_separator_suffix_single_file_target_mat std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_curdir_segment_double_separator_single_file_target_matches_plain_relative_output() +{ + let root = unique_temp_dir("compile_curdir_segment_double_separator_single_file_target"); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp root"); + let source = root.join("sample/main.dag"); + std::fs::write(&source, "module sample.main\nfn run() -> Unit { }") + .expect("failed to write single-file source"); + + let plain = Command::new(daglang_bin()) + .arg("compile") + .arg("sample/main.dag") + .current_dir(&root) + .output() + .expect("failed to run plain single-file compile"); + assert!( + plain.status.success(), + "plain single-file compile should succeed: {}", + String::from_utf8_lossy(&plain.stderr) + ); + + let curdir_segment_double_separator = Command::new(daglang_bin()) + .arg("compile") + .arg("./sample/main.dag//") + .current_dir(&root) + .output() + .expect("failed to run curdir-segment-double-separator single-file compile"); + assert!( + curdir_segment_double_separator.status.success(), + "curdir-segment-double-separator single-file compile should succeed: {}", + String::from_utf8_lossy(&curdir_segment_double_separator.stderr) + ); + + assert_eq!( + plain.stdout, curdir_segment_double_separator.stdout, + "plain and curdir-segment-double-separator single-file compile stdout should match" + ); + assert_eq!( + plain.stderr, curdir_segment_double_separator.stderr, + "plain and curdir-segment-double-separator single-file compile stderr should match" + ); + assert_no_stage_failures(&String::from_utf8_lossy(&plain.stderr)); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + +#[test] +fn compile_command_curdir_segment_double_separator_non_directory_root_matches_plain_relative_output() +{ + let root = unique_temp_dir("compile_curdir_segment_double_separator_non_directory_root"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + let root_file = root.join("input.txt"); + std::fs::write(&root_file, "not a directory").expect("failed to write non-directory root"); + + let plain = Command::new(daglang_bin()) + .arg("compile") + .arg("input.txt") + .current_dir(&root) + .output() + .expect("failed to run plain non-directory-root compile"); + assert!( + !plain.status.success(), + "plain non-directory-root compile should fail" + ); + + let curdir_segment_double_separator = Command::new(daglang_bin()) + .arg("compile") + .arg("./input.txt//") + .current_dir(&root) + .output() + .expect("failed to run curdir-segment-double-separator non-directory-root compile"); + assert!( + !curdir_segment_double_separator.status.success(), + "curdir-segment-double-separator non-directory-root compile should fail" + ); + + assert_eq!( + plain.stdout, curdir_segment_double_separator.stdout, + "plain and curdir-segment-double-separator non-directory-root compile stdout should match" + ); + assert_eq!( + plain.stderr, curdir_segment_double_separator.stderr, + "plain and curdir-segment-double-separator non-directory-root compile stderr should match" + ); + let stderr = String::from_utf8_lossy(&plain.stderr); + assert!( + stderr.contains(&root_file.display().to_string()), + "curdir-segment-double-separator non-directory-root diagnostic should normalize to absolute path: {stderr}" + ); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_trailing_slash_non_directory_root_matches_plain_relative_output() { let root = unique_temp_dir("compile_trailing_slash_non_directory_root"); @@ -6398,6 +6631,49 @@ fn compile_command_curdir_segment_double_separator_suffix_root_matches_plain_rel std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_curdir_segment_double_separator_root_matches_plain_relative_output() { + let root = unique_temp_dir("compile_curdir_segment_double_separator_root"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + write_minimal_directory_compile_fixture(&root); + + let plain = Command::new(daglang_bin()) + .arg("compile") + .arg("dsl") + .current_dir(&root) + .output() + .expect("failed to run plain-relative root compile"); + assert!( + plain.status.success(), + "plain-relative root compile should succeed: {}", + String::from_utf8_lossy(&plain.stderr) + ); + + let curdir_segment_double_separator = Command::new(daglang_bin()) + .arg("compile") + .arg("./dsl//") + .current_dir(&root) + .output() + .expect("failed to run curdir-segment-double-separator root compile"); + assert!( + curdir_segment_double_separator.status.success(), + "curdir-segment-double-separator root compile should succeed: {}", + String::from_utf8_lossy(&curdir_segment_double_separator.stderr) + ); + + assert_eq!( + plain.stdout, curdir_segment_double_separator.stdout, + "plain-relative and curdir-segment-double-separator root compile stdout should match" + ); + assert_eq!( + plain.stderr, curdir_segment_double_separator.stderr, + "plain-relative and curdir-segment-double-separator root compile stderr should match" + ); + assert_no_stage_failures(&String::from_utf8_lossy(&plain.stderr)); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_curdir_double_separator_root_matches_plain_relative_output() { let root = unique_temp_dir("compile_curdir_double_separator_root"); From 187f67044d75bbb5bb1945a635590c9ef47521b6 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 13:53:39 +0000 Subject: [PATCH 145/169] Add compile relative curdir trailing regressions Co-authored-by: Brian Searls --- .../daglang-cli/tests/compile_commands.rs | 274 ++++++++++++++++++ 1 file changed, 274 insertions(+) diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index e3707fc445d..0e3f31f3e87 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -791,6 +791,49 @@ fn compile_command_curdir_segment_missing_root_matches_plain_relative_output() { std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_curdir_segment_trailing_slash_missing_root_matches_plain_relative_output() { + let root = unique_temp_dir("compile_curdir_segment_trailing_slash_missing_root"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + let missing = root.join("missing_root"); + + let plain = Command::new(daglang_bin()) + .arg("compile") + .arg("missing_root") + .current_dir(&root) + .output() + .expect("failed to run plain missing-root compile"); + assert!(!plain.status.success(), "plain missing-root compile should fail"); + + let curdir_segment_trailing = Command::new(daglang_bin()) + .arg("compile") + .arg("./missing_root/") + .current_dir(&root) + .output() + .expect("failed to run curdir-segment-trailing missing-root compile"); + assert!( + !curdir_segment_trailing.status.success(), + "curdir-segment-trailing missing-root compile should fail" + ); + + assert_eq!( + plain.stdout, curdir_segment_trailing.stdout, + "plain and curdir-segment-trailing missing-root compile stdout should match" + ); + assert_eq!( + plain.stderr, curdir_segment_trailing.stderr, + "plain and curdir-segment-trailing missing-root compile stderr should match" + ); + let stderr = String::from_utf8_lossy(&plain.stderr); + assert!( + stderr.contains(&missing.display().to_string()), + "curdir-segment-trailing missing-root diagnostic should normalize to absolute path: {stderr}" + ); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_curdir_suffix_missing_root_matches_plain_relative_output() { let root = unique_temp_dir("compile_curdir_suffix_missing_root"); @@ -1273,6 +1316,53 @@ fn compile_command_curdir_segment_missing_single_file_target_matches_plain_relat std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_curdir_segment_trailing_slash_missing_single_file_target_matches_plain_relative_output( +) { + let root = unique_temp_dir("compile_curdir_segment_trailing_missing_single_file_target"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + let missing = root.join("missing.dag"); + + let plain = Command::new(daglang_bin()) + .arg("compile") + .arg("missing.dag") + .current_dir(&root) + .output() + .expect("failed to run plain missing single-file compile"); + assert!( + !plain.status.success(), + "plain missing single-file compile should fail" + ); + + let curdir_segment_trailing = Command::new(daglang_bin()) + .arg("compile") + .arg("./missing.dag/") + .current_dir(&root) + .output() + .expect("failed to run curdir-segment-trailing missing single-file compile"); + assert!( + !curdir_segment_trailing.status.success(), + "curdir-segment-trailing missing single-file compile should fail" + ); + + assert_eq!( + plain.stdout, curdir_segment_trailing.stdout, + "plain and curdir-segment-trailing missing single-file compile stdout should match" + ); + assert_eq!( + plain.stderr, curdir_segment_trailing.stderr, + "plain and curdir-segment-trailing missing single-file compile stderr should match" + ); + let stderr = String::from_utf8_lossy(&plain.stderr); + assert!( + stderr.contains(&format!("failed to read {}", missing.display())), + "curdir-segment-trailing missing single-file diagnostic should normalize to absolute path: {stderr}" + ); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_curdir_suffix_missing_single_file_target_matches_plain_relative_output() { let root = unique_temp_dir("compile_curdir_suffix_missing_single_file_target"); @@ -1654,6 +1744,55 @@ fn compile_command_curdir_segment_invalid_single_file_target_matches_plain_relat std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_curdir_segment_trailing_slash_invalid_single_file_target_matches_plain_relative_output( +) { + let root = unique_temp_dir("compile_curdir_segment_trailing_invalid_single_file_target"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + let invalid_target = root.join("invalid.dag"); + std::fs::create_dir_all(&invalid_target) + .expect("failed to create invalid single-file target directory"); + + let plain = Command::new(daglang_bin()) + .arg("compile") + .arg("invalid.dag") + .current_dir(&root) + .output() + .expect("failed to run plain invalid single-file compile"); + assert!( + !plain.status.success(), + "plain invalid single-file compile should fail" + ); + + let curdir_segment_trailing = Command::new(daglang_bin()) + .arg("compile") + .arg("./invalid.dag/") + .current_dir(&root) + .output() + .expect("failed to run curdir-segment-trailing invalid single-file compile"); + assert!( + !curdir_segment_trailing.status.success(), + "curdir-segment-trailing invalid single-file compile should fail" + ); + + assert_eq!( + plain.stdout, curdir_segment_trailing.stdout, + "plain and curdir-segment-trailing invalid single-file compile stdout should match" + ); + assert_eq!( + plain.stderr, curdir_segment_trailing.stderr, + "plain and curdir-segment-trailing invalid single-file compile stderr should match" + ); + let stderr = String::from_utf8_lossy(&plain.stderr); + assert!( + stderr.contains(&format!("failed to read {}", invalid_target.display())), + "curdir-segment-trailing invalid single-file diagnostic should include normalized absolute path: {stderr}" + ); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_curdir_suffix_invalid_single_file_target_matches_plain_relative_output() { let root = unique_temp_dir("compile_curdir_suffix_invalid_single_file_target"); @@ -3661,6 +3800,51 @@ fn compile_command_curdir_segment_single_file_target_matches_plain_relative_outp std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_curdir_segment_trailing_slash_single_file_target_matches_plain_relative_output() { + let root = unique_temp_dir("compile_curdir_segment_trailing_single_file_target"); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp root"); + let source = root.join("sample/main.dag"); + std::fs::write(&source, "module sample.main\nfn run() -> Unit { }") + .expect("failed to write single-file source"); + + let plain = Command::new(daglang_bin()) + .arg("compile") + .arg("sample/main.dag") + .current_dir(&root) + .output() + .expect("failed to run plain single-file compile"); + assert!( + plain.status.success(), + "plain single-file compile should succeed: {}", + String::from_utf8_lossy(&plain.stderr) + ); + + let curdir_segment_trailing = Command::new(daglang_bin()) + .arg("compile") + .arg("./sample/main.dag/") + .current_dir(&root) + .output() + .expect("failed to run curdir-segment-trailing single-file compile"); + assert!( + curdir_segment_trailing.status.success(), + "curdir-segment-trailing single-file compile should succeed: {}", + String::from_utf8_lossy(&curdir_segment_trailing.stderr) + ); + + assert_eq!( + plain.stdout, curdir_segment_trailing.stdout, + "plain and curdir-segment-trailing single-file compile stdout should match" + ); + assert_eq!( + plain.stderr, curdir_segment_trailing.stderr, + "plain and curdir-segment-trailing single-file compile stderr should match" + ); + assert_no_stage_failures(&String::from_utf8_lossy(&plain.stderr)); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_curdir_suffix_single_file_target_matches_plain_relative_output() { let root = unique_temp_dir("compile_curdir_suffix_single_file_target"); @@ -3938,6 +4122,53 @@ fn compile_command_curdir_segment_double_separator_non_directory_root_matches_pl std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_curdir_segment_trailing_slash_non_directory_root_matches_plain_relative_output() { + let root = unique_temp_dir("compile_curdir_segment_trailing_non_directory_root"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + let root_file = root.join("input.txt"); + std::fs::write(&root_file, "not a directory").expect("failed to write non-directory root"); + + let plain = Command::new(daglang_bin()) + .arg("compile") + .arg("input.txt") + .current_dir(&root) + .output() + .expect("failed to run plain non-directory-root compile"); + assert!( + !plain.status.success(), + "plain non-directory-root compile should fail" + ); + + let curdir_segment_trailing = Command::new(daglang_bin()) + .arg("compile") + .arg("./input.txt/") + .current_dir(&root) + .output() + .expect("failed to run curdir-segment-trailing non-directory-root compile"); + assert!( + !curdir_segment_trailing.status.success(), + "curdir-segment-trailing non-directory-root compile should fail" + ); + + assert_eq!( + plain.stdout, curdir_segment_trailing.stdout, + "plain and curdir-segment-trailing non-directory-root compile stdout should match" + ); + assert_eq!( + plain.stderr, curdir_segment_trailing.stderr, + "plain and curdir-segment-trailing non-directory-root compile stderr should match" + ); + let stderr = String::from_utf8_lossy(&plain.stderr); + assert!( + stderr.contains(&root_file.display().to_string()), + "curdir-segment-trailing non-directory-root diagnostic should normalize to absolute path: {stderr}" + ); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_trailing_slash_non_directory_root_matches_plain_relative_output() { let root = unique_temp_dir("compile_trailing_slash_non_directory_root"); @@ -6459,6 +6690,49 @@ fn compile_command_curdir_segment_root_matches_plain_relative_output() { std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_curdir_segment_trailing_slash_root_matches_plain_relative_output() { + let root = unique_temp_dir("compile_curdir_segment_trailing_root"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + write_minimal_directory_compile_fixture(&root); + + let plain = Command::new(daglang_bin()) + .arg("compile") + .arg("dsl") + .current_dir(&root) + .output() + .expect("failed to run plain-relative root compile"); + assert!( + plain.status.success(), + "plain-relative root compile should succeed: {}", + String::from_utf8_lossy(&plain.stderr) + ); + + let curdir_segment_trailing = Command::new(daglang_bin()) + .arg("compile") + .arg("./dsl/") + .current_dir(&root) + .output() + .expect("failed to run curdir-segment-trailing root compile"); + assert!( + curdir_segment_trailing.status.success(), + "curdir-segment-trailing root compile should succeed: {}", + String::from_utf8_lossy(&curdir_segment_trailing.stderr) + ); + + assert_eq!( + plain.stdout, curdir_segment_trailing.stdout, + "plain-relative and curdir-segment-trailing root compile stdout should match" + ); + assert_eq!( + plain.stderr, curdir_segment_trailing.stderr, + "plain-relative and curdir-segment-trailing root compile stderr should match" + ); + assert_no_stage_failures(&String::from_utf8_lossy(&plain.stderr)); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_curdir_suffix_root_matches_plain_relative_output() { let root = unique_temp_dir("compile_curdir_suffix_root"); From 58acb1be2fa6c1e777d8f00819957141376db7ae Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 13:58:45 +0000 Subject: [PATCH 146/169] Add compile dot-double curdir suffix regressions Co-authored-by: Brian Searls --- .../daglang-cli/tests/compile_commands.rs | 280 ++++++++++++++++++ 1 file changed, 280 insertions(+) diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index 0e3f31f3e87..2af86c56b2a 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -834,6 +834,49 @@ fn compile_command_curdir_segment_trailing_slash_missing_root_matches_plain_rela std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_dot_double_separator_curdir_suffix_missing_root_matches_plain_relative_output() { + let root = unique_temp_dir("compile_dot_double_separator_curdir_suffix_missing_root"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + let missing = root.join("missing_root"); + + let plain = Command::new(daglang_bin()) + .arg("compile") + .arg("missing_root") + .current_dir(&root) + .output() + .expect("failed to run plain missing-root compile"); + assert!(!plain.status.success(), "plain missing-root compile should fail"); + + let dot_double_separator_curdir_suffix = Command::new(daglang_bin()) + .arg("compile") + .arg(".//missing_root/.") + .current_dir(&root) + .output() + .expect("failed to run dot-double-separator-curdir-suffix missing-root compile"); + assert!( + !dot_double_separator_curdir_suffix.status.success(), + "dot-double-separator-curdir-suffix missing-root compile should fail" + ); + + assert_eq!( + plain.stdout, dot_double_separator_curdir_suffix.stdout, + "plain and dot-double-separator-curdir-suffix missing-root compile stdout should match" + ); + assert_eq!( + plain.stderr, dot_double_separator_curdir_suffix.stderr, + "plain and dot-double-separator-curdir-suffix missing-root compile stderr should match" + ); + let stderr = String::from_utf8_lossy(&plain.stderr); + assert!( + stderr.contains(&missing.display().to_string()), + "dot-double-separator-curdir-suffix missing-root diagnostic should normalize to absolute path: {stderr}" + ); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_curdir_suffix_missing_root_matches_plain_relative_output() { let root = unique_temp_dir("compile_curdir_suffix_missing_root"); @@ -1363,6 +1406,54 @@ fn compile_command_curdir_segment_trailing_slash_missing_single_file_target_matc std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_dot_double_separator_curdir_suffix_missing_single_file_target_matches_plain_relative_output( +) { + let root = + unique_temp_dir("compile_dot_double_separator_curdir_suffix_missing_single_file_target"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + let missing = root.join("missing.dag"); + + let plain = Command::new(daglang_bin()) + .arg("compile") + .arg("missing.dag") + .current_dir(&root) + .output() + .expect("failed to run plain missing single-file compile"); + assert!( + !plain.status.success(), + "plain missing single-file compile should fail" + ); + + let dot_double_separator_curdir_suffix = Command::new(daglang_bin()) + .arg("compile") + .arg(".//missing.dag/.") + .current_dir(&root) + .output() + .expect("failed to run dot-double-separator-curdir-suffix missing single-file compile"); + assert!( + !dot_double_separator_curdir_suffix.status.success(), + "dot-double-separator-curdir-suffix missing single-file compile should fail" + ); + + assert_eq!( + plain.stdout, dot_double_separator_curdir_suffix.stdout, + "plain and dot-double-separator-curdir-suffix missing single-file compile stdout should match" + ); + assert_eq!( + plain.stderr, dot_double_separator_curdir_suffix.stderr, + "plain and dot-double-separator-curdir-suffix missing single-file compile stderr should match" + ); + let stderr = String::from_utf8_lossy(&plain.stderr); + assert!( + stderr.contains(&format!("failed to read {}", missing.display())), + "dot-double-separator-curdir-suffix missing single-file diagnostic should normalize to absolute path: {stderr}" + ); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_curdir_suffix_missing_single_file_target_matches_plain_relative_output() { let root = unique_temp_dir("compile_curdir_suffix_missing_single_file_target"); @@ -1793,6 +1884,56 @@ fn compile_command_curdir_segment_trailing_slash_invalid_single_file_target_matc std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_dot_double_separator_curdir_suffix_invalid_single_file_target_matches_plain_relative_output( +) { + let root = + unique_temp_dir("compile_dot_double_separator_curdir_suffix_invalid_single_file_target"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + let invalid_target = root.join("invalid.dag"); + std::fs::create_dir_all(&invalid_target) + .expect("failed to create invalid single-file target directory"); + + let plain = Command::new(daglang_bin()) + .arg("compile") + .arg("invalid.dag") + .current_dir(&root) + .output() + .expect("failed to run plain invalid single-file compile"); + assert!( + !plain.status.success(), + "plain invalid single-file compile should fail" + ); + + let dot_double_separator_curdir_suffix = Command::new(daglang_bin()) + .arg("compile") + .arg(".//invalid.dag/.") + .current_dir(&root) + .output() + .expect("failed to run dot-double-separator-curdir-suffix invalid single-file compile"); + assert!( + !dot_double_separator_curdir_suffix.status.success(), + "dot-double-separator-curdir-suffix invalid single-file compile should fail" + ); + + assert_eq!( + plain.stdout, dot_double_separator_curdir_suffix.stdout, + "plain and dot-double-separator-curdir-suffix invalid single-file compile stdout should match" + ); + assert_eq!( + plain.stderr, dot_double_separator_curdir_suffix.stderr, + "plain and dot-double-separator-curdir-suffix invalid single-file compile stderr should match" + ); + let stderr = String::from_utf8_lossy(&plain.stderr); + assert!( + stderr.contains(&format!("failed to read {}", invalid_target.display())), + "dot-double-separator-curdir-suffix invalid single-file diagnostic should include normalized absolute path: {stderr}" + ); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_curdir_suffix_invalid_single_file_target_matches_plain_relative_output() { let root = unique_temp_dir("compile_curdir_suffix_invalid_single_file_target"); @@ -3845,6 +3986,53 @@ fn compile_command_curdir_segment_trailing_slash_single_file_target_matches_plai std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_dot_double_separator_curdir_suffix_single_file_target_matches_plain_relative_output( +) { + let root = + unique_temp_dir("compile_dot_double_separator_curdir_suffix_single_file_target"); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp root"); + let source = root.join("sample/main.dag"); + std::fs::write(&source, "module sample.main\nfn run() -> Unit { }") + .expect("failed to write single-file source"); + + let plain = Command::new(daglang_bin()) + .arg("compile") + .arg("sample/main.dag") + .current_dir(&root) + .output() + .expect("failed to run plain single-file compile"); + assert!( + plain.status.success(), + "plain single-file compile should succeed: {}", + String::from_utf8_lossy(&plain.stderr) + ); + + let dot_double_separator_curdir_suffix = Command::new(daglang_bin()) + .arg("compile") + .arg(".//sample/main.dag/.") + .current_dir(&root) + .output() + .expect("failed to run dot-double-separator-curdir-suffix single-file compile"); + assert!( + dot_double_separator_curdir_suffix.status.success(), + "dot-double-separator-curdir-suffix single-file compile should succeed: {}", + String::from_utf8_lossy(&dot_double_separator_curdir_suffix.stderr) + ); + + assert_eq!( + plain.stdout, dot_double_separator_curdir_suffix.stdout, + "plain and dot-double-separator-curdir-suffix single-file compile stdout should match" + ); + assert_eq!( + plain.stderr, dot_double_separator_curdir_suffix.stderr, + "plain and dot-double-separator-curdir-suffix single-file compile stderr should match" + ); + assert_no_stage_failures(&String::from_utf8_lossy(&plain.stderr)); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_curdir_suffix_single_file_target_matches_plain_relative_output() { let root = unique_temp_dir("compile_curdir_suffix_single_file_target"); @@ -4169,6 +4357,55 @@ fn compile_command_curdir_segment_trailing_slash_non_directory_root_matches_plai std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_dot_double_separator_curdir_suffix_non_directory_root_matches_plain_relative_output( +) { + let root = + unique_temp_dir("compile_dot_double_separator_curdir_suffix_non_directory_root"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + let root_file = root.join("input.txt"); + std::fs::write(&root_file, "not a directory").expect("failed to write non-directory root"); + + let plain = Command::new(daglang_bin()) + .arg("compile") + .arg("input.txt") + .current_dir(&root) + .output() + .expect("failed to run plain non-directory-root compile"); + assert!( + !plain.status.success(), + "plain non-directory-root compile should fail" + ); + + let dot_double_separator_curdir_suffix = Command::new(daglang_bin()) + .arg("compile") + .arg(".//input.txt/.") + .current_dir(&root) + .output() + .expect("failed to run dot-double-separator-curdir-suffix non-directory-root compile"); + assert!( + !dot_double_separator_curdir_suffix.status.success(), + "dot-double-separator-curdir-suffix non-directory-root compile should fail" + ); + + assert_eq!( + plain.stdout, dot_double_separator_curdir_suffix.stdout, + "plain and dot-double-separator-curdir-suffix non-directory-root compile stdout should match" + ); + assert_eq!( + plain.stderr, dot_double_separator_curdir_suffix.stderr, + "plain and dot-double-separator-curdir-suffix non-directory-root compile stderr should match" + ); + let stderr = String::from_utf8_lossy(&plain.stderr); + assert!( + stderr.contains(&root_file.display().to_string()), + "dot-double-separator-curdir-suffix non-directory-root diagnostic should normalize to absolute path: {stderr}" + ); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_trailing_slash_non_directory_root_matches_plain_relative_output() { let root = unique_temp_dir("compile_trailing_slash_non_directory_root"); @@ -6733,6 +6970,49 @@ fn compile_command_curdir_segment_trailing_slash_root_matches_plain_relative_out std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_dot_double_separator_curdir_suffix_root_matches_plain_relative_output() { + let root = unique_temp_dir("compile_dot_double_separator_curdir_suffix_root"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + write_minimal_directory_compile_fixture(&root); + + let plain = Command::new(daglang_bin()) + .arg("compile") + .arg("dsl") + .current_dir(&root) + .output() + .expect("failed to run plain-relative root compile"); + assert!( + plain.status.success(), + "plain-relative root compile should succeed: {}", + String::from_utf8_lossy(&plain.stderr) + ); + + let dot_double_separator_curdir_suffix = Command::new(daglang_bin()) + .arg("compile") + .arg(".//dsl/.") + .current_dir(&root) + .output() + .expect("failed to run dot-double-separator-curdir-suffix root compile"); + assert!( + dot_double_separator_curdir_suffix.status.success(), + "dot-double-separator-curdir-suffix root compile should succeed: {}", + String::from_utf8_lossy(&dot_double_separator_curdir_suffix.stderr) + ); + + assert_eq!( + plain.stdout, dot_double_separator_curdir_suffix.stdout, + "plain-relative and dot-double-separator-curdir-suffix root compile stdout should match" + ); + assert_eq!( + plain.stderr, dot_double_separator_curdir_suffix.stderr, + "plain-relative and dot-double-separator-curdir-suffix root compile stderr should match" + ); + assert_no_stage_failures(&String::from_utf8_lossy(&plain.stderr)); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_curdir_suffix_root_matches_plain_relative_output() { let root = unique_temp_dir("compile_curdir_suffix_root"); From c8231562d85a5bfb94e11d231ff0d3f0cf2b15c4 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 14:01:27 +0000 Subject: [PATCH 147/169] Add compile dot-double curdir segment-suffix regressions Co-authored-by: Brian Searls --- .../daglang-cli/tests/compile_commands.rs | 287 ++++++++++++++++++ 1 file changed, 287 insertions(+) diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index 2af86c56b2a..a96d51e97f3 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -834,6 +834,53 @@ fn compile_command_curdir_segment_trailing_slash_missing_root_matches_plain_rela std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_dot_double_separator_curdir_segment_suffix_missing_root_matches_plain_relative_output( +) { + let root = + unique_temp_dir("compile_dot_double_separator_curdir_segment_suffix_missing_root"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + let missing = root.join("missing_root"); + + let plain = Command::new(daglang_bin()) + .arg("compile") + .arg("missing_root") + .current_dir(&root) + .output() + .expect("failed to run plain missing-root compile"); + assert!(!plain.status.success(), "plain missing-root compile should fail"); + + let dot_double_separator_curdir_segment_suffix = Command::new(daglang_bin()) + .arg("compile") + .arg(".//missing_root/./.") + .current_dir(&root) + .output() + .expect( + "failed to run dot-double-separator-curdir-segment-suffix missing-root compile", + ); + assert!( + !dot_double_separator_curdir_segment_suffix.status.success(), + "dot-double-separator-curdir-segment-suffix missing-root compile should fail" + ); + + assert_eq!( + plain.stdout, dot_double_separator_curdir_segment_suffix.stdout, + "plain and dot-double-separator-curdir-segment-suffix missing-root compile stdout should match" + ); + assert_eq!( + plain.stderr, dot_double_separator_curdir_segment_suffix.stderr, + "plain and dot-double-separator-curdir-segment-suffix missing-root compile stderr should match" + ); + let stderr = String::from_utf8_lossy(&plain.stderr); + assert!( + stderr.contains(&missing.display().to_string()), + "dot-double-separator-curdir-segment-suffix missing-root diagnostic should normalize to absolute path: {stderr}" + ); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_dot_double_separator_curdir_suffix_missing_root_matches_plain_relative_output() { let root = unique_temp_dir("compile_dot_double_separator_curdir_suffix_missing_root"); @@ -1406,6 +1453,55 @@ fn compile_command_curdir_segment_trailing_slash_missing_single_file_target_matc std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_dot_double_separator_curdir_segment_suffix_missing_single_file_target_matches_plain_relative_output( +) { + let root = unique_temp_dir( + "compile_dot_double_separator_curdir_segment_suffix_missing_single_file_target", + ); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + let missing = root.join("missing.dag"); + + let plain = Command::new(daglang_bin()) + .arg("compile") + .arg("missing.dag") + .current_dir(&root) + .output() + .expect("failed to run plain missing single-file compile"); + assert!( + !plain.status.success(), + "plain missing single-file compile should fail" + ); + + let dot_double_separator_curdir_segment_suffix = Command::new(daglang_bin()) + .arg("compile") + .arg(".//missing.dag/./.") + .current_dir(&root) + .output() + .expect("failed to run dot-double-separator-curdir-segment-suffix missing single-file compile"); + assert!( + !dot_double_separator_curdir_segment_suffix.status.success(), + "dot-double-separator-curdir-segment-suffix missing single-file compile should fail" + ); + + assert_eq!( + plain.stdout, dot_double_separator_curdir_segment_suffix.stdout, + "plain and dot-double-separator-curdir-segment-suffix missing single-file compile stdout should match" + ); + assert_eq!( + plain.stderr, dot_double_separator_curdir_segment_suffix.stderr, + "plain and dot-double-separator-curdir-segment-suffix missing single-file compile stderr should match" + ); + let stderr = String::from_utf8_lossy(&plain.stderr); + assert!( + stderr.contains(&format!("failed to read {}", missing.display())), + "dot-double-separator-curdir-segment-suffix missing single-file diagnostic should normalize to absolute path: {stderr}" + ); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_dot_double_separator_curdir_suffix_missing_single_file_target_matches_plain_relative_output( ) { @@ -1884,6 +1980,57 @@ fn compile_command_curdir_segment_trailing_slash_invalid_single_file_target_matc std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_dot_double_separator_curdir_segment_suffix_invalid_single_file_target_matches_plain_relative_output( +) { + let root = unique_temp_dir( + "compile_dot_double_separator_curdir_segment_suffix_invalid_single_file_target", + ); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + let invalid_target = root.join("invalid.dag"); + std::fs::create_dir_all(&invalid_target) + .expect("failed to create invalid single-file target directory"); + + let plain = Command::new(daglang_bin()) + .arg("compile") + .arg("invalid.dag") + .current_dir(&root) + .output() + .expect("failed to run plain invalid single-file compile"); + assert!( + !plain.status.success(), + "plain invalid single-file compile should fail" + ); + + let dot_double_separator_curdir_segment_suffix = Command::new(daglang_bin()) + .arg("compile") + .arg(".//invalid.dag/./.") + .current_dir(&root) + .output() + .expect("failed to run dot-double-separator-curdir-segment-suffix invalid single-file compile"); + assert!( + !dot_double_separator_curdir_segment_suffix.status.success(), + "dot-double-separator-curdir-segment-suffix invalid single-file compile should fail" + ); + + assert_eq!( + plain.stdout, dot_double_separator_curdir_segment_suffix.stdout, + "plain and dot-double-separator-curdir-segment-suffix invalid single-file compile stdout should match" + ); + assert_eq!( + plain.stderr, dot_double_separator_curdir_segment_suffix.stderr, + "plain and dot-double-separator-curdir-segment-suffix invalid single-file compile stderr should match" + ); + let stderr = String::from_utf8_lossy(&plain.stderr); + assert!( + stderr.contains(&format!("failed to read {}", invalid_target.display())), + "dot-double-separator-curdir-segment-suffix invalid single-file diagnostic should include normalized absolute path: {stderr}" + ); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_dot_double_separator_curdir_suffix_invalid_single_file_target_matches_plain_relative_output( ) { @@ -3986,6 +4133,53 @@ fn compile_command_curdir_segment_trailing_slash_single_file_target_matches_plai std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_dot_double_separator_curdir_segment_suffix_single_file_target_matches_plain_relative_output( +) { + let root = + unique_temp_dir("compile_dot_double_separator_curdir_segment_suffix_single_file_target"); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp root"); + let source = root.join("sample/main.dag"); + std::fs::write(&source, "module sample.main\nfn run() -> Unit { }") + .expect("failed to write single-file source"); + + let plain = Command::new(daglang_bin()) + .arg("compile") + .arg("sample/main.dag") + .current_dir(&root) + .output() + .expect("failed to run plain single-file compile"); + assert!( + plain.status.success(), + "plain single-file compile should succeed: {}", + String::from_utf8_lossy(&plain.stderr) + ); + + let dot_double_separator_curdir_segment_suffix = Command::new(daglang_bin()) + .arg("compile") + .arg(".//sample/main.dag/./.") + .current_dir(&root) + .output() + .expect("failed to run dot-double-separator-curdir-segment-suffix single-file compile"); + assert!( + dot_double_separator_curdir_segment_suffix.status.success(), + "dot-double-separator-curdir-segment-suffix single-file compile should succeed: {}", + String::from_utf8_lossy(&dot_double_separator_curdir_segment_suffix.stderr) + ); + + assert_eq!( + plain.stdout, dot_double_separator_curdir_segment_suffix.stdout, + "plain and dot-double-separator-curdir-segment-suffix single-file compile stdout should match" + ); + assert_eq!( + plain.stderr, dot_double_separator_curdir_segment_suffix.stderr, + "plain and dot-double-separator-curdir-segment-suffix single-file compile stderr should match" + ); + assert_no_stage_failures(&String::from_utf8_lossy(&plain.stderr)); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_dot_double_separator_curdir_suffix_single_file_target_matches_plain_relative_output( ) { @@ -4357,6 +4551,56 @@ fn compile_command_curdir_segment_trailing_slash_non_directory_root_matches_plai std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_dot_double_separator_curdir_segment_suffix_non_directory_root_matches_plain_relative_output( +) { + let root = unique_temp_dir( + "compile_dot_double_separator_curdir_segment_suffix_non_directory_root", + ); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + let root_file = root.join("input.txt"); + std::fs::write(&root_file, "not a directory").expect("failed to write non-directory root"); + + let plain = Command::new(daglang_bin()) + .arg("compile") + .arg("input.txt") + .current_dir(&root) + .output() + .expect("failed to run plain non-directory-root compile"); + assert!( + !plain.status.success(), + "plain non-directory-root compile should fail" + ); + + let dot_double_separator_curdir_segment_suffix = Command::new(daglang_bin()) + .arg("compile") + .arg(".//input.txt/./.") + .current_dir(&root) + .output() + .expect("failed to run dot-double-separator-curdir-segment-suffix non-directory-root compile"); + assert!( + !dot_double_separator_curdir_segment_suffix.status.success(), + "dot-double-separator-curdir-segment-suffix non-directory-root compile should fail" + ); + + assert_eq!( + plain.stdout, dot_double_separator_curdir_segment_suffix.stdout, + "plain and dot-double-separator-curdir-segment-suffix non-directory-root compile stdout should match" + ); + assert_eq!( + plain.stderr, dot_double_separator_curdir_segment_suffix.stderr, + "plain and dot-double-separator-curdir-segment-suffix non-directory-root compile stderr should match" + ); + let stderr = String::from_utf8_lossy(&plain.stderr); + assert!( + stderr.contains(&root_file.display().to_string()), + "dot-double-separator-curdir-segment-suffix non-directory-root diagnostic should normalize to absolute path: {stderr}" + ); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_dot_double_separator_curdir_suffix_non_directory_root_matches_plain_relative_output( ) { @@ -6970,6 +7214,49 @@ fn compile_command_curdir_segment_trailing_slash_root_matches_plain_relative_out std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_dot_double_separator_curdir_segment_suffix_root_matches_plain_relative_output() { + let root = unique_temp_dir("compile_dot_double_separator_curdir_segment_suffix_root"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + write_minimal_directory_compile_fixture(&root); + + let plain = Command::new(daglang_bin()) + .arg("compile") + .arg("dsl") + .current_dir(&root) + .output() + .expect("failed to run plain-relative root compile"); + assert!( + plain.status.success(), + "plain-relative root compile should succeed: {}", + String::from_utf8_lossy(&plain.stderr) + ); + + let dot_double_separator_curdir_segment_suffix = Command::new(daglang_bin()) + .arg("compile") + .arg(".//dsl/./.") + .current_dir(&root) + .output() + .expect("failed to run dot-double-separator-curdir-segment-suffix root compile"); + assert!( + dot_double_separator_curdir_segment_suffix.status.success(), + "dot-double-separator-curdir-segment-suffix root compile should succeed: {}", + String::from_utf8_lossy(&dot_double_separator_curdir_segment_suffix.stderr) + ); + + assert_eq!( + plain.stdout, dot_double_separator_curdir_segment_suffix.stdout, + "plain-relative and dot-double-separator-curdir-segment-suffix root compile stdout should match" + ); + assert_eq!( + plain.stderr, dot_double_separator_curdir_segment_suffix.stderr, + "plain-relative and dot-double-separator-curdir-segment-suffix root compile stderr should match" + ); + assert_no_stage_failures(&String::from_utf8_lossy(&plain.stderr)); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_dot_double_separator_curdir_suffix_root_matches_plain_relative_output() { let root = unique_temp_dir("compile_dot_double_separator_curdir_suffix_root"); From efdde93022a3bc4482427d727de2892c77b10adb Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 14:06:35 +0000 Subject: [PATCH 148/169] Add compile dot-double curdir segment-trailing regressions Co-authored-by: Brian Searls --- .../daglang-cli/tests/compile_commands.rs | 302 ++++++++++++++++++ 1 file changed, 302 insertions(+) diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index a96d51e97f3..e323ca259c2 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -834,6 +834,56 @@ fn compile_command_curdir_segment_trailing_slash_missing_root_matches_plain_rela std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_dot_double_separator_curdir_segment_trailing_slash_missing_root_matches_plain_relative_output( +) { + let root = unique_temp_dir( + "compile_dot_double_separator_curdir_segment_trailing_slash_missing_root", + ); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + let missing = root.join("missing_root"); + + let plain = Command::new(daglang_bin()) + .arg("compile") + .arg("missing_root") + .current_dir(&root) + .output() + .expect("failed to run plain missing-root compile"); + assert!(!plain.status.success(), "plain missing-root compile should fail"); + + let dot_double_separator_curdir_segment_trailing_slash = Command::new(daglang_bin()) + .arg("compile") + .arg(".//missing_root/") + .current_dir(&root) + .output() + .expect( + "failed to run dot-double-separator-curdir-segment-trailing-slash missing-root compile", + ); + assert!( + !dot_double_separator_curdir_segment_trailing_slash.status.success(), + "dot-double-separator-curdir-segment-trailing-slash missing-root compile should fail" + ); + + assert_eq!( + plain.stdout, + dot_double_separator_curdir_segment_trailing_slash.stdout, + "plain and dot-double-separator-curdir-segment-trailing-slash missing-root compile stdout should match" + ); + assert_eq!( + plain.stderr, + dot_double_separator_curdir_segment_trailing_slash.stderr, + "plain and dot-double-separator-curdir-segment-trailing-slash missing-root compile stderr should match" + ); + let stderr = String::from_utf8_lossy(&plain.stderr); + assert!( + stderr.contains(&missing.display().to_string()), + "dot-double-separator-curdir-segment-trailing-slash missing-root diagnostic should normalize to absolute path: {stderr}" + ); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_dot_double_separator_curdir_segment_suffix_missing_root_matches_plain_relative_output( ) { @@ -1453,6 +1503,57 @@ fn compile_command_curdir_segment_trailing_slash_missing_single_file_target_matc std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_dot_double_separator_curdir_segment_trailing_slash_missing_single_file_target_matches_plain_relative_output( +) { + let root = unique_temp_dir( + "compile_dot_double_separator_curdir_segment_trailing_missing_single_file_target", + ); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + let missing = root.join("missing.dag"); + + let plain = Command::new(daglang_bin()) + .arg("compile") + .arg("missing.dag") + .current_dir(&root) + .output() + .expect("failed to run plain missing single-file compile"); + assert!( + !plain.status.success(), + "plain missing single-file compile should fail" + ); + + let dot_double_separator_curdir_segment_trailing_slash = Command::new(daglang_bin()) + .arg("compile") + .arg(".//missing.dag/") + .current_dir(&root) + .output() + .expect("failed to run dot-double-separator-curdir-segment-trailing-slash missing single-file compile"); + assert!( + !dot_double_separator_curdir_segment_trailing_slash.status.success(), + "dot-double-separator-curdir-segment-trailing-slash missing single-file compile should fail" + ); + + assert_eq!( + plain.stdout, + dot_double_separator_curdir_segment_trailing_slash.stdout, + "plain and dot-double-separator-curdir-segment-trailing-slash missing single-file compile stdout should match" + ); + assert_eq!( + plain.stderr, + dot_double_separator_curdir_segment_trailing_slash.stderr, + "plain and dot-double-separator-curdir-segment-trailing-slash missing single-file compile stderr should match" + ); + let stderr = String::from_utf8_lossy(&plain.stderr); + assert!( + stderr.contains(&format!("failed to read {}", missing.display())), + "dot-double-separator-curdir-segment-trailing-slash missing single-file diagnostic should normalize to absolute path: {stderr}" + ); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_dot_double_separator_curdir_segment_suffix_missing_single_file_target_matches_plain_relative_output( ) { @@ -1980,6 +2081,59 @@ fn compile_command_curdir_segment_trailing_slash_invalid_single_file_target_matc std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_dot_double_separator_curdir_segment_trailing_slash_invalid_single_file_target_matches_plain_relative_output( +) { + let root = unique_temp_dir( + "compile_dot_double_separator_curdir_segment_trailing_invalid_single_file_target", + ); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + let invalid_target = root.join("invalid.dag"); + std::fs::create_dir_all(&invalid_target) + .expect("failed to create invalid single-file target directory"); + + let plain = Command::new(daglang_bin()) + .arg("compile") + .arg("invalid.dag") + .current_dir(&root) + .output() + .expect("failed to run plain invalid single-file compile"); + assert!( + !plain.status.success(), + "plain invalid single-file compile should fail" + ); + + let dot_double_separator_curdir_segment_trailing_slash = Command::new(daglang_bin()) + .arg("compile") + .arg(".//invalid.dag/") + .current_dir(&root) + .output() + .expect("failed to run dot-double-separator-curdir-segment-trailing-slash invalid single-file compile"); + assert!( + !dot_double_separator_curdir_segment_trailing_slash.status.success(), + "dot-double-separator-curdir-segment-trailing-slash invalid single-file compile should fail" + ); + + assert_eq!( + plain.stdout, + dot_double_separator_curdir_segment_trailing_slash.stdout, + "plain and dot-double-separator-curdir-segment-trailing-slash invalid single-file compile stdout should match" + ); + assert_eq!( + plain.stderr, + dot_double_separator_curdir_segment_trailing_slash.stderr, + "plain and dot-double-separator-curdir-segment-trailing-slash invalid single-file compile stderr should match" + ); + let stderr = String::from_utf8_lossy(&plain.stderr); + assert!( + stderr.contains(&format!("failed to read {}", invalid_target.display())), + "dot-double-separator-curdir-segment-trailing-slash invalid single-file diagnostic should include normalized absolute path: {stderr}" + ); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_dot_double_separator_curdir_segment_suffix_invalid_single_file_target_matches_plain_relative_output( ) { @@ -4133,6 +4287,56 @@ fn compile_command_curdir_segment_trailing_slash_single_file_target_matches_plai std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_dot_double_separator_curdir_segment_trailing_slash_single_file_target_matches_plain_relative_output( +) { + let root = unique_temp_dir( + "compile_dot_double_separator_curdir_segment_trailing_single_file_target", + ); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp root"); + let source = root.join("sample/main.dag"); + std::fs::write(&source, "module sample.main\nfn run() -> Unit { }") + .expect("failed to write single-file source"); + + let plain = Command::new(daglang_bin()) + .arg("compile") + .arg("sample/main.dag") + .current_dir(&root) + .output() + .expect("failed to run plain single-file compile"); + assert!( + plain.status.success(), + "plain single-file compile should succeed: {}", + String::from_utf8_lossy(&plain.stderr) + ); + + let dot_double_separator_curdir_segment_trailing_slash = Command::new(daglang_bin()) + .arg("compile") + .arg(".//sample/main.dag/") + .current_dir(&root) + .output() + .expect("failed to run dot-double-separator-curdir-segment-trailing-slash single-file compile"); + assert!( + dot_double_separator_curdir_segment_trailing_slash.status.success(), + "dot-double-separator-curdir-segment-trailing-slash single-file compile should succeed: {}", + String::from_utf8_lossy(&dot_double_separator_curdir_segment_trailing_slash.stderr) + ); + + assert_eq!( + plain.stdout, + dot_double_separator_curdir_segment_trailing_slash.stdout, + "plain and dot-double-separator-curdir-segment-trailing-slash single-file compile stdout should match" + ); + assert_eq!( + plain.stderr, + dot_double_separator_curdir_segment_trailing_slash.stderr, + "plain and dot-double-separator-curdir-segment-trailing-slash single-file compile stderr should match" + ); + assert_no_stage_failures(&String::from_utf8_lossy(&plain.stderr)); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_dot_double_separator_curdir_segment_suffix_single_file_target_matches_plain_relative_output( ) { @@ -4551,6 +4755,58 @@ fn compile_command_curdir_segment_trailing_slash_non_directory_root_matches_plai std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_dot_double_separator_curdir_segment_trailing_slash_non_directory_root_matches_plain_relative_output( +) { + let root = unique_temp_dir( + "compile_dot_double_separator_curdir_segment_trailing_non_directory_root", + ); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + let root_file = root.join("input.txt"); + std::fs::write(&root_file, "not a directory").expect("failed to write non-directory root"); + + let plain = Command::new(daglang_bin()) + .arg("compile") + .arg("input.txt") + .current_dir(&root) + .output() + .expect("failed to run plain non-directory-root compile"); + assert!( + !plain.status.success(), + "plain non-directory-root compile should fail" + ); + + let dot_double_separator_curdir_segment_trailing_slash = Command::new(daglang_bin()) + .arg("compile") + .arg(".//input.txt/") + .current_dir(&root) + .output() + .expect("failed to run dot-double-separator-curdir-segment-trailing-slash non-directory-root compile"); + assert!( + !dot_double_separator_curdir_segment_trailing_slash.status.success(), + "dot-double-separator-curdir-segment-trailing-slash non-directory-root compile should fail" + ); + + assert_eq!( + plain.stdout, + dot_double_separator_curdir_segment_trailing_slash.stdout, + "plain and dot-double-separator-curdir-segment-trailing-slash non-directory-root compile stdout should match" + ); + assert_eq!( + plain.stderr, + dot_double_separator_curdir_segment_trailing_slash.stderr, + "plain and dot-double-separator-curdir-segment-trailing-slash non-directory-root compile stderr should match" + ); + let stderr = String::from_utf8_lossy(&plain.stderr); + assert!( + stderr.contains(&root_file.display().to_string()), + "dot-double-separator-curdir-segment-trailing-slash non-directory-root diagnostic should normalize to absolute path: {stderr}" + ); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_dot_double_separator_curdir_segment_suffix_non_directory_root_matches_plain_relative_output( ) { @@ -7214,6 +7470,52 @@ fn compile_command_curdir_segment_trailing_slash_root_matches_plain_relative_out std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_dot_double_separator_curdir_segment_trailing_slash_root_matches_plain_relative_output( +) { + let root = unique_temp_dir("compile_dot_double_separator_curdir_segment_trailing_root"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + write_minimal_directory_compile_fixture(&root); + + let plain = Command::new(daglang_bin()) + .arg("compile") + .arg("dsl") + .current_dir(&root) + .output() + .expect("failed to run plain-relative root compile"); + assert!( + plain.status.success(), + "plain-relative root compile should succeed: {}", + String::from_utf8_lossy(&plain.stderr) + ); + + let dot_double_separator_curdir_segment_trailing_slash = Command::new(daglang_bin()) + .arg("compile") + .arg(".//dsl/") + .current_dir(&root) + .output() + .expect("failed to run dot-double-separator-curdir-segment-trailing-slash root compile"); + assert!( + dot_double_separator_curdir_segment_trailing_slash.status.success(), + "dot-double-separator-curdir-segment-trailing-slash root compile should succeed: {}", + String::from_utf8_lossy(&dot_double_separator_curdir_segment_trailing_slash.stderr) + ); + + assert_eq!( + plain.stdout, + dot_double_separator_curdir_segment_trailing_slash.stdout, + "plain-relative and dot-double-separator-curdir-segment-trailing-slash root compile stdout should match" + ); + assert_eq!( + plain.stderr, + dot_double_separator_curdir_segment_trailing_slash.stderr, + "plain-relative and dot-double-separator-curdir-segment-trailing-slash root compile stderr should match" + ); + assert_no_stage_failures(&String::from_utf8_lossy(&plain.stderr)); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_dot_double_separator_curdir_segment_suffix_root_matches_plain_relative_output() { let root = unique_temp_dir("compile_dot_double_separator_curdir_segment_suffix_root"); From 819f4dbbdd7a8016796e59430e4b58f44593ee37 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 14:11:03 +0000 Subject: [PATCH 149/169] Add compile dot-double curdir segment-double regressions Co-authored-by: Brian Searls --- .../daglang-cli/tests/compile_commands.rs | 299 ++++++++++++++++++ 1 file changed, 299 insertions(+) diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index e323ca259c2..ed7bfd3ddd0 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -1189,6 +1189,53 @@ fn compile_command_curdir_segment_double_separator_missing_root_matches_plain_re std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_dot_double_separator_curdir_segment_double_separator_missing_root_matches_plain_relative_output( +) { + let root = + unique_temp_dir("compile_dot_double_separator_curdir_segment_double_separator_missing_root"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + let missing = root.join("missing_root"); + + let plain = Command::new(daglang_bin()) + .arg("compile") + .arg("missing_root") + .current_dir(&root) + .output() + .expect("failed to run plain missing-root compile"); + assert!(!plain.status.success(), "plain missing-root compile should fail"); + + let dot_double_separator_curdir_segment_double_separator = Command::new(daglang_bin()) + .arg("compile") + .arg(".//missing_root//") + .current_dir(&root) + .output() + .expect("failed to run dot-double-separator-curdir-segment-double-separator missing-root compile"); + assert!( + !dot_double_separator_curdir_segment_double_separator.status.success(), + "dot-double-separator-curdir-segment-double-separator missing-root compile should fail" + ); + + assert_eq!( + plain.stdout, + dot_double_separator_curdir_segment_double_separator.stdout, + "plain and dot-double-separator-curdir-segment-double-separator missing-root compile stdout should match" + ); + assert_eq!( + plain.stderr, + dot_double_separator_curdir_segment_double_separator.stderr, + "plain and dot-double-separator-curdir-segment-double-separator missing-root compile stderr should match" + ); + let stderr = String::from_utf8_lossy(&plain.stderr); + assert!( + stderr.contains(&missing.display().to_string()), + "dot-double-separator-curdir-segment-double-separator missing-root diagnostic should normalize to absolute path: {stderr}" + ); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_curdir_double_separator_missing_root_matches_plain_relative_output() { let root = unique_temp_dir("compile_curdir_double_separator_missing_root"); @@ -1888,6 +1935,57 @@ fn compile_command_curdir_segment_double_separator_missing_single_file_target_ma std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_dot_double_separator_curdir_segment_double_separator_missing_single_file_target_matches_plain_relative_output( +) { + let root = unique_temp_dir( + "compile_dot_double_separator_curdir_segment_double_separator_missing_single_file_target", + ); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + let missing = root.join("missing.dag"); + + let plain = Command::new(daglang_bin()) + .arg("compile") + .arg("missing.dag") + .current_dir(&root) + .output() + .expect("failed to run plain missing single-file compile"); + assert!( + !plain.status.success(), + "plain missing single-file compile should fail" + ); + + let dot_double_separator_curdir_segment_double_separator = Command::new(daglang_bin()) + .arg("compile") + .arg(".//missing.dag//") + .current_dir(&root) + .output() + .expect("failed to run dot-double-separator-curdir-segment-double-separator missing single-file compile"); + assert!( + !dot_double_separator_curdir_segment_double_separator.status.success(), + "dot-double-separator-curdir-segment-double-separator missing single-file compile should fail" + ); + + assert_eq!( + plain.stdout, + dot_double_separator_curdir_segment_double_separator.stdout, + "plain and dot-double-separator-curdir-segment-double-separator missing single-file compile stdout should match" + ); + assert_eq!( + plain.stderr, + dot_double_separator_curdir_segment_double_separator.stderr, + "plain and dot-double-separator-curdir-segment-double-separator missing single-file compile stderr should match" + ); + let stderr = String::from_utf8_lossy(&plain.stderr); + assert!( + stderr.contains(&format!("failed to read {}", missing.display())), + "dot-double-separator-curdir-segment-double-separator missing single-file diagnostic should normalize to absolute path: {stderr}" + ); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_relative_and_absolute_invalid_single_file_targets_are_equivalent() { let root = unique_temp_dir("compile_relative_absolute_invalid_single_file_target"); @@ -2482,6 +2580,59 @@ fn compile_command_curdir_segment_double_separator_invalid_single_file_target_ma std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_dot_double_separator_curdir_segment_double_separator_invalid_single_file_target_matches_plain_relative_output( +) { + let root = unique_temp_dir( + "compile_dot_double_separator_curdir_segment_double_separator_invalid_single_file_target", + ); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + let invalid_target = root.join("invalid.dag"); + std::fs::create_dir_all(&invalid_target) + .expect("failed to create invalid single-file target directory"); + + let plain = Command::new(daglang_bin()) + .arg("compile") + .arg("invalid.dag") + .current_dir(&root) + .output() + .expect("failed to run plain invalid single-file compile"); + assert!( + !plain.status.success(), + "plain invalid single-file compile should fail" + ); + + let dot_double_separator_curdir_segment_double_separator = Command::new(daglang_bin()) + .arg("compile") + .arg(".//invalid.dag//") + .current_dir(&root) + .output() + .expect("failed to run dot-double-separator-curdir-segment-double-separator invalid single-file compile"); + assert!( + !dot_double_separator_curdir_segment_double_separator.status.success(), + "dot-double-separator-curdir-segment-double-separator invalid single-file compile should fail" + ); + + assert_eq!( + plain.stdout, + dot_double_separator_curdir_segment_double_separator.stdout, + "plain and dot-double-separator-curdir-segment-double-separator invalid single-file compile stdout should match" + ); + assert_eq!( + plain.stderr, + dot_double_separator_curdir_segment_double_separator.stderr, + "plain and dot-double-separator-curdir-segment-double-separator invalid single-file compile stderr should match" + ); + let stderr = String::from_utf8_lossy(&plain.stderr); + assert!( + stderr.contains(&format!("failed to read {}", invalid_target.display())), + "dot-double-separator-curdir-segment-double-separator invalid single-file diagnostic should include normalized absolute path: {stderr}" + ); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_curdir_double_separator_invalid_single_file_target_matches_plain_relative_output( ) { @@ -4660,6 +4811,56 @@ fn compile_command_curdir_segment_double_separator_single_file_target_matches_pl std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_dot_double_separator_curdir_segment_double_separator_single_file_target_matches_plain_relative_output( +) { + let root = unique_temp_dir( + "compile_dot_double_separator_curdir_segment_double_separator_single_file_target", + ); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp root"); + let source = root.join("sample/main.dag"); + std::fs::write(&source, "module sample.main\nfn run() -> Unit { }") + .expect("failed to write single-file source"); + + let plain = Command::new(daglang_bin()) + .arg("compile") + .arg("sample/main.dag") + .current_dir(&root) + .output() + .expect("failed to run plain single-file compile"); + assert!( + plain.status.success(), + "plain single-file compile should succeed: {}", + String::from_utf8_lossy(&plain.stderr) + ); + + let dot_double_separator_curdir_segment_double_separator = Command::new(daglang_bin()) + .arg("compile") + .arg(".//sample/main.dag//") + .current_dir(&root) + .output() + .expect("failed to run dot-double-separator-curdir-segment-double-separator single-file compile"); + assert!( + dot_double_separator_curdir_segment_double_separator.status.success(), + "dot-double-separator-curdir-segment-double-separator single-file compile should succeed: {}", + String::from_utf8_lossy(&dot_double_separator_curdir_segment_double_separator.stderr) + ); + + assert_eq!( + plain.stdout, + dot_double_separator_curdir_segment_double_separator.stdout, + "plain and dot-double-separator-curdir-segment-double-separator single-file compile stdout should match" + ); + assert_eq!( + plain.stderr, + dot_double_separator_curdir_segment_double_separator.stderr, + "plain and dot-double-separator-curdir-segment-double-separator single-file compile stderr should match" + ); + assert_no_stage_failures(&String::from_utf8_lossy(&plain.stderr)); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_curdir_segment_double_separator_non_directory_root_matches_plain_relative_output() { @@ -4708,6 +4909,58 @@ fn compile_command_curdir_segment_double_separator_non_directory_root_matches_pl std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_dot_double_separator_curdir_segment_double_separator_non_directory_root_matches_plain_relative_output( +) { + let root = unique_temp_dir( + "compile_dot_double_separator_curdir_segment_double_separator_non_directory_root", + ); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + let root_file = root.join("input.txt"); + std::fs::write(&root_file, "not a directory").expect("failed to write non-directory root"); + + let plain = Command::new(daglang_bin()) + .arg("compile") + .arg("input.txt") + .current_dir(&root) + .output() + .expect("failed to run plain non-directory-root compile"); + assert!( + !plain.status.success(), + "plain non-directory-root compile should fail" + ); + + let dot_double_separator_curdir_segment_double_separator = Command::new(daglang_bin()) + .arg("compile") + .arg(".//input.txt//") + .current_dir(&root) + .output() + .expect("failed to run dot-double-separator-curdir-segment-double-separator non-directory-root compile"); + assert!( + !dot_double_separator_curdir_segment_double_separator.status.success(), + "dot-double-separator-curdir-segment-double-separator non-directory-root compile should fail" + ); + + assert_eq!( + plain.stdout, + dot_double_separator_curdir_segment_double_separator.stdout, + "plain and dot-double-separator-curdir-segment-double-separator non-directory-root compile stdout should match" + ); + assert_eq!( + plain.stderr, + dot_double_separator_curdir_segment_double_separator.stderr, + "plain and dot-double-separator-curdir-segment-double-separator non-directory-root compile stderr should match" + ); + let stderr = String::from_utf8_lossy(&plain.stderr); + assert!( + stderr.contains(&root_file.display().to_string()), + "dot-double-separator-curdir-segment-double-separator non-directory-root diagnostic should normalize to absolute path: {stderr}" + ); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_curdir_segment_trailing_slash_non_directory_root_matches_plain_relative_output() { let root = unique_temp_dir("compile_curdir_segment_trailing_non_directory_root"); @@ -7817,6 +8070,52 @@ fn compile_command_curdir_segment_double_separator_root_matches_plain_relative_o std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_dot_double_separator_curdir_segment_double_separator_root_matches_plain_relative_output( +) { + let root = unique_temp_dir("compile_dot_double_separator_curdir_segment_double_separator_root"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + write_minimal_directory_compile_fixture(&root); + + let plain = Command::new(daglang_bin()) + .arg("compile") + .arg("dsl") + .current_dir(&root) + .output() + .expect("failed to run plain-relative root compile"); + assert!( + plain.status.success(), + "plain-relative root compile should succeed: {}", + String::from_utf8_lossy(&plain.stderr) + ); + + let dot_double_separator_curdir_segment_double_separator = Command::new(daglang_bin()) + .arg("compile") + .arg(".//dsl//") + .current_dir(&root) + .output() + .expect("failed to run dot-double-separator-curdir-segment-double-separator root compile"); + assert!( + dot_double_separator_curdir_segment_double_separator.status.success(), + "dot-double-separator-curdir-segment-double-separator root compile should succeed: {}", + String::from_utf8_lossy(&dot_double_separator_curdir_segment_double_separator.stderr) + ); + + assert_eq!( + plain.stdout, + dot_double_separator_curdir_segment_double_separator.stdout, + "plain-relative and dot-double-separator-curdir-segment-double-separator root compile stdout should match" + ); + assert_eq!( + plain.stderr, + dot_double_separator_curdir_segment_double_separator.stderr, + "plain-relative and dot-double-separator-curdir-segment-double-separator root compile stderr should match" + ); + assert_no_stage_failures(&String::from_utf8_lossy(&plain.stderr)); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_curdir_double_separator_root_matches_plain_relative_output() { let root = unique_temp_dir("compile_curdir_double_separator_root"); From 2e0e41fa61007672fe5592171a72f30d3b678aa8 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 14:16:47 +0000 Subject: [PATCH 150/169] Add compile dot-double curdir segment-double-suffix regressions Co-authored-by: Brian Searls --- .../daglang-cli/tests/compile_commands.rs | 313 ++++++++++++++++++ 1 file changed, 313 insertions(+) diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index ed7bfd3ddd0..a7e908f02e7 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -1146,6 +1146,56 @@ fn compile_command_curdir_segment_double_separator_suffix_missing_root_matches_p std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_dot_double_separator_curdir_segment_double_separator_suffix_missing_root_matches_plain_relative_output( +) { + let root = unique_temp_dir( + "compile_dot_double_separator_curdir_segment_double_separator_suffix_missing_root", + ); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + let missing = root.join("missing_root"); + + let plain = Command::new(daglang_bin()) + .arg("compile") + .arg("missing_root") + .current_dir(&root) + .output() + .expect("failed to run plain missing-root compile"); + assert!(!plain.status.success(), "plain missing-root compile should fail"); + + let dot_double_separator_curdir_segment_double_separator_suffix = Command::new(daglang_bin()) + .arg("compile") + .arg(".//missing_root//./.") + .current_dir(&root) + .output() + .expect("failed to run dot-double-separator-curdir-segment-double-separator-suffix missing-root compile"); + assert!( + !dot_double_separator_curdir_segment_double_separator_suffix + .status + .success(), + "dot-double-separator-curdir-segment-double-separator-suffix missing-root compile should fail" + ); + + assert_eq!( + plain.stdout, + dot_double_separator_curdir_segment_double_separator_suffix.stdout, + "plain and dot-double-separator-curdir-segment-double-separator-suffix missing-root compile stdout should match" + ); + assert_eq!( + plain.stderr, + dot_double_separator_curdir_segment_double_separator_suffix.stderr, + "plain and dot-double-separator-curdir-segment-double-separator-suffix missing-root compile stderr should match" + ); + let stderr = String::from_utf8_lossy(&plain.stderr); + assert!( + stderr.contains(&missing.display().to_string()), + "dot-double-separator-curdir-segment-double-separator-suffix missing-root diagnostic should normalize to absolute path: {stderr}" + ); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_curdir_segment_double_separator_missing_root_matches_plain_relative_output() { let root = unique_temp_dir("compile_curdir_segment_double_separator_missing_root"); @@ -1888,6 +1938,59 @@ fn compile_command_curdir_segment_double_separator_suffix_missing_single_file_ta std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_dot_double_separator_curdir_segment_double_separator_suffix_missing_single_file_target_matches_plain_relative_output( +) { + let root = unique_temp_dir( + "compile_dot_double_separator_curdir_segment_double_separator_suffix_missing_single_file_target", + ); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + let missing = root.join("missing.dag"); + + let plain = Command::new(daglang_bin()) + .arg("compile") + .arg("missing.dag") + .current_dir(&root) + .output() + .expect("failed to run plain missing single-file compile"); + assert!( + !plain.status.success(), + "plain missing single-file compile should fail" + ); + + let dot_double_separator_curdir_segment_double_separator_suffix = Command::new(daglang_bin()) + .arg("compile") + .arg(".//missing.dag//./.") + .current_dir(&root) + .output() + .expect("failed to run dot-double-separator-curdir-segment-double-separator-suffix missing single-file compile"); + assert!( + !dot_double_separator_curdir_segment_double_separator_suffix + .status + .success(), + "dot-double-separator-curdir-segment-double-separator-suffix missing single-file compile should fail" + ); + + assert_eq!( + plain.stdout, + dot_double_separator_curdir_segment_double_separator_suffix.stdout, + "plain and dot-double-separator-curdir-segment-double-separator-suffix missing single-file compile stdout should match" + ); + assert_eq!( + plain.stderr, + dot_double_separator_curdir_segment_double_separator_suffix.stderr, + "plain and dot-double-separator-curdir-segment-double-separator-suffix missing single-file compile stderr should match" + ); + let stderr = String::from_utf8_lossy(&plain.stderr); + assert!( + stderr.contains(&format!("failed to read {}", missing.display())), + "dot-double-separator-curdir-segment-double-separator-suffix missing single-file diagnostic should normalize to absolute path: {stderr}" + ); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_curdir_segment_double_separator_missing_single_file_target_matches_plain_relative_output( ) { @@ -2531,6 +2634,61 @@ fn compile_command_curdir_segment_double_separator_suffix_invalid_single_file_ta std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_dot_double_separator_curdir_segment_double_separator_suffix_invalid_single_file_target_matches_plain_relative_output( +) { + let root = unique_temp_dir( + "compile_dot_double_separator_curdir_segment_double_separator_suffix_invalid_single_file_target", + ); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + let invalid_target = root.join("invalid.dag"); + std::fs::create_dir_all(&invalid_target) + .expect("failed to create invalid single-file target directory"); + + let plain = Command::new(daglang_bin()) + .arg("compile") + .arg("invalid.dag") + .current_dir(&root) + .output() + .expect("failed to run plain invalid single-file compile"); + assert!( + !plain.status.success(), + "plain invalid single-file compile should fail" + ); + + let dot_double_separator_curdir_segment_double_separator_suffix = Command::new(daglang_bin()) + .arg("compile") + .arg(".//invalid.dag//./.") + .current_dir(&root) + .output() + .expect("failed to run dot-double-separator-curdir-segment-double-separator-suffix invalid single-file compile"); + assert!( + !dot_double_separator_curdir_segment_double_separator_suffix + .status + .success(), + "dot-double-separator-curdir-segment-double-separator-suffix invalid single-file compile should fail" + ); + + assert_eq!( + plain.stdout, + dot_double_separator_curdir_segment_double_separator_suffix.stdout, + "plain and dot-double-separator-curdir-segment-double-separator-suffix invalid single-file compile stdout should match" + ); + assert_eq!( + plain.stderr, + dot_double_separator_curdir_segment_double_separator_suffix.stderr, + "plain and dot-double-separator-curdir-segment-double-separator-suffix invalid single-file compile stderr should match" + ); + let stderr = String::from_utf8_lossy(&plain.stderr); + assert!( + stderr.contains(&format!("failed to read {}", invalid_target.display())), + "dot-double-separator-curdir-segment-double-separator-suffix invalid single-file diagnostic should include normalized absolute path: {stderr}" + ); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_curdir_segment_double_separator_invalid_single_file_target_matches_plain_relative_output( ) { @@ -4765,6 +4923,58 @@ fn compile_command_curdir_segment_double_separator_suffix_single_file_target_mat std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_dot_double_separator_curdir_segment_double_separator_suffix_single_file_target_matches_plain_relative_output( +) { + let root = unique_temp_dir( + "compile_dot_double_separator_curdir_segment_double_separator_suffix_single_file_target", + ); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp root"); + let source = root.join("sample/main.dag"); + std::fs::write(&source, "module sample.main\nfn run() -> Unit { }") + .expect("failed to write single-file source"); + + let plain = Command::new(daglang_bin()) + .arg("compile") + .arg("sample/main.dag") + .current_dir(&root) + .output() + .expect("failed to run plain single-file compile"); + assert!( + plain.status.success(), + "plain single-file compile should succeed: {}", + String::from_utf8_lossy(&plain.stderr) + ); + + let dot_double_separator_curdir_segment_double_separator_suffix = Command::new(daglang_bin()) + .arg("compile") + .arg(".//sample/main.dag//./.") + .current_dir(&root) + .output() + .expect("failed to run dot-double-separator-curdir-segment-double-separator-suffix single-file compile"); + assert!( + dot_double_separator_curdir_segment_double_separator_suffix + .status + .success(), + "dot-double-separator-curdir-segment-double-separator-suffix single-file compile should succeed: {}", + String::from_utf8_lossy(&dot_double_separator_curdir_segment_double_separator_suffix.stderr) + ); + + assert_eq!( + plain.stdout, + dot_double_separator_curdir_segment_double_separator_suffix.stdout, + "plain and dot-double-separator-curdir-segment-double-separator-suffix single-file compile stdout should match" + ); + assert_eq!( + plain.stderr, + dot_double_separator_curdir_segment_double_separator_suffix.stderr, + "plain and dot-double-separator-curdir-segment-double-separator-suffix single-file compile stderr should match" + ); + assert_no_stage_failures(&String::from_utf8_lossy(&plain.stderr)); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_curdir_segment_double_separator_single_file_target_matches_plain_relative_output() { @@ -4961,6 +5171,60 @@ fn compile_command_dot_double_separator_curdir_segment_double_separator_non_dire std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_dot_double_separator_curdir_segment_double_separator_suffix_non_directory_root_matches_plain_relative_output( +) { + let root = unique_temp_dir( + "compile_dot_double_separator_curdir_segment_double_separator_suffix_non_directory_root", + ); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + let root_file = root.join("input.txt"); + std::fs::write(&root_file, "not a directory").expect("failed to write non-directory root"); + + let plain = Command::new(daglang_bin()) + .arg("compile") + .arg("input.txt") + .current_dir(&root) + .output() + .expect("failed to run plain non-directory-root compile"); + assert!( + !plain.status.success(), + "plain non-directory-root compile should fail" + ); + + let dot_double_separator_curdir_segment_double_separator_suffix = Command::new(daglang_bin()) + .arg("compile") + .arg(".//input.txt//./.") + .current_dir(&root) + .output() + .expect("failed to run dot-double-separator-curdir-segment-double-separator-suffix non-directory-root compile"); + assert!( + !dot_double_separator_curdir_segment_double_separator_suffix + .status + .success(), + "dot-double-separator-curdir-segment-double-separator-suffix non-directory-root compile should fail" + ); + + assert_eq!( + plain.stdout, + dot_double_separator_curdir_segment_double_separator_suffix.stdout, + "plain and dot-double-separator-curdir-segment-double-separator-suffix non-directory-root compile stdout should match" + ); + assert_eq!( + plain.stderr, + dot_double_separator_curdir_segment_double_separator_suffix.stderr, + "plain and dot-double-separator-curdir-segment-double-separator-suffix non-directory-root compile stderr should match" + ); + let stderr = String::from_utf8_lossy(&plain.stderr); + assert!( + stderr.contains(&root_file.display().to_string()), + "dot-double-separator-curdir-segment-double-separator-suffix non-directory-root diagnostic should normalize to absolute path: {stderr}" + ); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_curdir_segment_trailing_slash_non_directory_root_matches_plain_relative_output() { let root = unique_temp_dir("compile_curdir_segment_trailing_non_directory_root"); @@ -8027,6 +8291,55 @@ fn compile_command_curdir_segment_double_separator_suffix_root_matches_plain_rel std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_dot_double_separator_curdir_segment_double_separator_suffix_root_matches_plain_relative_output( +) { + let root = + unique_temp_dir("compile_dot_double_separator_curdir_segment_double_separator_suffix_root"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + write_minimal_directory_compile_fixture(&root); + + let plain = Command::new(daglang_bin()) + .arg("compile") + .arg("dsl") + .current_dir(&root) + .output() + .expect("failed to run plain-relative root compile"); + assert!( + plain.status.success(), + "plain-relative root compile should succeed: {}", + String::from_utf8_lossy(&plain.stderr) + ); + + let dot_double_separator_curdir_segment_double_separator_suffix = Command::new(daglang_bin()) + .arg("compile") + .arg(".//dsl//./.") + .current_dir(&root) + .output() + .expect("failed to run dot-double-separator-curdir-segment-double-separator-suffix root compile"); + assert!( + dot_double_separator_curdir_segment_double_separator_suffix + .status + .success(), + "dot-double-separator-curdir-segment-double-separator-suffix root compile should succeed: {}", + String::from_utf8_lossy(&dot_double_separator_curdir_segment_double_separator_suffix.stderr) + ); + + assert_eq!( + plain.stdout, + dot_double_separator_curdir_segment_double_separator_suffix.stdout, + "plain-relative and dot-double-separator-curdir-segment-double-separator-suffix root compile stdout should match" + ); + assert_eq!( + plain.stderr, + dot_double_separator_curdir_segment_double_separator_suffix.stderr, + "plain-relative and dot-double-separator-curdir-segment-double-separator-suffix root compile stderr should match" + ); + assert_no_stage_failures(&String::from_utf8_lossy(&plain.stderr)); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_curdir_segment_double_separator_root_matches_plain_relative_output() { let root = unique_temp_dir("compile_curdir_segment_double_separator_root"); From b1c76a3664603bd09e8c63640b536e21676ae0ef Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 14:23:01 +0000 Subject: [PATCH 151/169] Add compile dot-double curdir suffix-double regressions Co-authored-by: Brian Searls --- .../daglang-cli/tests/compile_commands.rs | 297 ++++++++++++++++++ 1 file changed, 297 insertions(+) diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index a7e908f02e7..8eb42e8a2a3 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -8340,6 +8340,303 @@ fn compile_command_dot_double_separator_curdir_segment_double_separator_suffix_r std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_dot_double_separator_curdir_suffix_double_separator_root_matches_plain_relative_output( +) { + let root = unique_temp_dir("compile_dot_double_separator_curdir_suffix_double_separator_root"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + write_minimal_directory_compile_fixture(&root); + + let plain = Command::new(daglang_bin()) + .arg("compile") + .arg("dsl") + .current_dir(&root) + .output() + .expect("failed to run plain-relative root compile"); + assert!( + plain.status.success(), + "plain-relative root compile should succeed: {}", + String::from_utf8_lossy(&plain.stderr) + ); + + let dot_double_separator_curdir_suffix_double_separator = Command::new(daglang_bin()) + .arg("compile") + .arg(".//dsl//.") + .current_dir(&root) + .output() + .expect("failed to run dot-double-separator-curdir-suffix-double-separator root compile"); + assert!( + dot_double_separator_curdir_suffix_double_separator + .status + .success(), + "dot-double-separator-curdir-suffix-double-separator root compile should succeed: {}", + String::from_utf8_lossy(&dot_double_separator_curdir_suffix_double_separator.stderr) + ); + + assert_eq!( + plain.stdout, dot_double_separator_curdir_suffix_double_separator.stdout, + "plain-relative and dot-double-separator-curdir-suffix-double-separator root compile stdout should match" + ); + assert_eq!( + plain.stderr, dot_double_separator_curdir_suffix_double_separator.stderr, + "plain-relative and dot-double-separator-curdir-suffix-double-separator root compile stderr should match" + ); + assert_no_stage_failures(&String::from_utf8_lossy(&plain.stderr)); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + +#[test] +fn compile_command_dot_double_separator_curdir_suffix_double_separator_missing_root_matches_plain_relative_output( +) { + let root = unique_temp_dir("compile_dot_double_separator_curdir_suffix_double_separator_missing_root"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + let missing = root.join("missing_root"); + + let plain = Command::new(daglang_bin()) + .arg("compile") + .arg("missing_root") + .current_dir(&root) + .output() + .expect("failed to run plain missing-root compile"); + assert!(!plain.status.success(), "plain missing-root compile should fail"); + + let dot_double_separator_curdir_suffix_double_separator = Command::new(daglang_bin()) + .arg("compile") + .arg(".//missing_root//.") + .current_dir(&root) + .output() + .expect("failed to run dot-double-separator-curdir-suffix-double-separator missing-root compile"); + assert!( + !dot_double_separator_curdir_suffix_double_separator + .status + .success(), + "dot-double-separator-curdir-suffix-double-separator missing-root compile should fail" + ); + + assert_eq!( + plain.stdout, dot_double_separator_curdir_suffix_double_separator.stdout, + "plain and dot-double-separator-curdir-suffix-double-separator missing-root compile stdout should match" + ); + assert_eq!( + plain.stderr, dot_double_separator_curdir_suffix_double_separator.stderr, + "plain and dot-double-separator-curdir-suffix-double-separator missing-root compile stderr should match" + ); + let stderr = String::from_utf8_lossy(&plain.stderr); + assert!( + stderr.contains(&missing.display().to_string()), + "dot-double-separator-curdir-suffix-double-separator missing-root diagnostic should normalize to absolute path: {stderr}" + ); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + +#[test] +fn compile_command_dot_double_separator_curdir_suffix_double_separator_non_directory_root_matches_plain_relative_output( +) { + let root = unique_temp_dir( + "compile_dot_double_separator_curdir_suffix_double_separator_non_directory_root", + ); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + let root_file = root.join("input.txt"); + std::fs::write(&root_file, "not a directory").expect("failed to write non-directory root"); + + let plain = Command::new(daglang_bin()) + .arg("compile") + .arg("input.txt") + .current_dir(&root) + .output() + .expect("failed to run plain non-directory-root compile"); + assert!( + !plain.status.success(), + "plain non-directory-root compile should fail" + ); + + let dot_double_separator_curdir_suffix_double_separator = Command::new(daglang_bin()) + .arg("compile") + .arg(".//input.txt//.") + .current_dir(&root) + .output() + .expect("failed to run dot-double-separator-curdir-suffix-double-separator non-directory-root compile"); + assert!( + !dot_double_separator_curdir_suffix_double_separator + .status + .success(), + "dot-double-separator-curdir-suffix-double-separator non-directory-root compile should fail" + ); + + assert_eq!( + plain.stdout, dot_double_separator_curdir_suffix_double_separator.stdout, + "plain and dot-double-separator-curdir-suffix-double-separator non-directory-root compile stdout should match" + ); + assert_eq!( + plain.stderr, dot_double_separator_curdir_suffix_double_separator.stderr, + "plain and dot-double-separator-curdir-suffix-double-separator non-directory-root compile stderr should match" + ); + let stderr = String::from_utf8_lossy(&plain.stderr); + assert!( + stderr.contains(&root_file.display().to_string()), + "dot-double-separator-curdir-suffix-double-separator non-directory-root diagnostic should normalize to absolute path: {stderr}" + ); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + +#[test] +fn compile_command_dot_double_separator_curdir_suffix_double_separator_single_file_target_matches_plain_relative_output( +) { + let root = + unique_temp_dir("compile_dot_double_separator_curdir_suffix_double_separator_single_file"); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp root"); + let source = root.join("sample/main.dag"); + std::fs::write(&source, "module sample.main\nfn run() -> Unit { }") + .expect("failed to write single-file source"); + + let plain = Command::new(daglang_bin()) + .arg("compile") + .arg("sample/main.dag") + .current_dir(&root) + .output() + .expect("failed to run plain single-file compile"); + assert!( + plain.status.success(), + "plain single-file compile should succeed: {}", + String::from_utf8_lossy(&plain.stderr) + ); + + let dot_double_separator_curdir_suffix_double_separator = Command::new(daglang_bin()) + .arg("compile") + .arg(".//sample/main.dag//.") + .current_dir(&root) + .output() + .expect("failed to run dot-double-separator-curdir-suffix-double-separator single-file compile"); + assert!( + dot_double_separator_curdir_suffix_double_separator + .status + .success(), + "dot-double-separator-curdir-suffix-double-separator single-file compile should succeed: {}", + String::from_utf8_lossy(&dot_double_separator_curdir_suffix_double_separator.stderr) + ); + + assert_eq!( + plain.stdout, dot_double_separator_curdir_suffix_double_separator.stdout, + "plain and dot-double-separator-curdir-suffix-double-separator single-file compile stdout should match" + ); + assert_eq!( + plain.stderr, dot_double_separator_curdir_suffix_double_separator.stderr, + "plain and dot-double-separator-curdir-suffix-double-separator single-file compile stderr should match" + ); + assert_no_stage_failures(&String::from_utf8_lossy(&plain.stderr)); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + +#[test] +fn compile_command_dot_double_separator_curdir_suffix_double_separator_missing_single_file_target_matches_plain_relative_output( +) { + let root = unique_temp_dir( + "compile_dot_double_separator_curdir_suffix_double_separator_missing_single_file_target", + ); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + let missing = root.join("missing.dag"); + + let plain = Command::new(daglang_bin()) + .arg("compile") + .arg("missing.dag") + .current_dir(&root) + .output() + .expect("failed to run plain missing single-file compile"); + assert!( + !plain.status.success(), + "plain missing single-file compile should fail" + ); + + let dot_double_separator_curdir_suffix_double_separator = Command::new(daglang_bin()) + .arg("compile") + .arg(".//missing.dag//.") + .current_dir(&root) + .output() + .expect("failed to run dot-double-separator-curdir-suffix-double-separator missing single-file compile"); + assert!( + !dot_double_separator_curdir_suffix_double_separator + .status + .success(), + "dot-double-separator-curdir-suffix-double-separator missing single-file compile should fail" + ); + + assert_eq!( + plain.stdout, dot_double_separator_curdir_suffix_double_separator.stdout, + "plain and dot-double-separator-curdir-suffix-double-separator missing single-file compile stdout should match" + ); + assert_eq!( + plain.stderr, dot_double_separator_curdir_suffix_double_separator.stderr, + "plain and dot-double-separator-curdir-suffix-double-separator missing single-file compile stderr should match" + ); + let stderr = String::from_utf8_lossy(&plain.stderr); + assert!( + stderr.contains(&format!("failed to read {}", missing.display())), + "dot-double-separator-curdir-suffix-double-separator missing single-file diagnostic should normalize to absolute path: {stderr}" + ); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + +#[test] +fn compile_command_dot_double_separator_curdir_suffix_double_separator_invalid_single_file_target_matches_plain_relative_output( +) { + let root = unique_temp_dir( + "compile_dot_double_separator_curdir_suffix_double_separator_invalid_single_file_target", + ); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + let invalid_target = root.join("invalid.dag"); + std::fs::create_dir_all(&invalid_target) + .expect("failed to create invalid single-file target directory"); + + let plain = Command::new(daglang_bin()) + .arg("compile") + .arg("invalid.dag") + .current_dir(&root) + .output() + .expect("failed to run plain invalid single-file compile"); + assert!( + !plain.status.success(), + "plain invalid single-file compile should fail" + ); + + let dot_double_separator_curdir_suffix_double_separator = Command::new(daglang_bin()) + .arg("compile") + .arg(".//invalid.dag//.") + .current_dir(&root) + .output() + .expect("failed to run dot-double-separator-curdir-suffix-double-separator invalid single-file compile"); + assert!( + !dot_double_separator_curdir_suffix_double_separator + .status + .success(), + "dot-double-separator-curdir-suffix-double-separator invalid single-file compile should fail" + ); + + assert_eq!( + plain.stdout, dot_double_separator_curdir_suffix_double_separator.stdout, + "plain and dot-double-separator-curdir-suffix-double-separator invalid single-file compile stdout should match" + ); + assert_eq!( + plain.stderr, dot_double_separator_curdir_suffix_double_separator.stderr, + "plain and dot-double-separator-curdir-suffix-double-separator invalid single-file compile stderr should match" + ); + let stderr = String::from_utf8_lossy(&plain.stderr); + assert!( + stderr.contains(&format!("failed to read {}", invalid_target.display())), + "dot-double-separator-curdir-suffix-double-separator invalid single-file diagnostic should include normalized absolute path: {stderr}" + ); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_curdir_segment_double_separator_root_matches_plain_relative_output() { let root = unique_temp_dir("compile_curdir_segment_double_separator_root"); From be30977d7c611edd0e831f8d49c17ea2c14abd4a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 14:27:07 +0000 Subject: [PATCH 152/169] Add compile dot-double double and trailing regressions Co-authored-by: Brian Searls --- .../daglang-cli/tests/compile_commands.rs | 410 ++++++++++++++++++ 1 file changed, 410 insertions(+) diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index 8eb42e8a2a3..428483fcaef 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -8637,6 +8637,416 @@ fn compile_command_dot_double_separator_curdir_suffix_double_separator_invalid_s std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_dot_double_separator_double_separator_root_matches_plain_relative_output() { + let root = unique_temp_dir("compile_dot_double_separator_double_separator_root"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + write_minimal_directory_compile_fixture(&root); + + let plain = Command::new(daglang_bin()) + .arg("compile") + .arg("dsl") + .current_dir(&root) + .output() + .expect("failed to run plain-relative root compile"); + assert!( + plain.status.success(), + "plain-relative root compile should succeed: {}", + String::from_utf8_lossy(&plain.stderr) + ); + + let dot_double_separator_double_separator = Command::new(daglang_bin()) + .arg("compile") + .arg(".//dsl//") + .current_dir(&root) + .output() + .expect("failed to run dot-double-separator-double-separator root compile"); + assert!( + dot_double_separator_double_separator.status.success(), + "dot-double-separator-double-separator root compile should succeed: {}", + String::from_utf8_lossy(&dot_double_separator_double_separator.stderr) + ); + + assert_eq!( + plain.stdout, dot_double_separator_double_separator.stdout, + "plain-relative and dot-double-separator-double-separator root compile stdout should match" + ); + assert_eq!( + plain.stderr, dot_double_separator_double_separator.stderr, + "plain-relative and dot-double-separator-double-separator root compile stderr should match" + ); + assert_no_stage_failures(&String::from_utf8_lossy(&plain.stderr)); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + +#[test] +fn compile_command_dot_double_separator_double_separator_missing_root_matches_plain_relative_output() { + let root = unique_temp_dir("compile_dot_double_separator_double_separator_missing_root"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + let missing = root.join("missing_root"); + + let plain = Command::new(daglang_bin()) + .arg("compile") + .arg("missing_root") + .current_dir(&root) + .output() + .expect("failed to run plain missing-root compile"); + assert!(!plain.status.success(), "plain missing-root compile should fail"); + + let dot_double_separator_double_separator = Command::new(daglang_bin()) + .arg("compile") + .arg(".//missing_root//") + .current_dir(&root) + .output() + .expect("failed to run dot-double-separator-double-separator missing-root compile"); + assert!( + !dot_double_separator_double_separator.status.success(), + "dot-double-separator-double-separator missing-root compile should fail" + ); + + assert_eq!( + plain.stdout, dot_double_separator_double_separator.stdout, + "plain and dot-double-separator-double-separator missing-root compile stdout should match" + ); + assert_eq!( + plain.stderr, dot_double_separator_double_separator.stderr, + "plain and dot-double-separator-double-separator missing-root compile stderr should match" + ); + let stderr = String::from_utf8_lossy(&plain.stderr); + assert!( + stderr.contains(&missing.display().to_string()), + "dot-double-separator-double-separator missing-root diagnostic should normalize to absolute path: {stderr}" + ); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + +#[test] +fn compile_command_dot_double_separator_double_separator_non_directory_root_matches_plain_relative_output( +) { + let root = unique_temp_dir("compile_dot_double_separator_double_separator_non_directory_root"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + let root_file = root.join("input.txt"); + std::fs::write(&root_file, "not a directory").expect("failed to write non-directory root"); + + let plain = Command::new(daglang_bin()) + .arg("compile") + .arg("input.txt") + .current_dir(&root) + .output() + .expect("failed to run plain non-directory-root compile"); + assert!( + !plain.status.success(), + "plain non-directory-root compile should fail" + ); + + let dot_double_separator_double_separator = Command::new(daglang_bin()) + .arg("compile") + .arg(".//input.txt//") + .current_dir(&root) + .output() + .expect("failed to run dot-double-separator-double-separator non-directory-root compile"); + assert!( + !dot_double_separator_double_separator.status.success(), + "dot-double-separator-double-separator non-directory-root compile should fail" + ); + + assert_eq!( + plain.stdout, dot_double_separator_double_separator.stdout, + "plain and dot-double-separator-double-separator non-directory-root compile stdout should match" + ); + assert_eq!( + plain.stderr, dot_double_separator_double_separator.stderr, + "plain and dot-double-separator-double-separator non-directory-root compile stderr should match" + ); + let stderr = String::from_utf8_lossy(&plain.stderr); + assert!( + stderr.contains(&root_file.display().to_string()), + "dot-double-separator-double-separator non-directory-root diagnostic should normalize to absolute path: {stderr}" + ); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + +#[test] +fn compile_command_dot_double_separator_double_separator_single_file_target_matches_plain_relative_output( +) { + let root = unique_temp_dir("compile_dot_double_separator_double_separator_single_file_target"); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp root"); + let source = root.join("sample/main.dag"); + std::fs::write(&source, "module sample.main\nfn run() -> Unit { }") + .expect("failed to write single-file source"); + + let plain = Command::new(daglang_bin()) + .arg("compile") + .arg("sample/main.dag") + .current_dir(&root) + .output() + .expect("failed to run plain single-file compile"); + assert!( + plain.status.success(), + "plain single-file compile should succeed: {}", + String::from_utf8_lossy(&plain.stderr) + ); + + let dot_double_separator_double_separator = Command::new(daglang_bin()) + .arg("compile") + .arg(".//sample/main.dag//") + .current_dir(&root) + .output() + .expect("failed to run dot-double-separator-double-separator single-file compile"); + assert!( + dot_double_separator_double_separator.status.success(), + "dot-double-separator-double-separator single-file compile should succeed: {}", + String::from_utf8_lossy(&dot_double_separator_double_separator.stderr) + ); + + assert_eq!( + plain.stdout, dot_double_separator_double_separator.stdout, + "plain and dot-double-separator-double-separator single-file compile stdout should match" + ); + assert_eq!( + plain.stderr, dot_double_separator_double_separator.stderr, + "plain and dot-double-separator-double-separator single-file compile stderr should match" + ); + assert_no_stage_failures(&String::from_utf8_lossy(&plain.stderr)); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + +#[test] +fn compile_command_dot_double_separator_double_separator_missing_single_file_target_matches_plain_relative_output( +) { + let root = unique_temp_dir("compile_dot_double_separator_double_separator_missing_single_file_target"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + let missing = root.join("missing.dag"); + + let plain = Command::new(daglang_bin()) + .arg("compile") + .arg("missing.dag") + .current_dir(&root) + .output() + .expect("failed to run plain missing single-file compile"); + assert!( + !plain.status.success(), + "plain missing single-file compile should fail" + ); + + let dot_double_separator_double_separator = Command::new(daglang_bin()) + .arg("compile") + .arg(".//missing.dag//") + .current_dir(&root) + .output() + .expect("failed to run dot-double-separator-double-separator missing single-file compile"); + assert!( + !dot_double_separator_double_separator.status.success(), + "dot-double-separator-double-separator missing single-file compile should fail" + ); + + assert_eq!( + plain.stdout, dot_double_separator_double_separator.stdout, + "plain and dot-double-separator-double-separator missing single-file compile stdout should match" + ); + assert_eq!( + plain.stderr, dot_double_separator_double_separator.stderr, + "plain and dot-double-separator-double-separator missing single-file compile stderr should match" + ); + let stderr = String::from_utf8_lossy(&plain.stderr); + assert!( + stderr.contains(&format!("failed to read {}", missing.display())), + "dot-double-separator-double-separator missing single-file diagnostic should normalize to absolute path: {stderr}" + ); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + +#[test] +fn compile_command_dot_double_separator_double_separator_invalid_single_file_target_matches_plain_relative_output( +) { + let root = unique_temp_dir("compile_dot_double_separator_double_separator_invalid_single_file_target"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + let invalid_target = root.join("invalid.dag"); + std::fs::create_dir_all(&invalid_target) + .expect("failed to create invalid single-file target directory"); + + let plain = Command::new(daglang_bin()) + .arg("compile") + .arg("invalid.dag") + .current_dir(&root) + .output() + .expect("failed to run plain invalid single-file compile"); + assert!( + !plain.status.success(), + "plain invalid single-file compile should fail" + ); + + let dot_double_separator_double_separator = Command::new(daglang_bin()) + .arg("compile") + .arg(".//invalid.dag//") + .current_dir(&root) + .output() + .expect("failed to run dot-double-separator-double-separator invalid single-file compile"); + assert!( + !dot_double_separator_double_separator.status.success(), + "dot-double-separator-double-separator invalid single-file compile should fail" + ); + + assert_eq!( + plain.stdout, dot_double_separator_double_separator.stdout, + "plain and dot-double-separator-double-separator invalid single-file compile stdout should match" + ); + assert_eq!( + plain.stderr, dot_double_separator_double_separator.stderr, + "plain and dot-double-separator-double-separator invalid single-file compile stderr should match" + ); + let stderr = String::from_utf8_lossy(&plain.stderr); + assert!( + stderr.contains(&format!("failed to read {}", invalid_target.display())), + "dot-double-separator-double-separator invalid single-file diagnostic should include normalized absolute path: {stderr}" + ); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + +#[test] +fn compile_command_dot_double_separator_trailing_slash_root_matches_plain_relative_output() { + let root = unique_temp_dir("compile_dot_double_separator_trailing_slash_root"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + write_minimal_directory_compile_fixture(&root); + + let plain = Command::new(daglang_bin()) + .arg("compile") + .arg("dsl") + .current_dir(&root) + .output() + .expect("failed to run plain-relative root compile"); + assert!( + plain.status.success(), + "plain-relative root compile should succeed: {}", + String::from_utf8_lossy(&plain.stderr) + ); + + let dot_double_separator_trailing_slash = Command::new(daglang_bin()) + .arg("compile") + .arg(".//dsl/") + .current_dir(&root) + .output() + .expect("failed to run dot-double-separator-trailing-slash root compile"); + assert!( + dot_double_separator_trailing_slash.status.success(), + "dot-double-separator-trailing-slash root compile should succeed: {}", + String::from_utf8_lossy(&dot_double_separator_trailing_slash.stderr) + ); + + assert_eq!( + plain.stdout, dot_double_separator_trailing_slash.stdout, + "plain-relative and dot-double-separator-trailing-slash root compile stdout should match" + ); + assert_eq!( + plain.stderr, dot_double_separator_trailing_slash.stderr, + "plain-relative and dot-double-separator-trailing-slash root compile stderr should match" + ); + assert_no_stage_failures(&String::from_utf8_lossy(&plain.stderr)); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + +#[test] +fn compile_command_dot_double_separator_trailing_slash_missing_root_matches_plain_relative_output() { + let root = unique_temp_dir("compile_dot_double_separator_trailing_slash_missing_root"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + let missing = root.join("missing_root"); + + let plain = Command::new(daglang_bin()) + .arg("compile") + .arg("missing_root") + .current_dir(&root) + .output() + .expect("failed to run plain missing-root compile"); + assert!(!plain.status.success(), "plain missing-root compile should fail"); + + let dot_double_separator_trailing_slash = Command::new(daglang_bin()) + .arg("compile") + .arg(".//missing_root/") + .current_dir(&root) + .output() + .expect("failed to run dot-double-separator-trailing-slash missing-root compile"); + assert!( + !dot_double_separator_trailing_slash.status.success(), + "dot-double-separator-trailing-slash missing-root compile should fail" + ); + + assert_eq!( + plain.stdout, dot_double_separator_trailing_slash.stdout, + "plain and dot-double-separator-trailing-slash missing-root compile stdout should match" + ); + assert_eq!( + plain.stderr, dot_double_separator_trailing_slash.stderr, + "plain and dot-double-separator-trailing-slash missing-root compile stderr should match" + ); + let stderr = String::from_utf8_lossy(&plain.stderr); + assert!( + stderr.contains(&missing.display().to_string()), + "dot-double-separator-trailing-slash missing-root diagnostic should normalize to absolute path: {stderr}" + ); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + +#[test] +fn compile_command_dot_double_separator_trailing_slash_non_directory_root_matches_plain_relative_output( +) { + let root = unique_temp_dir("compile_dot_double_separator_trailing_slash_non_directory_root"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + let root_file = root.join("input.txt"); + std::fs::write(&root_file, "not a directory").expect("failed to write non-directory root"); + + let plain = Command::new(daglang_bin()) + .arg("compile") + .arg("input.txt") + .current_dir(&root) + .output() + .expect("failed to run plain non-directory-root compile"); + assert!( + !plain.status.success(), + "plain non-directory-root compile should fail" + ); + + let dot_double_separator_trailing_slash = Command::new(daglang_bin()) + .arg("compile") + .arg(".//input.txt/") + .current_dir(&root) + .output() + .expect("failed to run dot-double-separator-trailing-slash non-directory-root compile"); + assert!( + !dot_double_separator_trailing_slash.status.success(), + "dot-double-separator-trailing-slash non-directory-root compile should fail" + ); + + assert_eq!( + plain.stdout, dot_double_separator_trailing_slash.stdout, + "plain and dot-double-separator-trailing-slash non-directory-root compile stdout should match" + ); + assert_eq!( + plain.stderr, dot_double_separator_trailing_slash.stderr, + "plain and dot-double-separator-trailing-slash non-directory-root compile stderr should match" + ); + let stderr = String::from_utf8_lossy(&plain.stderr); + assert!( + stderr.contains(&root_file.display().to_string()), + "dot-double-separator-trailing-slash non-directory-root diagnostic should normalize to absolute path: {stderr}" + ); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_curdir_segment_double_separator_root_matches_plain_relative_output() { let root = unique_temp_dir("compile_curdir_segment_double_separator_root"); From 98ed6a95ea0d2c26d941d816989e43f4c6c9aa3d Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 14:30:42 +0000 Subject: [PATCH 153/169] Add compile dot-double trailing single-file regression Co-authored-by: Brian Searls --- .../daglang-cli/tests/compile_commands.rs | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index 428483fcaef..b7a89f95dbb 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -9047,6 +9047,52 @@ fn compile_command_dot_double_separator_trailing_slash_non_directory_root_matche std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_dot_double_separator_trailing_slash_single_file_target_matches_plain_relative_output( +) { + let root = unique_temp_dir("compile_dot_double_separator_trailing_slash_single_file_target"); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp root"); + let source = root.join("sample/main.dag"); + std::fs::write(&source, "module sample.main\nfn run() -> Unit { }") + .expect("failed to write single-file source"); + + let plain = Command::new(daglang_bin()) + .arg("compile") + .arg("sample/main.dag") + .current_dir(&root) + .output() + .expect("failed to run plain single-file compile"); + assert!( + plain.status.success(), + "plain single-file compile should succeed: {}", + String::from_utf8_lossy(&plain.stderr) + ); + + let dot_double_separator_trailing_slash = Command::new(daglang_bin()) + .arg("compile") + .arg(".//sample/main.dag/") + .current_dir(&root) + .output() + .expect("failed to run dot-double-separator-trailing-slash single-file compile"); + assert!( + dot_double_separator_trailing_slash.status.success(), + "dot-double-separator-trailing-slash single-file compile should succeed: {}", + String::from_utf8_lossy(&dot_double_separator_trailing_slash.stderr) + ); + + assert_eq!( + plain.stdout, dot_double_separator_trailing_slash.stdout, + "plain and dot-double-separator-trailing-slash single-file compile stdout should match" + ); + assert_eq!( + plain.stderr, dot_double_separator_trailing_slash.stderr, + "plain and dot-double-separator-trailing-slash single-file compile stderr should match" + ); + assert_no_stage_failures(&String::from_utf8_lossy(&plain.stderr)); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_curdir_segment_double_separator_root_matches_plain_relative_output() { let root = unique_temp_dir("compile_curdir_segment_double_separator_root"); From 1a368d3ffa360952c4bf23e142f59434cedf6413 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 14:33:50 +0000 Subject: [PATCH 154/169] Add compile bare dot-double separator regressions Co-authored-by: Brian Searls --- .../daglang-cli/tests/compile_commands.rs | 272 ++++++++++++++++++ 1 file changed, 272 insertions(+) diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index b7a89f95dbb..0349cbae1ce 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -8637,6 +8637,278 @@ fn compile_command_dot_double_separator_curdir_suffix_double_separator_invalid_s std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } +#[test] +fn compile_command_dot_double_separator_root_matches_plain_relative_output() { + let root = unique_temp_dir("compile_dot_double_separator_root"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + write_minimal_directory_compile_fixture(&root); + + let plain = Command::new(daglang_bin()) + .arg("compile") + .arg("dsl") + .current_dir(&root) + .output() + .expect("failed to run plain-relative root compile"); + assert!( + plain.status.success(), + "plain-relative root compile should succeed: {}", + String::from_utf8_lossy(&plain.stderr) + ); + + let dot_double_separator = Command::new(daglang_bin()) + .arg("compile") + .arg(".//dsl") + .current_dir(&root) + .output() + .expect("failed to run dot-double-separator root compile"); + assert!( + dot_double_separator.status.success(), + "dot-double-separator root compile should succeed: {}", + String::from_utf8_lossy(&dot_double_separator.stderr) + ); + + assert_eq!( + plain.stdout, dot_double_separator.stdout, + "plain-relative and dot-double-separator root compile stdout should match" + ); + assert_eq!( + plain.stderr, dot_double_separator.stderr, + "plain-relative and dot-double-separator root compile stderr should match" + ); + assert_no_stage_failures(&String::from_utf8_lossy(&plain.stderr)); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + +#[test] +fn compile_command_dot_double_separator_missing_root_matches_plain_relative_output() { + let root = unique_temp_dir("compile_dot_double_separator_missing_root"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + let missing = root.join("missing_root"); + + let plain = Command::new(daglang_bin()) + .arg("compile") + .arg("missing_root") + .current_dir(&root) + .output() + .expect("failed to run plain missing-root compile"); + assert!(!plain.status.success(), "plain missing-root compile should fail"); + + let dot_double_separator = Command::new(daglang_bin()) + .arg("compile") + .arg(".//missing_root") + .current_dir(&root) + .output() + .expect("failed to run dot-double-separator missing-root compile"); + assert!( + !dot_double_separator.status.success(), + "dot-double-separator missing-root compile should fail" + ); + + assert_eq!( + plain.stdout, dot_double_separator.stdout, + "plain and dot-double-separator missing-root compile stdout should match" + ); + assert_eq!( + plain.stderr, dot_double_separator.stderr, + "plain and dot-double-separator missing-root compile stderr should match" + ); + let stderr = String::from_utf8_lossy(&plain.stderr); + assert!( + stderr.contains(&missing.display().to_string()), + "dot-double-separator missing-root diagnostic should normalize to absolute path: {stderr}" + ); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + +#[test] +fn compile_command_dot_double_separator_non_directory_root_matches_plain_relative_output() { + let root = unique_temp_dir("compile_dot_double_separator_non_directory_root"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + let root_file = root.join("input.txt"); + std::fs::write(&root_file, "not a directory").expect("failed to write non-directory root"); + + let plain = Command::new(daglang_bin()) + .arg("compile") + .arg("input.txt") + .current_dir(&root) + .output() + .expect("failed to run plain non-directory-root compile"); + assert!( + !plain.status.success(), + "plain non-directory-root compile should fail" + ); + + let dot_double_separator = Command::new(daglang_bin()) + .arg("compile") + .arg(".//input.txt") + .current_dir(&root) + .output() + .expect("failed to run dot-double-separator non-directory-root compile"); + assert!( + !dot_double_separator.status.success(), + "dot-double-separator non-directory-root compile should fail" + ); + + assert_eq!( + plain.stdout, dot_double_separator.stdout, + "plain and dot-double-separator non-directory-root compile stdout should match" + ); + assert_eq!( + plain.stderr, dot_double_separator.stderr, + "plain and dot-double-separator non-directory-root compile stderr should match" + ); + let stderr = String::from_utf8_lossy(&plain.stderr); + assert!( + stderr.contains(&root_file.display().to_string()), + "dot-double-separator non-directory-root diagnostic should normalize to absolute path: {stderr}" + ); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + +#[test] +fn compile_command_dot_double_separator_single_file_target_matches_plain_relative_output() { + let root = unique_temp_dir("compile_dot_double_separator_single_file_target"); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp root"); + let source = root.join("sample/main.dag"); + std::fs::write(&source, "module sample.main\nfn run() -> Unit { }") + .expect("failed to write single-file source"); + + let plain = Command::new(daglang_bin()) + .arg("compile") + .arg("sample/main.dag") + .current_dir(&root) + .output() + .expect("failed to run plain single-file compile"); + assert!( + plain.status.success(), + "plain single-file compile should succeed: {}", + String::from_utf8_lossy(&plain.stderr) + ); + + let dot_double_separator = Command::new(daglang_bin()) + .arg("compile") + .arg(".//sample/main.dag") + .current_dir(&root) + .output() + .expect("failed to run dot-double-separator single-file compile"); + assert!( + dot_double_separator.status.success(), + "dot-double-separator single-file compile should succeed: {}", + String::from_utf8_lossy(&dot_double_separator.stderr) + ); + + assert_eq!( + plain.stdout, dot_double_separator.stdout, + "plain and dot-double-separator single-file compile stdout should match" + ); + assert_eq!( + plain.stderr, dot_double_separator.stderr, + "plain and dot-double-separator single-file compile stderr should match" + ); + assert_no_stage_failures(&String::from_utf8_lossy(&plain.stderr)); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + +#[test] +fn compile_command_dot_double_separator_missing_single_file_target_matches_plain_relative_output() { + let root = unique_temp_dir("compile_dot_double_separator_missing_single_file_target"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + let missing = root.join("missing.dag"); + + let plain = Command::new(daglang_bin()) + .arg("compile") + .arg("missing.dag") + .current_dir(&root) + .output() + .expect("failed to run plain missing single-file compile"); + assert!( + !plain.status.success(), + "plain missing single-file compile should fail" + ); + + let dot_double_separator = Command::new(daglang_bin()) + .arg("compile") + .arg(".//missing.dag") + .current_dir(&root) + .output() + .expect("failed to run dot-double-separator missing single-file compile"); + assert!( + !dot_double_separator.status.success(), + "dot-double-separator missing single-file compile should fail" + ); + + assert_eq!( + plain.stdout, dot_double_separator.stdout, + "plain and dot-double-separator missing single-file compile stdout should match" + ); + assert_eq!( + plain.stderr, dot_double_separator.stderr, + "plain and dot-double-separator missing single-file compile stderr should match" + ); + let stderr = String::from_utf8_lossy(&plain.stderr); + assert!( + stderr.contains(&format!("failed to read {}", missing.display())), + "dot-double-separator missing single-file diagnostic should normalize to absolute path: {stderr}" + ); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + +#[test] +fn compile_command_dot_double_separator_invalid_single_file_target_matches_plain_relative_output() { + let root = unique_temp_dir("compile_dot_double_separator_invalid_single_file_target"); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + let invalid_target = root.join("invalid.dag"); + std::fs::create_dir_all(&invalid_target) + .expect("failed to create invalid single-file target directory"); + + let plain = Command::new(daglang_bin()) + .arg("compile") + .arg("invalid.dag") + .current_dir(&root) + .output() + .expect("failed to run plain invalid single-file compile"); + assert!( + !plain.status.success(), + "plain invalid single-file compile should fail" + ); + + let dot_double_separator = Command::new(daglang_bin()) + .arg("compile") + .arg(".//invalid.dag") + .current_dir(&root) + .output() + .expect("failed to run dot-double-separator invalid single-file compile"); + assert!( + !dot_double_separator.status.success(), + "dot-double-separator invalid single-file compile should fail" + ); + + assert_eq!( + plain.stdout, dot_double_separator.stdout, + "plain and dot-double-separator invalid single-file compile stdout should match" + ); + assert_eq!( + plain.stderr, dot_double_separator.stderr, + "plain and dot-double-separator invalid single-file compile stderr should match" + ); + let stderr = String::from_utf8_lossy(&plain.stderr); + assert!( + stderr.contains(&format!("failed to read {}", invalid_target.display())), + "dot-double-separator invalid single-file diagnostic should include normalized absolute path: {stderr}" + ); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); +} + #[test] fn compile_command_dot_double_separator_double_separator_root_matches_plain_relative_output() { let root = unique_temp_dir("compile_dot_double_separator_double_separator_root"); From 459cf84d621f4ea8da2e6410d90c3b188355883a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 14:39:46 +0000 Subject: [PATCH 155/169] Add compile diagnostic ordering and determinism regressions Co-authored-by: Brian Searls --- .../daglang-cli/tests/compile_commands.rs | 188 ++++++++++++++++++ 1 file changed, 188 insertions(+) diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index 0349cbae1ce..ba4fbe3fd5d 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -10615,6 +10615,194 @@ fn compile_command_reports_diagnostics_for_invalid_file() { std::fs::remove_file(broken).expect("failed to remove temp broken source"); } +#[test] +fn compile_command_reports_file_line_col_for_broken_file() { + let broken_file = unique_temp_file("compile_broken_line_col"); + std::fs::write( + &broken_file, + "module tmp.bad\nfn broken( -> String {\n \"oops\"\n}\n", + ) + .expect("failed to create broken dag file"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&broken_file) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile for broken file"); + + assert!(!output.status.success(), "broken file should fail compile"); + let stderr = String::from_utf8_lossy(&output.stderr); + assert_no_stage_failures(&stderr); + assert!(stderr.contains("compile diagnostics")); + assert!( + stderr.contains(":2:12:"), + "expected file:line:col in stderr, got: {stderr}" + ); + + std::fs::remove_file(broken_file).expect("failed to remove broken dag file"); +} + +#[test] +fn compile_command_reports_lex_diagnostic_for_unknown_character() { + let broken_file = unique_temp_file("compile_lex_unknown_character"); + std::fs::write(&broken_file, "module tmp.lex\n$\n").expect("failed to create lex-invalid file"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&broken_file) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile for lex-invalid file"); + + assert!(!output.status.success(), "lex-invalid file should fail compile"); + let stderr = String::from_utf8_lossy(&output.stderr); + assert_no_stage_failures(&stderr); + assert!(stderr.contains("compile diagnostics")); + assert!(stderr.contains("unexpected character '$'")); + assert!( + stderr.contains(":2:1:"), + "expected lex diagnostic line/column in stderr, got: {stderr}" + ); + + std::fs::remove_file(broken_file).expect("failed to remove lex-invalid file"); +} + +#[test] +fn compile_command_directory_mode_aggregates_multiple_file_diagnostics() { + let root = unique_temp_dir("compile_directory_mode_errors"); + std::fs::create_dir_all(&root).expect("failed to create temp dir"); + std::fs::write(root.join("broken_a.dag"), "module sample.a\nfn") + .expect("failed to write broken_a"); + std::fs::write(root.join("broken_b.dag"), "module sample.b\nimport") + .expect("failed to write broken_b"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&root) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile on directory"); + + assert!( + !output.status.success(), + "directory compile should fail when multiple files are invalid" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert_no_stage_failures(&stderr); + assert!(stderr.contains("compile diagnostics")); + assert!(stderr.contains("broken_a.dag")); + assert!(stderr.contains("broken_b.dag")); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); +} + +#[test] +fn compile_command_directory_mode_outputs_deterministic_diagnostic_order() { + let root = unique_temp_dir("compile_directory_mode_order"); + std::fs::create_dir_all(&root).expect("failed to create temp dir"); + std::fs::write(root.join("z_broken.dag"), "module sample.z\nfn") + .expect("failed to write z_broken"); + std::fs::write(root.join("a_broken.dag"), "module sample.a\nfn") + .expect("failed to write a_broken"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&root) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile on directory"); + + assert!( + !output.status.success(), + "directory compile should fail when files are invalid" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert_no_stage_failures(&stderr); + let first_diagnostic_line = stderr + .lines() + .find(|line| line.contains(".dag:")) + .expect("expected at least one diagnostic line with file path"); + assert!( + first_diagnostic_line.contains("a_broken.dag"), + "diagnostics should be deterministically sorted by path: {stderr}" + ); + assert!(stderr.contains("z_broken.dag")); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); +} + +#[test] +fn compile_command_diagnostic_output_is_deterministic_for_same_input() { + let root = unique_temp_dir("compile_output_deterministic"); + std::fs::create_dir_all(&root).expect("failed to create temp dir"); + std::fs::write(root.join("z_lex.dag"), "module sample.lex\n$\n") + .expect("failed to write z_lex"); + std::fs::write(root.join("a_parse.dag"), "module sample.parse\nfn") + .expect("failed to write a_parse"); + + let first = Command::new(daglang_bin()) + .arg("compile") + .arg(&root) + .current_dir(workspace_root()) + .output() + .expect("failed to run first daglang compile"); + assert!(!first.status.success(), "first compile run should fail"); + + let second = Command::new(daglang_bin()) + .arg("compile") + .arg(&root) + .current_dir(workspace_root()) + .output() + .expect("failed to run second daglang compile"); + assert!(!second.status.success(), "second compile run should fail"); + + assert_eq!( + first.stderr, second.stderr, + "compile diagnostics should be deterministic for identical inputs" + ); + assert_eq!( + first.stdout, second.stdout, + "compile stdout should be deterministic for identical inputs" + ); + assert_no_stage_failures(&String::from_utf8_lossy(&first.stderr)); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); +} + +#[test] +fn compile_command_single_file_mode_ignores_sibling_broken_files() { + let root = unique_temp_dir("compile_single_file_ignores_sibling_broken"); + std::fs::create_dir_all(&root).expect("failed to create temp dir"); + let good_file = root.join("good.dag"); + std::fs::write(&good_file, "module sample.good\nfn ok() -> Unit {}") + .expect("failed to write good file"); + std::fs::write(root.join("broken.dag"), "module sample.broken\nfn") + .expect("failed to write broken sibling"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&good_file) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile on single file"); + + assert!( + output.status.success(), + "single-file compile should succeed even with sibling broken file: {}", + String::from_utf8_lossy(&output.stderr) + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert_no_stage_failures(&stderr); + assert!( + output.stderr.is_empty(), + "single-file compile should not emit stderr: {}", + String::from_utf8_lossy(&output.stderr) + ); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); +} + #[test] fn expand_command_reports_diagnostics_for_invalid_file() { let broken = unique_temp_file("expand_broken"); From bc4d6258bf902fc0e40a3709b33cf7789faae474 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 14:45:32 +0000 Subject: [PATCH 156/169] Add compile empty and non-dag directory diagnostics Co-authored-by: Brian Searls --- .../daglang-cli/tests/compile_commands.rs | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index ba4fbe3fd5d..fe5706186ca 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -10803,6 +10803,86 @@ fn compile_command_single_file_mode_ignores_sibling_broken_files() { std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); } +#[test] +fn compile_command_empty_directory_reports_lower_stage_error() { + let root = unique_temp_dir("compile_empty_directory_lower_error"); + std::fs::create_dir_all(&root).expect("failed to create temp dir"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&root) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile on empty directory"); + + assert!( + !output.status.success(), + "compile should fail for empty directory roots" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert_lower_stage_failure(&stderr); + assert!(stderr.contains("no callable or pipeline declarations to lower")); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); +} + +#[test] +fn compile_command_non_dag_only_directory_reports_lower_stage_error() { + let root = unique_temp_dir("compile_non_dag_only_directory"); + std::fs::create_dir_all(&root).expect("failed to create temp dir"); + std::fs::write(root.join("notes.txt"), "this should be ignored") + .expect("failed to write non-dag file"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&root) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile on non-dag-only directory"); + + assert!( + !output.status.success(), + "compile should fail with lower-stage error when no .dag files exist" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert_lower_stage_failure(&stderr); + assert!(stderr.contains("no callable or pipeline declarations to lower")); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); +} + +#[test] +fn compile_command_ignores_non_dag_files_when_dag_files_exist() { + let root = unique_temp_dir("compile_ignore_non_dag_mixed"); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp dir"); + std::fs::write(root.join("sample/main.dag"), "module sample.main\nfn run() -> Unit { }") + .expect("failed to write dag file"); + std::fs::write(root.join("notes.txt"), "module fake\n$").expect("failed to write txt file"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&root) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile on mixed dag/non-dag directory"); + + assert!( + output.status.success(), + "compile should succeed when dag files exist even with non-dag siblings: {}", + String::from_utf8_lossy(&output.stderr) + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.trim().is_empty(), "compile success should not emit stderr"); + assert_no_stage_failures(&stderr); + let stdout = String::from_utf8_lossy(&output.stdout); + assert!( + stdout.contains("Compiled 1 module(s)"), + "expected compile summary to report one dag module: {stdout}" + ); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); +} + #[test] fn expand_command_reports_diagnostics_for_invalid_file() { let broken = unique_temp_file("expand_broken"); From 30b27f9a8993131b3bbb48ee4e6bb15fa1f440ea Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 14:51:34 +0000 Subject: [PATCH 157/169] Sort compile diagnostics lex before parse Co-authored-by: Brian Searls --- core/daglang/daglang-cli/src/compile.rs | 83 ++++++++++++++++++- .../daglang-cli/tests/compile_commands.rs | 36 ++++++++ 2 files changed, 115 insertions(+), 4 deletions(-) diff --git a/core/daglang/daglang-cli/src/compile.rs b/core/daglang/daglang-cli/src/compile.rs index 5e85a77a2c4..5b5ffc113b2 100644 --- a/core/daglang/daglang-cli/src/compile.rs +++ b/core/daglang/daglang-cli/src/compile.rs @@ -5,6 +5,7 @@ use daglang_derive::{derive_artifacts, DerivedArtifacts}; use daglang_emit::{emit_rust_bundle, EmissionBundle}; use daglang_lower::{lower_typed_project, LoweredOp}; use daglang_resolve::{ModuleGraph, ResolveError, ResolvedModule}; +use daglang_syntax::diagnostic::{Diagnostic, DiagnosticKind}; use daglang_syntax::parser; use daglang_typecheck::{typecheck_module_graph_with_options, TypecheckOptions}; use gunbc_ir::Dag; @@ -136,10 +137,14 @@ fn format_resolve_error(error: ResolveError) -> String { match error { ResolveError::ParseErrors(files) => { let mut message = String::from("compile diagnostics:\n"); - for (_path, diagnostics) in files { - for diagnostic in diagnostics { - message.push_str(&format!(" {}\n", diagnostic.render())); - } + let diagnostics = normalize_diagnostics( + files + .into_iter() + .flat_map(|(_path, diagnostics)| diagnostics) + .collect(), + ); + for diagnostic in diagnostics { + message.push_str(&format!(" {}\n", diagnostic.render())); } message } @@ -147,6 +152,38 @@ fn format_resolve_error(error: ResolveError) -> String { } } +fn normalize_diagnostics(mut diagnostics: Vec) -> Vec { + diagnostics.sort_by_key(|diag| { + ( + diagnostic_kind_rank(&diag.kind), + diag.file + .as_ref() + .map(|file| file.display().to_string()) + .unwrap_or_default(), + diag.line.unwrap_or_default(), + diag.column.unwrap_or_default(), + diag.message.clone(), + ) + }); + diagnostics.dedup_by(|a, b| { + a.kind == b.kind + && a.file == b.file + && a.line == b.line + && a.column == b.column + && a.message == b.message + }); + diagnostics +} + +fn diagnostic_kind_rank(kind: &DiagnosticKind) -> u8 { + match kind { + DiagnosticKind::Lex => 0, + DiagnosticKind::Parse => 1, + DiagnosticKind::Resolve => 2, + DiagnosticKind::Pipeline => 3, + } +} + fn validate_module_path_consistency( graph: &ModuleGraph, roots: &[PathBuf], @@ -478,6 +515,44 @@ mod tests { std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } + #[test] + fn compile_directory_sorts_lex_before_parse_diagnostics() { + let root = std::env::temp_dir().join(format!( + "daglang_compile_lex_before_parse_{}_{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("system clock should be after unix epoch") + .as_nanos() + )); + std::fs::create_dir_all(&root).expect("failed to create temp root"); + std::fs::write(root.join("a_parse.dag"), "module sample.parse\nfn") + .expect("failed to write parse-error file"); + std::fs::write(root.join("z_lex.dag"), "module sample.lex\n$\n") + .expect("failed to write lex-error file"); + + let context = PipelineContext { + roots: vec![root.clone()], + target_file: None, + }; + + let error = compile_from_context(&context).expect_err("compile should fail"); + let first_diagnostic_line = error + .lines() + .find(|line| line.contains(".dag:")) + .expect("expected at least one file diagnostic line"); + assert!( + first_diagnostic_line.contains("z_lex.dag"), + "lex diagnostics should sort before parse diagnostics: {error}" + ); + assert!(error.contains("a_parse.dag")); + assert!(error.contains("unexpected character '$'")); + assert!(!error.contains("typecheck errors")); + assert!(!error.contains("lower error")); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); + } + #[test] fn compile_single_file_unresolved_service_call_fails_in_lower_stage() { let fixture = unique_temp_file("single_file_unresolved_service_call"); diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index fe5706186ca..985c9aee8e1 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -10770,6 +10770,42 @@ fn compile_command_diagnostic_output_is_deterministic_for_same_input() { std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); } +#[test] +fn compile_command_sorts_lex_diagnostics_before_parse_diagnostics() { + let root = unique_temp_dir("compile_kind_order"); + std::fs::create_dir_all(&root).expect("failed to create temp dir"); + std::fs::write(root.join("a_parse.dag"), "module sample.parse\nfn") + .expect("failed to write parse-error file"); + std::fs::write(root.join("z_lex.dag"), "module sample.lex\n$\n") + .expect("failed to write lex-error file"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&root) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile on mixed-error directory"); + + assert!( + !output.status.success(), + "compile should fail when diagnostics are present" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert_no_stage_failures(&stderr); + let first_diagnostic_line = stderr + .lines() + .find(|line| line.contains(".dag:")) + .expect("expected at least one diagnostic line"); + assert!( + first_diagnostic_line.contains("z_lex.dag"), + "lex diagnostics should sort before parse diagnostics: {stderr}" + ); + assert!(stderr.contains("a_parse.dag")); + assert!(stderr.contains("unexpected character '$'")); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); +} + #[test] fn compile_command_single_file_mode_ignores_sibling_broken_files() { let root = unique_temp_dir("compile_single_file_ignores_sibling_broken"); From 3836af7ef9d422fa054d1d009ac96a4c10c2a57c Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 15:07:36 +0000 Subject: [PATCH 158/169] Normalize configured resource type names across compile stages Co-authored-by: Brian Searls --- core/daglang/daglang-cli/src/compile.rs | 132 +++++++++++++++++ .../daglang-cli/tests/compile_commands.rs | 138 ++++++++++++++++++ core/daglang/daglang-lower/src/lib.rs | 57 +++++++- core/daglang/daglang-typecheck/src/lib.rs | 80 +++++++++- 4 files changed, 404 insertions(+), 3 deletions(-) diff --git a/core/daglang/daglang-cli/src/compile.rs b/core/daglang/daglang-cli/src/compile.rs index 5b5ffc113b2..e3e08bb1cac 100644 --- a/core/daglang/daglang-cli/src/compile.rs +++ b/core/daglang/daglang-cli/src/compile.rs @@ -684,6 +684,68 @@ func run() -> { ok: Bool } uses fs: MissingResource { std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } + #[test] + fn compile_single_file_uses_resource_with_runtime_config_suffix_succeeds() { + let fixture = unique_temp_file("single_file_uses_resource_with_config_suffix"); + std::fs::write( + &fixture, + r#"module sample.uses +resource Filesystem {} +func run() -> { ok: Bool } uses fs: Filesystem(mode: ReadWrite) { + return { ok: true } +} +"#, + ) + .expect("failed to write configured uses fixture"); + + let context = PipelineContext { + roots: vec![fixture.parent().expect("fixture should have parent").to_path_buf()], + target_file: Some(fixture.clone()), + }; + + let output = compile_from_context(&context).expect("compile should succeed"); + assert!(!output.lowered_dag.nodes.is_empty()); + assert!(output.derived.manifest.total_nodes > 0); + assert!(!output.emitted.files.is_empty()); + + std::fs::remove_file(fixture).expect("failed to cleanup fixture"); + } + + #[test] + fn compile_directory_uses_resource_with_runtime_config_suffix_succeeds() { + let root = std::env::temp_dir().join(format!( + "daglang_compile_uses_config_suffix_dir_{}_{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("system clock should be after unix epoch") + .as_nanos() + )); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp root"); + std::fs::write( + root.join("sample/main.dag"), + r#"module sample.main +resource Filesystem {} +func run() -> { ok: Bool } uses fs: Filesystem(mode: ReadWrite) { + return { ok: true } +} +"#, + ) + .expect("failed to write configured uses source"); + + let context = PipelineContext { + roots: vec![root.clone()], + target_file: None, + }; + + let output = compile_from_context(&context).expect("compile should succeed"); + assert!(!output.lowered_dag.nodes.is_empty()); + assert!(output.derived.manifest.total_nodes > 0); + assert!(!output.emitted.files.is_empty()); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); + } + #[test] fn compile_single_file_unresolved_provides_fails_in_lower_stage() { let fixture = unique_temp_file("single_file_unresolved_provides"); @@ -744,6 +806,76 @@ func run() -> { ok: Bool } provides out: MissingResource { std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } + #[test] + fn compile_single_file_provides_resource_with_runtime_config_suffix_succeeds() { + let fixture = unique_temp_file("single_file_provides_resource_with_config_suffix"); + std::fs::write( + &fixture, + r#"module sample.provides +resource ArtifactStore { + release { + let done = true + } +} +func run() -> { ok: Bool } provides out: ArtifactStore(kind: temporary) { + return { ok: true } +} +"#, + ) + .expect("failed to write configured provides fixture"); + + let context = PipelineContext { + roots: vec![fixture.parent().expect("fixture should have parent").to_path_buf()], + target_file: Some(fixture.clone()), + }; + + let output = compile_from_context(&context).expect("compile should succeed"); + assert!(!output.lowered_dag.nodes.is_empty()); + assert!(output.derived.manifest.total_nodes > 0); + assert!(!output.emitted.files.is_empty()); + + std::fs::remove_file(fixture).expect("failed to cleanup fixture"); + } + + #[test] + fn compile_directory_provides_resource_with_runtime_config_suffix_succeeds() { + let root = std::env::temp_dir().join(format!( + "daglang_compile_provides_config_suffix_dir_{}_{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("system clock should be after unix epoch") + .as_nanos() + )); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp root"); + std::fs::write( + root.join("sample/main.dag"), + r#"module sample.main +resource ArtifactStore { + release { + let done = true + } +} +func run() -> { ok: Bool } provides out: ArtifactStore(kind: temporary) { + return { ok: true } +} +"#, + ) + .expect("failed to write configured provides source"); + + let context = PipelineContext { + roots: vec![root.clone()], + target_file: None, + }; + + let output = compile_from_context(&context).expect("compile should succeed"); + assert!(!output.lowered_dag.nodes.is_empty()); + assert!(output.derived.manifest.total_nodes > 0); + assert!(!output.emitted.files.is_empty()); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); + } + #[test] fn compile_single_file_allows_unresolved_imports_in_relaxed_mode() { let fixture = unique_temp_file("single_file_unresolved_import"); diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index 985c9aee8e1..26f72d445f7 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -10161,6 +10161,74 @@ func run() -> { ok: Bool } provides out: MissingResource { std::fs::remove_file(fixture).expect("failed to cleanup fixture"); } +#[test] +fn compile_command_single_file_accepts_uses_resource_with_runtime_config_suffix() { + let fixture = unique_temp_file("compile_single_file_uses_config_suffix"); + std::fs::write( + &fixture, + r#"module sample.uses +resource Filesystem {} +func run() -> { ok: Bool } uses fs: Filesystem(mode: ReadWrite) { + return { ok: true } +} +"#, + ) + .expect("failed to write fixture"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&fixture) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile for configured uses fixture"); + + assert!( + output.status.success(), + "single-file compile should accept configured uses resource type: {}", + String::from_utf8_lossy(&output.stderr) + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert_no_stage_failures(&stderr); + + std::fs::remove_file(fixture).expect("failed to cleanup fixture"); +} + +#[test] +fn compile_command_single_file_accepts_provides_resource_with_runtime_config_suffix() { + let fixture = unique_temp_file("compile_single_file_provides_config_suffix"); + std::fs::write( + &fixture, + r#"module sample.provides +resource ArtifactStore { + release { + let done = true + } +} +func run() -> { ok: Bool } provides out: ArtifactStore(kind: temporary) { + return { ok: true } +} +"#, + ) + .expect("failed to write fixture"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&fixture) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile for configured provides fixture"); + + assert!( + output.status.success(), + "single-file compile should accept configured provides resource type: {}", + String::from_utf8_lossy(&output.stderr) + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert_no_stage_failures(&stderr); + + std::fs::remove_file(fixture).expect("failed to cleanup fixture"); +} + #[test] fn compile_command_single_file_allows_unresolved_imports() { let fixture = unique_temp_file("compile_single_file_unresolved_import"); @@ -11717,6 +11785,39 @@ fn compile_command_directory_mode_fails_on_unknown_uses_resource_type() { std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); } +#[test] +fn compile_command_directory_mode_accepts_uses_resource_with_runtime_config_suffix() { + let root = unique_temp_dir("configured_uses_type"); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp dir"); + std::fs::write( + root.join("sample/main.dag"), + r#"module sample.main +resource Filesystem {} +func run() -> { ok: Bool } uses fs: Filesystem(mode: ReadWrite) { + return { ok: true } +} +"#, + ) + .expect("failed to write source"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&root) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile on directory"); + + assert!( + output.status.success(), + "directory compile should accept configured uses resource type: {}", + String::from_utf8_lossy(&output.stderr) + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); +} + #[test] fn compile_command_directory_mode_fails_on_duplicate_uses_binding() { let root = unique_temp_dir("duplicate_uses_binding"); @@ -11904,6 +12005,43 @@ fn compile_command_directory_mode_fails_on_unknown_provides_resource_type() { std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); } +#[test] +fn compile_command_directory_mode_accepts_provides_resource_with_runtime_config_suffix() { + let root = unique_temp_dir("configured_provides_type"); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp dir"); + std::fs::write( + root.join("sample/main.dag"), + r#"module sample.main +resource ArtifactStore { + release { + let done = true + } +} +func run() -> { ok: Bool } provides out: ArtifactStore(kind: temporary) { + return { ok: true } +} +"#, + ) + .expect("failed to write source"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&root) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile on directory"); + + assert!( + output.status.success(), + "directory compile should accept configured provides resource type: {}", + String::from_utf8_lossy(&output.stderr) + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); +} + #[test] fn compile_command_directory_mode_fails_on_ambiguous_provides_resource_type() { let root = unique_temp_dir("ambiguous_provides_type"); diff --git a/core/daglang/daglang-lower/src/lib.rs b/core/daglang/daglang-lower/src/lib.rs index 7d1c0699ad2..9d432ff7882 100644 --- a/core/daglang/daglang-lower/src/lib.rs +++ b/core/daglang/daglang-lower/src/lib.rs @@ -1459,7 +1459,17 @@ fn resource_type_name(ty: &TypeExpr) -> String { } fn canonical_type_name(name: &str) -> String { - name.split('<').next().unwrap_or(name).trim().to_string() + let base_without_config = name.split('(').next().unwrap_or(name).trim(); + let base_without_annotations = base_without_config + .split_whitespace() + .next() + .unwrap_or(base_without_config); + base_without_annotations + .split('<') + .next() + .unwrap_or(base_without_annotations) + .trim() + .to_string() } fn add_resource_lifecycle_nodes( @@ -2192,6 +2202,28 @@ func run() -> { ok: Bool } uses fs: MissingResource { )); } + #[test] + fn uses_clause_with_runtime_config_suffix_resolves_resource_type() { + let typed = typed_project_from_sources(&[( + "dsl/resources/configured_uses_wiring.dag", + r#"module sample.resources +resource TempFile { + acquire { let path = "/tmp/file" } + release { let done = true } +} +func run() -> { ok: Bool } uses fs: TempFile(mode: ReadWrite) { + return { ok: true } +}"#, + )]); + let dag = lower_typed_project(&typed).expect("lowering should succeed"); + assert!(dag.edges.iter().any(|edge| { + edge.from_node.0 == "acquire_resource_sample_resources_TempFile" + && edge.from_port.0 == "resource_handle" + && edge.to_node.0 == "sample.resources::run" + && edge.to_port.0 == "__deps" + })); + } + #[test] fn provides_clause_adds_provider_node_and_edge() { let typed = typed_project_from_sources(&[( @@ -2240,6 +2272,29 @@ func run() -> { ok: Bool } provides out: MissingResource { )); } + #[test] + fn provides_clause_with_runtime_config_suffix_resolves_resource_type() { + let typed = typed_project_from_sources(&[( + "dsl/resources/configured_provides_wiring.dag", + r#"module sample.resources +resource TempFile { + release { + let done = true + } +} +func run() -> { ok: Bool } provides file: TempFile(mode: ReadWrite) { + return { ok: true } +}"#, + )]); + let dag = lower_typed_project(&typed).expect("lowering should succeed"); + assert!(dag.edges.iter().any(|edge| { + edge.from_node.0 == "provide_resource_sample_resources_run_file" + && edge.from_port.0 == "file" + && edge.to_node.0 == "release_resource_sample_resources_TempFile" + && edge.to_port.0 == "resource_handle" + })); + } + #[test] fn provides_resource_with_release_wires_provider_output_to_lifecycle_release() { let typed = typed_project_from_sources(&[( diff --git a/core/daglang/daglang-typecheck/src/lib.rs b/core/daglang/daglang-typecheck/src/lib.rs index c3b87d861fa..3c5cc1fe71c 100644 --- a/core/daglang/daglang-typecheck/src/lib.rs +++ b/core/daglang/daglang-typecheck/src/lib.rs @@ -1183,7 +1183,7 @@ fn validate_uses_clauses( }); } if !allow_unresolved_references { - let resource_type = canonical_type_name(&type_expr_to_string(&usage.resource_type)); + let resource_type = resource_type_name(&usage.resource_type); match resolve_resource_type_name(&resource_type, registry) { ResourceTypeResolution::Resolved(_) => {} ResourceTypeResolution::Ambiguous => { @@ -1221,7 +1221,7 @@ fn validate_provides_clauses( }); } if !allow_unresolved_references { - let resource_type = canonical_type_name(&type_expr_to_string(&provided.resource_type)); + let resource_type = resource_type_name(&provided.resource_type); match resolve_resource_type_name(&resource_type, registry) { ResourceTypeResolution::Resolved(_) => {} ResourceTypeResolution::Ambiguous => { @@ -1981,6 +1981,22 @@ fn canonical_type_name(name: &str) -> String { name.split('<').next().unwrap_or(name).trim().to_string() } +fn canonical_resource_type_name(name: &str) -> String { + let base_without_config = name.split('(').next().unwrap_or(name).trim(); + let base_without_annotations = base_without_config + .split_whitespace() + .next() + .unwrap_or(base_without_config); + canonical_type_name(base_without_annotations) +} + +fn resource_type_name(resource_type: &TypeExpr) -> String { + match resource_type { + TypeExpr::Named(name) | TypeExpr::Generic(name, _) => canonical_resource_type_name(name), + TypeExpr::Optional(inner) | TypeExpr::Annotated(inner, _) => resource_type_name(inner), + } +} + #[derive(Debug, Clone, PartialEq, Eq)] struct BodyCall { callee: String, @@ -3210,6 +3226,26 @@ service FsStorage implements Storage { ))); } + #[test] + fn strict_mode_accepts_uses_resource_type_with_runtime_config_suffix() { + let graph = module_graph_from_sources(&[( + "sample/main.dag", + r#"module sample.main +resource Filesystem {} +func run() -> { ok: Bool } uses fs: Filesystem(mode: ReadWrite) { + return { ok: true } +}"#, + )]); + let typed = typecheck_module_graph_with_options( + graph, + TypecheckOptions { + allow_unresolved_imports: false, + }, + ) + .expect("configured resource type should resolve in strict mode"); + assert_eq!(typed.modules.len(), 1); + } + #[test] fn strict_mode_reports_ambiguous_used_resource_type() { let graph = module_graph_from_sources(&[ @@ -3340,6 +3376,46 @@ func run() -> { ok: Bool } uses fs: SharedResource { ))); } + #[test] + fn strict_mode_accepts_provides_resource_type_with_runtime_config_suffix() { + let graph = module_graph_from_sources(&[( + "sample/main.dag", + r#"module sample.main +resource ArtifactStore {} +func run() -> { ok: Bool } provides out: ArtifactStore(kind: temporary) { + return { ok: true } +}"#, + )]); + let typed = typecheck_module_graph_with_options( + graph, + TypecheckOptions { + allow_unresolved_imports: false, + }, + ) + .expect("configured provided resource type should resolve in strict mode"); + assert_eq!(typed.modules.len(), 1); + } + + #[test] + fn strict_mode_accepts_annotated_provides_resource_type_reference() { + let graph = module_graph_from_sources(&[( + "sample/main.dag", + r#"module sample.main +resource ArtifactStore {} +func run() -> { ok: Bool } provides out: ArtifactStore @test_integration { + return { ok: true } +}"#, + )]); + let typed = typecheck_module_graph_with_options( + graph, + TypecheckOptions { + allow_unresolved_imports: false, + }, + ) + .expect("annotated provided resource type should resolve in strict mode"); + assert_eq!(typed.modules.len(), 1); + } + #[test] fn strict_mode_reports_ambiguous_provided_resource_type() { let graph = module_graph_from_sources(&[ From 6c4bd4f7268fffcbb7b60c05adab0a9198fac89e Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 15:22:17 +0000 Subject: [PATCH 159/169] Resolve uses-bound capability calls across typecheck and lower Co-authored-by: Brian Searls --- core/daglang/daglang-cli/src/compile.rs | 74 +++ .../daglang-cli/tests/compile_commands.rs | 77 +++ core/daglang/daglang-lower/src/lib.rs | 59 ++- core/daglang/daglang-typecheck/src/lib.rs | 488 ++++++++++++++++-- 4 files changed, 647 insertions(+), 51 deletions(-) diff --git a/core/daglang/daglang-cli/src/compile.rs b/core/daglang/daglang-cli/src/compile.rs index e3e08bb1cac..02e895929af 100644 --- a/core/daglang/daglang-cli/src/compile.rs +++ b/core/daglang/daglang-cli/src/compile.rs @@ -624,6 +624,80 @@ func run(path: String) -> { body: String } { std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } + #[test] + fn compile_single_file_uses_bound_resource_capability_call_succeeds() { + let fixture = unique_temp_file("single_file_uses_bound_resource_capability_call"); + std::fs::write( + &fixture, + r#"module sample.resources +resource Filesystem { + capability read { + input { path: String } + output { body: String } + } +} +func run(path: String) -> { body: String } uses fs: Filesystem { + let response = fs.read(path: path) + return { body: response.body } +} +"#, + ) + .expect("failed to write resource-bound capability fixture"); + + let context = PipelineContext { + roots: vec![fixture.parent().expect("fixture should have parent").to_path_buf()], + target_file: Some(fixture.clone()), + }; + + let output = compile_from_context(&context).expect("compile should succeed"); + assert!(!output.lowered_dag.nodes.is_empty()); + assert!(output.derived.manifest.total_nodes > 0); + assert!(!output.emitted.files.is_empty()); + + std::fs::remove_file(fixture).expect("failed to cleanup fixture"); + } + + #[test] + fn compile_directory_uses_bound_resource_capability_call_succeeds() { + let root = std::env::temp_dir().join(format!( + "daglang_compile_resource_bound_service_call_dir_{}_{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("system clock should be after unix epoch") + .as_nanos() + )); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp root"); + std::fs::write( + root.join("sample/main.dag"), + r#"module sample.main +resource Filesystem { + capability read { + input { path: String } + output { body: String } + } +} +func run(path: String) -> { body: String } uses fs: Filesystem { + let response = fs.read(path: path) + return { body: response.body } +} +"#, + ) + .expect("failed to write resource-bound capability source"); + + let context = PipelineContext { + roots: vec![root.clone()], + target_file: None, + }; + + let output = compile_from_context(&context).expect("compile should succeed"); + assert!(!output.lowered_dag.nodes.is_empty()); + assert!(output.derived.manifest.total_nodes > 0); + assert!(!output.emitted.files.is_empty()); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); + } + #[test] fn compile_single_file_unresolved_uses_fails_in_lower_stage() { let fixture = unique_temp_file("single_file_unresolved_uses"); diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index 26f72d445f7..0bbbd2c5185 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -10097,6 +10097,83 @@ func run(path: String) -> { body: String } { std::fs::remove_file(fixture).expect("failed to cleanup fixture"); } +#[test] +fn compile_command_single_file_accepts_resource_bound_capability_calls() { + let fixture = unique_temp_file("compile_resource_bound_capability_single_file"); + std::fs::write( + &fixture, + r#"module sample.resources +resource Filesystem { + capability read { + input { path: String } + output { body: String } + } +} +func run(path: String) -> { body: String } uses fs: Filesystem { + let response = fs.read(path: path) + return { body: response.body } +} +"#, + ) + .expect("failed to write fixture"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&fixture) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile for resource-bound capability fixture"); + + assert!( + output.status.success(), + "single-file compile should accept resource-bound capability calls: {}", + String::from_utf8_lossy(&output.stderr) + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert_no_stage_failures(&stderr); + + std::fs::remove_file(fixture).expect("failed to cleanup fixture"); +} + +#[test] +fn compile_command_directory_mode_accepts_resource_bound_capability_calls() { + let root = unique_temp_dir("resource_bound_capability_directory_mode"); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp dir"); + std::fs::write( + root.join("sample/main.dag"), + r#"module sample.main +resource Filesystem { + capability read { + input { path: String } + output { body: String } + } +} +func run(path: String) -> { body: String } uses fs: Filesystem { + let response = fs.read(path: path) + return { body: response.body } +} +"#, + ) + .expect("failed to write source"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&root) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile on directory"); + + assert!( + output.status.success(), + "directory compile should accept resource-bound capability calls: {}", + String::from_utf8_lossy(&output.stderr) + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); +} + #[test] fn compile_command_single_file_unresolved_uses_reports_lower_error() { let fixture = unique_temp_file("compile_unresolved_uses_single_file"); diff --git a/core/daglang/daglang-lower/src/lib.rs b/core/daglang/daglang-lower/src/lib.rs index 9d432ff7882..b02952a674b 100644 --- a/core/daglang/daglang-lower/src/lib.rs +++ b/core/daglang/daglang-lower/src/lib.rs @@ -1195,10 +1195,31 @@ fn add_service_call_edges( for module in &project.modules { let module_name = module.module_path.join("."); for item in &module.ast.items { - let (item_name, params, stmts) = match &item.node { - Item::FnDef(def) => (&def.name, &def.params, def.body.stmts.as_slice()), - Item::FuncDef(def) => (&def.name, &def.params, def.body.stmts.as_slice()), - Item::PatternDef(def) => (&def.name, &def.params, def.body.stmts.as_slice()), + let (item_name, params, stmts, uses_bindings) = match &item.node { + Item::FnDef(def) => ( + &def.name, + &def.params, + def.body.stmts.as_slice(), + HashSet::new(), + ), + Item::FuncDef(def) => ( + &def.name, + &def.params, + def.body.stmts.as_slice(), + def.uses + .iter() + .map(|usage| usage.binding.clone()) + .collect::>(), + ), + Item::PatternDef(def) => ( + &def.name, + &def.params, + def.body.stmts.as_slice(), + def.uses + .iter() + .map(|usage| usage.binding.clone()) + .collect::>(), + ), _ => continue, }; let Some(target) = @@ -1214,6 +1235,13 @@ fn add_service_call_edges( collect_service_calls_from_stmts(stmts, &mut service_calls); for call in service_calls { let Some(source) = resolve_service_endpoint(&call.path, service_registry) else { + if call + .path + .first() + .is_some_and(|segment| uses_bindings.contains(segment)) + { + continue; + } return Err(LowerError::UnresolvedServiceCall { caller: format!("{module_name}::{item_name}"), service_call: call.path.join("."), @@ -2065,6 +2093,29 @@ func run(path: String) -> { body: String } { })); } + #[test] + fn resource_bound_capability_calls_do_not_raise_unresolved_service_call_errors() { + let typed = typed_project_from_sources(&[( + "dsl/resources/resource_capability_calls.dag", + r#"module sample.resources +resource Filesystem { + capability read { + input { path: String } + output { body: String } + } +} +func run(path: String) -> { body: String } uses fs: Filesystem { + let response = fs.read(path: path) + return { body: response.body } +}"#, + )]); + let dag = lower_typed_project(&typed).expect("lowering should succeed"); + assert!(dag + .nodes + .iter() + .any(|node| node.id.0 == "sample.resources::run")); + } + #[test] fn unresolved_service_call_reports_lower_error() { let typed = typed_project_from_sources(&[( diff --git a/core/daglang/daglang-typecheck/src/lib.rs b/core/daglang/daglang-typecheck/src/lib.rs index 3c5cc1fe71c..257821e36ae 100644 --- a/core/daglang/daglang-typecheck/src/lib.rs +++ b/core/daglang/daglang-typecheck/src/lib.rs @@ -442,6 +442,7 @@ pub fn typecheck_module_graph_with_options( let service_call_registry = collect_service_call_contracts(&graph.modules); let interface_registry = collect_interfaces(&graph.modules); let resource_type_registry = collect_resource_types(&graph.modules); + let resource_capability_registry = collect_resource_capabilities(&graph.modules); let available_modules = graph .modules .iter() @@ -457,6 +458,7 @@ pub fn typecheck_module_graph_with_options( service_call_registry: &service_call_registry, interface_registry: &interface_registry, resource_type_registry: &resource_type_registry, + resource_capability_registry: &resource_capability_registry, allow_unresolved_references: options.allow_unresolved_imports, }; @@ -506,6 +508,7 @@ struct TypecheckContext<'a> { service_call_registry: &'a ServiceCallRegistry, interface_registry: &'a InterfaceRegistry, resource_type_registry: &'a ResourceTypeRegistry, + resource_capability_registry: &'a ResourceCapabilityRegistry, allow_unresolved_references: bool, } @@ -530,6 +533,10 @@ fn collect_signatures( record_type_registry: context.record_type_registry, callable_registry: context.callable_registry, service_call_registry: context.service_call_registry, + interface_registry: context.interface_registry, + resource_type_registry: context.resource_type_registry, + resource_capability_registry: context.resource_capability_registry, + allow_unresolved_references: context.allow_unresolved_references, }; for item in &module.ast.items { @@ -569,9 +576,9 @@ fn collect_signatures( &def.name, &def.params, ReturnContract::single(type_expr_to_string(&def.return_type)), + &[], &def.body.stmts, &body_context, - context.allow_unresolved_references, errors, ); signatures.push(TypedItemSignature::Fn(TypedCallableSignature { @@ -622,9 +629,9 @@ fn collect_signatures( &def.name, &def.params, ReturnContract::record(field_signature_map(&def.outputs)), + &def.uses, &def.body.stmts, &body_context, - context.allow_unresolved_references, errors, ); signatures.push(TypedItemSignature::Func(TypedCallableSignature { @@ -674,9 +681,9 @@ fn collect_signatures( &def.name, &def.params, ReturnContract::record(field_signature_map(&def.outputs)), + &def.uses, &def.body.stmts, &body_context, - context.allow_unresolved_references, errors, ); signatures.push(TypedItemSignature::Pattern(TypedCallableSignature { @@ -948,6 +955,11 @@ struct ResourceTypeRegistry { short: HashMap>, } +#[derive(Debug, Clone, Default)] +struct ResourceCapabilityRegistry { + full: HashMap>, +} + #[derive(Debug, Clone, Default)] struct GenericArityRegistry { full: HashMap, @@ -988,6 +1000,29 @@ struct BodyInferenceContext<'a> { record_type_registry: &'a RecordTypeRegistry, callable_registry: &'a HashMap>, service_call_registry: &'a ServiceCallRegistry, + interface_registry: &'a InterfaceRegistry, + resource_type_registry: &'a ResourceTypeRegistry, + resource_capability_registry: &'a ResourceCapabilityRegistry, + allow_unresolved_references: bool, +} + +#[derive(Debug, Clone, Default)] +struct BoundServiceCallRegistry { + by_binding: HashMap, +} + +#[derive(Debug, Clone)] +enum BoundServiceCallBinding { + Resolved(HashMap), + Deferred, +} + +#[derive(Debug, Clone)] +enum BoundServiceCallResolution { + Resolved(ServiceCallContract), + MissingCapability, + Deferred, + NotBound, } fn collect_interfaces(modules: &[ResolvedModule]) -> InterfaceRegistry { @@ -1052,6 +1087,35 @@ fn collect_resource_types(modules: &[ResolvedModule]) -> ResourceTypeRegistry { registry } +fn collect_resource_capabilities(modules: &[ResolvedModule]) -> ResourceCapabilityRegistry { + let mut registry = ResourceCapabilityRegistry::default(); + for module in modules { + let module_name = module.module_path.join("."); + for item in &module.ast.items { + let Item::ResourceDef(resource) = &item.node else { + continue; + }; + let capabilities = resource + .capabilities + .iter() + .map(|capability| { + ( + capability.name.clone(), + CapabilityContract { + inputs: field_signature_map(&capability.inputs), + outputs: field_signature_map(&capability.outputs), + }, + ) + }) + .collect::>(); + registry + .full + .insert(format!("{module_name}.{}", resource.name), capabilities); + } + } + registry +} + #[derive(Debug, Clone, PartialEq, Eq)] struct InterfaceContract { capabilities: HashMap, @@ -1267,18 +1331,19 @@ fn validate_callable_body( caller: &str, params: &[Param], return_contract: ReturnContract, + uses: &[UsesClause], stmts: &[Stmt], body_context: &BodyInferenceContext<'_>, - allow_unresolved_references: bool, errors: &mut Vec, ) { + let bound_service_registry = build_bound_service_call_registry(uses, body_context); let mut calls = Vec::new(); collect_calls_from_stmts(stmts, &mut calls); for call in calls { let contract = match body_context.callable_registry.get(&call.callee) { Some(Some(contract)) => contract, Some(None) => { - if !allow_unresolved_references { + if !body_context.allow_unresolved_references { errors.push(TypeError::AmbiguousCallTarget { caller: caller.to_string(), callee: call.callee.clone(), @@ -1287,7 +1352,7 @@ fn validate_callable_body( continue; } None => { - if !allow_unresolved_references { + if !body_context.allow_unresolved_references { errors.push(TypeError::UnresolvedCallTarget { caller: caller.to_string(), callee: call.callee.clone(), @@ -1328,28 +1393,38 @@ fn validate_callable_body( collect_service_calls_from_stmts(stmts, &mut service_calls); for call in service_calls { let service_call_name = call.path.join("."); - let contract = - match resolve_service_call_contract(&call.path, body_context.service_call_registry) { - ServiceCallResolution::Resolved(contract) => contract, + let contract = match resolve_service_call_contract(&call.path, body_context.service_call_registry) + { + ServiceCallResolution::Resolved(contract) => Some(contract), ServiceCallResolution::Ambiguous => { - if !allow_unresolved_references { + if !body_context.allow_unresolved_references { errors.push(TypeError::AmbiguousServiceCall { caller: caller.to_string(), - service_call: service_call_name, + service_call: service_call_name.clone(), }); } - continue; + None } ServiceCallResolution::Missing => { - if !allow_unresolved_references { - errors.push(TypeError::UnresolvedServiceCall { - caller: caller.to_string(), - service_call: service_call_name, - }); + match resolve_bound_service_call_contract(&call.path, &bound_service_registry) { + BoundServiceCallResolution::Resolved(contract) => Some(contract), + BoundServiceCallResolution::MissingCapability + | BoundServiceCallResolution::NotBound => { + if !body_context.allow_unresolved_references { + errors.push(TypeError::UnresolvedServiceCall { + caller: caller.to_string(), + service_call: service_call_name.clone(), + }); + } + None + } + BoundServiceCallResolution::Deferred => None, } - continue; } }; + let Some(contract) = contract else { + continue; + }; if call.arg_count != contract.arity { errors.push(TypeError::ServiceCallArityMismatch { caller: caller.to_string(), @@ -1396,6 +1471,7 @@ fn validate_callable_body( expr, &local_bindings, body_context, + &bound_service_registry, errors, ); local_bindings.insert(name.clone(), inferred); @@ -1406,6 +1482,7 @@ fn validate_callable_body( expr, &local_bindings, body_context, + &bound_service_registry, errors, )); } @@ -1418,6 +1495,7 @@ fn validate_callable_body( fields, &local_bindings, body_context, + &bound_service_registry, errors, ); } @@ -1438,6 +1516,7 @@ fn validate_return_stmt( fields: &[(String, Expr)], local_bindings: &HashMap, body_context: &BodyInferenceContext<'_>, + bound_service_registry: &BoundServiceCallRegistry, errors: &mut Vec, ) { match return_contract { @@ -1453,6 +1532,7 @@ fn validate_return_stmt( &fields[0].1, local_bindings, body_context, + bound_service_registry, errors, ); push_type_mismatch_if_needed(ty, &inferred, errors); @@ -1470,6 +1550,7 @@ fn validate_return_stmt( expr, local_bindings, body_context, + bound_service_registry, errors, ); push_type_mismatch_if_needed(expected_ty, &inferred, errors); @@ -1482,6 +1563,7 @@ fn infer_expr_type( expr: &Expr, local_bindings: &HashMap, body_context: &BodyInferenceContext<'_>, + bound_service_registry: &BoundServiceCallRegistry, errors: &mut Vec, ) -> ValueType { match expr { @@ -1494,7 +1576,13 @@ fn infer_expr_type( }, Expr::Ident(name) => local_bindings.get(name).cloned().unwrap_or(ValueType::Unknown), Expr::FieldAccess(base, field) => { - let base_type = infer_expr_type(base, local_bindings, body_context, errors); + let base_type = infer_expr_type( + base, + local_bindings, + body_context, + bound_service_registry, + errors, + ); match base_type { ValueType::Record(fields) => match fields.get(field) { Some(ty) => ValueType::Named(ty.clone()), @@ -1527,7 +1615,13 @@ fn infer_expr_type( } Expr::Call(name, args) => { for (_, arg) in args { - infer_expr_type(arg, local_bindings, body_context, errors); + infer_expr_type( + arg, + local_bindings, + body_context, + bound_service_registry, + errors, + ); } body_context .callable_registry @@ -1538,18 +1632,44 @@ fn infer_expr_type( } Expr::ServiceCall(path, args) => { for (_, arg) in args { - infer_expr_type(arg, local_bindings, body_context, errors); + infer_expr_type( + arg, + local_bindings, + body_context, + bound_service_registry, + errors, + ); } match resolve_service_call_contract(path, body_context.service_call_registry) { ServiceCallResolution::Resolved(contract) => ValueType::Record(contract.outputs), - ServiceCallResolution::Ambiguous | ServiceCallResolution::Missing => { - ValueType::Unknown + ServiceCallResolution::Ambiguous => ValueType::Unknown, + ServiceCallResolution::Missing => { + match resolve_bound_service_call_contract(path, bound_service_registry) { + BoundServiceCallResolution::Resolved(contract) => { + ValueType::Record(contract.outputs) + } + BoundServiceCallResolution::MissingCapability + | BoundServiceCallResolution::Deferred + | BoundServiceCallResolution::NotBound => ValueType::Unknown, + } } } } Expr::BinOp(lhs, op, rhs) => { - let lhs_ty = infer_expr_type(lhs, local_bindings, body_context, errors); - let rhs_ty = infer_expr_type(rhs, local_bindings, body_context, errors); + let lhs_ty = infer_expr_type( + lhs, + local_bindings, + body_context, + bound_service_registry, + errors, + ); + let rhs_ty = infer_expr_type( + rhs, + local_bindings, + body_context, + bound_service_registry, + errors, + ); match op { daglang_syntax::ast::BinOp::Eq | daglang_syntax::ast::BinOp::Ne @@ -1570,7 +1690,13 @@ fn infer_expr_type( } } Expr::UnaryOp(op, inner) => { - let inner_ty = infer_expr_type(inner, local_bindings, body_context, errors); + let inner_ty = infer_expr_type( + inner, + local_bindings, + body_context, + bound_service_registry, + errors, + ); match op { daglang_syntax::ast::UnaryOp::Not => ValueType::Named("Bool".to_string()), daglang_syntax::ast::UnaryOp::Neg => inner_ty, @@ -1579,14 +1705,26 @@ fn infer_expr_type( Expr::StringInterp(parts) => { for part in parts { if let daglang_syntax::ast::StringPart::Expr(inner) = part { - infer_expr_type(inner, local_bindings, body_context, errors); + infer_expr_type( + inner, + local_bindings, + body_context, + bound_service_registry, + errors, + ); } } ValueType::Named("String".to_string()) } Expr::Record(type_name, fields) => { for (_, value) in fields { - infer_expr_type(value, local_bindings, body_context, errors); + infer_expr_type( + value, + local_bindings, + body_context, + bound_service_registry, + errors, + ); } if let Some(name) = type_name { ValueType::Named(name.clone()) @@ -1597,7 +1735,13 @@ fn infer_expr_type( .map(|(name, expr)| { ( name.clone(), - infer_expr_type(expr, local_bindings, body_context, errors) + infer_expr_type( + expr, + local_bindings, + body_context, + bound_service_registry, + errors, + ) .display_name() .unwrap_or_else(|| "Any".to_string()), ) @@ -1607,20 +1751,56 @@ fn infer_expr_type( } } Expr::Match(scrutinee, arms) => { - infer_expr_type(scrutinee, local_bindings, body_context, errors); + infer_expr_type( + scrutinee, + local_bindings, + body_context, + bound_service_registry, + errors, + ); for arm in arms { if let Some(guard) = &arm.guard { - infer_expr_type(guard, local_bindings, body_context, errors); + infer_expr_type( + guard, + local_bindings, + body_context, + bound_service_registry, + errors, + ); } - infer_expr_type(&arm.body, local_bindings, body_context, errors); + infer_expr_type( + &arm.body, + local_bindings, + body_context, + bound_service_registry, + errors, + ); } ValueType::Unknown } Expr::If(cond, then_expr, else_expr) => { - infer_expr_type(cond, local_bindings, body_context, errors); - let then_ty = infer_expr_type(then_expr, local_bindings, body_context, errors); + infer_expr_type( + cond, + local_bindings, + body_context, + bound_service_registry, + errors, + ); + let then_ty = infer_expr_type( + then_expr, + local_bindings, + body_context, + bound_service_registry, + errors, + ); let else_ty = else_expr.as_ref().map(|otherwise| { - infer_expr_type(otherwise, local_bindings, body_context, errors) + infer_expr_type( + otherwise, + local_bindings, + body_context, + bound_service_registry, + errors, + ) }); match else_ty { Some(otherwise) @@ -1633,41 +1813,113 @@ fn infer_expr_type( } } Expr::For(_, iterable, body) => { - infer_expr_type(iterable, local_bindings, body_context, errors); - infer_expr_type(body, local_bindings, body_context, errors); + infer_expr_type( + iterable, + local_bindings, + body_context, + bound_service_registry, + errors, + ); + infer_expr_type( + body, + local_bindings, + body_context, + bound_service_registry, + errors, + ); ValueType::Unknown } Expr::Pipe(lhs, rhs) => { - infer_expr_type(lhs, local_bindings, body_context, errors); - infer_expr_type(rhs, local_bindings, body_context, errors) + infer_expr_type( + lhs, + local_bindings, + body_context, + bound_service_registry, + errors, + ); + infer_expr_type( + rhs, + local_bindings, + body_context, + bound_service_registry, + errors, + ) } - Expr::Lambda(_, body) => infer_expr_type(body, local_bindings, body_context, errors), + Expr::Lambda(_, body) => infer_expr_type( + body, + local_bindings, + body_context, + bound_service_registry, + errors, + ), Expr::List(items) => { for item in items { - infer_expr_type(item, local_bindings, body_context, errors); + infer_expr_type( + item, + local_bindings, + body_context, + bound_service_registry, + errors, + ); } ValueType::Named("List".to_string()) } Expr::Map(entries) => { for (key, value) in entries { - infer_expr_type(key, local_bindings, body_context, errors); - infer_expr_type(value, local_bindings, body_context, errors); + infer_expr_type( + key, + local_bindings, + body_context, + bound_service_registry, + errors, + ); + infer_expr_type( + value, + local_bindings, + body_context, + bound_service_registry, + errors, + ); } ValueType::Named("Map".to_string()) } Expr::Guarded(inner, guard) => { - infer_expr_type(inner, local_bindings, body_context, errors); - infer_expr_type(guard, local_bindings, body_context, errors); + infer_expr_type( + inner, + local_bindings, + body_context, + bound_service_registry, + errors, + ); + infer_expr_type( + guard, + local_bindings, + body_context, + bound_service_registry, + errors, + ); ValueType::Unknown } - Expr::After(inner, _) => infer_expr_type(inner, local_bindings, body_context, errors), + Expr::After(inner, _) => infer_expr_type( + inner, + local_bindings, + body_context, + bound_service_registry, + errors, + ), Expr::Return(fields) => ValueType::Record( fields .iter() .map(|(name, expr)| { ( name.clone(), - infer_expr_type(expr, local_bindings, body_context, errors) + infer_expr_type( + expr, + local_bindings, + body_context, + bound_service_registry, + errors, + ) .display_name() .unwrap_or_else(|| "Any".to_string()), ) @@ -1977,6 +2229,85 @@ fn resolve_service_call_contract( } } +fn build_bound_service_call_registry( + uses: &[UsesClause], + body_context: &BodyInferenceContext<'_>, +) -> BoundServiceCallRegistry { + let mut registry = BoundServiceCallRegistry::default(); + for usage in uses { + let resource_type = resource_type_name(&usage.resource_type); + let binding = match resolve_resource_type_name(&resource_type, body_context.resource_type_registry) { + ResourceTypeResolution::Resolved(resolved_type) => { + if let Some(interface_contract) = + body_context.interface_registry.full.get(&resolved_type) + { + let capabilities = interface_contract + .capabilities + .iter() + .map(|(name, contract)| { + ( + name.clone(), + ServiceCallContract { + arity: contract.inputs.len(), + params: contract.inputs.keys().cloned().collect(), + outputs: contract.outputs.clone(), + }, + ) + }) + .collect::>(); + BoundServiceCallBinding::Resolved(capabilities) + } else if let Some(resource_capabilities) = + body_context.resource_capability_registry.full.get(&resolved_type) + { + let capabilities = resource_capabilities + .iter() + .map(|(name, contract)| { + ( + name.clone(), + ServiceCallContract { + arity: contract.inputs.len(), + params: contract.inputs.keys().cloned().collect(), + outputs: contract.outputs.clone(), + }, + ) + }) + .collect::>(); + BoundServiceCallBinding::Resolved(capabilities) + } else { + BoundServiceCallBinding::Deferred + } + } + ResourceTypeResolution::Ambiguous | ResourceTypeResolution::Missing => { + BoundServiceCallBinding::Deferred + } + }; + registry.by_binding.insert(usage.binding.clone(), binding); + } + registry +} + +fn resolve_bound_service_call_contract( + call_path: &[String], + registry: &BoundServiceCallRegistry, +) -> BoundServiceCallResolution { + if call_path.len() != 2 { + return BoundServiceCallResolution::NotBound; + } + let binding = &call_path[0]; + let capability = &call_path[1]; + let Some(binding_contracts) = registry.by_binding.get(binding) else { + return BoundServiceCallResolution::NotBound; + }; + match binding_contracts { + BoundServiceCallBinding::Resolved(capabilities) => capabilities + .get(capability) + .cloned() + .map(BoundServiceCallResolution::Resolved) + .unwrap_or(BoundServiceCallResolution::MissingCapability), + BoundServiceCallBinding::Deferred => BoundServiceCallResolution::Deferred, + } +} + fn canonical_type_name(name: &str) -> String { name.split('<').next().unwrap_or(name).trim().to_string() } @@ -2768,6 +3099,69 @@ func run() -> { ok: Bool } { ))); } + #[test] + fn uses_bound_resource_capability_call_typechecks_and_infers_outputs() { + let graph = module_graph_from_sources(&[( + "resource_bound_service_call.dag", + r#"module sample.resources +resource Filesystem { + capability read { + input { path: String } + output { body: String } + } +} +func run(path: String) -> { body: String } uses fs: Filesystem { + let response = fs.read(path: path) + return { body: response.body } +}"#, + )]); + let typed = typecheck_module_graph_with_options( + graph, + TypecheckOptions { + allow_unresolved_imports: false, + }, + ) + .expect("resource-bound capability calls should typecheck in strict mode"); + assert_eq!(typed.modules.len(), 1); + } + + #[test] + fn uses_bound_resource_capability_call_reports_arity_mismatch() { + let graph = module_graph_from_sources(&[( + "resource_bound_service_call_arity.dag", + r#"module sample.resources +resource Filesystem { + capability read { + input { path: String } + output { body: String } + } +} +func run() -> { ok: Bool } uses fs: Filesystem { + let response = fs.read() + return { ok: true } +}"#, + )]); + let errors = typecheck_module_graph_with_options( + graph, + TypecheckOptions { + allow_unresolved_imports: false, + }, + ) + .expect_err("resource-bound capability calls should enforce arity"); + assert!(errors.iter().any(|error| matches!( + error, + TypeError::ServiceCallArityMismatch { + caller, + service_call, + expected, + got + } if caller == "run" + && service_call == "fs.read" + && *expected == 1 + && *got == 0 + ))); + } + #[test] fn strict_mode_reports_unresolved_service_call() { let graph = module_graph_from_sources(&[( From 9c447a53fea9ae00c852e2d5715b468ed752f23c Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 15:27:51 +0000 Subject: [PATCH 160/169] Register collection intrinsic callable contracts in strict mode Co-authored-by: Brian Searls --- core/daglang/daglang-cli/src/compile.rs | 42 ++++++ .../daglang-cli/tests/compile_commands.rs | 40 +++++ core/daglang/daglang-typecheck/src/lib.rs | 139 ++++++++++++++++++ 3 files changed, 221 insertions(+) diff --git a/core/daglang/daglang-cli/src/compile.rs b/core/daglang/daglang-cli/src/compile.rs index 02e895929af..2a37a42b92a 100644 --- a/core/daglang/daglang-cli/src/compile.rs +++ b/core/daglang/daglang-cli/src/compile.rs @@ -1068,6 +1068,48 @@ fn run() -> Unit { std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } + #[test] + fn compile_directory_collection_intrinsics_typecheck_in_strict_mode() { + let root = std::env::temp_dir().join(format!( + "daglang_compile_collection_intrinsics_dir_{}_{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("system clock should be after unix epoch") + .as_nanos() + )); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp root"); + std::fs::write( + root.join("sample/main.dag"), + r#"module sample.main +type Stage { + success: Bool, + skipped: Bool, + name: String +} +fn summarize(stages: List) -> Int { + let passed = stages |> filter(s => s.success) |> count() + let labels = stages |> map(s => s.name) |> join(",") + let done = labels |> ends_with("ok") + passed +} +"#, + ) + .expect("failed to write collection intrinsic source"); + + let context = PipelineContext { + roots: vec![root.clone()], + target_file: None, + }; + + let output = compile_from_context(&context).expect("compile should succeed"); + assert!(!output.lowered_dag.nodes.is_empty()); + assert!(output.derived.manifest.total_nodes > 0); + assert!(!output.emitted.files.is_empty()); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); + } + #[test] fn compile_single_file_duplicate_service_suppresses_ambiguous_service_call() { let fixture = unique_temp_file("single_file_duplicate_service"); diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index 0bbbd2c5185..a0b40d400d7 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -13111,6 +13111,46 @@ fn compile_command_directory_mode_fails_on_unresolved_callable_target() { std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); } +#[test] +fn compile_command_directory_mode_accepts_collection_intrinsic_call_targets() { + let root = unique_temp_dir("collection_intrinsic_call_targets"); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp dir"); + std::fs::write( + root.join("sample/main.dag"), + r#"module sample.main +type Stage { + success: Bool, + skipped: Bool, + name: String +} +fn summarize(stages: List) -> Int { + let passed = stages |> filter(s => s.success) |> count() + let labels = stages |> map(s => s.name) |> join(",") + let done = labels |> ends_with("ok") + passed +} +"#, + ) + .expect("failed to write source"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&root) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile on directory"); + + assert!( + output.status.success(), + "directory compile should accept collection intrinsic call targets: {}", + String::from_utf8_lossy(&output.stderr) + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); +} + #[test] fn compile_command_directory_mode_fails_on_type_mismatch() { let root = unique_temp_dir("type_mismatch"); diff --git a/core/daglang/daglang-typecheck/src/lib.rs b/core/daglang/daglang-typecheck/src/lib.rs index 257821e36ae..2364725385b 100644 --- a/core/daglang/daglang-typecheck/src/lib.rs +++ b/core/daglang/daglang-typecheck/src/lib.rs @@ -892,9 +892,121 @@ fn collect_unique_callables( .or_insert_with(|| Some(contract)); } } + for (name, contract) in builtin_callable_contracts() { + callables.entry(name).or_insert(Some(contract)); + } callables } +fn builtin_callable_contracts() -> Vec<(String, CallableContract)> { + vec![ + ( + "map".to_string(), + CallableContract { + arity: 1, + params: HashSet::from(["f".to_string()]), + output: ValueType::Named("List".to_string()), + }, + ), + ( + "filter".to_string(), + CallableContract { + arity: 1, + params: HashSet::from(["predicate".to_string()]), + output: ValueType::Named("List".to_string()), + }, + ), + ( + "join".to_string(), + CallableContract { + arity: 1, + params: HashSet::from(["separator".to_string()]), + output: ValueType::Named("String".to_string()), + }, + ), + ( + "count".to_string(), + CallableContract { + arity: 0, + params: HashSet::new(), + output: ValueType::Named("Int".to_string()), + }, + ), + ( + "all".to_string(), + CallableContract { + arity: 1, + params: HashSet::from(["predicate".to_string()]), + output: ValueType::Named("Bool".to_string()), + }, + ), + ( + "sort_by".to_string(), + CallableContract { + arity: 1, + params: HashSet::from(["key_fn".to_string()]), + output: ValueType::Named("List".to_string()), + }, + ), + ( + "first".to_string(), + CallableContract { + arity: 0, + params: HashSet::new(), + output: ValueType::Named("Any".to_string()), + }, + ), + ( + "last".to_string(), + CallableContract { + arity: 0, + params: HashSet::new(), + output: ValueType::Named("Any".to_string()), + }, + ), + ( + "ends_with".to_string(), + CallableContract { + arity: 1, + params: HashSet::from(["suffix".to_string()]), + output: ValueType::Named("Bool".to_string()), + }, + ), + ( + "to_bytes".to_string(), + CallableContract { + arity: 1, + params: HashSet::from(["value".to_string()]), + output: ValueType::Named("Bytes".to_string()), + }, + ), + ( + "to_json".to_string(), + CallableContract { + arity: 1, + params: HashSet::from(["value".to_string()]), + output: ValueType::Named("Json".to_string()), + }, + ), + ( + "hash".to_string(), + CallableContract { + arity: 1, + params: HashSet::from(["value".to_string()]), + output: ValueType::Named("String".to_string()), + }, + ), + ( + "eq".to_string(), + CallableContract { + arity: 2, + params: HashSet::from(["lhs".to_string(), "rhs".to_string()]), + output: ValueType::Named("Bool".to_string()), + }, + ), + ] +} + fn collect_service_call_contracts(modules: &[ResolvedModule]) -> ServiceCallRegistry { let mut registry = ServiceCallRegistry::default(); for module in modules { @@ -3028,6 +3140,33 @@ fn run() -> String { helper() }"#, assert_eq!(typed.modules.len(), 1); } + #[test] + fn strict_mode_accepts_collection_intrinsic_call_targets() { + let graph = module_graph_from_sources(&[( + "sample/intrinsics.dag", + r#"module sample.intrinsics +type Stage { + success: Bool, + skipped: Bool, + name: String +} +fn summarize(stages: List) -> Int { + let passed = stages |> filter(s => s.success) |> count() + let labels = stages |> map(s => s.name) |> join(",") + let done = labels |> ends_with("ok") + passed +}"#, + )]); + let typed = typecheck_module_graph_with_options( + graph, + TypecheckOptions { + allow_unresolved_imports: false, + }, + ) + .expect("collection intrinsics should be recognized in strict mode"); + assert_eq!(typed.modules.len(), 1); + } + #[test] fn unknown_named_call_argument_is_reported() { let graph = module_graph_from_sources(&[( From 4ee00e414eb2b0b116ecab75c6f9514fad927cca Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 15:46:59 +0000 Subject: [PATCH 161/169] Allow strict callable checks for function-typed parameters Co-authored-by: Brian Searls --- core/daglang/daglang-cli/src/compile.rs | 34 ++ .../daglang-cli/tests/compile_commands.rs | 32 ++ core/daglang/daglang-typecheck/src/lib.rs | 457 +++++++++--------- 3 files changed, 285 insertions(+), 238 deletions(-) diff --git a/core/daglang/daglang-cli/src/compile.rs b/core/daglang/daglang-cli/src/compile.rs index 2a37a42b92a..6b6b7ecb14a 100644 --- a/core/daglang/daglang-cli/src/compile.rs +++ b/core/daglang/daglang-cli/src/compile.rs @@ -1110,6 +1110,40 @@ fn summarize(stages: List) -> Int { std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } + #[test] + fn compile_directory_function_typed_parameter_calls_typecheck_in_strict_mode() { + let root = std::env::temp_dir().join(format!( + "daglang_compile_fn_typed_param_calls_dir_{}_{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("system clock should be after unix epoch") + .as_nanos() + )); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp root"); + std::fs::write( + root.join("sample/main.dag"), + r#"module sample.main +fn apply(value: Int, callback: fn(Int) -> Int) -> Int { + callback(value) +} +"#, + ) + .expect("failed to write callback source"); + + let context = PipelineContext { + roots: vec![root.clone()], + target_file: None, + }; + + let output = compile_from_context(&context).expect("compile should succeed"); + assert!(!output.lowered_dag.nodes.is_empty()); + assert!(output.derived.manifest.total_nodes > 0); + assert!(!output.emitted.files.is_empty()); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); + } + #[test] fn compile_single_file_duplicate_service_suppresses_ambiguous_service_call() { let fixture = unique_temp_file("single_file_duplicate_service"); diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index a0b40d400d7..1736af79121 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -13151,6 +13151,38 @@ fn summarize(stages: List) -> Int { std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); } +#[test] +fn compile_command_directory_mode_accepts_function_typed_parameter_calls() { + let root = unique_temp_dir("function_typed_parameter_calls"); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp dir"); + std::fs::write( + root.join("sample/main.dag"), + r#"module sample.main +fn apply(value: Int, callback: fn(Int) -> Int) -> Int { + callback(value) +} +"#, + ) + .expect("failed to write source"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&root) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile on directory"); + + assert!( + output.status.success(), + "directory compile should accept function-typed parameter calls: {}", + String::from_utf8_lossy(&output.stderr) + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); +} + #[test] fn compile_command_directory_mode_fails_on_type_mismatch() { let root = unique_temp_dir("type_mismatch"); diff --git a/core/daglang/daglang-typecheck/src/lib.rs b/core/daglang/daglang-typecheck/src/lib.rs index 2364725385b..eef242313fd 100644 --- a/core/daglang/daglang-typecheck/src/lib.rs +++ b/core/daglang/daglang-typecheck/src/lib.rs @@ -1137,6 +1137,14 @@ enum BoundServiceCallResolution { NotBound, } +struct ExprInferenceContext<'a> { + record_type_registry: &'a RecordTypeRegistry, + callable_registry: &'a HashMap>, + service_call_registry: &'a ServiceCallRegistry, + bound_service_registry: &'a BoundServiceCallRegistry, + param_callable_contracts: &'a HashMap, +} + fn collect_interfaces(modules: &[ResolvedModule]) -> InterfaceRegistry { let mut registry = InterfaceRegistry::default(); for module in modules { @@ -1439,6 +1447,85 @@ fn validate_use_provide_binding_conflicts( } } +fn collect_param_callable_contracts(params: &[Param]) -> HashMap { + params + .iter() + .filter_map(|param| { + parse_function_type_callable_contract(¶m.ty) + .map(|contract| (param.name.clone(), contract)) + }) + .collect() +} + +fn parse_function_type_callable_contract(ty: &TypeExpr) -> Option { + let raw = type_expr_to_string(ty); + let compact = raw.chars().filter(|ch| !ch.is_whitespace()).collect::(); + if !compact.starts_with("fn(") { + return None; + } + let close_paren = find_matching_paren(&compact, 2)?; + let args = &compact[3..close_paren]; + let output = compact + .get(close_paren + 1..)? + .strip_prefix("->") + .filter(|text| !text.is_empty())? + .to_string(); + Some(CallableContract { + arity: parse_function_type_arity(args), + params: HashSet::new(), + output: ValueType::Named(output), + }) +} + +fn find_matching_paren(text: &str, open_index: usize) -> Option { + let mut depth = 0usize; + for (index, ch) in text.char_indices().filter(|(idx, _)| *idx >= open_index) { + match ch { + '(' => depth += 1, + ')' => { + depth = depth.checked_sub(1)?; + if depth == 0 { + return Some(index); + } + } + _ => {} + } + } + None +} + +fn parse_function_type_arity(args: &str) -> usize { + if args.is_empty() { + return 0; + } + let mut arity = 1usize; + let mut paren_depth = 0usize; + let mut angle_depth = 0usize; + let mut bracket_depth = 0usize; + let mut brace_depth = 0usize; + for ch in args.chars() { + match ch { + '(' => paren_depth += 1, + ')' => paren_depth = paren_depth.saturating_sub(1), + '<' => angle_depth += 1, + '>' => angle_depth = angle_depth.saturating_sub(1), + '[' => bracket_depth += 1, + ']' => bracket_depth = bracket_depth.saturating_sub(1), + '{' => brace_depth += 1, + '}' => brace_depth = brace_depth.saturating_sub(1), + ',' if paren_depth == 0 + && angle_depth == 0 + && bracket_depth == 0 + && brace_depth == 0 => + { + arity += 1; + } + _ => {} + } + } + arity +} + fn validate_callable_body( caller: &str, params: &[Param], @@ -1449,29 +1536,40 @@ fn validate_callable_body( errors: &mut Vec, ) { let bound_service_registry = build_bound_service_call_registry(uses, body_context); + let param_callable_contracts = collect_param_callable_contracts(params); + let infer_context = ExprInferenceContext { + record_type_registry: body_context.record_type_registry, + callable_registry: body_context.callable_registry, + service_call_registry: body_context.service_call_registry, + bound_service_registry: &bound_service_registry, + param_callable_contracts: ¶m_callable_contracts, + }; let mut calls = Vec::new(); collect_calls_from_stmts(stmts, &mut calls); for call in calls { - let contract = match body_context.callable_registry.get(&call.callee) { - Some(Some(contract)) => contract, - Some(None) => { - if !body_context.allow_unresolved_references { - errors.push(TypeError::AmbiguousCallTarget { - caller: caller.to_string(), - callee: call.callee.clone(), - }); + let contract = match param_callable_contracts.get(&call.callee) { + Some(contract) => contract, + None => match body_context.callable_registry.get(&call.callee) { + Some(Some(contract)) => contract, + Some(None) => { + if !body_context.allow_unresolved_references { + errors.push(TypeError::AmbiguousCallTarget { + caller: caller.to_string(), + callee: call.callee.clone(), + }); + } + continue; } - continue; - } - None => { - if !body_context.allow_unresolved_references { - errors.push(TypeError::UnresolvedCallTarget { - caller: caller.to_string(), - callee: call.callee.clone(), - }); + None => { + if !body_context.allow_unresolved_references { + errors.push(TypeError::UnresolvedCallTarget { + caller: caller.to_string(), + callee: call.callee.clone(), + }); + } + continue; } - continue; - } + }, }; if call.arg_count != contract.arity { errors.push(TypeError::CallArityMismatch { @@ -1582,8 +1680,7 @@ fn validate_callable_body( let inferred = infer_expr_type( expr, &local_bindings, - body_context, - &bound_service_registry, + &infer_context, errors, ); local_bindings.insert(name.clone(), inferred); @@ -1593,8 +1690,7 @@ fn validate_callable_body( trailing_expr_type = Some(infer_expr_type( expr, &local_bindings, - body_context, - &bound_service_registry, + &infer_context, errors, )); } @@ -1606,8 +1702,7 @@ fn validate_callable_body( &return_contract, fields, &local_bindings, - body_context, - &bound_service_registry, + &infer_context, errors, ); } @@ -1627,8 +1722,7 @@ fn validate_return_stmt( return_contract: &ReturnContract, fields: &[(String, Expr)], local_bindings: &HashMap, - body_context: &BodyInferenceContext<'_>, - bound_service_registry: &BoundServiceCallRegistry, + infer_context: &ExprInferenceContext<'_>, errors: &mut Vec, ) { match return_contract { @@ -1643,8 +1737,7 @@ fn validate_return_stmt( let inferred = infer_expr_type( &fields[0].1, local_bindings, - body_context, - bound_service_registry, + infer_context, errors, ); push_type_mismatch_if_needed(ty, &inferred, errors); @@ -1661,8 +1754,7 @@ fn validate_return_stmt( let inferred = infer_expr_type( expr, local_bindings, - body_context, - bound_service_registry, + infer_context, errors, ); push_type_mismatch_if_needed(expected_ty, &inferred, errors); @@ -1674,8 +1766,7 @@ fn validate_return_stmt( fn infer_expr_type( expr: &Expr, local_bindings: &HashMap, - body_context: &BodyInferenceContext<'_>, - bound_service_registry: &BoundServiceCallRegistry, + infer_context: &ExprInferenceContext<'_>, errors: &mut Vec, ) -> ValueType { match expr { @@ -1688,13 +1779,7 @@ fn infer_expr_type( }, Expr::Ident(name) => local_bindings.get(name).cloned().unwrap_or(ValueType::Unknown), Expr::FieldAccess(base, field) => { - let base_type = infer_expr_type( - base, - local_bindings, - body_context, - bound_service_registry, - errors, - ); + let base_type = infer_expr_type(base, local_bindings, infer_context, errors); match base_type { ValueType::Record(fields) => match fields.get(field) { Some(ty) => ValueType::Named(ty.clone()), @@ -1706,10 +1791,8 @@ fn infer_expr_type( ValueType::Unknown } }, - ValueType::Named(name) => match resolve_record_fields( - &name, - body_context.record_type_registry, - ) { + ValueType::Named(name) => { + match resolve_record_fields(&name, infer_context.record_type_registry) { Some(fields) => match fields.get(field) { Some(ty) => ValueType::Named(ty.clone()), None => { @@ -1721,42 +1804,38 @@ fn infer_expr_type( } }, None => ValueType::Unknown, - }, + } + } ValueType::Unknown => ValueType::Unknown, } } Expr::Call(name, args) => { for (_, arg) in args { - infer_expr_type( - arg, - local_bindings, - body_context, - bound_service_registry, - errors, - ); + infer_expr_type(arg, local_bindings, infer_context, errors); + } + if let Some(contract) = infer_context.param_callable_contracts.get(name) { + contract.output.clone() + } else { + infer_context + .callable_registry + .get(name) + .and_then(|entry| entry.as_ref()) + .map(|contract| contract.output.clone()) + .unwrap_or(ValueType::Unknown) } - body_context - .callable_registry - .get(name) - .and_then(|entry| entry.as_ref()) - .map(|contract| contract.output.clone()) - .unwrap_or(ValueType::Unknown) } Expr::ServiceCall(path, args) => { for (_, arg) in args { - infer_expr_type( - arg, - local_bindings, - body_context, - bound_service_registry, - errors, - ); + infer_expr_type(arg, local_bindings, infer_context, errors); } - match resolve_service_call_contract(path, body_context.service_call_registry) { + match resolve_service_call_contract(path, infer_context.service_call_registry) { ServiceCallResolution::Resolved(contract) => ValueType::Record(contract.outputs), ServiceCallResolution::Ambiguous => ValueType::Unknown, ServiceCallResolution::Missing => { - match resolve_bound_service_call_contract(path, bound_service_registry) { + match resolve_bound_service_call_contract( + path, + infer_context.bound_service_registry, + ) { BoundServiceCallResolution::Resolved(contract) => { ValueType::Record(contract.outputs) } @@ -1768,20 +1847,8 @@ fn infer_expr_type( } } Expr::BinOp(lhs, op, rhs) => { - let lhs_ty = infer_expr_type( - lhs, - local_bindings, - body_context, - bound_service_registry, - errors, - ); - let rhs_ty = infer_expr_type( - rhs, - local_bindings, - body_context, - bound_service_registry, - errors, - ); + let lhs_ty = infer_expr_type(lhs, local_bindings, infer_context, errors); + let rhs_ty = infer_expr_type(rhs, local_bindings, infer_context, errors); match op { daglang_syntax::ast::BinOp::Eq | daglang_syntax::ast::BinOp::Ne @@ -1802,13 +1869,7 @@ fn infer_expr_type( } } Expr::UnaryOp(op, inner) => { - let inner_ty = infer_expr_type( - inner, - local_bindings, - body_context, - bound_service_registry, - errors, - ); + let inner_ty = infer_expr_type(inner, local_bindings, infer_context, errors); match op { daglang_syntax::ast::UnaryOp::Not => ValueType::Named("Bool".to_string()), daglang_syntax::ast::UnaryOp::Neg => inner_ty, @@ -1817,26 +1878,14 @@ fn infer_expr_type( Expr::StringInterp(parts) => { for part in parts { if let daglang_syntax::ast::StringPart::Expr(inner) = part { - infer_expr_type( - inner, - local_bindings, - body_context, - bound_service_registry, - errors, - ); + infer_expr_type(inner, local_bindings, infer_context, errors); } } ValueType::Named("String".to_string()) } Expr::Record(type_name, fields) => { for (_, value) in fields { - infer_expr_type( - value, - local_bindings, - body_context, - bound_service_registry, - errors, - ); + infer_expr_type(value, local_bindings, infer_context, errors); } if let Some(name) = type_name { ValueType::Named(name.clone()) @@ -1847,15 +1896,9 @@ fn infer_expr_type( .map(|(name, expr)| { ( name.clone(), - infer_expr_type( - expr, - local_bindings, - body_context, - bound_service_registry, - errors, - ) - .display_name() - .unwrap_or_else(|| "Any".to_string()), + infer_expr_type(expr, local_bindings, infer_context, errors) + .display_name() + .unwrap_or_else(|| "Any".to_string()), ) }) .collect(), @@ -1863,56 +1906,20 @@ fn infer_expr_type( } } Expr::Match(scrutinee, arms) => { - infer_expr_type( - scrutinee, - local_bindings, - body_context, - bound_service_registry, - errors, - ); + infer_expr_type(scrutinee, local_bindings, infer_context, errors); for arm in arms { if let Some(guard) = &arm.guard { - infer_expr_type( - guard, - local_bindings, - body_context, - bound_service_registry, - errors, - ); + infer_expr_type(guard, local_bindings, infer_context, errors); } - infer_expr_type( - &arm.body, - local_bindings, - body_context, - bound_service_registry, - errors, - ); + infer_expr_type(&arm.body, local_bindings, infer_context, errors); } ValueType::Unknown } Expr::If(cond, then_expr, else_expr) => { - infer_expr_type( - cond, - local_bindings, - body_context, - bound_service_registry, - errors, - ); - let then_ty = infer_expr_type( - then_expr, - local_bindings, - body_context, - bound_service_registry, - errors, - ); + infer_expr_type(cond, local_bindings, infer_context, errors); + let then_ty = infer_expr_type(then_expr, local_bindings, infer_context, errors); let else_ty = else_expr.as_ref().map(|otherwise| { - infer_expr_type( - otherwise, - local_bindings, - body_context, - bound_service_registry, - errors, - ) + infer_expr_type(otherwise, local_bindings, infer_context, errors) }); match else_ty { Some(otherwise) @@ -1925,115 +1932,43 @@ fn infer_expr_type( } } Expr::For(_, iterable, body) => { - infer_expr_type( - iterable, - local_bindings, - body_context, - bound_service_registry, - errors, - ); - infer_expr_type( - body, - local_bindings, - body_context, - bound_service_registry, - errors, - ); + infer_expr_type(iterable, local_bindings, infer_context, errors); + infer_expr_type(body, local_bindings, infer_context, errors); ValueType::Unknown } Expr::Pipe(lhs, rhs) => { - infer_expr_type( - lhs, - local_bindings, - body_context, - bound_service_registry, - errors, - ); - infer_expr_type( - rhs, - local_bindings, - body_context, - bound_service_registry, - errors, - ) + infer_expr_type(lhs, local_bindings, infer_context, errors); + infer_expr_type(rhs, local_bindings, infer_context, errors) } - Expr::Lambda(_, body) => infer_expr_type( - body, - local_bindings, - body_context, - bound_service_registry, - errors, - ), + Expr::Lambda(_, body) => infer_expr_type(body, local_bindings, infer_context, errors), Expr::List(items) => { for item in items { - infer_expr_type( - item, - local_bindings, - body_context, - bound_service_registry, - errors, - ); + infer_expr_type(item, local_bindings, infer_context, errors); } ValueType::Named("List".to_string()) } Expr::Map(entries) => { for (key, value) in entries { - infer_expr_type( - key, - local_bindings, - body_context, - bound_service_registry, - errors, - ); - infer_expr_type( - value, - local_bindings, - body_context, - bound_service_registry, - errors, - ); + infer_expr_type(key, local_bindings, infer_context, errors); + infer_expr_type(value, local_bindings, infer_context, errors); } ValueType::Named("Map".to_string()) } Expr::Guarded(inner, guard) => { - infer_expr_type( - inner, - local_bindings, - body_context, - bound_service_registry, - errors, - ); - infer_expr_type( - guard, - local_bindings, - body_context, - bound_service_registry, - errors, - ); + infer_expr_type(inner, local_bindings, infer_context, errors); + infer_expr_type(guard, local_bindings, infer_context, errors); ValueType::Unknown } - Expr::After(inner, _) => infer_expr_type( - inner, - local_bindings, - body_context, - bound_service_registry, - errors, - ), + Expr::After(inner, _) => infer_expr_type(inner, local_bindings, infer_context, errors), Expr::Return(fields) => ValueType::Record( fields .iter() .map(|(name, expr)| { ( name.clone(), - infer_expr_type( - expr, - local_bindings, - body_context, - bound_service_registry, - errors, - ) - .display_name() - .unwrap_or_else(|| "Any".to_string()), + infer_expr_type(expr, local_bindings, infer_context, errors) + .display_name() + .unwrap_or_else(|| "Any".to_string()), ) }) .collect(), @@ -3167,6 +3102,52 @@ fn summarize(stages: List) -> Int { assert_eq!(typed.modules.len(), 1); } + #[test] + fn strict_mode_accepts_function_typed_parameter_call_targets() { + let graph = module_graph_from_sources(&[( + "sample/callback.dag", + r#"module sample.callback +fn apply(value: Int, callback: fn(Int) -> Int) -> Int { + callback(value) +}"#, + )]); + let typed = typecheck_module_graph_with_options( + graph, + TypecheckOptions { + allow_unresolved_imports: false, + }, + ) + .expect("function-typed parameters should be callable in strict mode"); + assert_eq!(typed.modules.len(), 1); + } + + #[test] + fn function_typed_parameter_call_reports_arity_mismatch() { + let graph = module_graph_from_sources(&[( + "sample/callback_arity.dag", + r#"module sample.callback +fn apply(callback: fn(Int) -> Int) -> Int { + callback() +}"#, + )]); + let errors = typecheck_module_graph_with_options( + graph, + TypecheckOptions { + allow_unresolved_imports: false, + }, + ) + .expect_err("callback calls should enforce function-type arity"); + assert!(errors.iter().any(|error| matches!( + error, + TypeError::CallArityMismatch { + caller, + callee, + expected, + got + } if caller == "apply" && callee == "callback" && *expected == 1 && *got == 0 + ))); + } + #[test] fn unknown_named_call_argument_is_reported() { let graph = module_graph_from_sources(&[( From 3242663d5039f00a67035130d15344003b8f6c92 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 16:02:07 +0000 Subject: [PATCH 162/169] Resolve sum-variant constructors as callable targets Co-authored-by: Brian Searls --- core/daglang/daglang-cli/src/compile.rs | 38 +++++ .../daglang-cli/tests/compile_commands.rs | 36 +++++ core/daglang/daglang-typecheck/src/lib.rs | 142 +++++++++++++++--- 3 files changed, 192 insertions(+), 24 deletions(-) diff --git a/core/daglang/daglang-cli/src/compile.rs b/core/daglang/daglang-cli/src/compile.rs index 6b6b7ecb14a..b22e8985d9e 100644 --- a/core/daglang/daglang-cli/src/compile.rs +++ b/core/daglang/daglang-cli/src/compile.rs @@ -1144,6 +1144,44 @@ fn apply(value: Int, callback: fn(Int) -> Int) -> Int { std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } + #[test] + fn compile_directory_sum_variant_constructor_calls_typecheck_in_strict_mode() { + let root = std::env::temp_dir().join(format!( + "daglang_compile_sum_variant_constructor_calls_dir_{}_{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("system clock should be after unix epoch") + .as_nanos() + )); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp root"); + std::fs::write( + root.join("sample/main.dag"), + r#"module sample.main +type CloudConfig + = GcpConfig { project: String, region: String } + | AwsConfig { region: String } + +fn make_gcp() -> CloudConfig { + GcpConfig(project: "gunbc", region: "us-central1") +} +"#, + ) + .expect("failed to write constructor source"); + + let context = PipelineContext { + roots: vec![root.clone()], + target_file: None, + }; + + let output = compile_from_context(&context).expect("compile should succeed"); + assert!(!output.lowered_dag.nodes.is_empty()); + assert!(output.derived.manifest.total_nodes > 0); + assert!(!output.emitted.files.is_empty()); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); + } + #[test] fn compile_single_file_duplicate_service_suppresses_ambiguous_service_call() { let fixture = unique_temp_file("single_file_duplicate_service"); diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index 1736af79121..04c206bda78 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -13183,6 +13183,42 @@ fn apply(value: Int, callback: fn(Int) -> Int) -> Int { std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); } +#[test] +fn compile_command_directory_mode_accepts_sum_variant_constructor_calls() { + let root = unique_temp_dir("sum_variant_constructor_calls"); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp dir"); + std::fs::write( + root.join("sample/main.dag"), + r#"module sample.main +type CloudConfig + = GcpConfig { project: String, region: String } + | AwsConfig { region: String } + +fn make_gcp() -> CloudConfig { + GcpConfig(project: "gunbc", region: "us-central1") +} +"#, + ) + .expect("failed to write source"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&root) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile on directory"); + + assert!( + output.status.success(), + "directory compile should accept sum variant constructor calls: {}", + String::from_utf8_lossy(&output.stderr) + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); +} + #[test] fn compile_command_directory_mode_fails_on_type_mismatch() { let root = unique_temp_dir("type_mismatch"); diff --git a/core/daglang/daglang-typecheck/src/lib.rs b/core/daglang/daglang-typecheck/src/lib.rs index eef242313fd..f498b899b4b 100644 --- a/core/daglang/daglang-typecheck/src/lib.rs +++ b/core/daglang/daglang-typecheck/src/lib.rs @@ -25,7 +25,7 @@ use std::path::PathBuf; use daglang_resolve::{ModuleGraph, ResolvedModule}; use daglang_syntax::ast::{ - Expr, Field, Item, Param, ProvidesClause, SourceFile, Stmt, TypeExpr, UsesClause, + Expr, Field, Item, Param, ProvidesClause, SourceFile, Stmt, TypeBody, TypeExpr, UsesClause, }; /// A typechecked project snapshot. @@ -865,31 +865,55 @@ fn collect_unique_callables( let mut callables = HashMap::>::new(); for module in modules { for item in &module.ast.items { - let (name, params, output) = match &item.node { - Item::FnDef(def) => ( - &def.name, - &def.params, - ValueType::Named(type_expr_to_string(&def.return_type)), + match &item.node { + Item::FnDef(def) => register_callable_contract( + &mut callables, + def.name.clone(), + CallableContract { + arity: def.params.len(), + params: def.params.iter().map(|param| param.name.clone()).collect(), + output: ValueType::Named(type_expr_to_string(&def.return_type)), + }, ), - Item::FuncDef(def) => (&def.name, &def.params, ValueType::Record(field_signature_map(&def.outputs))), - Item::PatternDef(def) => { - (&def.name, &def.params, ValueType::Record(field_signature_map(&def.outputs))) - } - _ => continue, - }; - let contract = CallableContract { - arity: params.len(), - params: params.iter().map(|param| param.name.clone()).collect(), - output, - }; - callables - .entry(name.clone()) - .and_modify(|existing| { - if existing.is_some() { - *existing = None; + Item::FuncDef(def) => register_callable_contract( + &mut callables, + def.name.clone(), + CallableContract { + arity: def.params.len(), + params: def.params.iter().map(|param| param.name.clone()).collect(), + output: ValueType::Record(field_signature_map(&def.outputs)), + }, + ), + Item::PatternDef(def) => register_callable_contract( + &mut callables, + def.name.clone(), + CallableContract { + arity: def.params.len(), + params: def.params.iter().map(|param| param.name.clone()).collect(), + output: ValueType::Record(field_signature_map(&def.outputs)), + }, + ), + Item::TypeDef(def) => { + if let TypeBody::Sum(variants) = &def.body { + for variant in variants { + register_callable_contract( + &mut callables, + variant.name.clone(), + CallableContract { + arity: variant.fields.len(), + params: variant + .fields + .iter() + .map(|field| field.name.clone()) + .collect(), + output: ValueType::Named(def.name.clone()), + }, + ); + } } - }) - .or_insert_with(|| Some(contract)); + } + _ => {} + } } } for (name, contract) in builtin_callable_contracts() { @@ -898,6 +922,21 @@ fn collect_unique_callables( callables } +fn register_callable_contract( + callables: &mut HashMap>, + name: String, + contract: CallableContract, +) { + callables + .entry(name) + .and_modify(|existing| { + if existing.is_some() { + *existing = None; + } + }) + .or_insert(Some(contract)); +} + fn builtin_callable_contracts() -> Vec<(String, CallableContract)> { vec![ ( @@ -3148,6 +3187,61 @@ fn apply(callback: fn(Int) -> Int) -> Int { ))); } + #[test] + fn strict_mode_accepts_sum_variant_constructor_call_targets() { + let graph = module_graph_from_sources(&[( + "sample/constructors.dag", + r#"module sample.constructors +type CloudConfig + = GcpConfig { project: String, region: String } + | AwsConfig { region: String } + +fn make_gcp() -> CloudConfig { + GcpConfig(project: "gunbc", region: "us-central1") +} +"#, + )]); + let typed = typecheck_module_graph_with_options( + graph, + TypecheckOptions { + allow_unresolved_imports: false, + }, + ) + .expect("sum variant constructors should resolve as callable targets"); + assert_eq!(typed.modules.len(), 1); + } + + #[test] + fn sum_variant_constructor_call_reports_arity_mismatch() { + let graph = module_graph_from_sources(&[( + "sample/constructor_arity.dag", + r#"module sample.constructors +type CloudConfig + = GcpConfig { project: String, region: String } + +fn make_gcp() -> CloudConfig { + GcpConfig(project: "gunbc") +} +"#, + )]); + let errors = typecheck_module_graph_with_options( + graph, + TypecheckOptions { + allow_unresolved_imports: false, + }, + ) + .expect_err("variant constructor calls should enforce field arity"); + assert!(errors.iter().any(|error| matches!( + error, + TypeError::CallArityMismatch { + caller, + callee, + expected, + got + } if caller == "make_gcp" && callee == "GcpConfig" && *expected == 2 && *got == 1 + ))); + } + #[test] fn unknown_named_call_argument_is_reported() { let graph = module_graph_from_sources(&[( From 36d79b9e0f9e4b0624e4b60def16a675fd78f1fd Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 16:05:27 +0000 Subject: [PATCH 163/169] Seed strict helper callable contracts for DSL corpus Co-authored-by: Brian Searls --- core/daglang/daglang-cli/src/compile.rs | 52 +++++++ .../daglang-cli/tests/compile_commands.rs | 50 +++++++ core/daglang/daglang-typecheck/src/lib.rs | 128 ++++++++++++++++++ 3 files changed, 230 insertions(+) diff --git a/core/daglang/daglang-cli/src/compile.rs b/core/daglang/daglang-cli/src/compile.rs index b22e8985d9e..2768a75c3fe 100644 --- a/core/daglang/daglang-cli/src/compile.rs +++ b/core/daglang/daglang-cli/src/compile.rs @@ -1182,6 +1182,58 @@ fn make_gcp() -> CloudConfig { std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } + #[test] + fn compile_directory_std_helper_intrinsics_typecheck_in_strict_mode() { + let root = std::env::temp_dir().join(format!( + "daglang_compile_std_helper_intrinsics_dir_{}_{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("system clock should be after unix epoch") + .as_nanos() + )); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp root"); + std::fs::write( + root.join("sample/main.dag"), + r#"module sample.main +type DocgenSources {} + +fn run(sources: DocgenSources, payload: String) -> String { + let a = "template" |> replace_section("section", "value") + let b = render_test_listings(sources: sources) + let c = render_graph_structure(sources: sources) + let d = render_source_artifacts(sources: sources) + let e = compute_topology_diff(current: "{}", base: "{}") + let f = render_annotated_mermaid(diff: e, topology: "{}", title: "title") + let g = detect_runtime() + let h = generate() + let i = now() + let j = build_token( + payload: payload, + scheme: "Bearer", + header_name: "Authorization", + source_id: "source", + required_scopes: ["gist"] + ) + a +} +"#, + ) + .expect("failed to write helper intrinsic source"); + + let context = PipelineContext { + roots: vec![root.clone()], + target_file: None, + }; + + let output = compile_from_context(&context).expect("compile should succeed"); + assert!(!output.lowered_dag.nodes.is_empty()); + assert!(output.derived.manifest.total_nodes > 0); + assert!(!output.emitted.files.is_empty()); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); + } + #[test] fn compile_single_file_duplicate_service_suppresses_ambiguous_service_call() { let fixture = unique_temp_file("single_file_duplicate_service"); diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index 04c206bda78..fec718e0071 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -13219,6 +13219,56 @@ fn make_gcp() -> CloudConfig { std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); } +#[test] +fn compile_command_directory_mode_accepts_std_helper_intrinsic_call_targets() { + let root = unique_temp_dir("std_helper_intrinsic_call_targets"); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp dir"); + std::fs::write( + root.join("sample/main.dag"), + r#"module sample.main +type DocgenSources {} + +fn run(sources: DocgenSources, payload: String) -> String { + let a = "template" |> replace_section("section", "value") + let b = render_test_listings(sources: sources) + let c = render_graph_structure(sources: sources) + let d = render_source_artifacts(sources: sources) + let e = compute_topology_diff(current: "{}", base: "{}") + let f = render_annotated_mermaid(diff: e, topology: "{}", title: "title") + let g = detect_runtime() + let h = generate() + let i = now() + let j = build_token( + payload: payload, + scheme: "Bearer", + header_name: "Authorization", + source_id: "source", + required_scopes: ["gist"] + ) + a +} +"#, + ) + .expect("failed to write source"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&root) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile on directory"); + + assert!( + output.status.success(), + "directory compile should accept std helper intrinsic call targets: {}", + String::from_utf8_lossy(&output.stderr) + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); +} + #[test] fn compile_command_directory_mode_fails_on_type_mismatch() { let root = unique_temp_dir("type_mismatch"); diff --git a/core/daglang/daglang-typecheck/src/lib.rs b/core/daglang/daglang-typecheck/src/lib.rs index f498b899b4b..d537e6750a9 100644 --- a/core/daglang/daglang-typecheck/src/lib.rs +++ b/core/daglang/daglang-typecheck/src/lib.rs @@ -1043,6 +1043,96 @@ fn builtin_callable_contracts() -> Vec<(String, CallableContract)> { output: ValueType::Named("Bool".to_string()), }, ), + ( + "replace_section".to_string(), + CallableContract { + arity: 2, + params: HashSet::from(["section".to_string(), "replacement".to_string()]), + output: ValueType::Named("String".to_string()), + }, + ), + ( + "render_test_listings".to_string(), + CallableContract { + arity: 1, + params: HashSet::from(["sources".to_string()]), + output: ValueType::Named("String".to_string()), + }, + ), + ( + "render_graph_structure".to_string(), + CallableContract { + arity: 1, + params: HashSet::from(["sources".to_string()]), + output: ValueType::Named("String".to_string()), + }, + ), + ( + "render_source_artifacts".to_string(), + CallableContract { + arity: 1, + params: HashSet::from(["sources".to_string()]), + output: ValueType::Named("String".to_string()), + }, + ), + ( + "build_token".to_string(), + CallableContract { + arity: 5, + params: HashSet::from([ + "payload".to_string(), + "scheme".to_string(), + "header_name".to_string(), + "source_id".to_string(), + "required_scopes".to_string(), + ]), + output: ValueType::Named("AccessToken".to_string()), + }, + ), + ( + "generate".to_string(), + CallableContract { + arity: 0, + params: HashSet::new(), + output: ValueType::Named("String".to_string()), + }, + ), + ( + "now".to_string(), + CallableContract { + arity: 0, + params: HashSet::new(), + output: ValueType::Named("String".to_string()), + }, + ), + ( + "compute_topology_diff".to_string(), + CallableContract { + arity: 2, + params: HashSet::from(["current".to_string(), "base".to_string()]), + output: ValueType::Named("DagDiff".to_string()), + }, + ), + ( + "render_annotated_mermaid".to_string(), + CallableContract { + arity: 3, + params: HashSet::from([ + "diff".to_string(), + "topology".to_string(), + "title".to_string(), + ]), + output: ValueType::Named("String".to_string()), + }, + ), + ( + "detect_runtime".to_string(), + CallableContract { + arity: 0, + params: HashSet::new(), + output: ValueType::Named("CloudRuntime".to_string()), + }, + ), ] } @@ -3141,6 +3231,44 @@ fn summarize(stages: List) -> Int { assert_eq!(typed.modules.len(), 1); } + #[test] + fn strict_mode_accepts_std_helper_intrinsic_call_targets() { + let graph = module_graph_from_sources(&[( + "sample/helpers.dag", + r#"module sample.helpers +type DocgenSources {} + +fn run(sources: DocgenSources, payload: String) -> String { + let a = "template" |> replace_section("section", "value") + let b = render_test_listings(sources: sources) + let c = render_graph_structure(sources: sources) + let d = render_source_artifacts(sources: sources) + let e = compute_topology_diff(current: "{}", base: "{}") + let f = render_annotated_mermaid(diff: e, topology: "{}", title: "title") + let g = detect_runtime() + let h = generate() + let i = now() + let j = build_token( + payload: payload, + scheme: "Bearer", + header_name: "Authorization", + source_id: "source", + required_scopes: ["gist"] + ) + a +} +"#, + )]); + let typed = typecheck_module_graph_with_options( + graph, + TypecheckOptions { + allow_unresolved_imports: false, + }, + ) + .expect("std helper intrinsics should be recognized in strict mode"); + assert_eq!(typed.modules.len(), 1); + } + #[test] fn strict_mode_accepts_function_typed_parameter_call_targets() { let graph = module_graph_from_sources(&[( From ba21b5ff27e094b7442bcf1fc1970cad46e833e8 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 16:13:12 +0000 Subject: [PATCH 164/169] Honor defaulted call and service arguments in strict mode Co-authored-by: Brian Searls --- core/daglang/daglang-cli/src/compile.rs | 80 ++++++++++ .../daglang-cli/tests/compile_commands.rs | 76 +++++++++ core/daglang/daglang-typecheck/src/lib.rs | 146 ++++++++++++++++-- 3 files changed, 289 insertions(+), 13 deletions(-) diff --git a/core/daglang/daglang-cli/src/compile.rs b/core/daglang/daglang-cli/src/compile.rs index 2768a75c3fe..0f6e0a316da 100644 --- a/core/daglang/daglang-cli/src/compile.rs +++ b/core/daglang/daglang-cli/src/compile.rs @@ -2482,6 +2482,39 @@ fn run() -> String { fmt() } std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } + #[test] + fn compile_directory_call_with_defaulted_params_succeeds() { + let root = std::env::temp_dir().join(format!( + "daglang_compile_call_defaults_dir_{}_{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("system clock should be after unix epoch") + .as_nanos() + )); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp root"); + std::fs::write( + root.join("sample/main.dag"), + r#"module sample.main +fn greet(name: String, punctuation: String = "!") -> String { name } +fn run() -> String { greet(name: "hi") } +"#, + ) + .expect("failed to write defaulted callable source"); + + let context = PipelineContext { + roots: vec![root.clone()], + target_file: None, + }; + + let output = compile_from_context(&context).expect("compile should succeed"); + assert!(!output.lowered_dag.nodes.is_empty()); + assert!(output.derived.manifest.total_nodes > 0); + assert!(!output.emitted.files.is_empty()); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); + } + #[test] fn compile_directory_unknown_named_call_argument_fails_in_typecheck_stage() { let root = std::env::temp_dir().join(format!( @@ -2592,6 +2625,53 @@ func run() -> { body: String } { std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } + #[test] + fn compile_directory_service_call_with_defaulted_inputs_succeeds() { + let root = std::env::temp_dir().join(format!( + "daglang_compile_service_call_defaults_dir_{}_{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("system clock should be after unix epoch") + .as_nanos() + )); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp root"); + std::fs::write( + root.join("sample/main.dag"), + r#"module sample.main +interface Storage { + capability read { + input { + path: String, + recursive: Bool = false + } + output { ok: Bool } + } +} +service FsStorage implements Storage { + operation read(path: String, recursive: Bool = false) -> { ok: Bool } +} +func run() -> { ok: Bool } { + let response = FsStorage.read(path: "/tmp") + return { ok: response.ok } +} +"#, + ) + .expect("failed to write defaulted service-call source"); + + let context = PipelineContext { + roots: vec![root.clone()], + target_file: None, + }; + + let output = compile_from_context(&context).expect("compile should succeed"); + assert!(!output.lowered_dag.nodes.is_empty()); + assert!(output.derived.manifest.total_nodes > 0); + assert!(!output.emitted.files.is_empty()); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); + } + #[test] fn compile_directory_unknown_named_service_argument_fails_in_typecheck_stage() { let root = std::env::temp_dir().join(format!( diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index fec718e0071..1d6f98892b9 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -12717,6 +12717,37 @@ fn run() -> String { fmt() } std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); } +#[test] +fn compile_command_directory_mode_accepts_omitting_defaulted_call_args() { + let root = unique_temp_dir("call_default_args"); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp dir"); + std::fs::write( + root.join("sample/main.dag"), + r#"module sample.main +fn greet(name: String, punctuation: String = "!") -> String { name } +fn run() -> String { greet(name: "hi") } +"#, + ) + .expect("failed to write source"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&root) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile on directory"); + + assert!( + output.status.success(), + "directory compile should accept omitted defaulted call args: {}", + String::from_utf8_lossy(&output.stderr) + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); +} + #[test] fn compile_command_directory_mode_fails_on_unknown_named_call_argument_typecheck_error() { let root = unique_temp_dir("call_unknown_arg"); @@ -12826,6 +12857,51 @@ func run() -> { ok: Bool } { std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); } +#[test] +fn compile_command_directory_mode_accepts_omitting_defaulted_service_call_args() { + let root = unique_temp_dir("service_call_default_args"); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp dir"); + std::fs::write( + root.join("sample/main.dag"), + r#"module sample.main +interface Storage { + capability read { + input { + path: String, + recursive: Bool = false + } + output { ok: Bool } + } +} +service FsStorage implements Storage { + operation read(path: String, recursive: Bool = false) -> { ok: Bool } +} +func run() -> { ok: Bool } { + let response = FsStorage.read(path: "/tmp") + return { ok: response.ok } +} +"#, + ) + .expect("failed to write source"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&root) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile on directory"); + + assert!( + output.status.success(), + "directory compile should accept omitted defaulted service call args: {}", + String::from_utf8_lossy(&output.stderr) + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); +} + #[test] fn compile_command_directory_mode_fails_on_unknown_named_service_argument_typecheck_error() { let root = unique_temp_dir("service_call_unknown_arg"); diff --git a/core/daglang/daglang-typecheck/src/lib.rs b/core/daglang/daglang-typecheck/src/lib.rs index d537e6750a9..242ef57fb86 100644 --- a/core/daglang/daglang-typecheck/src/lib.rs +++ b/core/daglang/daglang-typecheck/src/lib.rs @@ -870,7 +870,7 @@ fn collect_unique_callables( &mut callables, def.name.clone(), CallableContract { - arity: def.params.len(), + arity: required_param_arity(&def.params), params: def.params.iter().map(|param| param.name.clone()).collect(), output: ValueType::Named(type_expr_to_string(&def.return_type)), }, @@ -879,7 +879,7 @@ fn collect_unique_callables( &mut callables, def.name.clone(), CallableContract { - arity: def.params.len(), + arity: required_param_arity(&def.params), params: def.params.iter().map(|param| param.name.clone()).collect(), output: ValueType::Record(field_signature_map(&def.outputs)), }, @@ -888,7 +888,7 @@ fn collect_unique_callables( &mut callables, def.name.clone(), CallableContract { - arity: def.params.len(), + arity: required_param_arity(&def.params), params: def.params.iter().map(|param| param.name.clone()).collect(), output: ValueType::Record(field_signature_map(&def.outputs)), }, @@ -900,7 +900,7 @@ fn collect_unique_callables( &mut callables, variant.name.clone(), CallableContract { - arity: variant.fields.len(), + arity: required_field_arity(&variant.fields), params: variant .fields .iter() @@ -922,6 +922,22 @@ fn collect_unique_callables( callables } +fn required_param_arity(params: &[Param]) -> usize { + params.iter().filter(|param| param.default.is_none()).count() +} + +fn required_field_arity(fields: &[Field]) -> usize { + fields.iter().filter(|field| field.default.is_none()).count() +} + +fn callable_contract_max_arity(contract: &CallableContract) -> usize { + contract.arity.max(contract.params.len()) +} + +fn service_contract_max_arity(contract: &ServiceCallContract) -> usize { + contract.arity.max(contract.params.len()) +} + fn register_callable_contract( callables: &mut HashMap>, name: String, @@ -1014,7 +1030,7 @@ fn builtin_callable_contracts() -> Vec<(String, CallableContract)> { ( "to_bytes".to_string(), CallableContract { - arity: 1, + arity: 0, params: HashSet::from(["value".to_string()]), output: ValueType::Named("Bytes".to_string()), }, @@ -1022,7 +1038,7 @@ fn builtin_callable_contracts() -> Vec<(String, CallableContract)> { ( "to_json".to_string(), CallableContract { - arity: 1, + arity: 0, params: HashSet::from(["value".to_string()]), output: ValueType::Named("Json".to_string()), }, @@ -1030,7 +1046,7 @@ fn builtin_callable_contracts() -> Vec<(String, CallableContract)> { ( "hash".to_string(), CallableContract { - arity: 1, + arity: 0, params: HashSet::from(["value".to_string()]), output: ValueType::Named("String".to_string()), }, @@ -1039,7 +1055,7 @@ fn builtin_callable_contracts() -> Vec<(String, CallableContract)> { "eq".to_string(), CallableContract { arity: 2, - params: HashSet::from(["lhs".to_string(), "rhs".to_string()]), + params: HashSet::from(["a".to_string(), "b".to_string()]), output: ValueType::Named("Bool".to_string()), }, ), @@ -1146,7 +1162,7 @@ fn collect_service_call_contracts(modules: &[ResolvedModule]) -> ServiceCallRegi }; for operation in &service.operations { let contract = ServiceCallContract { - arity: operation.inputs.len(), + arity: required_field_arity(&operation.inputs), params: operation .inputs .iter() @@ -1700,11 +1716,17 @@ fn validate_callable_body( } }, }; - if call.arg_count != contract.arity { + let max_arity = callable_contract_max_arity(contract); + if call.arg_count < contract.arity || call.arg_count > max_arity { + let expected = if call.arg_count < contract.arity { + contract.arity + } else { + max_arity + }; errors.push(TypeError::CallArityMismatch { caller: caller.to_string(), callee: call.callee.clone(), - expected: contract.arity, + expected, got: call.arg_count, }); } @@ -1764,11 +1786,17 @@ fn validate_callable_body( let Some(contract) = contract else { continue; }; - if call.arg_count != contract.arity { + let max_arity = service_contract_max_arity(&contract); + if call.arg_count < contract.arity || call.arg_count > max_arity { + let expected = if call.arg_count < contract.arity { + contract.arity + } else { + max_arity + }; errors.push(TypeError::ServiceCallArityMismatch { caller: caller.to_string(), service_call: service_call_name.clone(), - expected: contract.arity, + expected, got: call.arg_count, }); } @@ -3092,6 +3120,46 @@ service FsStorage implements Storage { ))); } + #[test] + fn call_with_too_many_args_is_reported() { + let graph = module_graph_from_sources(&[( + "arity_overflow.dag", + "module sample.calls\nfn fmt(value: String) -> String { value }\nfn run() -> String { fmt(\"a\", \"b\") }", + )]); + let errors = typecheck_module_graph(graph).expect_err("too many call args should fail"); + assert!(errors.iter().any(|error| matches!( + error, + TypeError::CallArityMismatch { + caller, + callee, + expected, + got + } if caller == "run" && callee == "fmt" && *expected == 1 && *got == 2 + ))); + } + + #[test] + fn strict_mode_allows_call_omitting_defaulted_params() { + let graph = module_graph_from_sources(&[( + "defaulted_call.dag", + r#"module sample.calls +fn greet(name: String, punctuation: String = "!") -> String { + name +} +fn run() -> String { + greet(name: "hi") +}"#, + )]); + let typed = typecheck_module_graph_with_options( + graph, + TypecheckOptions { + allow_unresolved_imports: false, + }, + ) + .expect("defaulted callable params should be optional at call sites"); + assert_eq!(typed.modules.len(), 1); + } + #[test] fn strict_mode_reports_ambiguous_call_target() { let graph = module_graph_from_sources(&[ @@ -3441,6 +3509,58 @@ func run() -> { ok: Bool } { ))); } + #[test] + fn service_call_with_too_many_args_is_reported() { + let graph = module_graph_from_sources(&[( + "service_arity_overflow.dag", + r#"module sample.services +service FsStorage { + operation read(path: String) -> { body: String } +} +func run() -> { ok: Bool } { + let response = FsStorage.read(path: "/tmp", extra: "value") + return { ok: true } +}"#, + )]); + let errors = typecheck_module_graph(graph) + .expect_err("too many service call arguments should fail"); + assert!(errors.iter().any(|error| matches!( + error, + TypeError::ServiceCallArityMismatch { + caller, + service_call, + expected, + got + } if caller == "run" + && service_call == "FsStorage.read" + && *expected == 1 + && *got == 2 + ))); + } + + #[test] + fn strict_mode_allows_service_call_omitting_defaulted_inputs() { + let graph = module_graph_from_sources(&[( + "service_default_input.dag", + r#"module sample.services +service FsStorage { + operation read(path: String, recursive: Bool = false) -> { ok: Bool } +} +func run() -> { ok: Bool } { + let response = FsStorage.read(path: "/tmp") + return { ok: response.ok } +}"#, + )]); + let typed = typecheck_module_graph_with_options( + graph, + TypecheckOptions { + allow_unresolved_imports: false, + }, + ) + .expect("service inputs with defaults should be optional at call sites"); + assert_eq!(typed.modules.len(), 1); + } + #[test] fn uses_bound_resource_capability_call_typechecks_and_infers_outputs() { let graph = module_graph_from_sources(&[( From d257b738a7ddf862d08b8f4c033d213d9cbe1a46 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 16:18:14 +0000 Subject: [PATCH 165/169] Allow pattern wiring arguments in strict call validation Co-authored-by: Brian Searls --- core/daglang/daglang-cli/src/compile.rs | 38 ++++++++++ .../daglang-cli/tests/compile_commands.rs | 36 ++++++++++ core/daglang/daglang-typecheck/src/lib.rs | 70 +++++++++++++++---- 3 files changed, 130 insertions(+), 14 deletions(-) diff --git a/core/daglang/daglang-cli/src/compile.rs b/core/daglang/daglang-cli/src/compile.rs index 0f6e0a316da..b131c9e0bb4 100644 --- a/core/daglang/daglang-cli/src/compile.rs +++ b/core/daglang/daglang-cli/src/compile.rs @@ -2515,6 +2515,44 @@ fn run() -> String { greet(name: "hi") } std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } + #[test] + fn compile_directory_pattern_call_with_extra_named_wiring_args_succeeds() { + let root = std::env::temp_dir().join(format!( + "daglang_compile_pattern_wiring_args_dir_{}_{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("system clock should be after unix epoch") + .as_nanos() + )); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp root"); + std::fs::write( + root.join("sample/main.dag"), + r#"module sample.main +pattern ensure(should_act: Bool = true) -> { acted: Bool } { + return { acted: should_act } +} +fn run() -> Bool { + let result = ensure(check: true, action: false) + result.acted +} +"#, + ) + .expect("failed to write pattern wiring source"); + + let context = PipelineContext { + roots: vec![root.clone()], + target_file: None, + }; + + let output = compile_from_context(&context).expect("compile should succeed"); + assert!(!output.lowered_dag.nodes.is_empty()); + assert!(output.derived.manifest.total_nodes > 0); + assert!(!output.emitted.files.is_empty()); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); + } + #[test] fn compile_directory_unknown_named_call_argument_fails_in_typecheck_stage() { let root = std::env::temp_dir().join(format!( diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index 1d6f98892b9..8288b8b9069 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -12748,6 +12748,42 @@ fn run() -> String { greet(name: "hi") } std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); } +#[test] +fn compile_command_directory_mode_accepts_pattern_calls_with_extra_named_wiring_args() { + let root = unique_temp_dir("pattern_wiring_call_args"); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp dir"); + std::fs::write( + root.join("sample/main.dag"), + r#"module sample.main +pattern ensure(should_act: Bool = true) -> { acted: Bool } { + return { acted: should_act } +} +fn run() -> Bool { + let result = ensure(check: true, action: false) + result.acted +} +"#, + ) + .expect("failed to write source"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&root) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile on directory"); + + assert!( + output.status.success(), + "directory compile should accept pattern calls with extra named wiring args: {}", + String::from_utf8_lossy(&output.stderr) + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); +} + #[test] fn compile_command_directory_mode_fails_on_unknown_named_call_argument_typecheck_error() { let root = unique_temp_dir("call_unknown_arg"); diff --git a/core/daglang/daglang-typecheck/src/lib.rs b/core/daglang/daglang-typecheck/src/lib.rs index 242ef57fb86..ba12a7706c4 100644 --- a/core/daglang/daglang-typecheck/src/lib.rs +++ b/core/daglang/daglang-typecheck/src/lib.rs @@ -439,6 +439,7 @@ pub fn typecheck_module_graph_with_options( let generic_arity_registry = collect_generic_arities(&graph.modules); let record_type_registry = collect_record_types(&graph.modules); let callable_registry = collect_unique_callables(&graph.modules); + let pattern_callable_names = collect_pattern_callable_names(&graph.modules); let service_call_registry = collect_service_call_contracts(&graph.modules); let interface_registry = collect_interfaces(&graph.modules); let resource_type_registry = collect_resource_types(&graph.modules); @@ -455,6 +456,7 @@ pub fn typecheck_module_graph_with_options( generic_arity_registry: &generic_arity_registry, record_type_registry: &record_type_registry, callable_registry: &callable_registry, + pattern_callable_names: &pattern_callable_names, service_call_registry: &service_call_registry, interface_registry: &interface_registry, resource_type_registry: &resource_type_registry, @@ -505,6 +507,7 @@ struct TypecheckContext<'a> { generic_arity_registry: &'a GenericArityRegistry, record_type_registry: &'a RecordTypeRegistry, callable_registry: &'a HashMap>, + pattern_callable_names: &'a HashSet, service_call_registry: &'a ServiceCallRegistry, interface_registry: &'a InterfaceRegistry, resource_type_registry: &'a ResourceTypeRegistry, @@ -532,6 +535,7 @@ fn collect_signatures( let body_context = BodyInferenceContext { record_type_registry: context.record_type_registry, callable_registry: context.callable_registry, + pattern_callable_names: context.pattern_callable_names, service_call_registry: context.service_call_registry, interface_registry: context.interface_registry, resource_type_registry: context.resource_type_registry, @@ -922,6 +926,17 @@ fn collect_unique_callables( callables } +fn collect_pattern_callable_names(modules: &[ResolvedModule]) -> HashSet { + modules + .iter() + .flat_map(|module| module.ast.items.iter()) + .filter_map(|item| match &item.node { + Item::PatternDef(def) => Some(def.name.clone()), + _ => None, + }) + .collect() +} + fn required_param_arity(params: &[Param]) -> usize { params.iter().filter(|param| param.default.is_none()).count() } @@ -1256,6 +1271,7 @@ impl ReturnContract { struct BodyInferenceContext<'a> { record_type_registry: &'a RecordTypeRegistry, callable_registry: &'a HashMap>, + pattern_callable_names: &'a HashSet, service_call_registry: &'a ServiceCallRegistry, interface_registry: &'a InterfaceRegistry, resource_type_registry: &'a ResourceTypeRegistry, @@ -1716,19 +1732,22 @@ fn validate_callable_body( } }, }; - let max_arity = callable_contract_max_arity(contract); - if call.arg_count < contract.arity || call.arg_count > max_arity { - let expected = if call.arg_count < contract.arity { - contract.arity - } else { - max_arity - }; - errors.push(TypeError::CallArityMismatch { - caller: caller.to_string(), - callee: call.callee.clone(), - expected, - got: call.arg_count, - }); + let is_pattern_callable = body_context.pattern_callable_names.contains(&call.callee); + if !is_pattern_callable { + let max_arity = callable_contract_max_arity(contract); + if call.arg_count < contract.arity || call.arg_count > max_arity { + let expected = if call.arg_count < contract.arity { + contract.arity + } else { + max_arity + }; + errors.push(TypeError::CallArityMismatch { + caller: caller.to_string(), + callee: call.callee.clone(), + expected, + got: call.arg_count, + }); + } } let mut seen_named = HashSet::new(); for named in call.named_args { @@ -1740,7 +1759,7 @@ fn validate_callable_body( }); continue; } - if !contract.params.contains(&named) { + if !is_pattern_callable && !contract.params.contains(&named) { errors.push(TypeError::UnknownCallArgument { caller: caller.to_string(), callee: call.callee.clone(), @@ -3160,6 +3179,29 @@ fn run() -> String { assert_eq!(typed.modules.len(), 1); } + #[test] + fn strict_mode_allows_pattern_calls_with_extra_wiring_named_args() { + let graph = module_graph_from_sources(&[( + "pattern_wiring_call.dag", + r#"module sample.calls +pattern ensure(should_act: Bool = true) -> { acted: Bool } { + return { acted: should_act } +} +fn run() -> Bool { + let result = ensure(check: true, action: false) + result.acted +}"#, + )]); + let typed = typecheck_module_graph_with_options( + graph, + TypecheckOptions { + allow_unresolved_imports: false, + }, + ) + .expect("pattern calls should allow extra named wiring arguments"); + assert_eq!(typed.modules.len(), 1); + } + #[test] fn strict_mode_reports_ambiguous_call_target() { let graph = module_graph_from_sources(&[ From 05f1daf5262be43dd037ef6662c4147da05474b3 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 16:27:58 +0000 Subject: [PATCH 166/169] Retain callable generic type parameters for strict checks Co-authored-by: Brian Searls --- core/daglang/daglang-cli/src/compile.rs | 75 +++++++++++++++++++ .../daglang-cli/tests/compile_commands.rs | 71 ++++++++++++++++++ core/daglang/daglang-syntax/src/lib.rs | 3 + core/daglang/daglang-syntax/src/parser.rs | 56 +++++++++++++- core/daglang/daglang-typecheck/src/lib.rs | 66 ++++++++++++++-- 5 files changed, 261 insertions(+), 10 deletions(-) diff --git a/core/daglang/daglang-cli/src/compile.rs b/core/daglang/daglang-cli/src/compile.rs index b131c9e0bb4..2975a064dc4 100644 --- a/core/daglang/daglang-cli/src/compile.rs +++ b/core/daglang/daglang-cli/src/compile.rs @@ -2553,6 +2553,81 @@ fn run() -> Bool { std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } + #[test] + fn compile_directory_generic_fn_type_params_typecheck_in_strict_mode() { + let root = std::env::temp_dir().join(format!( + "daglang_compile_generic_fn_type_params_dir_{}_{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("system clock should be after unix epoch") + .as_nanos() + )); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp root"); + std::fs::write( + root.join("sample/main.dag"), + r#"module sample.main +fn identity(value: T) -> T { + value +} +fn relay(value: T) -> T { + identity(value: value) +} +"#, + ) + .expect("failed to write generic fn source"); + + let context = PipelineContext { + roots: vec![root.clone()], + target_file: None, + }; + + let output = compile_from_context(&context).expect("compile should succeed"); + assert!(!output.lowered_dag.nodes.is_empty()); + assert!(output.derived.manifest.total_nodes > 0); + assert!(!output.emitted.files.is_empty()); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); + } + + #[test] + fn compile_directory_generic_pattern_type_params_typecheck_in_strict_mode() { + let root = std::env::temp_dir().join(format!( + "daglang_compile_generic_pattern_type_params_dir_{}_{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("system clock should be after unix epoch") + .as_nanos() + )); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp root"); + std::fs::write( + root.join("sample/main.dag"), + r#"module sample.main +pattern passthrough(value: T) -> { value: T } { + return { value: value } +} +fn relay(value: T) -> T { + let result = passthrough(value: value) + result.value +} +"#, + ) + .expect("failed to write generic pattern source"); + + let context = PipelineContext { + roots: vec![root.clone()], + target_file: None, + }; + + let output = compile_from_context(&context).expect("compile should succeed"); + assert!(!output.lowered_dag.nodes.is_empty()); + assert!(output.derived.manifest.total_nodes > 0); + assert!(!output.emitted.files.is_empty()); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); + } + #[test] fn compile_directory_unknown_named_call_argument_fails_in_typecheck_stage() { let root = std::env::temp_dir().join(format!( diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index 8288b8b9069..3ef96d4e876 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -12784,6 +12784,77 @@ fn run() -> Bool { std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); } +#[test] +fn compile_command_directory_mode_accepts_generic_fn_type_params() { + let root = unique_temp_dir("generic_fn_type_params"); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp dir"); + std::fs::write( + root.join("sample/main.dag"), + r#"module sample.main +fn identity(value: T) -> T { + value +} +fn relay(value: T) -> T { + identity(value: value) +} +"#, + ) + .expect("failed to write source"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&root) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile on directory"); + + assert!( + output.status.success(), + "directory compile should accept generic fn type params: {}", + String::from_utf8_lossy(&output.stderr) + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); +} + +#[test] +fn compile_command_directory_mode_accepts_generic_pattern_type_params() { + let root = unique_temp_dir("generic_pattern_type_params"); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp dir"); + std::fs::write( + root.join("sample/main.dag"), + r#"module sample.main +pattern passthrough(value: T) -> { value: T } { + return { value: value } +} +fn relay(value: T) -> T { + let result = passthrough(value: value) + result.value +} +"#, + ) + .expect("failed to write source"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&root) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile on directory"); + + assert!( + output.status.success(), + "directory compile should accept generic pattern type params: {}", + String::from_utf8_lossy(&output.stderr) + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); +} + #[test] fn compile_command_directory_mode_fails_on_unknown_named_call_argument_typecheck_error() { let root = unique_temp_dir("call_unknown_arg"); diff --git a/core/daglang/daglang-syntax/src/lib.rs b/core/daglang/daglang-syntax/src/lib.rs index cce38975e37..dc67301fed4 100644 --- a/core/daglang/daglang-syntax/src/lib.rs +++ b/core/daglang/daglang-syntax/src/lib.rs @@ -130,6 +130,7 @@ pub mod ast { #[derive(Debug)] pub struct FnDef { pub name: String, + pub type_params: Vec, pub params: Vec, pub return_type: TypeExpr, pub body: FnBody, @@ -139,6 +140,7 @@ pub mod ast { #[derive(Debug)] pub struct FuncDef { pub name: String, + pub type_params: Vec, pub params: Vec, pub outputs: Vec, pub uses: Vec, @@ -151,6 +153,7 @@ pub mod ast { #[derive(Debug)] pub struct PatternDef { pub name: String, + pub type_params: Vec, pub params: Vec, pub outputs: Vec, pub uses: Vec, diff --git a/core/daglang/daglang-syntax/src/parser.rs b/core/daglang/daglang-syntax/src/parser.rs index 70a2db7ff17..75ccd174d7e 100644 --- a/core/daglang/daglang-syntax/src/parser.rs +++ b/core/daglang/daglang-syntax/src/parser.rs @@ -556,11 +556,23 @@ impl Parser { params.push(self.expect_ident()?); // optional bounds `: Bound + Bound` if self.eat(&TokenKind::Colon) { + if self.eat(&TokenKind::Arrow) { + let output_type = self.expect_ident()?; + if !params.contains(&output_type) { + params.push(output_type); + } + } self.skip_type_bound(); } while self.eat(&TokenKind::Comma) { params.push(self.expect_ident()?); if self.eat(&TokenKind::Colon) { + if self.eat(&TokenKind::Arrow) { + let output_type = self.expect_ident()?; + if !params.contains(&output_type) { + params.push(output_type); + } + } self.skip_type_bound(); } } @@ -702,7 +714,7 @@ impl Parser { fn parse_fn_def(&mut self) -> Result { self.expect(&TokenKind::Fn)?; let name = self.expect_ident()?; - let _tp = self.parse_optional_type_params()?; + let type_params = self.parse_optional_type_params()?; self.expect(&TokenKind::LParen)?; let params = self.parse_param_list()?; self.expect(&TokenKind::RParen)?; @@ -715,6 +727,7 @@ impl Parser { }; Ok(FnDef { name, + type_params, params, return_type, body, @@ -724,7 +737,7 @@ impl Parser { fn parse_func_def(&mut self, leading: Vec) -> Result { self.expect(&TokenKind::Func)?; let name = self.expect_ident()?; - let _tp = self.parse_optional_type_params()?; + let type_params = self.parse_optional_type_params()?; self.expect(&TokenKind::LParen)?; let params = self.parse_param_list()?; self.expect(&TokenKind::RParen)?; @@ -739,6 +752,7 @@ impl Parser { let body = self.parse_func_body_lossy()?; Ok(FuncDef { name, + type_params, params, outputs, uses, @@ -751,7 +765,7 @@ impl Parser { fn parse_pattern_def(&mut self) -> Result { self.expect(&TokenKind::Pattern)?; let name = self.expect_ident()?; - let _tp = self.parse_optional_type_params()?; + let type_params = self.parse_optional_type_params()?; self.expect(&TokenKind::LParen)?; let params = self.parse_param_list()?; self.expect(&TokenKind::RParen)?; @@ -762,6 +776,7 @@ impl Parser { let body = self.parse_func_body_lossy()?; Ok(PatternDef { name, + type_params, params, outputs, uses, @@ -2260,11 +2275,44 @@ mod tests { let sf = parse_or_panic("module test\nfn greet(name: String) -> String { name }"); assert_eq!(sf.items.len(), 1); match &sf.items[0].node { - Item::FnDef(f) => assert_eq!(f.name, "greet"), + Item::FnDef(f) => { + assert_eq!(f.name, "greet"); + assert!(f.type_params.is_empty()); + } + other => panic!("expected FnDef, got {other:?}"), + } + } + + #[test] + fn parse_fn_type_params_are_preserved() { + let sf = parse_or_panic("module test\nfn identity(value: T) -> T { value }"); + match &sf.items[0].node { + Item::FnDef(f) => assert_eq!(f.type_params, vec!["T".to_string()]), other => panic!("expected FnDef, got {other:?}"), } } + #[test] + fn parse_pattern_type_params_capture_arrow_output_variable() { + let sf = parse_or_panic( + "module test\npattern upsert R>(value: Check) -> { value: R } { return { value: value } }", + ); + match &sf.items[0].node { + Item::PatternDef(p) => { + assert_eq!( + p.type_params, + vec![ + "Check".to_string(), + "Create".to_string(), + "Resolve".to_string(), + "R".to_string() + ] + ); + } + other => panic!("expected PatternDef, got {other:?}"), + } + } + #[test] fn parse_type_alias() { let sf = parse_or_panic("module test\ntype Url = String"); diff --git a/core/daglang/daglang-typecheck/src/lib.rs b/core/daglang/daglang-typecheck/src/lib.rs index ba12a7706c4..beff0d4939b 100644 --- a/core/daglang/daglang-typecheck/src/lib.rs +++ b/core/daglang/daglang-typecheck/src/lib.rs @@ -558,16 +558,17 @@ fn collect_signatures( } Item::FnDef(def) => { record_duplicate_item_name(module_name, &def.name, &mut seen_items, errors); + let item_known_types = extend_known_types(&module_known_types, &def.type_params); validate_params( &def.name, &def.params, - &module_known_types, + &item_known_types, context.generic_arity_registry, errors, ); validate_type_expr( &def.return_type, - &module_known_types, + &item_known_types, context.generic_arity_registry, &format!("{}.return", def.name), errors, @@ -600,17 +601,18 @@ fn collect_signatures( } Item::FuncDef(def) => { record_duplicate_item_name(module_name, &def.name, &mut seen_items, errors); + let item_known_types = extend_known_types(&module_known_types, &def.type_params); validate_params( &def.name, &def.params, - &module_known_types, + &item_known_types, context.generic_arity_registry, errors, ); validate_outputs( &def.name, &def.outputs, - &module_known_types, + &item_known_types, context.generic_arity_registry, errors, ); @@ -660,17 +662,18 @@ fn collect_signatures( } Item::PatternDef(def) => { record_duplicate_item_name(module_name, &def.name, &mut seen_items, errors); + let item_known_types = extend_known_types(&module_known_types, &def.type_params); validate_params( &def.name, &def.params, - &module_known_types, + &item_known_types, context.generic_arity_registry, errors, ); validate_outputs( &def.name, &def.outputs, - &module_known_types, + &item_known_types, context.generic_arity_registry, errors, ); @@ -746,6 +749,12 @@ fn collect_signatures( signatures } +fn extend_known_types(base: &HashSet, additional: &[String]) -> HashSet { + let mut known = base.clone(); + known.extend(additional.iter().cloned()); + known +} + fn collect_known_types(modules: &[ResolvedModule]) -> HashSet { let mut known = builtin_type_names(); for module in modules { @@ -3379,6 +3388,51 @@ fn run(sources: DocgenSources, payload: String) -> String { assert_eq!(typed.modules.len(), 1); } + #[test] + fn strict_mode_accepts_generic_fn_type_params() { + let graph = module_graph_from_sources(&[( + "sample/generic_fn.dag", + r#"module sample.generic +fn identity(value: T) -> T { + value +} +fn relay(value: T) -> T { + identity(value: value) +}"#, + )]); + let typed = typecheck_module_graph_with_options( + graph, + TypecheckOptions { + allow_unresolved_imports: false, + }, + ) + .expect("generic fn type parameters should be treated as known types"); + assert_eq!(typed.modules.len(), 1); + } + + #[test] + fn strict_mode_accepts_generic_pattern_type_params() { + let graph = module_graph_from_sources(&[( + "sample/generic_pattern.dag", + r#"module sample.generic +pattern passthrough(value: T) -> { value: T } { + return { value: value } +} +fn relay(value: T) -> T { + let result = passthrough(value: value) + result.value +}"#, + )]); + let typed = typecheck_module_graph_with_options( + graph, + TypecheckOptions { + allow_unresolved_imports: false, + }, + ) + .expect("generic pattern type parameters should be treated as known types"); + assert_eq!(typed.modules.len(), 1); + } + #[test] fn strict_mode_accepts_function_typed_parameter_call_targets() { let graph = module_graph_from_sources(&[( From e2866555baff30f4a77917e548c834c0c915347b Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 16:36:39 +0000 Subject: [PATCH 167/169] Contextualize named record returns and resource config types Co-authored-by: Brian Searls --- core/daglang/daglang-cli/src/compile.rs | 78 ++++++ .../daglang-cli/tests/compile_commands.rs | 74 ++++++ core/daglang/daglang-typecheck/src/lib.rs | 231 ++++++++++++++++-- 3 files changed, 357 insertions(+), 26 deletions(-) diff --git a/core/daglang/daglang-cli/src/compile.rs b/core/daglang/daglang-cli/src/compile.rs index 2975a064dc4..eb297ba6079 100644 --- a/core/daglang/daglang-cli/src/compile.rs +++ b/core/daglang/daglang-cli/src/compile.rs @@ -2628,6 +2628,84 @@ fn relay(value: T) -> T { std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } + #[test] + fn compile_directory_named_record_literal_return_succeeds_in_strict_mode() { + let root = std::env::temp_dir().join(format!( + "daglang_compile_named_record_literal_return_dir_{}_{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("system clock should be after unix epoch") + .as_nanos() + )); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp root"); + std::fs::write( + root.join("sample/main.dag"), + r#"module sample.main +type StageResult { + success: Bool, + skipped: Bool +} +fn result() -> StageResult { + { success: true, skipped: false } +} +"#, + ) + .expect("failed to write named record literal source"); + + let context = PipelineContext { + roots: vec![root.clone()], + target_file: None, + }; + + let output = compile_from_context(&context).expect("compile should succeed"); + assert!(!output.lowered_dag.nodes.is_empty()); + assert!(output.derived.manifest.total_nodes > 0); + assert!(!output.emitted.files.is_empty()); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); + } + + #[test] + fn compile_directory_resource_config_named_return_succeeds_in_strict_mode() { + let root = std::env::temp_dir().join(format!( + "daglang_compile_resource_config_named_return_dir_{}_{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("system clock should be after unix epoch") + .as_nanos() + )); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp root"); + std::fs::write( + root.join("sample/main.dag"), + r#"module sample.main +resource GcsBucket { + config { + name: String, + project: String + } +} +fn gcp_dev_storage() -> GcsBucket.Config { + { name: "gunbc-dev-artifacts", project: "gunbai-auto" } +} +"#, + ) + .expect("failed to write resource config source"); + + let context = PipelineContext { + roots: vec![root.clone()], + target_file: None, + }; + + let output = compile_from_context(&context).expect("compile should succeed"); + assert!(!output.lowered_dag.nodes.is_empty()); + assert!(output.derived.manifest.total_nodes > 0); + assert!(!output.emitted.files.is_empty()); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); + } + #[test] fn compile_directory_unknown_named_call_argument_fails_in_typecheck_stage() { let root = std::env::temp_dir().join(format!( diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index 3ef96d4e876..2c8baafd1ec 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -12855,6 +12855,80 @@ fn relay(value: T) -> T { std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); } +#[test] +fn compile_command_directory_mode_accepts_named_record_literal_returns() { + let root = unique_temp_dir("named_record_literal_return"); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp dir"); + std::fs::write( + root.join("sample/main.dag"), + r#"module sample.main +type StageResult { + success: Bool, + skipped: Bool +} +fn result() -> StageResult { + { success: true, skipped: false } +} +"#, + ) + .expect("failed to write source"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&root) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile on directory"); + + assert!( + output.status.success(), + "directory compile should accept named-record literal returns: {}", + String::from_utf8_lossy(&output.stderr) + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); +} + +#[test] +fn compile_command_directory_mode_accepts_resource_config_named_type_returns() { + let root = unique_temp_dir("resource_config_named_return"); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp dir"); + std::fs::write( + root.join("sample/main.dag"), + r#"module sample.main +resource GcsBucket { + config { + name: String, + project: String + } +} +fn gcp_dev_storage() -> GcsBucket.Config { + { name: "gunbc-dev-artifacts", project: "gunbai-auto" } +} +"#, + ) + .expect("failed to write source"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&root) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile on directory"); + + assert!( + output.status.success(), + "directory compile should accept resource config named-type returns: {}", + String::from_utf8_lossy(&output.stderr) + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); +} + #[test] fn compile_command_directory_mode_fails_on_unknown_named_call_argument_typecheck_error() { let root = unique_temp_dir("call_unknown_arg"); diff --git a/core/daglang/daglang-typecheck/src/lib.rs b/core/daglang/daglang-typecheck/src/lib.rs index beff0d4939b..acb9dbc16c4 100644 --- a/core/daglang/daglang-typecheck/src/lib.rs +++ b/core/daglang/daglang-typecheck/src/lib.rs @@ -760,9 +760,17 @@ fn collect_known_types(modules: &[ResolvedModule]) -> HashSet { for module in modules { let module_prefix = module.module_path.join("."); for item in &module.ast.items { - if let Item::TypeDef(def) = &item.node { - known.insert(def.name.clone()); - known.insert(format!("{module_prefix}.{}", def.name)); + match &item.node { + Item::TypeDef(def) => { + known.insert(def.name.clone()); + known.insert(format!("{module_prefix}.{}", def.name)); + } + Item::ResourceDef(def) => { + let config_name = format!("{}.Config", def.name); + known.insert(config_name.clone()); + known.insert(format!("{module_prefix}.{config_name}")); + } + _ => {} } } } @@ -782,6 +790,23 @@ fn collect_generic_arities(modules: &[ResolvedModule]) -> GenericArityRegistry { let (name, arity) = match &item.node { Item::TypeDef(def) => (&def.name, def.params.len()), Item::InterfaceDef(def) => (&def.name, def.type_params.len()), + Item::ResourceDef(def) => { + let name = format!("{}.Config", def.name); + let full_name = format!("{module_prefix}.{name}"); + registry.full.insert(full_name, 0); + registry + .short + .entry(name) + .and_modify(|existing| { + if let Some(current) = existing { + if *current != 0 { + *existing = None; + } + } + }) + .or_insert(Some(0)); + continue; + } _ => continue, }; let full_name = format!("{module_prefix}.{name}"); @@ -807,26 +832,50 @@ fn collect_record_types(modules: &[ResolvedModule]) -> RecordTypeRegistry { for module in modules { let module_prefix = module.module_path.join("."); for item in &module.ast.items { - let Item::TypeDef(def) = &item.node else { - continue; - }; - let daglang_syntax::ast::TypeBody::Record(fields) = &def.body else { - continue; - }; - let signature = field_signature_map(fields); - let full_name = format!("{module_prefix}.{}", def.name); - registry.full.insert(full_name.clone(), signature); - registry - .short - .entry(def.name.clone()) - .and_modify(|existing| { - if let Some(current) = existing { - if current != &full_name { - *existing = None; - } - } - }) - .or_insert(Some(full_name)); + match &item.node { + Item::TypeDef(def) => { + let daglang_syntax::ast::TypeBody::Record(fields) = &def.body else { + continue; + }; + let signature = field_signature_map(fields); + let full_name = format!("{module_prefix}.{}", def.name); + registry.full.insert(full_name.clone(), signature.clone()); + registry + .full + .entry(def.name.clone()) + .or_insert(signature); + registry + .short + .entry(def.name.clone()) + .and_modify(|existing| { + if let Some(current) = existing { + if current != &full_name { + *existing = None; + } + } + }) + .or_insert(Some(full_name)); + } + Item::ResourceDef(def) if !def.config.is_empty() => { + let signature = field_signature_map(&def.config); + let config_name = format!("{}.Config", def.name); + let full_name = format!("{module_prefix}.{config_name}"); + registry.full.insert(full_name.clone(), signature.clone()); + registry.full.insert(config_name.clone(), signature); + registry + .short + .entry(config_name) + .and_modify(|existing| { + if let Some(current) = existing { + if current != &full_name { + *existing = None; + } + } + }) + .or_insert(Some(full_name)); + } + _ => {} + } } } registry @@ -1426,6 +1475,7 @@ fn builtin_type_names() -> HashSet { "Float".to_string(), "String".to_string(), "Bytes".to_string(), + "Secret".to_string(), "Json".to_string(), "Record".to_string(), "List".to_string(), @@ -1446,6 +1496,7 @@ fn builtin_type_arities() -> HashMap { ("Float".to_string(), 0), ("String".to_string(), 0), ("Bytes".to_string(), 0), + ("Secret".to_string(), 0), ("Json".to_string(), 0), ("Record".to_string(), 0), ("List".to_string(), 1), @@ -1859,6 +1910,7 @@ fn validate_callable_body( .collect::>(); let mut saw_explicit_return = false; let mut trailing_expr_type = None; + let mut trailing_expr = None; for stmt in stmts { match stmt { Stmt::Let(name, expr) | Stmt::Assign(name, expr) => { @@ -1870,8 +1922,10 @@ fn validate_callable_body( ); local_bindings.insert(name.clone(), inferred); trailing_expr_type = None; + trailing_expr = None; } Stmt::Expr(expr) => { + trailing_expr = Some(expr); trailing_expr_type = Some(infer_expr_type( expr, &local_bindings, @@ -1882,6 +1936,7 @@ fn validate_callable_body( Stmt::Return(fields) => { saw_explicit_return = true; trailing_expr_type = None; + trailing_expr = None; validate_return_stmt( caller, &return_contract, @@ -1895,8 +1950,17 @@ fn validate_callable_body( } if !saw_explicit_return { if let ReturnContract::Single { ty } = &return_contract { - let inferred = trailing_expr_type - .unwrap_or_else(|| ValueType::Named("Unit".to_string())); + let inferred = match trailing_expr { + Some(expr) => infer_expr_type_for_expected_named_record( + expr, + ty, + &local_bindings, + &infer_context, + errors, + ), + None => trailing_expr_type + .unwrap_or_else(|| ValueType::Named("Unit".to_string())), + }; push_type_mismatch_if_needed(ty, &inferred, errors); } } @@ -1919,8 +1983,9 @@ fn validate_return_stmt( }); return; } - let inferred = infer_expr_type( + let inferred = infer_expr_type_for_expected_named_record( &fields[0].1, + ty, local_bindings, infer_context, errors, @@ -1948,6 +2013,53 @@ fn validate_return_stmt( } } +fn infer_expr_type_for_expected_named_record( + expr: &Expr, + expected_type: &str, + local_bindings: &HashMap, + infer_context: &ExprInferenceContext<'_>, + errors: &mut Vec, +) -> ValueType { + let Expr::Record(None, fields) = expr else { + return infer_expr_type(expr, local_bindings, infer_context, errors); + }; + + let Some(expected_fields) = resolve_record_fields(expected_type, infer_context.record_type_registry) else { + return infer_expr_type(expr, local_bindings, infer_context, errors); + }; + + let mut inferred_fields = HashMap::new(); + let mut compatible = true; + for (name, value_expr) in fields { + let inferred = infer_expr_type(value_expr, local_bindings, infer_context, errors); + let inferred_name = inferred + .display_name() + .unwrap_or_else(|| "Any".to_string()); + inferred_fields.insert(name.clone(), inferred_name.clone()); + let Some(expected_field_ty) = expected_fields.get(name) else { + errors.push(TypeError::NoSuchField { + ty: expected_type.to_string(), + field: name.clone(), + }); + compatible = false; + continue; + }; + if !types_match(expected_field_ty, &inferred_name) { + errors.push(TypeError::TypeMismatch { + expected: expected_field_ty.clone(), + got: inferred_name, + }); + compatible = false; + } + } + + if compatible { + ValueType::Named(expected_type.to_string()) + } else { + ValueType::Record(inferred_fields) + } +} + fn infer_expr_type( expr: &Expr, local_bindings: &HashMap, @@ -3433,6 +3545,73 @@ fn relay(value: T) -> T { assert_eq!(typed.modules.len(), 1); } + #[test] + fn strict_mode_accepts_untyped_record_literal_for_named_return() { + let graph = module_graph_from_sources(&[( + "sample/records.dag", + r#"module sample.records +type StageResult { + success: Bool, + skipped: Bool +} +fn result() -> StageResult { + { success: true, skipped: false } +}"#, + )]); + let typed = typecheck_module_graph_with_options( + graph, + TypecheckOptions { + allow_unresolved_imports: false, + }, + ) + .expect("record literals should satisfy named-record return contracts"); + assert_eq!(typed.modules.len(), 1); + } + + #[test] + fn strict_mode_accepts_resource_config_named_type_returns() { + let graph = module_graph_from_sources(&[( + "sample/resources.dag", + r#"module sample.resources +resource GcsBucket { + config { + name: String, + project: String + } +} +fn gcp_dev_storage() -> GcsBucket.Config { + { name: "gunbc-dev-artifacts", project: "gunbai-auto" } +}"#, + )]); + let typed = typecheck_module_graph_with_options( + graph, + TypecheckOptions { + allow_unresolved_imports: false, + }, + ) + .expect("resource config named types should be recognized in strict mode"); + assert_eq!(typed.modules.len(), 1); + } + + #[test] + fn strict_mode_accepts_secret_builtin_type() { + let graph = module_graph_from_sources(&[( + "sample/secret.dag", + r#"module sample.secret +fn identity(value: Secret) -> Secret { + value +}"#, + )]); + let typed = typecheck_module_graph_with_options( + graph, + TypecheckOptions { + allow_unresolved_imports: false, + }, + ) + .expect("Secret should be recognized as builtin type"); + assert_eq!(typed.modules.len(), 1); + } + #[test] fn strict_mode_accepts_function_typed_parameter_call_targets() { let graph = module_graph_from_sources(&[( From 4362cc34b69730426b1d7ce285236c63a0d42488 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 16:41:37 +0000 Subject: [PATCH 168/169] Infer zero-arity variant identifiers as typed values Co-authored-by: Brian Searls --- core/daglang/daglang-cli/src/compile.rs | 35 ++++++++++++++++ .../daglang-cli/tests/compile_commands.rs | 33 +++++++++++++++ core/daglang/daglang-typecheck/src/lib.rs | 40 ++++++++++++++++++- 3 files changed, 107 insertions(+), 1 deletion(-) diff --git a/core/daglang/daglang-cli/src/compile.rs b/core/daglang/daglang-cli/src/compile.rs index eb297ba6079..96f64aeffc4 100644 --- a/core/daglang/daglang-cli/src/compile.rs +++ b/core/daglang/daglang-cli/src/compile.rs @@ -1182,6 +1182,41 @@ fn make_gcp() -> CloudConfig { std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } + #[test] + fn compile_directory_zero_arity_variant_identifier_returns_typecheck_in_strict_mode() { + let root = std::env::temp_dir().join(format!( + "daglang_compile_zero_arity_variant_identifier_dir_{}_{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("system clock should be after unix epoch") + .as_nanos() + )); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp root"); + std::fs::write( + root.join("sample/main.dag"), + r#"module sample.main +type Environment = Dev | Ci +fn env() -> Environment { + Dev +} +"#, + ) + .expect("failed to write zero-arity variant source"); + + let context = PipelineContext { + roots: vec![root.clone()], + target_file: None, + }; + + let output = compile_from_context(&context).expect("compile should succeed"); + assert!(!output.lowered_dag.nodes.is_empty()); + assert!(output.derived.manifest.total_nodes > 0); + assert!(!output.emitted.files.is_empty()); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); + } + #[test] fn compile_directory_std_helper_intrinsics_typecheck_in_strict_mode() { let root = std::env::temp_dir().join(format!( diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index 2c8baafd1ec..682b42b8731 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -13476,6 +13476,39 @@ fn make_gcp() -> CloudConfig { std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); } +#[test] +fn compile_command_directory_mode_accepts_zero_arity_variant_identifier_returns() { + let root = unique_temp_dir("zero_arity_variant_identifier_returns"); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp dir"); + std::fs::write( + root.join("sample/main.dag"), + r#"module sample.main +type Environment = Dev | Ci +fn env() -> Environment { + Dev +} +"#, + ) + .expect("failed to write source"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&root) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile on directory"); + + assert!( + output.status.success(), + "directory compile should accept zero-arity variant identifier returns: {}", + String::from_utf8_lossy(&output.stderr) + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); +} + #[test] fn compile_command_directory_mode_accepts_std_helper_intrinsic_call_targets() { let root = unique_temp_dir("std_helper_intrinsic_call_targets"); diff --git a/core/daglang/daglang-typecheck/src/lib.rs b/core/daglang/daglang-typecheck/src/lib.rs index acb9dbc16c4..f243997a95f 100644 --- a/core/daglang/daglang-typecheck/src/lib.rs +++ b/core/daglang/daglang-typecheck/src/lib.rs @@ -2074,7 +2074,25 @@ fn infer_expr_type( daglang_syntax::ast::Literal::Bool(_) => ValueType::Named("Bool".to_string()), daglang_syntax::ast::Literal::None => ValueType::Named("Unit".to_string()), }, - Expr::Ident(name) => local_bindings.get(name).cloned().unwrap_or(ValueType::Unknown), + Expr::Ident(name) => local_bindings + .get(name) + .cloned() + .or_else(|| { + infer_context + .param_callable_contracts + .get(name) + .filter(|contract| callable_contract_max_arity(contract) == 0) + .map(|contract| contract.output.clone()) + }) + .or_else(|| { + infer_context + .callable_registry + .get(name) + .and_then(|entry| entry.as_ref()) + .filter(|contract| callable_contract_max_arity(contract) == 0) + .map(|contract| contract.output.clone()) + }) + .unwrap_or(ValueType::Unknown), Expr::FieldAccess(base, field) => { let base_type = infer_expr_type(base, local_bindings, infer_context, errors); match base_type { @@ -3713,6 +3731,26 @@ fn make_gcp() -> CloudConfig { ))); } + #[test] + fn strict_mode_accepts_zero_arity_variant_as_identifier_value() { + let graph = module_graph_from_sources(&[( + "sample/variant_ident.dag", + r#"module sample.variant +type Environment = Dev | Ci +fn env() -> Environment { + Dev +}"#, + )]); + let typed = typecheck_module_graph_with_options( + graph, + TypecheckOptions { + allow_unresolved_imports: false, + }, + ) + .expect("zero-arity variants should be inferred from identifier expressions"); + assert_eq!(typed.modules.len(), 1); + } + #[test] fn unknown_named_call_argument_is_reported() { let graph = module_graph_from_sources(&[( From cc422f83bd06ebd9008538ef766ad2b9db3e0d7c Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Feb 2026 16:52:28 +0000 Subject: [PATCH 169/169] Skip false tail mismatches for lossy parsed fn bodies Co-authored-by: Brian Searls --- core/daglang/daglang-cli/src/compile.rs | 42 ++++++++++++++ .../daglang-cli/tests/compile_commands.rs | 40 +++++++++++++ core/daglang/daglang-syntax/src/lib.rs | 2 + core/daglang/daglang-syntax/src/parser.rs | 39 ++++++++++++- core/daglang/daglang-typecheck/src/lib.rs | 57 ++++++++++++++++--- 5 files changed, 169 insertions(+), 11 deletions(-) diff --git a/core/daglang/daglang-cli/src/compile.rs b/core/daglang/daglang-cli/src/compile.rs index 96f64aeffc4..2b8f7c1dac8 100644 --- a/core/daglang/daglang-cli/src/compile.rs +++ b/core/daglang/daglang-cli/src/compile.rs @@ -1217,6 +1217,48 @@ fn env() -> Environment { std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); } + #[test] + fn compile_directory_lossy_match_fn_body_does_not_fail_missing_tail_mismatch() { + let root = std::env::temp_dir().join(format!( + "daglang_compile_lossy_match_body_dir_{}_{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("system clock should be after unix epoch") + .as_nanos() + )); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp root"); + std::fs::write( + root.join("sample/main.dag"), + r#"module sample.main +type CloudConfig + = GcpConfig { project: String } + | AwsConfig { account: String } +type CloudProvider = Gcp | Aws + +fn provider_of(config: CloudConfig) -> CloudProvider { + match config { + GcpConfig { ... } => Gcp + AwsConfig { ... } => Aws + } +} +"#, + ) + .expect("failed to write lossy match source"); + + let context = PipelineContext { + roots: vec![root.clone()], + target_file: None, + }; + + let output = compile_from_context(&context).expect("compile should succeed"); + assert!(!output.lowered_dag.nodes.is_empty()); + assert!(output.derived.manifest.total_nodes > 0); + assert!(!output.emitted.files.is_empty()); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp root"); + } + #[test] fn compile_directory_std_helper_intrinsics_typecheck_in_strict_mode() { let root = std::env::temp_dir().join(format!( diff --git a/core/daglang/daglang-cli/tests/compile_commands.rs b/core/daglang/daglang-cli/tests/compile_commands.rs index 682b42b8731..eae838de13f 100644 --- a/core/daglang/daglang-cli/tests/compile_commands.rs +++ b/core/daglang/daglang-cli/tests/compile_commands.rs @@ -13509,6 +13509,46 @@ fn env() -> Environment { std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); } +#[test] +fn compile_command_directory_mode_accepts_lossy_match_body_without_tail_mismatch() { + let root = unique_temp_dir("lossy_match_body"); + std::fs::create_dir_all(root.join("sample")).expect("failed to create temp dir"); + std::fs::write( + root.join("sample/main.dag"), + r#"module sample.main +type CloudConfig + = GcpConfig { project: String } + | AwsConfig { account: String } +type CloudProvider = Gcp | Aws + +fn provider_of(config: CloudConfig) -> CloudProvider { + match config { + GcpConfig { ... } => Gcp + AwsConfig { ... } => Aws + } +} +"#, + ) + .expect("failed to write source"); + + let output = Command::new(daglang_bin()) + .arg("compile") + .arg(&root) + .current_dir(workspace_root()) + .output() + .expect("failed to run daglang compile on directory"); + + assert!( + output.status.success(), + "directory compile should tolerate lossy match-body parsing without false tail mismatch: {}", + String::from_utf8_lossy(&output.stderr) + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert_no_stage_failures(&stderr); + + std::fs::remove_dir_all(root).expect("failed to cleanup temp dir"); +} + #[test] fn compile_command_directory_mode_accepts_std_helper_intrinsic_call_targets() { let root = unique_temp_dir("std_helper_intrinsic_call_targets"); diff --git a/core/daglang/daglang-syntax/src/lib.rs b/core/daglang/daglang-syntax/src/lib.rs index dc67301fed4..209d632c063 100644 --- a/core/daglang/daglang-syntax/src/lib.rs +++ b/core/daglang/daglang-syntax/src/lib.rs @@ -344,12 +344,14 @@ pub mod ast { #[derive(Debug, Clone)] pub struct FnBody { pub stmts: Vec, + pub lossy: bool, } /// Effectful function body (can include service calls, resource ops). #[derive(Debug, Clone)] pub struct FuncBody { pub stmts: Vec, + pub lossy: bool, } /// Statement in a function body. diff --git a/core/daglang/daglang-syntax/src/parser.rs b/core/daglang/daglang-syntax/src/parser.rs index 75ccd174d7e..7579d3efef6 100644 --- a/core/daglang/daglang-syntax/src/parser.rs +++ b/core/daglang/daglang-syntax/src/parser.rs @@ -723,7 +723,10 @@ impl Parser { let body = if self.eat(&TokenKind::LBrace) { self.parse_fn_body_lossy()? } else { - FnBody { stmts: Vec::new() } + FnBody { + stmts: Vec::new(), + lossy: false, + } }; Ok(FnDef { name, @@ -1249,7 +1252,10 @@ impl Parser { self.pos = start_pos; self.errors.truncate(start_errors); self.consume_brace_block_contents()?; - Ok(FnBody { stmts: Vec::new() }) + Ok(FnBody { + stmts: Vec::new(), + lossy: true, + }) } fn parse_func_body_lossy(&mut self) -> Result { @@ -1266,7 +1272,10 @@ impl Parser { self.pos = start_pos; self.errors.truncate(start_errors); self.consume_brace_block_contents()?; - Ok(FuncBody { stmts: Vec::new() }) + Ok(FuncBody { + stmts: Vec::new(), + lossy: true, + }) } // ── fields / params ──────────────────────────────────────────── @@ -1467,12 +1476,14 @@ impl Parser { fn parse_fn_body(&mut self) -> Result { Ok(FnBody { stmts: self.parse_stmts()?, + lossy: false, }) } fn parse_func_body(&mut self) -> Result { Ok(FuncBody { stmts: self.parse_stmts()?, + lossy: false, }) } @@ -2313,6 +2324,28 @@ mod tests { } } + #[test] + fn parse_fn_body_marks_lossy_for_unsupported_match_arms() { + let sf = parse_or_panic( + r#"module test +type CloudConfig = GcpConfig { project: String } | AwsConfig { account: String } +type CloudProvider = Gcp | Aws +fn provider_of(config: CloudConfig) -> CloudProvider { + match config { + GcpConfig { ... } => Gcp + AwsConfig { ... } => Aws + } +}"#, + ); + match &sf.items[2].node { + Item::FnDef(f) => { + assert!(f.body.lossy); + assert!(f.body.stmts.is_empty()); + } + other => panic!("expected FnDef, got {other:?}"), + } + } + #[test] fn parse_type_alias() { let sf = parse_or_panic("module test\ntype Url = String"); diff --git a/core/daglang/daglang-typecheck/src/lib.rs b/core/daglang/daglang-typecheck/src/lib.rs index f243997a95f..ac760c98c60 100644 --- a/core/daglang/daglang-typecheck/src/lib.rs +++ b/core/daglang/daglang-typecheck/src/lib.rs @@ -582,7 +582,10 @@ fn collect_signatures( &def.params, ReturnContract::single(type_expr_to_string(&def.return_type)), &[], - &def.body.stmts, + CallableBodyRef { + stmts: &def.body.stmts, + is_lossy: def.body.lossy, + }, &body_context, errors, ); @@ -636,7 +639,10 @@ fn collect_signatures( &def.params, ReturnContract::record(field_signature_map(&def.outputs)), &def.uses, - &def.body.stmts, + CallableBodyRef { + stmts: &def.body.stmts, + is_lossy: def.body.lossy, + }, &body_context, errors, ); @@ -689,7 +695,10 @@ fn collect_signatures( &def.params, ReturnContract::record(field_signature_map(&def.outputs)), &def.uses, - &def.body.stmts, + CallableBodyRef { + stmts: &def.body.stmts, + is_lossy: def.body.lossy, + }, &body_context, errors, ); @@ -1364,6 +1373,11 @@ struct ExprInferenceContext<'a> { param_callable_contracts: &'a HashMap, } +struct CallableBodyRef<'a> { + stmts: &'a [Stmt], + is_lossy: bool, +} + fn collect_interfaces(modules: &[ResolvedModule]) -> InterfaceRegistry { let mut registry = InterfaceRegistry::default(); for module in modules { @@ -1752,7 +1766,7 @@ fn validate_callable_body( params: &[Param], return_contract: ReturnContract, uses: &[UsesClause], - stmts: &[Stmt], + body: CallableBodyRef<'_>, body_context: &BodyInferenceContext<'_>, errors: &mut Vec, ) { @@ -1766,7 +1780,7 @@ fn validate_callable_body( param_callable_contracts: ¶m_callable_contracts, }; let mut calls = Vec::new(); - collect_calls_from_stmts(stmts, &mut calls); + collect_calls_from_stmts(body.stmts, &mut calls); for call in calls { let contract = match param_callable_contracts.get(&call.callee) { Some(contract) => contract, @@ -1830,7 +1844,7 @@ fn validate_callable_body( } let mut service_calls = Vec::new(); - collect_service_calls_from_stmts(stmts, &mut service_calls); + collect_service_calls_from_stmts(body.stmts, &mut service_calls); for call in service_calls { let service_call_name = call.path.join("."); let contract = match resolve_service_call_contract(&call.path, body_context.service_call_registry) @@ -1911,7 +1925,7 @@ fn validate_callable_body( let mut saw_explicit_return = false; let mut trailing_expr_type = None; let mut trailing_expr = None; - for stmt in stmts { + for stmt in body.stmts { match stmt { Stmt::Let(name, expr) | Stmt::Assign(name, expr) => { let inferred = infer_expr_type( @@ -1948,7 +1962,7 @@ fn validate_callable_body( } } } - if !saw_explicit_return { + if !body.is_lossy && !saw_explicit_return { if let ReturnContract::Single { ty } = &return_contract { let inferred = match trailing_expr { Some(expr) => infer_expr_type_for_expected_named_record( @@ -3751,6 +3765,33 @@ fn env() -> Environment { assert_eq!(typed.modules.len(), 1); } + #[test] + fn strict_mode_skips_missing_tail_mismatch_for_lossy_fn_body() { + let graph = module_graph_from_sources(&[( + "sample/lossy_match.dag", + r#"module sample.lossy +type CloudConfig + = GcpConfig { project: String } + | AwsConfig { account: String } +type CloudProvider = Gcp | Aws + +fn provider_of(config: CloudConfig) -> CloudProvider { + match config { + GcpConfig { ... } => Gcp + AwsConfig { ... } => Aws + } +}"#, + )]); + let typed = typecheck_module_graph_with_options( + graph, + TypecheckOptions { + allow_unresolved_imports: false, + }, + ) + .expect("lossy fn bodies should not emit missing-tail unit mismatch diagnostics"); + assert_eq!(typed.modules.len(), 1); + } + #[test] fn unknown_named_call_argument_is_reported() { let graph = module_graph_from_sources(&[(