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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions dsl/shared/dag_util.dag
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@ import std.types {
Document,
DocumentLine,
DocumentSection,
DocumentFile,
RenderedTextFile,
StageResult,
Summary,
TextFile,
TextFilePath
}
// --- Result aggregation -----------------------------------------------
Expand Down Expand Up @@ -73,7 +73,7 @@ fn generated_header(tool: String) -> String {
}

// --- Structured document rendering -------------------------------------
// Shared "rendering engine" for Document/TextFile trees.
// Shared "rendering engine" for Document/DocumentFile trees.

fn text_line(text: String) -> DocumentLine {
{ text: text, is_comment: false, is_blank: false }
Expand Down
2 changes: 1 addition & 1 deletion dsl/std/types.dag
Original file line number Diff line number Diff line change
Expand Up @@ -547,7 +547,7 @@ type Document {
trailing_newline: Bool
}

type TextFile {
type DocumentFile {
path: String
document: Document
}
Expand Down
76 changes: 75 additions & 1 deletion src/v1/04_infer.dag
Original file line number Diff line number Diff line change
Expand Up @@ -2841,6 +2841,59 @@ fn record_lit_alias_struct_fields(type_name: String, scope: InferScope) -> List<
}
}

// Strict record construction (§5 fail-closed): the SOURCE field-name set of the concrete
// record a construction targets, or `none` whenever we cannot enumerate it with confidence —
// so the undefined-field check stays silent rather than false-positive. We fire ONLY when the
// constructed name resolves to a plain, fully-grounded record: non-generic (`params == 0`) and
// a direct product (`connective == Conj`) with declared fields. Everything else returns `none`:
// - generics (`Monoid<T>`, `Vendor<Domain>`): newtype flattening rewrites a single-field
// wrapper to its innermost name (`Monoid` resolves to `[op, identity]`, not `[semigroup,
// identity]`), so the resolved set is not source-faithful;
// - concrete aliases of single-field generics (`Watt = Measure<Power, One, Nat>`): the wrapper
// dissolves to its `<M>` carrier, so the resolved node is not a record at all;
// - a name resolving to a coproduct/enum — including the losing side of a §3 name collision
// (the `Vendor` enum shadowing the `Vendor<Domain>` record): no product fields to check.
// This keeps full teeth on plain records, where every real bug it finds lives (`BoolWitness
// { holds }`, `NodeLocus { node }`, `PortLocus { port }`). Dissolution trigger: when field-name
// resolution is source-faithful under flattening, widen the gate to generics and aliases.
fn record_lit_construction_field_names(type_name: String, scope: InferScope) -> List<String>? {
let si = scope.type_env.source_indices
match lookup_type_by_name(env: scope.type_env, name: type_name) {
Present { value: decl } =>
if (decl.params |> count == 0) && (decl.connective == Conj) && (decl.children |> count > 0) {
Present { value: decl.children |> map(f => authored_name_at(source_indices: si, node: f)) }
} else {
none
}
Absent => none
}
}

// Strict record construction: a constructed NAME can legitimately arm two distinct coproducts
// with different field sets (e.g. `TextNode` is `Fragment.TextNode { content }` in one and
// `MarkupNode.TextNode { text }` in another). The import-scoped `struct_fields`/`arm_fields`
// only see the arm reachable from `expected`; this secondary scan, run ONLY on a would-be
// error, asks whether `field` is valid on a same-named arm of ANY coproduct in the env — so
// `TextNode { text }` stays silent when *some* `TextNode` arm has `text`, while a field present
// on NO same-named arm keeps its loud error (§5). Record-name collisions are NOT handled here:
// duplicate record names across layers are a §3 violation that must be de-conflicted at the
// source (cf. the `TextFile` → `DocumentFile` split), not papered over by a suppressor.
fn field_in_any_variant_named(type_name: String, field: String, scope: InferScope) -> Bool {
let si = scope.type_env.source_indices
scope.type_env.bindings |> map_values |> any(binding =>
let node = binding.resolved
if (node.connective == Disj) {
node.children |> any(arm =>
if authored_name_at(source_indices: si, node: arm) == type_name {
let ea = expand_type_for_field_access(n: arm, env: scope.type_env, module_name: scope.module_name)
let fields = if (ea.connective == Conj) { ea.children } else { arm.children }
fields |> any(f => authored_name_at(source_indices: si, node: f) == field)
} else { false }
)
} else { false }
)
}

