Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions src/v3/compiler/src/lower.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8562,6 +8562,10 @@ fn lower_field_path_expr(
if let Some(port) = resolve_data_path(dag, &value_body, rest, span) {
return port;
}
return unresolved_port(
dag,
data_field_path_diagnostic(dag, decl_id, &value_body, rest, span),
);
}
}
unresolved_port(
Expand All @@ -8577,6 +8581,49 @@ fn lower_field_path_expr(
)
}

fn data_declared_type_display_name(dag: &Dag, decl_id: DeclarationId) -> String {
dag.declaration(decl_id)
.meta_tag
.map(|ty| declaration_display_name(dag, ty))
.unwrap_or_else(|| declaration_display_name(dag, decl_id))
}

fn data_field_path_diagnostic(
dag: &Dag,
decl_id: DeclarationId,
value_body: &crate::dag::ValueBody,
segments: &[String],
span: &SourceSpan,
) -> Diagnostic {
let type_name = data_declared_type_display_name(dag, decl_id);
let field = segments.first().map_or("<empty>", String::as_str);
let name = match value_body {
crate::dag::ValueBody::Scalar(_) => format!("{type_name} has no field {field}"),
crate::dag::ValueBody::Structural { .. } => {
format!(
"{type_name} has no compile-time scalar field path `{}`",
segments.join(".")
)
}
crate::dag::ValueBody::Unparsed(_) => {
format!("{type_name} data value body is not lowered to a compile-time field value")
}
crate::dag::ValueBody::List(_) | crate::dag::ValueBody::Map(_) => {
format!(
"{type_name} data value does not support dotted field path `{}`",
segments.join(".")
)
}
};
Diagnostic::ResolveError {
name,
span: span.clone(),
correction: crate::diagnostics::Correction::deferred_for_diagnostic_class(

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BLOCKING: Diagnostic::ResolveError has a fixes: Vec<Correction> field, not correction, and Correction::deferred_for_diagnostic_class does not exist, so this new fail-closed path cannot compile.

"LoweringDiagnostic",
),
}
}

/// DB-10 (3a.2): walk a sequence of field segments through a data
/// declaration's `ValueBody`, emitting a `Value` node at the terminal
/// scalar literal. Returns `None` if the path cannot be fully
Expand Down
108 changes: 94 additions & 14 deletions src/v3/compiler/tests/integration/m2_feature_parity_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ use v3_compiler::dag::{
literal_bits_int, ArrowBody, AtomPayload, Behavior, ComparisonOp, Dag, DeclarationId,
LiteralBits, LogicalOp, OperatorKind, PortId, TransformNode, TransformTarget, TypeConnective,
};
use v3_compiler::emit::{emit_go_text, emit_python_text};
use v3_compiler::emit_rust::emit_rust;
use v3_compiler::parse_for_test;
use v3_compiler::parse_surface::{SurfaceExpr, SurfaceItem};
use v3_compiler::tokenize_for_test;
Expand Down Expand Up @@ -232,6 +234,25 @@ fn diagnostic_summary(dag: &Dag) -> String {
.join("\n")
}

fn assert_no_field_project(dag: &Dag, field: &str) {
let has_field_project = dag.nodes().iter().any(|n| {
matches!(
n,
v3_compiler::dag::Behavior::Transform(t)
if matches!(
&t.target,
v3_compiler::dag::TransformTarget::UnresolvedFieldProject { field_label }
| v3_compiler::dag::TransformTarget::ResolvedFieldProject { field_label }
if field_label == field
)
)
});
assert!(
!has_field_project,
"data-resolved `{field}` access must not lower to a FieldProject Transform"
);
}

#[test]
fn test_3a2_scalar_data_compiles_and_populates_value_body() {
match compile_to_dag("data answer: Int = 42", "test.v3") {
Expand Down Expand Up @@ -808,21 +829,80 @@ fn test_3a2_data_field_access_resolves_statically() {
// Must NOT contain a FieldProject<host> Transform — that would
// indicate runtime field access on a data value, defeating the
// lowering-time inlining.
let has_field_project_host = dag.nodes().iter().any(|n| {
matches!(
n,
v3_compiler::dag::Behavior::Transform(t)
if matches!(
&t.target,
v3_compiler::dag::TransformTarget::UnresolvedFieldProject { field_label } if field_label == "host"
)
assert_no_field_project(&dag, "host");
}

fn emit_all_targets(dag: &Dag, case: &str) -> Vec<(&'static str, String)> {
vec![
(
"Rust",
emit_rust(dag).unwrap_or_else(|err| panic!("Rust emits {case}: {err:?}")),
),
(
"Python",
emit_python_text(dag).unwrap_or_else(|err| panic!("Python emits {case}: {err:?}")),
),
(
"Go",
emit_go_text(dag).unwrap_or_else(|err| panic!("Go emits {case}: {err:?}")),
),
]
}

mod db_10_data_value_inline_demo {
use super::*;

#[test]
fn scalar_data_inlines_across_emitters() {
let scalar_src = "data answer: Int = 42\n\
fn f() -> Int = answer + 1\n\
let result: Int = f()";
let scalar = cached_compile_to_dag(scalar_src, "db_10_scalar_inline_demo.v3");
for (target, emitted) in emit_all_targets(&scalar, "scalar data inline demo") {
assert!(
emitted.contains("42 + 1"),
"{target} output must inline `answer` in `f` as `42 + 1`; got:\n{emitted}"
);
}
}

#[test]
fn record_field_inlines_across_emitters_without_field_project() {
let record_src = "type Config { host: String, port: Int }\n\
data config: Config = { host: \"h\", port: 8080 }\n\
fn host() -> String = config.host\n\
let result: String = host()";
let record = cached_compile_to_dag(record_src, "db_10_record_inline_demo.v3");
assert_no_field_project(&record, "host");
for (target, emitted) in emit_all_targets(&record, "record data inline demo") {
assert!(
emitted.contains("\"h\""),
"{target} output must inline `config.host` as string literal `\"h\"`; got:\n{emitted}"
);
}
}

#[test]
fn scalar_data_field_access_rejects_with_declared_type_diagnostic() {
let err = compile_to_dag(
"data x: Int = 42\nfn bad() -> Int = x.foo",
"db_10_scalar_data_field_rejects.v3",
)
});
assert!(
!has_field_project_host,
"`cfg.host` on a data declaration must NOT lower to a runtime \
FieldProject<host> Transform; the literal must be inlined at compile time"
);
.expect_err("scalar data dotted field access must fail closed");
let CompileError::Semantic(dag) = err else {
panic!("expected semantic diagnostic for scalar data field access, got {err:?}");
};
let joined = dag
.diagnostics()
.iter()
.map(|(_, d)| d.message())
.collect::<Vec<_>>()
.join("\n");
assert!(
joined.contains("Int has no field foo"),
"scalar data field diagnostic should name the declared type and field; got:\n{joined}"
);
}
}

// =================================================================
Expand Down
Loading