Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
f020ef1
WIP: R3 P0 — FieldProject dual-authority dissolution (Class B; illega…
briansrls May 10, 2026
311f772
Merge remote-tracking branch 'origin/main' into session/bright-carp-806
briansrls May 10, 2026
0f42f53
WIP: R3 P0 — FieldProject dual-authority dissolution (Class B; illega…
briansrls May 10, 2026
3a61b5d
Regenerate FieldProject lens snapshots
briansrls May 10, 2026
5716cbe
Address FieldProject review feedback
briansrls May 10, 2026
43d664d
Merge remote-tracking branch 'origin/main' into session/bright-carp-806
briansrls May 10, 2026
0a0747d
WIP: R3 P0 — FieldProject dual-authority dissolution (Class B; illega…
briansrls May 10, 2026
1959a2e
Merge remote-tracking branch 'origin/main' into session/bright-carp-806
briansrls May 10, 2026
cba8d6d
WIP: R3 P0 — FieldProject dual-authority dissolution (Class B; illega…
briansrls May 10, 2026
7828a09
Merge remote-tracking branch 'origin/main' into session/bright-carp-806
briansrls May 10, 2026
07b978a
Regenerate FieldProject bootstrap snapshots
briansrls May 10, 2026
08c6873
Merge remote-tracking branch 'origin/main' into session/bright-carp-806
briansrls May 10, 2026
8c15fe7
Evaluate resolved FieldProject labels
briansrls May 10, 2026
5c94b02
Correct FieldProject substrate assertions
briansrls May 10, 2026
a425ddc
Merge remote-tracking branch 'origin/main' into session/bright-carp-806
briansrls May 10, 2026
a513e94
WIP: R3 P0 — FieldProject dual-authority dissolution (Class B; illega…
briansrls May 10, 2026
0dcd3c1
Merge remote-tracking branch 'origin/main' into session/bright-carp-806
briansrls May 10, 2026
baa1a55
Update FieldProject coproduct commentary
briansrls May 10, 2026
38a8f3d
Satisfy FieldProject clippy checks
briansrls May 10, 2026
f045504
Merge remote-tracking branch 'origin/main' into session/bright-carp-806
briansrls May 10, 2026
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
457 changes: 189 additions & 268 deletions src/v3/compiler/src/bootstrap_generated.rs

Large diffs are not rendered by default.

457 changes: 189 additions & 268 deletions src/v3/compiler/src/bootstrap_generated_without_parse_surface.rs

Large diffs are not rendered by default.

66 changes: 32 additions & 34 deletions src/v3/compiler/src/dag.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1725,9 +1725,6 @@ fn match_payload_field_projection_descent_relation(
let Behavior::Transform(transform) = dag.resolve_producer_opt(&arg)? else {
return None;
};
let TransformTarget::FieldProject { field_label, .. } = &transform.target else {
return None;
};
if transform.inputs.len() != 1 {
return None;
}
Expand All @@ -1745,20 +1742,25 @@ fn match_payload_field_projection_descent_relation(
continue;
}
let facts = variant_structural_facts(dag, &path.pattern)?;
// Validate the projection's structural label appears in the
// resolved variant's Conj field set; otherwise this isn't a
// descent on the variant payload.
let payload_ty = facts
.payload_fields
.iter()
.find(|(label, _)| label == field_label)
.map(|(_, ty)| *ty)?;
let (field_name, payload_ty) = match &transform.target {
TransformTarget::UnresolvedFieldProject { field_label } => facts
.payload_fields
.iter()
.find(|(label, _)| label == field_label)
.map(|(label, ty)| (label.clone(), *ty))?,
TransformTarget::ResolvedFieldProject { field_label } => facts
.payload_fields
.iter()
.find(|(label, _)| label == field_label)
.map(|(label, ty)| (label.clone(), *ty))?,
_ => return None,
};
let element_type = named_type_name(dag, payload_ty)?;
return Some(SubValueRelation::StrictSubValue {
field: InductiveField {
type_name: facts.type_name,
variant_name: facts.variant_name,
field_name: field_label.clone(),
field_name,
shape: RecursionShape::DirectRecursion,
element_type,
},
Expand Down Expand Up @@ -1834,7 +1836,9 @@ fn field_project_input_for_port(dag: &Dag, port: PortId) -> Option<PortId> {
let Behavior::Transform(transform) = dag.resolve_producer_opt(&port)? else {
return None;
};
let TransformTarget::FieldProject { .. } = &transform.target else {
let (TransformTarget::UnresolvedFieldProject { .. }
| TransformTarget::ResolvedFieldProject { .. }) = &transform.target
else {
return None;
};
if transform.inputs.len() != 1 {
Expand Down Expand Up @@ -2123,18 +2127,18 @@ impl ValueNode {
///
/// 4-pattern check on (Callable, FieldProject, Operator):
/// - Pattern 1 (fact placement): fails. Callable dispatches via
/// the declaration's Arrow body; FieldProject dispatches via
/// the input port's resolved Conj + field label; Operator
/// the declaration's Arrow body; unresolved FieldProject dispatches
/// via the input port's resolved Conj + field label; Operator
/// dispatches via the operand type's algebra walk.
/// - Pattern 2 (variant-is-data): fails. Callable carries a
/// DeclarationId; FieldProject carries a field label plus an
/// optional post-infer child declaration carrier;
/// Operator carries an OperatorKind.
/// DeclarationId; resolved FieldProject carries the projected field
/// label resolved against the parent Conj; Operator carries an
/// OperatorKind.
/// - Pattern 3 (algebraic form): fails.
/// - Pattern 4 (dimensional): fails.
///
/// Verdict: mixed lifecycle. `Callable` and `FieldProject` are
/// terminal;
/// Verdict: mixed lifecycle. `Callable` and `ResolvedFieldProject`
/// are terminal; `UnresolvedFieldProject` is pre-infer only.
/// `Operator` is 🟡 scaffold with an explicit M2+ dissolution
/// trigger (surface grammar adoption of direct algebra field
/// access or a parse-time desugaring pass).
Expand All @@ -2143,20 +2147,14 @@ pub enum TransformTarget {
/// A user function or resolved declaration. Inference walks the
/// referenced declaration's `Arrow` connective via `resolve_arrow`.
Callable(DeclarationId),
/// Structural projection on a Conj-typed parent value. The
/// input port is the single authority for the parent type;
/// inference walks that input through any Instantiation /
/// ResolvedIdentifier edges, looks up `field_label` on the
/// reached Conj, and resolves the output through the same
/// substitution context. `field_child` is the post-infer phase
/// carrier for the resolved projected child declaration, so
/// downstream consumers can read typed child identity without
/// repeating the label lookup. No synthesized accessor
/// declaration.
FieldProject {
field_label: String,
field_child: Option<DeclarationId>,
},
/// Pre-infer structural projection on a Conj-typed parent value.
/// Inference is the only authority allowed to turn this label into
/// a projected child declaration.
UnresolvedFieldProject { field_label: String },
/// Post-infer structural projection. The field label is the single
/// authority; inference and emit resolve it against the parent Conj
/// so duplicate-typed fields cannot collapse to the same child id.
ResolvedFieldProject { field_label: String },
/// A primitive binary operator. Inference dispatches on the
/// `OperatorKind` variant directly: arithmetic returns the operand
/// type, comparison returns Bool, and logical is Bool-monomorphic.
Expand Down
82 changes: 66 additions & 16 deletions src/v3/compiler/src/dag/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -304,14 +304,32 @@ impl Dag {
) -> Option<TypeShape> {
match target {
TransformTarget::Callable(target) => self.callable_output_shape(*target),
TransformTarget::FieldProject { field_child, .. } => field_child.map(|child| {
self.assert_declaration_exists(child, "push_transform(target.field_child)");
TypeShape::new(child)
}),
TransformTarget::UnresolvedFieldProject { .. } => None,
TransformTarget::ResolvedFieldProject { field_label } => {
self.field_project_output_shape(field_label, inputs)
}
TransformTarget::Operator(kind) => self.operator_output_shape(*kind, inputs),
}
}

fn field_project_output_shape(
&self,
field_label: &str,
inputs: &[PortId],
) -> Option<TypeShape> {
let input_shape = self.resolved_port_shape(*inputs.first()?)?;
let mut subst = Vec::new();
let conj_id = self.walk_to_conj_decl_with_subst(input_shape.declaration, &mut subst, 0)?;
let TypeConnective::Conj { children } = &self.declaration(conj_id).connective else {
return None;
};
let field_ty = children
.iter()
.find(|field| field.label == field_label)
.map(|field| field.ty)?;
self.signature_type_shape(field_ty, &subst, 0)
}

fn callable_output_shape(&self, decl_id: DeclarationId) -> Option<TypeShape> {
let mut subst = Vec::new();
self.callable_output_shape_with_subst(decl_id, &mut subst, 0)
Expand Down Expand Up @@ -447,6 +465,44 @@ impl Dag {
}
}

fn walk_to_conj_decl_with_subst(
&self,
current: DeclarationId,
subst: &mut Vec<Vec<TemplateArgument>>,
depth: usize,
) -> Option<DeclarationId> {
if depth >= BUILDER_TYPE_WALK_DEPTH_LIMIT {
return None;
}
match &self.declaration(current).connective {
TypeConnective::Conj { .. } => Some(current),
TypeConnective::Instantiation {
template,
arguments,
} => {
subst.push(arguments.clone());
let result = self.walk_to_conj_decl_with_subst(*template, subst, depth + 1);
if result.is_none() {
subst.pop();
}
result
}
TypeConnective::Atom(AtomPayload::ResolvedByStructure(next))
| TypeConnective::Atom(AtomPayload::ResolvedByName(next)) => {
self.walk_to_conj_decl_with_subst(*next, subst, depth + 1)
}
TypeConnective::Atom(AtomPayload::TypeParam(_)) => {
let bound = self.lookup_template_argument(current, subst)?;
self.walk_to_conj_decl_with_subst(bound, subst, depth + 1)
}
TypeConnective::Atom(AtomPayload::Literal(_))
| TypeConnective::Atom(AtomPayload::UnresolvedIdentifier(_))
| TypeConnective::Arrow { .. }
| TypeConnective::Disj { .. }
| TypeConnective::Cardinality(_) => None,
}
}

fn callable_output_shape_with_subst(
&self,
decl_id: DeclarationId,
Expand Down Expand Up @@ -759,7 +815,8 @@ impl Dag {
);
}
}
TransformTarget::FieldProject { .. } => {
TransformTarget::UnresolvedFieldProject { .. }
| TransformTarget::ResolvedFieldProject { .. } => {
assert!(
inputs.len() == 1,
"push_transform(FieldProject) requires exactly one input port"
Expand Down Expand Up @@ -1324,22 +1381,15 @@ mod tests {
}

#[test]
#[should_panic(expected = "push_transform(target.field_child): unknown declaration")]
fn push_transform_rejects_unknown_field_project_child() {
#[should_panic(expected = "push_transform(FieldProject) requires exactly one input port")]
fn push_transform_rejects_resolved_field_project_bad_arity() {
let mut dag = Dag::new();
let parent = dag.alloc_port_with_shape(
dag.declaration_by_name("Dag")
.expect("bootstrap Dag declaration")
.id
.into(),
);

let _ = dag.push_transform(
TransformTarget::FieldProject {
TransformTarget::ResolvedFieldProject {
field_label: "nodes".to_string(),
field_child: Some(DeclarationId(u32::MAX)),
},
vec![parent],
vec![],
span(),
);
}
Expand Down
18 changes: 11 additions & 7 deletions src/v3/compiler/src/emit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1543,10 +1543,14 @@ impl<'a> Ctx<'a> {
) -> Result<String, EmitError> {
match &t.target {
TransformTarget::Operator(op) => self.render_operator(t, *op, locals),
TransformTarget::FieldProject {
field_label,
field_child: _,
} => self.render_field_project(t, field_label, locals),
TransformTarget::UnresolvedFieldProject { field_label } => {
Err(EmitError::UnsupportedBehavior(format!(
"field projection .{field_label} is unresolved; emit expects post-infer FieldProject targets"
)))
}
TransformTarget::ResolvedFieldProject { field_label } => {
self.render_field_project(t, field_label, locals)
}
TransformTarget::Callable(target) => {
let (template, arguments) = callable_template(*target, self.dag);
if let Some(strategy) = self.indexes.callables.get(&template) {
Expand All @@ -1566,9 +1570,9 @@ impl<'a> Ctx<'a> {
locals: &RenderLocals,
) -> Result<String, EmitError> {
if t.inputs.len() != 1 {
return Err(EmitError::UnsupportedBehavior(format!(
"field projection .{field_label} expected one input"
)));
return Err(EmitError::UnsupportedBehavior(
"field projection expected one input".to_string(),
));
}
if let Some(binding) = locals
.payload_bindings
Expand Down
7 changes: 6 additions & 1 deletion src/v3/compiler/src/emit/python_target.rs
Original file line number Diff line number Diff line change
Expand Up @@ -873,7 +873,12 @@ impl<'a> Ctx<'a> {
) -> Result<String, EmitPythonError> {
match &t.target {
TransformTarget::Operator(op) => self.render_operator(t, *op, locals),
TransformTarget::FieldProject { field_label, .. } => {
TransformTarget::UnresolvedFieldProject { field_label } => {
Err(EmitPythonError::Unsupported(format!(
"field projection .{field_label} is unresolved; emit_python expects post-infer FieldProject targets"
)))
}
TransformTarget::ResolvedFieldProject { field_label } => {
if let Some(binding) = locals
.payload_bindings
.get(&t.inputs[0])
Expand Down
32 changes: 14 additions & 18 deletions src/v3/compiler/src/emit/rust_target.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2836,9 +2836,9 @@ fn transform_input_disposition(
dispositions: &HashMap<DeclarationId, Vec<ParameterDispositionBinding>>,
) -> ParameterDispositionBinding {
match &transform.target {
TransformTarget::Operator(_) | TransformTarget::FieldProject { .. } => {
ParameterDispositionBinding::Borrowed
}
TransformTarget::Operator(_)
| TransformTarget::UnresolvedFieldProject { .. }
| TransformTarget::ResolvedFieldProject { .. } => ParameterDispositionBinding::Borrowed,
TransformTarget::Callable(target) => {
callable_input_disposition_for_target(dag, *target, slot, dispositions)
}
Expand Down Expand Up @@ -3667,10 +3667,14 @@ impl<'a> Ctx<'a> {
let expr = self.render_operator(t, *op, locals)?;
self.adjust_owned_expr(t.output, expr, mode)
}
TransformTarget::FieldProject {
field_label,
field_child,
} => self.render_field_project(t, field_label, locals, *field_child, mode),
TransformTarget::UnresolvedFieldProject { field_label } => {
Err(EmitError::UnsupportedBehavior(format!(
"field projection .{field_label} is unresolved; emit_rust expects post-infer FieldProject targets"
)))
}
TransformTarget::ResolvedFieldProject { field_label } => {
self.render_field_project(t, field_label, locals, mode)
}
TransformTarget::Callable(target) => {
let expr = self.render_callable_transform(t, *target, locals)?;
self.adjust_owned_expr(t.output, expr, mode)
Expand Down Expand Up @@ -3702,17 +3706,11 @@ impl<'a> Ctx<'a> {
t: &TransformNode,
field_label: &str,
locals: &RenderLocals,
field_child: Option<DeclarationId>,
mode: RenderMode,
) -> Result<String, EmitError> {
if field_child.is_none() {
return Err(EmitError::UnsupportedBehavior(format!(
"field projection .{field_label} is missing its resolved field child carrier; emit_rust expects post-infer FieldProject targets"
)));
}
if t.inputs.len() != 1 {
return Err(EmitError::UnsupportedBehavior(format!(
"field projection .{field_label} arity {} is not supported; expected exactly one parent input",
"field projection arity {} is not supported; expected exactly one parent input",
t.inputs.len()
)));
}
Expand Down Expand Up @@ -6197,9 +6195,8 @@ not user `fn` data; must not set return-carrier / Rc on callable params (PR #676
let output = dag.alloc_port(Some(node_id));
dag.push_node(Behavior::Transform(TransformNode {
id: node_id,
target: TransformTarget::FieldProject {
target: TransformTarget::ResolvedFieldProject {
field_label: "nodes".to_string(),
field_child: Some(dag_nodes_type),
},
inputs: vec![parent_port],
output,
Expand Down Expand Up @@ -6253,9 +6250,8 @@ not user `fn` data; must not set return-carrier / Rc on callable params (PR #676
let output = dag.alloc_port(Some(node_id));
dag.push_node(Behavior::Transform(TransformNode {
id: node_id,
target: TransformTarget::FieldProject {
target: TransformTarget::ResolvedFieldProject {
field_label: "nodes".to_string(),
field_child: Some(dag_nodes_type),
},
inputs: vec![parent_port],
output,
Expand Down
Loading
Loading