fn infer_record_lit(type_name: String?, field_inits: List<Node>, span: SourceSpan, name_span: SourceSpan, scope: InferScope, expected: Node?) -> InferResult {
let struct_fields = match type_name {
Present { value: tn } =>
Expand Down Expand Up @@ -2870,8 +2923,29 @@ fn infer_record_lit(type_name: String?, field_inits: List<Node>, span: SourceSpa
}
}
}
let construction_field_names = match type_name {
Present { value: tn } => record_lit_construction_field_names(type_name: tn, scope: scope)
Absent => none
}
let tn_str = match type_name {
Present { value: tn } => tn
Absent => ""
}
let fi_infer_results = map(field_inits, fi =>
let fi_name = field_init_node_name_at(n: fi, source_indices: scope.type_env.source_indices)
// Strict record construction (§5 fail-closed): a field that matches no field of a
// confidently-RESOLVED type is a loud, located error, not silently ignored. `none`
// (unresolved / generic / anonymous record) keeps the path silent — closes the
// fail-open `infer_record_lit` hole without false-positives.
let unknown_field_diags = match construction_field_names {
Present { value: names } =>
if (names |> any(n => n == fi_name)) || field_in_any_variant_named(type_name: tn_str, field: fi_name, scope: scope) {
[]
} else {
[make_error_node(diagnostic: FieldNotFound { field: fi_name, type_name: tn_str, span: fi.span }, module_name: scope.module_name)]
}
Absent => []
}
let field_expected = match struct_fields |> filter(sf => authored_name_at(source_indices: scope.type_env.source_indices, node: sf) == fi_name) |> first {
Present { value: sf } =>
let ft = match sf.inferred {
Expand Down Expand Up @@ -2908,7 +2982,7 @@ fn infer_record_lit(type_name: String?, field_inits: List<Node>, span: SourceSpa
FieldInferResult {
typed_field: make_field_init_node(name: fi_name, value: ar_typed, span: fi.span, name_span: node_name_span(n: fi)),
infer_result: ar,
diagnostics: concat(ar_diags, field_type_diags)
diagnostics: concat(concat(ar_diags, field_type_diags), unknown_field_diags)
}
)
let typed_fields = fi_infer_results |> map(fir => fir.typed_field)
Expand Down
137 changes: 136 additions & 1 deletion src/v1/stage0/src/v1_compiler_infer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5465,6 +5465,100 @@ pub fn record_lit_alias_struct_fields(
}
}

pub fn record_lit_construction_field_names(
type_name: String,
scope: Rc<InferScope>,
) -> Option<Rc<Vec<String>>> {
{
let si = scope.type_env.clone().source_indices.clone();
match lookup_type_by_name(scope.type_env.clone(), type_name) {
Some(decl) => {
if ((((decl.params.clone().len() as i64) == 0)
&& (decl.connective.clone() == Connective::Conj))
&& ((decl.children.clone().len() as i64) > 0))
{
Some(Rc::new({
let mut __result = Vec::new();
for f in decl.children.clone().iter().cloned() {
__result.push(authored_name_at(si.clone(), f.clone()));
}
__result
}))
} else {
None
}
}
None => None,
}
}
}

pub fn field_in_any_variant_named(type_name: String, field: String, scope: Rc<InferScope>) -> bool {
{
let si = scope.type_env.clone().source_indices.clone();
{
let mut __found = false;
for binding in Rc::new(v1_rt::map_values(&scope.type_env.clone().bindings.clone()))
.iter()
.cloned()
{
if {
let node = binding.resolved.clone();
if (node.connective.clone() == Connective::Disj) {
{
let mut __found = false;
for arm in node.children.clone().iter().cloned() {
if if (authored_name_at(si.clone(), arm.clone()).as_str()
== type_name.clone().as_str())
{
{
let ea = expand_type_for_field_access(
arm.clone(),
scope.type_env.clone(),
scope.module_name.clone(),
);
let fields = if (ea.connective.clone() == Connective::Conj)
{
ea.children.clone()
} else {
arm.children.clone()
};
{
let mut __found = false;
for f in fields.clone().iter().cloned() {
if (authored_name_at(si.clone(), f.clone())
.as_str()
== field.clone().as_str())
{
__found = true;
break;
}
}
__found
}
}
} else {
false
} {
__found = true;
break;
}
}
__found
}
} else {
false
}
} {
__found = true;
break;
}
}
__found
}
}
}

pub fn infer_record_lit(
type_name: Option<String>,
field_inits: Rc<Vec<Rc<Node>>>,
Expand Down Expand Up @@ -5512,6 +5606,14 @@ pub fn infer_record_lit(
},
},
};
let construction_field_names = match type_name.clone() {
Some(tn) => record_lit_construction_field_names(tn.clone(), scope.clone()),
None => None,
};
let tn_str = match type_name.clone() {
Some(tn) => tn.clone(),
None => "".to_string(),
};
let fi_infer_results = Rc::new({
let mut __result = Vec::new();
for fi in field_inits.iter().cloned() {
Expand All @@ -5520,6 +5622,36 @@ pub fn infer_record_lit(
fi.clone(),
scope.type_env.clone().source_indices.clone(),
);
let unknown_field_diags = match construction_field_names.clone() {
Some(names) => {
if ({
let mut __found = false;
for n in names.clone().iter().cloned() {
if (n.clone().as_str() == fi_name.clone().as_str()) {
__found = true;
break;
}
}
__found
} || field_in_any_variant_named(
tn_str.clone(),
fi_name.clone(),
scope.clone(),
)) {
Rc::new(vec![])
} else {
Rc::new(vec![make_error_node(
Rc::new(CompilerDiagnostic::FieldNotFound {
field: fi_name.clone(),
type_name: tn_str.clone(),
span: fi.span.clone(),
}),
scope.module_name.clone(),
)])
}
}
None => Rc::new(vec![]),
};
let field_expected = match Rc::new({
let mut __result = Vec::new();
for sf in struct_fields.clone().iter().cloned() {
Expand Down Expand Up @@ -5594,7 +5726,10 @@ pub fn infer_record_lit(
node_name_span(fi.clone()),
),
infer_result: ar.clone(),
diagnostics: v1_rt::concat(ar_diags.clone(), field_type_diags.clone()),
diagnostics: v1_rt::concat(
v1_rt::concat(ar_diags.clone(), field_type_diags.clone()),
unknown_field_diags.clone(),
),
})
});
}
Expand Down
2 changes: 1 addition & 1 deletion src/v2/compiler/manual/infer_ground_add_mvp.dag
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ fn infer_receipt_runtime_value() -> RuntimeValue {
fn infer_receipt_runtime_diagnostic(n: Node) -> Diagnostic {
Diagnostic {
reason: ^infer_receipt_extdeps,
at: NodeLocus { node: n },
at: node_locus(node: n),
correction: Unavailable { reason: AmbiguousIntent }
}
}
Expand Down
38 changes: 0 additions & 38 deletions src/v2/extdeps/formats/spice_passive_projection_test.dag
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,6 @@ import v2.std.timeseries_signal {
InductorPassive,
ResistorPassive,
}
import v2.std.verification {
BoolWitness,
BoolWitnessClaim,
ManualAnchorAbsent,
TestClaim,
TestClassification,
Tier1,
Unit,
manual_claim_anchor
}

test fn spice_resistor_projects_resistor_passive_topology_holds() -> Bool {
let component = Resistor {
Expand Down Expand Up @@ -84,31 +74,3 @@ test fn passive_topology_preserves_component_name_holds() -> Bool {
Absent => false
}
}

data claim_spice_resistor_passive_spec: TestClaim = BoolWitnessClaim {
label: "extdeps/formats/spice_passive_projection: spice Resistor projects ResistorPassive topology",
anchor: manual_claim_anchor(anchor: ManualAnchorAbsent),
witness: BoolWitness { holds: spice_resistor_projects_resistor_passive_topology_holds() },
classification: TestClassification { tier: Tier1, layer: Unit }
}

data claim_spice_capacitor_passive_arm: TestClaim = BoolWitnessClaim {
label: "extdeps/formats/spice_passive_projection: spice Capacitor projects CapacitorPassive arm",
anchor: manual_claim_anchor(anchor: ManualAnchorAbsent),
witness: BoolWitness { holds: spice_capacitor_projects_capacitor_passive_arm_holds() },
classification: TestClassification { tier: Tier1, layer: Unit }
}

data claim_spice_inductor_passive_arm: TestClaim = BoolWitnessClaim {
label: "extdeps/formats/spice_passive_projection: spice Inductor projects InductorPassive arm",
anchor: manual_claim_anchor(anchor: ManualAnchorAbsent),
witness: BoolWitness { holds: spice_inductor_projects_inductor_passive_arm_holds() },
classification: TestClassification { tier: Tier1, layer: Unit }
}

data claim_passive_topology_preserves_name: TestClaim = BoolWitnessClaim {
label: "extdeps/formats/spice_passive_projection: passive_topology_from_component preserves name",
anchor: manual_claim_anchor(anchor: ManualAnchorAbsent),
witness: BoolWitness { holds: passive_topology_preserves_component_name_holds() },
classification: TestClassification { tier: Tier1, layer: Unit }
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import v2.compiler.compile {
compile_lens_gate_rejected_diagnostic
}
import v2.compiler.infer { InferredTree }
import v2.std.diagnostic { Accepted, Diagnostics, None, Outcome, PortLocus, Rejected, Some, outcome_rejected }
import v2.std.diagnostic { Accepted, Diagnostics, None, Outcome, Rejected, Some, outcome_rejected, port_locus }
import v2.std.logic { Bool }
import v2.std.node { Atom, Conj, Node, Symbol, TypeNode, SyntheticOccurrence }
import v2.std.verification {
Expand Down Expand Up @@ -43,7 +43,7 @@ fn apply_lens_mode_tree() -> InferredTree {

fn apply_lens_mode_rejecting_gate(_tree: InferredTree) -> Outcome<Witness<Node>> {
outcome_rejected(
compile_lens_gate_rejected_diagnostic(at: PortLocus { port: ^apply_lens_mode_gate_at })
compile_lens_gate_rejected_diagnostic(at: port_locus(port: ^apply_lens_mode_gate_at))
)
}

Expand Down
4 changes: 2 additions & 2 deletions src/v2/lens/application/enforce_rejecting_gate_fires.dag
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import v2.compiler.compile {
compile_lens_gate_rejected_diagnostic
}
import v2.compiler.infer { InferredTree }
import v2.std.diagnostic { Accepted, Outcome, PortLocus, Rejected, outcome_rejected }
import v2.std.diagnostic { Accepted, Outcome, Rejected, outcome_rejected, port_locus }
import v2.std.logic { Bool }
import v2.std.node { Atom, Conj, Node, Symbol, TypeNode, SyntheticOccurrence }
import v2.std.verification {
Expand Down Expand Up @@ -41,7 +41,7 @@ fn enforce_reject_tree() -> InferredTree {

fn enforce_rejecting_gate(_tree: InferredTree) -> Outcome<Witness<Node>> {
outcome_rejected(
compile_lens_gate_rejected_diagnostic(at: PortLocus { port: ^enforce_reject_gate_at })
compile_lens_gate_rejected_diagnostic(at: port_locus(port: ^enforce_reject_gate_at))
)
}

Expand Down
Loading
Loading