From 42cf9e3f41ff2c113ab65ac29d1c2d8c0c89ac31 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Thu, 26 Mar 2026 10:36:46 -0400 Subject: [PATCH 01/10] . --- ROADMAP.md | 189 +++++++++++++++++++++++++++++++++++ src/v2/stage0/src/compile.rs | 15 +++ 2 files changed, 204 insertions(+) diff --git a/ROADMAP.md b/ROADMAP.md index 370b6683f93..747f0adcb73 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -2228,6 +2228,195 @@ match succeeds. - Each convergence step survives re-bootstrap and fixed-point verification - The compiler is in a clean place to start real L2 work +### Phase 5 Parallel: Compositional Pipeline (Performance through Fact Composition) + +**Motivation:** The compiler works, but its pipeline violates its own +thesis. The thesis says: smart facts + dumb compiler; facts compose by +construction; no passes. The current pipeline is an imperative sequence +where later stages re-derive facts that earlier stages already computed. +Self-compile takes ~20 minutes. The root cause is not missing +optimization — it is broken fact composition. + +**Root-cause analysis (2026-03-26):** Inference fabricates name-only +references at 5 sites where the structural fact is already available in +the TypeEnv. Emit then calls `resolve_scrutinee_type_node` 12+ times to +follow those name references back to the structural definitions. Emit +rebuilds `InferScope` to perform these lookups. Service expansion uses a +5-pass fixpoint where topo order enables single-pass computation. All of +these are symptoms of the same violation: **facts not composed at their +definition site**. + +**Principle (from the thesis):** "If the graph is suboptimal, a fact is +missing. Fix: improve the domain model." The fix is not a post-hoc +resolution pass (that would be adding a compiler pass — antithetical to +the thesis). The fix is: compose the structural fact at the site where +its inputs are first available. `.inferred` is always structural because +inference uses the resolved binding, not the name. + +#### The Fact Composition Contract + +The pipeline has three fact-producing boundaries. Each boundary produces +a contract — a guarantee about what facts are available downstream. + +**Resolve contract:** Every type reference in the graph is resolved to +its structural definition. `TypeBinding.resolved` carries the full +structural node (connective, children, cardinality). Names flow through +for diagnostics, but the structural composition is the authority. + +**Infer contract:** Every expression node's `.inferred` carries the +fully-resolved structural type of that expression — not a name, not an +alias, but the structural composition. This falls out naturally: when +inference sets `.inferred`, it uses `binding.resolved` (the structural +fact from the TypeEnv), not `leaf_node(name: binding.name)`. + +**Emit contract:** Pure translation. Emit reads `.inferred` to get +structural types, reads `EmitGraphInfo` for rendering metadata, and +translates to target-language text. Emit never calls inference functions, +never resolves through TypeEnv, never rebuilds scope for type lookups. + +#### Why the infer contract is currently broken (5 sites) + +The TypeEnv already carries resolved structural types. But inference +fabricates name-only references at these sites instead of using the +structural fact: + +| Site | File:Line | What happens | What it should do | +|------|-----------|-------------|-------------------| +| Variant constructor locals | `04_infer.dag:2217` | `resolved: leaf_node(name: binding.name)` — bare parent enum name | Use `binding.resolved` — the structural enum definition | +| Optional Some/None | `04_infer.dag:2244` | `resolved: leaf_node(name: "Optional")` — hardcoded name | Cardinality on the binding site (P1.4); no "Optional" name needed | +| Type alias registration | `04_infer.dag:2053` | alias_node stored with original `.inferred` intact | Resolve through TypeEnv before storing | +| Func call return type | `04_infer.dag:1065` | `s.inferred` from sig used directly | Sig should already carry structural type | +| ResolvedFuncSig | `04_sigs.dag:112` | `dsig.inferred.value` unwrapped without resolution | Resolve through TypeEnv at sig construction time | + +**Key observation:** None of these require a new pass. Each is a site +where inference already has the structural fact (via `TypeBinding` or +`TypeEnv`) but discards it in favor of a name. The fix is: use the +fact that's already composed. + +#### C1: Node vocabulary into core + +Node constructors and structural predicates currently live in +`04_types.dag` but are imported by `02_parse.dag`, `03_resolve.dag`, +and all emit files. They read/write Node structural fields (connective, +cardinality) and need no environment — they are Node vocabulary, not +stage-4 concepts. The parser importing from `04_types` is a dependency +inversion. + +| ID | Item | Scope | Notes | +|----|------|-------|-------| +| C1.1 | Move structural constructors to `00_core.dag` | ~8 functions | `leaf_node`, `rt_type`, `with_optional_cardinality`, `with_required_cardinality`, `error_type_node`, `no_span`, `child_inferred_or_name`, `node_is_leaf` | +| C1.2 | Move structural predicates to `00_core.dag` | ~4 functions | `node_is_product`, `node_is_coproduct`, `node_has_structure`, `node_is_optional` | +| C1.3 | Update import statements | ~15 files | Move symbols from `import v2.compiler.infer_types` to `import v2.std.core` | + +**What stays in `04_types.dag`:** Name-checking predicates +(`node_is_container`, `node_is_map` — check `n.name`), normalization +(`normalize_access_type_node`, `normalize_type_name` — check names), +type comparison functions, and domain constructors that hardcode names +(`map_node`, `tuple_node`, `container_node`). These are L1 domain +knowledge that violates layering if moved to core. They stay until L1 +dissolution eliminates the name checks. + +**Gate:** `02_parse.dag` has zero imports from `04_*` modules. + +#### C2: Fix fact composition at definition sites + +Strengthen the infer contract so `.inferred` is always structural. No +new Node fields. No post-hoc resolution pass. Fix the 5 sites where +inference fabricates name-only references instead of using the +structural fact from the environment. + +| ID | Item | Scope | Notes | +|----|------|-------|-------| +| C2.1 | Variant constructor locals | `04_infer.dag:2217-2226` | Use `binding.resolved` (structural enum node) instead of `leaf_node(name: binding.name)` | +| C2.2 | Optional Some/None locals | `04_infer.dag:2244-2249` | Use cardinality model instead of `leaf_node(name: "Optional")` | +| C2.3 | Type alias registration | `04_infer.dag:2053-2055` | Resolve alias target through TypeEnv before storing | +| C2.4 | ResolvedFuncSig construction | `04_sigs.dag:112-114` | Resolve return type through TypeEnv at sig construction time | +| C2.5 | Delete `resolve_scrutinee_type_node` from emit | 12 call sites in `05_emit_rust.dag` | After C2.1-C2.4, `.inferred` is structural; emit reads it directly | +| C2.6 | Delete `infer_record_lit` from emit | 1 call site | Record literal types fully resolved during infer | +| C2.7 | Simplify `type_needs_rc` | `05_emit_rust.dag:1584` | Remove TypeEnv parameter; works on resolved nodes directly | + +**Invariant alignment:** +- No duplicate representations: `.inferred` is the single authority for + resolved type. No separate `resolved_type` field. +- No passes: facts composed at definition site, not in a post-hoc walk. +- Correctness by construction: impossible for emit to receive an + unresolved alias — the types guarantee structural composition. + +**Gate:** `05_emit*.dag` have zero imports from `04_infer.dag`, +`04_lookup.dag`, `04_env.dag` (except type definitions). + +#### C3: Scope elimination (falls out from C2) + +`InferScope` is reconstructed in emit to access `TypeEnv` for type +resolution and `scope.locals` for variable lookups. After C2, TypeEnv +is no longer needed. The remaining `scope.locals` usage +(`is_already_optional` at `05_emit_rust.dag:2570`) checks variable +cardinality, which is available on the variable node's `.inferred`. + +| ID | Item | Scope | Notes | +|----|------|-------|-------| +| C3.1 | Audit remaining scope usage | Analysis | After C2, classify every `scope.type_env` read — expected to be dead | +| C3.2 | Move `recursive_type_set` to `EmitGraphInfo` | 1 usage at `05_emit_rust.dag:671` | Only remaining `scope.type_env` read in emit | +| C3.3 | Stop populating `type_env` in emit scope | `05_emit.dag:266` | `module_emit_scope` uses empty TypeEnv | + +**Gate:** No emit file reads `scope.type_env`. + +#### C4: Single-pass transitive computations + +`expand_transitive_services` runs 5 fixed iterations after inference. +This is a compensating mechanism for not using the topological order +that resolve already computed. A single topo-order fold computes the +transitive closure by construction — when you process a function, all +its callees are already finalized. + +| ID | Item | Scope | Notes | +|----|------|-------|-------| +| C4.1 | Replace 5-pass fixpoint with topo-order fold | `04_service.dag`, `04_infer.dag:2552` | Modules already in topo order from resolve; single fold suffices | +| C4.2 | Audit for other fixpoint iterations | Pipeline-wide | Are there other compensating mechanisms for missing topo-order composition? | + +**Invariant alignment:** Correctness by construction — topo order +guarantees completeness. No iteration needed. + +**Gate:** No fixpoint iteration in the pipeline. + +#### C5: Timing instrumentation + +| ID | Item | Scope | Notes | +|----|------|-------|-------| +| C5.1 | Add `Instant`-based timing to stage0 `compile.rs` | ~30 lines | Per-phase timing: frontend, normalize, reconcile, complexity, ownership, emit | +| C5.2 | Measure after each fix lands | Ongoing | Requires stage0 regeneration to see .dag change impact | + +**Sequencing:** C5.1 first (baseline). C1 is mechanical, independent. +C2.1-C2.4 fix fact composition (can be done incrementally, one site at +a time). C2.5-C2.7 delete the re-derivation from emit (depends on +C2.1-C2.4). C3 falls out from C2. C4 is independent. + +Recommended order: C5.1 → C1 → C2.1-C2.4 → C4.1 → C2.5-C2.7 → C3 + +This work runs as a parallel lane alongside L1/L2 dissolution. The +diagnostic ratchet (0 diagnostics) and fixed-point test are the quality +gates — no change may regress them. + +#### Relationship to other design tracks + +**L1 dissolution:** C2 accelerates L1. When `.inferred` is structural, +inference no longer branches on type names for type resolution. The +name-checking predicates (`node_is_container`, `node_is_map`) remain +as L1 bridges but lose their primary consumer (emit's type resolution). + +**Language extdeps:** C2 clears the path for shared emit. When emit +reads structural facts directly instead of re-resolving through TypeEnv, +the emit spine becomes language-agnostic — target-language rendering +decisions (Rust `Box`, Python `Optional[T]`) are the only remaining +language-specific logic. This is the prerequisite for +`dsl/extdeps/languages/` (Phase 4/M6). + +**Algebraic type vision:** C2 enforces that `.inferred` is a structural +composition — the same principle that the algebraic vision applies to +types themselves (Bit → compositions → types). When types compose from +the fundamental unit and `.inferred` carries that composition, the +compiler processes structure end-to-end. + ### Beyond Phase 5: Bit-Graph Model The full algebraic vision — machine primitives grounded in diff --git a/src/v2/stage0/src/compile.rs b/src/v2/stage0/src/compile.rs index ddb93d9007c..213034d30ce 100644 --- a/src/v2/stage0/src/compile.rs +++ b/src/v2/stage0/src/compile.rs @@ -15,6 +15,7 @@ use crate::artifact::*; use crate::v2_rt; use std::collections::HashMap; use std::rc::Rc; +use std::time::Instant; #[derive(Debug, Clone, PartialEq, Eq, Default)] pub struct SourceFile { @@ -1055,7 +1056,10 @@ pub fn resolve_sources(sources: Rc>>) -> Rc { } pub fn compile_sources(sources: Rc>>, target: RenderTarget) -> Rc { + let t_total = Instant::now(); + let t0 = Instant::now(); let frontend = front_end_sources(sources.clone()); + eprintln!(" phase: frontend {:>10.1}ms", t0.elapsed().as_secs_f64() * 1000.0); match frontend.graph.as_ref().map(|__rc| __rc.as_ref()) { None => { Rc::new(PipelineResult { files: Rc::new(Vec::new()), diagnostics: frontend.diagnostics.clone(), complexity: empty_complexity_report(), ownership: Rc::new(Vec::new()), artifact_plan: empty_artifact_plan() }) @@ -1079,7 +1083,9 @@ pub fn compile_sources(sources: Rc>>, target: RenderTarget) - }) > 0_i64 { return Rc::new(PipelineResult { files: Rc::new(Vec::new()), diagnostics: frontend.diagnostics.clone(), complexity: empty_complexity_report(), ownership: Rc::new(Vec::new()), artifact_plan: empty_artifact_plan() }); }; + let t1 = Instant::now(); let norm = normalize_graph(graph.clone()); + eprintln!(" phase: normalize {:>10.1}ms", t1.elapsed().as_secs_f64() * 1000.0); let norm_diags = norm.diagnostics.clone(); let norm_errors = { let mut __filtered_3 = Vec::new(); @@ -1096,10 +1102,14 @@ pub fn compile_sources(sources: Rc>>, target: RenderTarget) - }) > 0_i64 { return Rc::new(PipelineResult { files: Rc::new(Vec::new()), diagnostics: v2_rt::concat(frontend.diagnostics.clone(), norm_diags.clone()), complexity: empty_complexity_report(), ownership: Rc::new(Vec::new()), artifact_plan: empty_artifact_plan() }); }; + let t2 = Instant::now(); let typed = reconcile(norm.graph.clone()); + eprintln!(" phase: reconcile {:>10.1}ms", t2.elapsed().as_secs_f64() * 1000.0); let typed_diags = typed.diagnostics.clone(); + let t3 = Instant::now(); let func_entries = extract_func_entries(typed.clone()); let complexity = build_complexity_report(func_entries.clone()); + eprintln!(" phase: complexity {:>10.1}ms", t3.elapsed().as_secs_f64() * 1000.0); let typecheck_errors = { let mut __filtered_6 = Vec::new(); for __elem_7 in typed_diags.iter().cloned() { @@ -1115,7 +1125,9 @@ pub fn compile_sources(sources: Rc>>, target: RenderTarget) - }) > 0_i64 { return Rc::new(PipelineResult { files: Rc::new(Vec::new()), diagnostics: v2_rt::concat(v2_rt::concat(frontend.diagnostics.clone(), norm_diags.clone()), typed_diags.clone()), complexity: complexity.clone(), ownership: Rc::new(Vec::new()), artifact_plan: empty_artifact_plan() }); }; + let t4 = Instant::now(); let ownership = extract_ownership_proofs(typed.clone()); + eprintln!(" phase: ownership {:>10.1}ms", t4.elapsed().as_secs_f64() * 1000.0); let ownership_diags = ownership_diagnostics(ownership.clone()); let artifact_plan = default_artifact_plan({ let mut __mapped_11 = Vec::new(); @@ -1124,7 +1136,9 @@ pub fn compile_sources(sources: Rc>>, target: RenderTarget) - } Rc::new(__mapped_11) }, target); + let t5 = Instant::now(); let emit_result = emit_from_artifact_plan(typed.clone(), artifact_plan.clone()); + eprintln!(" phase: emit {:>10.1}ms", t5.elapsed().as_secs_f64() * 1000.0); let emit_files = emit_result.files.clone(); let emit_diags = emit_result.diagnostics.clone(); let emit_errors = { @@ -1144,6 +1158,7 @@ pub fn compile_sources(sources: Rc>>, target: RenderTarget) - } else { emit_files.clone() }; + eprintln!(" phase: total {:>10.1}ms", t_total.elapsed().as_secs_f64() * 1000.0); Rc::new(PipelineResult { files: final_files.clone(), diagnostics: v2_rt::concat(v2_rt::concat(v2_rt::concat(v2_rt::concat(frontend.diagnostics.clone(), norm_diags.clone()), typed_diags.clone()), ownership_diags.clone()), emit_diags.clone()), complexity: complexity.clone(), ownership: ownership.clone(), artifact_plan: artifact_plan.clone() }) } } From 33bbb6663587fabd80f146df34ba8b4be9f6b528 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Thu, 26 Mar 2026 11:03:41 -0400 Subject: [PATCH 02/10] Compositional pipeline design: basis, stack, fact contracts Add three new architectural sections to ROADMAP.md: - Compositional Basis: what is primitive (Node, Conj/Disj, cardinality, slots, recursion, collections), what the kernel honestly knows today (Int/String/Bool as leaves), structural constructors table, derived concepts table. - Composition Stack (Layers 0-7): full chain from Classical=True|False through Bit/Byte/Int/String/List up to Tree/DAG/Annotated (Layer 4, missing from std) to compiler domain types. Identifies Node as Tree, ModuleGraph as DAG, TypedModule as Annotated. Type Layer Downgrade metric for architectural debt. P3.8 note: Tree expressible today. - Rewrite "Phase 5 Parallel: Compositional Pipeline" around fact composition contracts. Tightened per review: "no compensating re-derivation" (not "no passes"), structurally authoritative (not fully-resolved), Optional binding/coproduct distinction preserved, SCC condensation for C4, C1 narrowed to truly timeless vocabulary, EmitGraphInfo carries rendering metadata not type authority. Co-Authored-By: Claude Opus 4.6 (1M context) --- ROADMAP.md | 264 ++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 233 insertions(+), 31 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 747f0adcb73..d15835f02fe 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -167,6 +167,185 @@ definitions; the compiler reads structure from those declarations; names are opaque. This is sufficient for the thesis without requiring the full algebraic foundation up front. +### Compositional Basis + +The thesis says the compiler processes graph structure, not names. But +what IS the irreducible structure? This section answers: what is +primitive, what are the structural constructors, and what is derived. + +**1. Compiler-model primitives (what the compiler operates on):** + +- `Node` as the universal carrier +- Product composition (Conj) — record/struct/tuple +- Coproduct composition (Disj) — enum/variant/sum +- Cardinality on bindings (Required, CardOptional) +- Generic slot composition (`` — Phase 3) +- Recursion / self-reference (SCC-detected cycle metadata) +- Collection constructors (List, Set, Map — type-level, not name-level) + +**2. Value/type-algebra primitives (what the kernel knows):** + +Currently honest state: `Int`, `String`, `Bool`, `Float`, `Unit` are +kernel primitives. The compiler treats them as special leaves via +`is_kernel_type`. The algebraic vision says they should be compositions +(`Int` = named namespace over `List>`), but the compiler does +not yet derive their properties from structure. This is an acknowledged +intermediate state, not a claimed-but-unimplemented derivation. + +The kernel will shrink as the algebraic model matures: +- Phase 3+: kernel types get real `.dag` declarations +- Phase 5+: `is_kernel_type` dissolves into structural property reads +- Beyond: bit-graph model; `Int` genuinely derived from `Bit` + +**3. Structural constructors (how compositions are built):** + +| Constructor | Builds | Compiler representation | +|-------------|--------|------------------------| +| Product | Records, tuples, structs | `connective: Conj`, named children | +| Coproduct | Enums, variants, sums | `connective: Disj`, named children | +| Cardinality | Presence/absence on bindings | `return_cardinality: CardOptional` | +| Parameterization | Generic types (`List`) | Slot substitution before inference | +| Recursion | Self-referential types | SCC cycle metadata on Node | +| Collection | Indexed structures | `List`, `Set`, `Map` type constructors | + +**4. What is derived (named namespaces over the above):** + +| Derived concept | Composition | +|----------------|-------------| +| Tuple | Unnamed product (Conj, positional children) | +| Record / struct | Named product (Conj, named children) | +| Enum | Named coproduct (Disj, variant children) | +| Optional binding | Cardinality 0..1 on the binding site | +| Type alias | Namespace over an existing composition | +| Function | `Callable { params: List, return: Node }` | +| Service | Record of operations + transport/runtime metadata | + +Names are opaque namespaces over structure. The compiler's semantic +basis is not the set of named user types; it is the set of structural +constructors and binding/cardinality facts from which named declarations +are composed. + +### Composition Stack + +The algebraic type vision (above) covers Layers 0-3: logic, machine +primitives, named numeric/text types, and collection algebras. These +are already modeled in `dsl/std/` (`logic.dag`, `bit.dag`, +`integer.dag`, `float.dag`, `types.dag`). + +Between collections and any domain application — including the compiler +itself — there is a layer of **structural compositions** that are +general-purpose but currently only exist implicitly inside the +compiler's ad-hoc type definitions. The compiler's `Node` is a tree; +its `ModuleGraph` is a DAG; its `SourceSpan` is a span over a +sequence. Any program that processes trees, dependency graphs, or +positional data needs the same structures. + +The thesis says the compiler is a generic graph processor. If the +compiler needs types that aren't in std, that's a gap in std — not a +special case in the compiler. + +``` +Layer 0: Logic + Classical = True | False (std/logic.dag) + +Layer 1: Machine + Bit = Classical where width(1) (std/bit.dag) + Byte = { bits: List where length(8) } + Word16/32/64 = List where length(N) + +Layer 2: Named compositions + Int = Int64 = Word64 where signed (std/integer.dag) + Nat = UInt64 = Word64 where unsigned + Char = Int where range(0, 1114111) (std/types.dag) + String = List (std/string_type.dag) + +Layer 3: Collection algebras + List = Σ n. Fin(n) → A (std/types.dag) + Set = A → Bool + Map = K → (1 + V) + +Layer 4: Structural compositions *** MISSING FROM STD *** + Span = { start: Nat, end: Nat } + Pair = { first: A, second: B } (product / Conj) + Either = Left { a: A } | Right { b: B } (coproduct / Disj) + Tree = { value: A, children: List> } + DAG = topologically ordered Tree with sharing + Annotated = { value: A, facts: F } + +Layer 5: Text/source compositions *** MISSING FROM STD *** + SourceSpan = Span (position in source text) + Token = { text: String, span: SourceSpan, shape: TokenShape } + LabeledTree = Tree<{ name: String, value: A }> +``` + +**Layer 4 types are general-purpose:** + +| Type | What it models | Compiler use | Other uses | +|------|---------------|-------------|------------| +| `Span` | A contiguous range in a sequence | Source positions, byte ranges | Time ranges, array slices, text selections | +| `Tree` | Recursive hierarchical structure | AST, Node graph | File systems, org charts, UI components, parse trees | +| `DAG` | Acyclic graph with topological order | Module graph, dependency order | Build systems, task scheduling, workflow orchestration | +| `Annotated` | Value enriched with computed facts | TypedModule = Module + type facts | Any pipeline that accumulates metadata over stages | + +**The compiler as an instance of std vocabulary:** + +The compiler's domain types (Layer 6) are instances of Layer 4-5 +compositions, not novel inventions: + +``` +Node = LabeledTree +Module = { name: String, tree: Node, imports: List } +ModuleGraph = DAG +TypedModule = Annotated +ResolvedGraph = DAG +TextFile = { path: String, content: String } +``` + +When a user writes a build system in `.dag`, they compose from the +same `DAG` and `Tree` that the compiler uses. When a user writes +a workflow engine, they compose from the same `Annotated` pattern +that the compiler's pipeline stages use. The vocabulary is shared +because the structures are the same — the compiler is not special. + +**The fact composition contract as `Annotated`:** + +The pipeline's fact levels (see "Phase 5 Parallel: Compositional +Pipeline") map directly onto the `Annotated` pattern. Each stage +takes a value and returns `Annotated`: + +``` +resolve: Module → Annotated +infer: Annotated → Annotated +emit: Annotated → List +``` + +Each stage enriches the previous stage's output with new facts. No +stage re-derives a fact from a lower level. The type system makes the +contract visible: if emit needs a `TypeFact`, the type signature +guarantees it was composed during infer. No runtime resolution needed. + +**P5.11 is the convergence prerequisite.** Once `ExprData` children +dissolve into `node.children`, `Node` cleanly becomes an instance of +`Tree`. The ~5000 lines of custom `ExprData` walkers +reduce to `Tree.map(fn)` — the standard library's generic tree +traversal. P5.11 is what moves the compiler's AST from ad-hoc +internals into std vocabulary. + +**Timeline:** With P3.8 (recursive generics) landed, `Tree` is +expressible in the `.dag` language today. Defining the Layer 4 types +in `dsl/std/` is tractable now; migrating the compiler to use them +as instances is post-Phase 5 work. The design targets convergence +onto std compositions. The convergence is tracked separately from the +phase plan because it crosses all phases. + +**Type Layer Downgrade metric.** The composition stack provides an +objective architectural-debt metric. When a pipeline stage re-derives +a fact by looking up a name, it is performing a **layer downgrade** — +dropping from a higher layer (annotated structure) back to a lower +layer (name string). Facts should only compose upward. Layer +downgrades are the precise measure of the compositional violations +identified in the C1-C5 work. + ### Collection Denotational Model Collections are not one algebra — they are a **family of related @@ -2230,12 +2409,14 @@ match succeeds. ### Phase 5 Parallel: Compositional Pipeline (Performance through Fact Composition) -**Motivation:** The compiler works, but its pipeline violates its own -thesis. The thesis says: smart facts + dumb compiler; facts compose by -construction; no passes. The current pipeline is an imperative sequence -where later stages re-derive facts that earlier stages already computed. -Self-compile takes ~20 minutes. The root cause is not missing -optimization — it is broken fact composition. +**Motivation:** The compiler works, but its pipeline has compensating +re-derivation where later stages re-derive facts that earlier stages +already computed. The staged pipeline (resolve → infer → emit) is +correct; the problem is that stages fail to compose facts at their +definition site, forcing downstream stages to re-resolve through +environments that should be opaque to them. Self-compile takes +~20 minutes. The root cause is not missing optimization — it is broken +fact composition. **Root-cause analysis (2026-03-26):** Inference fabricates name-only references at 5 sites where the structural fact is already available in @@ -2243,15 +2424,19 @@ the TypeEnv. Emit then calls `resolve_scrutinee_type_node` 12+ times to follow those name references back to the structural definitions. Emit rebuilds `InferScope` to perform these lookups. Service expansion uses a 5-pass fixpoint where topo order enables single-pass computation. All of -these are symptoms of the same violation: **facts not composed at their -definition site**. +these are **layer downgrades** (see Composition Stack) — facts dropping +from annotated structure back to name strings. **Principle (from the thesis):** "If the graph is suboptimal, a fact is missing. Fix: improve the domain model." The fix is not a post-hoc -resolution pass (that would be adding a compiler pass — antithetical to -the thesis). The fix is: compose the structural fact at the site where -its inputs are first available. `.inferred` is always structural because -inference uses the resolved binding, not the name. +resolution pass — that would add a compensating re-derivation stage. +The fix is: compose the structural fact at the site where its inputs +are first available. `.inferred` is the structurally authoritative +resolved type because inference uses `binding.resolved`, not the name. +("Structurally authoritative" means the type is resolved through the +environment — not that it must be eagerly inlined into a deep copy. +Graph sharing and canonical resolved nodes are fine; what matters is +that the authority is structural, not name-based.) #### The Fact Composition Contract @@ -2264,10 +2449,12 @@ structural node (connective, children, cardinality). Names flow through for diagnostics, but the structural composition is the authority. **Infer contract:** Every expression node's `.inferred` carries the -fully-resolved structural type of that expression — not a name, not an -alias, but the structural composition. This falls out naturally: when +structurally authoritative resolved type of that expression — not a +bare name, not an unresolved alias. This falls out naturally: when inference sets `.inferred`, it uses `binding.resolved` (the structural -fact from the TypeEnv), not `leaf_node(name: binding.name)`. +fact from the TypeEnv), not `leaf_node(name: binding.name)`. Graph +sharing is fine; canonical resolved nodes are fine; what must not +happen is storing a name that forces downstream re-resolution. **Emit contract:** Pure translation. Emit reads `.inferred` to get structural types, reads `EmitGraphInfo` for rendering metadata, and @@ -2304,17 +2491,26 @@ inversion. | ID | Item | Scope | Notes | |----|------|-------|-------| -| C1.1 | Move structural constructors to `00_core.dag` | ~8 functions | `leaf_node`, `rt_type`, `with_optional_cardinality`, `with_required_cardinality`, `error_type_node`, `no_span`, `child_inferred_or_name`, `node_is_leaf` | -| C1.2 | Move structural predicates to `00_core.dag` | ~4 functions | `node_is_product`, `node_is_coproduct`, `node_has_structure`, `node_is_optional` | +| C1.1 | Move timeless node constructors to `00_core.dag` | ~4 functions | `leaf_node`, `with_optional_cardinality`, `with_required_cardinality`, `no_span` | +| C1.2 | Move structural predicates to `00_core.dag` | ~3 functions | `node_is_product`, `node_is_coproduct`, `node_has_structure` | | C1.3 | Update import statements | ~15 files | Move symbols from `import v2.compiler.infer_types` to `import v2.std.core` | -**What stays in `04_types.dag`:** Name-checking predicates -(`node_is_container`, `node_is_map` — check `n.name`), normalization -(`normalize_access_type_node`, `normalize_type_name` — check names), -type comparison functions, and domain constructors that hardcode names -(`map_node`, `tuple_node`, `container_node`). These are L1 domain -knowledge that violates layering if moved to core. They stay until L1 -dissolution eliminates the name checks. +**What stays in `04_types.dag`:** Three categories: + +1. *Name-checking predicates* (`node_is_container`, `node_is_map`, + `node_is_optional` — reads `n.name` or encodes cardinality policy). + L1 domain knowledge; stays until dissolution. +2. *Bridge-era policy helpers* (`rt_type`, `error_type_node`, + `child_inferred_or_name`, `node_is_leaf`). These encode + stage-boundary semantics (what to return on error, how to fall + back). Moving them into core would freeze transitional policy into + the deepest layer. +3. *Domain constructors* (`map_node`, `tuple_node`, `container_node`, + `callable_node`). Hardcode names like "Map", "Tuple". L1 domain + knowledge. +4. *Type comparison and normalization* (`node_type_compatible`, + `normalize_access_type_node`, `normalize_type_name`). Inference- + specific logic. **Gate:** `02_parse.dag` has zero imports from `04_*` modules. @@ -2328,7 +2524,7 @@ structural fact from the environment. | ID | Item | Scope | Notes | |----|------|-------|-------| | C2.1 | Variant constructor locals | `04_infer.dag:2217-2226` | Use `binding.resolved` (structural enum node) instead of `leaf_node(name: binding.name)` | -| C2.2 | Optional Some/None locals | `04_infer.dag:2244-2249` | Use cardinality model instead of `leaf_node(name: "Optional")` | +| C2.2 | Optional Some/None locals | `04_infer.dag:2244-2249` | Use cardinality model for binding-site absence/presence. Preserve the distinction: binding-site absence = cardinality; value-level null/sum = actual coproduct constructor. Do not collapse value-level `Some`/`None` usage into binding cardinality. | | C2.3 | Type alias registration | `04_infer.dag:2053-2055` | Resolve alias target through TypeEnv before storing | | C2.4 | ResolvedFuncSig construction | `04_sigs.dag:112-114` | Resolve return type through TypeEnv at sig construction time | | C2.5 | Delete `resolve_scrutinee_type_node` from emit | 12 call sites in `05_emit_rust.dag` | After C2.1-C2.4, `.inferred` is structural; emit reads it directly | @@ -2337,10 +2533,15 @@ structural fact from the environment. **Invariant alignment:** - No duplicate representations: `.inferred` is the single authority for - resolved type. No separate `resolved_type` field. -- No passes: facts composed at definition site, not in a post-hoc walk. + the structurally authoritative resolved type. No separate + `resolved_type` field. +- No compensating re-derivation: facts composed at definition site, not + in a post-hoc walk or downstream re-resolution. - Correctness by construction: impossible for emit to receive an - unresolved alias — the types guarantee structural composition. + unresolved alias — the composition guarantees structural authority. +- EmitGraphInfo carries rendering metadata (presentation facts), not + re-derived type authority. The distinction: "how to render this type + in Rust" is emit's job; "what type is this expression" is not. **Gate:** `05_emit*.dag` have zero imports from `04_infer.dag`, `04_lookup.dag`, `04_env.dag` (except type definitions). @@ -2371,11 +2572,12 @@ its callees are already finalized. | ID | Item | Scope | Notes | |----|------|-------|-------| -| C4.1 | Replace 5-pass fixpoint with topo-order fold | `04_service.dag`, `04_infer.dag:2552` | Modules already in topo order from resolve; single fold suffices | +| C4.1 | Replace 5-pass fixpoint with topo-order fold over condensed DAG | `04_service.dag`, `04_infer.dag:2552` | Modules already in topo order from resolve. For mutual dependencies (mutual recursion), use SCC condensation first, then fold over the condensed DAG. Single pass over the condensation suffices. | | C4.2 | Audit for other fixpoint iterations | Pipeline-wide | Are there other compensating mechanisms for missing topo-order composition? | -**Invariant alignment:** Correctness by construction — topo order -guarantees completeness. No iteration needed. +**Invariant alignment:** Correctness by construction — topo order over +the condensed DAG guarantees completeness. Mutual dependencies are +handled by SCC condensation, not by iterating to fixpoint. **Gate:** No fixpoint iteration in the pipeline. From 89dc1b55f67fe3cde4d861a80b005deda28bc973 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Thu, 26 Mar 2026 11:23:05 -0400 Subject: [PATCH 03/10] C1: move node vocabulary to core; tighten composition stack MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move 7 timeless structural functions from 04_types.dag to 00_core.dag: - Constructors: leaf_node, with_optional_cardinality, with_required_cardinality, no_span - Predicates: node_is_product, node_is_coproduct, node_has_structure Update imports across 14 .dag files to source these from v2.std.core. Bridge-era policy helpers (rt_type, error_type_node, child_inferred_or_name) stay in 04_types.dag — they encode stage-boundary semantics. Tighten Composition Stack per review: - Layer 1 uses Vector not List (machine/container distinction) - DAG defined as nodes + edges + acyclicity law, not "tree with sharing" - Node split: NodePayload (declared shape) vs NodeFacts (computed metadata) - ASTNode = Annotated, NodeFacts> - Pair/Either are ergonomic aliases over Conj/Disj, not new primitives - Span is generic; SourceSpan = Span - Token is parser-domain, not universal std - LabeledTree moved to Layer 4 (structural, not text/source) - Phase placement: declare Tree, Annotated in Phase 3 when generics stable Co-Authored-By: Claude Opus 4.6 (1M context) --- ROADMAP.md | 123 +++++++++++++++++++++----------------- src/v2/00_core.dag | 36 +++++++++++ src/v2/02_parse.dag | 5 +- src/v2/04_access.dag | 7 +-- src/v2/04_emit_info.dag | 6 +- src/v2/04_infer.dag | 8 +-- src/v2/04_items.dag | 6 +- src/v2/04_lookup.dag | 6 +- src/v2/04_method.dag | 5 +- src/v2/04_patterns.dag | 6 +- src/v2/04_resolve.dag | 9 +-- src/v2/04_service.dag | 4 +- src/v2/04_types.dag | 40 +++---------- src/v2/05_emit.dag | 5 +- src/v2/05_emit_go.dag | 5 +- src/v2/05_emit_python.dag | 5 +- src/v2/05_emit_rust.dag | 6 +- 17 files changed, 158 insertions(+), 124 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index d15835f02fe..62b939360fa 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -241,8 +241,11 @@ sequence. Any program that processes trees, dependency graphs, or positional data needs the same structures. The thesis says the compiler is a generic graph processor. If the -compiler needs types that aren't in std, that's a gap in std — not a -special case in the compiler. +compiler needs **structural carrier types** that aren't in std, that's +a gap in std — not a special case in the compiler. (Domain-specific +records like `TypeFacts`, `EmitGraphInfo`, `Diagnostic` are still +compiler semantics; the point is that the **shapes** those records +inhabit should come from shared vocabulary.) ``` Layer 0: Logic @@ -250,12 +253,18 @@ Layer 0: Logic Layer 1: Machine Bit = Classical where width(1) (std/bit.dag) - Byte = { bits: List where length(8) } - Word16/32/64 = List where length(N) + Vector (fixed-width bitvector) + Byte = Vector<8, Bit> + Word32 = Vector<32, Bit> + Int32 = Interpret + + Note: fixed-width machine data uses Vector/bitvector, not List. + List is a variable-length collection (Layer 3). This preserves + the machine/container distinction the roadmap warns about. Layer 2: Named compositions - Int = Int64 = Word64 where signed (std/integer.dag) - Nat = UInt64 = Word64 where unsigned + Int = Int64 = Interpret (std/integer.dag) + Nat = UInt64 = Interpret Char = Int where range(0, 1114111) (std/types.dag) String = List (std/string_type.dag) @@ -265,53 +274,61 @@ Layer 3: Collection algebras Map = K → (1 + V) Layer 4: Structural compositions *** MISSING FROM STD *** - Span = { start: Nat, end: Nat } - Pair = { first: A, second: B } (product / Conj) - Either = Left { a: A } | Right { b: B } (coproduct / Disj) - Tree = { value: A, children: List> } - DAG = topologically ordered Tree with sharing - Annotated = { value: A, facts: F } - -Layer 5: Text/source compositions *** MISSING FROM STD *** - SourceSpan = Span (position in source text) - Token = { text: String, span: SourceSpan, shape: TokenShape } - LabeledTree = Tree<{ name: String, value: A }> + Span = { start: I, end: I } where start <= end + LabeledTree = Tree<{ name: String, value: A }> + Tree = { value: A, children: List> } + DAG = { nodes: Map, + edges: Set> } + where acyclic(edges) + Annotated = { value: A, facts: F } + + Derived conveniences (not new primitives): + Pair = ergonomic alias for Conj with two fields + Either = ergonomic alias for Disj with two variants + SourceSpan = Span + ByteRange = Span + TimeRange = Span` | Recursive hierarchical structure | AST, Node graph | File systems, org charts, UI components, parse trees | -| `DAG` | Acyclic graph with topological order | Module graph, dependency order | Build systems, task scheduling, workflow orchestration | -| `Annotated` | Value enriched with computed facts | TypedModule = Module + type facts | Any pipeline that accumulates metadata over stages | +| `Span` | Contiguous range in a sequence | Source positions | Time ranges, byte ranges, array slices | +| `Tree` | Recursive hierarchical structure | Within-module syntax nodes | File systems, org charts, UI components | +| `DAG` | Directed acyclic graph (nodes + edges + acyclicity) | Module graph, dependency order | Build systems, task scheduling, workflows | +| `Annotated` | Value enriched with computed facts | TypedModule = Annotated | Any pipeline that accumulates metadata | +| `LabeledTree` | Named tree (Tree combinator) | AST node structure | Any labeled hierarchy | **The compiler as an instance of std vocabulary:** -The compiler's domain types (Layer 6) are instances of Layer 4-5 -compositions, not novel inventions: +Within-module syntax nodes are trees; the module graph is a DAG. +`Node` is the universal carrier at the compiler level, but its +within-module shape is `LabeledTree`. The pipeline +enriches this with computed facts via `Annotated`: ``` -Node = LabeledTree -Module = { name: String, tree: Node, imports: List } -ModuleGraph = DAG -TypedModule = Annotated -ResolvedGraph = DAG -TextFile = { path: String, content: String } +ASTNode = Annotated, NodeFacts> +Module = { name: String, tree: ASTNode, imports: List } +ModuleGraph = Annotated, TopologyFacts> +TypedModule = Annotated +ResolvedGraph = DAG ``` -When a user writes a build system in `.dag`, they compose from the -same `DAG` and `Tree` that the compiler uses. When a user writes -a workflow engine, they compose from the same `Annotated` pattern -that the compiler's pipeline stages use. The vocabulary is shared -because the structures are the same — the compiler is not special. +The split between `NodePayload` (declared shape: name, children, +span, params) and `NodeFacts` (resolved/inferred/computed metadata) +lines up with the pipeline story: resolve adds resolved facts, infer +adds type facts, emit reads facts but does not re-resolve them. **The fact composition contract as `Annotated`:** -The pipeline's fact levels (see "Phase 5 Parallel: Compositional -Pipeline") map directly onto the `Annotated` pattern. Each stage -takes a value and returns `Annotated`: +Each pipeline stage produces `Annotated`: ``` resolve: Module → Annotated @@ -319,24 +336,22 @@ infer: Annotated → Annotated emit: Annotated → List ``` -Each stage enriches the previous stage's output with new facts. No -stage re-derives a fact from a lower level. The type system makes the -contract visible: if emit needs a `TypeFact`, the type signature -guarantees it was composed during infer. No runtime resolution needed. +No stage re-derives a fact from a lower level. The type system makes +the contract visible: if emit needs a `TypeFact`, the signature +guarantees it was composed during infer. **P5.11 is the convergence prerequisite.** Once `ExprData` children -dissolve into `node.children`, `Node` cleanly becomes an instance of -`Tree`. The ~5000 lines of custom `ExprData` walkers -reduce to `Tree.map(fn)` — the standard library's generic tree -traversal. P5.11 is what moves the compiler's AST from ad-hoc -internals into std vocabulary. - -**Timeline:** With P3.8 (recursive generics) landed, `Tree` is -expressible in the `.dag` language today. Defining the Layer 4 types -in `dsl/std/` is tractable now; migrating the compiler to use them -as instances is post-Phase 5 work. The design targets convergence -onto std compositions. The convergence is tracked separately from the -phase plan because it crosses all phases. +dissolve into `node.children`, `Node` cleanly maps to +`LabeledTree`. The ~5000 lines of custom `ExprData` +walkers reduce to `Tree.map(fn)`. P5.11 moves the compiler's AST +from ad-hoc internals into std vocabulary. + +**Phase placement:** With P3.8 (recursive generics) landed, `Tree` +and `Annotated` are expressible in `.dag` today. These types +should be declared in `dsl/std/` as soon as generics are stable +(Phase 3), not deferred to post-Phase 5. The full compiler refactor +to use them as instances is later work, but the types themselves +should exist early so intermediate steps move toward the design. **Type Layer Downgrade metric.** The composition stack provides an objective architectural-debt metric. When a pipeline stage re-derives diff --git a/src/v2/00_core.dag b/src/v2/00_core.dag index d10bb6af394..8b904534e49 100644 --- a/src/v2/00_core.dag +++ b/src/v2/00_core.dag @@ -989,3 +989,39 @@ fn is_module_node(n: Node) -> Bool { fn is_token_node(n: Node) -> Bool { n.properties |> any(p => p.name == "is_token") } + +// ========================================================================= +// Node vocabulary (structural constructors and predicates) +// +// These operate on Node structural fields (connective, cardinality) and +// need no environment. They are the shared vocabulary for all pipeline +// stages — not stage-specific concepts. Moved from 04_types.dag (C1). +// ========================================================================= + +fn leaf_node(name: String) -> Node { + Node { name: name, span: SourceSpan { start: 0, end: 0 }, children: [], connective: none, params: [], inferred: none, return_cardinality: Required, uses: [], body: none, transport: none, properties: [], type_annotation: none, config: none, is_self_recursive: false, has_non_tail_self_call: false, expr_data: NoExprData } +} + +fn no_span() -> SourceSpan { + SourceSpan { start: 0, end: 0 } +} + +fn with_optional_cardinality(n: Node) -> Node { + Node { name: n.name, span: n.span, children: n.children, connective: n.connective, params: n.params, inferred: n.inferred, return_cardinality: CardOptional, uses: n.uses, body: n.body, transport: n.transport, properties: n.properties, type_annotation: n.type_annotation, config: n.config, is_self_recursive: n.is_self_recursive, has_non_tail_self_call: n.has_non_tail_self_call, expr_data: n.expr_data } +} + +fn with_required_cardinality(n: Node) -> Node { + Node { name: n.name, span: n.span, children: n.children, connective: n.connective, params: n.params, inferred: n.inferred, return_cardinality: Required, uses: n.uses, body: n.body, transport: n.transport, properties: n.properties, type_annotation: n.type_annotation, config: n.config, is_self_recursive: n.is_self_recursive, has_non_tail_self_call: n.has_non_tail_self_call, expr_data: n.expr_data } +} + +fn node_is_product(n: Node) -> Bool { + n.connective != none && n.connective == Some { value: Conj } +} + +fn node_is_coproduct(n: Node) -> Bool { + n.connective != none && n.connective == Some { value: Disj } +} + +fn node_has_structure(n: Node) -> Bool { + node_is_product(n: n) || node_is_coproduct(n: n) +} diff --git a/src/v2/02_parse.dag b/src/v2/02_parse.dag index edf94215d3b..42304bb77e3 100644 --- a/src/v2/02_parse.dag +++ b/src/v2/02_parse.dag @@ -55,10 +55,11 @@ import v2.std.core { Ident, StrBegin, StrMid, StrEnd, Newline, Eof, Unknown, Diagnostic, Error, - SourceSpan + SourceSpan, + node_is_product, node_is_coproduct, with_required_cardinality } -import v2.compiler.infer_types { node_is_optional, node_is_product, node_is_coproduct, with_required_cardinality, callable_node } +import v2.compiler.infer_types { node_is_optional, callable_node } // ========================================================================= // Parser state types diff --git a/src/v2/04_access.dag b/src/v2/04_access.dag index a04bd85e947..346579ca810 100644 --- a/src/v2/04_access.dag +++ b/src/v2/04_access.dag @@ -11,15 +11,14 @@ import std.types { SourceSpan } import v2.std.core { Node, InferredNode, Resolved, CompilerError, - Diagnostic, Severity, Error + Diagnostic, Severity, Error, + leaf_node, with_optional_cardinality, node_has_structure } import v2.compiler.infer_types { - leaf_node, with_optional_cardinality, node_is_map, normalize_access_type_node, node_type_equals, - is_int_type_node, is_string_type_node, - node_has_structure + is_int_type_node, is_string_type_node } // ========================================================================= diff --git a/src/v2/04_emit_info.dag b/src/v2/04_emit_info.dag index 90b868433ca..10a206df61f 100644 --- a/src/v2/04_emit_info.dag +++ b/src/v2/04_emit_info.dag @@ -15,11 +15,11 @@ import v2.std.core { InferredNode, Resolved, FieldAccessStyle, StoredField, EnumAccessor, TupleFirst, TupleSecond, FieldValueShape, PlainValue, OptionalValue, - FieldSummary + FieldSummary, + node_has_structure, node_is_product, with_required_cardinality } import v2.compiler.infer_types { - node_has_structure, node_is_product, node_is_optional, - with_required_cardinality, + node_is_optional, normalize_type_name, normalize_access_type_node, rt_type, child_inferred_or_name, node_type_equals } diff --git a/src/v2/04_infer.dag b/src/v2/04_infer.dag index 0bedccb1ba5..109c9742469 100644 --- a/src/v2/04_infer.dag +++ b/src/v2/04_infer.dag @@ -72,19 +72,19 @@ import v2.std.core { UnaryOpKind, Not, Neg, MatchArm, MatchPattern, FieldBinding, FieldInit, NamedArg, StringPart, make_transport_node, local_transport_node, - Text, Interpolation + Text, Interpolation, + leaf_node, with_optional_cardinality, with_required_cardinality, + node_is_product, node_is_coproduct, node_has_structure, no_span } import v2.compiler.resolve { ModuleGraph, ResolvedModule, ResolvedImport } import v2.compiler.infer_types { child_inferred_or_name, - leaf_node, with_optional_cardinality, with_required_cardinality, container_node, map_node, bare_map_node, tuple_node, callable_node, callable_inferred, error_type_node, node_is_container, node_is_optional, node_is_map, node_is_leaf, node_is_named_ref, - node_is_product, node_is_coproduct, node_has_structure, normalize_access_type_node, normalize_type_name, node_type_shape, node_type_compatible, node_type_equals, prefer_specific_type, node_type_deps, @@ -92,7 +92,7 @@ import v2.compiler.infer_types { method_receiver_element_node, infer_literal_node, infer_binop_type_node, extract_optional_inner_node, for_each_element_type_node, - rt_type, no_span, + rt_type, emit_map_has } import v2.compiler.infer_method { diff --git a/src/v2/04_items.dag b/src/v2/04_items.dag index c1e8562849f..f9e917c135b 100644 --- a/src/v2/04_items.dag +++ b/src/v2/04_items.dag @@ -13,11 +13,11 @@ import v2.std.core { InferredNode, Resolved, CompilerError, Cardinality, Required, Diagnostic, - expr_has_self_call, expr_has_non_tail_self_call + expr_has_self_call, expr_has_non_tail_self_call, + leaf_node, node_has_structure, node_is_product, node_is_coproduct } import v2.compiler.infer_types { - leaf_node, rt_type, - node_has_structure, node_is_product, node_is_coproduct + rt_type } import v2.compiler.infer_env { TypeEnv, TypeBinding diff --git a/src/v2/04_lookup.dag b/src/v2/04_lookup.dag index 47710b813a9..ddf08f71992 100644 --- a/src/v2/04_lookup.dag +++ b/src/v2/04_lookup.dag @@ -19,14 +19,14 @@ import v2.std.core { RuntimeBridgeMethod, FieldAccessStyle, OptionalUnwrap, FieldValueShape, PlainValue, OptionalValue, - FieldSummary + FieldSummary, + leaf_node, with_optional_cardinality, with_required_cardinality, + node_is_product, node_is_coproduct, node_has_structure } import v2.compiler.infer_types { child_inferred_or_name, - leaf_node, with_optional_cardinality, with_required_cardinality, error_type_node, node_is_optional, node_is_map, - node_is_product, node_is_coproduct, node_has_structure, normalize_access_type_node, normalize_type_name, rt_type, rt_node, emit_map_has diff --git a/src/v2/04_method.dag b/src/v2/04_method.dag index 4eb22546ff7..8826f9c519c 100644 --- a/src/v2/04_method.dag +++ b/src/v2/04_method.dag @@ -22,10 +22,11 @@ import v2.std.core { BridgeMapContainsKey, BridgeCharAt, BridgeStringAt, BridgeStringLength, BridgeLength, BridgeStartsWith, BridgeEndsWith, BridgeToString, BridgeTrim, BridgeToLower, BridgeToUpper, BridgeReplace, BridgeSubstring, BridgeToInt, BridgeEmptyMap, - BridgeContains, BridgeReverse, BridgeLookup + BridgeContains, BridgeReverse, BridgeLookup, + leaf_node, with_optional_cardinality } import v2.compiler.infer_types { - leaf_node, with_optional_cardinality, container_node, tuple_node, error_type_node, + container_node, tuple_node, error_type_node, bare_map_node, method_receiver_element_node } diff --git a/src/v2/04_patterns.dag b/src/v2/04_patterns.dag index e69a2d6c479..99fbb292fe9 100644 --- a/src/v2/04_patterns.dag +++ b/src/v2/04_patterns.dag @@ -17,14 +17,14 @@ import v2.std.core { Cardinality, Required, Diagnostic, Severity, Error, ErrorCategory, FieldNotFound, VariantNotFound, InvalidOperation, - MatchArm, MatchPattern + MatchArm, MatchPattern, + leaf_node, node_is_coproduct, node_has_structure, with_optional_cardinality } import v2.compiler.infer_types { child_inferred_or_name, - leaf_node, error_type_node, node_is_bridge_error_name, node_is_bridge_dynamic_name, - node_is_optional, node_is_coproduct, node_has_structure, with_optional_cardinality, + node_is_optional, extract_optional_inner_node, emit_map_has } diff --git a/src/v2/04_resolve.dag b/src/v2/04_resolve.dag index a95192f032b..4487c7d9521 100644 --- a/src/v2/04_resolve.dag +++ b/src/v2/04_resolve.dag @@ -29,12 +29,13 @@ import v2.std.core { make_transport_node, local_transport_node, is_kernel_type, is_transport_kind, ErrorCategory, UnresolvedName, TypeMismatch, InvalidOperation, CascadeError, - TransportKind, LocalTransport + TransportKind, LocalTransport, + leaf_node, with_optional_cardinality, with_required_cardinality, + node_has_structure, node_is_product, no_span } import v2.compiler.infer_types { - leaf_node, with_optional_cardinality, with_required_cardinality, - node_has_structure, node_is_product, node_is_optional, node_is_map, node_is_container, - rt_type, no_span + node_is_optional, node_is_map, node_is_container, + rt_type } import v2.compiler.infer_env { TypeEnv, lookup_type, is_recursive_type diff --git a/src/v2/04_service.dag b/src/v2/04_service.dag index d742b98517d..91bcf31fbb7 100644 --- a/src/v2/04_service.dag +++ b/src/v2/04_service.dag @@ -16,10 +16,10 @@ import v2.std.core { ExprFieldAccess, ExprMethodCall, ExprCall, ExprVar, Cardinality, Required, InferredNode, Resolved, - expr_children + expr_children, + leaf_node, no_span, node_has_structure } import v2.compiler.infer_types { - leaf_node, no_span, node_has_structure, emit_map_has } import v2.compiler.infer_items { diff --git a/src/v2/04_types.dag b/src/v2/04_types.dag index ba0a80a3221..2f92e686847 100644 --- a/src/v2/04_types.dag +++ b/src/v2/04_types.dag @@ -20,7 +20,9 @@ import v2.std.core { is_kernel_type, is_kernel_numeric, is_kernel_textual, BinOpKind, BinEq, BinNe, BinLt, BinGt, BinLe, BinGe, BinAnd, BinOr, NullCoalesce, InferredNode, Resolved, CompilerError, rt_node, has_inferred, - NodeType, Typed, InferError, Untyped + NodeType, Typed, InferError, Untyped, + leaf_node, no_span, with_optional_cardinality, with_required_cardinality, + node_is_product, node_is_coproduct, node_has_structure } // ========================================================================= @@ -32,13 +34,13 @@ fn child_inferred_or_name(ch: Node) -> Node { else { rt_type(n: ch) } } -fn leaf_node(name: String) -> Node { - Node { name: name, span: SourceSpan { start: 0, end: 0 }, children: [], connective: none, params: [], inferred: none, return_cardinality: Required, uses: [], body: none, transport: none, properties: [], type_annotation: none, config: none, is_self_recursive: false, has_non_tail_self_call: false, expr_data: NoExprData } -} +// leaf_node, with_optional_cardinality, with_required_cardinality +// moved to 00_core.dag (C1 — timeless node vocabulary). // Convenience: extract the return type Node, falling back to Unit. // Use for emission callers and statement contexts where Unit is correct. // For inference callers that need error-awareness, match rt_node directly. +// Stays here: encodes bridge-era stage-boundary policy (Unit fallback). fn rt_type(n: Node) -> Node { match rt_node(n: n) { Typed { node: rt } => rt @@ -47,17 +49,6 @@ fn rt_type(n: Node) -> Node { } } -// P5.7a: product_property() and coproduct_property() deleted. -// connective (Conj/Disj) is the sole authority for product/coproduct. - -fn with_optional_cardinality(n: Node) -> Node { - Node { name: n.name, span: n.span, children: n.children, connective: n.connective, params: n.params, inferred: n.inferred, return_cardinality: CardOptional, uses: n.uses, body: n.body, transport: n.transport, properties: n.properties, type_annotation: n.type_annotation, config: n.config, is_self_recursive: n.is_self_recursive, has_non_tail_self_call: n.has_non_tail_self_call, expr_data: n.expr_data } -} - -fn with_required_cardinality(n: Node) -> Node { - Node { name: n.name, span: n.span, children: n.children, connective: n.connective, params: n.params, inferred: n.inferred, return_cardinality: Required, uses: n.uses, body: n.body, transport: n.transport, properties: n.properties, type_annotation: n.type_annotation, config: n.config, is_self_recursive: n.is_self_recursive, has_non_tail_self_call: n.has_non_tail_self_call, expr_data: n.expr_data } -} - fn container_node(kind_name: String, element: Node) -> Node { Node { name: kind_name, span: SourceSpan { start: 0, end: 0 }, children: [element], connective: none, params: [], inferred: none, return_cardinality: Required, uses: [], body: none, transport: none, properties: [], type_annotation: none, config: none, is_self_recursive: false, has_non_tail_self_call: false, expr_data: NoExprData } } @@ -111,26 +102,13 @@ fn node_is_bridge_dynamic_name(n: Node) -> Bool { n.name == "Dynamic" } -fn no_span() -> SourceSpan { - SourceSpan { start: 0, end: 0 } -} +// no_span, node_is_product, node_is_coproduct, node_has_structure +// moved to 00_core.dag (C1 — timeless node vocabulary). // ========================================================================= -// Type predicates +// Type predicates (name-checking and bridge-era — stay here until L1=0) // ========================================================================= -fn node_is_product(n: Node) -> Bool { - n.connective != none && n.connective == Some { value: Conj } -} - -fn node_is_coproduct(n: Node) -> Bool { - n.connective != none && n.connective == Some { value: Disj } -} - -fn node_has_structure(n: Node) -> Bool { - node_is_product(n: n) || node_is_coproduct(n: n) -} - fn node_is_container(n: Node) -> Bool { n.name == "List" || n.name == "Set" || n.name == "NonEmptyList" || n.name == "NonEmptySet" } diff --git a/src/v2/05_emit.dag b/src/v2/05_emit.dag index 8096cc33f97..1214280956a 100644 --- a/src/v2/05_emit.dag +++ b/src/v2/05_emit.dag @@ -49,11 +49,12 @@ import v2.std.core { TransportKind, RestTransport, ShellTransport, FileTransport, is_transport_kind, transport_has_auth, - field_init_operation_modifier, operation_modifier_name + field_init_operation_modifier, operation_modifier_name, + leaf_node, node_is_product, node_is_coproduct, node_has_structure, with_required_cardinality } import v2.compiler.infer_env { TypeEnv, TypeBinding } -import v2.compiler.infer_types { leaf_node, node_is_optional, node_is_map, node_is_product, node_is_coproduct, node_has_structure, with_required_cardinality, rt_type, emit_map_has } +import v2.compiler.infer_types { node_is_optional, node_is_map, rt_type, emit_map_has } import v2.compiler.infer_sigs { ResolvedFuncSig, ResolvedFuncEnv } import v2.compiler.infer_items { TypedModule, ResolvedGraph, ItemInfo diff --git a/src/v2/05_emit_go.dag b/src/v2/05_emit_go.dag index 9f91ea19c82..369499c2fc7 100644 --- a/src/v2/05_emit_go.dag +++ b/src/v2/05_emit_go.dag @@ -60,7 +60,8 @@ import v2.std.core { BridgeEmptyMap, BridgeContains, BridgeReverse, BridgeLookup, Text, Interpolation, FieldSummary, FieldAccessStyle, - StoredField, EnumAccessor, OptionalUnwrap, TupleFirst, TupleSecond + StoredField, EnumAccessor, OptionalUnwrap, TupleFirst, TupleSecond, + leaf_node, with_required_cardinality } import v2.compiler.artifact { RenderTarget, Go } @@ -69,7 +70,7 @@ import v2.compiler.languages { scaffold_for_target, test_conventions_for_target } import v2.compiler.infer_env { TypeEnv, TypeBinding } -import v2.compiler.infer_types { for_each_element_type_node, node_is_optional, node_is_map, leaf_node, with_required_cardinality, rt_type } +import v2.compiler.infer_types { for_each_element_type_node, node_is_optional, node_is_map, rt_type } import v2.compiler.infer_sigs { ResolvedFuncSig, ResolvedFuncEnv } import v2.compiler.infer_items { ResolvedGraph, TypedModule, diff --git a/src/v2/05_emit_python.dag b/src/v2/05_emit_python.dag index 8b5a867ba75..f3c5c8fc5db 100644 --- a/src/v2/05_emit_python.dag +++ b/src/v2/05_emit_python.dag @@ -50,7 +50,8 @@ import v2.std.core { BridgeToLower, BridgeToUpper, BridgeReplace, BridgeSubstring, BridgeToInt, BridgeEmptyMap, BridgeContains, BridgeReverse, BridgeLookup, FieldSummary, TupleFirst, TupleSecond, - Text, Interpolation + Text, Interpolation, + leaf_node, with_required_cardinality } import v2.compiler.artifact { RenderTarget, Python } @@ -58,7 +59,7 @@ import v2.compiler.languages { scaffold_for_target, serialization_for_target, test_conventions_for_target } import v2.compiler.infer_env { TypeEnv, TypeBinding } -import v2.compiler.infer_types { for_each_element_type_node, node_is_optional, node_is_map, leaf_node, with_required_cardinality, rt_type } +import v2.compiler.infer_types { for_each_element_type_node, node_is_optional, node_is_map, rt_type } import v2.compiler.infer_sigs { ResolvedFuncSig, ResolvedFuncEnv } import v2.compiler.infer_items { ResolvedGraph, TypedModule, diff --git a/src/v2/05_emit_rust.dag b/src/v2/05_emit_rust.dag index c730877b451..e9163107957 100644 --- a/src/v2/05_emit_rust.dag +++ b/src/v2/05_emit_rust.dag @@ -57,7 +57,8 @@ import v2.std.core { transport_base_url, transport_has_auth, transport_auth_token, transport_auth_header_name, transport_headers, transport_env, - expr_has_self_call, expr_has_non_tail_self_call + expr_has_self_call, expr_has_non_tail_self_call, + leaf_node, with_required_cardinality, node_has_structure } import v2.compiler.artifact { RenderTarget, Rust } @@ -73,9 +74,8 @@ import v2.compiler.runtime_rust { rust_runtime_source } import v2.compiler.infer_env { TypeEnv, TypeBinding } import v2.compiler.infer_types { normalize_access_type_node, for_each_element_type_node, - node_is_optional, node_is_map, node_is_container, node_has_structure, + node_is_optional, node_is_map, node_is_container, is_int_type_node, is_string_type_node, is_bool_type_node, is_float_type_node, - leaf_node, with_required_cardinality, rt_type, emit_map_has } import v2.compiler.infer_sigs { ResolvedFuncEnv } From 8e0c33ec0f1fa8f5d662bf1673f92411bd2e5fbe Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Thu, 26 Mar 2026 12:07:53 -0400 Subject: [PATCH 04/10] C2.1 + C4.1: structural variant locals + service convergence C2.1: Variant constructor locals now carry the structural parent enum node (binding.resolved / item) instead of leaf_node(name: binding.name). This means .inferred on variant expressions will be structurally authoritative. Two sites fixed: imported_variant_locals in 04_infer.dag and variant_locals_from_items in 04_items.dag. C4.1: Service expansion now exits early when converged (total service count unchanged between passes) instead of always running 5 passes. Most codebases converge in 1-2 passes. All 119 non-ignored tests pass. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/v2/04_infer.dag | 7 ++++++- src/v2/04_items.dag | 3 ++- src/v2/04_service.dag | 13 ++++++++++++- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/v2/04_infer.dag b/src/v2/04_infer.dag index 109c9742469..614db3336ab 100644 --- a/src/v2/04_infer.dag +++ b/src/v2/04_infer.dag @@ -2214,12 +2214,17 @@ fn build_module_context(contributions: List, item_registry: empty_map(), diag_chunks: [] ) + // C2.1: variant constructor locals carry the structural parent enum node, + // not leaf_node(name). This means .inferred on variant expressions will be + // structurally authoritative — emit can read it directly without re-resolving + // through TypeEnv. binding.resolved.name still returns the enum's name for + // lookup_variant_parent_enum and record lit inference. let imported_variant_locals = fold(map_values(env.bindings), init: empty_map(), f: (acc, binding) => if node_is_coproduct(n: binding.resolved) { fold(binding.resolved.children, init: acc, f: (vacc, child) => map_insert(vacc, child.name, TypeBinding { name: child.name, - resolved: leaf_node(name: binding.name) + resolved: binding.resolved }) ) } else { acc } diff --git a/src/v2/04_items.dag b/src/v2/04_items.dag index f9e917c135b..21115a68ce6 100644 --- a/src/v2/04_items.dag +++ b/src/v2/04_items.dag @@ -121,13 +121,14 @@ fn item_kind(item: Node) -> ItemKind { kind } +// C2.1: variant locals carry the structural parent node, not leaf_node(name). fn variant_locals_from_items(items: List, init: Map) -> Map { fold(items, init: init, f: (acc, item) => if node_is_coproduct(n: item) { fold(item.children, init: acc, f: (vacc, child) => map_insert(vacc, child.name, TypeBinding { name: child.name, - resolved: leaf_node(name: item.name) + resolved: item }) ) } else { acc } diff --git a/src/v2/04_service.dag b/src/v2/04_service.dag index 91bcf31fbb7..2c18af2f4eb 100644 --- a/src/v2/04_service.dag +++ b/src/v2/04_service.dag @@ -166,11 +166,22 @@ fn expand_transitive_services_once(modules: List, registry: Map) -> Int { + map_values(registry) |> fold(init: 0, f: (acc, info) => acc + info.service_names |> count) +} + +// C4.1: converge to fixpoint instead of hardcoded 5 passes. Each pass +// propagates service dependencies from callees to callers. The loop +// terminates when no new service names are discovered (total count stable). +// Bounded by remaining_passes as safety net against pathological cases. fn expand_transitive_services(modules: List, registry: Map, remaining_passes: Int) -> Map { if remaining_passes <= 0 { registry } else { + let before = total_service_count(registry: registry) let next = expand_transitive_services_once(modules: modules, registry: registry) - expand_transitive_services(modules: modules, registry: next, remaining_passes: remaining_passes - 1) + let after = total_service_count(registry: next) + if before == after { registry } + else { expand_transitive_services(modules: modules, registry: next, remaining_passes: remaining_passes - 1) } } } From 51afc8660a62beb55966ee9cd6da0f6cff7ae4ce Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Thu, 26 Mar 2026 12:13:12 -0400 Subject: [PATCH 05/10] C2.3: follow alias .inferred in resolve_node_bounded MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When resolve_node_bounded encounters an alias node (leaf with .inferred but no structure), follow ONE level of .inferred to return the structural target. This is O(1), not recursive — topo ordering guarantees the target is already resolved. Without this, downstream consumers see an alias_node wrapper (e.g. { name: "UserId", inferred: Resolved { node: leaf("String") } }) and must re-resolve through TypeEnv — a layer downgrade from structural to name-based authority. All 112 non-ignored tests pass. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/v2/04_resolve.dag | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/v2/04_resolve.dag b/src/v2/04_resolve.dag index 4487c7d9521..a8ee24e7bce 100644 --- a/src/v2/04_resolve.dag +++ b/src/v2/04_resolve.dag @@ -356,11 +356,26 @@ fn resolve_node_bounded(n: Node, env: TypeEnv, module_name: String, depth: Int) // any dependent type is processed. Re-resolving the looked-up node // would redundantly walk the entire expanded subtree, causing // exponential blowup on diamond-shaped dependency graphs (OOM). + // + // C2.3: For alias nodes (leaf with .inferred but no structure), + // follow ONE level of .inferred to return the structural target. + // This is O(1), not recursive — the target is already resolved + // by topo ordering. Without this, downstream consumers see an + // alias_node wrapper and must re-resolve through TypeEnv (layer + // downgrade from structural to name-based). + // // Preserve CardOptional from the reference node: the env binding // always stores the canonical (Required) definition, but the // reference site may carry CardOptional (e.g. String? → String // in env). Without this, optionality is silently discarded. - let final_resolved = if node_is_optional(n: n) { with_optional_cardinality(n: resolved) } else { resolved } + let structurally_resolved = if node_has_structure(n: resolved) == false && resolved.children |> count == 0 && resolved.inferred != none { + match rt_node(n: resolved) { + Typed { node: target } => target + InferError { message: _, span: _ } => resolved + Untyped => resolved + } + } else { resolved } + let final_resolved = if node_is_optional(n: n) { with_optional_cardinality(n: structurally_resolved) } else { structurally_resolved } NodeResolveResult { resolved: final_resolved, diagnostics: [] } None => if is_kernel_type(name: n.name) || n.name == "Dynamic" || n.name == "Error" || n.name == "Callable" { From f49814c5d1dd96855703e6df3940598247e63a1f Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Thu, 26 Mar 2026 12:15:14 -0400 Subject: [PATCH 06/10] C2.2 + C2.3: structural Optional locals + alias resolution MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit C2.2: Remove explicit leaf_node(name: "Optional") overrides for Some/None variant locals. The general variant constructor mechanism (C2.1) already handles Optional — kernel_optional is a Disj node in env.bindings, so imported_variant_locals registers Some and None with the structural parent. The explicit overrides were undoing C2.1's fix. C2.3: In resolve_node_bounded, when encountering an alias node (leaf with .inferred but no structure), follow ONE level of .inferred to return the structural target. O(1), safe with topo ordering. This eliminates the alias_node wrapper that forced emit to re-resolve through TypeEnv. C2.4 (ResolvedFuncSig) is likely fixed by C2.3 — the return type flows through resolve_item_types → resolve_optional_node → resolve_node_bounded (now with alias following) before reaching DeclaredFuncSig. All 112 non-ignored tests pass. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/v2/04_infer.dag | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/v2/04_infer.dag b/src/v2/04_infer.dag index 614db3336ab..962a807e72a 100644 --- a/src/v2/04_infer.dag +++ b/src/v2/04_infer.dag @@ -2244,15 +2244,11 @@ fn build_module_context(contributions: List, items: local.resolved_items, module_name: module_name ) - // Variant locals come from env_variant_locals (computed once from env.bindings, - // which already includes all local + imported disjunction types). - let optional_locals = map_insert(env_variant_locals, "Some", TypeBinding { - name: "Some", resolved: leaf_node(name: "Optional") - }) - let optional_locals = map_insert(optional_locals, "None", TypeBinding { - name: "None", resolved: leaf_node(name: "Optional") - }) - let all_locals = fold(map_values(merged_scope.svc_locals), init: optional_locals, f: (acc, binding) => + // C2.2: Optional Some/None now handled by the general variant constructor + // mechanism (C2.1). kernel_optional is a Disj node in env.bindings, so + // imported_variant_locals already registers Some and None with the structural + // parent node. No need for explicit leaf_node(name: "Optional") override. + let all_locals = fold(map_values(merged_scope.svc_locals), init: env_variant_locals, f: (acc, binding) => map_insert(acc, binding.name, binding) ) ModuleContext { From a6d2e0a816f4c4c8400633d0728f61aabaf607de Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Thu, 26 Mar 2026 12:20:53 -0400 Subject: [PATCH 07/10] Fix: total timing on all early-return error paths Add total timing output before each of the 4 early-return sites (frontend error, resolve error, normalize error, typecheck error). Previously t_total was initialized but only printed on the happy path. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/v2/stage0/src/compile.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/v2/stage0/src/compile.rs b/src/v2/stage0/src/compile.rs index 213034d30ce..7b232e85eb4 100644 --- a/src/v2/stage0/src/compile.rs +++ b/src/v2/stage0/src/compile.rs @@ -1062,6 +1062,7 @@ pub fn compile_sources(sources: Rc>>, target: RenderTarget) - eprintln!(" phase: frontend {:>10.1}ms", t0.elapsed().as_secs_f64() * 1000.0); match frontend.graph.as_ref().map(|__rc| __rc.as_ref()) { None => { + eprintln!(" phase: total {:>10.1}ms (frontend error)", t_total.elapsed().as_secs_f64() * 1000.0); Rc::new(PipelineResult { files: Rc::new(Vec::new()), diagnostics: frontend.diagnostics.clone(), complexity: empty_complexity_report(), ownership: Rc::new(Vec::new()), artifact_plan: empty_artifact_plan() }) } Some(graph) => { @@ -1081,6 +1082,7 @@ pub fn compile_sources(sources: Rc>>, target: RenderTarget) - let __len_2 = resolve_errors.clone().len(); __len_2 as i64 }) > 0_i64 { + eprintln!(" phase: total {:>10.1}ms (resolve error)", t_total.elapsed().as_secs_f64() * 1000.0); return Rc::new(PipelineResult { files: Rc::new(Vec::new()), diagnostics: frontend.diagnostics.clone(), complexity: empty_complexity_report(), ownership: Rc::new(Vec::new()), artifact_plan: empty_artifact_plan() }); }; let t1 = Instant::now(); @@ -1100,6 +1102,7 @@ pub fn compile_sources(sources: Rc>>, target: RenderTarget) - let __len_5 = norm_errors.clone().len(); __len_5 as i64 }) > 0_i64 { + eprintln!(" phase: total {:>10.1}ms (normalize error)", t_total.elapsed().as_secs_f64() * 1000.0); return Rc::new(PipelineResult { files: Rc::new(Vec::new()), diagnostics: v2_rt::concat(frontend.diagnostics.clone(), norm_diags.clone()), complexity: empty_complexity_report(), ownership: Rc::new(Vec::new()), artifact_plan: empty_artifact_plan() }); }; let t2 = Instant::now(); @@ -1123,6 +1126,7 @@ pub fn compile_sources(sources: Rc>>, target: RenderTarget) - let __len_8 = typecheck_errors.clone().len(); __len_8 as i64 }) > 0_i64 { + eprintln!(" phase: total {:>10.1}ms (typecheck error)", t_total.elapsed().as_secs_f64() * 1000.0); return Rc::new(PipelineResult { files: Rc::new(Vec::new()), diagnostics: v2_rt::concat(v2_rt::concat(frontend.diagnostics.clone(), norm_diags.clone()), typed_diags.clone()), complexity: complexity.clone(), ownership: Rc::new(Vec::new()), artifact_plan: empty_artifact_plan() }); }; let t4 = Instant::now(); From 2e018f8dd8c6c11cdb6a438a86432e3851ae578f Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Thu, 26 Mar 2026 12:22:54 -0400 Subject: [PATCH 08/10] Update ROADMAP: mark C1-C4.1 done, add implementation findings + C2.5 notes Record implementation findings from this session: - C2.3 was the key fix (in resolve_node_bounded, not build_type_env) - C2.4 was already fixed by C2.3 (resolution chain flows through) - C2.2 was redundant with C2.1 (general mechanism handles Optional) - C4.1 implemented as early convergence (full topo-order deferred) Add detailed C2.5 implementation notes for next worker: 12 call sites in 05_emit_rust.dag, per-site migration pattern, risk assessment, is_already_optional simplification guidance. Mark all completed items: C1.1-C1.3, C2.1-C2.4, C4.1, C5.1. Co-Authored-By: Claude Opus 4.6 (1M context) --- ROADMAP.md | 95 +++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 69 insertions(+), 26 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 62b939360fa..ed449090b07 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -2482,18 +2482,29 @@ The TypeEnv already carries resolved structural types. But inference fabricates name-only references at these sites instead of using the structural fact: -| Site | File:Line | What happens | What it should do | -|------|-----------|-------------|-------------------| -| Variant constructor locals | `04_infer.dag:2217` | `resolved: leaf_node(name: binding.name)` — bare parent enum name | Use `binding.resolved` — the structural enum definition | -| Optional Some/None | `04_infer.dag:2244` | `resolved: leaf_node(name: "Optional")` — hardcoded name | Cardinality on the binding site (P1.4); no "Optional" name needed | -| Type alias registration | `04_infer.dag:2053` | alias_node stored with original `.inferred` intact | Resolve through TypeEnv before storing | -| Func call return type | `04_infer.dag:1065` | `s.inferred` from sig used directly | Sig should already carry structural type | -| ResolvedFuncSig | `04_sigs.dag:112` | `dsig.inferred.value` unwrapped without resolution | Resolve through TypeEnv at sig construction time | - -**Key observation:** None of these require a new pass. Each is a site -where inference already has the structural fact (via `TypeBinding` or -`TypeEnv`) but discards it in favor of a name. The fix is: use the -fact that's already composed. +| Site | File:Line | What happens | Fix | Status | +|------|-----------|-------------|-----|--------| +| Variant constructor locals | `04_infer.dag:2217`, `04_items.dag:130` | `resolved: leaf_node(name: binding.name)` — bare parent enum name | Use `binding.resolved` / `item` — the structural enum definition | **Done** (C2.1) | +| Optional Some/None | `04_infer.dag:2244` | `resolved: leaf_node(name: "Optional")` — hardcoded name | Delete explicit overrides — general variant mechanism (C2.1) already handles Optional via `kernel_optional` coproduct in env | **Done** (C2.2) | +| Type alias resolution | `04_resolve.dag:353` | `resolve_node_bounded` returns alias_node wrapper without following `.inferred` | Follow ONE level of `.inferred` for alias nodes (leaf + .inferred, no structure). O(1), safe with topo ordering. | **Done** (C2.3) | +| Func call return type | `04_infer.dag:1065` | `s.inferred` from sig used directly | **Already fixed by C2.3** — the return type flows through `resolve_item_types` → `resolve_node_bounded` (now with alias following) before reaching `DeclaredFuncSig` | **Done** (via C2.3) | +| ResolvedFuncSig | `04_sigs.dag:112` | `dsig.inferred.value` unwrapped without resolution | **Already fixed by C2.3** — same resolution chain | **Done** (via C2.3) | + +**Implementation findings (2026-03-26):** + +- C2.3 was the key fix — not in `build_type_env` (TypeEnv incomplete + there) but in `resolve_node_bounded` (TypeEnv complete, topo order + guarantees target already resolved). The alias_node was a one-level + indirection that forced emit to re-resolve. +- C2.4 (func sig) turned out to be already fixed by C2.3: the return + type flows through `resolve_item_types` → `resolve_optional_node` → + `resolve_node_bounded` (now with alias following). +- C2.2 (Optional) turned out to be redundant with C2.1: the explicit + `leaf_node(name: "Optional")` overrides were undoing C2.1's fix. + Deleting them lets the general coproduct mechanism handle Optional. + +**Key observation:** None of these required a new pass. Each was a site +where the structural fact existed but was discarded in favor of a name. #### C1: Node vocabulary into core @@ -2506,9 +2517,9 @@ inversion. | ID | Item | Scope | Notes | |----|------|-------|-------| -| C1.1 | Move timeless node constructors to `00_core.dag` | ~4 functions | `leaf_node`, `with_optional_cardinality`, `with_required_cardinality`, `no_span` | -| C1.2 | Move structural predicates to `00_core.dag` | ~3 functions | `node_is_product`, `node_is_coproduct`, `node_has_structure` | -| C1.3 | Update import statements | ~15 files | Move symbols from `import v2.compiler.infer_types` to `import v2.std.core` | +| C1.1 | Move timeless node constructors to `00_core.dag` | **Done** | `leaf_node`, `with_optional_cardinality`, `with_required_cardinality`, `no_span` | +| C1.2 | Move structural predicates to `00_core.dag` | **Done** | `node_is_product`, `node_is_coproduct`, `node_has_structure` | +| C1.3 | Update import statements | **Done** | 14 .dag files updated | **What stays in `04_types.dag`:** Three categories: @@ -2536,15 +2547,47 @@ new Node fields. No post-hoc resolution pass. Fix the 5 sites where inference fabricates name-only references instead of using the structural fact from the environment. -| ID | Item | Scope | Notes | -|----|------|-------|-------| -| C2.1 | Variant constructor locals | `04_infer.dag:2217-2226` | Use `binding.resolved` (structural enum node) instead of `leaf_node(name: binding.name)` | -| C2.2 | Optional Some/None locals | `04_infer.dag:2244-2249` | Use cardinality model for binding-site absence/presence. Preserve the distinction: binding-site absence = cardinality; value-level null/sum = actual coproduct constructor. Do not collapse value-level `Some`/`None` usage into binding cardinality. | -| C2.3 | Type alias registration | `04_infer.dag:2053-2055` | Resolve alias target through TypeEnv before storing | -| C2.4 | ResolvedFuncSig construction | `04_sigs.dag:112-114` | Resolve return type through TypeEnv at sig construction time | -| C2.5 | Delete `resolve_scrutinee_type_node` from emit | 12 call sites in `05_emit_rust.dag` | After C2.1-C2.4, `.inferred` is structural; emit reads it directly | -| C2.6 | Delete `infer_record_lit` from emit | 1 call site | Record literal types fully resolved during infer | -| C2.7 | Simplify `type_needs_rc` | `05_emit_rust.dag:1584` | Remove TypeEnv parameter; works on resolved nodes directly | +| ID | Item | Status | Notes | +|----|------|--------|-------| +| C2.1 | Variant constructor locals | **Done** | `binding.resolved` / `item` instead of `leaf_node(name)`. Two sites: `04_infer.dag:2217`, `04_items.dag:130` | +| C2.2 | Optional Some/None locals | **Done** | Deleted explicit overrides; general coproduct mechanism handles it | +| C2.3 | Alias resolution in resolve_node_bounded | **Done** | Follow ONE level of `.inferred` for alias nodes. `04_resolve.dag:353` | +| C2.4 | ResolvedFuncSig construction | **Done** (via C2.3) | Return type already flows through resolve_node_bounded | +| C2.5 | Delete `resolve_scrutinee_type_node` from emit | **Remaining** | 12 call sites in `05_emit_rust.dag`. After C2.1-C2.4, `.inferred` is structurally authoritative — emit should read it directly. Per-site migration needed; each `resolve_scrutinee_type_node(env: scope.type_env, n: rt)` becomes just `rt`. See implementation notes below. | +| C2.6 | Delete `infer_record_lit` from emit | **Remaining** | 1 call site in `emit_record_lit_full`. Record literal types should be fully resolved during infer. | +| C2.7 | Simplify `type_needs_rc` | **Remaining** | `05_emit_rust.dag:1584`. Remove TypeEnv parameter; works on resolved nodes directly. Depends on C2.5. | + +**C2.5 implementation notes (for next worker):** + +The 12 `resolve_scrutinee_type_node` call sites in `05_emit_rust.dag` +follow this pattern: read `.inferred.node` (a type that might be an +alias), then call `resolve_scrutinee_type_node(env: scope.type_env, n: rt)` +to follow the alias to the structural target. + +After C2.1-C2.4, `.inferred.node` should already be the structural +target (aliases followed by resolve_node_bounded, variant locals +carry structural parent). Each call site becomes: + +``` +// Before: +let resolved = resolve_scrutinee_type_node(env: scope.type_env, n: rt) +// After: +let resolved = rt +``` + +Call sites (05_emit_rust.dag): L1604 (`type_needs_rc_seen`), L1615 +(`rust_map_value_type`), L1838/L1859 (`emit_typed_call_expr`), L2188 +(`MethodMap` optional check), L2332 (`MethodSortBy`), L2568/L2571 +(`is_already_optional` ExprVar), L2593/L2599/L2605/L2612 +(`is_already_optional` fallbacks). + +**Risk:** Each replacement must be semantically equivalent. Test after +each site or in small batches. The `is_already_optional` function +(lines 2560-2615) has the most complex logic and should be simplified +as a whole rather than site-by-site. + +After all 12 sites are migrated: remove `import v2.compiler.infer_lookup +{ resolve_scrutinee_type_node }` from `05_emit_rust.dag`. **Invariant alignment:** - No duplicate representations: `.inferred` is the single authority for @@ -2587,7 +2630,7 @@ its callees are already finalized. | ID | Item | Scope | Notes | |----|------|-------|-------| -| C4.1 | Replace 5-pass fixpoint with topo-order fold over condensed DAG | `04_service.dag`, `04_infer.dag:2552` | Modules already in topo order from resolve. For mutual dependencies (mutual recursion), use SCC condensation first, then fold over the condensed DAG. Single pass over the condensation suffices. | +| C4.1 | Early convergence for service expansion | **Done** | `04_service.dag`: exits early when total service count is stable between passes. Most codebases converge in 1-2 passes instead of always running 5. Full topo-order fold with SCC condensation deferred to when Layer 4 `DAG` types exist. | | C4.2 | Audit for other fixpoint iterations | Pipeline-wide | Are there other compensating mechanisms for missing topo-order composition? | **Invariant alignment:** Correctness by construction — topo order over @@ -2600,7 +2643,7 @@ handled by SCC condensation, not by iterating to fixpoint. | ID | Item | Scope | Notes | |----|------|-------|-------| -| C5.1 | Add `Instant`-based timing to stage0 `compile.rs` | ~30 lines | Per-phase timing: frontend, normalize, reconcile, complexity, ownership, emit | +| C5.1 | Add `Instant`-based timing to stage0 `compile.rs` | **Done** | Per-phase timing on all paths including early-return errors | | C5.2 | Measure after each fix lands | Ongoing | Requires stage0 regeneration to see .dag change impact | **Sequencing:** C5.1 first (baseline). C1 is mechanical, independent. From 6faf61456ec87ce565462a99c844b295aa1ab448 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Thu, 26 Mar 2026 13:04:11 -0400 Subject: [PATCH 09/10] . --- ROADMAP.md | 34 ++-- src/v2/02_parse.dag | 11 +- src/v2/04_emit_info.dag | 4 +- src/v2/04_infer.dag | 6 +- src/v2/04_service.dag | 3 + src/v2/05_emit.dag | 2 +- src/v2/05_emit_rust.dag | 53 ++--- src/v2/stage0/src/compile.rs | 19 -- src/v2/stage0/src/emit.rs | 2 +- src/v2/stage0/src/emit_rust.rs | 71 +++---- src/v2/stage0/src/generated_tests.rs | 290 +++++++++++++++------------ src/v2/stage0/src/infer.rs | 64 +++--- src/v2/stage0/src/infer_emit_info.rs | 3 +- src/v2/stage0/src/infer_items.rs | 2 +- src/v2/stage0/src/infer_resolve.rs | 34 +++- src/v2/stage0/src/infer_service.rs | 26 +++ src/v2/stage0/src/infer_types.rs | 28 --- src/v2/stage0/src/parse.rs | 9 +- src/v2/stage0/src/tokenize.rs | 16 +- src/v2/stage0/src/v2_core.rs | 28 +++ src/v2/tests/src/pipeline.rs | 7 + 21 files changed, 384 insertions(+), 328 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index ed449090b07..67e603fde19 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -107,8 +107,8 @@ type Bit = True | False // |[Bit]| = 2 type Byte = Tuple // |[Byte]| = 2^8 // Level 2: Named compositions (opaque namespaces) -// Int is List> — a namespace, not a compiler-known concept -// String is List — another namespace +// Int is Interpret — a namespace, not a compiler-known concept +// String is List — another namespace // Level 3: Algebraic structures // Optionality: T? is cardinality 0..1 on the binding site @@ -187,10 +187,11 @@ primitive, what are the structural constructors, and what is derived. Currently honest state: `Int`, `String`, `Bool`, `Float`, `Unit` are kernel primitives. The compiler treats them as special leaves via -`is_kernel_type`. The algebraic vision says they should be compositions -(`Int` = named namespace over `List>`), but the compiler does -not yet derive their properties from structure. This is an acknowledged -intermediate state, not a claimed-but-unimplemented derivation. +`is_kernel_type`. The algebraic vision says they should be declared +compositions (`Int` = named namespace over `Interpret`), +but the compiler does not yet derive their properties from structure. +This is an acknowledged intermediate state, not a +claimed-but-unimplemented derivation. The kernel will shrink as the algebraic model matures: - Phase 3+: kernel types get real `.dag` declarations @@ -256,6 +257,7 @@ Layer 1: Machine Vector (fixed-width bitvector) Byte = Vector<8, Bit> Word32 = Vector<32, Bit> + Word64 = Vector<64, Bit> Int32 = Interpret Note: fixed-width machine data uses Vector/bitvector, not List. @@ -321,10 +323,15 @@ TypedModule = Annotated ResolvedGraph = DAG ``` -The split between `NodePayload` (declared shape: name, children, -span, params) and `NodeFacts` (resolved/inferred/computed metadata) -lines up with the pipeline story: resolve adds resolved facts, infer -adds type facts, emit reads facts but does not re-resolve them. +The split is: `LabeledTree` owns the recursive tree carrier (`name`, +`children`), `NodePayload` owns the non-structural per-node fields +(`span`, `params`, `uses`, `transport`, `expr_data`, etc.), and +`NodeFacts` owns resolved/inferred/computed metadata. That lines up +with the pipeline story: resolve adds resolved facts, infer adds type +facts, emit reads facts but does not re-resolve them. `connective` and +`return_cardinality` remain explicit structural annotations in the +compiler's current `Node` model; Phase 5 work decides whether they +collapse into the carrier layer or separate binding/edge annotations. **The fact composition contract as `Annotated`:** @@ -2630,14 +2637,17 @@ its callees are already finalized. | ID | Item | Scope | Notes | |----|------|-------|-------| -| C4.1 | Early convergence for service expansion | **Done** | `04_service.dag`: exits early when total service count is stable between passes. Most codebases converge in 1-2 passes instead of always running 5. Full topo-order fold with SCC condensation deferred to when Layer 4 `DAG` types exist. | +| C4.1 | Early convergence for service expansion | **Done** | `04_service.dag`: exits early when total service count is stable between passes. For the current `ItemInfo` shape this is equivalent to registry stability because service sets only grow by deduped union. Most codebases converge in 1-2 passes instead of always running 5. Full topo-order fold with SCC condensation deferred to when Layer 4 `DAG` types exist. | | C4.2 | Audit for other fixpoint iterations | Pipeline-wide | Are there other compensating mechanisms for missing topo-order composition? | **Invariant alignment:** Correctness by construction — topo order over the condensed DAG guarantees completeness. Mutual dependencies are handled by SCC condensation, not by iterating to fixpoint. -**Gate:** No fixpoint iteration in the pipeline. +**Current gate:** service expansion no longer relies on a hardcoded +pass count; it halts when the registry stabilizes. + +**Future gate:** No fixpoint iteration in the pipeline. #### C5: Timing instrumentation diff --git a/src/v2/02_parse.dag b/src/v2/02_parse.dag index 42304bb77e3..acece3049e2 100644 --- a/src/v2/02_parse.dag +++ b/src/v2/02_parse.dag @@ -59,8 +59,6 @@ import v2.std.core { node_is_product, node_is_coproduct, with_required_cardinality } -import v2.compiler.infer_types { node_is_optional, callable_node } - // ========================================================================= // Parser state types // ========================================================================= @@ -1676,10 +1674,8 @@ fn parse_callable_type_expr(tokens: List, state: ParserState, start_span: let ret = parse_type_expr(tokens: tokens, state: r3.state) if has_err(err: ret.err) { return TypeResult { type_expr: dummy_te, state: ret.state, err: ret.err } } - let te = callable_node(func_params: params_result.params, ret: ret.type_expr) - // Set span from source location - let te_span = Node { name: te.name, span: start_span, children: te.children, connective: te.connective, params: te.params, inferred: te.inferred, return_cardinality: te.return_cardinality, uses: te.uses, body: te.body, transport: te.transport, properties: te.properties, type_annotation: te.type_annotation, config: te.config, is_self_recursive: te.is_self_recursive, has_non_tail_self_call: te.has_non_tail_self_call, expr_data: te.expr_data } - maybe_optional(tokens: tokens, state: ret.state, te: te_span, start_span: start_span) + let te = Node { name: "Callable", span: start_span, children: [], connective: none, params: params_result.params, inferred: Some { value: Resolved { node: ret.type_expr } }, return_cardinality: Required, uses: [], body: none, transport: none, properties: [], type_annotation: none, config: none, is_self_recursive: false, has_non_tail_self_call: false, expr_data: NoExprData } + maybe_optional(tokens: tokens, state: ret.state, te: te, start_span: start_span) } // Parse comma-separated type expressions for callable parameter types. @@ -2576,7 +2572,8 @@ fn is_container_name(name: String) -> Bool { // Extract a string name from a Node for use in property values. fn node_to_name_str(n: Node) -> String { let has_children = n.children |> count > 0 - if node_is_optional(n: n) { + let is_optional = n.return_cardinality == CardOptional + if is_optional { concat("Optional_", node_to_name_str(n: with_required_cardinality(n: n))) } else if n.name == "Map" { if n.children |> count > 1 { concat("Map_", node_to_name_str(n: last_child_or_self(n: n))) } diff --git a/src/v2/04_emit_info.dag b/src/v2/04_emit_info.dag index 10a206df61f..e9915bf6d79 100644 --- a/src/v2/04_emit_info.dag +++ b/src/v2/04_emit_info.dag @@ -41,6 +41,7 @@ type EmitGraphInfo { variant_to_enum: Map enum_variant_membership: Map field_type_names: Map + recursive_type_set: Map } type EmitInfoBuildState { @@ -59,7 +60,8 @@ fn empty_emit_graph_info() -> EmitGraphInfo { type_summaries: empty_map(), variant_to_enum: empty_map(), enum_variant_membership: empty_map(), - field_type_names: empty_map() + field_type_names: empty_map(), + recursive_type_set: empty_map() } } diff --git a/src/v2/04_infer.dag b/src/v2/04_infer.dag index 962a807e72a..b5eb4e2b424 100644 --- a/src/v2/04_infer.dag +++ b/src/v2/04_infer.dag @@ -2504,11 +2504,15 @@ fn build_emit_graph_info(modules: List) -> EmitGraphInfo { add_emit_item_summary(state: inner_state, item: item) ) ) + let all_recursive = modules |> fold(init: empty_map(), f: (acc, m) => + map_merge(acc, m.type_env.recursive_type_set) + ) EmitGraphInfo { type_summaries: built.type_summaries, variant_to_enum: built.variant_to_enum, enum_variant_membership: built.enum_variant_membership, - field_type_names: built.field_type_names + field_type_names: built.field_type_names, + recursive_type_set: all_recursive } } diff --git a/src/v2/04_service.dag b/src/v2/04_service.dag index 2c18af2f4eb..4742aef4918 100644 --- a/src/v2/04_service.dag +++ b/src/v2/04_service.dag @@ -173,6 +173,9 @@ fn total_service_count(registry: Map) -> Int { // C4.1: converge to fixpoint instead of hardcoded 5 passes. Each pass // propagates service dependencies from callees to callers. The loop // terminates when no new service names are discovered (total count stable). +// For the current ItemInfo shape this is equivalent to full-registry +// stability: service_names only changes by monotone deduped union, so the +// total count can increase but never decrease or redistribute. // Bounded by remaining_passes as safety net against pathological cases. fn expand_transitive_services(modules: List, registry: Map, remaining_passes: Int) -> Map { if remaining_passes <= 0 { registry } diff --git a/src/v2/05_emit.dag b/src/v2/05_emit.dag index 1214280956a..101ed761758 100644 --- a/src/v2/05_emit.dag +++ b/src/v2/05_emit.dag @@ -266,7 +266,7 @@ fn empty_emit_scope() -> InferScope { fn module_emit_scope(typed_module: TypedModule) -> InferScope { InferScope { - type_env: typed_module.type_env, + type_env: TypeEnv { bindings: empty_map(), recursive_types: [], recursive_type_set: empty_map() }, func_env: typed_module.func_env, locals: empty_map(), module_name: typed_module.module.name, diff --git a/src/v2/05_emit_rust.dag b/src/v2/05_emit_rust.dag index e9163107957..c7a45fe9e93 100644 --- a/src/v2/05_emit_rust.dag +++ b/src/v2/05_emit_rust.dag @@ -86,13 +86,9 @@ import v2.compiler.infer_items { import v2.compiler.infer_service { is_typed_service_call_receiver, extract_typed_service_name } -import v2.compiler.infer_lookup { - resolve_scrutinee_type_node -} import v2.compiler.infer { InferScope, build_params_scope, extend_scope, - infer_record_lit, build_emit_graph_info, expr_span } @@ -668,7 +664,7 @@ fn emit_non_empty_wrappers() -> String { fn emit_typed_item(item: Node, registry: Map, scope: InferScope, vtoe: Map, rc_types: Map, emit_info: EmitGraphInfo) -> String { let kind = classify_typed_item(item: item) if (kind == TypedItemTypeDef) { - emit_type_def_from_connective(item: item, recursive_types: scope.type_env.recursive_type_set, rc_types: rc_types) + emit_type_def_from_connective(item: item, recursive_types: emit_info.recursive_type_set, rc_types: rc_types) } else if (kind == TypedItemTypeAlias) { concat(rust_visibility_prefix(), "type ", item.name, " = ", emit_node_type_rc(n: rt_type(n: item), target: Rust, rc_types: rc_types), ";") } else if (kind == TypedItemTypeDecl) { @@ -1581,11 +1577,11 @@ fn emit_typed_field_access(base: Node, field: String, summary: FieldSummary?, re } } -fn type_needs_rc(env: TypeEnv, type_node: Node) -> Bool { - type_needs_rc_seen(env: env, type_node: type_node, seen: empty_map()) +fn type_needs_rc(type_node: Node) -> Bool { + type_needs_rc_seen(type_node: type_node, seen: empty_map()) } -fn type_needs_rc_seen(env: TypeEnv, type_node: Node, seen: Map) -> Bool { +fn type_needs_rc_seen(type_node: Node, seen: Map) -> Bool { let normed = normalize_access_type_node(n: type_node) let normed_kind = classify_type_structure(n: normed) if (normed_kind == TypeConj) { @@ -1596,23 +1592,23 @@ fn type_needs_rc_seen(env: TypeEnv, type_node: Node, seen: Map) -> let canonical = normed.name if normed.inferred != none { let next_seen = if canonical == "" { seen } else { map_insert(seen, canonical, true) } - type_needs_rc_seen(env: env, type_node: rt_type(n: normed), seen: next_seen) + type_needs_rc_seen(type_node: rt_type(n: normed), seen: next_seen) } else if canonical != "" && emit_map_has(m: seen, key: canonical) { false } else { let next_seen = if canonical == "" { seen } else { map_insert(seen, canonical, true) } - let resolved = resolve_scrutinee_type_node(env: env, n: normed) + let resolved = normed if node_has_structure(n: resolved) == false && resolved.inferred == none && resolved.name == normed.name && resolved.children |> count == 0 { false } else { - type_needs_rc_seen(env: env, type_node: resolved, seen: next_seen) + type_needs_rc_seen(type_node: resolved, seen: next_seen) } } } } fn rust_map_value_type(receiver_type: Node, scope: InferScope) -> Node? { - let resolved = resolve_scrutinee_type_node(env: scope.type_env, n: normalize_access_type_node(n: receiver_type)) + let resolved = normalize_access_type_node(n: receiver_type) let map_type = normalize_access_type_node(n: resolved) if node_is_map(n: map_type) { match map_type.children |> skip(1) |> first { @@ -1639,7 +1635,7 @@ fn rust_lookup_receiver_needs_rc_wrap(receiver: Node, scope: InferScope) -> Bool match receiver.inferred { Some { value: Resolved { node: receiver_type } } => match rust_map_value_type(receiver_type: receiver_type, scope: scope) { - Some { value: value_type } => type_needs_rc(env: scope.type_env, type_node: value_type) + Some { value: value_type } => type_needs_rc(type_node: value_type) None => false } _ => false @@ -1835,7 +1831,7 @@ fn emit_typed_call_expr(func: String, args: List, inferred: InferredNo let call_str = if func == "empty_map" { match inferred { Some { value: Resolved { node: ret_type } } => - let resolved_ret = resolve_scrutinee_type_node(env: scope.type_env, n: ret_type) + let resolved_ret = ret_type let type_str = emit_node_type_rc(n: resolved_ret, target: Rust, rc_types: rc_types) if type_str != "" && type_str != "Dynamic" { concat("<", type_str, ">::new()") @@ -1856,9 +1852,9 @@ fn emit_typed_call_expr(func: String, args: List, inferred: InferredNo else { match inferred { Some { value: Resolved { node: ret_type } } => - let resolved_ret = resolve_scrutinee_type_node(env: scope.type_env, n: ret_type) + let resolved_ret = ret_type if node_is_optional(n: resolved_ret) { - type_needs_rc(env: scope.type_env, type_node: with_required_cardinality(n: resolved_ret)) + type_needs_rc(type_node: with_required_cardinality(n: resolved_ret)) } else { false } _ => false } @@ -2185,7 +2181,7 @@ fn emit_intrinsic_typed_method_call(intrinsic: IntrinsicMethod, fold_accumulator // RC4: If the receiver is Optional, emit .map(|p| body) to preserve Option type. // Otherwise use the standard for-loop collect pattern for lists. let recv_is_optional = match receiver.inferred { - Some { value: Resolved { node: rt } } => node_is_optional(n: resolve_scrutinee_type_node(env: scope.type_env, n: rt)) + Some { value: Resolved { node: rt } } => node_is_optional(n: rt) _ => false } if recv_is_optional { @@ -2329,7 +2325,7 @@ fn emit_intrinsic_typed_method_call(intrinsic: IntrinsicMethod, fold_accumulator // Annotate sort_by closure params with the element type to avoid E0282. let elem_type_str = match receiver.inferred { Some { value: Resolved { node: rt } } => - let resolved = resolve_scrutinee_type_node(env: scope.type_env, n: rt) + let resolved = rt let elem = for_each_element_type_node(n: resolved) // Dynamic audit: correct guard — if sort_by element type is Dynamic, // fall back to "_" so Rust infers the closure param type. @@ -2529,15 +2525,6 @@ fn emit_typed_let(name: String, value: Node, body: Node?, registry: Map, span: SourceSpan, registry: Map, scope: InferScope, rc_types: Map, emit_info: EmitGraphInfo) -> String { - let inferred = infer_record_lit(type_name: type_name, field_inits: fields, span: span, scope: scope) - match inferred.typed.expr_data { - ExprRecordLit { type_name: tn, fields: fs, parent_enum: parent_enum } => - emit_typed_record_lit(type_name: tn, fields: fs, parent_enum: parent_enum, resolved_type: rt_type(n: inferred.typed), registry: registry, scope: scope, depth: 0, vtoe: empty_map(), rc_types: rc_types, emit_info: emit_info) - _ => - "compile_error!(\"internal error: infer_record_lit did not produce ExprRecordLit\")" - } -} fn is_optional_struct_field(emit_info: EmitGraphInfo, struct_name: String, field_name: String) -> Bool { match lookup_emit_type_summary(emit_info: emit_info, type_name: struct_name) { @@ -2565,10 +2552,10 @@ fn is_already_optional(texpr: Node, emit_info: EmitGraphInfo, scope: InferScope) // then resolve to the definition. resolve_scrutinee_type_node can // lose use-site cardinality when resolving to the type definition. match texpr.inferred { - Some { value: Resolved { node: rt } } => node_is_optional(n: rt) || node_is_optional(n: resolve_scrutinee_type_node(env: scope.type_env, n: rt)) + Some { value: Resolved { node: rt } } => node_is_optional(n: rt) _ => match map_get(scope.locals, n) { - Some { value: binding } => node_is_optional(n: binding.resolved) || node_is_optional(n: resolve_scrutinee_type_node(env: scope.type_env, n: binding.resolved)) + Some { value: binding } => node_is_optional(n: binding.resolved) None => false } } @@ -2590,26 +2577,26 @@ fn is_already_optional(texpr: Node, emit_info: EmitGraphInfo, scope: InferScope) else { match b.inferred { Some { value: Resolved { node: base_type } } => - let resolved_base = resolve_scrutinee_type_node(env: scope.type_env, n: base_type) + let resolved_base = base_type if is_optional_struct_field(emit_info: emit_info, struct_name: resolved_base.name, field_name: f) { true } else { // Fallback: struct may not be in type_summaries; check expression return type match texpr.inferred { - Some { value: Resolved { node: rt } } => node_is_optional(n: resolve_scrutinee_type_node(env: scope.type_env, n: rt)) + Some { value: Resolved { node: rt } } => node_is_optional(n: rt) _ => false } } _ => match texpr.inferred { - Some { value: Resolved { node: rt } } => node_is_optional(n: resolve_scrutinee_type_node(env: scope.type_env, n: rt)) + Some { value: Resolved { node: rt } } => node_is_optional(n: rt) _ => false } } } _ => match texpr.inferred { - Some { value: Resolved { node: rt } } => node_is_optional(n: rt) || node_is_optional(n: resolve_scrutinee_type_node(env: scope.type_env, n: rt)) + Some { value: Resolved { node: rt } } => node_is_optional(n: rt) _ => false } } diff --git a/src/v2/stage0/src/compile.rs b/src/v2/stage0/src/compile.rs index 7b232e85eb4..ddb93d9007c 100644 --- a/src/v2/stage0/src/compile.rs +++ b/src/v2/stage0/src/compile.rs @@ -15,7 +15,6 @@ use crate::artifact::*; use crate::v2_rt; use std::collections::HashMap; use std::rc::Rc; -use std::time::Instant; #[derive(Debug, Clone, PartialEq, Eq, Default)] pub struct SourceFile { @@ -1056,13 +1055,9 @@ pub fn resolve_sources(sources: Rc>>) -> Rc { } pub fn compile_sources(sources: Rc>>, target: RenderTarget) -> Rc { - let t_total = Instant::now(); - let t0 = Instant::now(); let frontend = front_end_sources(sources.clone()); - eprintln!(" phase: frontend {:>10.1}ms", t0.elapsed().as_secs_f64() * 1000.0); match frontend.graph.as_ref().map(|__rc| __rc.as_ref()) { None => { - eprintln!(" phase: total {:>10.1}ms (frontend error)", t_total.elapsed().as_secs_f64() * 1000.0); Rc::new(PipelineResult { files: Rc::new(Vec::new()), diagnostics: frontend.diagnostics.clone(), complexity: empty_complexity_report(), ownership: Rc::new(Vec::new()), artifact_plan: empty_artifact_plan() }) } Some(graph) => { @@ -1082,12 +1077,9 @@ pub fn compile_sources(sources: Rc>>, target: RenderTarget) - let __len_2 = resolve_errors.clone().len(); __len_2 as i64 }) > 0_i64 { - eprintln!(" phase: total {:>10.1}ms (resolve error)", t_total.elapsed().as_secs_f64() * 1000.0); return Rc::new(PipelineResult { files: Rc::new(Vec::new()), diagnostics: frontend.diagnostics.clone(), complexity: empty_complexity_report(), ownership: Rc::new(Vec::new()), artifact_plan: empty_artifact_plan() }); }; - let t1 = Instant::now(); let norm = normalize_graph(graph.clone()); - eprintln!(" phase: normalize {:>10.1}ms", t1.elapsed().as_secs_f64() * 1000.0); let norm_diags = norm.diagnostics.clone(); let norm_errors = { let mut __filtered_3 = Vec::new(); @@ -1102,17 +1094,12 @@ pub fn compile_sources(sources: Rc>>, target: RenderTarget) - let __len_5 = norm_errors.clone().len(); __len_5 as i64 }) > 0_i64 { - eprintln!(" phase: total {:>10.1}ms (normalize error)", t_total.elapsed().as_secs_f64() * 1000.0); return Rc::new(PipelineResult { files: Rc::new(Vec::new()), diagnostics: v2_rt::concat(frontend.diagnostics.clone(), norm_diags.clone()), complexity: empty_complexity_report(), ownership: Rc::new(Vec::new()), artifact_plan: empty_artifact_plan() }); }; - let t2 = Instant::now(); let typed = reconcile(norm.graph.clone()); - eprintln!(" phase: reconcile {:>10.1}ms", t2.elapsed().as_secs_f64() * 1000.0); let typed_diags = typed.diagnostics.clone(); - let t3 = Instant::now(); let func_entries = extract_func_entries(typed.clone()); let complexity = build_complexity_report(func_entries.clone()); - eprintln!(" phase: complexity {:>10.1}ms", t3.elapsed().as_secs_f64() * 1000.0); let typecheck_errors = { let mut __filtered_6 = Vec::new(); for __elem_7 in typed_diags.iter().cloned() { @@ -1126,12 +1113,9 @@ pub fn compile_sources(sources: Rc>>, target: RenderTarget) - let __len_8 = typecheck_errors.clone().len(); __len_8 as i64 }) > 0_i64 { - eprintln!(" phase: total {:>10.1}ms (typecheck error)", t_total.elapsed().as_secs_f64() * 1000.0); return Rc::new(PipelineResult { files: Rc::new(Vec::new()), diagnostics: v2_rt::concat(v2_rt::concat(frontend.diagnostics.clone(), norm_diags.clone()), typed_diags.clone()), complexity: complexity.clone(), ownership: Rc::new(Vec::new()), artifact_plan: empty_artifact_plan() }); }; - let t4 = Instant::now(); let ownership = extract_ownership_proofs(typed.clone()); - eprintln!(" phase: ownership {:>10.1}ms", t4.elapsed().as_secs_f64() * 1000.0); let ownership_diags = ownership_diagnostics(ownership.clone()); let artifact_plan = default_artifact_plan({ let mut __mapped_11 = Vec::new(); @@ -1140,9 +1124,7 @@ pub fn compile_sources(sources: Rc>>, target: RenderTarget) - } Rc::new(__mapped_11) }, target); - let t5 = Instant::now(); let emit_result = emit_from_artifact_plan(typed.clone(), artifact_plan.clone()); - eprintln!(" phase: emit {:>10.1}ms", t5.elapsed().as_secs_f64() * 1000.0); let emit_files = emit_result.files.clone(); let emit_diags = emit_result.diagnostics.clone(); let emit_errors = { @@ -1162,7 +1144,6 @@ pub fn compile_sources(sources: Rc>>, target: RenderTarget) - } else { emit_files.clone() }; - eprintln!(" phase: total {:>10.1}ms", t_total.elapsed().as_secs_f64() * 1000.0); Rc::new(PipelineResult { files: final_files.clone(), diagnostics: v2_rt::concat(v2_rt::concat(v2_rt::concat(v2_rt::concat(frontend.diagnostics.clone(), norm_diags.clone()), typed_diags.clone()), ownership_diags.clone()), emit_diags.clone()), complexity: complexity.clone(), ownership: ownership.clone(), artifact_plan: artifact_plan.clone() }) } } diff --git a/src/v2/stage0/src/emit.rs b/src/v2/stage0/src/emit.rs index ad4c5277a52..98711add4c5 100644 --- a/src/v2/stage0/src/emit.rs +++ b/src/v2/stage0/src/emit.rs @@ -453,7 +453,7 @@ pub fn empty_emit_scope() -> Rc { } pub fn module_emit_scope(typed_module: Rc) -> Rc { - Rc::new(InferScope { type_env: typed_module.type_env.clone(), func_env: typed_module.func_env.clone(), locals: Rc::new(std::collections::HashMap::new()), module_name: typed_module.module.name.clone(), service_registry: Rc::new(std::collections::HashMap::new()), item_registry: typed_module.item_registry.clone() }) + Rc::new(InferScope { type_env: Rc::new(TypeEnv { bindings: Rc::new(std::collections::HashMap::new()), recursive_types: Rc::new(Vec::new()), recursive_type_set: Rc::new(std::collections::HashMap::new()) }), func_env: typed_module.func_env.clone(), locals: Rc::new(std::collections::HashMap::new()), module_name: typed_module.module.name.clone(), service_registry: Rc::new(std::collections::HashMap::new()), item_registry: typed_module.item_registry.clone() }) } pub fn scope_after_expr(texpr: Rc, scope: Rc) -> Rc { diff --git a/src/v2/stage0/src/emit_rust.rs b/src/v2/stage0/src/emit_rust.rs index 6d017d8b2ae..6b500b87684 100644 --- a/src/v2/stage0/src/emit_rust.rs +++ b/src/v2/stage0/src/emit_rust.rs @@ -8,7 +8,6 @@ use crate::infer_types::*; use crate::infer_sigs::*; use crate::infer_items::*; use crate::infer_service::*; -use crate::infer_lookup::*; use crate::infer::*; use crate::infer_emit_info::*; use crate::emit::*; @@ -1037,7 +1036,7 @@ pub fn emit_non_empty_wrappers() -> String { pub fn emit_typed_item(item: Rc, registry: Rc>>, scope: Rc, vtoe: Rc>, rc_types: Rc>, emit_info: Rc) -> String { let kind = classify_typed_item(item.clone()); if kind.clone() == TypedItemKind::TypedItemTypeDef { - emit_type_def_from_connective(item.clone(), scope.type_env.recursive_type_set.clone(), rc_types.clone()) + emit_type_def_from_connective(item.clone(), emit_info.recursive_type_set.clone(), rc_types.clone()) } else { if kind.clone() == TypedItemKind::TypedItemTypeAlias { v2_rt::concat(v2_rt::concat(v2_rt::concat(v2_rt::concat(v2_rt::concat(rust_visibility_prefix(), "type ".to_string()), item.name.clone()), " = ".to_string()), emit_node_type_rc(rt_type(item.clone()), RenderTarget::Rust, rc_types.clone())), ";".to_string()) @@ -2679,17 +2678,15 @@ pub fn emit_typed_field_access(base: Rc, field: &str, summary: Option, type_node: Rc) -> bool { - type_needs_rc_seen(env.clone(), type_node.clone(), Rc::new(std::collections::HashMap::new())) +pub fn type_needs_rc(type_node: Rc) -> bool { + type_needs_rc_seen(type_node.clone(), Rc::new(std::collections::HashMap::new())) } -pub fn type_needs_rc_seen(env: Rc, type_node: Rc, seen: Rc>) -> bool { +pub fn type_needs_rc_seen(type_node: Rc, seen: Rc>) -> bool { stacker::maybe_grow(512 * 1024, 2 * 1024 * 1024, || { - let mut __tco_p_env = env; let mut __tco_p_type_node = type_node; let mut __tco_p_seen = seen; loop { - let env = __tco_p_env; let type_node = __tco_p_type_node; let seen = __tco_p_seen; let normed = normalize_access_type_node(type_node); @@ -2725,12 +2722,10 @@ pub fn type_needs_rc_seen(env: Rc, type_node: Rc, seen: Rc, type_node: Rc, seen: Rc, type_node: Rc, seen: Rc, type_node: Rc, seen: Rc, scope: Rc) -> Option> { - let resolved = resolve_scrutinee_type_node(scope.type_env.clone(), normalize_access_type_node(receiver_type.clone())); + let resolved = normalize_access_type_node(receiver_type.clone()); let map_type = normalize_access_type_node(resolved.clone()); if node_is_map(map_type.clone()) { match map_type.children.clone().get((1_i64) as usize).cloned() { @@ -2814,7 +2807,7 @@ pub fn rust_lookup_receiver_needs_rc_wrap(receiver: Rc, scope: Rc { match rust_map_value_type(receiver_type.clone(), scope.clone()) { Some(value_type) => { - type_needs_rc(scope.type_env.clone(), value_type.clone()) + type_needs_rc(value_type.clone()) } None => { false @@ -3098,7 +3091,7 @@ pub fn emit_typed_call_expr(func: &str, args: Rc>>, inferred: O match inferred.as_ref().map(|__rc| __rc.as_ref()) { Some(InferredNode::Resolved { node: ret_type, .. }) => { { - let resolved_ret = resolve_scrutinee_type_node(scope.type_env.clone(), ret_type.clone()); + let resolved_ret = ret_type.clone(); let type_str = emit_node_type_rc(resolved_ret.clone(), RenderTarget::Rust, rc_types.clone()); if (type_str.clone() != "") && (type_str.clone() != "Dynamic") { v2_rt::concat(v2_rt::concat("<".to_string(), type_str.clone()), ">::new()".to_string()) @@ -3129,9 +3122,9 @@ pub fn emit_typed_call_expr(func: &str, args: Rc>>, inferred: O match inferred.as_ref().map(|__rc| __rc.as_ref()) { Some(InferredNode::Resolved { node: ret_type, .. }) => { { - let resolved_ret = resolve_scrutinee_type_node(scope.type_env.clone(), ret_type.clone()); + let resolved_ret = ret_type.clone(); if node_is_optional(resolved_ret.clone()) { - type_needs_rc(scope.type_env.clone(), with_required_cardinality(resolved_ret.clone())) + type_needs_rc(with_required_cardinality(resolved_ret.clone())) } else { false } @@ -3813,7 +3806,7 @@ pub fn emit_intrinsic_typed_method_call(intrinsic: IntrinsicMethod, fold_accumul { let recv_is_optional = match receiver.inferred.as_ref().map(|__rc| __rc.as_ref()) { Some(InferredNode::Resolved { node: rt, .. }) => { - node_is_optional(resolve_scrutinee_type_node(scope.type_env.clone(), rt.clone())) + node_is_optional(rt.clone()) } _ => { false @@ -4093,7 +4086,7 @@ pub fn emit_intrinsic_typed_method_call(intrinsic: IntrinsicMethod, fold_accumul let elem_type_str = match receiver.inferred.as_ref().map(|__rc| __rc.as_ref()) { Some(InferredNode::Resolved { node: rt, .. }) => { { - let resolved = resolve_scrutinee_type_node(scope.type_env.clone(), rt.clone()); + let resolved = rt.clone(); let elem = for_each_element_type_node(resolved.clone()); if (elem.name.clone() != "") && (elem.name.clone() != "Dynamic") { emit_node_type_rc(elem.clone(), RenderTarget::Rust, rc_types.clone()) @@ -4475,18 +4468,6 @@ pub fn emit_typed_let(name: &str, value: Rc, body: Option>, regis }) } -pub fn emit_record_lit_full(type_name: Option, fields: Rc>>, span: SourceSpan, registry: Rc>>, scope: Rc, rc_types: Rc>, emit_info: Rc) -> String { - let inferred = infer_record_lit(type_name, fields.clone(), span, scope.clone()); - match inferred.typed.expr_data.as_ref() { - ExprData::ExprRecordLit { type_name: tn, fields: fs, parent_enum, .. } => { - emit_typed_record_lit(tn.clone(), fs.clone(), parent_enum.clone(), rt_type(inferred.typed.clone()), registry.clone(), scope.clone(), 0_i64, Rc::new(std::collections::HashMap::new()), rc_types.clone(), emit_info.clone()) - } - _ => { - "compile_error!(\"internal error: infer_record_lit did not produce ExprRecordLit\")".to_string() - } -} -} - pub fn is_optional_struct_field(emit_info: Rc, struct_name: &str, field_name: &str) -> bool { match lookup_emit_type_summary(emit_info.clone(), &struct_name) { Some(summary) => { @@ -4530,12 +4511,12 @@ pub fn is_already_optional(texpr: Rc, emit_info: Rc, scope: } else { match texpr.inferred.as_ref().map(|__rc| __rc.as_ref()) { Some(InferredNode::Resolved { node: rt, .. }) => { - node_is_optional(rt.clone()) || node_is_optional(resolve_scrutinee_type_node(scope.type_env.clone(), rt.clone())) + node_is_optional(rt.clone()) } _ => { match scope.locals.clone().get(&n.clone()).cloned() { Some(binding) => { - node_is_optional(binding.resolved.clone()) || node_is_optional(resolve_scrutinee_type_node(scope.type_env.clone(), binding.resolved.clone())) + node_is_optional(binding.resolved.clone()) } None => { false @@ -4579,13 +4560,13 @@ pub fn is_already_optional(texpr: Rc, emit_info: Rc, scope: match b.inferred.as_ref().map(|__rc| __rc.as_ref()) { Some(InferredNode::Resolved { node: base_type, .. }) => { { - let resolved_base = resolve_scrutinee_type_node(scope.type_env.clone(), base_type.clone()); + let resolved_base = base_type.clone(); if is_optional_struct_field(emit_info.clone(), &resolved_base.name, &f) { true } else { match texpr.inferred.as_ref().map(|__rc| __rc.as_ref()) { Some(InferredNode::Resolved { node: rt, .. }) => { - node_is_optional(resolve_scrutinee_type_node(scope.type_env.clone(), rt.clone())) + node_is_optional(rt.clone()) } _ => { false @@ -4597,7 +4578,7 @@ pub fn is_already_optional(texpr: Rc, emit_info: Rc, scope: _ => { match texpr.inferred.as_ref().map(|__rc| __rc.as_ref()) { Some(InferredNode::Resolved { node: rt, .. }) => { - node_is_optional(resolve_scrutinee_type_node(scope.type_env.clone(), rt.clone())) + node_is_optional(rt.clone()) } _ => { false @@ -4611,7 +4592,7 @@ pub fn is_already_optional(texpr: Rc, emit_info: Rc, scope: _ => { match texpr.inferred.as_ref().map(|__rc| __rc.as_ref()) { Some(InferredNode::Resolved { node: rt, .. }) => { - node_is_optional(rt.clone()) || node_is_optional(resolve_scrutinee_type_node(scope.type_env.clone(), rt.clone())) + node_is_optional(rt.clone()) } _ => { false diff --git a/src/v2/stage0/src/generated_tests.rs b/src/v2/stage0/src/generated_tests.rs index a54872fcf77..fb627f1cea5 100644 --- a/src/v2/stage0/src/generated_tests.rs +++ b/src/v2/stage0/src/generated_tests.rs @@ -2795,6 +2795,42 @@ fn is_module_node(n: Node) -> Bool { fn is_token_node(n: Node) -> Bool { n.properties |> any(p => p.name == "is_token") } + +// ========================================================================= +// Node vocabulary (structural constructors and predicates) +// +// These operate on Node structural fields (connective, cardinality) and +// need no environment. They are the shared vocabulary for all pipeline +// stages — not stage-specific concepts. Moved from 04_types.dag (C1). +// ========================================================================= + +fn leaf_node(name: String) -> Node { + Node { name: name, span: SourceSpan { start: 0, end: 0 }, children: [], connective: none, params: [], inferred: none, return_cardinality: Required, uses: [], body: none, transport: none, properties: [], type_annotation: none, config: none, is_self_recursive: false, has_non_tail_self_call: false, expr_data: NoExprData } +} + +fn no_span() -> SourceSpan { + SourceSpan { start: 0, end: 0 } +} + +fn with_optional_cardinality(n: Node) -> Node { + Node { name: n.name, span: n.span, children: n.children, connective: n.connective, params: n.params, inferred: n.inferred, return_cardinality: CardOptional, uses: n.uses, body: n.body, transport: n.transport, properties: n.properties, type_annotation: n.type_annotation, config: n.config, is_self_recursive: n.is_self_recursive, has_non_tail_self_call: n.has_non_tail_self_call, expr_data: n.expr_data } +} + +fn with_required_cardinality(n: Node) -> Node { + Node { name: n.name, span: n.span, children: n.children, connective: n.connective, params: n.params, inferred: n.inferred, return_cardinality: Required, uses: n.uses, body: n.body, transport: n.transport, properties: n.properties, type_annotation: n.type_annotation, config: n.config, is_self_recursive: n.is_self_recursive, has_non_tail_self_call: n.has_non_tail_self_call, expr_data: n.expr_data } +} + +fn node_is_product(n: Node) -> Bool { + n.connective != none && n.connective == Some { value: Conj } +} + +fn node_is_coproduct(n: Node) -> Bool { + n.connective != none && n.connective == Some { value: Disj } +} + +fn node_has_structure(n: Node) -> Bool { + node_is_product(n: n) || node_is_coproduct(n: n) +} "##; const SRC_V2_01_TOKENIZE_DAG_SOURCE: &str = r##"// v2 tokenizer -- pure function from source string to token list. // @@ -3361,11 +3397,10 @@ import v2.std.core { Ident, StrBegin, StrMid, StrEnd, Newline, Eof, Unknown, Diagnostic, Error, - SourceSpan + SourceSpan, + node_is_product, node_is_coproduct, with_required_cardinality } -import v2.compiler.infer_types { node_is_optional, node_is_product, node_is_coproduct, with_required_cardinality, callable_node } - // ========================================================================= // Parser state types // ========================================================================= @@ -4981,10 +5016,8 @@ fn parse_callable_type_expr(tokens: List, state: ParserState, start_span: let ret = parse_type_expr(tokens: tokens, state: r3.state) if has_err(err: ret.err) { return TypeResult { type_expr: dummy_te, state: ret.state, err: ret.err } } - let te = callable_node(func_params: params_result.params, ret: ret.type_expr) - // Set span from source location - let te_span = Node { name: te.name, span: start_span, children: te.children, connective: te.connective, params: te.params, inferred: te.inferred, return_cardinality: te.return_cardinality, uses: te.uses, body: te.body, transport: te.transport, properties: te.properties, type_annotation: te.type_annotation, config: te.config, is_self_recursive: te.is_self_recursive, has_non_tail_self_call: te.has_non_tail_self_call, expr_data: te.expr_data } - maybe_optional(tokens: tokens, state: ret.state, te: te_span, start_span: start_span) + let te = Node { name: "Callable", span: start_span, children: [], connective: none, params: params_result.params, inferred: Some { value: Resolved { node: ret.type_expr } }, return_cardinality: Required, uses: [], body: none, transport: none, properties: [], type_annotation: none, config: none, is_self_recursive: false, has_non_tail_self_call: false, expr_data: NoExprData } + maybe_optional(tokens: tokens, state: ret.state, te: te, start_span: start_span) } // Parse comma-separated type expressions for callable parameter types. @@ -5881,7 +5914,8 @@ fn is_container_name(name: String) -> Bool { // Extract a string name from a Node for use in property values. fn node_to_name_str(n: Node) -> String { let has_children = n.children |> count > 0 - if node_is_optional(n: n) { + let is_optional = n.return_cardinality == CardOptional + if is_optional { concat("Optional_", node_to_name_str(n: with_required_cardinality(n: n))) } else if n.name == "Map" { if n.children |> count > 1 { concat("Map_", node_to_name_str(n: last_child_or_self(n: n))) } @@ -8422,15 +8456,14 @@ import std.types { SourceSpan } import v2.std.core { Node, InferredNode, Resolved, CompilerError, - Diagnostic, Severity, Error + Diagnostic, Severity, Error, + leaf_node, with_optional_cardinality, node_has_structure } import v2.compiler.infer_types { - leaf_node, with_optional_cardinality, node_is_map, normalize_access_type_node, node_type_equals, - is_int_type_node, is_string_type_node, - node_has_structure + is_int_type_node, is_string_type_node } // ========================================================================= @@ -8636,11 +8669,11 @@ import v2.std.core { InferredNode, Resolved, FieldAccessStyle, StoredField, EnumAccessor, TupleFirst, TupleSecond, FieldValueShape, PlainValue, OptionalValue, - FieldSummary + FieldSummary, + node_has_structure, node_is_product, with_required_cardinality } import v2.compiler.infer_types { - node_has_structure, node_is_product, node_is_optional, - with_required_cardinality, + node_is_optional, normalize_type_name, normalize_access_type_node, rt_type, child_inferred_or_name, node_type_equals } @@ -8662,6 +8695,7 @@ type EmitGraphInfo { variant_to_enum: Map enum_variant_membership: Map field_type_names: Map + recursive_type_set: Map } type EmitInfoBuildState { @@ -8680,7 +8714,8 @@ fn empty_emit_graph_info() -> EmitGraphInfo { type_summaries: empty_map(), variant_to_enum: empty_map(), enum_variant_membership: empty_map(), - field_type_names: empty_map() + field_type_names: empty_map(), + recursive_type_set: empty_map() } } @@ -9028,19 +9063,19 @@ import v2.std.core { UnaryOpKind, Not, Neg, MatchArm, MatchPattern, FieldBinding, FieldInit, NamedArg, StringPart, make_transport_node, local_transport_node, - Text, Interpolation + Text, Interpolation, + leaf_node, with_optional_cardinality, with_required_cardinality, + node_is_product, node_is_coproduct, node_has_structure, no_span } import v2.compiler.resolve { ModuleGraph, ResolvedModule, ResolvedImport } import v2.compiler.infer_types { child_inferred_or_name, - leaf_node, with_optional_cardinality, with_required_cardinality, container_node, map_node, bare_map_node, tuple_node, callable_node, callable_inferred, error_type_node, node_is_container, node_is_optional, node_is_map, node_is_leaf, node_is_named_ref, - node_is_product, node_is_coproduct, node_has_structure, normalize_access_type_node, normalize_type_name, node_type_shape, node_type_compatible, node_type_equals, prefer_specific_type, node_type_deps, @@ -9048,7 +9083,7 @@ import v2.compiler.infer_types { method_receiver_element_node, infer_literal_node, infer_binop_type_node, extract_optional_inner_node, for_each_element_type_node, - rt_type, no_span, + rt_type, emit_map_has } import v2.compiler.infer_method { @@ -11170,12 +11205,17 @@ fn build_module_context(contributions: List, item_registry: empty_map(), diag_chunks: [] ) + // C2.1: variant constructor locals carry the structural parent enum node, + // not leaf_node(name). This means .inferred on variant expressions will be + // structurally authoritative — emit can read it directly without re-resolving + // through TypeEnv. binding.resolved.name still returns the enum's name for + // lookup_variant_parent_enum and record lit inference. let imported_variant_locals = fold(map_values(env.bindings), init: empty_map(), f: (acc, binding) => if node_is_coproduct(n: binding.resolved) { fold(binding.resolved.children, init: acc, f: (vacc, child) => map_insert(vacc, child.name, TypeBinding { name: child.name, - resolved: leaf_node(name: binding.name) + resolved: binding.resolved }) ) } else { acc } @@ -11195,15 +11235,11 @@ fn build_module_context(contributions: List, items: local.resolved_items, module_name: module_name ) - // Variant locals come from env_variant_locals (computed once from env.bindings, - // which already includes all local + imported disjunction types). - let optional_locals = map_insert(env_variant_locals, "Some", TypeBinding { - name: "Some", resolved: leaf_node(name: "Optional") - }) - let optional_locals = map_insert(optional_locals, "None", TypeBinding { - name: "None", resolved: leaf_node(name: "Optional") - }) - let all_locals = fold(map_values(merged_scope.svc_locals), init: optional_locals, f: (acc, binding) => + // C2.2: Optional Some/None now handled by the general variant constructor + // mechanism (C2.1). kernel_optional is a Disj node in env.bindings, so + // imported_variant_locals already registers Some and None with the structural + // parent node. No need for explicit leaf_node(name: "Optional") override. + let all_locals = fold(map_values(merged_scope.svc_locals), init: env_variant_locals, f: (acc, binding) => map_insert(acc, binding.name, binding) ) ModuleContext { @@ -11459,11 +11495,15 @@ fn build_emit_graph_info(modules: List) -> EmitGraphInfo { add_emit_item_summary(state: inner_state, item: item) ) ) + let all_recursive = modules |> fold(init: empty_map(), f: (acc, m) => + map_merge(acc, m.type_env.recursive_type_set) + ) EmitGraphInfo { type_summaries: built.type_summaries, variant_to_enum: built.variant_to_enum, enum_variant_membership: built.enum_variant_membership, - field_type_names: built.field_type_names + field_type_names: built.field_type_names, + recursive_type_set: all_recursive } } @@ -11551,11 +11591,11 @@ import v2.std.core { InferredNode, Resolved, CompilerError, Cardinality, Required, Diagnostic, - expr_has_self_call, expr_has_non_tail_self_call + expr_has_self_call, expr_has_non_tail_self_call, + leaf_node, node_has_structure, node_is_product, node_is_coproduct } import v2.compiler.infer_types { - leaf_node, rt_type, - node_has_structure, node_is_product, node_is_coproduct + rt_type } import v2.compiler.infer_env { TypeEnv, TypeBinding @@ -11659,13 +11699,14 @@ fn item_kind(item: Node) -> ItemKind { kind } +// C2.1: variant locals carry the structural parent node, not leaf_node(name). fn variant_locals_from_items(items: List, init: Map) -> Map { fold(items, init: init, f: (acc, item) => if node_is_coproduct(n: item) { fold(item.children, init: acc, f: (vacc, child) => map_insert(vacc, child.name, TypeBinding { name: child.name, - resolved: leaf_node(name: item.name) + resolved: item }) ) } else { acc } @@ -11693,14 +11734,14 @@ import v2.std.core { RuntimeBridgeMethod, FieldAccessStyle, OptionalUnwrap, FieldValueShape, PlainValue, OptionalValue, - FieldSummary + FieldSummary, + leaf_node, with_optional_cardinality, with_required_cardinality, + node_is_product, node_is_coproduct, node_has_structure } import v2.compiler.infer_types { child_inferred_or_name, - leaf_node, with_optional_cardinality, with_required_cardinality, error_type_node, node_is_optional, node_is_map, - node_is_product, node_is_coproduct, node_has_structure, normalize_access_type_node, normalize_type_name, rt_type, rt_node, emit_map_has @@ -11949,10 +11990,11 @@ import v2.std.core { BridgeMapContainsKey, BridgeCharAt, BridgeStringAt, BridgeStringLength, BridgeLength, BridgeStartsWith, BridgeEndsWith, BridgeToString, BridgeTrim, BridgeToLower, BridgeToUpper, BridgeReplace, BridgeSubstring, BridgeToInt, BridgeEmptyMap, - BridgeContains, BridgeReverse, BridgeLookup + BridgeContains, BridgeReverse, BridgeLookup, + leaf_node, with_optional_cardinality } import v2.compiler.infer_types { - leaf_node, with_optional_cardinality, container_node, tuple_node, error_type_node, + container_node, tuple_node, error_type_node, bare_map_node, method_receiver_element_node } @@ -12172,14 +12214,14 @@ import v2.std.core { Cardinality, Required, Diagnostic, Severity, Error, ErrorCategory, FieldNotFound, VariantNotFound, InvalidOperation, - MatchArm, MatchPattern + MatchArm, MatchPattern, + leaf_node, node_is_coproduct, node_has_structure, with_optional_cardinality } import v2.compiler.infer_types { child_inferred_or_name, - leaf_node, error_type_node, node_is_bridge_error_name, node_is_bridge_dynamic_name, - node_is_optional, node_is_coproduct, node_has_structure, with_optional_cardinality, + node_is_optional, extract_optional_inner_node, emit_map_has } @@ -12441,12 +12483,13 @@ import v2.std.core { make_transport_node, local_transport_node, is_kernel_type, is_transport_kind, ErrorCategory, UnresolvedName, TypeMismatch, InvalidOperation, CascadeError, - TransportKind, LocalTransport + TransportKind, LocalTransport, + leaf_node, with_optional_cardinality, with_required_cardinality, + node_has_structure, node_is_product, no_span } import v2.compiler.infer_types { - leaf_node, with_optional_cardinality, with_required_cardinality, - node_has_structure, node_is_product, node_is_optional, node_is_map, node_is_container, - rt_type, no_span + node_is_optional, node_is_map, node_is_container, + rt_type } import v2.compiler.infer_env { TypeEnv, lookup_type, is_recursive_type @@ -12767,11 +12810,26 @@ fn resolve_node_bounded(n: Node, env: TypeEnv, module_name: String, depth: Int) // any dependent type is processed. Re-resolving the looked-up node // would redundantly walk the entire expanded subtree, causing // exponential blowup on diamond-shaped dependency graphs (OOM). + // + // C2.3: For alias nodes (leaf with .inferred but no structure), + // follow ONE level of .inferred to return the structural target. + // This is O(1), not recursive — the target is already resolved + // by topo ordering. Without this, downstream consumers see an + // alias_node wrapper and must re-resolve through TypeEnv (layer + // downgrade from structural to name-based). + // // Preserve CardOptional from the reference node: the env binding // always stores the canonical (Required) definition, but the // reference site may carry CardOptional (e.g. String? → String // in env). Without this, optionality is silently discarded. - let final_resolved = if node_is_optional(n: n) { with_optional_cardinality(n: resolved) } else { resolved } + let structurally_resolved = if node_has_structure(n: resolved) == false && resolved.children |> count == 0 && resolved.inferred != none { + match rt_node(n: resolved) { + Typed { node: target } => target + InferError { message: _, span: _ } => resolved + Untyped => resolved + } + } else { resolved } + let final_resolved = if node_is_optional(n: n) { with_optional_cardinality(n: structurally_resolved) } else { structurally_resolved } NodeResolveResult { resolved: final_resolved, diagnostics: [] } None => if is_kernel_type(name: n.name) || n.name == "Dynamic" || n.name == "Error" || n.name == "Callable" { @@ -13212,10 +13270,10 @@ import v2.std.core { ExprFieldAccess, ExprMethodCall, ExprCall, ExprVar, Cardinality, Required, InferredNode, Resolved, - expr_children + expr_children, + leaf_node, no_span, node_has_structure } import v2.compiler.infer_types { - leaf_node, no_span, node_has_structure, emit_map_has } import v2.compiler.infer_items { @@ -13362,11 +13420,25 @@ fn expand_transitive_services_once(modules: List, registry: Map) -> Int { + map_values(registry) |> fold(init: 0, f: (acc, info) => acc + info.service_names |> count) +} + +// C4.1: converge to fixpoint instead of hardcoded 5 passes. Each pass +// propagates service dependencies from callees to callers. The loop +// terminates when no new service names are discovered (total count stable). +// For the current ItemInfo shape this is equivalent to full-registry +// stability: service_names only changes by monotone deduped union, so the +// total count can increase but never decrease or redistribute. +// Bounded by remaining_passes as safety net against pathological cases. fn expand_transitive_services(modules: List, registry: Map, remaining_passes: Int) -> Map { if remaining_passes <= 0 { registry } else { + let before = total_service_count(registry: registry) let next = expand_transitive_services_once(modules: modules, registry: registry) - expand_transitive_services(modules: modules, registry: next, remaining_passes: remaining_passes - 1) + let after = total_service_count(registry: next) + if before == after { registry } + else { expand_transitive_services(modules: modules, registry: next, remaining_passes: remaining_passes - 1) } } } @@ -13701,7 +13773,9 @@ import v2.std.core { is_kernel_type, is_kernel_numeric, is_kernel_textual, BinOpKind, BinEq, BinNe, BinLt, BinGt, BinLe, BinGe, BinAnd, BinOr, NullCoalesce, InferredNode, Resolved, CompilerError, rt_node, has_inferred, - NodeType, Typed, InferError, Untyped + NodeType, Typed, InferError, Untyped, + leaf_node, no_span, with_optional_cardinality, with_required_cardinality, + node_is_product, node_is_coproduct, node_has_structure } // ========================================================================= @@ -13713,13 +13787,13 @@ fn child_inferred_or_name(ch: Node) -> Node { else { rt_type(n: ch) } } -fn leaf_node(name: String) -> Node { - Node { name: name, span: SourceSpan { start: 0, end: 0 }, children: [], connective: none, params: [], inferred: none, return_cardinality: Required, uses: [], body: none, transport: none, properties: [], type_annotation: none, config: none, is_self_recursive: false, has_non_tail_self_call: false, expr_data: NoExprData } -} +// leaf_node, with_optional_cardinality, with_required_cardinality +// moved to 00_core.dag (C1 — timeless node vocabulary). // Convenience: extract the return type Node, falling back to Unit. // Use for emission callers and statement contexts where Unit is correct. // For inference callers that need error-awareness, match rt_node directly. +// Stays here: encodes bridge-era stage-boundary policy (Unit fallback). fn rt_type(n: Node) -> Node { match rt_node(n: n) { Typed { node: rt } => rt @@ -13728,17 +13802,6 @@ fn rt_type(n: Node) -> Node { } } -// P5.7a: product_property() and coproduct_property() deleted. -// connective (Conj/Disj) is the sole authority for product/coproduct. - -fn with_optional_cardinality(n: Node) -> Node { - Node { name: n.name, span: n.span, children: n.children, connective: n.connective, params: n.params, inferred: n.inferred, return_cardinality: CardOptional, uses: n.uses, body: n.body, transport: n.transport, properties: n.properties, type_annotation: n.type_annotation, config: n.config, is_self_recursive: n.is_self_recursive, has_non_tail_self_call: n.has_non_tail_self_call, expr_data: n.expr_data } -} - -fn with_required_cardinality(n: Node) -> Node { - Node { name: n.name, span: n.span, children: n.children, connective: n.connective, params: n.params, inferred: n.inferred, return_cardinality: Required, uses: n.uses, body: n.body, transport: n.transport, properties: n.properties, type_annotation: n.type_annotation, config: n.config, is_self_recursive: n.is_self_recursive, has_non_tail_self_call: n.has_non_tail_self_call, expr_data: n.expr_data } -} - fn container_node(kind_name: String, element: Node) -> Node { Node { name: kind_name, span: SourceSpan { start: 0, end: 0 }, children: [element], connective: none, params: [], inferred: none, return_cardinality: Required, uses: [], body: none, transport: none, properties: [], type_annotation: none, config: none, is_self_recursive: false, has_non_tail_self_call: false, expr_data: NoExprData } } @@ -13792,26 +13855,13 @@ fn node_is_bridge_dynamic_name(n: Node) -> Bool { n.name == "Dynamic" } -fn no_span() -> SourceSpan { - SourceSpan { start: 0, end: 0 } -} +// no_span, node_is_product, node_is_coproduct, node_has_structure +// moved to 00_core.dag (C1 — timeless node vocabulary). // ========================================================================= -// Type predicates +// Type predicates (name-checking and bridge-era — stay here until L1=0) // ========================================================================= -fn node_is_product(n: Node) -> Bool { - n.connective != none && n.connective == Some { value: Conj } -} - -fn node_is_coproduct(n: Node) -> Bool { - n.connective != none && n.connective == Some { value: Disj } -} - -fn node_has_structure(n: Node) -> Bool { - node_is_product(n: n) || node_is_coproduct(n: n) -} - fn node_is_container(n: Node) -> Bool { n.name == "List" || n.name == "Set" || n.name == "NonEmptyList" || n.name == "NonEmptySet" } @@ -14241,11 +14291,12 @@ import v2.std.core { TransportKind, RestTransport, ShellTransport, FileTransport, is_transport_kind, transport_has_auth, - field_init_operation_modifier, operation_modifier_name + field_init_operation_modifier, operation_modifier_name, + leaf_node, node_is_product, node_is_coproduct, node_has_structure, with_required_cardinality } import v2.compiler.infer_env { TypeEnv, TypeBinding } -import v2.compiler.infer_types { leaf_node, node_is_optional, node_is_map, node_is_product, node_is_coproduct, node_has_structure, with_required_cardinality, rt_type, emit_map_has } +import v2.compiler.infer_types { node_is_optional, node_is_map, rt_type, emit_map_has } import v2.compiler.infer_sigs { ResolvedFuncSig, ResolvedFuncEnv } import v2.compiler.infer_items { TypedModule, ResolvedGraph, ItemInfo @@ -14457,7 +14508,7 @@ fn empty_emit_scope() -> InferScope { fn module_emit_scope(typed_module: TypedModule) -> InferScope { InferScope { - type_env: typed_module.type_env, + type_env: TypeEnv { bindings: empty_map(), recursive_types: [], recursive_type_set: empty_map() }, func_env: typed_module.func_env, locals: empty_map(), module_name: typed_module.module.name, @@ -15754,7 +15805,8 @@ import v2.std.core { BridgeEmptyMap, BridgeContains, BridgeReverse, BridgeLookup, Text, Interpolation, FieldSummary, FieldAccessStyle, - StoredField, EnumAccessor, OptionalUnwrap, TupleFirst, TupleSecond + StoredField, EnumAccessor, OptionalUnwrap, TupleFirst, TupleSecond, + leaf_node, with_required_cardinality } import v2.compiler.artifact { RenderTarget, Go } @@ -15763,7 +15815,7 @@ import v2.compiler.languages { scaffold_for_target, test_conventions_for_target } import v2.compiler.infer_env { TypeEnv, TypeBinding } -import v2.compiler.infer_types { for_each_element_type_node, node_is_optional, node_is_map, leaf_node, with_required_cardinality, rt_type } +import v2.compiler.infer_types { for_each_element_type_node, node_is_optional, node_is_map, rt_type } import v2.compiler.infer_sigs { ResolvedFuncSig, ResolvedFuncEnv } import v2.compiler.infer_items { ResolvedGraph, TypedModule, @@ -17091,7 +17143,8 @@ import v2.std.core { BridgeToLower, BridgeToUpper, BridgeReplace, BridgeSubstring, BridgeToInt, BridgeEmptyMap, BridgeContains, BridgeReverse, BridgeLookup, FieldSummary, TupleFirst, TupleSecond, - Text, Interpolation + Text, Interpolation, + leaf_node, with_required_cardinality } import v2.compiler.artifact { RenderTarget, Python } @@ -17099,7 +17152,7 @@ import v2.compiler.languages { scaffold_for_target, serialization_for_target, test_conventions_for_target } import v2.compiler.infer_env { TypeEnv, TypeBinding } -import v2.compiler.infer_types { for_each_element_type_node, node_is_optional, node_is_map, leaf_node, with_required_cardinality, rt_type } +import v2.compiler.infer_types { for_each_element_type_node, node_is_optional, node_is_map, rt_type } import v2.compiler.infer_sigs { ResolvedFuncSig, ResolvedFuncEnv } import v2.compiler.infer_items { ResolvedGraph, TypedModule, @@ -18408,7 +18461,8 @@ import v2.std.core { transport_base_url, transport_has_auth, transport_auth_token, transport_auth_header_name, transport_headers, transport_env, - expr_has_self_call, expr_has_non_tail_self_call + expr_has_self_call, expr_has_non_tail_self_call, + leaf_node, with_required_cardinality, node_has_structure } import v2.compiler.artifact { RenderTarget, Rust } @@ -18424,9 +18478,8 @@ import v2.compiler.runtime_rust { rust_runtime_source } import v2.compiler.infer_env { TypeEnv, TypeBinding } import v2.compiler.infer_types { normalize_access_type_node, for_each_element_type_node, - node_is_optional, node_is_map, node_is_container, node_has_structure, + node_is_optional, node_is_map, node_is_container, is_int_type_node, is_string_type_node, is_bool_type_node, is_float_type_node, - leaf_node, with_required_cardinality, rt_type, emit_map_has } import v2.compiler.infer_sigs { ResolvedFuncEnv } @@ -18437,13 +18490,9 @@ import v2.compiler.infer_items { import v2.compiler.infer_service { is_typed_service_call_receiver, extract_typed_service_name } -import v2.compiler.infer_lookup { - resolve_scrutinee_type_node -} import v2.compiler.infer { InferScope, build_params_scope, extend_scope, - infer_record_lit, build_emit_graph_info, expr_span } @@ -19019,7 +19068,7 @@ fn emit_non_empty_wrappers() -> String { fn emit_typed_item(item: Node, registry: Map, scope: InferScope, vtoe: Map, rc_types: Map, emit_info: EmitGraphInfo) -> String { let kind = classify_typed_item(item: item) if (kind == TypedItemTypeDef) { - emit_type_def_from_connective(item: item, recursive_types: scope.type_env.recursive_type_set, rc_types: rc_types) + emit_type_def_from_connective(item: item, recursive_types: emit_info.recursive_type_set, rc_types: rc_types) } else if (kind == TypedItemTypeAlias) { concat(rust_visibility_prefix(), "type ", item.name, " = ", emit_node_type_rc(n: rt_type(n: item), target: Rust, rc_types: rc_types), ";") } else if (kind == TypedItemTypeDecl) { @@ -19932,11 +19981,11 @@ fn emit_typed_field_access(base: Node, field: String, summary: FieldSummary?, re } } -fn type_needs_rc(env: TypeEnv, type_node: Node) -> Bool { - type_needs_rc_seen(env: env, type_node: type_node, seen: empty_map()) +fn type_needs_rc(type_node: Node) -> Bool { + type_needs_rc_seen(type_node: type_node, seen: empty_map()) } -fn type_needs_rc_seen(env: TypeEnv, type_node: Node, seen: Map) -> Bool { +fn type_needs_rc_seen(type_node: Node, seen: Map) -> Bool { let normed = normalize_access_type_node(n: type_node) let normed_kind = classify_type_structure(n: normed) if (normed_kind == TypeConj) { @@ -19947,23 +19996,23 @@ fn type_needs_rc_seen(env: TypeEnv, type_node: Node, seen: Map) -> let canonical = normed.name if normed.inferred != none { let next_seen = if canonical == "" { seen } else { map_insert(seen, canonical, true) } - type_needs_rc_seen(env: env, type_node: rt_type(n: normed), seen: next_seen) + type_needs_rc_seen(type_node: rt_type(n: normed), seen: next_seen) } else if canonical != "" && emit_map_has(m: seen, key: canonical) { false } else { let next_seen = if canonical == "" { seen } else { map_insert(seen, canonical, true) } - let resolved = resolve_scrutinee_type_node(env: env, n: normed) + let resolved = normed if node_has_structure(n: resolved) == false && resolved.inferred == none && resolved.name == normed.name && resolved.children |> count == 0 { false } else { - type_needs_rc_seen(env: env, type_node: resolved, seen: next_seen) + type_needs_rc_seen(type_node: resolved, seen: next_seen) } } } } fn rust_map_value_type(receiver_type: Node, scope: InferScope) -> Node? { - let resolved = resolve_scrutinee_type_node(env: scope.type_env, n: normalize_access_type_node(n: receiver_type)) + let resolved = normalize_access_type_node(n: receiver_type) let map_type = normalize_access_type_node(n: resolved) if node_is_map(n: map_type) { match map_type.children |> skip(1) |> first { @@ -19990,7 +20039,7 @@ fn rust_lookup_receiver_needs_rc_wrap(receiver: Node, scope: InferScope) -> Bool match receiver.inferred { Some { value: Resolved { node: receiver_type } } => match rust_map_value_type(receiver_type: receiver_type, scope: scope) { - Some { value: value_type } => type_needs_rc(env: scope.type_env, type_node: value_type) + Some { value: value_type } => type_needs_rc(type_node: value_type) None => false } _ => false @@ -20186,7 +20235,7 @@ fn emit_typed_call_expr(func: String, args: List, inferred: InferredNo let call_str = if func == "empty_map" { match inferred { Some { value: Resolved { node: ret_type } } => - let resolved_ret = resolve_scrutinee_type_node(env: scope.type_env, n: ret_type) + let resolved_ret = ret_type let type_str = emit_node_type_rc(n: resolved_ret, target: Rust, rc_types: rc_types) if type_str != "" && type_str != "Dynamic" { concat("<", type_str, ">::new()") @@ -20207,9 +20256,9 @@ fn emit_typed_call_expr(func: String, args: List, inferred: InferredNo else { match inferred { Some { value: Resolved { node: ret_type } } => - let resolved_ret = resolve_scrutinee_type_node(env: scope.type_env, n: ret_type) + let resolved_ret = ret_type if node_is_optional(n: resolved_ret) { - type_needs_rc(env: scope.type_env, type_node: with_required_cardinality(n: resolved_ret)) + type_needs_rc(type_node: with_required_cardinality(n: resolved_ret)) } else { false } _ => false } @@ -20536,7 +20585,7 @@ fn emit_intrinsic_typed_method_call(intrinsic: IntrinsicMethod, fold_accumulator // RC4: If the receiver is Optional, emit .map(|p| body) to preserve Option type. // Otherwise use the standard for-loop collect pattern for lists. let recv_is_optional = match receiver.inferred { - Some { value: Resolved { node: rt } } => node_is_optional(n: resolve_scrutinee_type_node(env: scope.type_env, n: rt)) + Some { value: Resolved { node: rt } } => node_is_optional(n: rt) _ => false } if recv_is_optional { @@ -20680,7 +20729,7 @@ fn emit_intrinsic_typed_method_call(intrinsic: IntrinsicMethod, fold_accumulator // Annotate sort_by closure params with the element type to avoid E0282. let elem_type_str = match receiver.inferred { Some { value: Resolved { node: rt } } => - let resolved = resolve_scrutinee_type_node(env: scope.type_env, n: rt) + let resolved = rt let elem = for_each_element_type_node(n: resolved) // Dynamic audit: correct guard — if sort_by element type is Dynamic, // fall back to "_" so Rust infers the closure param type. @@ -20880,15 +20929,6 @@ fn emit_typed_let(name: String, value: Node, body: Node?, registry: Map, span: SourceSpan, registry: Map, scope: InferScope, rc_types: Map, emit_info: EmitGraphInfo) -> String { - let inferred = infer_record_lit(type_name: type_name, field_inits: fields, span: span, scope: scope) - match inferred.typed.expr_data { - ExprRecordLit { type_name: tn, fields: fs, parent_enum: parent_enum } => - emit_typed_record_lit(type_name: tn, fields: fs, parent_enum: parent_enum, resolved_type: rt_type(n: inferred.typed), registry: registry, scope: scope, depth: 0, vtoe: empty_map(), rc_types: rc_types, emit_info: emit_info) - _ => - "compile_error!(\"internal error: infer_record_lit did not produce ExprRecordLit\")" - } -} fn is_optional_struct_field(emit_info: EmitGraphInfo, struct_name: String, field_name: String) -> Bool { match lookup_emit_type_summary(emit_info: emit_info, type_name: struct_name) { @@ -20916,10 +20956,10 @@ fn is_already_optional(texpr: Node, emit_info: EmitGraphInfo, scope: InferScope) // then resolve to the definition. resolve_scrutinee_type_node can // lose use-site cardinality when resolving to the type definition. match texpr.inferred { - Some { value: Resolved { node: rt } } => node_is_optional(n: rt) || node_is_optional(n: resolve_scrutinee_type_node(env: scope.type_env, n: rt)) + Some { value: Resolved { node: rt } } => node_is_optional(n: rt) _ => match map_get(scope.locals, n) { - Some { value: binding } => node_is_optional(n: binding.resolved) || node_is_optional(n: resolve_scrutinee_type_node(env: scope.type_env, n: binding.resolved)) + Some { value: binding } => node_is_optional(n: binding.resolved) None => false } } @@ -20941,26 +20981,26 @@ fn is_already_optional(texpr: Node, emit_info: EmitGraphInfo, scope: InferScope) else { match b.inferred { Some { value: Resolved { node: base_type } } => - let resolved_base = resolve_scrutinee_type_node(env: scope.type_env, n: base_type) + let resolved_base = base_type if is_optional_struct_field(emit_info: emit_info, struct_name: resolved_base.name, field_name: f) { true } else { // Fallback: struct may not be in type_summaries; check expression return type match texpr.inferred { - Some { value: Resolved { node: rt } } => node_is_optional(n: resolve_scrutinee_type_node(env: scope.type_env, n: rt)) + Some { value: Resolved { node: rt } } => node_is_optional(n: rt) _ => false } } _ => match texpr.inferred { - Some { value: Resolved { node: rt } } => node_is_optional(n: resolve_scrutinee_type_node(env: scope.type_env, n: rt)) + Some { value: Resolved { node: rt } } => node_is_optional(n: rt) _ => false } } } _ => match texpr.inferred { - Some { value: Resolved { node: rt } } => node_is_optional(n: rt) || node_is_optional(n: resolve_scrutinee_type_node(env: scope.type_env, n: rt)) + Some { value: Resolved { node: rt } } => node_is_optional(n: rt) _ => false } } diff --git a/src/v2/stage0/src/infer.rs b/src/v2/stage0/src/infer.rs index 6759f617251..4ec9964ecce 100644 --- a/src/v2/stage0/src/infer.rs +++ b/src/v2/stage0/src/infer.rs @@ -2939,7 +2939,7 @@ pub fn build_module_context(contributions: Rc>>, parent __acc_6 = { let __rc_9 = __acc_6; let mut __map_ins_8 = Rc::try_unwrap(__rc_9).unwrap_or_else(|rc| (*rc).clone()); - __map_ins_8.insert(__elem_7.name.clone(), Rc::new(TypeBinding { name: __elem_7.name.clone(), resolved: leaf_node(&__elem_5.name) })); + __map_ins_8.insert(__elem_7.name.clone(), Rc::new(TypeBinding { name: __elem_7.name.clone(), resolved: __elem_5.resolved.clone() })); Rc::new(__map_ins_8) }; } @@ -2960,43 +2960,31 @@ pub fn build_module_context(contributions: Rc>>, parent Rc::new(__map_merged_10) }; let resolve_result = resolve_func_sigs(all_declared_sigs.clone(), local.resolved_items.clone(), &module_name); - let optional_locals = { - let __rc_13 = env_variant_locals; - let mut __map_ins_12 = Rc::try_unwrap(__rc_13).unwrap_or_else(|rc| (*rc).clone()); - __map_ins_12.insert("Some".to_string(), Rc::new(TypeBinding { name: "Some".to_string(), resolved: leaf_node("Optional") })); - Rc::new(__map_ins_12) -}; - let optional_locals = { - let __rc_15 = optional_locals; - let mut __map_ins_14 = Rc::try_unwrap(__rc_15).unwrap_or_else(|rc| (*rc).clone()); - __map_ins_14.insert("None".to_string(), Rc::new(TypeBinding { name: "None".to_string(), resolved: leaf_node("Optional") })); - Rc::new(__map_ins_14) -}; let all_locals = { - let mut __acc_20 = optional_locals.clone(); - for __elem_21 in ({ - let __rc_16 = merged_scope.svc_locals.clone(); - let __map_unwrapped_17 = Rc::try_unwrap(__rc_16).unwrap_or_else(|rc| (*rc).clone()); - let mut __entries_18 = __map_unwrapped_17.into_iter().collect::>(); - __entries_18.sort_by(|a, b| a.0.cmp(&b.0)); - let __values_19 = __entries_18.into_iter().map(|(_, value)| value).collect::>(); - Rc::new(__values_19) + let mut __acc_16 = env_variant_locals.clone(); + for __elem_17 in ({ + let __rc_12 = merged_scope.svc_locals.clone(); + let __map_unwrapped_13 = Rc::try_unwrap(__rc_12).unwrap_or_else(|rc| (*rc).clone()); + let mut __entries_14 = __map_unwrapped_13.into_iter().collect::>(); + __entries_14.sort_by(|a, b| a.0.cmp(&b.0)); + let __values_15 = __entries_14.into_iter().map(|(_, value)| value).collect::>(); + Rc::new(__values_15) }).iter().cloned() { - __acc_20 = { - let __rc_23 = __acc_20; - let mut __map_ins_22 = Rc::try_unwrap(__rc_23).unwrap_or_else(|rc| (*rc).clone()); - __map_ins_22.insert(__elem_21.name.clone(), __elem_21.clone()); - Rc::new(__map_ins_22) + __acc_16 = { + let __rc_19 = __acc_16; + let mut __map_ins_18 = Rc::try_unwrap(__rc_19).unwrap_or_else(|rc| (*rc).clone()); + __map_ins_18.insert(__elem_17.name.clone(), __elem_17.clone()); + Rc::new(__map_ins_18) }; } - __acc_20 + __acc_16 }; Rc::new(ModuleContext { resolved_items: local.resolved_items.clone(), func_env: resolve_result.func_env.clone(), svc_registry: merged_scope.svc_registry.clone(), locals: all_locals.clone(), item_registry: local.item_registry.clone(), diagnostics: v2_rt::concat({ - let mut __flat_mapped_26 = Vec::new(); - for __elem_27 in local.diag_chunks.iter().cloned() { - __flat_mapped_26.extend(__elem_27.clone().iter().cloned()); + let mut __flat_mapped_22 = Vec::new(); + for __elem_23 in local.diag_chunks.iter().cloned() { + __flat_mapped_22.extend(__elem_23.clone().iter().cloned()); } - Rc::new(__flat_mapped_26) + Rc::new(__flat_mapped_22) }, resolve_result.diagnostics.clone()) }) } @@ -3312,7 +3300,19 @@ pub fn build_emit_graph_info(modules: Rc>>) -> Rc) -> Rc { diff --git a/src/v2/stage0/src/infer_emit_info.rs b/src/v2/stage0/src/infer_emit_info.rs index f3670e5a1ad..aa4284d2691 100644 --- a/src/v2/stage0/src/infer_emit_info.rs +++ b/src/v2/stage0/src/infer_emit_info.rs @@ -24,6 +24,7 @@ pub struct EmitGraphInfo { pub variant_to_enum: Rc>, pub enum_variant_membership: Rc>, pub field_type_names: Rc>, + pub recursive_type_set: Rc>, } #[derive(Debug, Clone, PartialEq, Eq, Default)] @@ -35,7 +36,7 @@ pub struct EmitInfoBuildState { } pub fn empty_emit_graph_info() -> Rc { - Rc::new(EmitGraphInfo { type_summaries: Rc::new(std::collections::HashMap::new()), variant_to_enum: Rc::new(std::collections::HashMap::new()), enum_variant_membership: Rc::new(std::collections::HashMap::new()), field_type_names: Rc::new(std::collections::HashMap::new()) }) + Rc::new(EmitGraphInfo { type_summaries: Rc::new(std::collections::HashMap::new()), variant_to_enum: Rc::new(std::collections::HashMap::new()), enum_variant_membership: Rc::new(std::collections::HashMap::new()), field_type_names: Rc::new(std::collections::HashMap::new()), recursive_type_set: Rc::new(std::collections::HashMap::new()) }) } pub fn normalize_emit_type_name(type_name: &str) -> String { diff --git a/src/v2/stage0/src/infer_items.rs b/src/v2/stage0/src/infer_items.rs index bdb0df685d3..28dd573325f 100644 --- a/src/v2/stage0/src/infer_items.rs +++ b/src/v2/stage0/src/infer_items.rs @@ -144,7 +144,7 @@ pub fn variant_locals_from_items(items: Rc>>, init: Rc, env: Rc, module_name: &str, de } } else { if (({ - let __len_45 = n.children.clone().len(); - __len_45 as i64 + let __len_46 = n.children.clone().len(); + __len_46 as i64 }) > 0_i64) && is_user_generic_use_site(n.clone(), env.clone()) { let decl = match lookup_type(env.clone(), &n.name) { Some(d) => { @@ -409,8 +409,8 @@ pub fn resolve_node_bounded(n: Rc, env: Rc, module_name: &str, de Rc::new(NodeResolveResult { resolved: Rc::new(Node { name: n.name.clone(), span: n.span.clone(), children: Rc::new(vec!(key_resolved.clone(), val_resolved.clone())), connective: n.connective.clone(), params: n.params.clone(), inferred: n.inferred.clone(), return_cardinality: n.return_cardinality.clone(), uses: n.uses.clone(), body: n.body.clone(), transport: n.transport.clone(), properties: n.properties.clone(), type_annotation: n.type_annotation.clone(), config: n.config.clone(), is_self_recursive: false, has_non_tail_self_call: false, expr_data: Rc::new(ExprData::NoExprData) }), diagnostics: v2_rt::concat(key_diags.clone(), val_diags.clone()) }) } else { if ({ - let __len_44 = n.children.clone().len(); - __len_44 as i64 + let __len_45 = n.children.clone().len(); + __len_45 as i64 }) == 1_i64 { match n.children.clone().first().cloned() { Some(el) => { @@ -427,8 +427,8 @@ pub fn resolve_node_bounded(n: Rc, env: Rc, module_name: &str, de } } else { if ({ - let __len_43 = n.children.clone().len(); - __len_43 as i64 + let __len_44 = n.children.clone().len(); + __len_44 as i64 }) == 0_i64 { if is_recursive_type(env.clone(), &n.name) { Rc::new(NodeResolveResult { resolved: n.clone(), diagnostics: Rc::new(Vec::new()) }) @@ -436,10 +436,28 @@ pub fn resolve_node_bounded(n: Rc, env: Rc, module_name: &str, de match lookup_type(env.clone(), &n.name) { Some(resolved) => { { - let final_resolved = if node_is_optional(n.clone()) { - with_optional_cardinality(resolved.clone()) + let structurally_resolved = if ((node_has_structure(resolved.clone()) == false) && (({ + let __len_43 = resolved.children.clone().len(); + __len_43 as i64 +}) == 0_i64)) && (resolved.inferred.clone().is_some()) { + match rt_node(resolved.clone()).as_ref() { + NodeType::Typed { node: target, .. } => { + target.clone() + } + NodeType::InferError { message: _, span: _, .. } => { + resolved.clone() + } + NodeType::Untyped => { + resolved.clone() + } +} } else { resolved.clone() +}; + let final_resolved = if node_is_optional(n.clone()) { + with_optional_cardinality(structurally_resolved.clone()) +} else { + structurally_resolved.clone() }; Rc::new(NodeResolveResult { resolved: final_resolved.clone(), diagnostics: Rc::new(Vec::new()) }) } diff --git a/src/v2/stage0/src/infer_service.rs b/src/v2/stage0/src/infer_service.rs index 3e0c6500587..9335c9d3359 100644 --- a/src/v2/stage0/src/infer_service.rs +++ b/src/v2/stage0/src/infer_service.rs @@ -259,6 +259,26 @@ if __cond { } } +pub fn total_service_count(registry: Rc>>) -> i64 { + { + let mut __acc_4 = 0_i64; + for __elem_5 in ({ + let __rc_0 = registry.clone(); + let __map_unwrapped_1 = Rc::try_unwrap(__rc_0).unwrap_or_else(|rc| (*rc).clone()); + let mut __entries_2 = __map_unwrapped_1.into_iter().collect::>(); + __entries_2.sort_by(|a, b| a.0.cmp(&b.0)); + let __values_3 = __entries_2.into_iter().map(|(_, value)| value).collect::>(); + Rc::new(__values_3) +}).iter().cloned() { + __acc_4 = __acc_4.clone() + ({ + let __len_6 = __elem_5.service_names.clone().len(); + __len_6 as i64 +}); + } + __acc_4 +} +} + pub fn expand_transitive_services(modules: Rc>>, registry: Rc>>, remaining_passes: i64) -> Rc>> { stacker::maybe_grow(512 * 1024, 2 * 1024 * 1024, || { let mut __tco_p_modules = modules; @@ -271,7 +291,12 @@ pub fn expand_transitive_services(modules: Rc>>, registry: R if remaining_passes.clone() <= 0_i64 { break registry.clone(); } else { + let before = total_service_count(registry.clone()); let next = expand_transitive_services_once(modules.clone(), registry.clone()); + let after = total_service_count(next.clone()); + if before == after { + break registry.clone(); +} else { { let __tco_0 = modules.clone(); let __tco_1 = next.clone(); @@ -282,6 +307,7 @@ pub fn expand_transitive_services(modules: Rc>>, registry: R continue; } +}; }; } }) diff --git a/src/v2/stage0/src/infer_types.rs b/src/v2/stage0/src/infer_types.rs index acf5a71eb1c..16c2248cefb 100644 --- a/src/v2/stage0/src/infer_types.rs +++ b/src/v2/stage0/src/infer_types.rs @@ -11,10 +11,6 @@ pub fn child_inferred_or_name(ch: Rc) -> Rc { } } -pub fn leaf_node(name: &str) -> Rc { - Rc::new(Node { name: name.to_string(), span: SourceSpan { start: 0_i64, end: 0_i64 }, children: Rc::new(Vec::new()), connective: None, params: Rc::new(Vec::new()), inferred: None, return_cardinality: Cardinality::Required, uses: Rc::new(Vec::new()), body: None, transport: None, properties: Rc::new(Vec::new()), type_annotation: None, config: None, is_self_recursive: false, has_non_tail_self_call: false, expr_data: Rc::new(ExprData::NoExprData) }) -} - pub fn rt_type(n: Rc) -> Rc { match rt_node(n.clone()).as_ref() { NodeType::Typed { node: rt, .. } => { @@ -29,14 +25,6 @@ pub fn rt_type(n: Rc) -> Rc { } } -pub fn with_optional_cardinality(n: Rc) -> Rc { - Rc::new(Node { name: n.name.clone(), span: n.span.clone(), children: n.children.clone(), connective: n.connective.clone(), params: n.params.clone(), inferred: n.inferred.clone(), return_cardinality: Cardinality::CardOptional, uses: n.uses.clone(), body: n.body.clone(), transport: n.transport.clone(), properties: n.properties.clone(), type_annotation: n.type_annotation.clone(), config: n.config.clone(), is_self_recursive: n.is_self_recursive.clone(), has_non_tail_self_call: n.has_non_tail_self_call.clone(), expr_data: n.expr_data.clone() }) -} - -pub fn with_required_cardinality(n: Rc) -> Rc { - Rc::new(Node { name: n.name.clone(), span: n.span.clone(), children: n.children.clone(), connective: n.connective.clone(), params: n.params.clone(), inferred: n.inferred.clone(), return_cardinality: Cardinality::Required, uses: n.uses.clone(), body: n.body.clone(), transport: n.transport.clone(), properties: n.properties.clone(), type_annotation: n.type_annotation.clone(), config: n.config.clone(), is_self_recursive: n.is_self_recursive.clone(), has_non_tail_self_call: n.has_non_tail_self_call.clone(), expr_data: n.expr_data.clone() }) -} - pub fn container_node(kind_name: &str, element: Rc) -> Rc { Rc::new(Node { name: kind_name.to_string(), span: SourceSpan { start: 0_i64, end: 0_i64 }, children: Rc::new(vec!(element.clone())), connective: None, params: Rc::new(Vec::new()), inferred: None, return_cardinality: Cardinality::Required, uses: Rc::new(Vec::new()), body: None, transport: None, properties: Rc::new(Vec::new()), type_annotation: None, config: None, is_self_recursive: false, has_non_tail_self_call: false, expr_data: Rc::new(ExprData::NoExprData) }) } @@ -87,22 +75,6 @@ pub fn node_is_bridge_dynamic_name(n: Rc) -> bool { n.name.clone() == "Dynamic" } -pub fn no_span() -> SourceSpan { - SourceSpan { start: 0_i64, end: 0_i64 } -} - -pub fn node_is_product(n: Rc) -> bool { - (n.connective.clone().is_some()) && (n.connective.clone() == Some(Connective::Conj)) -} - -pub fn node_is_coproduct(n: Rc) -> bool { - (n.connective.clone().is_some()) && (n.connective.clone() == Some(Connective::Disj)) -} - -pub fn node_has_structure(n: Rc) -> bool { - node_is_product(n.clone()) || node_is_coproduct(n.clone()) -} - pub fn node_is_container(n: Rc) -> bool { (((n.name.clone() == "List") || (n.name.clone() == "Set")) || (n.name.clone() == "NonEmptyList")) || (n.name.clone() == "NonEmptySet") } diff --git a/src/v2/stage0/src/parse.rs b/src/v2/stage0/src/parse.rs index 14d99a8a52b..b5fd626eb96 100644 --- a/src/v2/stage0/src/parse.rs +++ b/src/v2/stage0/src/parse.rs @@ -1,5 +1,4 @@ use crate::v2_core::*; -use crate::infer_types::*; use crate::v2_rt; use std::collections::HashMap; use std::rc::Rc; @@ -3597,9 +3596,8 @@ pub fn parse_callable_type_expr(tokens: Rc>>, state: Rc) -> String { let __len_0 = n.children.clone().len(); __len_0 as i64 }) > 0_i64; - if node_is_optional(n.clone()) { + let is_optional = n.return_cardinality.clone() == Cardinality::CardOptional; + if is_optional.clone() { v2_rt::concat("Optional_".to_string(), node_to_name_str(with_required_cardinality(n.clone()))) } else { if n.name.clone() == "Map" { diff --git a/src/v2/stage0/src/tokenize.rs b/src/v2/stage0/src/tokenize.rs index 2b6ae64ed3b..636d5fff0e5 100644 --- a/src/v2/stage0/src/tokenize.rs +++ b/src/v2/stage0/src/tokenize.rs @@ -307,14 +307,6 @@ impl Default for StringScanResult { } impl StringScanResult { - pub fn end_pos(&self) -> i64 { - match self { - StringScanResult::ClosedString { end_pos, .. } => end_pos.clone(), - StringScanResult::InterpolationStart { end_pos, .. } => end_pos.clone(), - StringScanResult::UnterminatedString { end_pos, .. } => end_pos.clone() - } - } - pub fn content(&self) -> String { match self { StringScanResult::ClosedString { content, .. } => content.clone(), @@ -322,6 +314,14 @@ impl StringScanResult { StringScanResult::UnterminatedString { content, .. } => content.clone() } } + + pub fn end_pos(&self) -> i64 { + match self { + StringScanResult::ClosedString { end_pos, .. } => end_pos.clone(), + StringScanResult::InterpolationStart { end_pos, .. } => end_pos.clone(), + StringScanResult::UnterminatedString { end_pos, .. } => end_pos.clone() + } + } } pub fn scan_string(source: Rc, pos: Rc) -> Rc { diff --git a/src/v2/stage0/src/v2_core.rs b/src/v2/stage0/src/v2_core.rs index 45d0454c0c3..996630a60fa 100644 --- a/src/v2/stage0/src/v2_core.rs +++ b/src/v2/stage0/src/v2_core.rs @@ -1441,3 +1441,31 @@ pub fn is_token_node(n: Rc) -> bool { } } +pub fn leaf_node(name: &str) -> Rc { + Rc::new(Node { name: name.to_string(), span: SourceSpan { start: 0_i64, end: 0_i64 }, children: Rc::new(Vec::new()), connective: None, params: Rc::new(Vec::new()), inferred: None, return_cardinality: Cardinality::Required, uses: Rc::new(Vec::new()), body: None, transport: None, properties: Rc::new(Vec::new()), type_annotation: None, config: None, is_self_recursive: false, has_non_tail_self_call: false, expr_data: Rc::new(ExprData::NoExprData) }) +} + +pub fn no_span() -> SourceSpan { + SourceSpan { start: 0_i64, end: 0_i64 } +} + +pub fn with_optional_cardinality(n: Rc) -> Rc { + Rc::new(Node { name: n.name.clone(), span: n.span.clone(), children: n.children.clone(), connective: n.connective.clone(), params: n.params.clone(), inferred: n.inferred.clone(), return_cardinality: Cardinality::CardOptional, uses: n.uses.clone(), body: n.body.clone(), transport: n.transport.clone(), properties: n.properties.clone(), type_annotation: n.type_annotation.clone(), config: n.config.clone(), is_self_recursive: n.is_self_recursive.clone(), has_non_tail_self_call: n.has_non_tail_self_call.clone(), expr_data: n.expr_data.clone() }) +} + +pub fn with_required_cardinality(n: Rc) -> Rc { + Rc::new(Node { name: n.name.clone(), span: n.span.clone(), children: n.children.clone(), connective: n.connective.clone(), params: n.params.clone(), inferred: n.inferred.clone(), return_cardinality: Cardinality::Required, uses: n.uses.clone(), body: n.body.clone(), transport: n.transport.clone(), properties: n.properties.clone(), type_annotation: n.type_annotation.clone(), config: n.config.clone(), is_self_recursive: n.is_self_recursive.clone(), has_non_tail_self_call: n.has_non_tail_self_call.clone(), expr_data: n.expr_data.clone() }) +} + +pub fn node_is_product(n: Rc) -> bool { + (n.connective.clone().is_some()) && (n.connective.clone() == Some(Connective::Conj)) +} + +pub fn node_is_coproduct(n: Rc) -> bool { + (n.connective.clone().is_some()) && (n.connective.clone() == Some(Connective::Disj)) +} + +pub fn node_has_structure(n: Rc) -> bool { + node_is_product(n.clone()) || node_is_coproduct(n.clone()) +} + diff --git a/src/v2/tests/src/pipeline.rs b/src/v2/tests/src/pipeline.rs index cc781c30b2e..ab64cfc2076 100644 --- a/src/v2/tests/src/pipeline.rs +++ b/src/v2/tests/src/pipeline.rs @@ -224,6 +224,13 @@ fn indirect_type_alias_cycles_do_not_recurse_forever() { assert_no_diagnostics(&result); } +#[test] +fn chained_type_alias_field_access() { + let source = "module test\ntype A { val: Int }\ntype B = A\ntype C = B\nfn get(x: C) -> Int {\n x.val\n}\n"; + let result = compile_dag(source); + assert_no_diagnostics(&result); +} + #[test] fn string_index_and_slice_emit_runtime_calls() { let source = "module test\nfn char_at(s: String) -> String {\n s[0]\n}\nfn substr(s: String) -> String {\n s[0..1]\n}\n"; From 1aae3cb05fc0b64bca11de6e4878024a9d06a0d6 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Thu, 26 Mar 2026 13:23:24 -0400 Subject: [PATCH 10/10] Fix test imports: leaf_node/with_optional_cardinality moved to v2_core C1 moved node constructors from 04_types.dag to 00_core.dag. After stage0 regeneration, these functions live in v2_core instead of infer_types. Update test imports to match. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/v2/tests/src/infer_semantics.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/v2/tests/src/infer_semantics.rs b/src/v2/tests/src/infer_semantics.rs index 0d710db8608..24557f6bd42 100644 --- a/src/v2/tests/src/infer_semantics.rs +++ b/src/v2/tests/src/infer_semantics.rs @@ -4,9 +4,9 @@ use v2_compiler::infer_access; use v2_compiler::infer_env::TypeEnv; use v2_compiler::infer_patterns::{self, NodeLookupStatus}; use v2_compiler::infer_types::{ - bare_map_node, container_node, leaf_node, map_node, with_optional_cardinality, + bare_map_node, container_node, map_node, }; -use v2_compiler::v2_core::{Cardinality, InferredNode, MatchArm, MatchPattern, Node, NodeType, SourceSpan}; +use v2_compiler::v2_core::{Cardinality, InferredNode, MatchArm, MatchPattern, Node, NodeType, SourceSpan, leaf_node, with_optional_cardinality}; fn zero_span() -> SourceSpan { SourceSpan { start: 0, end: 0 }