From 0f7726237b88fb9834b671bf7cdcd1e84939fba2 Mon Sep 17 00:00:00 2001 From: Fuad Hasan Date: Sun, 23 Aug 2026 20:43:36 +0600 Subject: [PATCH] Add optional narrowing and payload access Insert FlowTyped between CFG and ownership consumers, centralize effective expression types and carrier/value place resolution, and lower optional presence and payload operations explicitly through HIR, MIR, and LLVM. Module.EffectiveExprType, place.Resolve, and PayloadOrigins centralize shared flow/place invariants; no compatibility wrappers or parallel walkers remain. Validated with uncached go tests, go vet, race tests, build script, bundled x_test fixtures, formatting audit, and git diff --check. --- COMPILER_GUIDELINES.md | 7 +- README.md | 3 +- docs/allocator-provenance.md | 15 +- docs/copy-move-mock-programs.md | 5 +- docs/language-spec.md | 45 +- docs/ownership-pointer-model.md | 24 +- internal/backend/llvm/emitter_test.go | 99 +- internal/backend/llvm/instruction_emit.go | 51 +- internal/backend/llvm/type_layout.go | 2 + internal/diagnostics/codes.go | 2 + internal/ir/constfold.go | 2 + internal/ir/hir/lower/module_lower.go | 146 ++- internal/ir/hir/lower/module_lower_test.go | 75 ++ internal/ir/mir/model.go | 17 + internal/ir/mir/module_lower.go | 7 + internal/ir/mir/module_lower_test.go | 42 + internal/ir/nodes.go | 62 +- internal/lsp/cursor.go | 2 +- internal/lsp/hover.go | 10 +- internal/lsp/server_test.go | 101 ++ internal/phase/phase.go | 4 + internal/phase/phase_test.go | 2 +- internal/pipeline/pipeline.go | 12 +- internal/pipeline/pipeline_test.go | 401 ++++++++ internal/project/modules.go | 24 + internal/project/modules_test.go | 18 +- internal/semantics/flowresult/result.go | 45 + internal/semantics/ownership/expr.go | 34 +- internal/semantics/ownership/ownership.go | 10 +- .../semantics/ownership/ownership_test.go | 95 ++ internal/semantics/ownership/reference.go | 64 +- internal/semantics/place/origin.go | 158 ++- internal/semantics/place/origin_test.go | 61 +- internal/semantics/typechecker/check_call.go | 6 +- internal/semantics/typechecker/check_expr.go | 68 +- internal/semantics/typechecker/check_stmt.go | 8 +- internal/semantics/typechecker/errors.go | 14 + internal/semantics/typechecker/flow.go | 955 ++++++++++++++++++ internal/semantics/typechecker/flow_test.go | 70 ++ internal/semantics/typechecker/typechecker.go | 9 +- internal/semantics/typeinfo/capabilities.go | 7 +- internal/semantics/typeinfo/compatibility.go | 6 +- .../semantics/typeinfo/compatibility_test.go | 12 + internal/semantics/typeinfo/types_test.go | 3 + .../peeper.toml | 7 + .../src/main.peep | 8 + .../peeper.toml | 7 + .../src/main.peep | 3 + .../peeper.toml | 7 + .../src/main.peep | 15 + .../peeper.toml | 7 + .../src/main.peep | 6 + .../peeper.toml | 7 + .../src/main.peep | 13 + x_test/runtime_optional_narrowing/peeper.toml | 6 + .../runtime_optional_narrowing/src/main.peep | 109 ++ 56 files changed, 2690 insertions(+), 308 deletions(-) create mode 100644 internal/semantics/flowresult/result.go create mode 100644 internal/semantics/typechecker/flow.go create mode 100644 internal/semantics/typechecker/flow_test.go create mode 100644 x_test/negative_optional_index_invalidation/peeper.toml create mode 100644 x_test/negative_optional_index_invalidation/src/main.peep create mode 100644 x_test/negative_optional_missing_proof/peeper.toml create mode 100644 x_test/negative_optional_missing_proof/src/main.peep create mode 100644 x_test/negative_optional_partial_move/peeper.toml create mode 100644 x_test/negative_optional_partial_move/src/main.peep create mode 100644 x_test/negative_optional_unstable_index/peeper.toml create mode 100644 x_test/negative_optional_unstable_index/src/main.peep create mode 100644 x_test/negative_optional_use_after_consume/peeper.toml create mode 100644 x_test/negative_optional_use_after_consume/src/main.peep create mode 100644 x_test/runtime_optional_narrowing/peeper.toml create mode 100644 x_test/runtime_optional_narrowing/src/main.peep diff --git a/COMPILER_GUIDELINES.md b/COMPILER_GUIDELINES.md index b81a381..df93430 100644 --- a/COMPILER_GUIDELINES.md +++ b/COMPILER_GUIDELINES.md @@ -107,7 +107,7 @@ arbitrary total order to hide dependency. Control-flow-sensitive rules belong on a representation that understands reachability and predecessors. Examples include return completeness, definite -initialization, ownership state, and future narrowing. +initialization, ownership state, and optional narrowing. Control-flow edges must carry semantic kinds when analyses depend on branch meaning. Consumers must not infer true/false, return, unwind, or cleanup meaning @@ -120,6 +120,11 @@ CFG topology should remain a control-flow artifact. Analysis outputs such as cleanup plans or narrowing facts belong to their analyses unless they are part of graph topology itself. +Optional narrowing produces `Module.Flow` after CFG construction and before +definite initialization and ownership. Downstream phases query effective +per-use types and consume recorded optional-test, payload, and origin evidence. +They must not re-detect `none` comparisons from AST shape or backend text. + ## 6. Centralize Structural Traversal Do not duplicate exhaustive AST, HIR, MIR, expression, place, type, or member diff --git a/README.md b/README.md index 8efe5bc..5f981db 100644 --- a/README.md +++ b/README.md @@ -83,7 +83,8 @@ source -> tokens -> AST -> name and base-type semantics - -> typed CFG + -> CFG + -> flow typing and optional narrowing -> definite initialization -> ownership -> project-wide usage analysis diff --git a/docs/allocator-provenance.md b/docs/allocator-provenance.md index 1776765..18c3f0b 100644 --- a/docs/allocator-provenance.md +++ b/docs/allocator-provenance.md @@ -148,7 +148,7 @@ scoped allocator contexts remain later work. | --- | --- | | `Allocator` | `allocator` | | `*T` | `{T* data, allocator}` | -| `?*T` | same as `*T`; `data == null` means `none` | +| `?*T` | `{i1 present, {T* data, allocator} value}` | | `[]T` | `{T* data, usize length, usize capacity, allocator}` | | `str` | `{byte* data, usize length, allocator}` | | `*Iface` | `{rawptr data, rawptr dispatch, allocator}` | @@ -156,8 +156,15 @@ scoped allocator contexts remain later work. | `&T`, `&mut T`, slice views | unchanged non-owning layouts; view length is target `usize` | | `rawptr`, `cstr` | unchanged; no provenance | -`none` for `?*T` zeroes full carrier. Optional presence checks inspect only -`data`. Valid owners always have non-null data and allocator. +`none` for `?*T` clears the tagged carrier. Optional presence checks inspect the +`present` field; proven payload access projects `value`. A present owned payload +retains its allocator provenance through moves and optional wrapping. + +Flow typing runs after CFG construction and records payload-access and resolved +origin evidence before ownership. Ownership consumes that evidence. HIR and MIR +represent presence tests and payload projections explicitly; backend lowering +does not rediscover `none` comparisons or infer optional layout from emitted +text. `usize` above is backend `IndexType`: `i32` on supported 32-bit targets and `i64` on supported 64-bit targets. Length, capacity, and index operands must @@ -346,7 +353,7 @@ Each behavior step requires Go tests plus bundled `x_test/` fixtures. declarations fail before LLVM; imported Peeper owner ABI remains valid - runtime: allocator counters prove allocation/deallocation pair and exactly-once release; two allocator descriptors prove each owner routes to origin -- backend: 64-bit and 32-bit layout/object checks; pointer niche, dynamic header, +- backend: 64-bit and 32-bit layout/object checks; tagged optional owner, dynamic header, vtable slots, size/alignment forwarding - regression: existing ownership, array, slice, interface, borrow, runtime-symbol, and cleanup fixtures diff --git a/docs/copy-move-mock-programs.md b/docs/copy-move-mock-programs.md index 0a192bc..bcd2736 100644 --- a/docs/copy-move-mock-programs.md +++ b/docs/copy-move-mock-programs.md @@ -84,14 +84,15 @@ storage and require a named allocating clone method. ```peep fn optionals() { let scalar: ?i32 = 7 - let scalar_copy = optional_copy(&scalar) + let scalar_copy = scalar let owner: ?*Node = none let owner_clone = clone_optional_owner(&owner) } ``` -Optional values always move implicitly. Duplication APIs are user-defined. +Optional values copy when their payload copies. An optional with a move-only +payload moves implicitly; duplication remains a user-defined API. ## By-Value Calls And Borrows diff --git a/docs/language-spec.md b/docs/language-spec.md index 9ca9e61..76e4893 100644 --- a/docs/language-spec.md +++ b/docs/language-spec.md @@ -15,7 +15,8 @@ Core rules: - Builtin concepts must use builtin syntax, not library-shaped names. - Heap allocation is explicit. - Scalars and raw pointers copy implicitly. -- Composites move implicitly on every by-value use. +- Non-optional composites move implicitly on every by-value use. +- Optionals copy only when their payload type copies; otherwise they move. - Duplication beyond implicit scalar/raw copy is an ordinary user-defined method API. - Types containing tracked ownership cannot be copied. - Live owned values are destroyed automatically at normal scope exit. @@ -28,7 +29,7 @@ Core rules: | --- | --- | --- | | scalar builtin | Scalar value | Implicit copy | | `T` | Composite value | Implicit move; user methods may construct duplicates | -| `?T` | Optional value | Implicit move | +| `?T` | Optional value | Copies when `T` copies; otherwise moves | | `*T` | Unique non-null heap handle to `T` | Implicit move; never copyable | | `rawptr` | Opaque nullable pointer | Copyable pointer bits | | `&T` | Shared reference to `T` | Copyable temporary view | @@ -72,6 +73,46 @@ must be ordered, within the byte length, and on UTF-8 codepoint boundaries. Invalid bounds or boundaries trap at runtime. The owner remains responsible for backing storage and is dropped exactly once. +## Optional Values And Flow Narrowing + +`?T` contains either one `T` value or `none`. `none` is valid only where an +optional type is expected. A `T` value promotes to `?T`; this permits one-layer +promotion such as `?T` to `??T` when the outer optional is expected. Assigning +or passing a whole optional to an explicit optional destination preserves its +carrier type instead of reading its payload. + +Comparing a stable optional place with `none` establishes presence on one CFG +edge. `x != none` proves presence on the true edge; `x == none` proves presence +on the false edge. Reversed operands have identical meaning. Each proof unwraps +one optional layer, so nested optionals require one proof per layer. A proven +ordinary value use has payload type `T`; an unproven use retains `?T` and cannot +stand in for `T`. + +Stable places are variables, field and nested-field projections, constant-folded +indexes, and direct resolved integral binding indexes. Other computed indexes +are unstable. Assigning a carrier or ancestor, mutating overlapping storage +through an alias, changing a binding used as an index, or calling code that may +mutate overlapping storage invalidates affected facts. Unknown raw-pointer +effects invalidate all potentially reachable facts; calls also invalidate +mutable module-global facts. Payload-descendant mutation preserves presence of +the containing optional. + +Facts survive terminating guards, intersect at joins, and reach a fixed point +through loops. Logical `&&` and `||` are eager: their right operands receive no +short-circuit proof, though the completed boolean result may refine its outgoing +CFG edge. + +Presence checks never consume. Reading a copyable payload, including a shared +reference payload, preserves the carrier. Moving a move-only payload from a +direct named local or parameter consumes the whole carrier. Move-only payloads +cannot be partially moved from fields, indexes, pointees, or other projected +places; borrow them instead. Reassigning a consumed carrier reinitializes it. + +Every optional currently uses tagged `{present, value}` runtime storage, +including pointer payloads. Optional-to-optional equality, fallback operators, +explicit unwrap syntax, optional chaining, and optional patterns are not part of +current language surface. Niche layout remains separate future work. + ## Numeric Literals And Conversions Numeric literals may carry an attached explicit source type: diff --git a/docs/ownership-pointer-model.md b/docs/ownership-pointer-model.md index c4e64ec..5fce451 100644 --- a/docs/ownership-pointer-model.md +++ b/docs/ownership-pointer-model.md @@ -108,11 +108,20 @@ Implementation status: - `none` lowers in expected optional contexts. - `T` can lower to `?T` as `some(T)`. -- `?*T` should use pointer niche layout. -- other optionals currently use tagged layout. +- every optional uses tagged `{present, value}` layout, including `?*T`. - `rawptr` is nullable by default, so `?rawptr` is not part of target model. -Future layout work may add niche detection for more types. +CFG flow typing narrows stable optional variables, fields, nested projections, +constant-folded indexes, and direct integral binding indexes after semantic +`none` tests. Joins intersect presence facts, loops run to a fixed point, and +carrier, alias, index-dependency, call, global, and scope invalidation remove +facts when their proof may no longer hold. + +An optional copies only when its payload copies. Reading a proven copyable or +shared-reference payload preserves the carrier. Moving a move-only payload from +a direct named optional consumes the whole carrier; moving one from a field, +index, pointee, or other partial place is rejected. Presence checks never +consume. Pointer niche layout remains future issue #30 work. ## Strings @@ -132,8 +141,9 @@ permanent literal storage, but never owns or frees its backing bytes. ## Copy And Move Integer/float scalars, bool, byte, char, raw pointers, and cstr copy implicitly. -Shared references duplicate their borrow header. Every other value moves on a -by-value use. +Shared references duplicate their borrow header. Optionals follow their payload: +`?T` copies when `T` copies and moves otherwise. Every remaining value moves on +a by-value use. ```peep struct Buffer { @@ -398,6 +408,8 @@ struct Node { - `@expr` produces a non-owning raw pointer to addressable storage. - `?T` is optional for non-raw values. - `?*T` is nullable heap-handle storage. +- an optional copies only when its payload copies; otherwise it moves. +- move-only payload extraction consumes a direct named carrier and is rejected from partial places. - `str` is an owned immutable text value; `&str` is its borrowed view. - allocator returns `*T`. - `free` consumes allocator-created `*T`. @@ -405,7 +417,7 @@ struct Node { - safe code cannot forget or leak an owned value. - reference returns declare parameter or receiver origins with `from`. - borrowed rvalue temporaries live through one full expression only. -- every composite moves implicitly on by-value use. +- every non-optional composite moves implicitly on by-value use. - `*T`, `*Interface`, dynamic arrays/strings, and mutable references never duplicate implicitly. - `rawptr` copy is shallow address-bit copy because it owns nothing. - bare interfaces are unsized contracts; runtime values require `&`, `&mut`, or `*`. diff --git a/internal/backend/llvm/emitter_test.go b/internal/backend/llvm/emitter_test.go index 12d3039..d834156 100644 --- a/internal/backend/llvm/emitter_test.go +++ b/internal/backend/llvm/emitter_test.go @@ -954,6 +954,53 @@ func TestGenerateLLVMIRLowersOptionalOwnedPointerAsTagged(t *testing.T) { } } +func TestGenerateLLVMIRLowersTaggedOptionalOwnedDropAcrossTargetWidths(t *testing.T) { + for _, compilerTarget := range []struct { + name string + info target.Info + bits int + indexType string + }{ + {name: "amd64", info: testLinuxAMD64, bits: target.Bits64, indexType: "i64"}, + {name: "386", info: testLinux386, bits: target.Bits32, indexType: "i32"}, + } { + t.Run(compilerTarget.name, func(t *testing.T) { + types := newLLVMTypeFixture(compilerTarget.bits) + mod := &mir.Module{ + Name: "test", Types: types.table, FilePath: unixTestPath, + Funcs: []*mir.Function{{ + Name: "release", Params: []ir.Param{{Name: "value", Type: types.optionalOwnedI32}}, + ReturnType: types.void, + Blocks: []*mir.Block{{ + ID: 0, + Instrs: []mir.Instr{&mir.Drop{Value: &mir.RefName{Name: "value", Type: types.optionalOwnedI32}}}, + Term: &mir.Ret{}, + }}, + }}, + } + out := GenerateLLVMIR(mod, diagnostics.NewDiagnosticBag(), compilerTarget.info, false) + carrier := "{ i1, { i32*, i8* } }" + if !strings.Contains(out, "define void @release("+carrier+" %value)") || + !strings.Contains(out, "extractvalue "+carrier+" %value, 0") || + !strings.Contains(out, "extractvalue "+carrier+" %value, 1") || + !strings.Contains(out, "br i1") || + !strings.Contains(out, "ptrtoint i32* getelementptr (i32, i32* null, i32 1) to "+compilerTarget.indexType) { + t.Fatalf("expected tagged optional drop using %s target width, got:\n%s", compilerTarget.indexType, out) + } + + clang, err := exec.LookPath("clang") + if err != nil { + return + } + cmd := exec.Command(clang, "-target", compilerTarget.info.LLVMTriple, "-x", "ir", "-c", "-o", filepath.Join(t.TempDir(), "optional.o"), "-") + cmd.Stdin = strings.NewReader(out) + if output, err := cmd.CombinedOutput(); err != nil { + t.Fatalf("%s tagged optional LLVM is invalid: %v\n%s\n%s", compilerTarget.name, err, output, out) + } + }) + } +} + func TestGenerateLLVMIRDefaultDescriptorEmitted(t *testing.T) { mod := &mir.Module{ Name: "test", Types: llvmTypes.table, @@ -1604,7 +1651,7 @@ func TestGenerateLLVMIRLowersZeroValueOptionals(t *testing.T) { }}, }, { - Name: "niche", + Name: "tagged_ptr", ReturnType: llvmTypes.optionalOwnedI32, EntryID: 0, Blocks: []*mir.Block{{ @@ -1625,7 +1672,7 @@ func TestGenerateLLVMIRLowersZeroValueOptionals(t *testing.T) { if !strings.Contains(irText, "ret { i1, i32 } zeroinitializer") { t.Fatalf("expected tagged optional none as zeroinitializer, got:\n%s", irText) } - if !strings.Contains(irText, "define { i1, { i32*, i8* } } @niche(") { + if !strings.Contains(irText, "define { i1, { i32*, i8* } } @tagged_ptr(") { t.Fatalf("expected tagged optional pointer return type, got:\n%s", irText) } if !strings.Contains(irText, "ret { i1, { i32*, i8* } } zeroinitializer") { @@ -1653,7 +1700,7 @@ func TestGenerateLLVMIRLowersOptionalSome(t *testing.T) { }}, }, { - Name: "niche", + Name: "tagged_ptr", Params: []ir.Param{{Name: "p", Type: llvmTypes.ownedI32}}, ReturnType: llvmTypes.optionalOwnedI32, EntryID: 0, @@ -1675,7 +1722,7 @@ func TestGenerateLLVMIRLowersOptionalSome(t *testing.T) { if !strings.Contains(irText, "insertvalue { i1, i32 } %") || !strings.Contains(irText, "i32 7, 1") { t.Fatalf("expected tagged optional payload, got:\n%s", irText) } - if !strings.Contains(irText, "define { i1, { i32*, i8* } } @niche({ i32*, i8* } %p)") { + if !strings.Contains(irText, "define { i1, { i32*, i8* } } @tagged_ptr({ i32*, i8* } %p)") { t.Fatalf("expected tagged optional pointer ABI, got:\n%s", irText) } if !strings.Contains(irText, "insertvalue { i1, { i32*, i8* } } %") || !strings.Contains(irText, "{ i32*, i8* } %p, 1") { @@ -1683,7 +1730,7 @@ func TestGenerateLLVMIRLowersOptionalSome(t *testing.T) { } } -func TestGenerateLLVMIRComparesTaggedOptionalWithNone(t *testing.T) { +func TestGenerateLLVMIRReadsTaggedOptionalPresence(t *testing.T) { const targetTriple = "x86_64-unknown-linux-gnu" mod := &mir.Module{ Name: "test", @@ -1698,11 +1745,8 @@ func TestGenerateLLVMIRComparesTaggedOptionalWithNone(t *testing.T) { ID: 0, Instrs: []mir.Instr{ &mir.Assign{Name: "x", Value: &mir.OptionalSome{Value: &mir.RefConst{Value: "7", Type: llvmTypes.i32}, Type: llvmTypes.optionalI32}}, - &mir.Assign{Name: "none", Value: &mir.ZeroValue{Type: llvmTypes.optionalI32}}, - &mir.Assign{Name: "isnone", Value: &mir.Binary{ - Op: "==", - Left: &mir.RefName{Name: "x", Type: llvmTypes.optionalI32}, - Right: &mir.RefName{Name: "none", Type: llvmTypes.optionalI32}, + &mir.Assign{Name: "present", Value: &mir.OptionalPresent{ + Value: &mir.RefName{Name: "x", Type: llvmTypes.optionalI32}, Type: llvmTypes.boolType, }}, }, @@ -1716,8 +1760,39 @@ func TestGenerateLLVMIRComparesTaggedOptionalWithNone(t *testing.T) { if !strings.Contains(irText, "extractvalue { i1, i32 } %") { t.Fatalf("expected optional tag extraction, got:\n%s", irText) } - if !strings.Contains(irText, "icmp eq i1") { - t.Fatalf("expected tag compare against none, got:\n%s", irText) + if strings.Contains(irText, "icmp eq i1") { + t.Fatalf("presence read must not compare aggregate text against none, got:\n%s", irText) + } +} + +func TestGenerateLLVMIRLoadsTaggedOptionalPayload(t *testing.T) { + mod := &mir.Module{ + Name: "test", Types: llvmTypes.table, FilePath: unixTestPath, + Funcs: []*mir.Function{{ + Name: "payload", Params: []ir.Param{{Name: "value", Type: llvmTypes.optionalI32}}, ReturnType: llvmTypes.i32, EntryID: 0, + Blocks: []*mir.Block{{ + ID: 0, + Instrs: []mir.Instr{&mir.Assign{Name: "payload", Value: &mir.Load{ + Place: &mir.Place{ + Root: &mir.RefName{Name: "value", Type: llvmTypes.optionalI32}, + Projections: []mir.PlaceProjection{ + {Kind: mir.PlaceProjectionOptionalPayload, Type: llvmTypes.i32}, + }, + Type: llvmTypes.i32, + }, + Type: llvmTypes.i32, + }}}, + Term: &mir.Ret{Value: &mir.RefName{Name: "payload", Type: llvmTypes.i32}}, + }}, + }}, + } + + irText := GenerateLLVMIR(mod, diagnostics.NewDiagnosticBag(), testLinuxAMD64, false) + if !strings.Contains(irText, "getelementptr inbounds { i1, i32 }, { i1, i32 }*") || !strings.Contains(irText, "i32 1") { + t.Fatalf("expected named optional payload field GEP, got:\n%s", irText) + } + if !strings.Contains(irText, "load i32, i32*") { + t.Fatalf("expected optional payload load, got:\n%s", irText) } } diff --git a/internal/backend/llvm/instruction_emit.go b/internal/backend/llvm/instruction_emit.go index 0e723df..4c28728 100644 --- a/internal/backend/llvm/instruction_emit.go +++ b/internal/backend/llvm/instruction_emit.go @@ -181,6 +181,8 @@ func placeNeedsRootAddr(types *ir.TypeTable, place *mir.Place) bool { return false case mir.PlaceProjectionField: return true + case mir.PlaceProjectionOptionalPayload: + return true case mir.PlaceProjectionIndex: rootType, ok := types.Type(mirRefType(place.Root)) if !ok { @@ -260,6 +262,12 @@ func emitPlacePtr(b *llvmBuilder, place *mir.Place) (llvmPlace, bool) { } hasCurrent = true addressed = true + case mir.PlaceProjectionOptionalPayload: + if !hasCurrent { + b.emitter.markInvalid("optional payload place requires addressable storage") + return llvmPlace{}, false + } + current = b.namedFieldPlace(current, llvmFieldValue) default: b.emitter.markInvalid(fmt.Sprintf("unsupported MIR place projection %d", projection.Kind)) return llvmPlace{}, false @@ -584,9 +592,6 @@ func emitValueExpr(b *llvmBuilder, expr mir.ValueExpr) llvmValue { } return b.arithmetic(opcode, left, shiftCount) case "==", "!=", "<", "<=", ">", ">=": - if result, ok := emitOptionalNoneCompare(b, e.Op, e.Left, e.Right, left, right); ok { - return result - } if isFloatType(b.emitter.mod.Types, leftType) { pred := map[string]string{"==": "oeq", "!=": "one", "<": "olt", "<=": "ole", ">": "ogt", ">=": "oge"}[e.Op] return b.compare("fcmp", pred, left, right) @@ -666,6 +671,8 @@ func emitValueExpr(b *llvmBuilder, expr mir.ValueExpr) llvmValue { } value := b.insertField(b.zero(b.emitter.layout(e.Type)), b.value("true", llvmScalarLayout("i1")), llvmFieldPresent) return b.insertField(value, emitRef(b, e.Value), llvmFieldValue) + case *mir.OptionalPresent: + return b.extractField(emitRef(b, e.Value), llvmFieldPresent) case *mir.InterfaceMake: value := emitRef(b, e.Value) dataPtr := value @@ -708,44 +715,6 @@ func emitValueExpr(b *llvmBuilder, expr mir.ValueExpr) llvmValue { }) } -func emitOptionalNoneCompare(b *llvmBuilder, op string, leftRef, rightRef mir.ValueRef, leftValue, rightValue llvmValue) (llvmValue, bool) { - if op != "==" && op != "!=" { - return llvmValue{}, false - } - leftType, leftOK := b.emitter.mod.Types.Type(mirRefType(leftRef)) - rightType, rightOK := b.emitter.mod.Types.Type(mirRefType(rightRef)) - leftOptional := leftOK && leftType.Kind == ir.TypeOptional - rightOptional := rightOK && rightType.Kind == ir.TypeOptional - if !leftOptional && !rightOptional { - return llvmValue{}, false - } - leftNone := leftValue.Text == "zeroinitializer" - rightNone := rightValue.Text == "zeroinitializer" - if leftNone && rightNone { - if op == "==" { - return b.value("true", llvmScalarLayout("i1")), true - } - return b.value("false", llvmScalarLayout("i1")), true - } - var value llvmValue - if leftNone { - value = rightValue - } else if rightNone { - value = leftValue - } else { - if b != nil && b.emitter != nil { - b.emitter.markInvalid("optional equality currently requires `none` on one side") - } - return b.value("false", llvmScalarLayout("i1")), true - } - tag := b.extractField(value, llvmFieldPresent) - pred := "eq" - if op == "!=" { - pred = "ne" - } - return b.compare("icmp", pred, tag, b.value("false", tag.Layout)), true -} - func emitRef(b *llvmBuilder, ref mir.ValueRef) llvmValue { if ref == nil { b.invariant("reference emission requires MIR value") diff --git a/internal/backend/llvm/type_layout.go b/internal/backend/llvm/type_layout.go index 3461982..51aff5f 100644 --- a/internal/backend/llvm/type_layout.go +++ b/internal/backend/llvm/type_layout.go @@ -433,6 +433,8 @@ func mirValueType(expr mir.ValueExpr) ir.TypeID { return v.Type case *mir.OptionalSome: return v.Type + case *mir.OptionalPresent: + return v.Type case *mir.InterfaceMake: return v.Type case *mir.InterfaceCall: diff --git a/internal/diagnostics/codes.go b/internal/diagnostics/codes.go index 5da2069..71eb339 100644 --- a/internal/diagnostics/codes.go +++ b/internal/diagnostics/codes.go @@ -73,6 +73,8 @@ const ( ErrBorrowConflict = "T0037" ErrUnknownIdentifier = "T0038" ErrUninitializedVariable = "T0039" + ErrOptionalPayloadProof = "T0041" + ErrUnstableNarrowing = "T0042" // Module/Import errors (M prefix) ErrModuleNotFound = "M0001" diff --git a/internal/ir/constfold.go b/internal/ir/constfold.go index 0887bc6..ab58536 100644 --- a/internal/ir/constfold.go +++ b/internal/ir/constfold.go @@ -22,6 +22,8 @@ func FoldExpr(types *TypeTable, expr Expr, env map[string]constvalue.Value) Expr return expr case *OptionalSome: return &OptionalSome{Value: FoldExpr(types, node.Value, env), Type: node.Type, SourceInfo: node.SourceInfo} + case *OptionalPresent: + return &OptionalPresent{Value: FoldExpr(types, node.Value, env), Type: node.Type, SourceInfo: node.SourceInfo} case *Ident: if env != nil { if value, ok := env[node.Name]; ok && value != nil { diff --git a/internal/ir/hir/lower/module_lower.go b/internal/ir/hir/lower/module_lower.go index 6b66187..716399f 100644 --- a/internal/ir/hir/lower/module_lower.go +++ b/internal/ir/hir/lower/module_lower.go @@ -292,7 +292,7 @@ func lowerPlace(ctx *project.CompilerContext, module *project.Module, scope *sym }) out.Type = loweredTypeID(ctx, module, field.Type) out.Location = ast.LocOf(selector) - return out + return appendOptionalPayloadPlace(ctx, module, selector, out) } } if index, ok := expr.(*ast.IndexExpr); ok && index != nil && index.Expr != nil && index.Index != nil { @@ -307,18 +307,50 @@ func lowerPlace(ctx *project.CompilerContext, module *project.Module, scope *sym } } out := lowerPlace(ctx, module, scope, index.Expr) - out.Type = loweredTypeID(ctx, module, exprResolvedType(module, index)) + baseType := loweredRuntimeType(module, exprResolvedType(module, index.Expr), nil) + if target, _, reference := typeinfo.ReferenceTarget(typeinfo.Underlying(baseType)); reference { + baseType = target + } + array, ok := typeinfo.Underlying(baseType).(*typeinfo.ArrayType) + if !ok || array == nil || array.Elem == nil { + panic("HIR lowering: index base missing array element type") + } + out.Type = loweredTypeID(ctx, module, array.Elem) out.Location = ast.LocOf(index) out.Projections = append(out.Projections, ir.PlaceProjection{ Kind: ir.PlaceProjectionIndex, Index: indexExpr, Type: out.Type, Location: ast.LocOf(index), }) - return out + return appendOptionalPayloadPlace(ctx, module, index, out) } } - typeText := loweredTypeID(ctx, module, exprResolvedType(module, expr)) - return &ir.Place{ - Root: lowerASTExpr(ctx, module, scope, expr, nil), Type: typeText, Location: ast.LocOf(expr), + ident, ok := expr.(*ast.Ident) + if !ok || ident == nil { + typeID := loweredTypeID(ctx, module, exprResolvedType(module, expr)) + return &ir.Place{Root: lowerASTExpr(ctx, module, scope, expr, nil), Type: typeID, Location: ast.LocOf(expr)} + } + root := lowerIdentExpr(ctx, module, scope, ident, ir.InvalidType) + out := &ir.Place{ + Root: root, Type: root.TypeID(), Location: ast.LocOf(expr), + } + return appendOptionalPayloadPlace(ctx, module, expr, out) +} + +func appendOptionalPayloadPlace(ctx *project.CompilerContext, module *project.Module, expr ast.Expr, out *ir.Place) *ir.Place { + if ctx == nil || module == nil || module.Flow == nil || expr == nil || out == nil { + return out + } + payload := module.Flow.Payloads[expr.ID()] + for range payload.Depth { + optional, ok := ctx.Types.Type(out.Type) + if !ok || optional.Kind != ir.TypeOptional || optional.Elem == ir.InvalidType { + break + } + out.Type = optional.Elem + out.Projections = append(out.Projections, ir.PlaceProjection{ + Kind: ir.PlaceProjectionOptionalPayload, Type: out.Type, Location: ast.LocOf(expr), + }) } + return out } func lowerReferenceValue(ctx *project.CompilerContext, module *project.Module, scope *symbols.Scope, expr ast.Expr, resultType typeinfo.Type, typeID ir.TypeID) ir.Expr { @@ -409,6 +441,22 @@ func lowerASTExpr(ctx *project.CompilerContext, module *project.Module, scope *s if resolvedType != nil { resolvedTypeID = loweredTypeID(ctx, module, resolvedType) } + if module != nil && module.Flow != nil { + if test, ok := module.Flow.OptionalTests[expr.ID()]; ok { + subject, _ := module.TypedASTNodes[test.SubjectID].(ast.Expr) + present := &ir.OptionalPresent{ + Value: lowerASTExpr(ctx, module, scope, subject, nil), + Type: loweredTypeID(ctx, module, &typeinfo.BoolType{}), + } + if test.PresentWhenTrue { + return present + } + return &ir.Unary{Op: "!", Arg: present, Type: present.Type} + } + if payload := module.Flow.Payloads[expr.ID()]; payload.Depth > 0 && place.IsPlaceExpr(expr) { + return &ir.Load{Place: lowerPlace(ctx, module, scope, expr)} + } + } if innerExpected := optionalSomeInnerType(module, expectedType, resolvedType, expr); innerExpected != nil { return &ir.OptionalSome{ Value: lowerASTExpr(ctx, module, scope, expr, innerExpected), @@ -462,27 +510,7 @@ func lowerASTExpr(ctx *project.CompilerContext, module *project.Module, scope *s return &ir.InvalidExpr{Message: "`none` requires optional context", Type: ir.InvalidType, SourceInfo: ir.SourceInfo{Location: loc}} case *ast.Ident: - var sym *symbols.Symbol - var ok bool - if module != nil && module.Semantics != nil { - sym = module.Semantics.ResolvedSymbols[node.ID()] - ok = sym != nil - } - if !ok { - sym, ok = scope.Lookup(node.Name) - } - if !ok || sym == nil { - return &ir.InvalidExpr{Message: "unresolved identifier: " + node.Name, Type: ir.InvalidType, SourceInfo: ir.SourceInfo{Location: loc}} - } - t := resolvedTypeID - if t == ir.InvalidType { - if symType, ok := symbols.GetSymbolType(sym); ok { - t = loweredTypeID(ctx, module, symType) - } else { - t = ir.InvalidType - } - } - return &ir.Ident{Name: symbolName(module, sym), Type: t, SymbolID: sym.ID, SourceInfo: ir.SourceInfo{Location: loc}} + return lowerIdentExpr(ctx, module, scope, node, resolvedTypeID) case *ast.ScopeResolution: var sym *symbols.Symbol @@ -555,25 +583,9 @@ func lowerASTExpr(ctx *project.CompilerContext, module *project.Module, scope *s if rightExpected == nil { rightExpected = rightType } - if _, ok := node.Left.(*ast.NoneLit); ok && rightExpected != nil { - leftExpected = rightExpected - } - if _, ok := node.Right.(*ast.NoneLit); ok && leftExpected != nil { - rightExpected = leftExpected - } - } - var left, right ir.Expr - if _, none := node.Left.(*ast.NoneLit); none { - right = lowerASTExpr(ctx, module, scope, node.Right, rightExpected) - left = lowerOptionalNone(ctx, right.TypeID(), ast.LocOf(node.Left)) - } else { - left = lowerASTExpr(ctx, module, scope, node.Left, leftExpected) - } - if _, none := node.Right.(*ast.NoneLit); none { - right = lowerOptionalNone(ctx, left.TypeID(), ast.LocOf(node.Right)) - } else { - right = lowerASTExpr(ctx, module, scope, node.Right, rightExpected) } + left := lowerASTExpr(ctx, module, scope, node.Left, leftExpected) + right := lowerASTExpr(ctx, module, scope, node.Right, rightExpected) t := resolvedTypeID if t == ir.InvalidType { t = left.TypeID() @@ -726,15 +738,16 @@ func optionalSomeInnerType(module *project.Module, expectedType, resolvedType ty if !ok || expected == nil || expected.Inner == nil { return nil } - // Typechecker accepts T in ?T contexts. HIR must keep the source expr at - // type T and add the optional container explicitly so MIR/LLVM can choose - // tagged or niche ABI later. - switch loweredRuntimeType(module, resolvedType, nil).(type) { - case *typeinfo.OptionalType, *typeinfo.NoneType: + resolved := loweredRuntimeType(module, resolvedType, nil) + if typeinfo.SameType(expected, resolved) { return nil - default: + } + // Typechecker accepts one-layer promotion into optional contexts. HIR keeps + // source carrier intact and materializes tagged outer container explicitly. + if typeinfo.SameType(expected.Inner, resolved) { return expected.Inner } + return nil } func lowerSelectorMethodCall(ctx *project.CompilerContext, module *project.Module, scope *symbols.Scope, selector *ast.SelectorExpr, call *ast.CallExpr) ir.Expr { @@ -766,7 +779,7 @@ func lowerSelectorMethodCall(ctx *project.CompilerContext, module *project.Modul } } methodSym := module.Semantics.ResolvedSymbols[selector.Name.ID()] - fnType, _ := module.Semantics.ExprTypes[selector.ID()].(*typeinfo.FuncType) + fnType, _ := exprResolvedType(module, selector).(*typeinfo.FuncType) if methodSym == nil || fnType == nil || len(fnType.Params) == 0 { return &ir.InvalidExpr{Message: "unsupported selector call lowering", Type: ir.InvalidType} } @@ -971,10 +984,35 @@ func lowerAllocCall(ctx *project.CompilerContext, module *project.Module, scope } func exprResolvedType(module *project.Module, expr ast.Expr) typeinfo.Type { - if module == nil || module.Semantics == nil || expr == nil { + if module == nil || expr == nil { return nil } - return module.Semantics.ExprTypes[expr.ID()] + return module.EffectiveExprType(expr.ID()) +} + +func lowerIdentExpr(ctx *project.CompilerContext, module *project.Module, scope *symbols.Scope, node *ast.Ident, typeID ir.TypeID) ir.Expr { + if node == nil { + return &ir.InvalidExpr{Message: "nil identifier", Type: ir.InvalidType} + } + var sym *symbols.Symbol + if module != nil && module.Semantics != nil { + sym = module.Semantics.ResolvedSymbols[node.ID()] + } + if sym == nil && scope != nil { + sym, _ = scope.Lookup(node.Name) + } + if sym == nil { + return &ir.InvalidExpr{Message: "unresolved identifier: " + node.Name, Type: ir.InvalidType, SourceInfo: ir.SourceInfo{Location: ast.LocOf(node)}} + } + if typeID == ir.InvalidType { + if symType, ok := symbols.GetSymbolType(sym); ok { + typeID = loweredTypeID(ctx, module, symType) + } + } + return &ir.Ident{ + Name: symbolName(module, sym), Type: typeID, SymbolID: sym.ID, + SourceInfo: ir.SourceInfo{Location: ast.LocOf(node)}, + } } func lowerNumberLit(ctx *project.CompilerContext, module *project.Module, node *ast.NumberLit, expectedType typeinfo.Type, loc *source.Location) ir.Expr { diff --git a/internal/ir/hir/lower/module_lower_test.go b/internal/ir/hir/lower/module_lower_test.go index a1cbf02..f84e5c1 100644 --- a/internal/ir/hir/lower/module_lower_test.go +++ b/internal/ir/hir/lower/module_lower_test.go @@ -10,6 +10,7 @@ import ( "compiler/internal/frontend/lexer" "compiler/internal/frontend/parser" "compiler/internal/ir" + "compiler/internal/ir/cfg" "compiler/internal/ir/hir" "compiler/internal/project" "compiler/internal/semantics/binder" @@ -39,6 +40,9 @@ func generateTestHIR(t *testing.T, filePath, importPath, src string, beforeLower binder.Bind(ctx, module) resolver.Resolve(ctx, module) typechecker.Check(ctx, module) + module.TypedASTNodes = ast.Index(module.AST) + module.CFG = cfg.BuildModule(module.AST) + module.Flow = typechecker.CheckFlow(ctx, module) if diag.HasErrors() { t.Fatalf("unexpected diagnostics:\n%s", diag.EmitAllToString()) } @@ -123,6 +127,77 @@ func TestGenerateHIRLowersIndexExpr(t *testing.T) { } } +func TestGenerateHIRLowersOptionalFlowEvidence(t *testing.T) { + out := generateTestHIR(t, "hir_optional_flow_test"+peeper.SourceExt, "hir_optional_flow_test", `fn read(value: ?i32) -> i32 { + if value != none { + return value; + } + return 0; +}`) + branch, ok := out.Funcs[0].Body.Stmts[0].(*hir.If) + if !ok { + t.Fatalf("first statement = %T, want If", out.Funcs[0].Body.Stmts[0]) + } + present, ok := branch.Cond.(*ir.OptionalPresent) + if !ok || out.Types.Text(present.Value.TypeID()) != "?i32" || out.Types.Text(present.TypeID()) != "bool" { + t.Fatalf("condition = %#v, want OptionalPresent(?i32) -> bool", branch.Cond) + } + ret, ok := branch.Then.Stmts[0].(*hir.Return) + if !ok { + t.Fatalf("then statement = %T, want Return", branch.Then.Stmts[0]) + } + load, ok := ret.Value.(*ir.Load) + if !ok || load.Place == nil || len(load.Place.Projections) != 1 || + load.Place.Projections[0].Kind != ir.PlaceProjectionOptionalPayload || out.Types.Text(load.TypeID()) != "i32" { + t.Fatalf("proven value = %#v, want i32 optional payload load", ret.Value) + } +} + +func TestGenerateHIRKeepsOptionalIndexCarrierBeforePayloadProjection(t *testing.T) { + out := generateTestHIR(t, "hir_optional_index_flow_test"+peeper.SourceExt, "hir_optional_index_flow_test", `fn read(values: [1]?i32) -> i32 { + if values[0] == none { + return 0; + } + return values[0]; +}`) + ret, ok := out.Funcs[0].Body.Stmts[1].(*hir.Return) + if !ok { + t.Fatalf("second statement = %T, want Return", out.Funcs[0].Body.Stmts[1]) + } + load, ok := ret.Value.(*ir.Load) + if !ok || load.Place == nil || len(load.Place.Projections) != 2 { + t.Fatalf("proven index = %#v, want index then optional payload load", ret.Value) + } + index := load.Place.Projections[0] + payload := load.Place.Projections[1] + if index.Kind != ir.PlaceProjectionIndex || out.Types.Text(index.Type) != "?i32" || + payload.Kind != ir.PlaceProjectionOptionalPayload || out.Types.Text(payload.Type) != "i32" { + t.Fatalf("projections = %#v, want index:?i32 then optional-payload:i32", load.Place.Projections) + } +} + +func TestGenerateHIRPreservesExplicitOptionalCarrierInsideProof(t *testing.T) { + out := generateTestHIR(t, "hir_optional_carrier_test"+peeper.SourceExt, "hir_optional_carrier_test", `fn keep(value: ?i32) -> ?i32 { + if value != none { + let carrier: ?i32 = value; + return carrier; + } + return none; +}`) + branch, ok := out.Funcs[0].Body.Stmts[0].(*hir.If) + if !ok || branch.Then == nil || len(branch.Then.Stmts) == 0 { + t.Fatalf("first statement = %#v, want populated If", out.Funcs[0].Body.Stmts[0]) + } + binding, ok := branch.Then.Stmts[0].(*hir.Binding) + if !ok { + t.Fatalf("then statement = %T, want Binding", branch.Then.Stmts[0]) + } + ident, ok := binding.Value.(*ir.Ident) + if !ok || out.Types.Text(ident.TypeID()) != "?i32" { + t.Fatalf("explicit carrier value = %#v, want ?i32 Ident", binding.Value) + } +} + func TestGenerateHIRPreservesSourceAndSymbolIdentity(t *testing.T) { out := generateTestHIR(t, "hir_identity_test"+peeper.SourceExt, "hir_identity_test", `fn echo(value: i32) -> i32 { let copy = value; diff --git a/internal/ir/mir/model.go b/internal/ir/mir/model.go index ae19910..58a32b9 100644 --- a/internal/ir/mir/model.go +++ b/internal/ir/mir/model.go @@ -179,6 +179,7 @@ const ( PlaceProjectionDeref PlaceProjectionKind = iota PlaceProjectionField PlaceProjectionIndex + PlaceProjectionOptionalPayload ) type PlaceProjection struct { @@ -282,6 +283,12 @@ type OptionalSome struct { Location *source.Location } +type OptionalPresent struct { + Value ValueRef + Type ir.TypeID + Location *source.Location +} + type InterfaceMake struct { Value ValueRef DataType ir.TypeID @@ -346,6 +353,7 @@ func (*DynamicArrayAlloc) valueExprNode() {} func (*Alloc) valueExprNode() {} func (*ZeroValue) valueExprNode() {} func (*OptionalSome) valueExprNode() {} +func (*OptionalPresent) valueExprNode() {} func (*InterfaceMake) valueExprNode() {} func (*InterfaceCall) valueExprNode() {} func (*StringLiteral) valueExprNode() {} @@ -379,6 +387,7 @@ func (v *DynamicArrayOp) SourceLocation() *source.Location { return v.Locatio func (v *Alloc) SourceLocation() *source.Location { return v.Location } func (v *ZeroValue) SourceLocation() *source.Location { return v.Location } func (v *OptionalSome) SourceLocation() *source.Location { return v.Location } +func (v *OptionalPresent) SourceLocation() *source.Location { return v.Location } func (v *InterfaceMake) SourceLocation() *source.Location { return v.Location } func (v *InterfaceCall) SourceLocation() *source.Location { return v.Location } @@ -416,6 +425,8 @@ func (p *Place) Text() string { b.WriteString(projection.Index.Text()) } b.WriteString("]") + case PlaceProjectionOptionalPayload: + b.WriteString(".value") } } return b.String() @@ -504,6 +515,12 @@ func (v *OptionalSome) Text() string { } return "some(" + v.Value.Text() + ")" } +func (v *OptionalPresent) Text() string { + if v == nil || v.Value == nil { + return "present()" + } + return "present(" + v.Value.Text() + ")" +} func (v *InterfaceMake) Text() string { if v == nil { diff --git a/internal/ir/mir/module_lower.go b/internal/ir/mir/module_lower.go index f6e6b3c..8c9ef38 100644 --- a/internal/ir/mir/module_lower.go +++ b/internal/ir/mir/module_lower.go @@ -438,6 +438,8 @@ func (l *lowerer) lowerPlace(place *ir.Place, out *[]Instr) *Place { case ir.PlaceProjectionIndex: lowered.Kind = PlaceProjectionIndex lowered.Index = l.lowerExpr(projection.Index, out) + case ir.PlaceProjectionOptionalPayload: + lowered.Kind = PlaceProjectionOptionalPayload default: panic(fmt.Sprintf("unsupported HIR place projection %d", projection.Kind)) } @@ -502,6 +504,11 @@ func (l *lowerer) lowerExpr(expr ir.Expr, out *[]Instr) ValueRef { name := l.nextTemp() l.appendInstr(out, &Assign{Name: name, Value: &OptionalSome{Value: value, Type: e.TypeID(), Location: e.Origin().Location}}) return &RefName{Name: name, Type: e.TypeID(), Location: e.Origin().Location} + case *ir.OptionalPresent: + value := l.lowerExpr(e.Value, out) + name := l.nextTemp() + l.appendInstr(out, &Assign{Name: name, Value: &OptionalPresent{Value: value, Type: e.TypeID(), Location: e.Origin().Location}}) + return &RefName{Name: name, Type: e.TypeID(), Location: e.Origin().Location} case *ir.Ident: return &RefName{Name: e.Name, Type: e.TypeID(), Location: e.Origin().Location} case *ir.Unary: diff --git a/internal/ir/mir/module_lower_test.go b/internal/ir/mir/module_lower_test.go index f0fc433..b6e9d83 100644 --- a/internal/ir/mir/module_lower_test.go +++ b/internal/ir/mir/module_lower_test.go @@ -503,6 +503,48 @@ func TestGenerateMIRLowersOptionalSome(t *testing.T) { } } +func TestGenerateMIRLowersOptionalFlowOperations(t *testing.T) { + payloadPlace := &ir.Place{ + Root: &ir.Ident{Name: "value", Type: mirTypes.optionalI32}, + Projections: []ir.PlaceProjection{ + {Kind: ir.PlaceProjectionOptionalPayload, Type: mirTypes.i32}, + }, + Type: mirTypes.i32, + } + mod := &hir.Module{ + Name: "test", Types: mirTypes.table, + Funcs: []*hir.Function{ + { + Name: "present", Params: []ir.Param{{Name: "value", Type: mirTypes.optionalI32}}, ReturnType: mirTypes.boolType, + Body: &hir.Block{Stmts: []hir.Stmt{&hir.Return{Value: &ir.OptionalPresent{ + Value: &ir.Ident{Name: "value", Type: mirTypes.optionalI32}, Type: mirTypes.boolType, + }}}}, + }, + { + Name: "payload", Params: []ir.Param{{Name: "value", Type: mirTypes.optionalI32}}, ReturnType: mirTypes.i32, + Body: &hir.Block{Stmts: []hir.Stmt{&hir.Return{Value: &ir.Load{Place: payloadPlace}}}}, + }, + }, + } + + out := GenerateMIR(mod, cfgForHIR(mod), nil, nil, nil) + presentAssign, ok := out.Funcs[0].Blocks[0].Instrs[0].(*Assign) + if !ok { + t.Fatalf("presence instruction = %T, want Assign", out.Funcs[0].Blocks[0].Instrs[0]) + } + if _, ok := presentAssign.Value.(*OptionalPresent); !ok { + t.Fatalf("presence value = %T, want OptionalPresent", presentAssign.Value) + } + payloadAssign, ok := out.Funcs[1].Blocks[0].Instrs[0].(*Assign) + if !ok { + t.Fatalf("payload instruction = %T, want Assign", out.Funcs[1].Blocks[0].Instrs[0]) + } + load, ok := payloadAssign.Value.(*Load) + if !ok || load.Place == nil || len(load.Place.Projections) != 1 || load.Place.Projections[0].Kind != PlaceProjectionOptionalPayload { + t.Fatalf("payload value = %#v, want optional payload place load", payloadAssign.Value) + } +} + func TestGenerateMIRLowersProjectedRawAddressDirectly(t *testing.T) { mod := &hir.Module{ Name: "test", Types: mirTypes.table, diff --git a/internal/ir/nodes.go b/internal/ir/nodes.go index b3d40eb..1cc0532 100644 --- a/internal/ir/nodes.go +++ b/internal/ir/nodes.go @@ -85,6 +85,12 @@ type OptionalSome struct { Type TypeID } +type OptionalPresent struct { + SourceInfo + Value Expr + Type TypeID +} + type Ident struct { SourceInfo Name string @@ -120,6 +126,7 @@ const ( PlaceProjectionDeref PlaceProjectionKind = iota PlaceProjectionField PlaceProjectionIndex + PlaceProjectionOptionalPayload ) type PlaceProjection struct { @@ -268,6 +275,7 @@ var ( _ Expr = (*BoolLit)(nil) _ Expr = (*ZeroValue)(nil) _ Expr = (*OptionalSome)(nil) + _ Expr = (*OptionalPresent)(nil) _ Expr = (*Ident)(nil) _ Expr = (*Unary)(nil) _ Expr = (*Binary)(nil) @@ -290,25 +298,27 @@ var ( _ Expr = (*Drop)(nil) ) -func (*InvalidExpr) exprNode() {} -func (*InvalidExpr) forEachChild(func(Expr)) {} -func (*IntLit) exprNode() {} -func (*IntLit) forEachChild(func(Expr)) {} -func (*FloatLit) exprNode() {} -func (*FloatLit) forEachChild(func(Expr)) {} -func (*StringLit) exprNode() {} -func (*StringLit) forEachChild(func(Expr)) {} -func (*BoolLit) exprNode() {} -func (*BoolLit) forEachChild(func(Expr)) {} -func (*ZeroValue) exprNode() {} -func (*ZeroValue) forEachChild(func(Expr)) {} -func (*OptionalSome) exprNode() {} -func (e *OptionalSome) forEachChild(visit func(Expr)) { visit(e.Value) } -func (*Ident) exprNode() {} -func (*Ident) forEachChild(func(Expr)) {} -func (*Unary) exprNode() {} -func (e *Unary) forEachChild(visit func(Expr)) { visit(e.Arg) } -func (*Binary) exprNode() {} +func (*InvalidExpr) exprNode() {} +func (*InvalidExpr) forEachChild(func(Expr)) {} +func (*IntLit) exprNode() {} +func (*IntLit) forEachChild(func(Expr)) {} +func (*FloatLit) exprNode() {} +func (*FloatLit) forEachChild(func(Expr)) {} +func (*StringLit) exprNode() {} +func (*StringLit) forEachChild(func(Expr)) {} +func (*BoolLit) exprNode() {} +func (*BoolLit) forEachChild(func(Expr)) {} +func (*ZeroValue) exprNode() {} +func (*ZeroValue) forEachChild(func(Expr)) {} +func (*OptionalSome) exprNode() {} +func (e *OptionalSome) forEachChild(visit func(Expr)) { visit(e.Value) } +func (*OptionalPresent) exprNode() {} +func (e *OptionalPresent) forEachChild(visit func(Expr)) { visit(e.Value) } +func (*Ident) exprNode() {} +func (*Ident) forEachChild(func(Expr)) {} +func (*Unary) exprNode() {} +func (e *Unary) forEachChild(visit func(Expr)) { visit(e.Arg) } +func (*Binary) exprNode() {} func (e *Binary) forEachChild(visit func(Expr)) { visit(e.Left) visit(e.Right) @@ -486,6 +496,18 @@ func (e *OptionalSome) TypeID() TypeID { } return e.Type } +func (e *OptionalPresent) String() string { + if e == nil || e.Value == nil { + return "present()" + } + return "present(" + e.Value.String() + ")" +} +func (e *OptionalPresent) TypeID() TypeID { + if e == nil { + return InvalidType + } + return e.Type +} func (e *Ident) String() string { return e.Name } func (e *Ident) TypeID() TypeID { if e == nil { @@ -583,6 +605,8 @@ func (p *Place) String() string { b.WriteString(projection.Index.String()) } b.WriteString("]") + case PlaceProjectionOptionalPayload: + b.WriteString(".value") } } return b.String() diff --git a/internal/lsp/cursor.go b/internal/lsp/cursor.go index 3e13045..ae950d6 100644 --- a/internal/lsp/cursor.go +++ b/internal/lsp/cursor.go @@ -204,7 +204,7 @@ func selectorBaseType(expr ast.Expr, parents map[ast.NodeID]ast.Node, module *pr if expr == nil || module == nil || module.Semantics == nil { return nil, false } - baseType, ok := normalizedSelectorBaseType(module.Semantics.ExprTypes[expr.ID()]) + baseType, ok := normalizedSelectorBaseType(module.EffectiveExprType(expr.ID())) if ok { return baseType, true } diff --git a/internal/lsp/hover.go b/internal/lsp/hover.go index ed8fccf..32bf5be 100644 --- a/internal/lsp/hover.go +++ b/internal/lsp/hover.go @@ -319,12 +319,13 @@ func resolveSelectorHoverSubject(cc *cursorContext) *hoverSubject { Decl: documentedDeclAncestor(ident, cc.parents), Location: ast.LocOf(ident), Symbol: sym, + ExprType: cc.module.EffectiveExprType(sel.ID()), } } if subject := resolveInterfaceSelectorMethodHoverSubject(cc, sel, ident); subject != nil { return subject } - if exprType, ok := cc.module.Semantics.ExprTypes[sel.ID()]; ok { + if exprType := cc.module.EffectiveExprType(sel.ID()); exprType != nil { return &hoverSubject{ Kind: hoverSubjectExpr, Node: ident, @@ -380,6 +381,7 @@ func resolveSymbolHoverSubject(cc *cursorContext) *hoverSubject { Decl: documentedDeclAncestor(ident, cc.parents), Location: ast.LocOf(ident), Symbol: sym, + ExprType: cc.module.EffectiveExprType(ident.ID()), } if sym.Kind == symbols.SymbolType { if typ, ok := symbols.GetSymbolType(sym); ok { @@ -492,8 +494,8 @@ func resolveExprHoverSubject(cc *cursorContext) *hoverSubject { if _, ok := cc.node.(ast.Expr); !ok { return nil } - exprType, ok := cc.module.Semantics.ExprTypes[cc.node.ID()] - if !ok { + exprType := cc.module.EffectiveExprType(cc.node.ID()) + if exprType == nil { return nil } return &hoverSubject{ @@ -515,7 +517,7 @@ func renderHoverSubject(subject *hoverSubject) string { if subject.Symbol == nil { return "" } - text = renderSymbol(subject.Symbol, symbolRenderContext{Declaration: subject.Decl}) + text = renderSymbol(subject.Symbol, symbolRenderContext{Type: subject.ExprType, Declaration: subject.Decl}) if typ, ok := symbols.GetSymbolType(subject.Symbol); ok && typ != nil { if subject.Symbol.Kind == symbols.SymbolType { text += renderTypeDetails(typ, subject.MethodSymbols) diff --git a/internal/lsp/server_test.go b/internal/lsp/server_test.go index fe05070..a68cff8 100644 --- a/internal/lsp/server_test.go +++ b/internal/lsp/server_test.go @@ -13,6 +13,7 @@ import ( "testing" "time" + "compiler/internal/diagnostics" "compiler/internal/driver" "compiler/internal/project" "compiler/internal/semantics/symbols" @@ -1285,6 +1286,106 @@ func TestHoverShowsBinaryExpressionType(t *testing.T) { } } +func TestHoverShowsFlowRefinedOptionalUseType(t *testing.T) { + root := t.TempDir() + mainPath := filepath.Join(root, "main"+peeper.SourceExt) + state := NewServerState() + state.RootDir = root + + outside := hoverAtSource(t, state, mainPath, `fn read(value: ?i32) -> i32 { + let carrier: ?i32 = __CURSOR__value; + if value == none { + return 0; + } + return value; +}`) + if outside == nil || !strings.Contains(outside.Contents.Value, "(param) value: ?i32") { + t.Fatalf("outside-proof hover = %#v, want ?i32", outside) + } + + inside := hoverAtSource(t, state, mainPath, `fn read(value: ?i32) -> i32 { + let carrier: ?i32 = value; + if value == none { + return 0; + } + return __CURSOR__value; +}`) + if inside == nil || !strings.Contains(inside.Contents.Value, "(param) value: i32") { + t.Fatalf("inside-proof hover = %#v, want i32", inside) + } +} + +func TestLSPRefreshesOptionalFlowDiagnosticsAfterEdit(t *testing.T) { + tests := []struct { + name string + code string + invalid string + valid string + }{ + { + name: "missing proof", + code: diagnostics.ErrOptionalPayloadProof, + invalid: `fn read(value: ?i32) -> i32 { + return value; +}`, + valid: `fn read(value: ?i32) -> i32 { + if value == none { + return 0; + } + return value; +}`, + }, + { + name: "unstable index", + code: diagnostics.ErrUnstableNarrowing, + invalid: `fn read(values: [2]?i32, index: usize) -> i32 { + if values[index + 1] == none { + return 0; + } + return values[index + 1]; +}`, + valid: `fn read(values: [2]?i32, index: usize) -> i32 { + if values[index] == none { + return 0; + } + return values[index]; +}`, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + root := t.TempDir() + filePath := filepath.Join(root, "main"+peeper.SourceExt) + state := NewServerState() + state.RootDir = root + version := 1 + state.applyDocumentSnapshot(filePath, &test.invalid, &version) + params := publishCurrentDiagnostics(t, state, filePath) + found := false + for _, diagnostic := range params.Diagnostics { + if diagnostic.Code == test.code { + found = true + break + } + } + if !found { + t.Fatalf("version %d diagnostics = %#v, want %s", version, params.Diagnostics, test.code) + } + + version++ + state.applyDocumentSnapshot(filePath, &test.valid, &version) + params = publishCurrentDiagnostics(t, state, filePath) + if params.Version == nil || *params.Version != version { + t.Fatalf("recovered diagnostic version = %v, want %d", params.Version, version) + } + if hasErrorDiagnostic(params.Diagnostics) { + t.Fatalf("optional flow diagnostics remained after edit: %#v", params.Diagnostics) + } + }) + } +} + func TestHoverShowsDeclarationNodeSignature(t *testing.T) { root := t.TempDir() mainPath := filepath.Join(root, "main"+peeper.SourceExt) diff --git a/internal/phase/phase.go b/internal/phase/phase.go index d4f1f70..89ba27d 100644 --- a/internal/phase/phase.go +++ b/internal/phase/phase.go @@ -21,6 +21,8 @@ const ( Typechecked // CFG includes finalized topology and CFG diagnostics. CFG + // FlowTyped includes CFG-refined expression types and place origins. + FlowTyped // DefiniteInit records completion of diagnostic-only initialization checks. DefiniteInit // Ownership includes ownership cleanup results. @@ -56,6 +58,8 @@ func (phase Phase) String() string { return "typechecked" case CFG: return "CFG" + case FlowTyped: + return "flow-typed" case DefiniteInit: return "definite-init" case Ownership: diff --git a/internal/phase/phase_test.go b/internal/phase/phase_test.go index c333536..795cc5c 100644 --- a/internal/phase/phase_test.go +++ b/internal/phase/phase_test.go @@ -5,7 +5,7 @@ import "testing" func TestPhaseString(t *testing.T) { for phase, want := range map[Phase]string{ None: "none", Setup: "setup", Load: "load", Parsed: "parsed", - Typechecked: "typechecked", CFG: "CFG", DefiniteInit: "definite-init", + Typechecked: "typechecked", CFG: "CFG", FlowTyped: "flow-typed", DefiniteInit: "definite-init", Ownership: "ownership", Usage: "usage", HIR: "HIR", MIR: "MIR", Backend: "backend", Finalize: "finalize", } { diff --git a/internal/pipeline/pipeline.go b/internal/pipeline/pipeline.go index 1b4965f..c5064a5 100644 --- a/internal/pipeline/pipeline.go +++ b/internal/pipeline/pipeline.go @@ -322,6 +322,8 @@ func nextModulePhase(current phase.Phase) phase.Phase { case phase.Typechecked: return phase.CFG case phase.CFG: + return phase.FlowTyped + case phase.FlowTyped: return phase.DefiniteInit case phase.DefiniteInit: return phase.Ownership @@ -352,8 +354,10 @@ func importPrerequisitePhase(next phase.Phase) phase.Phase { return phase.Collected case phase.CFG: return phase.Typechecked - case phase.DefiniteInit: + case phase.FlowTyped: return phase.CFG + case phase.DefiniteInit: + return phase.FlowTyped case phase.Ownership: return phase.DefiniteInit case phase.Usage: @@ -438,6 +442,12 @@ func advanceModulePhase(ctx *project.CompilerContext, module *project.Module, di if module.CFG == nil { return false } + if module.Phase < phase.FlowTyped { + module.Flow = typechecker.CheckFlow(phaseCtx, module) + module.Phase = phase.FlowTyped + ctx.Metrics.AddPhaseAdvance() + return true + } if module.Phase < phase.DefiniteInit { definiteinit.Check( module.CFG, diff --git a/internal/pipeline/pipeline_test.go b/internal/pipeline/pipeline_test.go index 92357e4..da6db66 100644 --- a/internal/pipeline/pipeline_test.go +++ b/internal/pipeline/pipeline_test.go @@ -552,6 +552,7 @@ func TestPipelineAdvanceModulePhaseRunsOnePhaseAtATime(t *testing.T) { phase.ConstEval, phase.Typechecked, phase.CFG, + phase.FlowTyped, phase.DefiniteInit, phase.Ownership, } @@ -565,6 +566,9 @@ func TestPipelineAdvanceModulePhaseRunsOnePhaseAtATime(t *testing.T) { if wantPhase == phase.CFG && (entry.CFG == nil || len(entry.CFG.Functions) == 0) { t.Fatal("CFG phase must retain canonical graph") } + if wantPhase == phase.FlowTyped && entry.Flow == nil { + t.Fatal("flow-typed phase must retain canonical result") + } if wantPhase < phase.HIR && entry.HIR != nil { t.Fatalf("phase %v produced HIR before mandatory semantics completed", wantPhase) } @@ -2002,3 +2006,400 @@ fn main() -> i32 { }) } } + +func TestPipelineAcceptsOptionalNarrowingAcrossCFGAndStablePlaces(t *testing.T) { + tests := []struct { + name string + src string + }{ + { + name: "polarity and reversed operands", + src: `fn direct(value: ?i32) -> i32 { + if value != none { + return value; + } + return 0; +} + +fn reverseEq(value: ?i32) -> i32 { + if none == value { + return 0; + } + return value; +} + +fn reverseNe(value: ?i32) -> i32 { + if none != value { + return value; + } + return 0; +}`, + }, + { + name: "terminating guard and inferred payload", + src: `fn guarded(value: ?i32) -> i32 { + if value == none { + return 0; + } + let payload = value; + return payload; +}`, + }, + { + name: "field and stable indexes", + src: `struct Holder { + field: ?i32, + items: [2]?i32 +} + +struct Outer { + inner: Holder +} + +fn fields(outer: Outer, holder: Holder, index: usize) -> i32 { + if outer.inner.field != none { + return outer.inner.field; + } + if holder.field != none { + return holder.field; + } + if holder.items[0] != none { + return holder.items[0]; + } + if holder.items[index] != none { + return holder.items[index]; + } + return 0; +}`, + }, + { + name: "nested optional proofs", + src: `fn nested(value: ? ?i32) -> i32 { + if value != none { + if value != none { + return value; + } + } + return 0; +}`, + }, + { + name: "nested inferred carrier and shadowed identity", + src: `fn inferred(value: ? ?i32) -> i32 { + if value == none { + return 0; + } + let inner = value; + if inner == none { + return 0; + } + return inner; +} + +fn shadowed(value: ?i32) -> i32 { + if value == none { + return 0; + } + { + let value: ?i32 = none; + if value != none { + return value; + } + } + return value; +}`, + }, + { + name: "join loop and eager result facts", + src: `fn joined(value: ?i32, choose: bool) -> i32 { + if choose { + if value == none { + return 0; + } + } else { + if value == none { + return 0; + } + } + return value; +} + +fn looped(value: ?i32) -> i32 { + for value != none { + return value; + } + return 0; +} + +fn eager(value: ?i32) -> i32 { + if value != none && true { + return value; + } + if value == none || false { + return 0; + } + return value; +}`, + }, + { + name: "payload descendant and disjoint mutation", + src: `struct Payload { + value: i32 +} + +struct Holder { + maybe: ?i32, + other: i32 +} + +fn Write(_: &mut i32) {} + +fn descendant(mut value: ?Payload) -> i32 { + if value == none { + return 0; + } + value.value = 7; + return value.value; +} + +fn disjoint(mut holder: Holder) -> i32 { + if holder.maybe == none { + return 0; + } + holder.other = 1; + Write(&mut holder.other); + return holder.maybe; +}`, + }, + { + name: "eager call ordering preserves fresh and disjoint proofs", + src: `struct Holder { + maybe: ?i32, + other: i32 +} + +fn Mutate(_: &mut Holder) -> bool { return true; } + +fn Touch(_: &mut i32) -> bool { return true; } + +fn fresh(holder: &mut Holder) -> i32 { + if Mutate(holder) && holder.maybe != none { + return holder.maybe; + } + return 0; +} + +fn disjoint(holder: &mut Holder) -> i32 { + if holder.maybe != none && Touch(&mut holder.other) { + return holder.maybe; + } + return 0; +}`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + diag := buildPipelineTestWithConfig(t, project.Config{RootDir: ".", Extension: peeper.SourceExt}, "", tt.src) + if diag.HasErrors() { + t.Fatalf("optional narrowing failed:\n%s", diag.EmitAllToString()) + } + }) + } +} + +func TestPipelineRejectsInvalidOptionalPayloadAccess(t *testing.T) { + tests := []struct { + name string + code string + src string + }{ + { + name: "missing proof", + code: "T0041", + src: `fn invalid(value: ?i32) -> i32 { return value; }`, + }, + { + name: "computed index", + code: "T0042", + src: `fn invalid(values: [2]?i32, index: usize) -> i32 { + if values[index + 1] != none { + return values[index + 1]; + } + return 0; +}`, + }, + { + name: "index dependency invalidated", + code: "T0041", + src: `fn invalid(values: [2]?i32) -> i32 { + let mut index: usize = 0; + if values[index] != none { + index = 1; + return values[index]; + } + return 0; +}`, + }, + { + name: "exact carrier assignment invalidated", + code: "T0041", + src: `fn invalid(mut value: ?i32) -> i32 { + if value == none { + return 0; + } + value = 1; + return value; +}`, + }, + { + name: "ancestor assignment invalidated", + code: "T0041", + src: `struct Holder { + field: ?i32 +} + +fn invalid(mut holder: Holder) -> i32 { + if holder.field == none { + return 0; + } + holder = .Holder{field = 1}; + return holder.field; +}`, + }, + { + name: "nested ancestor assignment invalidated", + code: "T0041", + src: `struct Holder { + field: ?i32 +} + +struct Outer { + inner: Holder +} + +fn invalid(mut outer: Outer) -> i32 { + if outer.inner.field == none { + return 0; + } + outer.inner = .Holder{field = 1}; + return outer.inner.field; +}`, + }, + { + name: "mutable reference call invalidated", + code: "T0041", + src: `struct Holder { + field: ?i32 +} + +fn Write(_: &mut Holder) {} + +fn invalid(holder: &mut Holder) -> i32 { + if holder.field == none { + return 0; + } + Write(holder); + return holder.field; +}`, + }, + { + name: "mutable reference call invalidates later argument", + code: "T0041", + src: `struct Holder { + field: ?i32 +} + +fn Write(_: &mut Holder) -> i32 { return 0; } + +fn Use(_: i32, _: i32) {} + +fn invalid(holder: &mut Holder) { + if holder.field == none { + return; + } + Use(Write(holder), holder.field); +}`, + }, + { + name: "known raw pointer call invalidated", + code: "T0041", + src: `fn Touch(_: rawptr) {} + +fn invalid(mut value: ?i32) -> i32 { + if value == none { + return 0; + } + Touch(@value); + return value; +}`, + }, + { + name: "unknown raw pointer call invalidated", + code: "T0041", + src: `fn Touch(_: rawptr) {} + +fn invalid(value: ?i32, pointer: rawptr) -> i32 { + if value == none { + return 0; + } + Touch(pointer); + return value; +}`, + }, + { + name: "optional reference carrier assignment invalidated", + code: "T0041", + src: `fn Read(_: &i32) {} + +fn invalid(value: i32) { + let mut maybe: ?&i32 = &value; + if maybe == none { + return; + } + maybe = none; + Read(maybe); +}`, + }, + { + name: "eager right operand has no proof", + code: "T0041", + src: `fn invalid(value: ?i32) -> bool { + return value != none && value > 0; +}`, + }, + { + name: "eager later call invalidates result proof", + code: "T0041", + src: `struct Holder { + maybe: ?i32 +} + +fn Mutate(_: &mut Holder) -> bool { return true; } + +fn invalid(holder: &mut Holder) -> i32 { + if holder.maybe != none && Mutate(holder) { + return holder.maybe; + } + return 0; +}`, + }, + { + name: "unreachable payload use still checked", + code: "T0041", + src: `fn invalid(value: ?i32) -> i32 { + return 0; + return value; +}`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + diag := buildPipelineTestWithConfig(t, project.Config{RootDir: ".", Extension: peeper.SourceExt}, "", tt.src) + if !strings.Contains(diag.EmitAllToString(), tt.code) { + t.Fatalf("expected %s diagnostic, got:\n%s", tt.code, diag.EmitAllToString()) + } + }) + } +} diff --git a/internal/project/modules.go b/internal/project/modules.go index 9472bbd..0833654 100644 --- a/internal/project/modules.go +++ b/internal/project/modules.go @@ -11,6 +11,7 @@ import ( "compiler/internal/ir/hir" "compiler/internal/ir/mir" "compiler/internal/phase" + "compiler/internal/semantics/flowresult" "compiler/internal/semantics/intrinsics" "compiler/internal/semantics/ownershipresult" "compiler/internal/semantics/symbols" @@ -69,6 +70,7 @@ type Module struct { // Canonical IR slots. HIR *hir.Module CFG *cfg.Module + Flow *flowresult.Result Ownership ownershipresult.Result MIR *mir.Module LLVMIR string @@ -90,6 +92,7 @@ type SemanticInfo struct { // misclassification. ExpandedDefaultBindings map[ast.NodeID]struct{} ExprTypes map[ast.NodeID]typeinfo.Type + OptionalTests map[ast.NodeID]flowresult.OptionalTest ConstValues map[symbols.SymbolID]constvalue.Value MethodSets map[string][]*symbols.Symbol MethodSymbol map[ast.NodeID]*symbols.Symbol @@ -133,6 +136,7 @@ func NewSemanticInfo() *SemanticInfo { ResolvedSymbols: make(map[ast.NodeID]*symbols.Symbol), ExpandedDefaultBindings: make(map[ast.NodeID]struct{}), ExprTypes: make(map[ast.NodeID]typeinfo.Type), + OptionalTests: make(map[ast.NodeID]flowresult.OptionalTest), ConstValues: make(map[symbols.SymbolID]constvalue.Value), MethodSets: make(map[string][]*symbols.Symbol), MethodSymbol: make(map[ast.NodeID]*symbols.Symbol), @@ -150,6 +154,23 @@ func (m *Module) ResetSemanticData() { m.Semantics = NewSemanticInfo() } +// EffectiveExprType returns per-use flow refinement when available and falls +// back to the canonical base typechecker result. +func (m *Module) EffectiveExprType(id ast.NodeID) typeinfo.Type { + if m == nil { + return nil + } + if m.Flow != nil { + if typ := m.Flow.ExprTypes[id]; typ != nil { + return typ + } + } + if m.Semantics == nil { + return nil + } + return m.Semantics.ExprTypes[id] +} + // resetToPhase retains artifacts through phase and invalidates downstream data. func (m *Module) resetToPhase(retained phase.Phase) { if m == nil { @@ -167,6 +188,9 @@ func (m *Module) resetToPhase(retained phase.Phase) { if retained < phase.CFG { m.CFG = nil } + if retained < phase.FlowTyped { + m.Flow = nil + } if retained < phase.Ownership { m.Ownership = nil } diff --git a/internal/project/modules_test.go b/internal/project/modules_test.go index b83778f..1d0e872 100644 --- a/internal/project/modules_test.go +++ b/internal/project/modules_test.go @@ -9,8 +9,10 @@ import ( "compiler/internal/ir/hir" "compiler/internal/ir/mir" "compiler/internal/phase" + "compiler/internal/semantics/flowresult" "compiler/internal/semantics/ownershipresult" "compiler/internal/semantics/symbols" + "compiler/internal/semantics/typeinfo" ) func moduleWithArtifacts() *Module { @@ -22,6 +24,7 @@ func moduleWithArtifacts() *Module { TypedASTNodes: map[ast.NodeID]ast.Node{1: &ast.BadStmt{}}, HIR: &hir.Module{}, CFG: &cfg.Module{Functions: []*cfg.Graph{{}}}, + Flow: &flowresult.Result{ExprTypes: map[ast.NodeID]typeinfo.Type{1: typeinfo.DefaultIntegerType()}}, Ownership: ownershipresult.Result{1: &ownershipresult.CleanupPlan{}}, MIR: &mir.Module{}, LLVMIR: "stale IR", @@ -37,6 +40,7 @@ func TestModuleResetToPhaseClearsOnlyDownstreamArtifacts(t *testing.T) { astNodes bool hir bool cfg bool + flow bool ownership bool mir bool llvm bool @@ -44,12 +48,13 @@ func TestModuleResetToPhaseClearsOnlyDownstreamArtifacts(t *testing.T) { {phase: phase.Parsed}, {phase: phase.Typechecked, scope: true, semantics: true, exportAPI: true, astNodes: true}, {phase: phase.CFG, scope: true, semantics: true, exportAPI: true, astNodes: true, cfg: true}, - {phase: phase.DefiniteInit, scope: true, semantics: true, exportAPI: true, astNodes: true, cfg: true}, - {phase: phase.Ownership, scope: true, semantics: true, exportAPI: true, astNodes: true, cfg: true, ownership: true}, - {phase: phase.Usage, scope: true, semantics: true, exportAPI: true, astNodes: true, cfg: true, ownership: true}, - {phase: phase.HIR, scope: true, semantics: true, exportAPI: true, astNodes: true, hir: true, cfg: true, ownership: true}, - {phase: phase.MIR, scope: true, semantics: true, exportAPI: true, astNodes: true, hir: true, cfg: true, ownership: true, mir: true}, - {phase: phase.Backend, scope: true, semantics: true, exportAPI: true, astNodes: true, hir: true, cfg: true, ownership: true, mir: true, llvm: true}, + {phase: phase.FlowTyped, scope: true, semantics: true, exportAPI: true, astNodes: true, cfg: true, flow: true}, + {phase: phase.DefiniteInit, scope: true, semantics: true, exportAPI: true, astNodes: true, cfg: true, flow: true}, + {phase: phase.Ownership, scope: true, semantics: true, exportAPI: true, astNodes: true, cfg: true, flow: true, ownership: true}, + {phase: phase.Usage, scope: true, semantics: true, exportAPI: true, astNodes: true, cfg: true, flow: true, ownership: true}, + {phase: phase.HIR, scope: true, semantics: true, exportAPI: true, astNodes: true, hir: true, cfg: true, flow: true, ownership: true}, + {phase: phase.MIR, scope: true, semantics: true, exportAPI: true, astNodes: true, hir: true, cfg: true, flow: true, ownership: true, mir: true}, + {phase: phase.Backend, scope: true, semantics: true, exportAPI: true, astNodes: true, hir: true, cfg: true, flow: true, ownership: true, mir: true, llvm: true}, } for _, test := range tests { module := moduleWithArtifacts() @@ -59,6 +64,7 @@ func TestModuleResetToPhaseClearsOnlyDownstreamArtifacts(t *testing.T) { (module.TypedASTNodes != nil) != test.astNodes || (module.SemanticExportFingerprint != "") != test.exportAPI || (module.CFG != nil) != test.cfg || + (module.Flow != nil) != test.flow || (module.Ownership != nil) != test.ownership || (module.MIR != nil) != test.mir || (module.LLVMIR != "") != test.llvm { diff --git a/internal/semantics/flowresult/result.go b/internal/semantics/flowresult/result.go new file mode 100644 index 0000000..56486f5 --- /dev/null +++ b/internal/semantics/flowresult/result.go @@ -0,0 +1,45 @@ +// Package flowresult defines semantic evidence produced by flow typing and +// consumed by ownership, lowering, and language tooling. +package flowresult + +import ( + "compiler/internal/frontend/ast" + "compiler/internal/ir" + "compiler/internal/ir/cfg" + "compiler/internal/semantics/place" + "compiler/internal/semantics/symbols" + "compiler/internal/semantics/typeinfo" +) + +type PresenceFact struct { + CarrierOrigins []place.Origin + Depth int + Dependencies []symbols.SymbolID +} + +type Facts struct { + Presence []PresenceFact + ReferenceOrigins map[symbols.SymbolID][]place.Origin + RawPointerOrigins map[symbols.SymbolID][]place.Origin +} + +type PayloadAccess struct { + CarrierOrigins []place.Origin + Depth int + Direct bool +} + +type OptionalTest struct { + SubjectID ast.NodeID + PresentWhenTrue bool + Depth int +} + +type Result struct { + SiteFacts map[ir.NodeID]map[cfg.SiteID]Facts + ExprTypes map[ast.NodeID]typeinfo.Type + Payloads map[ast.NodeID]PayloadAccess + OptionalTests map[ast.NodeID]OptionalTest + ResolvedStorageOrigins map[ast.NodeID][]place.Origin + ResolvedValueOrigins map[ast.NodeID][]place.Origin +} diff --git a/internal/semantics/ownership/expr.go b/internal/semantics/ownership/expr.go index 01b513d..7fc6a13 100644 --- a/internal/semantics/ownership/expr.go +++ b/internal/semantics/ownership/expr.go @@ -39,7 +39,7 @@ func (a *analyzer) checkExpr( return } if !projectionBase { - a.checkStorageAccess(scope, e, st, loans, storageAccessForUse(a.exprType(e), use)) + a.checkStorageAccess(e, loans, storageAccessForUse(a.exprType(e), use)) } case *ast.AddressExpr: access := storageSharedBorrow @@ -50,7 +50,7 @@ func (a *analyzer) checkExpr( case *ast.SelectorExpr: a.checkSelector(scope, e, st, use, loans) if !projectionBase { - a.checkStorageAccess(scope, e, st, loans, storageAccessForUse(a.exprType(e), use)) + a.checkStorageAccess(e, loans, storageAccessForUse(a.exprType(e), use)) } case *ast.IndexExpr: if typeinfo.IsInvalidOrUnknown(a.exprType(e)) { @@ -67,7 +67,7 @@ func (a *analyzer) checkExpr( access = storageMutableBorrow } } - a.checkStorageAccess(scope, e, st, loans, access) + a.checkStorageAccess(e, loans, access) } if slicing { return @@ -76,6 +76,11 @@ func (a *analyzer) checkExpr( return } if use != useRead && ownershipTrackedType(a.exprType(e)) { + if a.partialOptionalPayloadMove(e) { + a.ctx.Diagnostics.AddError(diagnostics.ErrInvalidCopy, + "move-only optional payload cannot be moved from partial place; borrow it instead", ast.LocOf(e), "") + return + } a.ctx.Diagnostics.AddError(diagnostics.ErrInvalidCopy, "move-only indexed element cannot be used by value; borrow it with `&` or `&mut`", ast.LocOf(e), "") } @@ -132,7 +137,7 @@ func (a *analyzer) checkAddressExpr( } a.checkExpr(scope, expr.Expr, st, useRead, loans, true) if expr.Mode != ast.AddressRaw { - a.checkStorageAccess(scope, expr.Expr, st, loans, access) + a.checkStorageAccess(expr.Expr, loans, access) } } @@ -205,6 +210,11 @@ func (a *analyzer) checkSelector( return } if ownershipTrackedType(a.exprType(selector)) { + if a.partialOptionalPayloadMove(selector) { + a.ctx.Diagnostics.AddError(diagnostics.ErrInvalidCopy, + "move-only optional payload cannot be moved from partial place; borrow it instead", ast.LocOf(selector), "") + return + } a.ctx.Diagnostics.AddError(diagnostics.ErrInvalidCopy, "move-only subexpression must be bound before it can be consumed", ast.LocOf(selector), "") } @@ -324,9 +334,9 @@ func (a *analyzer) checkCallArgument( a.checkAddressExpr(scope, explicitBorrow, st, loans, access) } else { a.checkExpr(scope, arg, st, useRead, loans, true) - a.checkStorageAccess(scope, arg, st, loans, access) + a.checkStorageAccess(arg, loans, access) } - origins := a.originsForExpr(scope, arg, st) + origins := a.originsForExpr(arg) if len(origins) == 0 { return } @@ -348,10 +358,18 @@ func (a *analyzer) checkCallArgument( } func (a *analyzer) exprType(expr ast.Expr) typeinfo.Type { - if a == nil || a.module == nil || a.module.Semantics == nil || expr == nil { + if a == nil || a.module == nil || expr == nil { return nil } - return a.module.Semantics.ExprTypes[expr.ID()] + return a.module.EffectiveExprType(expr.ID()) +} + +func (a *analyzer) partialOptionalPayloadMove(expr ast.Expr) bool { + if a == nil || a.module == nil || a.module.Flow == nil || expr == nil { + return false + } + payload, ok := a.module.Flow.Payloads[expr.ID()] + return ok && payload.Depth > 0 && !payload.Direct } func (a *analyzer) updatePointerSymbol(sym *symbols.Symbol, scope *symbols.Scope, value ast.Expr, st state) { diff --git a/internal/semantics/ownership/ownership.go b/internal/semantics/ownership/ownership.go index db96b93..f12e585 100644 --- a/internal/semantics/ownership/ownership.go +++ b/internal/semantics/ownership/ownership.go @@ -378,12 +378,12 @@ func (a *analyzer) applyStmt(node *site, st state) { case *ast.ConstDecl: a.applyBinding(scope, s, s.Value, st, loans) case *ast.AssignStmt: - reference, hasReference := a.referenceValueForExpr(scope, s.Value, st) + reference, hasReference := a.referenceValueForExpr(s.Value) delete(a.cleanup.BeforeAssign, ir.NodeID(s.ID())) a.checkExpr(scope, s.Value, st, useConsume, loans, false) if _, ok := s.Target.(*ast.Ident); !ok { a.checkExpr(scope, s.Target, st, useRead, loans, true) - a.checkStorageAccess(scope, s.Target, st, loans, storageMutate) + a.checkStorageAccess(s.Target, loans, storageMutate) if typeinfo.NeedsDrop(a.exprType(s.Target)) { a.cleanup.BeforeAssign[ir.NodeID(s.ID())] = struct{}{} } @@ -391,7 +391,7 @@ func (a *analyzer) applyStmt(node *site, st state) { if target, ok := s.Target.(*ast.Ident); ok && scope != nil { if sym, found := scope.Lookup(target.Name); found { if _, referenceTarget := referenceMutability(sym); !referenceTarget { - a.checkStorageAccess(scope, target, st, loans, storageMutate) + a.checkStorageAccess(target, loans, storageMutate) } if typ, ok := symbols.GetSymbolType(sym); ok && typeinfo.NeedsDrop(typ) { if _, live := st.live[sym]; live { @@ -408,7 +408,7 @@ func (a *analyzer) applyStmt(node *site, st state) { } case *ast.ReturnStmt: a.checkPointerEscape(scope, s.Value, st) - a.validateReferenceReturn(scope, s, st) + a.validateReferenceReturn(scope, s) a.checkExpr(scope, s.Value, st, useConsume, loans, false) a.cleanupBeforeReturn(scope, s, st, loans) case *ast.ExprStmt: @@ -437,7 +437,7 @@ func (a *analyzer) applyBinding(scope *symbols.Scope, stmt ast.Stmt, value ast.E if scope == nil || stmt == nil { return } - reference, hasReference := a.referenceValueForExpr(scope, value, st) + reference, hasReference := a.referenceValueForExpr(value) a.checkExpr(scope, value, st, useConsume, loans, false) sym, found := scope.LookupNode(stmt) if !found || sym == nil { diff --git a/internal/semantics/ownership/ownership_test.go b/internal/semantics/ownership/ownership_test.go index bdc6bb3..83ea318 100644 --- a/internal/semantics/ownership/ownership_test.go +++ b/internal/semantics/ownership/ownership_test.go @@ -50,6 +50,7 @@ func checkOwnershipSource(t *testing.T, src string) *ownershipResult { typechecker.Check(ctx, module) module.TypedASTNodes = ast.Index(module.AST) module.CFG = cfg.BuildModule(module.AST) + module.Flow = typechecker.CheckFlow(ctx, module) module.Ownership = Check(ctx, module) return &ownershipResult{DiagnosticBag: diag, ctx: ctx, module: module} } @@ -533,6 +534,100 @@ func TestCopyableIndexedElementReadAllowed(t *testing.T) { } } +func TestCopyableOptionalPayloadCanBeReadRepeatedly(t *testing.T) { + diag := checkOwnershipSource(t, `fn sum(value: ?i32) -> i32 { + if value == none { + return 0; + } + return value + value; +}`) + if diag.HasErrors() { + t.Fatalf("unexpected diagnostics:\n%s", diag.EmitAllToString()) + } +} + +func TestMoveOnlyOptionalPayloadConsumesNamedCarrier(t *testing.T) { + diag := checkOwnershipSource(t, `struct Token { value: i32 } +fn Consume(_: Token) {} +fn bad(value: ?Token) { + if value == none { + return; + } + Consume(value); + Consume(value); +}`) + if !hasOwnershipCode(diag, diagnostics.ErrUseAfterMove) { + t.Fatalf("expected optional carrier use-after-move, got:\n%s", diag.EmitAllToString()) + } +} + +func TestMoveOnlyOptionalCarrierCanBeReinitialized(t *testing.T) { + diag := checkOwnershipSource(t, `struct Token { value: i32 } +fn Consume(_: Token) {} +fn valid(mut value: ?Token) { + if value == none { + return; + } + Consume(value); + value = .Token{value = 2}; + if value == none { + return; + } + Consume(value); +}`) + if diag.HasErrors() { + t.Fatalf("unexpected diagnostics:\n%s", diag.EmitAllToString()) + } +} + +func TestMoveOnlyOptionalPartialPlacesAreRejected(t *testing.T) { + diag := checkOwnershipSource(t, `struct Token { value: i32 } +struct Holder { field: ?Token, items: [1]?Token } +fn Consume(_: Token) {} +fn bad(holder: Holder) { + if holder.field != none { + Consume(holder.field); + } + if holder.items[0] != none { + Consume(holder.items[0]); + } +}`) + out := diag.EmitAllToString() + if count := strings.Count(out, "move-only optional payload"); count != 2 { + t.Fatalf("expected two optional partial-move diagnostics, got %d:\n%s", count, out) + } +} + +func TestFlowResolvesBorrowedOptionalPayloadStorage(t *testing.T) { + result := checkOwnershipSource(t, `struct Token { value: i32 } +fn inspect(value: ?Token) { + if value == none { + return; + } + let reference = &value; +}`) + if result.HasErrors() { + t.Fatalf("unexpected diagnostics:\n%s", result.EmitAllToString()) + } + fn := result.module.AST.Stmts[1].(*ast.FnDecl) + binding := fn.Body.Stmts[1].(*ast.LetDecl) + address := binding.Value.(*ast.AddressExpr) + valueUse := address.Expr.(*ast.Ident) + function, _ := result.module.ModuleScope.Lookup("inspect") + value, _ := function.Scope.Lookup("value") + storage := []place.Origin{{Root: value}} + payload := []place.Origin{{ + Root: value, + Projections: []place.OriginProjection{{Kind: place.OriginOptionalPayload}}, + }} + if got := result.module.Flow.ResolvedStorageOrigins[valueUse.ID()]; !place.SameOrigins(got, storage) { + t.Fatalf("payload storage origins = %#v, want carrier %#v", got, storage) + } + if got := result.module.Flow.ResolvedValueOrigins[valueUse.ID()]; !place.SameOrigins(got, payload) { + t.Fatalf("payload value origins = %#v, want %#v", got, payload) + } +} + func TestReferenceReceiverDoesNotCopyMoveOnlyOwner(t *testing.T) { diag := checkOwnershipSource(t, `struct Counter { value: i32 diff --git a/internal/semantics/ownership/reference.go b/internal/semantics/ownership/reference.go index fd274f2..ba46d05 100644 --- a/internal/semantics/ownership/reference.go +++ b/internal/semantics/ownership/reference.go @@ -4,12 +4,10 @@ import ( "maps" "slices" - "compiler/internal/constvalue" "compiler/internal/diagnostics" "compiler/internal/frontend/ast" "compiler/internal/ir/cfg" "compiler/internal/project" - "compiler/internal/semantics/consteval" "compiler/internal/semantics/place" "compiler/internal/semantics/symbols" "compiler/internal/semantics/typeinfo" @@ -131,9 +129,7 @@ func (ctx *loanContext) addTemporary(value []referenceLoan, call ast.Node) { } func (a *analyzer) checkStorageAccess( - scope *symbols.Scope, expr ast.Expr, - st state, loans *loanContext, access storageAccess, ) { @@ -141,7 +137,7 @@ func (a *analyzer) checkStorageAccess( return } a.reportLoanConflict( - a.originsForExpr(scope, expr, st), + a.originsForExpr(expr), a.referenceHolder(expr), access, expr, @@ -326,21 +322,15 @@ func (a *analyzer) referenceHolder(expr ast.Expr) *symbols.Symbol { } } -func (a *analyzer) referenceValueForExpr(scope *symbols.Scope, expr ast.Expr, st state) ([]referenceLoan, bool) { - if a == nil || scope == nil || expr == nil { +func (a *analyzer) referenceValueForExpr(expr ast.Expr) ([]referenceLoan, bool) { + if a == nil || expr == nil { return []referenceLoan{}, false } _, mutable, ok := typeinfo.ReferenceValueTarget(a.exprType(expr)) if !ok { return []referenceLoan{}, false } - if ident, ok := expr.(*ast.Ident); ok { - sym := a.module.Semantics.ResolvedSymbols[ident.ID()] - if value, tracked := st.references[sym]; tracked { - return copyReferenceLoans(value), true - } - } - origins := a.originsForExpr(scope, expr, st) + origins := a.originsForExpr(expr) if len(origins) == 0 { return []referenceLoan{}, false } @@ -352,57 +342,21 @@ func (a *analyzer) referenceValueForExpr(scope *symbols.Scope, expr ast.Expr, st }}, true } -func (a *analyzer) originsForExpr(scope *symbols.Scope, expr ast.Expr, st state) []place.Origin { - if a == nil || scope == nil || expr == nil { - return nil - } - return place.Origins(scope, expr, place.OriginOptions{ - ExprType: a.exprType, - ResolveBinding: a.expandedDefaultBinding, - ReferenceOrigins: func(sym *symbols.Symbol) []place.Origin { - return referenceOrigins(st.references[sym]) - }, - CallOrigins: func(call *ast.CallExpr) []place.Origin { - return a.callReturnOrigins(scope, call, st) - }, - ConstantIndex: func(index ast.Expr) (string, bool) { - expected := a.exprType(index) - if !typeinfo.IsIntegral(expected) { - expected = typeinfo.DefaultIntegerType() - } - value, evaluated := consteval.EvaluateExpr(a.ctx, a.module, scope, index, expected) - integer, integral := value.(*constvalue.IntConst) - if !evaluated || !integral || integer == nil { - return "", false - } - return integer.Text(), true - }, - }) -} - -func (a *analyzer) callReturnOrigins(scope *symbols.Scope, call *ast.CallExpr, st state) []place.Origin { - if a == nil || call == nil || call.Callee == nil { - return nil - } - fnType, _ := typeinfo.Underlying(a.exprType(call.Callee)).(*typeinfo.FuncType) - if fnType == nil || fnType.ReturnOrigins == nil { +func (a *analyzer) originsForExpr(expr ast.Expr) []place.Origin { + if a == nil || a.module == nil || a.module.Flow == nil || expr == nil { return nil } - var origins []place.Origin - for _, source := range typeinfo.ReturnOriginSources(call, fnType) { - origins = place.MergeOrigins(origins, a.originsForExpr(scope, source, st)) - } - return origins + return place.CloneOrigins(a.module.Flow.ResolvedValueOrigins[expr.ID()]) } -func (a *analyzer) validateReferenceReturn(scope *symbols.Scope, stmt *ast.ReturnStmt, st state) { +func (a *analyzer) validateReferenceReturn(scope *symbols.Scope, stmt *ast.ReturnStmt) { if a == nil || a.function == nil || scope == nil || stmt == nil || stmt.Value == nil { return } if _, _, reference := typeinfo.ReferenceValueTarget(a.exprType(stmt.Value)); !reference { return } - value, found := a.referenceValueForExpr(scope, stmt.Value, st) + value, found := a.referenceValueForExpr(stmt.Value) if !found { return } diff --git a/internal/semantics/place/origin.go b/internal/semantics/place/origin.go index 1e5caac..ea17e11 100644 --- a/internal/semantics/place/origin.go +++ b/internal/semantics/place/origin.go @@ -12,13 +12,16 @@ const ( OriginPointee OriginProjectionKind = iota OriginField OriginIndex + OriginBindingIndex + OriginOptionalPayload OriginWildcard ) type OriginProjection struct { - Kind OriginProjectionKind - Field string - Index string + Kind OriginProjectionKind + Field string + Index string + Binding *symbols.Symbol } type Origin struct { @@ -26,71 +29,127 @@ type Origin struct { Projections []OriginProjection } -type OriginOptions struct { - ExprType ExprTypeFunc - ResolveBinding BindingResolver - ReferenceOrigins func(*symbols.Symbol) []Origin - CallOrigins func(*ast.CallExpr) []Origin - ConstantIndex func(ast.Expr) (string, bool) +type ResolveOptions struct { + ExprType ExprTypeFunc + ResolveBinding BindingResolver + ReferenceOrigins func(*symbols.Symbol) []Origin + RawPointerOrigins func(*symbols.Symbol) []Origin + CallOrigins func(*ast.CallExpr) []Origin + ConstantIndex func(ast.Expr) (string, bool) + PayloadDepth func(ast.Expr) int } -// Origins resolves safe-reference dereferences eagerly. Canonical origins never -// retain a reference binding as storage identity when its referent is known. -func Origins(scope *symbols.Scope, expr ast.Expr, opts OriginOptions) []Origin { +// Resolution keeps carrier storage distinct from referenced value storage. +// Stable is false when any projection cannot retain identity across CFG sites. +type Resolution struct { + StorageOrigins []Origin + ValueOrigins []Origin + Dependencies []*symbols.Symbol + Stable bool +} + +// Resolve is the canonical place walk. Value origins preserve the previous +// eager safe-reference normalization; storage origins retain carrier identity. +func Resolve(scope *symbols.Scope, expr ast.Expr, opts ResolveOptions) Resolution { if scope == nil || expr == nil { - return nil + return Resolution{} } switch node := expr.(type) { case *ast.AddressExpr: - return Origins(scope, node.Expr, opts) + return Resolve(scope, node.Expr, opts) case *ast.Ident: - var sym *symbols.Symbol - var found bool - if opts.ResolveBinding != nil { - binding, resolved := opts.ResolveBinding(node) - if resolved { - sym, found = binding.Symbol, true - } - } - if !found { - sym, found = scope.Lookup(node.Name) - } + sym, found := resolveSymbol(scope, node, opts.ResolveBinding) if !found || sym == nil { - return nil + return Resolution{} } + storage := []Origin{{Root: sym}} if typ, ok := symbols.GetSymbolType(sym); ok { if _, _, reference := typeinfo.ReferenceValueTarget(typ); reference && opts.ReferenceOrigins != nil { - return CloneOrigins(opts.ReferenceOrigins(sym)) + return Resolution{ + StorageOrigins: storage, + ValueOrigins: CloneOrigins(opts.ReferenceOrigins(sym)), + Stable: true, + } + } + if _, raw := typeinfo.Underlying(typ).(*typeinfo.RawPtrType); raw && opts.RawPointerOrigins != nil { + return Resolution{ + StorageOrigins: storage, + ValueOrigins: CloneOrigins(opts.RawPointerOrigins(sym)), + Stable: true, + } } } - return []Origin{{Root: sym}} + return Resolution{StorageOrigins: storage, ValueOrigins: CloneOrigins(storage), Stable: true} case *ast.SelectorExpr: if node.Name == nil { - return nil + return Resolution{} + } + base := Resolve(scope, node.Expr, opts) + origins := appendIndirectProjection(base.ValueOrigins, node.Expr, opts.ExprType) + origins = appendOptionalPayloadProjections(origins, node.Expr, opts.PayloadDepth) + origins = appendOriginProjection(origins, OriginProjection{Kind: OriginField, Field: node.Name.Name}) + return Resolution{ + StorageOrigins: origins, + ValueOrigins: CloneOrigins(origins), + Dependencies: append([]*symbols.Symbol(nil), base.Dependencies...), + Stable: base.Stable && len(origins) > 0, } - origins := Origins(scope, node.Expr, opts) - origins = appendIndirectProjection(origins, node.Expr, opts.ExprType) - return appendOriginProjection(origins, OriginProjection{Kind: OriginField, Field: node.Name.Name}) case *ast.IndexExpr: - origins := Origins(scope, node.Expr, opts) - origins = appendIndirectProjection(origins, node.Expr, opts.ExprType) + base := Resolve(scope, node.Expr, opts) + origins := appendIndirectProjection(base.ValueOrigins, node.Expr, opts.ExprType) + origins = appendOptionalPayloadProjections(origins, node.Expr, opts.PayloadDepth) + dependencies := append([]*symbols.Symbol(nil), base.Dependencies...) if _, rangeIndex := node.Index.(*ast.RangeExpr); rangeIndex { - return appendOriginProjection(origins, OriginProjection{Kind: OriginWildcard}) + origins = appendOriginProjection(origins, OriginProjection{Kind: OriginWildcard}) + return Resolution{StorageOrigins: origins, ValueOrigins: CloneOrigins(origins)} } if opts.ConstantIndex != nil { if value, ok := opts.ConstantIndex(node.Index); ok { - return appendOriginProjection(origins, OriginProjection{Kind: OriginIndex, Index: value}) + origins = appendOriginProjection(origins, OriginProjection{Kind: OriginIndex, Index: value}) + return Resolution{ + StorageOrigins: origins, + ValueOrigins: CloneOrigins(origins), + Dependencies: dependencies, + Stable: base.Stable && len(origins) > 0, + } + } + } + if index, ok := node.Index.(*ast.Ident); ok { + if sym, found := resolveSymbol(scope, index, opts.ResolveBinding); found && sym != nil { + if typ, typed := symbols.GetSymbolType(sym); typed && typeinfo.IsIntegral(typ) { + origins = appendOriginProjection(origins, OriginProjection{Kind: OriginBindingIndex, Binding: sym}) + dependencies = append(dependencies, sym) + return Resolution{ + StorageOrigins: origins, + ValueOrigins: CloneOrigins(origins), + Dependencies: dependencies, + Stable: base.Stable && len(origins) > 0, + } + } } } - return appendOriginProjection(origins, OriginProjection{Kind: OriginWildcard}) + origins = appendOriginProjection(origins, OriginProjection{Kind: OriginWildcard}) + return Resolution{StorageOrigins: origins, ValueOrigins: CloneOrigins(origins), Dependencies: dependencies} case *ast.CallExpr: if opts.CallOrigins != nil { - return CloneOrigins(opts.CallOrigins(node)) + return Resolution{ValueOrigins: CloneOrigins(opts.CallOrigins(node))} } - return nil + return Resolution{} default: - return nil + return Resolution{} + } +} + +func resolveSymbol(scope *symbols.Scope, ident *ast.Ident, resolve BindingResolver) (*symbols.Symbol, bool) { + if ident == nil { + return nil, false + } + if resolve != nil { + if binding, found := resolve(ident); found { + return binding.Symbol, binding.Symbol != nil + } } + return scope.Lookup(ident.Name) } func CloneOrigins(origins []Origin) []Origin { @@ -141,7 +200,8 @@ func SameOrigins(left, right []Origin) bool { // OriginsOverlap is conservative unless two canonical paths prove disjoint at // a concrete field or fixed index. Prefixes overlap because one path names -// storage containing the other; wildcards overlap every descendant. +// storage containing the other; symbolic indexes and wildcards may alias any +// indexed descendant. func OriginsOverlap(left, right []Origin) bool { for _, leftOrigin := range left { for _, rightOrigin := range right { @@ -163,6 +223,22 @@ func appendIndirectProjection(origins []Origin, base ast.Expr, exprType ExprType return appendOriginProjection(origins, OriginProjection{Kind: OriginPointee}) } +func appendOptionalPayloadProjections(origins []Origin, base ast.Expr, payloadDepth func(ast.Expr) int) []Origin { + if payloadDepth == nil { + return origins + } + return PayloadOrigins(origins, payloadDepth(base)) +} + +// PayloadOrigins projects carrier storage through exact proven optional layers. +func PayloadOrigins(origins []Origin, depth int) []Origin { + out := CloneOrigins(origins) + for range depth { + out = appendOriginProjection(out, OriginProjection{Kind: OriginOptionalPayload}) + } + return out +} + func appendOriginProjection(origins []Origin, projection OriginProjection) []Origin { out := CloneOrigins(origins) for i := range out { diff --git a/internal/semantics/place/origin_test.go b/internal/semantics/place/origin_test.go index 1dcb523..f41802b 100644 --- a/internal/semantics/place/origin_test.go +++ b/internal/semantics/place/origin_test.go @@ -163,7 +163,7 @@ func TestPlaceLocalRootPreservesBindingLocalAndPointerCutoff(t *testing.T) { } } -func TestOriginsPreferResolvedBindingOverShadowingScope(t *testing.T) { +func TestResolvePreferResolvedBindingOverShadowingScope(t *testing.T) { scope := symbols.NewScope(nil) callerValue := symbols.New("value", symbols.SymbolVar, nil, nil) declarationValue := symbols.New("value", symbols.SymbolConst, nil, nil) @@ -172,17 +172,18 @@ func TestOriginsPreferResolvedBindingOverShadowingScope(t *testing.T) { } ident := &ast.Ident{Name: "value"} - origins := Origins(scope, ident, OriginOptions{ + resolved := Resolve(scope, ident, ResolveOptions{ ResolveBinding: func(*ast.Ident) (Binding, bool) { return Binding{Symbol: declarationValue}, true }, }) - if !SameOrigins(origins, []Origin{{Root: declarationValue}}) { - t.Fatalf("origins = %#v, want declaration binding", origins) + want := []Origin{{Root: declarationValue}} + if !SameOrigins(resolved.StorageOrigins, want) || !SameOrigins(resolved.ValueOrigins, want) || !resolved.Stable { + t.Fatalf("resolution = %#v, want stable declaration binding", resolved) } } -func TestOriginsNormalizeReferenceRootsAndProjections(t *testing.T) { +func TestResolveSeparatesReferenceStorageAndValueProjections(t *testing.T) { scope := symbols.NewScope(nil) value := symbols.New("value", symbols.SymbolVar, nil, nil) value.BindType(&typeinfo.StructType{Fields: []typeinfo.Field{{Name: "items", Type: &typeinfo.ArrayType{Len: "2", Elem: typeinfo.DefaultIntegerType()}}}}) @@ -202,7 +203,7 @@ func TestOriginsNormalizeReferenceRootsAndProjections(t *testing.T) { base: reference.Type, field: value.Type.(*typeinfo.StructType).Fields[0].Type, } - origins := Origins(scope, index, OriginOptions{ + resolved := Resolve(scope, index, ResolveOptions{ ExprType: func(expr ast.Expr) typeinfo.Type { return types[expr] }, ReferenceOrigins: func(sym *symbols.Symbol) []Origin { if sym == reference { @@ -216,12 +217,17 @@ func TestOriginsNormalizeReferenceRootsAndProjections(t *testing.T) { {Kind: OriginField, Field: "items"}, {Kind: OriginIndex, Index: "1"}, }}} - if !SameOrigins(origins, want) { - t.Fatalf("origins = %#v, want %#v", origins, want) + if !SameOrigins(resolved.ValueOrigins, want) || !resolved.Stable { + t.Fatalf("resolution = %#v, want stable value origins %#v", resolved, want) + } + if !SameOrigins(Resolve(scope, base, ResolveOptions{ + ReferenceOrigins: func(*symbols.Symbol) []Origin { return []Origin{{Root: value}} }, + }).StorageOrigins, []Origin{{Root: reference}}) { + t.Fatal("reference carrier storage did not retain binding identity") } } -func TestOriginsPreserveOwningPointeeAndCollapseUnknownDescendants(t *testing.T) { +func TestResolvePreserveOwningPointeeAndCollapseUnknownDescendants(t *testing.T) { scope := symbols.NewScope(nil) owner := symbols.New("owner", symbols.SymbolVar, nil, nil) inner := &typeinfo.ArrayType{Len: "2", Elem: typeinfo.DefaultIntegerType()} @@ -238,7 +244,7 @@ func TestOriginsPreserveOwningPointeeAndCollapseUnknownDescendants(t *testing.T) base: owner.Type, first: inner, } - origins := Origins(scope, second, OriginOptions{ + resolved := Resolve(scope, second, ResolveOptions{ ExprType: func(expr ast.Expr) typeinfo.Type { return types[expr] }, ConstantIndex: func(expr ast.Expr) (string, bool) { literal, ok := expr.(*ast.NumberLit) @@ -252,8 +258,31 @@ func TestOriginsPreserveOwningPointeeAndCollapseUnknownDescendants(t *testing.T) {Kind: OriginPointee}, {Kind: OriginWildcard}, }}} - if !SameOrigins(origins, want) { - t.Fatalf("origins = %#v, want %#v", origins, want) + if !SameOrigins(resolved.ValueOrigins, want) || resolved.Stable { + t.Fatalf("resolution = %#v, want unstable origins %#v", resolved, want) + } +} + +func TestResolveUsesBindingIndexIdentityAfterConstantEvaluation(t *testing.T) { + scope := symbols.NewScope(nil) + values := symbols.New("values", symbols.SymbolParam, nil, nil) + values.BindType(&typeinfo.ArrayType{Len: "2", Elem: typeinfo.DefaultIntegerType()}) + index := symbols.New("index", symbols.SymbolParam, nil, nil) + index.BindType(typeinfo.DefaultIntegerType()) + if err := scope.Declare(values); err != nil { + t.Fatal(err) + } + if err := scope.Declare(index); err != nil { + t.Fatal(err) + } + expr := &ast.IndexExpr{Expr: &ast.Ident{Name: "values"}, Index: &ast.Ident{Name: "index"}} + resolved := Resolve(scope, expr, ResolveOptions{ + ConstantIndex: func(ast.Expr) (string, bool) { return "", false }, + }) + want := []Origin{{Root: values, Projections: []OriginProjection{{Kind: OriginBindingIndex, Binding: index}}}} + if !resolved.Stable || !SameOrigins(resolved.StorageOrigins, want) || + len(resolved.Dependencies) != 1 || resolved.Dependencies[0] != index { + t.Fatalf("resolution = %#v, want binding-dependent stable index", resolved) } } @@ -281,7 +310,12 @@ func TestOriginsOverlap(t *testing.T) { other := symbols.New("other", symbols.SymbolVar, nil, nil) field := func(name string) OriginProjection { return OriginProjection{Kind: OriginField, Field: name} } index := func(value string) OriginProjection { return OriginProjection{Kind: OriginIndex, Index: value} } + bindingIndex := func(binding *symbols.Symbol) OriginProjection { + return OriginProjection{Kind: OriginBindingIndex, Binding: binding} + } wildcard := OriginProjection{Kind: OriginWildcard} + leftIndex := symbols.New("leftIndex", symbols.SymbolVar, nil, nil) + rightIndex := symbols.New("rightIndex", symbols.SymbolVar, nil, nil) tests := []struct { name string @@ -295,6 +329,9 @@ func TestOriginsOverlap(t *testing.T) { {name: "same field", left: []Origin{{Root: root, Projections: []OriginProjection{field("value")}}}, right: []Origin{{Root: root, Projections: []OriginProjection{field("value")}}}, overlap: true}, {name: "different fields", left: []Origin{{Root: root, Projections: []OriginProjection{field("left")}}}, right: []Origin{{Root: root, Projections: []OriginProjection{field("right")}}}}, {name: "different fixed indexes", left: []Origin{{Root: root, Projections: []OriginProjection{index("0")}}}, right: []Origin{{Root: root, Projections: []OriginProjection{index("1")}}}}, + {name: "same binding index", left: []Origin{{Root: root, Projections: []OriginProjection{bindingIndex(leftIndex)}}}, right: []Origin{{Root: root, Projections: []OriginProjection{bindingIndex(leftIndex)}}}, overlap: true}, + {name: "different binding indexes may alias", left: []Origin{{Root: root, Projections: []OriginProjection{bindingIndex(leftIndex)}}}, right: []Origin{{Root: root, Projections: []OriginProjection{bindingIndex(rightIndex)}}}, overlap: true}, + {name: "binding and fixed indexes may alias", left: []Origin{{Root: root, Projections: []OriginProjection{bindingIndex(leftIndex)}}}, right: []Origin{{Root: root, Projections: []OriginProjection{index("1")}}}, overlap: true}, {name: "wildcard index", left: []Origin{{Root: root, Projections: []OriginProjection{wildcard}}}, right: []Origin{{Root: root, Projections: []OriginProjection{index("1")}}}, overlap: true}, {name: "different projection kinds", left: []Origin{{Root: root, Projections: []OriginProjection{field("value")}}}, right: []Origin{{Root: root, Projections: []OriginProjection{index("0")}}}, overlap: true}, {name: "any origin pair", left: []Origin{{Root: other}, {Root: root, Projections: []OriginProjection{field("value")}}}, right: []Origin{{Root: root, Projections: []OriginProjection{field("value")}}}, overlap: true}, diff --git a/internal/semantics/typechecker/check_call.go b/internal/semantics/typechecker/check_call.go index 1389b41..9cf2721 100644 --- a/internal/semantics/typechecker/check_call.go +++ b/internal/semantics/typechecker/check_call.go @@ -17,7 +17,7 @@ func (c *checker) typeFreeExpr(scope *symbols.Scope, node *ast.FreeExpr) typeinf if node == nil || node.Expr == nil { return &typeinfo.InvalidType{} } - operandType := c.typeExpr(scope, node.Expr, nil) + operandType := c.typePayloadExpr(scope, node.Expr, nil) if typeinfo.IsInvalidOrUnknown(operandType) { return &typeinfo.InvalidType{} } @@ -33,7 +33,7 @@ func (c *checker) typePrintExpr(scope *symbols.Scope, node *ast.PrintExpr) typei if node == nil || node.Expr == nil { return &typeinfo.InvalidType{} } - operandType := c.typeExpr(scope, node.Expr, nil) + operandType := c.typePayloadExpr(scope, node.Expr, nil) if typeinfo.IsInvalidOrUnknown(operandType) { return &typeinfo.InvalidType{} } @@ -77,7 +77,7 @@ func (c *checker) typeCallExpr(scope *symbols.Scope, node *ast.CallExpr, expecte } } } - calleeType := c.typeExpr(scope, node.Callee, expected) + calleeType := c.typePayloadExpr(scope, node.Callee, expected) if sym := c.callableSymbol(node.Callee); sym != nil { c.expandCallDefaults(node, sym, c.callableModule(node.Callee)) } diff --git a/internal/semantics/typechecker/check_expr.go b/internal/semantics/typechecker/check_expr.go index a2eeefb..0c59007 100644 --- a/internal/semantics/typechecker/check_expr.go +++ b/internal/semantics/typechecker/check_expr.go @@ -18,19 +18,34 @@ import ( "compiler/pkg/numeric" ) -// typeExpr computes the type of an expression using scope lookup, records it in the -// module's ExprTypes side table for downstream phases, and returns it. -func (c *checker) typeExpr(scope *symbols.Scope, expr ast.Expr, expected typeinfo.Type) (resolved typeinfo.Type) { +// typeExpr records canonical base typing, then applies per-use flow refinement. +// Recursive typing stays in typeExprBase so both passes use one AST switch. +func (c *checker) typeExpr(scope *symbols.Scope, expr ast.Expr, expected typeinfo.Type) typeinfo.Type { + base := c.typeExprBase(scope, expr, expected) + if call, ok := expr.(*ast.CallExpr); ok && c.flow != nil && c.flow.analyzer != nil { + c.flow.analyzer.invalidateCall(c, scope, call, c.flow.state) + if c.flow.events != nil { + c.flow.events.next++ + c.flow.events.calls = append(c.flow.events.calls, flowCallEvent{order: c.flow.events.next, call: call}) + } + } + if base == nil || expr == nil { + return base + } + if c.module != nil && c.module.Semantics != nil && c.flow == nil { + c.module.Semantics.ExprTypes[expr.ID()] = base + } + resolved := c.effectiveExpressionType(scope, expr, base, expected) + if c.flow != nil && resolved != nil { + c.flow.result.ExprTypes[expr.ID()] = resolved + } + return resolved +} + +func (c *checker) typeExprBase(scope *symbols.Scope, expr ast.Expr, expected typeinfo.Type) typeinfo.Type { if expr == nil { return nil } - defer func() { - if resolved != nil { - if c.module != nil && c.module.Semantics != nil { - c.module.Semantics.ExprTypes[expr.ID()] = resolved - } - } - }() switch node := expr.(type) { case *ast.NumberLit: return c.typeNumber(node, expected) @@ -153,7 +168,7 @@ func (c *checker) typeUnaryExpr(scope *symbols.Scope, node *ast.UnaryExpr, expec } } - argType := c.typeExpr(scope, node.Expr, argExpected) + argType := c.typePayloadExpr(scope, node.Expr, argExpected) argType = c.requireValueType(node.Expr, argType, "unary operand") if typeinfo.IsInvalidOrUnknown(argType) { return &typeinfo.InvalidType{} @@ -185,7 +200,7 @@ func (c *checker) typeAddressExpr(scope *symbols.Scope, node *ast.AddressExpr, e if node == nil || node.Expr == nil { return &typeinfo.InvalidType{} } - valueType := c.typeExpr(scope, node.Expr, nil) + valueType := c.typePayloadExpr(scope, node.Expr, nil) valueType = c.requireValueType(node.Expr, valueType, "address operand") if typeinfo.IsInvalidOrUnknown(valueType) { return &typeinfo.InvalidType{} @@ -227,6 +242,11 @@ func (c *checker) typeAddressExpr(scope *symbols.Scope, node *ast.AddressExpr, e } func (c *checker) typeBinaryExpr(scope *symbols.Scope, node *ast.BinaryExpr, expected typeinfo.Type) typeinfo.Type { + optionalTest := (node.Op == "==" || node.Op == "!=") && isNoneExpr(node.Left) != isNoneExpr(node.Right) + if !optionalTest { + c.payloadContext++ + defer func() { c.payloadContext-- }() + } operandExpected := expected if binaryResultIsBool(node.Op) { operandExpected = nil @@ -241,7 +261,7 @@ func (c *checker) typeBinaryExpr(scope *symbols.Scope, node *ast.BinaryExpr, exp } right = c.typeExpr(scope, node.Right, rightExpected) } else if isNoneExpr(node.Left) && !isNoneExpr(node.Right) { - right = c.typeExpr(scope, node.Right, operandExpected) + right = c.typeOptionalTestExpr(scope, node.Right, operandExpected) left = c.typeExpr(scope, node.Left, optionalOperandExpected(right)) } else if leftNumber, leftLiteral := node.Left.(*ast.NumberLit); leftLiteral { if rightNumber, rightLiteral := node.Right.(*ast.NumberLit); !rightLiteral { @@ -261,12 +281,13 @@ func (c *checker) typeBinaryExpr(scope *symbols.Scope, node *ast.BinaryExpr, exp left = c.typeExpr(scope, node.Left, operandExpected) right = c.typeExpr(scope, node.Right, left) } else { - left = c.typeExpr(scope, node.Left, operandExpected) - rightExpected := operandExpected if isNoneExpr(node.Right) { - rightExpected = optionalOperandExpected(left) + left = c.typeOptionalTestExpr(scope, node.Left, operandExpected) + right = c.typeExpr(scope, node.Right, optionalOperandExpected(left)) + } else { + left = c.typeExpr(scope, node.Left, operandExpected) + right = c.typeExpr(scope, node.Right, operandExpected) } - right = c.typeExpr(scope, node.Right, rightExpected) } left = c.requireValueType(node.Left, left, "left operand") right = c.requireValueType(node.Right, right, "right operand") @@ -280,6 +301,13 @@ func (c *checker) typeBinaryExpr(scope *symbols.Scope, node *ast.BinaryExpr, exp "optional equality currently requires `none` on one side")) return &typeinfo.InvalidType{} } + if optionalTest { + subject := node.Left + if isNoneExpr(subject) { + subject = node.Right + } + c.recordOptionalTest(node, subject) + } if node.Op == "<<" || node.Op == ">>" { if !typeinfo.IsIntegral(left) { @@ -382,7 +410,7 @@ func (c *checker) typeSelectorExpr(scope *symbols.Scope, node *ast.SelectorExpr) if node == nil || node.Expr == nil || node.Name == nil { return &typeinfo.InvalidType{} } - baseType := c.typeExpr(scope, node.Expr, nil) + baseType := c.typePayloadExpr(scope, node.Expr, nil) if baseType == nil || typeinfo.IsInvalidOrUnknown(baseType) { return &typeinfo.InvalidType{} } @@ -409,7 +437,7 @@ func (c *checker) typeIndexExpr(scope *symbols.Scope, node *ast.IndexExpr) typei if node == nil || node.Expr == nil || node.Index == nil { return &typeinfo.InvalidType{} } - baseType := c.typeExpr(scope, node.Expr, nil) + baseType := c.typePayloadExpr(scope, node.Expr, nil) if typeinfo.IsInvalidOrUnknown(baseType) { return &typeinfo.InvalidType{} } @@ -720,7 +748,7 @@ func (c *checker) typeAsExpr(scope *symbols.Scope, node *ast.AsExpr) typeinfo.Ty if node.Expr == nil { return &typeinfo.InvalidType{} } - exprType := c.typeExpr(scope, node.Expr, nil) + exprType := c.typePayloadExpr(scope, node.Expr, nil) exprType = c.requireValueType(node.Expr, exprType, "cast") if typeinfo.IsInvalidOrUnknown(exprType) { return &typeinfo.InvalidType{} diff --git a/internal/semantics/typechecker/check_stmt.go b/internal/semantics/typechecker/check_stmt.go index 3bc09e6..331842a 100644 --- a/internal/semantics/typechecker/check_stmt.go +++ b/internal/semantics/typechecker/check_stmt.go @@ -74,6 +74,9 @@ func (c *checker) checkStmt(scope *symbols.Scope, stmt ast.Stmt, returnType type if condType != nil && !typeinfo.IsInvalidOrUnknown(condType) && !typeinfo.IsCondition(condType) { c.ctx.Diagnostics.Add(explicitBoolCastRequiredError(node.Cond, "if condition must be bool")) } + if c.siteOnly { + return + } c.checkBlock(scope, node.Then, returnType) c.checkStmt(scope, node.Else, returnType) case *ast.ForStmt: @@ -83,6 +86,9 @@ func (c *checker) checkStmt(scope *symbols.Scope, stmt ast.Stmt, returnType type c.ctx.Diagnostics.Add(explicitBoolCastRequiredError(node.Cond, "for condition must be bool")) } } + if c.siteOnly { + return + } c.checkBlock(scope, node.Body, returnType) case *ast.ExprStmt: if node.Expr == nil { @@ -105,7 +111,7 @@ func (c *checker) checkAssign(scope *symbols.Scope, node *ast.AssignStmt) { if c == nil || scope == nil || node == nil || node.Target == nil || node.Value == nil { return } - targetType := c.typeExpr(scope, node.Target, nil) + targetType := c.typeWholeCarrierExpr(scope, node.Target, nil) if targetType == nil || typeinfo.IsInvalidOrUnknown(targetType) { return } diff --git a/internal/semantics/typechecker/errors.go b/internal/semantics/typechecker/errors.go index cf0fa6f..27ccf24 100644 --- a/internal/semantics/typechecker/errors.go +++ b/internal/semantics/typechecker/errors.go @@ -46,6 +46,20 @@ func typeMismatchError(node ast.Node, message string) *diagnostics.Diagnostic { WithCode(diagnostics.ErrTypeMismatch) } +func optionalPayloadProofError(node ast.Node) *diagnostics.Diagnostic { + return diagnostics.NewError("optional payload use requires a presence proof"). + WithPrimaryLabel(ast.LocOf(node), "payload is not proven present here"). + WithCode(diagnostics.ErrOptionalPayloadProof). + WithHelp("guard this stable place with `value != none` or return after `value == none`") +} + +func unstableOptionalNarrowingError(node ast.Node) *diagnostics.Diagnostic { + return diagnostics.NewError("optional narrowing subject is not a stable place"). + WithPrimaryLabel(ast.LocOf(node), "this expression can change between the test and use"). + WithCode(diagnostics.ErrUnstableNarrowing). + WithHelp("bind the expression or index to a direct local before testing it") +} + func notCallableError(node ast.Node, message string) *diagnostics.Diagnostic { return diagnostics.NewError(message). WithPrimaryLabel(ast.LocOf(node), ""). diff --git a/internal/semantics/typechecker/flow.go b/internal/semantics/typechecker/flow.go new file mode 100644 index 0000000..ab9f2f0 --- /dev/null +++ b/internal/semantics/typechecker/flow.go @@ -0,0 +1,955 @@ +package typechecker + +import ( + "maps" + + "compiler/internal/constvalue" + "compiler/internal/frontend/ast" + "compiler/internal/ir" + "compiler/internal/ir/cfg" + "compiler/internal/project" + "compiler/internal/semantics/consteval" + "compiler/internal/semantics/flowresult" + "compiler/internal/semantics/place" + "compiler/internal/semantics/symbols" + "compiler/internal/semantics/typeinfo" +) + +type presenceStateFact struct { + origins []place.Origin + depth int + dependencies []*symbols.Symbol +} + +type flowState struct { + presence []presenceStateFact + references map[*symbols.Symbol][]place.Origin + rawPointers map[*symbols.Symbol][]place.Origin +} + +type flowCheck struct { + result *flowresult.Result + state *flowState + analyzer *flowAnalyzer + events *flowExpressionEvents +} + +type flowCallEvent struct { + order int + call *ast.CallExpr +} + +type flowExpressionEvents struct { + next int + tests map[ast.NodeID]int + calls []flowCallEvent +} + +type edgePresenceFact struct { + presence presenceStateFact + order int +} + +type flowAnalyzer struct { + ctx *project.CompilerContext + module *project.Module + functionScope *symbols.Scope + graph *cfg.Graph + returnType typeinfo.Type + result *flowresult.Result + sites map[cfg.SiteID]*cfg.Site + inStates map[cfg.SiteID]flowState +} + +// CheckFlow runs optional/origin facts to fixed point, then records exact +// per-use types through the existing checker implementation. +func CheckFlow(ctx *project.CompilerContext, module *project.Module) *flowresult.Result { + result := &flowresult.Result{ + SiteFacts: make(map[ir.NodeID]map[cfg.SiteID]flowresult.Facts), + ExprTypes: make(map[ast.NodeID]typeinfo.Type), + Payloads: make(map[ast.NodeID]flowresult.PayloadAccess), + OptionalTests: make(map[ast.NodeID]flowresult.OptionalTest), + ResolvedStorageOrigins: make(map[ast.NodeID][]place.Origin), + ResolvedValueOrigins: make(map[ast.NodeID][]place.Origin), + } + if ctx == nil || module == nil || module.CFG == nil || module.Semantics == nil || module.ModuleScope == nil { + return result + } + for _, graph := range module.CFG.Functions { + if graph == nil { + continue + } + fn, _ := module.TypedASTNodes[ast.NodeID(graph.NodeID)].(*ast.FnDecl) + if fn == nil { + continue + } + var sym *symbols.Symbol + if fn.Receiver != nil { + sym = module.Semantics.MethodSymbol[fn.ID()] + } else if fn.Name != nil { + sym, _ = module.ModuleScope.Lookup(fn.Name.Name) + } + if sym == nil || sym.Scope == nil { + continue + } + fnType := typeinfo.FuncTypeFromDeclWithOptions(fn, project.TypeSyntaxOptions(ctx, module, nil, false)) + analyzer := &flowAnalyzer{ + ctx: ctx, module: module, functionScope: sym.Scope, + graph: graph, result: result, sites: make(map[cfg.SiteID]*cfg.Site), + inStates: make(map[cfg.SiteID]flowState), + } + if fnType != nil { + analyzer.returnType = fnType.Return + } + analyzer.run() + } + return result +} + +func (c *checker) typePayloadExpr(scope *symbols.Scope, expr ast.Expr, expected typeinfo.Type) typeinfo.Type { + c.payloadContext++ + defer func() { c.payloadContext-- }() + return c.typeExpr(scope, expr, expected) +} + +func (c *checker) typeOptionalTestExpr(scope *symbols.Scope, expr ast.Expr, expected typeinfo.Type) typeinfo.Type { + c.optionalTestContext++ + defer func() { c.optionalTestContext-- }() + return c.typeExpr(scope, expr, expected) +} + +func (c *checker) typeWholeCarrierExpr(scope *symbols.Scope, expr ast.Expr, expected typeinfo.Type) typeinfo.Type { + previous := c.wholeCarrierExpr + c.wholeCarrierExpr = expr + defer func() { c.wholeCarrierExpr = previous }() + return c.typeExpr(scope, expr, expected) +} + +func (c *checker) effectiveExpressionType(scope *symbols.Scope, expr ast.Expr, base, expected typeinfo.Type) typeinfo.Type { + if c == nil || expr == nil || base == nil { + return base + } + resolution := place.Resolution{} + if c.flow != nil { + resolution = c.resolveFlowPlace(scope, expr, *c.flow.state) + } + if c.wholeCarrierExpr == expr { + c.recordFlowResolution(expr, resolution) + return base + } + if !isOptionalType(base) { + c.recordFlowResolution(expr, resolution) + return base + } + required := payloadDepthForExpected(base, expected) + if c.payloadContext > 0 && required == 0 { + required = optionalLayerCount(base) + } + if c.flow == nil { + if c.optionalTestContext > 0 || required == 0 { + return base + } + return unwrapOptionalLayers(base, required) + } + + proven := presenceDepth(c.flow.state.presence, resolution.StorageOrigins) + resolved := unwrapOptionalLayers(base, proven) + applied := optionalLayerCount(base) - optionalLayerCount(resolved) + if c.optionalTestContext == 0 { + if _, explicitCarrier := typeinfo.Underlying(expected).(*typeinfo.OptionalType); explicitCarrier { + c.recordFlowResolution(expr, resolution) + return base + } + } + if applied > 0 { + c.recordPayloadAccess(expr, resolution, applied) + } + valueOrigins := place.PayloadOrigins(resolution.StorageOrigins, applied) + if _, _, reference := typeinfo.ReferenceValueTarget(resolved); reference { + valueOrigins = place.CloneOrigins(resolution.ValueOrigins) + } else if _, raw := typeinfo.Underlying(resolved).(*typeinfo.RawPtrType); raw { + valueOrigins = place.CloneOrigins(resolution.ValueOrigins) + } + resolution.ValueOrigins = valueOrigins + c.recordFlowResolution(expr, resolution) + if c.optionalTestContext > 0 { + return resolved + } + if required <= applied { + return resolved + } + if place.IsPlaceExpr(expr) && !resolution.Stable { + c.ctx.Diagnostics.Add(unstableOptionalNarrowingError(expr)) + } else { + c.ctx.Diagnostics.Add(optionalPayloadProofError(expr)) + } + return unwrapOptionalLayers(base, required) +} + +func payloadDepthForExpected(src, expected typeinfo.Type) int { + if src == nil || expected == nil { + return 0 + } + if _, optional := typeinfo.Underlying(expected).(*typeinfo.OptionalType); optional { + return 0 + } + current := src + for depth := 1; ; depth++ { + optional, ok := typeinfo.Underlying(current).(*typeinfo.OptionalType) + if !ok || optional == nil || optional.Inner == nil { + return 0 + } + current = optional.Inner + if typeinfo.Assignable(expected, current) { + return depth + } + } +} + +func optionalLayerCount(typ typeinfo.Type) int { + depth := 0 + for { + optional, ok := typeinfo.Underlying(typ).(*typeinfo.OptionalType) + if !ok || optional == nil || optional.Inner == nil { + return depth + } + depth++ + typ = optional.Inner + } +} + +func unwrapOptionalLayers(typ typeinfo.Type, depth int) typeinfo.Type { + for range depth { + optional, ok := typeinfo.Underlying(typ).(*typeinfo.OptionalType) + if !ok || optional == nil || optional.Inner == nil { + break + } + typ = optional.Inner + } + return typ +} + +func (c *checker) recordOptionalTest(node *ast.BinaryExpr, subject ast.Expr) { + if c == nil || node == nil || subject == nil { + return + } + test := flowresult.OptionalTest{SubjectID: subject.ID(), PresentWhenTrue: node.Op == "!="} + if c.flow == nil { + if c.module != nil && c.module.Semantics != nil { + c.module.Semantics.OptionalTests[node.ID()] = test + } + return + } + if payload, ok := c.flow.result.Payloads[subject.ID()]; ok { + test.Depth = payload.Depth + } + c.flow.result.OptionalTests[node.ID()] = test + if c.flow.events != nil { + c.flow.events.next++ + c.flow.events.tests[node.ID()] = c.flow.events.next + } +} + +func (c *checker) recordPayloadAccess(expr ast.Expr, resolution place.Resolution, depth int) { + if c == nil || c.flow == nil || expr == nil || depth <= 0 { + return + } + direct := len(resolution.StorageOrigins) == 1 && + resolution.StorageOrigins[0].Root != nil && len(resolution.StorageOrigins[0].Projections) == 0 + c.flow.result.Payloads[expr.ID()] = flowresult.PayloadAccess{ + CarrierOrigins: place.CloneOrigins(resolution.StorageOrigins), + Depth: depth, + Direct: direct, + } +} + +func (c *checker) recordFlowResolution(expr ast.Expr, resolution place.Resolution) { + if c == nil || c.flow == nil || expr == nil { + return + } + id := expr.ID() + c.flow.result.ResolvedStorageOrigins[id] = place.CloneOrigins(resolution.StorageOrigins) + c.flow.result.ResolvedValueOrigins[id] = place.CloneOrigins(resolution.ValueOrigins) +} + +func (c *checker) resolveFlowPlace(scope *symbols.Scope, expr ast.Expr, st flowState) place.Resolution { + if c == nil || c.module == nil || c.module.Semantics == nil { + return place.Resolution{} + } + return place.Resolve(scope, expr, place.ResolveOptions{ + ExprType: func(node ast.Expr) typeinfo.Type { + if node == nil { + return nil + } + if c.flow != nil { + if typ := c.flow.result.ExprTypes[node.ID()]; typ != nil { + return typ + } + } + return c.module.Semantics.ExprTypes[node.ID()] + }, + ResolveBinding: c.expandedDefaultBinding, + ReferenceOrigins: func(sym *symbols.Symbol) []place.Origin { + return st.references[sym] + }, + RawPointerOrigins: func(sym *symbols.Symbol) []place.Origin { + return st.rawPointers[sym] + }, + CallOrigins: func(call *ast.CallExpr) []place.Origin { + if call == nil || call.Callee == nil { + return nil + } + calleeType := c.module.Semantics.ExprTypes[call.Callee.ID()] + if c.flow != nil && c.flow.result.ExprTypes[call.Callee.ID()] != nil { + calleeType = c.flow.result.ExprTypes[call.Callee.ID()] + } + fn, _ := typeinfo.Underlying(calleeType).(*typeinfo.FuncType) + var origins []place.Origin + for _, source := range typeinfo.ReturnOriginSources(call, fn) { + origins = place.MergeOrigins(origins, c.resolveFlowPlace(scope, source, st).ValueOrigins) + } + return origins + }, + ConstantIndex: func(index ast.Expr) (string, bool) { + expected := c.module.Semantics.ExprTypes[index.ID()] + if !typeinfo.IsIntegral(expected) { + expected = typeinfo.DefaultIntegerType() + } + value, evaluated := consteval.EvaluateExpr(c.ctx, c.module, scope, index, expected) + integer, integral := value.(*constvalue.IntConst) + if !evaluated || !integral || integer == nil { + return "", false + } + return integer.Text(), true + }, + PayloadDepth: func(base ast.Expr) int { + if c.flow == nil || base == nil { + return 0 + } + return c.flow.result.Payloads[base.ID()].Depth + }, + }) +} + +func (a *flowAnalyzer) run() { + if a == nil || a.graph == nil || a.graph.Entry == nil || len(a.graph.Entry.Sites) == 0 { + return + } + order := make([]cfg.SiteID, 0) + for _, block := range a.graph.Blocks { + if block == nil { + continue + } + for _, site := range block.Sites { + if site != nil { + a.sites[site.ID] = site + order = append(order, site.ID) + } + } + } + entryState := newFlowState() + for _, sym := range a.functionScope.Symbols() { + if sym == nil || sym.Kind != symbols.SymbolParam { + continue + } + if typ, ok := symbols.GetSymbolType(sym); ok { + if _, _, reference := typeinfo.ReferenceValueTarget(typ); reference { + entryState.references[sym] = []place.Origin{{Root: sym}} + } + } + } + entry := a.graph.Entry.Sites[0].ID + a.inStates[entry] = entryState + queue := []cfg.SiteID{entry} + queued := map[cfg.SiteID]bool{entry: true} + for _, id := range order { + if id == entry || len(a.sites[id].Predecessors) != 0 { + continue + } + a.inStates[id] = copyFlowState(entryState) + queue = append(queue, id) + queued[id] = true + } + for { + if len(queue) == 0 { + for _, id := range order { + if _, visited := a.inStates[id]; visited { + continue + } + a.inStates[id] = copyFlowState(entryState) + queue = append(queue, id) + queued[id] = true + break + } + if len(queue) == 0 { + break + } + } + id := queue[0] + queue = queue[1:] + queued[id] = false + site := a.sites[id] + if site == nil { + continue + } + input := copyFlowState(a.inStates[id]) + if a.result.SiteFacts[a.graph.NodeID] == nil { + a.result.SiteFacts[a.graph.NodeID] = make(map[cfg.SiteID]flowresult.Facts) + } + a.result.SiteFacts[a.graph.NodeID][id] = snapshotFlowState(input) + next := copyFlowState(input) + events := a.applySite(site, &next) + for _, edge := range site.Successors { + if a.sites[edge.To] == nil { + continue + } + out := copyFlowState(next) + if site.Kind == cfg.SiteTerminator { + a.applyConditionEdge(site, edge.Kind, &out, events) + } + current, exists := a.inStates[edge.To] + merged := out + if exists { + merged = mergeFlowStates(current, out) + } + if exists && sameFlowState(current, merged) { + continue + } + a.inStates[edge.To] = merged + if !queued[edge.To] { + queue = append(queue, edge.To) + queued[edge.To] = true + } + } + } +} + +func newFlowState() flowState { + return flowState{ + references: make(map[*symbols.Symbol][]place.Origin), + rawPointers: make(map[*symbols.Symbol][]place.Origin), + } +} + +func copyFlowState(src flowState) flowState { + dst := newFlowState() + for _, fact := range src.presence { + dst.presence = append(dst.presence, presenceStateFact{ + origins: place.CloneOrigins(fact.origins), depth: fact.depth, + dependencies: append([]*symbols.Symbol(nil), fact.dependencies...), + }) + } + for sym, origins := range src.references { + dst.references[sym] = place.CloneOrigins(origins) + } + for sym, origins := range src.rawPointers { + dst.rawPointers[sym] = place.CloneOrigins(origins) + } + return dst +} + +func snapshotFlowState(st flowState) flowresult.Facts { + facts := flowresult.Facts{ + ReferenceOrigins: make(map[symbols.SymbolID][]place.Origin), + RawPointerOrigins: make(map[symbols.SymbolID][]place.Origin), + } + for _, fact := range st.presence { + dependencies := make([]symbols.SymbolID, 0, len(fact.dependencies)) + for _, sym := range fact.dependencies { + if sym != nil { + dependencies = append(dependencies, sym.ID) + } + } + facts.Presence = append(facts.Presence, flowresult.PresenceFact{ + CarrierOrigins: place.CloneOrigins(fact.origins), Depth: fact.depth, Dependencies: dependencies, + }) + } + for sym, origins := range st.references { + if sym != nil { + facts.ReferenceOrigins[sym.ID] = place.CloneOrigins(origins) + } + } + for sym, origins := range st.rawPointers { + if sym != nil { + facts.RawPointerOrigins[sym.ID] = place.CloneOrigins(origins) + } + } + return facts +} + +func (a *flowAnalyzer) applySite(site *cfg.Site, st *flowState) *flowExpressionEvents { + events := &flowExpressionEvents{tests: make(map[ast.NodeID]int)} + if site == nil || st == nil { + return events + } + scope := a.module.Semantics.BlockScopes[ast.NodeID(site.ScopeID)] + if scope == nil { + scope = a.functionScope + } + checker := &checker{ + ctx: a.ctx, module: a.module, siteOnly: true, + flow: &flowCheck{result: a.result, state: st, analyzer: a, events: events}, + } + switch site.Kind { + case cfg.SiteStatement, cfg.SiteTerminator: + stmt, _ := a.module.TypedASTNodes[ast.NodeID(site.NodeID)].(ast.Stmt) + if stmt == nil { + return events + } + checker.checkStmt(scope, stmt, a.returnType) + a.applyStatementEffects(checker, scope, stmt, st) + case cfg.SiteScopeExit: + block, _ := a.module.TypedASTNodes[ast.NodeID(site.NodeID)].(*ast.BlockStmt) + if block != nil { + blockScope := a.module.Semantics.BlockScopes[block.ID()] + if blockScope == nil { + return events + } + clearFlowScope(blockScope, st) + } + } + return events +} + +func (a *flowAnalyzer) applyStatementEffects(c *checker, scope *symbols.Scope, stmt ast.Stmt, st *flowState) { + if c == nil || stmt == nil || st == nil { + return + } + switch node := stmt.(type) { + case *ast.LetDecl: + if sym, found := scope.LookupNode(node); found { + a.updateOriginBinding(c, scope, sym, node.Value, st) + } + case *ast.ConstDecl: + if sym, found := scope.LookupNode(node); found { + a.updateOriginBinding(c, scope, sym, node.Value, st) + } + case *ast.AssignStmt: + resolution := c.resolveFlowPlace(scope, node.Target, *st) + invalidatePresenceOrigins(st, resolution.StorageOrigins) + if sym := a.assignedSymbol(scope, node.Target); sym != nil { + invalidatePresenceDependency(st, sym) + a.updateOriginBinding(c, scope, sym, node.Value, st) + } + } +} + +func (a *flowAnalyzer) assignedSymbol(scope *symbols.Scope, expr ast.Expr) *symbols.Symbol { + ident, ok := expr.(*ast.Ident) + if !ok || ident == nil { + return nil + } + if sym := a.module.Semantics.ResolvedSymbols[ident.ID()]; sym != nil { + return sym + } + sym, _ := scope.Lookup(ident.Name) + return sym +} + +func (a *flowAnalyzer) updateOriginBinding(c *checker, scope *symbols.Scope, sym *symbols.Symbol, value ast.Expr, st *flowState) { + if sym == nil || st == nil { + return + } + typ, typed := symbols.GetSymbolType(sym) + if !typed { + delete(st.references, sym) + delete(st.rawPointers, sym) + return + } + if _, _, reference := typeinfo.ReferenceValueTarget(typ); reference { + st.references[sym] = place.CloneOrigins(c.resolveFlowPlace(scope, value, *st).ValueOrigins) + } else { + delete(st.references, sym) + } + if _, raw := typeinfo.Underlying(typ).(*typeinfo.RawPtrType); raw { + if origins, known := a.rawPointerOrigins(c, scope, value, *st); known { + st.rawPointers[sym] = origins + } else { + delete(st.rawPointers, sym) + } + } else { + delete(st.rawPointers, sym) + } +} + +func (a *flowAnalyzer) invalidateCall(c *checker, scope *symbols.Scope, call *ast.CallExpr, st *flowState) { + if call == nil || call.Callee == nil || st == nil { + return + } + calleeType := a.result.ExprTypes[call.Callee.ID()] + if calleeType == nil { + calleeType = a.module.Semantics.ExprTypes[call.Callee.ID()] + } + fn, _ := typeinfo.Underlying(calleeType).(*typeinfo.FuncType) + args := append([]ast.Expr(nil), call.Args...) + if selector, method := call.Callee.(*ast.SelectorExpr); method && selector != nil { + args = append([]ast.Expr{selector.Expr}, args...) + } + if fn != nil && len(fn.Params) == len(args) { + for index, arg := range args { + param := fn.Params[index] + if _, mutable, reference := typeinfo.ReferenceValueTarget(param); reference && mutable { + invalidatePresenceOrigins(st, c.resolveFlowPlace(scope, arg, *st).ValueOrigins) + } + if _, raw := typeinfo.Underlying(param).(*typeinfo.RawPtrType); raw { + if origins, known := a.rawPointerOrigins(c, scope, arg, *st); known { + invalidatePresenceOrigins(st, origins) + } else { + st.presence = nil + } + } + } + } + for _, sym := range a.module.ModuleScope.Symbols() { + if sym != nil && sym.IsMutable() { + invalidatePresenceOrigins(st, []place.Origin{{Root: sym}}) + } + } +} + +func (a *flowAnalyzer) rawPointerOrigins(c *checker, scope *symbols.Scope, expr ast.Expr, st flowState) ([]place.Origin, bool) { + switch node := expr.(type) { + case *ast.AddressExpr: + if node != nil && node.Mode == ast.AddressRaw { + origins := c.resolveFlowPlace(scope, node.Expr, st).ValueOrigins + return origins, len(origins) > 0 + } + case *ast.Ident: + if sym := a.assignedSymbol(scope, node); sym != nil { + origins, known := st.rawPointers[sym] + return place.CloneOrigins(origins), known + } + case *ast.AsExpr: + return a.rawPointerOrigins(c, scope, node.Expr, st) + } + return nil, false +} + +func (a *flowAnalyzer) applyConditionEdge(site *cfg.Site, edge cfg.EdgeKind, st *flowState, events *flowExpressionEvents) { + if st == nil || (edge != cfg.EdgeTrue && edge != cfg.EdgeFalse) { + return + } + stmt, _ := a.module.TypedASTNodes[ast.NodeID(site.NodeID)].(ast.Stmt) + var condition ast.Expr + switch node := stmt.(type) { + case *ast.IfStmt: + condition = node.Cond + case *ast.ForStmt: + condition = node.Cond + } + if condition == nil { + return + } + scope := a.module.Semantics.BlockScopes[ast.NodeID(site.ScopeID)] + if scope == nil { + scope = a.functionScope + } + for _, implied := range a.impliedPresence(scope, condition, edge == cfg.EdgeTrue, *st, events) { + filtered := copyFlowState(*st) + filtered.presence = []presenceStateFact{implied.presence} + checker := &checker{ctx: a.ctx, module: a.module, flow: &flowCheck{result: a.result, state: &filtered}} + for _, call := range events.calls { + if call.order > implied.order { + a.invalidateCall(checker, scope, call.call, &filtered) + } + } + if len(filtered.presence) > 0 { + addPresenceFact(st, filtered.presence[0]) + } + } +} + +func (a *flowAnalyzer) impliedPresence( + scope *symbols.Scope, + expr ast.Expr, + truth bool, + st flowState, + events *flowExpressionEvents, +) []edgePresenceFact { + if expr == nil { + return nil + } + if test, found := a.result.OptionalTests[expr.ID()]; found { + if truth != test.PresentWhenTrue { + return nil + } + subject, _ := a.module.TypedASTNodes[test.SubjectID].(ast.Expr) + checker := &checker{ctx: a.ctx, module: a.module, flow: &flowCheck{result: a.result, state: &st}} + resolution := checker.resolveFlowPlace(scope, subject, st) + if !resolution.Stable || len(resolution.StorageOrigins) == 0 { + a.ctx.Diagnostics.Add(unstableOptionalNarrowingError(subject)) + return nil + } + return []edgePresenceFact{{ + presence: presenceStateFact{ + origins: place.CloneOrigins(resolution.StorageOrigins), depth: test.Depth + 1, + dependencies: append([]*symbols.Symbol(nil), resolution.Dependencies...), + }, + order: events.tests[expr.ID()], + }} + } + switch node := expr.(type) { + case *ast.UnaryExpr: + if node.Op == "!" { + return a.impliedPresence(scope, node.Expr, !truth, st, events) + } + case *ast.BinaryExpr: + switch node.Op { + case "&&": + if truth { + return unionEdgePresenceFacts( + a.impliedPresence(scope, node.Left, true, st, events), + a.impliedPresence(scope, node.Right, true, st, events), + ) + } + return intersectEdgePresenceFacts( + a.impliedPresence(scope, node.Left, false, st, events), + a.impliedPresence(scope, node.Right, false, st, events), + ) + case "||": + if truth { + return intersectEdgePresenceFacts( + a.impliedPresence(scope, node.Left, true, st, events), + a.impliedPresence(scope, node.Right, true, st, events), + ) + } + return unionEdgePresenceFacts( + a.impliedPresence(scope, node.Left, false, st, events), + a.impliedPresence(scope, node.Right, false, st, events), + ) + } + } + return nil +} + +func presenceDepth(facts []presenceStateFact, origins []place.Origin) int { + for _, fact := range facts { + if place.SameOrigins(fact.origins, origins) { + return fact.depth + } + } + return 0 +} + +func addPresenceFact(st *flowState, added presenceStateFact) { + if st == nil || len(added.origins) == 0 || added.depth <= 0 { + return + } + for index := range st.presence { + if place.SameOrigins(st.presence[index].origins, added.origins) { + if added.depth > st.presence[index].depth { + st.presence[index].depth = added.depth + } + st.presence[index].dependencies = mergeDependencies(st.presence[index].dependencies, added.dependencies) + return + } + } + st.presence = append(st.presence, presenceStateFact{ + origins: place.CloneOrigins(added.origins), depth: added.depth, + dependencies: append([]*symbols.Symbol(nil), added.dependencies...), + }) +} + +func unionEdgePresenceFacts(left, right []edgePresenceFact) []edgePresenceFact { + merged := append([]edgePresenceFact(nil), left...) + for _, candidate := range right { + found := false + for index := range merged { + if !place.SameOrigins(merged[index].presence.origins, candidate.presence.origins) { + continue + } + found = true + merged[index].presence.dependencies = mergeDependencies( + merged[index].presence.dependencies, candidate.presence.dependencies, + ) + if candidate.presence.depth > merged[index].presence.depth { + merged[index].presence.depth = candidate.presence.depth + merged[index].order = candidate.order + } else if candidate.presence.depth == merged[index].presence.depth { + merged[index].order = max(merged[index].order, candidate.order) + } + break + } + if !found { + candidate.presence.origins = place.CloneOrigins(candidate.presence.origins) + candidate.presence.dependencies = append([]*symbols.Symbol(nil), candidate.presence.dependencies...) + merged = append(merged, candidate) + } + } + return merged +} + +func intersectEdgePresenceFacts(left, right []edgePresenceFact) []edgePresenceFact { + out := make([]edgePresenceFact, 0) + for _, leftFact := range left { + for _, rightFact := range right { + if !place.SameOrigins(leftFact.presence.origins, rightFact.presence.origins) { + continue + } + out = append(out, edgePresenceFact{ + presence: presenceStateFact{ + origins: place.CloneOrigins(leftFact.presence.origins), + depth: min(leftFact.presence.depth, rightFact.presence.depth), + dependencies: mergeDependencies( + leftFact.presence.dependencies, rightFact.presence.dependencies, + ), + }, + order: min(leftFact.order, rightFact.order), + }) + break + } + } + return out +} + +func intersectPresenceFacts(left, right []presenceStateFact) []presenceStateFact { + out := make([]presenceStateFact, 0) + for _, leftFact := range left { + for _, rightFact := range right { + if !place.SameOrigins(leftFact.origins, rightFact.origins) { + continue + } + out = append(out, presenceStateFact{ + origins: place.CloneOrigins(leftFact.origins), depth: min(leftFact.depth, rightFact.depth), + dependencies: mergeDependencies(leftFact.dependencies, rightFact.dependencies), + }) + break + } + } + return out +} + +func mergeFlowStates(left, right flowState) flowState { + merged := newFlowState() + merged.presence = intersectPresenceFacts(left.presence, right.presence) + for sym, origins := range left.references { + merged.references[sym] = place.CloneOrigins(origins) + } + for sym, origins := range right.references { + merged.references[sym] = place.MergeOrigins(merged.references[sym], origins) + } + for sym, origins := range left.rawPointers { + merged.rawPointers[sym] = place.CloneOrigins(origins) + } + for sym, origins := range right.rawPointers { + merged.rawPointers[sym] = place.MergeOrigins(merged.rawPointers[sym], origins) + } + return merged +} + +func sameFlowState(left, right flowState) bool { + if len(left.presence) != len(right.presence) || len(left.references) != len(right.references) || + len(left.rawPointers) != len(right.rawPointers) { + return false + } + for _, fact := range left.presence { + if presenceDepth(right.presence, fact.origins) != fact.depth { + return false + } + } + if !maps.EqualFunc(left.references, right.references, place.SameOrigins) || + !maps.EqualFunc(left.rawPointers, right.rawPointers, place.SameOrigins) { + return false + } + return true +} + +func mergeDependencies(left, right []*symbols.Symbol) []*symbols.Symbol { + merged := append([]*symbols.Symbol(nil), left...) + for _, candidate := range right { + found := false + for _, existing := range merged { + if existing == candidate { + found = true + break + } + } + if !found { + merged = append(merged, candidate) + } + } + return merged +} + +func invalidatePresenceDependency(st *flowState, assigned *symbols.Symbol) { + if st == nil || assigned == nil { + return + } + kept := st.presence[:0] + for _, fact := range st.presence { + dependent := false + for _, dependency := range fact.dependencies { + if dependency == assigned { + dependent = true + break + } + } + if !dependent { + kept = append(kept, fact) + } + } + st.presence = kept +} + +func invalidatePresenceOrigins(st *flowState, mutated []place.Origin) { + if st == nil || len(mutated) == 0 { + return + } + kept := st.presence[:0] + for _, fact := range st.presence { + if !place.OriginsOverlap(fact.origins, mutated) { + kept = append(kept, fact) + continue + } + preserved := fact.depth + for _, mutation := range mutated { + for _, carrier := range fact.origins { + if !place.OriginsOverlap([]place.Origin{carrier}, []place.Origin{mutation}) { + continue + } + payloadDepth := payloadDescendantDepth(carrier, mutation) + if payloadDepth == 0 { + preserved = 0 + } else { + preserved = min(preserved, payloadDepth) + } + } + } + if preserved > 0 { + fact.depth = preserved + kept = append(kept, fact) + } + } + st.presence = kept +} + +func payloadDescendantDepth(carrier, mutation place.Origin) int { + if carrier.Root == nil || carrier.Root != mutation.Root || len(mutation.Projections) <= len(carrier.Projections) { + return 0 + } + for index := range carrier.Projections { + if carrier.Projections[index] != mutation.Projections[index] { + return 0 + } + } + depth := 0 + for _, projection := range mutation.Projections[len(carrier.Projections):] { + if projection.Kind != place.OriginOptionalPayload { + break + } + depth++ + } + return depth +} + +func clearFlowScope(scope *symbols.Scope, st *flowState) { + if scope == nil || st == nil { + return + } + for _, sym := range scope.Symbols() { + delete(st.references, sym) + delete(st.rawPointers, sym) + invalidatePresenceDependency(st, sym) + invalidatePresenceOrigins(st, []place.Origin{{Root: sym}}) + } +} diff --git a/internal/semantics/typechecker/flow_test.go b/internal/semantics/typechecker/flow_test.go new file mode 100644 index 0000000..791f9c9 --- /dev/null +++ b/internal/semantics/typechecker/flow_test.go @@ -0,0 +1,70 @@ +package typechecker + +import ( + "testing" + + "compiler/internal/frontend/ast" + "compiler/internal/project" + "compiler/internal/semantics/flowresult" + "compiler/internal/semantics/place" + "compiler/internal/semantics/symbols" + "compiler/internal/semantics/typeinfo" +) + +func TestClearFlowScopeRemovesOnlyExitedBindingFacts(t *testing.T) { + outerScope := symbols.NewScope(nil) + innerScope := symbols.NewScope(outerScope) + outer := symbols.New("outer", symbols.SymbolVar, &ast.LetDecl{IsMutable: true}, nil) + inner := symbols.New("inner", symbols.SymbolVar, &ast.LetDecl{IsMutable: true}, nil) + if err := outerScope.Declare(outer); err != nil { + t.Fatal(err) + } + if err := innerScope.Declare(inner); err != nil { + t.Fatal(err) + } + outerOrigins := []place.Origin{{Root: outer}} + innerOrigins := []place.Origin{{Root: inner}} + state := flowState{ + presence: []presenceStateFact{ + {origins: outerOrigins, depth: 1}, + {origins: innerOrigins, depth: 1}, + {origins: outerOrigins, depth: 1, dependencies: []*symbols.Symbol{inner}}, + }, + references: map[*symbols.Symbol][]place.Origin{outer: outerOrigins, inner: innerOrigins}, + rawPointers: map[*symbols.Symbol][]place.Origin{outer: outerOrigins, inner: innerOrigins}, + } + + clearFlowScope(innerScope, &state) + + if len(state.presence) != 1 || state.presence[0].origins[0].Root != outer { + t.Fatalf("presence after scope exit = %#v, want only outer fact", state.presence) + } + if _, exists := state.references[inner]; exists { + t.Fatal("scope exit retained inner reference origin") + } + if _, exists := state.rawPointers[inner]; exists { + t.Fatal("scope exit retained inner raw-pointer origin") + } + if len(state.references[outer]) != 1 || len(state.rawPointers[outer]) != 1 { + t.Fatal("scope exit removed outer origin evidence") + } +} + +func TestInvalidateCallClearsMutableModuleVariableFacts(t *testing.T) { + moduleScope := symbols.NewScope(nil) + global := symbols.New("maybe", symbols.SymbolVar, &ast.LetDecl{IsMutable: true, IsModuleVar: true}, nil) + if err := moduleScope.Declare(global); err != nil { + t.Fatal(err) + } + state := flowState{presence: []presenceStateFact{{origins: []place.Origin{{Root: global}}, depth: 1}}} + analyzer := flowAnalyzer{ + module: &project.Module{ModuleScope: moduleScope, Semantics: project.NewSemanticInfo()}, + result: &flowresult.Result{ExprTypes: make(map[ast.NodeID]typeinfo.Type)}, + } + + analyzer.invalidateCall(&checker{}, nil, &ast.CallExpr{Callee: &ast.Ident{Name: "Touch"}}, &state) + + if len(state.presence) != 0 { + t.Fatalf("presence after call = %#v, want mutable module fact invalidated", state.presence) + } +} diff --git a/internal/semantics/typechecker/typechecker.go b/internal/semantics/typechecker/typechecker.go index 9d097eb..068cb34 100644 --- a/internal/semantics/typechecker/typechecker.go +++ b/internal/semantics/typechecker/typechecker.go @@ -9,8 +9,13 @@ import ( ) type checker struct { - ctx *project.CompilerContext - module *project.Module + ctx *project.CompilerContext + module *project.Module + flow *flowCheck + siteOnly bool + payloadContext int + optionalTestContext int + wholeCarrierExpr ast.Expr } // Concrete references convert to satisfied interface borrows, while owned diff --git a/internal/semantics/typeinfo/capabilities.go b/internal/semantics/typeinfo/capabilities.go index ffcde39..671aade 100644 --- a/internal/semantics/typeinfo/capabilities.go +++ b/internal/semantics/typeinfo/capabilities.go @@ -79,12 +79,13 @@ func IsCondition(t Type) bool { } func IsImplicitCopyType(t Type) bool { - switch Underlying(t).(type) { + switch typ := Underlying(t).(type) { case *IntegerType, *ByteType, *CharType, *FloatType, *BoolType, *CStrType, *RawPtrType, *AllocatorType: return true case *RefType: - ref, _ := Underlying(t).(*RefType) - return ref != nil && !ref.Mutable + return typ != nil && !typ.Mutable + case *OptionalType: + return typ != nil && IsImplicitCopyType(typ.Inner) default: return false } diff --git a/internal/semantics/typeinfo/compatibility.go b/internal/semantics/typeinfo/compatibility.go index de63ebd..08f45cd 100644 --- a/internal/semantics/typeinfo/compatibility.go +++ b/internal/semantics/typeinfo/compatibility.go @@ -156,6 +156,9 @@ func checkOptionalCompatibility(dst, src Type) Compatibility { if _, ok := Underlying(src).(*NoneType); ok { return Compatible } + if SameType(left.Inner, src) { + return Compatible + } right, ok := Underlying(src).(*OptionalType) if ok && right != nil { if SameType(left.Inner, right.Inner) { @@ -163,9 +166,6 @@ func checkOptionalCompatibility(dst, src Type) Compatibility { } return Incompatible } - if SameType(left.Inner, src) { - return Compatible - } return Incompatible } diff --git a/internal/semantics/typeinfo/compatibility_test.go b/internal/semantics/typeinfo/compatibility_test.go index d1c4aa4..2b2a6be 100644 --- a/internal/semantics/typeinfo/compatibility_test.go +++ b/internal/semantics/typeinfo/compatibility_test.go @@ -136,3 +136,15 @@ func TestOptionalArrayAndReferenceCompatibility(t *testing.T) { t.Fatalf("shared-to-mutable ref compat = %v, want incompatible", got) } } + +func TestOptionalCompatibilityAllowsOneLayerPromotion(t *testing.T) { + i32 := &IntegerType{Signed: true, Bits: 32} + inner := &OptionalType{Inner: i32} + outer := &OptionalType{Inner: inner} + if CheckCompatibility(outer, inner) != Compatible { + t.Fatal("?T must promote into ??T as one intact payload layer") + } + if CheckCompatibility(inner, inner) != Compatible { + t.Fatal("exact optional carrier assignment must remain compatible") + } +} diff --git a/internal/semantics/typeinfo/types_test.go b/internal/semantics/typeinfo/types_test.go index bef4e75..25011af 100644 --- a/internal/semantics/typeinfo/types_test.go +++ b/internal/semantics/typeinfo/types_test.go @@ -77,6 +77,9 @@ func TestCopyCapabilitiesFollowStructuralModel(t *testing.T) { if !IsImplicitCopyType(i32) || !IsImplicitCopyType(&RawPtrType{}) || !IsImplicitCopyType(&RefType{Target: i32}) { t.Fatalf("scalar, raw pointer, and shared reference should copy implicitly") } + if !IsImplicitCopyType(&OptionalType{Inner: i32}) || IsImplicitCopyType(&OptionalType{Inner: &StructType{}}) { + t.Fatalf("optional copyability should follow payload copyability") + } if IsImplicitCopyType(&StructType{Fields: []Field{{Name: "value", Type: i32}}}) { t.Fatalf("struct should not copy implicitly") } diff --git a/x_test/negative_optional_index_invalidation/peeper.toml b/x_test/negative_optional_index_invalidation/peeper.toml new file mode 100644 index 0000000..e8e0c26 --- /dev/null +++ b/x_test/negative_optional_index_invalidation/peeper.toml @@ -0,0 +1,7 @@ +name = "negative_optional_index_invalidation" +build = "lib" + +[test] +mode = "check" +outcome = "failure" +stderr_contains = ["T0041", "optional payload use requires a presence proof"] diff --git a/x_test/negative_optional_index_invalidation/src/main.peep b/x_test/negative_optional_index_invalidation/src/main.peep new file mode 100644 index 0000000..fd05045 --- /dev/null +++ b/x_test/negative_optional_index_invalidation/src/main.peep @@ -0,0 +1,8 @@ +fn Invalidated(values: [2]?i32) -> i32 { + let mut index: usize = 0; + if values[index] != none { + index = 1; + return values[index]; + } + return 0; +} diff --git a/x_test/negative_optional_missing_proof/peeper.toml b/x_test/negative_optional_missing_proof/peeper.toml new file mode 100644 index 0000000..852ae6e --- /dev/null +++ b/x_test/negative_optional_missing_proof/peeper.toml @@ -0,0 +1,7 @@ +name = "negative_optional_missing_proof" +build = "lib" + +[test] +mode = "check" +outcome = "failure" +stderr_contains = ["T0041", "optional payload use requires a presence proof"] diff --git a/x_test/negative_optional_missing_proof/src/main.peep b/x_test/negative_optional_missing_proof/src/main.peep new file mode 100644 index 0000000..f17264d --- /dev/null +++ b/x_test/negative_optional_missing_proof/src/main.peep @@ -0,0 +1,3 @@ +fn MissingProof(value: ?i32) -> i32 { + return value; +} diff --git a/x_test/negative_optional_partial_move/peeper.toml b/x_test/negative_optional_partial_move/peeper.toml new file mode 100644 index 0000000..60b7986 --- /dev/null +++ b/x_test/negative_optional_partial_move/peeper.toml @@ -0,0 +1,7 @@ +name = "negative_optional_partial_move" +build = "lib" + +[test] +mode = "check" +outcome = "failure" +stderr_contains = ["T0035", "move-only optional payload"] diff --git a/x_test/negative_optional_partial_move/src/main.peep b/x_test/negative_optional_partial_move/src/main.peep new file mode 100644 index 0000000..c2bc632 --- /dev/null +++ b/x_test/negative_optional_partial_move/src/main.peep @@ -0,0 +1,15 @@ +struct Token { + value: i32 +} + +struct Holder { + token: ?Token +} + +fn Consume(_: Token) {} + +fn PartialMove(holder: Holder) { + if holder.token != none { + Consume(holder.token); + } +} diff --git a/x_test/negative_optional_unstable_index/peeper.toml b/x_test/negative_optional_unstable_index/peeper.toml new file mode 100644 index 0000000..1e65c3d --- /dev/null +++ b/x_test/negative_optional_unstable_index/peeper.toml @@ -0,0 +1,7 @@ +name = "negative_optional_unstable_index" +build = "lib" + +[test] +mode = "check" +outcome = "failure" +stderr_contains = ["T0042", "stable place"] diff --git a/x_test/negative_optional_unstable_index/src/main.peep b/x_test/negative_optional_unstable_index/src/main.peep new file mode 100644 index 0000000..61b8578 --- /dev/null +++ b/x_test/negative_optional_unstable_index/src/main.peep @@ -0,0 +1,6 @@ +fn Unstable(values: [2]?i32, index: usize) -> i32 { + if values[index + 1] != none { + return values[index + 1]; + } + return 0; +} diff --git a/x_test/negative_optional_use_after_consume/peeper.toml b/x_test/negative_optional_use_after_consume/peeper.toml new file mode 100644 index 0000000..7f43355 --- /dev/null +++ b/x_test/negative_optional_use_after_consume/peeper.toml @@ -0,0 +1,7 @@ +name = "negative_optional_use_after_consume" +build = "lib" + +[test] +mode = "check" +outcome = "failure" +stderr_contains = ["T0034", "moved"] diff --git a/x_test/negative_optional_use_after_consume/src/main.peep b/x_test/negative_optional_use_after_consume/src/main.peep new file mode 100644 index 0000000..69d77fd --- /dev/null +++ b/x_test/negative_optional_use_after_consume/src/main.peep @@ -0,0 +1,13 @@ +struct Token { + value: i32 +} + +fn Consume(_: Token) {} + +fn UseAfterConsume(value: ?Token) { + if value == none { + return; + } + Consume(value); + Consume(value); +} diff --git a/x_test/runtime_optional_narrowing/peeper.toml b/x_test/runtime_optional_narrowing/peeper.toml new file mode 100644 index 0000000..475c47c --- /dev/null +++ b/x_test/runtime_optional_narrowing/peeper.toml @@ -0,0 +1,6 @@ +name = "runtime_optional_narrowing" +build = "program" + +[test] +mode = "run" +outcome = "success" diff --git a/x_test/runtime_optional_narrowing/src/main.peep b/x_test/runtime_optional_narrowing/src/main.peep new file mode 100644 index 0000000..b869055 --- /dev/null +++ b/x_test/runtime_optional_narrowing/src/main.peep @@ -0,0 +1,109 @@ +struct Holder { + field: ?i32, + items: [2]?i32 +} + +struct Outer { + inner: Holder +} + +struct Token { + value: i32 +} + +fn Some(value: i32) -> ?i32 { + return value; +} + +fn Guard(value: ?i32) -> i32 { + if value == none { + return 10; + } + return value; +} + +fn Take(value: Token) -> i32 { + return value.value; +} + +fn TakeOptional(value: ?Token) -> i32 { + if value == none { + return 0; + } + return Take(value); +} + +fn main() -> i32 { + let maybe = Some(7); + if maybe == none { + return 1; + } + let carried: ?i32 = maybe; + if carried == none { + return 2; + } + if carried != 7 { + return 2; + } + let inferred = maybe; + if inferred != 7 { + return 2; + } + + let reversedEq = Some(7); + if none == reversedEq { + return 3; + } + if reversedEq != 7 { + return 4; + } + + let reversedNe = Some(7); + if none != reversedNe { + if reversedNe != 7 { + return 4; + } + } + + let holder = .Holder{field = Some(9), items = [2]?i32{Some(11), none}}; + if holder.field != none { + if holder.field != 9 { + return 5; + } + } + let nestedOuter = .Outer{inner = .Holder{field = Some(9), items = [2]?i32{none, none}}}; + if nestedOuter.inner.field != none { + if nestedOuter.inner.field != 9 { + return 5; + } + } + const index: usize = 0; + if holder.items[index] != none { + if holder.items[index] != 11 { + return 6; + } + } + let bindingIndex: usize = 0; + if holder.items[bindingIndex] != none { + if holder.items[bindingIndex] != 11 { + return 6; + } + } + + let inner: ?i32 = Some(13); + let outer: ? ?i32 = inner; + if outer != none { + if outer != none { + if outer != 13 { + return 7; + } + } + } + + let ownerResult = TakeOptional(.Token{value = 23}); + if ownerResult != 23 { + return 8; + } + + return Guard(Some(17)) - 17; +}