From 6158db0b95a6188b5829e77f72508421685f4415 Mon Sep 17 00:00:00 2001 From: Fuad Hasan Date: Mon, 24 Aug 2026 00:23:50 +0600 Subject: [PATCH] Add shared tagged variant core --- internal/backend/llvm/drop_emit.go | 66 ++- internal/backend/llvm/emitter.go | 44 ++ internal/backend/llvm/emitter_test.go | 183 +++++++- internal/backend/llvm/instruction_emit.go | 52 ++- internal/backend/llvm/type_layout.go | 78 +++- internal/backend/llvm/typed_builder.go | 62 +++ internal/ir/cfg/build.go | 40 +- internal/ir/cfg/cfg_test.go | 22 + internal/ir/cfg/model.go | 34 +- internal/ir/constfold.go | 8 +- internal/ir/fold_test.go | 2 +- internal/ir/hir/lower/lower_types.go | 29 +- internal/ir/hir/lower/module_lower.go | 46 +- internal/ir/hir/lower/module_lower_test.go | 79 +++- internal/ir/hir/model.go | 57 ++- internal/ir/hir/model_test.go | 25 ++ internal/ir/inspect_test.go | 2 +- internal/ir/mir/model.go | 62 ++- internal/ir/mir/module_lower.go | 37 +- internal/ir/mir/module_lower_test.go | 81 +++- internal/ir/nodes.go | 88 ++-- internal/ir/types.go | 104 ++++- internal/ir/types_test.go | 42 ++ internal/project/modules.go | 8 + internal/semantics/binder/binder.go | 2 + internal/semantics/collector/collector.go | 3 +- .../semantics/collector/collector_test.go | 29 ++ internal/semantics/flowresult/result.go | 22 +- internal/semantics/ownership/expr.go | 8 +- .../semantics/ownership/ownership_test.go | 2 +- internal/semantics/place/origin.go | 23 +- internal/semantics/place/origin_test.go | 12 + internal/semantics/typechecker/flow.go | 418 ++++++++++++------ internal/semantics/typechecker/flow_test.go | 43 +- internal/semantics/typeinfo/types.go | 61 +++ internal/semantics/typeinfo/types_test.go | 19 + .../runtime_optional_narrowing/src/main.peep | 34 ++ 37 files changed, 1540 insertions(+), 387 deletions(-) diff --git a/internal/backend/llvm/drop_emit.go b/internal/backend/llvm/drop_emit.go index 210e90c..bcec9bc 100644 --- a/internal/backend/llvm/drop_emit.go +++ b/internal/backend/llvm/drop_emit.go @@ -68,8 +68,8 @@ func emitDropValue(b *llvmBuilder, value llvmValue, typeID ir.TypeID) { emitOwnedPointerFree(b, value, typ.Elem) return } - if typ.Kind == ir.TypeOptional { - emitOptionalDrop(b, value, typ.Elem) + if typ.Kind == ir.TypeVariant { + emitVariantDrop(b, value, typ) return } if typ.Kind == ir.TypeArray && typ.Length == "" { @@ -161,24 +161,34 @@ func emitInterfaceStorageRelease(b *llvmBuilder, interfaceType ir.TypeID, interf b.call(releaseFn, []llvmValue{allocator, data}) } -func emitOptionalDrop(b *llvmBuilder, value llvmValue, inner ir.TypeID) { - if !typeNeedsDrop(b.emitter.mod.Types, inner) { +func emitVariantDrop(b *llvmBuilder, value llvmValue, variant ir.Type) { + dropCases := make([]int, 0, len(variant.Cases)) + for caseIndex, variantCase := range variant.Cases { + if variantCase.Payload != ir.InvalidType && typeNeedsDrop(b.emitter.mod.Types, variantCase.Payload) { + dropCases = append(dropCases, caseIndex) + } + } + if len(dropCases) == 0 { return } - present := b.extractField(value, llvmFieldPresent) - payload := b.extractField(value, llvmFieldValue) - emitConditionalDrop(b, present, payload, inner) -} - -func emitConditionalDrop(b *llvmBuilder, condition, value llvmValue, typeID ir.TypeID) { id := b.nextID b.nextID++ - dropLabel := fmt.Sprintf("drop_some_%d", id) - doneLabel := fmt.Sprintf("drop_done_%d", id) - b.condBranch(condition, dropLabel, doneLabel) - b.namedLabel(dropLabel) - emitDropValue(b, value, typeID) - b.branch(doneLabel) + tag := b.variantTag(value) + doneLabel := fmt.Sprintf("drop_variant_done_%d", id) + switchCases := make([]llvmSwitchCase, len(dropCases)) + for i, caseIndex := range dropCases { + switchCases[i] = llvmSwitchCase{ + Value: b.variantCaseTag(caseIndex, tag.Layout), + Label: fmt.Sprintf("drop_variant_%d_case_%d", id, caseIndex), + } + } + b.switchBranch(tag, doneLabel, switchCases) + for i, caseIndex := range dropCases { + b.namedLabel(switchCases[i].Label) + variantCase := variant.Cases[caseIndex] + emitDropValue(b, b.variantPayload(value, caseIndex), variantCase.Payload) + b.branch(doneLabel) + } b.namedLabel(doneLabel) } @@ -249,8 +259,12 @@ func typeNeedsDrop(types *ir.TypeTable, id ir.TypeID) bool { switch typ.Kind { case ir.TypeOwnedPtr, ir.TypeString: return true - case ir.TypeOptional: - return typeNeedsDrop(types, typ.Elem) + case ir.TypeVariant: + for _, variantCase := range typ.Cases { + if variantCase.Payload != ir.InvalidType && typeNeedsDrop(types, variantCase.Payload) { + return true + } + } case ir.TypeArray: return typ.Length == "" || typeNeedsDrop(types, typ.Elem) case ir.TypeStruct: @@ -273,8 +287,12 @@ func typeCarriesAllocatorID(types *ir.TypeTable, id ir.TypeID) bool { return true case ir.TypeOwnedPtr: return !isInterfaceType(types, typ.Elem) - case ir.TypeOptional: - return typeCarriesAllocatorID(types, typ.Elem) + case ir.TypeVariant: + for _, variantCase := range typ.Cases { + if variantCase.Payload != ir.InvalidType && typeCarriesAllocatorID(types, variantCase.Payload) { + return true + } + } case ir.TypeArray: return typ.Length == "" || typeCarriesAllocatorID(types, typ.Elem) case ir.TypeStruct: @@ -295,8 +313,12 @@ func typeNeedsRawFreeID(types *ir.TypeTable, id ir.TypeID) bool { switch typ.Kind { case ir.TypeOwnedPtr: return !isInterfaceType(types, typ.Elem) && typeNeedsRawFreeID(types, typ.Elem) - case ir.TypeOptional: - return typeNeedsRawFreeID(types, typ.Elem) + case ir.TypeVariant: + for _, variantCase := range typ.Cases { + if variantCase.Payload != ir.InvalidType && typeNeedsRawFreeID(types, variantCase.Payload) { + return true + } + } case ir.TypeArray: return typ.Length == "" || typeNeedsRawFreeID(types, typ.Elem) case ir.TypeStruct: diff --git a/internal/backend/llvm/emitter.go b/internal/backend/llvm/emitter.go index 082ee7e..16a8132 100644 --- a/internal/backend/llvm/emitter.go +++ b/internal/backend/llvm/emitter.go @@ -316,6 +316,8 @@ func GenerateLLVMIR(mod *mir.Module, diag *diagnostics.DiagnosticBag, targetInfo case *mir.Branch: cond := emitCondRef(lb, term.Cond) lb.condBranch(cond, fmt.Sprintf("b%d", term.ThenID), fmt.Sprintf("b%d", term.ElseID)) + case *mir.SwitchVariant: + emitVariantSwitch(lb, term) case *mir.Ret: if term.Value == nil || isVoidType(mod.Types, fn.ReturnType) { if returnLayout.Kind != llvmLayoutVoid { @@ -336,6 +338,48 @@ func GenerateLLVMIR(mod *mir.Module, diag *diagnostics.DiagnosticBag, targetInfo return finalLLVMText(&b, emitter) } +func emitVariantSwitch(b *llvmBuilder, term *mir.SwitchVariant) { + if b == nil || term == nil || term.Value == nil || len(term.Targets) == 0 { + if b != nil { + b.emitter.markInvalid("variant switch requires subject and targets") + } + return + } + variant, ok := b.emitter.mod.Types.Type(mirRefType(term.Value)) + if !ok || variant.Kind != ir.TypeVariant { + b.emitter.markInvalid("variant switch requires variant subject") + return + } + if len(term.Targets) != len(variant.Cases) { + b.emitter.markInvalid("variant switch must cover every case") + return + } + value := emitRef(b, term.Value) + tag := b.variantTag(value) + cases := make([]llvmSwitchCase, len(term.Targets)) + seen := make(map[int]struct{}, len(term.Targets)) + for i, target := range term.Targets { + if _, caseOK := variant.VariantCase(target.Case); !caseOK { + b.emitter.markInvalid(fmt.Sprintf("variant switch has invalid case %d", target.Case)) + return + } + if _, duplicate := seen[target.Case]; duplicate { + b.emitter.markInvalid(fmt.Sprintf("variant switch repeats case %d", target.Case)) + return + } + seen[target.Case] = struct{}{} + cases[i] = llvmSwitchCase{ + Value: b.variantCaseTag(target.Case, tag.Layout), + Label: fmt.Sprintf("b%d", target.TargetID), + } + } + invalidLabel := fmt.Sprintf("invalid_variant_%d", b.nextID) + b.nextID++ + b.switchBranch(tag, invalidLabel, cases) + b.namedLabel(invalidLabel) + b.trap() +} + // ValidateRuntimeSymbols checks runtime ABI reservations after ownership and // lowering have made actual print, allocation, and destruction use explicit. func ValidateRuntimeSymbols(modules []*mir.Module, diag *diagnostics.DiagnosticBag) bool { diff --git a/internal/backend/llvm/emitter_test.go b/internal/backend/llvm/emitter_test.go index d834156..9f1de1a 100644 --- a/internal/backend/llvm/emitter_test.go +++ b/internal/backend/llvm/emitter_test.go @@ -67,8 +67,8 @@ func newLLVMTypeFixture(indexBits int) llvmTypeFixture { u128: table.Intern(ir.Type{Kind: ir.TypeInteger, Bits: 128}), usize: usize, ownedI32: table.Intern(ir.Type{Kind: ir.TypeOwnedPtr, Elem: i32}), - optionalI32: table.Intern(ir.Type{Kind: ir.TypeOptional, Elem: i32}), - optionalOwnedI32: table.Intern(ir.Type{Kind: ir.TypeOptional, Elem: table.Intern(ir.Type{Kind: ir.TypeOwnedPtr, Elem: i32})}), + optionalI32: table.Intern(ir.OptionalVariant(i32)), + optionalOwnedI32: table.Intern(ir.OptionalVariant(table.Intern(ir.Type{Kind: ir.TypeOwnedPtr, Elem: i32}))), dynamicI32: dynamicI32, dynamicDynamicI32: table.Intern(ir.Type{Kind: ir.TypeArray, Elem: dynamicI32}), fixed3I32: table.Intern(ir.Type{Kind: ir.TypeArray, Elem: i32, Length: "3"}), @@ -112,9 +112,9 @@ func TestLLVMLayoutModelTypes(t *testing.T) { {types.Intern(ir.Type{Kind: ir.TypeInteger, Bits: 8388608}), "i8388608"}, {llvmTypes.stringType, "{ i8*, i64, i8* }"}, {llvmTypes.optionalI32, "{ i1, i32 }"}, - {types.Intern(ir.Type{Kind: ir.TypeOptional, Elem: llvmTypes.stringType}), "{ i1, { i8*, i64, i8* } }"}, + {types.Intern(ir.OptionalVariant(llvmTypes.stringType)), "{ i1, { i8*, i64, i8* } }"}, {llvmTypes.optionalOwnedI32, "{ i1, { i32*, i8* } }"}, - {types.Intern(ir.Type{Kind: ir.TypeOptional, Elem: ownedInterface}), "{ i1, { i8*, i8*, i8* } }"}, + {types.Intern(ir.OptionalVariant(ownedInterface)), "{ i1, { i8*, i8*, i8* } }"}, {llvmTypes.ownedI32, "{ i32*, i8* }"}, {ownedInterface, "{ i8*, i8*, i8* }"}, {llvmTypes.rawptr, "i8*"}, @@ -127,7 +127,7 @@ func TestLLVMLayoutModelTypes(t *testing.T) { {llvmTypes.refSliceI32, "{ i32*, i64 }"}, {llvmTypes.mutRefSliceI32, "{ i32*, i64 }"}, {types.Intern(ir.Type{Kind: ir.TypeOwnedPtr, Elem: llvmTypes.stringType}), "{ { i8*, i64, i8* }*, i8* }"}, - {types.Intern(ir.Type{Kind: ir.TypeArray, Elem: types.Intern(ir.Type{Kind: ir.TypeOptional, Elem: llvmTypes.stringType})}), "{ { i1, { i8*, i64, i8* } }*, i64, i64, i8* }"}, + {types.Intern(ir.Type{Kind: ir.TypeArray, Elem: types.Intern(ir.OptionalVariant(llvmTypes.stringType))}), "{ { i1, { i8*, i64, i8* } }*, i64, i64, i8* }"}, {types.Intern(ir.Type{Kind: ir.TypeStruct, Fields: []ir.TypeField{{Name: "x", Type: types.Intern(ir.Type{Kind: ir.TypeArray, Elem: llvmTypes.u8, Length: "2"})}}}), "{ [2 x i8] }"}, } for _, tt := range cases { @@ -138,6 +138,30 @@ func TestLLVMLayoutModelTypes(t *testing.T) { } } +func TestLLVMLayoutUsesTypedVariantCaseSlots(t *testing.T) { + types := ir.NewTypeTable() + i32 := types.Intern(ir.Type{Kind: ir.TypeInteger, Signed: true, Bits: 32}) + str := types.Intern(ir.Type{Kind: ir.TypeString}) + index := types.Intern(ir.Type{Kind: ir.TypeInteger, Bits: 64}) + types.SetIndexType(index) + result := types.Intern(ir.Type{ + Kind: ir.TypeVariant, Family: ir.VariantFamilyNamed, Name: "Result", Identity: "test::Result", + Cases: []ir.VariantCase{ + {Name: "Ok", Payload: i32}, + {Name: "Error", Payload: str}, + {Name: "Pending"}, + }, + }) + + layout, ok := llvmLayoutID(types, result) + if !ok || layout.Text != "{ i8, i32, { i8*, i64, i8* } }" { + t.Fatalf("variant layout = (%v, %t)", layout, ok) + } + if layout.VariantTag != 0 || layout.VariantPayloads[0] != 1 || layout.VariantPayloads[1] != 2 { + t.Fatalf("variant physical fields = tag %d, payloads %#v", layout.VariantTag, layout.VariantPayloads) + } +} + func TestLLVMLayoutUsesContextSizedUsize(t *testing.T) { for _, tt := range []struct { name string @@ -935,7 +959,7 @@ func TestGenerateLLVMIRLowersOwnedPointerStructLayout(t *testing.T) { } func TestGenerateLLVMIRLowersOptionalOwnedPointerAsTagged(t *testing.T) { - optionalOwnedI32 := llvmTypes.table.Intern(ir.Type{Kind: ir.TypeOptional, Elem: llvmTypes.ownedI32}) + optionalOwnedI32 := llvmTypes.table.Intern(ir.OptionalVariant(llvmTypes.ownedI32)) mod := &mir.Module{ Name: "test", Types: llvmTypes.table, Funcs: []*mir.Function{{ @@ -1124,7 +1148,7 @@ func TestGenerateLLVMIRAcceptsRawExternBoundaries(t *testing.T) { func TestGenerateLLVMIRUsesCarriedAllocatorForInterfaceDrops(t *testing.T) { iface := llvmTypes.table.Intern(ir.Type{Kind: ir.TypeInterface, Methods: []ir.TypeMethod{{Name: "take", Params: []ir.TypeField{{Name: "self", Type: llvmTypes.valueStruct}}, Return: llvmTypes.void}}}) ownedIface := llvmTypes.table.Intern(ir.Type{Kind: ir.TypeOwnedPtr, Elem: iface}) - optionalOwnedIface := llvmTypes.table.Intern(ir.Type{Kind: ir.TypeOptional, Elem: ownedIface}) + optionalOwnedIface := llvmTypes.table.Intern(ir.OptionalVariant(ownedIface)) for _, tt := range []struct { name string typeID ir.TypeID @@ -1680,7 +1704,7 @@ func TestGenerateLLVMIRLowersZeroValueOptionals(t *testing.T) { } } -func TestGenerateLLVMIRLowersOptionalSome(t *testing.T) { +func TestGenerateLLVMIRLowersVariantMake(t *testing.T) { const targetTriple = "x86_64-unknown-linux-gnu" mod := &mir.Module{ Name: "test", @@ -1694,7 +1718,7 @@ func TestGenerateLLVMIRLowersOptionalSome(t *testing.T) { Blocks: []*mir.Block{{ 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: "x", Value: &mir.VariantMake{Case: ir.OptionalPresentCase, Payload: &mir.RefConst{Value: "7", Type: llvmTypes.i32}, Type: llvmTypes.optionalI32}}, }, Term: &mir.Ret{Value: &mir.RefName{Name: "x", Type: llvmTypes.optionalI32}}, }}, @@ -1707,7 +1731,7 @@ func TestGenerateLLVMIRLowersOptionalSome(t *testing.T) { Blocks: []*mir.Block{{ ID: 0, Instrs: []mir.Instr{ - &mir.Assign{Name: "x", Value: &mir.OptionalSome{Value: &mir.RefName{Name: "p", Type: llvmTypes.ownedI32}, Type: llvmTypes.optionalOwnedI32}}, + &mir.Assign{Name: "x", Value: &mir.VariantMake{Case: ir.OptionalPresentCase, Payload: &mir.RefName{Name: "p", Type: llvmTypes.ownedI32}, Type: llvmTypes.optionalOwnedI32}}, }, Term: &mir.Ret{Value: &mir.RefName{Name: "x", Type: llvmTypes.optionalOwnedI32}}, }}, @@ -1744,9 +1768,10 @@ func TestGenerateLLVMIRReadsTaggedOptionalPresence(t *testing.T) { Blocks: []*mir.Block{{ 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: "present", Value: &mir.OptionalPresent{ + &mir.Assign{Name: "x", Value: &mir.VariantMake{Case: ir.OptionalPresentCase, Payload: &mir.RefConst{Value: "7", Type: llvmTypes.i32}, Type: llvmTypes.optionalI32}}, + &mir.Assign{Name: "present", Value: &mir.VariantIs{ Value: &mir.RefName{Name: "x", Type: llvmTypes.optionalI32}, + Case: ir.OptionalPresentCase, Type: llvmTypes.boolType, }}, }, @@ -1776,7 +1801,7 @@ func TestGenerateLLVMIRLoadsTaggedOptionalPayload(t *testing.T) { Place: &mir.Place{ Root: &mir.RefName{Name: "value", Type: llvmTypes.optionalI32}, Projections: []mir.PlaceProjection{ - {Kind: mir.PlaceProjectionOptionalPayload, Type: llvmTypes.i32}, + {Kind: mir.PlaceProjectionVariantPayload, Case: ir.OptionalPresentCase, Type: llvmTypes.i32}, }, Type: llvmTypes.i32, }, @@ -2688,3 +2713,135 @@ func TestGenerateLLVMIRLowersAlloc(t *testing.T) { t.Fatalf("expected null check for allocation, got:\n%s", out) } } + +func TestGenerateLLVMIRLowersSwitchVariant(t *testing.T) { + status := llvmTypes.table.Intern(ir.Type{ + Kind: ir.TypeVariant, Family: ir.VariantFamilyNamed, Name: "Status", Identity: "test::Status", + Cases: []ir.VariantCase{{Name: "Ready"}, {Name: "Waiting"}}, + }) + mod := &mir.Module{ + Name: "test", Types: llvmTypes.table, + Funcs: []*mir.Function{{ + Name: "select", Params: []ir.Param{{Name: "status", Type: status}}, ReturnType: llvmTypes.i32, EntryID: 0, + Blocks: []*mir.Block{ + {ID: 0, Term: &mir.SwitchVariant{Value: &mir.RefName{Name: "status", Type: status}, Targets: []mir.VariantTarget{{Case: 0, TargetID: 1}, {Case: 1, TargetID: 2}}}}, + {ID: 1, Term: &mir.Ret{Value: &mir.RefConst{Value: "1", Type: llvmTypes.i32}}}, + {ID: 2, Term: &mir.Ret{Value: &mir.RefConst{Value: "2", Type: llvmTypes.i32}}}, + }, + }}, + } + out := GenerateLLVMIR(mod, diagnostics.NewDiagnosticBag(), testLinuxAMD64, false) + if !strings.Contains(out, "switch i8") || !strings.Contains(out, "i8 0, label %b1") || + !strings.Contains(out, "i8 1, label %b2") || !strings.Contains(out, "call void @llvm.trap()") { + t.Fatalf("expected tagged switch with invalid-tag trap, got:\n%s", out) + } +} + +func TestGenerateLLVMIRRejectsIncompleteVariantSwitch(t *testing.T) { + status := llvmTypes.table.Intern(ir.Type{ + Kind: ir.TypeVariant, Family: ir.VariantFamilyNamed, Name: "Status", Identity: "test::IncompleteStatus", + Cases: []ir.VariantCase{{Name: "Ready"}, {Name: "Waiting"}}, + }) + mod := &mir.Module{ + Name: "test", Types: llvmTypes.table, + Funcs: []*mir.Function{{ + Name: "select", Params: []ir.Param{{Name: "status", Type: status}}, ReturnType: llvmTypes.i32, EntryID: 0, + Blocks: []*mir.Block{ + {ID: 0, Term: &mir.SwitchVariant{Value: &mir.RefName{Name: "status", Type: status}, Targets: []mir.VariantTarget{{Case: 0, TargetID: 1}}}}, + {ID: 1, Term: &mir.Ret{Value: &mir.RefConst{Value: "1", Type: llvmTypes.i32}}}, + }, + }}, + } + diag := diagnostics.NewDiagnosticBag() + if out := GenerateLLVMIR(mod, diag, testLinuxAMD64, false); out != "" { + t.Fatalf("incomplete variant switch must suppress LLVM output, got:\n%s", out) + } + if !diag.HasErrors() || !strings.Contains(diag.EmitAllToString(), "cover every case") { + t.Fatalf("incomplete variant switch diagnostic missing:\n%s", diag.EmitAllToString()) + } +} + +func TestLLVMVariantTagWidthsAndEmptyRejection(t *testing.T) { + types := ir.NewTypeTable() + for _, test := range []struct { + name string + count int + want string + }{ + {name: "i8 maximum", count: 256, want: "i8"}, + {name: "i16 minimum", count: 257, want: "i16"}, + {name: "i16 maximum", count: 65536, want: "i16"}, + {name: "i32 minimum", count: 65537, want: "i32"}, + } { + t.Run(test.name, func(t *testing.T) { + variant := ir.Type{ + Kind: ir.TypeVariant, Family: ir.VariantFamilyNamed, Name: "Many", Identity: "test::Many", + Cases: make([]ir.VariantCase, test.count), + } + layout, ok := llvmVariantLayout(types, variant) + if !ok || layout.Elements[layout.VariantTag].Text != test.want { + t.Fatalf("tag layout for %d cases = %#v, want %s", test.count, layout, test.want) + } + }) + } + if layout, ok := llvmVariantLayout(types, ir.Type{Kind: ir.TypeVariant, Family: ir.VariantFamilyNamed, Identity: "test::Empty"}); ok || layout != nil { + t.Fatalf("empty variant layout = %#v, %v; want rejection", layout, ok) + } +} + +func TestGenerateLLVMIRLowersNamedVariantOperationsAndDrop(t *testing.T) { + result := llvmTypes.table.Intern(ir.Type{ + Kind: ir.TypeVariant, Family: ir.VariantFamilyNamed, Name: "Result", Identity: "test::Result", + Cases: []ir.VariantCase{ + {Name: "Value", Payload: llvmTypes.i32}, + {Name: "Owned", Payload: llvmTypes.ownedI32}, + {Name: "Pending"}, + }, + }) + mod := &mir.Module{ + Name: "test", Types: llvmTypes.table, + Funcs: []*mir.Function{ + { + Name: "make_owned", Params: []ir.Param{{Name: "payload", Type: llvmTypes.ownedI32}}, ReturnType: result, + Blocks: []*mir.Block{{ID: 0, Instrs: []mir.Instr{&mir.Assign{ + Name: "result", Value: &mir.VariantMake{Case: 1, Payload: &mir.RefName{Name: "payload", Type: llvmTypes.ownedI32}, Type: result}, + }}, Term: &mir.Ret{Value: &mir.RefName{Name: "result", Type: result}}}}, + }, + { + Name: "is_owned", Params: []ir.Param{{Name: "result", Type: result}}, ReturnType: llvmTypes.boolType, + Blocks: []*mir.Block{{ID: 0, Instrs: []mir.Instr{&mir.Assign{ + Name: "owned", Value: &mir.VariantIs{Value: &mir.RefName{Name: "result", Type: result}, Case: 1, Type: llvmTypes.boolType}, + }}, Term: &mir.Ret{Value: &mir.RefName{Name: "owned", Type: llvmTypes.boolType}}}}, + }, + { + Name: "owned_payload", Params: []ir.Param{{Name: "result", Type: result}}, ReturnType: llvmTypes.ownedI32, + Blocks: []*mir.Block{{ID: 0, Instrs: []mir.Instr{&mir.Assign{ + Name: "payload", Value: &mir.Load{Place: &mir.Place{ + Root: &mir.RefName{Name: "result", Type: result}, + Projections: []mir.PlaceProjection{{Kind: mir.PlaceProjectionVariantPayload, Case: 1, Type: llvmTypes.ownedI32}}, + Type: llvmTypes.ownedI32, + }, Type: llvmTypes.ownedI32}, + }}, Term: &mir.Ret{Value: &mir.RefName{Name: "payload", Type: llvmTypes.ownedI32}}}}, + }, + { + Name: "release", Params: []ir.Param{{Name: "result", Type: result}}, ReturnType: llvmTypes.void, + Blocks: []*mir.Block{{ID: 0, Instrs: []mir.Instr{&mir.Drop{Value: &mir.RefName{Name: "result", Type: result}}}, Term: &mir.Ret{}}}, + }, + }, + } + out := GenerateLLVMIR(mod, diagnostics.NewDiagnosticBag(), testLinuxAMD64, false) + for _, expected := range []string{ + "insertvalue { i8, i32, { i32*, i8* } } zeroinitializer, i8 1, 0", + "insertvalue { i8, i32, { i32*, i8* } }", + "extractvalue { i8, i32, { i32*, i8* } } %result, 0", + "icmp eq i8", + "getelementptr inbounds { i8, i32, { i32*, i8* } }, { i8, i32, { i32*, i8* } }*", + "i32 0, i32 2", + "switch i8", + "extractvalue { i8, i32, { i32*, i8* } } %result, 2", + } { + if !strings.Contains(out, expected) { + t.Fatalf("missing %q in named variant LLVM:\n%s", expected, out) + } + } +} diff --git a/internal/backend/llvm/instruction_emit.go b/internal/backend/llvm/instruction_emit.go index 4c28728..bfd8bb1 100644 --- a/internal/backend/llvm/instruction_emit.go +++ b/internal/backend/llvm/instruction_emit.go @@ -181,7 +181,7 @@ func placeNeedsRootAddr(types *ir.TypeTable, place *mir.Place) bool { return false case mir.PlaceProjectionField: return true - case mir.PlaceProjectionOptionalPayload: + case mir.PlaceProjectionVariantPayload: return true case mir.PlaceProjectionIndex: rootType, ok := types.Type(mirRefType(place.Root)) @@ -262,12 +262,17 @@ func emitPlacePtr(b *llvmBuilder, place *mir.Place) (llvmPlace, bool) { } hasCurrent = true addressed = true - case mir.PlaceProjectionOptionalPayload: + case mir.PlaceProjectionVariantPayload: if !hasCurrent { - b.emitter.markInvalid("optional payload place requires addressable storage") + b.emitter.markInvalid("variant payload place requires addressable storage") return llvmPlace{}, false } - current = b.namedFieldPlace(current, llvmFieldValue) + index, ok := current.Pointee.VariantPayloads[projection.Case] + if !ok { + b.emitter.markInvalid(fmt.Sprintf("variant case %d has no payload", projection.Case)) + return llvmPlace{}, false + } + current = b.fieldPlace(current, index) default: b.emitter.markInvalid(fmt.Sprintf("unsupported MIR place projection %d", projection.Kind)) return llvmPlace{}, false @@ -664,15 +669,40 @@ func emitValueExpr(b *llvmBuilder, expr mir.ValueExpr) llvmValue { return emitAlloc(b, e) case *mir.ZeroValue: return b.zero(b.emitter.layout(e.Type)) - case *mir.OptionalSome: - optional, ok := b.emitter.mod.Types.Type(e.Type) - if !ok || optional.Kind != ir.TypeOptional { + case *mir.VariantMake: + variant, ok := b.emitter.mod.Types.Type(e.Type) + variantCase, caseOK := variant.VariantCase(e.Case) + if !ok || variant.Kind != ir.TypeVariant || !caseOK { + b.emitter.markInvalid("variant construction has invalid type or case") return b.value("0", b.emitter.layout(e.Type)) } - 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) + layout := b.emitter.layout(e.Type) + value := b.zero(layout) + tagLayout := layout.Elements[layout.VariantTag] + value = b.insertIndex(value, b.variantCaseTag(e.Case, tagLayout), layout.VariantTag) + if variantCase.Payload == ir.InvalidType { + if e.Payload != nil { + b.emitter.markInvalid("payloadless variant case has payload") + } + return value + } + if e.Payload == nil { + b.emitter.markInvalid("variant data case requires payload") + return value + } + return b.insertVariantPayload(value, emitRef(b, e.Payload), e.Case) + case *mir.VariantIs: + value := emitRef(b, e.Value) + variant, ok := b.emitter.mod.Types.Type(mirRefType(e.Value)) + if _, caseOK := variant.VariantCase(e.Case); !ok || variant.Kind != ir.TypeVariant || !caseOK { + b.emitter.markInvalid("variant test has invalid type or case") + return b.value("false", llvmScalarLayout("i1")) + } + tag := b.variantTag(value) + if tag.Layout.Text == "i1" && e.Case == ir.OptionalPresentCase { + return tag + } + return b.compare("icmp", "eq", tag, b.variantCaseTag(e.Case, tag.Layout)) case *mir.InterfaceMake: value := emitRef(b, e.Value) dataPtr := value diff --git a/internal/backend/llvm/type_layout.go b/internal/backend/llvm/type_layout.go index 51aff5f..8168c93 100644 --- a/internal/backend/llvm/type_layout.go +++ b/internal/backend/llvm/type_layout.go @@ -27,6 +27,7 @@ const ( llvmFieldLength llvmFieldName = "length" llvmFieldCapacity llvmFieldName = "capacity" llvmFieldAllocator llvmFieldName = "allocator" + llvmFieldTag llvmFieldName = "tag" llvmFieldPresent llvmFieldName = "present" llvmFieldValue llvmFieldName = "value" llvmFieldDispatch llvmFieldName = "dispatch" @@ -35,14 +36,16 @@ const ( // llvmLayout is backend-owned physical type evidence. Named carrier fields // keep ABI field knowledge out of lowering and drop emitters. type llvmLayout struct { - Text string - Kind llvmLayoutKind - Pointee *llvmLayout - Element *llvmLayout - Elements []*llvmLayout - Fields map[llvmFieldName]int - Return *llvmLayout - Parameters []*llvmLayout + Text string + Kind llvmLayoutKind + Pointee *llvmLayout + Element *llvmLayout + Elements []*llvmLayout + Fields map[llvmFieldName]int + VariantTag int + VariantPayloads map[int]int + Return *llvmLayout + Parameters []*llvmLayout } func llvmScalarLayout(text string) *llvmLayout { @@ -188,14 +191,8 @@ func llvmLayoutID(types *ir.TypeTable, id ir.TypeID) (*llvmLayout, bool) { return nil, false } return llvmPointerLayout(elem), true - case ir.TypeOptional: - inner, ok := llvmLayoutID(types, typ.Elem) - if !ok { - return nil, false - } - return llvmAggregateLayout([]*llvmLayout{llvmScalarLayout("i1"), inner}, map[llvmFieldName]int{ - llvmFieldPresent: 0, llvmFieldValue: 1, - }), true + case ir.TypeVariant: + return llvmVariantLayout(types, typ) case ir.TypeArray: elem, ok := llvmLayoutID(types, typ.Elem) if !ok { @@ -246,6 +243,51 @@ func llvmLayoutID(types *ir.TypeTable, id ir.TypeID) (*llvmLayout, bool) { } } +func llvmVariantLayout(types *ir.TypeTable, typ ir.Type) (*llvmLayout, bool) { + if len(typ.Cases) == 0 { + return nil, false + } + if payload, optional := typ.OptionalPayload(); optional { + payloadLayout, ok := llvmLayoutID(types, payload) + if !ok { + return nil, false + } + layout := llvmAggregateLayout([]*llvmLayout{llvmScalarLayout("i1"), payloadLayout}, map[llvmFieldName]int{ + llvmFieldPresent: 0, llvmFieldValue: 1, + }) + layout.VariantTag = 0 + layout.VariantPayloads = map[int]int{ir.OptionalPresentCase: 1} + return layout, true + } + if typ.Family != ir.VariantFamilyNamed || typ.Identity == "" { + return nil, false + } + tag := llvmScalarLayout("i32") + switch { + case len(typ.Cases) <= 256: + tag = llvmScalarLayout("i8") + case len(typ.Cases) <= 65536: + tag = llvmScalarLayout("i16") + } + elements := []*llvmLayout{tag} + payloads := make(map[int]int) + for caseIndex, variant := range typ.Cases { + if variant.Payload == ir.InvalidType { + continue + } + payload, ok := llvmLayoutID(types, variant.Payload) + if !ok { + return nil, false + } + payloads[caseIndex] = len(elements) + elements = append(elements, payload) + } + layout := llvmAggregateLayout(elements, map[llvmFieldName]int{llvmFieldTag: 0}) + layout.VariantTag = 0 + layout.VariantPayloads = payloads + return layout, true +} + func isInterfaceType(types *ir.TypeTable, id ir.TypeID) bool { typ, ok := types.Type(id) return ok && typ.Kind == ir.TypeInterface @@ -431,9 +473,9 @@ func mirValueType(expr mir.ValueExpr) ir.TypeID { return v.Type case *mir.ZeroValue: return v.Type - case *mir.OptionalSome: + case *mir.VariantMake: return v.Type - case *mir.OptionalPresent: + case *mir.VariantIs: return v.Type case *mir.InterfaceMake: return v.Type diff --git a/internal/backend/llvm/typed_builder.go b/internal/backend/llvm/typed_builder.go index 2cf1d02..94fcb41 100644 --- a/internal/backend/llvm/typed_builder.go +++ b/internal/backend/llvm/typed_builder.go @@ -20,6 +20,11 @@ type llvmIncoming struct { Label string } +type llvmSwitchCase struct { + Value llvmValue + Label string +} + func llvmLayoutsMatch(left, right *llvmLayout) bool { return left != nil && right != nil && left.Kind == right.Kind && left.Text == right.Text } @@ -147,6 +152,49 @@ func (b *llvmBuilder) insertField(aggregate, value llvmValue, field llvmFieldNam return b.insertIndex(aggregate, value, index) } +func (b *llvmBuilder) variantTag(value llvmValue) llvmValue { + if value.Layout == nil || value.Layout.VariantTag < 0 { + b.invariant("variant tag requires variant layout") + } + return b.extractIndex(value, value.Layout.VariantTag) +} + +func (b *llvmBuilder) variantCaseTag(caseIndex int, layout *llvmLayout) llvmValue { + if caseIndex < 0 || layout == nil || layout.Kind != llvmLayoutScalar { + b.invariant("variant case tag requires nonnegative case and scalar layout") + } + text := fmt.Sprintf("%d", caseIndex) + if layout.Text == "i1" { + if caseIndex > 1 { + b.invariant("i1 variant tag cannot represent case %d", caseIndex) + } + text = fmt.Sprintf("%t", caseIndex != 0) + } + return b.value(text, layout) +} + +func (b *llvmBuilder) variantPayload(value llvmValue, caseIndex int) llvmValue { + if value.Layout == nil { + b.invariant("variant payload requires variant layout") + } + index, ok := value.Layout.VariantPayloads[caseIndex] + if !ok { + b.invariant("variant layout %s has no payload for case %d", value.Layout.Text, caseIndex) + } + return b.extractIndex(value, index) +} + +func (b *llvmBuilder) insertVariantPayload(value, payload llvmValue, caseIndex int) llvmValue { + if value.Layout == nil { + b.invariant("variant payload insert requires variant layout") + } + index, ok := value.Layout.VariantPayloads[caseIndex] + if !ok { + b.invariant("variant layout %s has no payload for case %d", value.Layout.Text, caseIndex) + } + return b.insertIndex(value, payload, index) +} + func (b *llvmBuilder) compare(opcode, predicate string, left, right llvmValue) llvmValue { if !llvmLayoutsMatch(left.Layout, right.Layout) { b.invariant("compare %s with %s", left.Layout.Text, right.Layout.Text) @@ -351,6 +399,20 @@ func (b *llvmBuilder) condBranch(condition llvmValue, yes, no string) { b.line(fmt.Sprintf("br i1 %s, label %%%s, label %%%s", condition.Text, yes, no)) } +func (b *llvmBuilder) switchBranch(value llvmValue, fallback string, cases []llvmSwitchCase) { + if value.Layout == nil || value.Layout.Kind != llvmLayoutScalar || fallback == "" { + b.invariant("switch requires scalar value and fallback label") + } + parts := make([]string, len(cases)) + for i, switchCase := range cases { + if !llvmLayoutsMatch(value.Layout, switchCase.Value.Layout) || switchCase.Label == "" { + b.invariant("switch case requires matching value and label") + } + parts[i] = fmt.Sprintf("%s %s, label %%%s", switchCase.Value.Layout.Text, switchCase.Value.Text, switchCase.Label) + } + b.line(fmt.Sprintf("switch %s %s, label %%%s [ %s ]", value.Layout.Text, value.Text, fallback, strings.Join(parts, " "))) +} + func (b *llvmBuilder) ret(value llvmValue, expected *llvmLayout) { if value.Layout == nil || expected == nil || expected.Kind == llvmLayoutVoid || expected.Kind == llvmLayoutFunction || !llvmLayoutsMatch(value.Layout, expected) { diff --git a/internal/ir/cfg/build.go b/internal/ir/cfg/build.go index 398333b..7e598da 100644 --- a/internal/ir/cfg/build.go +++ b/internal/ir/cfg/build.go @@ -208,12 +208,20 @@ func finalizeSites(fn *Graph) { if block == nil { continue } - if branch, ok := block.Terminator.(*Branch); ok { + switch term := block.Terminator.(type) { + case *Branch: + block.Sites = append(block.Sites, &Site{ + Kind: SiteTerminator, + NodeID: term.NodeID, + ScopeID: term.ScopeID, + Location: term.Location, + }) + case *SwitchVariant: block.Sites = append(block.Sites, &Site{ Kind: SiteTerminator, - NodeID: branch.NodeID, - ScopeID: branch.ScopeID, - Location: branch.Location, + NodeID: term.NodeID, + ScopeID: term.ScopeID, + Location: term.Location, }) } if len(block.Sites) == 0 { @@ -230,17 +238,21 @@ func finalizeSites(fn *Graph) { continue } for index := 0; index+1 < len(block.Sites); index++ { - connectSites(block.Sites[index], block.Sites[index+1], EdgeNormal) + connectSites(block.Sites[index], block.Sites[index+1], EdgeNormal, 0) } last := block.Sites[len(block.Sites)-1] switch term := block.Terminator.(type) { case *Jump: - connectBlockSite(last, term.Target, EdgeNormal) + connectBlockSite(last, term.Target, EdgeNormal, 0) case *Branch: - connectBlockSite(last, term.TrueTarget, EdgeTrue) - connectBlockSite(last, term.FalseTarget, EdgeFalse) + connectBlockSite(last, term.TrueTarget, EdgeTrue, 0) + connectBlockSite(last, term.FalseTarget, EdgeFalse, 0) case *Return: - connectBlockSite(last, fn.Exit, EdgeReturn) + connectBlockSite(last, fn.Exit, EdgeReturn, 0) + case *SwitchVariant: + for _, target := range term.Targets { + connectBlockSite(last, target.Target, EdgeVariantCase, target.Case) + } case nil: default: panic(fmt.Sprintf("CFG finalization: unhandled terminator %T", block.Terminator)) @@ -248,23 +260,23 @@ func finalizeSites(fn *Graph) { } } -func connectBlockSite(from *Site, target *Block, kind EdgeKind) { +func connectBlockSite(from *Site, target *Block, kind EdgeKind, caseIndex int) { if target == nil || len(target.Sites) == 0 { return } - connectSites(from, target.Sites[0], kind) + connectSites(from, target.Sites[0], kind, caseIndex) } -func connectSites(from, to *Site, kind EdgeKind) { +func connectSites(from, to *Site, kind EdgeKind, caseIndex int) { if from == nil || to == nil { return } for _, existing := range from.Successors { - if existing.To == to.ID && existing.Kind == kind { + if existing.To == to.ID && existing.Kind == kind && existing.Case == caseIndex { return } } - edge := Edge{From: from.ID, To: to.ID, Kind: kind} + edge := Edge{From: from.ID, To: to.ID, Kind: kind, Case: caseIndex} from.Successors = append(from.Successors, edge) to.Predecessors = append(to.Predecessors, edge) } diff --git a/internal/ir/cfg/cfg_test.go b/internal/ir/cfg/cfg_test.go index 857483f..4ad7400 100644 --- a/internal/ir/cfg/cfg_test.go +++ b/internal/ir/cfg/cfg_test.go @@ -104,6 +104,28 @@ func TestBuildModuleCreatesCanonicalSiteAdjacency(t *testing.T) { } } +func TestFinalizeSitesLabelsVariantCaseEdges(t *testing.T) { + first := &Block{ID: 1} + second := &Block{ID: 2} + entry := &Block{ID: 0, Terminator: &SwitchVariant{ + NodeID: 41, + Targets: []VariantTarget{ + {Case: 0, Target: first}, + {Case: 1, Target: second}, + }, + }} + graph := &Graph{Entry: entry, Exit: &Block{ID: 3}, Blocks: []*Block{entry, first, second}} + finalizeSites(graph) + if len(entry.Sites) != 1 || len(entry.Sites[0].Successors) != 2 { + t.Fatalf("switch sites = %#v", entry.Sites) + } + for caseIndex, edge := range entry.Sites[0].Successors { + if edge.Kind != EdgeVariantCase || edge.Case != caseIndex { + t.Fatalf("switch edge %d = %#v", caseIndex, edge) + } + } +} + func TestBuildModulePreservesDisconnectedStatementsAfterReturn(t *testing.T) { location := source.NewLocation("cfg_test.peep", source.Position{Line: 2, Column: 1}, source.Position{Line: 2, Column: 10}) body := &ast.BlockStmt{NodeIDHolder: ast.NodeIDHolder{NodeID: 10}, Stmts: []ast.Stmt{ diff --git a/internal/ir/cfg/model.go b/internal/ir/cfg/model.go index 2ed60ef..b86fc6b 100644 --- a/internal/ir/cfg/model.go +++ b/internal/ir/cfg/model.go @@ -43,6 +43,7 @@ const ( EdgeTrue EdgeFalse EdgeReturn + EdgeVariantCase ) // Edge preserves branch meaning independently from adjacency ordering. @@ -50,6 +51,7 @@ type Edge struct { From SiteID To SiteID Kind EdgeKind + Case int } type SiteKind uint8 @@ -114,9 +116,22 @@ type Return struct { NodeID ir.NodeID } -func (*Jump) termNode() {} -func (*Branch) termNode() {} -func (*Return) termNode() {} +type VariantTarget struct { + Case int + Target *Block +} + +type SwitchVariant struct { + NodeID ir.NodeID + ScopeID ir.NodeID + Location *source.Location + Targets []VariantTarget +} + +func (*Jump) termNode() {} +func (*Branch) termNode() {} +func (*Return) termNode() {} +func (*SwitchVariant) termNode() {} func (t *Jump) Successors() []*Block { if t == nil || t.Target == nil { @@ -140,3 +155,16 @@ func (t *Branch) Successors() []*Block { } func (*Return) Successors() []*Block { return nil } + +func (t *SwitchVariant) Successors() []*Block { + if t == nil { + return nil + } + out := make([]*Block, 0, len(t.Targets)) + for _, target := range t.Targets { + if target.Target != nil { + out = append(out, target.Target) + } + } + return out +} diff --git a/internal/ir/constfold.go b/internal/ir/constfold.go index ab58536..5fcd384 100644 --- a/internal/ir/constfold.go +++ b/internal/ir/constfold.go @@ -20,10 +20,10 @@ func FoldExpr(types *TypeTable, expr Expr, env map[string]constvalue.Value) Expr return nil case *InvalidExpr, *IntLit, *FloatLit, *StringLit, *BoolLit, *ZeroValue: 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 *VariantMake: + return &VariantMake{Case: node.Case, Payload: FoldExpr(types, node.Payload, env), Type: node.Type, SourceInfo: node.SourceInfo} + case *VariantIs: + return &VariantIs{Value: FoldExpr(types, node.Value, env), Case: node.Case, 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/fold_test.go b/internal/ir/fold_test.go index 4965f6f..4d337e7 100644 --- a/internal/ir/fold_test.go +++ b/internal/ir/fold_test.go @@ -160,7 +160,7 @@ func TestFoldExprFoldsEveryCompositeExpression(t *testing.T) { name string expr Expr }{ - {name: "optional", expr: &OptionalSome{Value: foldable(), Type: i32, SourceInfo: SourceInfo{NodeID: 9, Location: loc}}}, + {name: "variant", expr: &VariantMake{Case: OptionalPresentCase, Payload: foldable(), Type: i32, SourceInfo: SourceInfo{NodeID: 9, Location: loc}}}, {name: "unary", expr: &Unary{Op: "opaque", Arg: foldable(), Type: i32, SourceInfo: SourceInfo{NodeID: 9, Location: loc}}}, {name: "binary", expr: &Binary{Op: "opaque", Left: foldable(), Right: foldable(), Type: i32, SourceInfo: SourceInfo{NodeID: 9, Location: loc}}}, {name: "call", expr: &Call{Callee: foldable(), Args: []Expr{foldable()}, Type: i32, SourceInfo: SourceInfo{NodeID: 9, Location: loc}}}, diff --git a/internal/ir/hir/lower/lower_types.go b/internal/ir/hir/lower/lower_types.go index 56dda99..8362747 100644 --- a/internal/ir/hir/lower/lower_types.go +++ b/internal/ir/hir/lower/lower_types.go @@ -27,6 +27,22 @@ func internRuntimeType(types *ir.TypeTable, t typeinfo.Type) ir.TypeID { if types == nil || t == nil { return ir.InvalidType } + if descriptor, ok := typeinfo.VariantDescriptorOf(t); ok { + cases := make([]ir.VariantCase, len(descriptor.Cases)) + for i, variantCase := range descriptor.Cases { + cases[i].Name = variantCase.Name + if variantCase.Payload != nil { + cases[i].Payload = internRuntimeType(types, variantCase.Payload) + } + } + if descriptor.Family == typeinfo.VariantFamilyOptional { + return types.Intern(ir.OptionalVariant(cases[ir.OptionalPresentCase].Payload)) + } + return types.Intern(ir.Type{ + Kind: ir.TypeVariant, Family: ir.VariantFamilyNamed, Name: t.Text(), + Identity: descriptor.Identity, Cases: cases, + }) + } switch typ := typeinfo.Underlying(t).(type) { case *typeinfo.InvalidType, *typeinfo.UnknownType: return ir.InvalidType @@ -71,11 +87,6 @@ func internRuntimeType(types *ir.TypeTable, t typeinfo.Type) ir.TypeID { return ir.InvalidType } return types.Intern(ir.Type{Kind: ir.TypeReference, Mutable: typ.Mutable, Elem: internRuntimeType(types, typ.Target)}) - case *typeinfo.OptionalType: - if typ == nil { - return ir.InvalidType - } - return types.Intern(ir.Type{Kind: ir.TypeOptional, Elem: internRuntimeType(types, typ.Inner)}) case *typeinfo.ArrayType: if typ == nil { return ir.InvalidType @@ -123,11 +134,6 @@ func internRuntimeType(types *ir.TypeTable, t typeinfo.Type) ir.TypeID { returnType = types.Intern(ir.Type{Kind: ir.TypeVoid}) } return types.Intern(ir.Type{Kind: ir.TypeFunction, Params: params, Return: returnType}) - case *typeinfo.EnumType: - if typ == nil { - return ir.InvalidType - } - return types.Intern(ir.Type{Kind: ir.TypeNamed, Name: typ.Text()}) default: return ir.InvalidType } @@ -177,6 +183,9 @@ func loweredRuntimeType(module *project.Module, t typeinfo.Type, seen map[*typei } seen[typ] = struct{}{} defer delete(seen, typ) + if enum, ok := typeinfo.Underlying(typ.Underlying).(*typeinfo.EnumType); ok { + return &typeinfo.DefinedType{Name: typ.Name, Identity: typ.Identity, Underlying: enum} + } return loweredRuntimeType(module, typ.Underlying, seen) case *typeinfo.OwnedPtrType: if typ == nil { diff --git a/internal/ir/hir/lower/module_lower.go b/internal/ir/hir/lower/module_lower.go index 716399f..23a4534 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 appendOptionalPayloadPlace(ctx, module, selector, out) + return appendVariantPayloadPlace(ctx, module, selector, out) } } if index, ok := expr.(*ast.IndexExpr); ok && index != nil && index.Expr != nil && index.Index != nil { @@ -320,7 +320,7 @@ func lowerPlace(ctx *project.CompilerContext, module *project.Module, scope *sym out.Projections = append(out.Projections, ir.PlaceProjection{ Kind: ir.PlaceProjectionIndex, Index: indexExpr, Type: out.Type, Location: ast.LocOf(index), }) - return appendOptionalPayloadPlace(ctx, module, index, out) + return appendVariantPayloadPlace(ctx, module, index, out) } } ident, ok := expr.(*ast.Ident) @@ -332,22 +332,24 @@ func lowerPlace(ctx *project.CompilerContext, module *project.Module, scope *sym out := &ir.Place{ Root: root, Type: root.TypeID(), Location: ast.LocOf(expr), } - return appendOptionalPayloadPlace(ctx, module, expr, out) + return appendVariantPayloadPlace(ctx, module, expr, out) } -func appendOptionalPayloadPlace(ctx *project.CompilerContext, module *project.Module, expr ast.Expr, out *ir.Place) *ir.Place { +func appendVariantPayloadPlace(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 { + for _, caseIndex := range payload.Cases { + variant, ok := ctx.Types.Type(out.Type) + variantCase, caseOK := variant.VariantCase(caseIndex) + if !ok || !caseOK || variantCase.Payload == ir.InvalidType { break } - out.Type = optional.Elem + out.Type = variantCase.Payload out.Projections = append(out.Projections, ir.PlaceProjection{ - Kind: ir.PlaceProjectionOptionalPayload, Type: out.Type, Location: ast.LocOf(expr), + Kind: ir.PlaceProjectionVariantPayload, Case: caseIndex, + Type: out.Type, Location: ast.LocOf(expr), }) } return out @@ -442,24 +444,26 @@ func lowerASTExpr(ctx *project.CompilerContext, module *project.Module, scope *s resolvedTypeID = loweredTypeID(ctx, module, resolvedType) } if module != nil && module.Flow != nil { - if test, ok := module.Flow.OptionalTests[expr.ID()]; ok { + if test, ok := module.Flow.VariantTests[expr.ID()]; ok { subject, _ := module.TypedASTNodes[test.SubjectID].(ast.Expr) - present := &ir.OptionalPresent{ + present := &ir.VariantIs{ Value: lowerASTExpr(ctx, module, scope, subject, nil), + Case: test.Case, Type: loweredTypeID(ctx, module, &typeinfo.BoolType{}), } - if test.PresentWhenTrue { + if test.CaseWhenTrue { return present } return &ir.Unary{Op: "!", Arg: present, Type: present.Type} } - if payload := module.Flow.Payloads[expr.ID()]; payload.Depth > 0 && place.IsPlaceExpr(expr) { + if payload := module.Flow.Payloads[expr.ID()]; len(payload.Cases) > 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), + if innerExpected := optionalPromotionInnerType(module, expectedType, resolvedType, expr); innerExpected != nil { + return &ir.VariantMake{ + Case: ir.OptionalPresentCase, + Payload: lowerASTExpr(ctx, module, scope, expr, innerExpected), Type: loweredTypeID(ctx, module, expectedType), SourceInfo: ir.SourceInfo{Location: loc}, } @@ -504,7 +508,7 @@ func lowerASTExpr(ctx *project.CompilerContext, module *project.Module, scope *s return &ir.BoolLit{Value: node.Value, Type: loweredTypeID(ctx, module, &typeinfo.BoolType{}), SourceInfo: ir.SourceInfo{Location: loc}} case *ast.NoneLit: - if none := lowerOptionalNone(ctx, expectedTypeID, loc); none != nil { + if none := lowerOptionalAbsent(ctx, expectedTypeID, loc); none != nil { return none } return &ir.InvalidExpr{Message: "`none` requires optional context", Type: ir.InvalidType, SourceInfo: ir.SourceInfo{Location: loc}} @@ -716,18 +720,18 @@ func lowerCollectionCall(ctx *project.CompilerContext, module *project.Module, s } } -func lowerOptionalNone(ctx *project.CompilerContext, typeID ir.TypeID, loc *source.Location) ir.Expr { +func lowerOptionalAbsent(ctx *project.CompilerContext, typeID ir.TypeID, loc *source.Location) ir.Expr { if ctx == nil || ctx.Types == nil { return nil } typ, ok := ctx.Types.Type(typeID) - if !ok || typ.Kind != ir.TypeOptional { + if _, optional := typ.OptionalPayload(); !ok || !optional { return nil } - return &ir.ZeroValue{Type: typeID, SourceInfo: ir.SourceInfo{Location: loc}} + return &ir.VariantMake{Case: ir.OptionalAbsentCase, Type: typeID, SourceInfo: ir.SourceInfo{Location: loc}} } -func optionalSomeInnerType(module *project.Module, expectedType, resolvedType typeinfo.Type, expr ast.Expr) typeinfo.Type { +func optionalPromotionInnerType(module *project.Module, expectedType, resolvedType typeinfo.Type, expr ast.Expr) typeinfo.Type { if expectedType == nil || resolvedType == nil || expr == nil { return nil } diff --git a/internal/ir/hir/lower/module_lower_test.go b/internal/ir/hir/lower/module_lower_test.go index f84e5c1..953e987 100644 --- a/internal/ir/hir/lower/module_lower_test.go +++ b/internal/ir/hir/lower/module_lower_test.go @@ -138,9 +138,12 @@ func TestGenerateHIRLowersOptionalFlowEvidence(t *testing.T) { if !ok { t.Fatalf("first statement = %T, want If", out.Funcs[0].Body.Stmts[0]) } - present, ok := branch.Cond.(*ir.OptionalPresent) + present, ok := branch.Cond.(*ir.VariantIs) 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) + t.Fatalf("condition = %#v, want VariantIs(?i32, Present) -> bool", branch.Cond) + } + if present.Case != ir.OptionalPresentCase { + t.Fatalf("condition case = %d, want Present", present.Case) } ret, ok := branch.Then.Stmts[0].(*hir.Return) if !ok { @@ -148,11 +151,61 @@ func TestGenerateHIRLowersOptionalFlowEvidence(t *testing.T) { } 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" { + load.Place.Projections[0].Kind != ir.PlaceProjectionVariantPayload || + load.Place.Projections[0].Case != ir.OptionalPresentCase || out.Types.Text(load.TypeID()) != "i32" { t.Fatalf("proven value = %#v, want i32 optional payload load", ret.Value) } } +func TestGenerateHIRKeepsProofAcrossImpossibleVariantEdge(t *testing.T) { + generateTestHIR(t, "hir_impossible_variant_edge_test"+peeper.SourceExt, "hir_impossible_variant_edge_test", `fn read(value: ?i32, other: ?i32) -> i32 { + if value != none { + if other != none && other == none { + let ignored = 0; + } + return value; + } + return 0; +}`) +} + +func TestGenerateHIRKeepsEagerConditionMutationOrdering(t *testing.T) { + generateTestHIR(t, "hir_eager_variant_mutation_test"+peeper.SourceExt, "hir_eager_variant_mutation_test", `struct Holder { + field: ?i32 +} + +fn Clear(holder: &mut Holder) -> bool { + holder.field = none; + return true; +} + +fn read(value: ?i32, other: Holder) -> i32 { + if value == none { + return 0; + } + let mut holder = other; + if holder.field != none && Clear(&mut holder) && holder.field == none { + return value; + } + return 0; +}`) +} + +func TestLoweredRuntimeTypeDoesNotInventUseSiteVariantIdentity(t *testing.T) { + consumer := &project.Module{Key: "local:consumer.peep", ModuleScope: symbols.NewScope(nil)} + typ := &typeinfo.DefinedType{ + Name: "Status", + Underlying: &typeinfo.EnumType{Variants: []string{"Ready"}}, + } + lowered, ok := loweredRuntimeType(consumer, typ, nil).(*typeinfo.DefinedType) + if !ok || lowered == nil { + t.Fatalf("lowered type = %T, want DefinedType", lowered) + } + if lowered.Identity != "" { + t.Fatalf("lowered type invented use-site identity %q", lowered.Identity) + } +} + 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 { @@ -171,7 +224,7 @@ func TestGenerateHIRKeepsOptionalIndexCarrierBeforePayloadProjection(t *testing. 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" { + payload.Kind != ir.PlaceProjectionVariantPayload || payload.Case != ir.OptionalPresentCase || out.Types.Text(payload.Type) != "i32" { t.Fatalf("projections = %#v, want index:?i32 then optional-payload:i32", load.Place.Projections) } } @@ -910,3 +963,21 @@ fn main() { let ignored = make(); }`) t.Fatalf("discarded value = %T, want call", stmt.Value) } } + +func TestInternRuntimeTypeUsesSharedVariantDescriptor(t *testing.T) { + types := ir.NewTypeTable() + i32 := types.Intern(ir.Type{Kind: ir.TypeInteger, Signed: true, Bits: 32}) + optionalID := internRuntimeType(types, &typeinfo.OptionalType{Inner: &typeinfo.IntegerType{Signed: true, Bits: 32}}) + if direct := types.Intern(ir.OptionalVariant(i32)); optionalID != direct { + t.Fatalf("semantic optional ID = %d, direct optional ID = %d", optionalID, direct) + } + enumID := internRuntimeType(types, &typeinfo.DefinedType{ + Name: "Status", + Underlying: &typeinfo.EnumType{Variants: []string{"Ready", "Waiting"}}, + }) + variant, ok := types.Type(enumID) + if !ok || variant.Kind != ir.TypeVariant || variant.Family != ir.VariantFamilyNamed || + variant.Identity != "Status" || len(variant.Cases) != 2 || variant.Cases[0].Name != "Ready" { + t.Fatalf("enum runtime type = %#v", variant) + } +} diff --git a/internal/ir/hir/model.go b/internal/ir/hir/model.go index 5e885ff..3e045ee 100644 --- a/internal/ir/hir/model.go +++ b/internal/ir/hir/model.go @@ -1,6 +1,7 @@ package hir import ( + "strconv" "strings" "compiler/internal/ir" @@ -120,14 +121,28 @@ type For struct { Location *source.Location } -func (*Block) stmtNode() {} -func (*Binding) stmtNode() {} -func (*ExprStmt) stmtNode() {} -func (*Assign) stmtNode() {} -func (*Invalid) stmtNode() {} -func (*Return) stmtNode() {} -func (*If) stmtNode() {} -func (*For) stmtNode() {} +type VariantCaseBlock struct { + Case int + Body *Block +} + +// SwitchVariant owns semantic subject and case bodies; CFG owns target edges. +type SwitchVariant struct { + Value ir.Expr + Cases []VariantCaseBlock + NodeID NodeID + Location *source.Location +} + +func (*Block) stmtNode() {} +func (*Binding) stmtNode() {} +func (*ExprStmt) stmtNode() {} +func (*Assign) stmtNode() {} +func (*Invalid) stmtNode() {} +func (*Return) stmtNode() {} +func (*If) stmtNode() {} +func (*For) stmtNode() {} +func (*SwitchVariant) stmtNode() {} func (s *Block) forEachChild(visit func(Stmt)) { for _, stmt := range s.Stmts { @@ -144,6 +159,11 @@ func (s *If) forEachChild(visit func(Stmt)) { visit(s.Else) } func (s *For) forEachChild(visit func(Stmt)) { visit(s.Body) } +func (s *SwitchVariant) forEachChild(visit func(Stmt)) { + for _, variantCase := range s.Cases { + visit(variantCase.Body) + } +} // InspectStmt traverses structured HIR in depth-first preorder. func InspectStmt(stmt Stmt, visit func(Stmt) bool) { @@ -177,6 +197,9 @@ func (f *If) sourceInfo() ir.SourceInfo { func (f *For) sourceInfo() ir.SourceInfo { return ir.SourceInfo{NodeID: f.NodeID, Location: f.Location} } +func (s *SwitchVariant) sourceInfo() ir.SourceInfo { + return ir.SourceInfo{NodeID: s.NodeID, Location: s.Location} +} func (m *Module) Text() string { if m == nil { @@ -320,3 +343,21 @@ func (s *For) appendText(b *strings.Builder, indent int) { writeIndent(b, indent) b.WriteString("}\n") } + +func (s *SwitchVariant) appendText(b *strings.Builder, indent int) { + writeIndent(b, indent) + b.WriteString("switch-variant ") + b.WriteString(s.Value.String()) + b.WriteString(" {\n") + for _, variantCase := range s.Cases { + writeIndent(b, indent+1) + b.WriteString("case ") + b.WriteString(strconv.Itoa(variantCase.Case)) + b.WriteString(" {\n") + appendBlockText(b, variantCase.Body, indent+2) + writeIndent(b, indent+1) + b.WriteString("}\n") + } + writeIndent(b, indent) + b.WriteString("}\n") +} diff --git a/internal/ir/hir/model_test.go b/internal/ir/hir/model_test.go index 2921860..c325723 100644 --- a/internal/ir/hir/model_test.go +++ b/internal/ir/hir/model_test.go @@ -1,6 +1,7 @@ package hir import ( + "strings" "testing" "compiler/internal/ir" @@ -47,3 +48,27 @@ func TestInspectStmtTraversesStructuredChildren(t *testing.T) { } } } + +func TestSwitchVariantHIRKeepsCaseBlocksAndText(t *testing.T) { + switchStmt := &SwitchVariant{ + Value: &ir.Ident{Name: "status"}, + Cases: []VariantCaseBlock{ + {Case: 0, Body: &Block{NodeID: 2}}, + {Case: 1, Body: &Block{NodeID: 3}}, + }, + NodeID: 1, + } + visited := make([]NodeID, 0) + InspectStmt(switchStmt, func(stmt Stmt) bool { + visited = append(visited, NodeIDOf(stmt)) + return true + }) + var text strings.Builder + switchStmt.appendText(&text, 0) + if got := text.String(); got != "switch-variant status {\n case 0 {\n }\n case 1 {\n }\n}\n" { + t.Fatalf("switch text = %q", got) + } + if len(visited) != 3 || visited[0] != 1 || visited[1] != 2 || visited[2] != 3 { + t.Fatalf("visited switch nodes = %v", visited) + } +} diff --git a/internal/ir/inspect_test.go b/internal/ir/inspect_test.go index 5513d63..286919f 100644 --- a/internal/ir/inspect_test.go +++ b/internal/ir/inspect_test.go @@ -22,7 +22,7 @@ func TestInspectExprVisitsCompositeChildrenInOrder(t *testing.T) { expr Expr want string }{ - {name: "optional", expr: &OptionalSome{Value: ident("value")}, want: "value"}, + {name: "variant", expr: &VariantMake{Case: OptionalPresentCase, Payload: ident("value")}, want: "value"}, {name: "unary", expr: &Unary{Arg: ident("arg")}, want: "arg"}, {name: "binary", expr: &Binary{Left: ident("left"), Right: ident("right")}, want: "left,right"}, {name: "call", expr: &Call{Callee: ident("callee"), Args: []Expr{ident("first"), ident("second")}}, want: "callee,first,second"}, diff --git a/internal/ir/mir/model.go b/internal/ir/mir/model.go index 58a32b9..86c8e9e 100644 --- a/internal/ir/mir/model.go +++ b/internal/ir/mir/model.go @@ -93,6 +93,17 @@ type Branch struct { Location *source.Location } +type VariantTarget struct { + Case int + TargetID int +} + +type SwitchVariant struct { + Value ValueRef + Targets []VariantTarget + Location *source.Location +} + type Ret struct { Value ValueRef Location *source.Location @@ -179,13 +190,14 @@ const ( PlaceProjectionDeref PlaceProjectionKind = iota PlaceProjectionField PlaceProjectionIndex - PlaceProjectionOptionalPayload + PlaceProjectionVariantPayload ) type PlaceProjection struct { Kind PlaceProjectionKind FieldIndex int Index ValueRef + Case int Type ir.TypeID Location *source.Location } @@ -277,14 +289,16 @@ type ZeroValue struct { Location *source.Location } -type OptionalSome struct { - Value ValueRef +type VariantMake struct { + Case int + Payload ValueRef Type ir.TypeID Location *source.Location } -type OptionalPresent struct { +type VariantIs struct { Value ValueRef + Case int Type ir.TypeID Location *source.Location } @@ -330,6 +344,16 @@ func (i *Branch) Text() string { return fmt.Sprintf("br %s, b%d, b%d", i.Cond.Text(), i.ThenID, i.ElseID) } +func (i *SwitchVariant) Text() string { + var b strings.Builder + b.WriteString("switch-variant ") + b.WriteString(i.Value.Text()) + for _, target := range i.Targets { + fmt.Fprintf(&b, ", case %d: b%d", target.Case, target.TargetID) + } + return b.String() +} + func (i *Ret) Text() string { if i == nil || i.Value == nil { return "ret" @@ -352,8 +376,8 @@ func (*ArrayLit) valueExprNode() {} func (*DynamicArrayAlloc) valueExprNode() {} func (*Alloc) valueExprNode() {} func (*ZeroValue) valueExprNode() {} -func (*OptionalSome) valueExprNode() {} -func (*OptionalPresent) valueExprNode() {} +func (*VariantMake) valueExprNode() {} +func (*VariantIs) valueExprNode() {} func (*InterfaceMake) valueExprNode() {} func (*InterfaceCall) valueExprNode() {} func (*StringLiteral) valueExprNode() {} @@ -366,6 +390,7 @@ func (i *Print) SourceLocation() *source.Location { return i.Locatio func (i *Drop) SourceLocation() *source.Location { return i.Location } func (i *Jump) SourceLocation() *source.Location { return i.Location } func (i *Branch) SourceLocation() *source.Location { return i.Location } +func (i *SwitchVariant) SourceLocation() *source.Location { return i.Location } func (i *Ret) SourceLocation() *source.Location { return i.Location } func (r *RefConst) SourceLocation() *source.Location { return r.Location } func (r *RefName) SourceLocation() *source.Location { return r.Location } @@ -386,8 +411,8 @@ func (v *DynamicArrayAlloc) SourceLocation() *source.Location { return v.Locatio func (v *DynamicArrayOp) SourceLocation() *source.Location { return v.Location } 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 *VariantMake) SourceLocation() *source.Location { return v.Location } +func (v *VariantIs) SourceLocation() *source.Location { return v.Location } func (v *InterfaceMake) SourceLocation() *source.Location { return v.Location } func (v *InterfaceCall) SourceLocation() *source.Location { return v.Location } @@ -425,8 +450,8 @@ func (p *Place) Text() string { b.WriteString(projection.Index.Text()) } b.WriteString("]") - case PlaceProjectionOptionalPayload: - b.WriteString(".value") + case PlaceProjectionVariantPayload: + fmt.Fprintf(&b, ".variant%d", projection.Case) } } return b.String() @@ -509,17 +534,20 @@ func (v *ZeroValue) Text() string { } return "zero" } -func (v *OptionalSome) Text() string { - if v == nil || v.Value == nil { - return "some()" +func (v *VariantMake) Text() string { + if v == nil { + return "variant()" + } + if v.Payload == nil { + return fmt.Sprintf("variant %d", v.Case) } - return "some(" + v.Value.Text() + ")" + return fmt.Sprintf("variant %d, %s", v.Case, v.Payload.Text()) } -func (v *OptionalPresent) Text() string { +func (v *VariantIs) Text() string { if v == nil || v.Value == nil { - return "present()" + return "is-variant()" } - return "present(" + v.Value.Text() + ")" + return fmt.Sprintf("is-variant %s, %d", v.Value.Text(), v.Case) } func (v *InterfaceMake) Text() string { diff --git a/internal/ir/mir/module_lower.go b/internal/ir/mir/module_lower.go index 8c9ef38..4c432db 100644 --- a/internal/ir/mir/module_lower.go +++ b/internal/ir/mir/module_lower.go @@ -364,6 +364,25 @@ func (l *lowerer) lowerCFGTerminator(source, exit *cfg.Block, blocks map[*cfg.Bl l.flushTemporaryDrops(&l.current.Instrs, temporaryMark) l.setBlockTerm(l.current, &Branch{Cond: lowered, ThenID: thenBlock.ID, ElseID: elseBlock.ID}) return true + case *cfg.SwitchVariant: + switchStmt, ok := statements[term.NodeID].(*hir.SwitchVariant) + if !ok || switchStmt == nil || len(switchStmt.Cases) != len(term.Targets) || len(term.Targets) == 0 { + return false + } + l.location = switchStmt.Location + targets := make([]VariantTarget, len(term.Targets)) + for i, target := range term.Targets { + block := blocks[target.Target] + if block == nil || switchStmt.Cases[i].Case != target.Case { + return false + } + targets[i] = VariantTarget{Case: target.Case, TargetID: block.ID} + } + temporaryMark := len(l.temporaryDrops) + value := l.lowerExpr(switchStmt.Value, &l.current.Instrs) + l.flushTemporaryDrops(&l.current.Instrs, temporaryMark) + l.setBlockTerm(l.current, &SwitchVariant{Value: value, Targets: targets}) + return true case *cfg.Return: ret, ok := statements[term.NodeID].(*hir.Return) if !ok || ret == nil { @@ -429,7 +448,7 @@ func (l *lowerer) lowerPlace(place *ir.Place, out *[]Instr) *Place { root := l.lowerExpr(place.Root, out) projections := make([]PlaceProjection, 0, len(place.Projections)) for _, projection := range place.Projections { - lowered := PlaceProjection{FieldIndex: projection.FieldIndex, Type: projection.Type, Location: projection.Location} + lowered := PlaceProjection{FieldIndex: projection.FieldIndex, Case: projection.Case, Type: projection.Type, Location: projection.Location} switch projection.Kind { case ir.PlaceProjectionDeref: lowered.Kind = PlaceProjectionDeref @@ -438,8 +457,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 + case ir.PlaceProjectionVariantPayload: + lowered.Kind = PlaceProjectionVariantPayload default: panic(fmt.Sprintf("unsupported HIR place projection %d", projection.Kind)) } @@ -462,6 +481,8 @@ func (l *lowerer) setBlockTerm(block *Block, term Terminator) { node.Location = l.location case *Branch: node.Location = l.location + case *SwitchVariant: + node.Location = l.location case *Jump: node.Location = l.location } @@ -499,15 +520,15 @@ func (l *lowerer) lowerExpr(expr ir.Expr, out *[]Instr) ValueRef { name := l.nextTemp() l.appendInstr(out, &Assign{Name: name, Value: &ZeroValue{Type: e.TypeID(), Location: e.Origin().Location}}) return &RefName{Name: name, Type: e.TypeID(), Location: e.Origin().Location} - case *ir.OptionalSome: - value := l.lowerExpr(e.Value, out) + case *ir.VariantMake: + payload := l.lowerExpr(e.Payload, out) name := l.nextTemp() - l.appendInstr(out, &Assign{Name: name, Value: &OptionalSome{Value: value, Type: e.TypeID(), Location: e.Origin().Location}}) + l.appendInstr(out, &Assign{Name: name, Value: &VariantMake{Case: e.Case, Payload: payload, Type: e.TypeID(), Location: e.Origin().Location}}) return &RefName{Name: name, Type: e.TypeID(), Location: e.Origin().Location} - case *ir.OptionalPresent: + case *ir.VariantIs: 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}}) + l.appendInstr(out, &Assign{Name: name, Value: &VariantIs{Value: value, Case: e.Case, 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} diff --git a/internal/ir/mir/module_lower_test.go b/internal/ir/mir/module_lower_test.go index b6e9d83..22bedba 100644 --- a/internal/ir/mir/module_lower_test.go +++ b/internal/ir/mir/module_lower_test.go @@ -50,7 +50,7 @@ var mirTypes = func() mirTypeFixture { rawptr: rawptr, usize: usize, ownedI32: table.Intern(ir.Type{Kind: ir.TypeOwnedPtr, Elem: i32}), - optionalI32: table.Intern(ir.Type{Kind: ir.TypeOptional, Elem: i32}), + optionalI32: table.Intern(ir.OptionalVariant(i32)), valueStruct: valueStruct, ownerStruct: ownerStruct, ownedValueStruct: table.Intern(ir.Type{Kind: ir.TypeOwnedPtr, Elem: valueStruct}), @@ -469,7 +469,7 @@ func TestGenerateMIRLowersZeroValue(t *testing.T) { } } -func TestGenerateMIRLowersOptionalSome(t *testing.T) { +func TestGenerateMIRLowersVariantMake(t *testing.T) { mod := &hir.Module{ Name: "test", Types: mirTypes.table, Funcs: []*hir.Function{ @@ -478,7 +478,7 @@ func TestGenerateMIRLowersOptionalSome(t *testing.T) { ReturnType: mirTypes.optionalI32, Body: &hir.Block{ Stmts: []hir.Stmt{ - &hir.Return{Value: &ir.OptionalSome{Value: &ir.IntLit{Value: "7", Type: mirTypes.i32}, Type: mirTypes.optionalI32}}, + &hir.Return{Value: &ir.VariantMake{Case: ir.OptionalPresentCase, Payload: &ir.IntLit{Value: "7", Type: mirTypes.i32}, Type: mirTypes.optionalI32}}, }, }, }, @@ -497,9 +497,9 @@ func TestGenerateMIRLowersOptionalSome(t *testing.T) { if !ok { t.Fatalf("expected assign, got %#v", block.Instrs[0]) } - some, ok := assign.Value.(*OptionalSome) - if !ok || some.Type != mirTypes.optionalI32 { - t.Fatalf("expected ?i32 optional some, got %#v", assign.Value) + variant, ok := assign.Value.(*VariantMake) + if !ok || variant.Type != mirTypes.optionalI32 || variant.Case != ir.OptionalPresentCase { + t.Fatalf("expected ?i32 present variant, got %#v", assign.Value) } } @@ -507,7 +507,7 @@ 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}, + {Kind: ir.PlaceProjectionVariantPayload, Case: ir.OptionalPresentCase, Type: mirTypes.i32}, }, Type: mirTypes.i32, } @@ -516,8 +516,8 @@ func TestGenerateMIRLowersOptionalFlowOperations(t *testing.T) { 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, + Body: &hir.Block{Stmts: []hir.Stmt{&hir.Return{Value: &ir.VariantIs{ + Value: &ir.Ident{Name: "value", Type: mirTypes.optionalI32}, Case: ir.OptionalPresentCase, Type: mirTypes.boolType, }}}}, }, { @@ -532,16 +532,71 @@ func TestGenerateMIRLowersOptionalFlowOperations(t *testing.T) { 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) + if present, ok := presentAssign.Value.(*VariantIs); !ok || present.Case != ir.OptionalPresentCase { + t.Fatalf("presence value = %#v, want VariantIs Present", 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) + if !ok || load.Place == nil || len(load.Place.Projections) != 1 || + load.Place.Projections[0].Kind != PlaceProjectionVariantPayload || load.Place.Projections[0].Case != ir.OptionalPresentCase { + t.Fatalf("payload value = %#v, want present variant payload place load", payloadAssign.Value) + } +} + +func TestSwitchVariantTerminatorText(t *testing.T) { + term := &SwitchVariant{ + Value: &RefName{Name: "status", Type: mirTypes.optionalI32}, + Targets: []VariantTarget{ + {Case: ir.OptionalAbsentCase, TargetID: 1}, + {Case: ir.OptionalPresentCase, TargetID: 2}, + }, + } + if got := term.Text(); got != "switch-variant status, case 0: b1, case 1: b2" { + t.Fatalf("switch terminator text = %q", got) + } +} + +func TestGenerateMIRLowersHIRAndCFGVariantSwitch(t *testing.T) { + ifStmt := &hir.If{ + Cond: &ir.BoolLit{Value: true, Type: mirTypes.boolType}, + Then: &hir.Block{}, + Else: &hir.Block{}, + } + fn := &hir.Function{ + Name: "select", Params: []ir.Param{{Name: "value", Type: mirTypes.optionalI32}}, ReturnType: mirTypes.void, + Body: &hir.Block{Stmts: []hir.Stmt{ifStmt}}, + } + mod := &hir.Module{Name: "test", Types: mirTypes.table, Funcs: []*hir.Function{fn}} + graphs := cfgForHIR(mod) + graph := graphs.Function(fn.NodeID) + branch, ok := graph.Entry.Terminator.(*cfg.Branch) + if !ok { + t.Fatalf("fixture entry = %#v, want branch", graph.Entry.Terminator) + } + switchStmt := &hir.SwitchVariant{ + Value: &ir.Ident{Name: "value", Type: mirTypes.optionalI32}, + Cases: []hir.VariantCaseBlock{{Case: ir.OptionalAbsentCase, Body: ifStmt.Then}, {Case: ir.OptionalPresentCase, Body: ifStmt.Else.(*hir.Block)}}, + NodeID: ifStmt.NodeID, + } + fn.Body.Stmts[0] = switchStmt + graph.Entry.Terminator = &cfg.SwitchVariant{ + NodeID: switchStmt.NodeID, + Targets: []cfg.VariantTarget{ + {Case: ir.OptionalAbsentCase, Target: branch.TrueTarget}, + {Case: ir.OptionalPresentCase, Target: branch.FalseTarget}, + }, + } + + out := GenerateMIR(mod, graphs, nil, nil, nil) + if out == nil || len(out.Funcs) != 1 { + t.Fatalf("MIR = %#v", out) + } + term, ok := out.Funcs[0].Blocks[0].Term.(*SwitchVariant) + if !ok || len(term.Targets) != 2 || term.Targets[0].Case != ir.OptionalAbsentCase || term.Targets[1].Case != ir.OptionalPresentCase { + t.Fatalf("MIR switch = %#v", out.Funcs[0].Blocks[0].Term) } } diff --git a/internal/ir/nodes.go b/internal/ir/nodes.go index 1cc0532..da78531 100644 --- a/internal/ir/nodes.go +++ b/internal/ir/nodes.go @@ -79,15 +79,17 @@ type ZeroValue struct { Type TypeID } -type OptionalSome struct { +type VariantMake struct { SourceInfo - Value Expr - Type TypeID + Case int + Payload Expr + Type TypeID } -type OptionalPresent struct { +type VariantIs struct { SourceInfo Value Expr + Case int Type TypeID } @@ -126,13 +128,14 @@ const ( PlaceProjectionDeref PlaceProjectionKind = iota PlaceProjectionField PlaceProjectionIndex - PlaceProjectionOptionalPayload + PlaceProjectionVariantPayload ) type PlaceProjection struct { Kind PlaceProjectionKind FieldIndex int Index Expr + Case int Type TypeID Location *source.Location } @@ -274,8 +277,8 @@ var ( _ Expr = (*StringLit)(nil) _ Expr = (*BoolLit)(nil) _ Expr = (*ZeroValue)(nil) - _ Expr = (*OptionalSome)(nil) - _ Expr = (*OptionalPresent)(nil) + _ Expr = (*VariantMake)(nil) + _ Expr = (*VariantIs)(nil) _ Expr = (*Ident)(nil) _ Expr = (*Unary)(nil) _ Expr = (*Binary)(nil) @@ -298,27 +301,31 @@ 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 (*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 (*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 (*VariantMake) exprNode() {} +func (e *VariantMake) forEachChild(visit func(Expr)) { + if e.Payload != nil { + visit(e.Payload) + } +} +func (*VariantIs) exprNode() {} +func (e *VariantIs) 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) @@ -484,25 +491,28 @@ func (e *ZeroValue) TypeID() TypeID { } return e.Type } -func (e *OptionalSome) String() string { - if e == nil || e.Value == nil { - return "some()" +func (e *VariantMake) String() string { + if e == nil { + return "variant()" + } + if e.Payload == nil { + return fmt.Sprintf("variant(%d)", e.Case) } - return "some(" + e.Value.String() + ")" + return fmt.Sprintf("variant(%d, %s)", e.Case, e.Payload.String()) } -func (e *OptionalSome) TypeID() TypeID { +func (e *VariantMake) TypeID() TypeID { if e == nil { return InvalidType } return e.Type } -func (e *OptionalPresent) String() string { +func (e *VariantIs) String() string { if e == nil || e.Value == nil { - return "present()" + return "is-variant()" } - return "present(" + e.Value.String() + ")" + return fmt.Sprintf("is-variant(%s, %d)", e.Value.String(), e.Case) } -func (e *OptionalPresent) TypeID() TypeID { +func (e *VariantIs) TypeID() TypeID { if e == nil { return InvalidType } @@ -605,8 +615,8 @@ func (p *Place) String() string { b.WriteString(projection.Index.String()) } b.WriteString("]") - case PlaceProjectionOptionalPayload: - b.WriteString(".value") + case PlaceProjectionVariantPayload: + fmt.Fprintf(&b, ".variant%d", projection.Case) } } return b.String() diff --git a/internal/ir/types.go b/internal/ir/types.go index 7b853b9..96c4e17 100644 --- a/internal/ir/types.go +++ b/internal/ir/types.go @@ -28,7 +28,7 @@ const ( TypeRawPtr TypeOwnedPtr TypeReference - TypeOptional + TypeVariant TypeArray TypeSlice TypeStruct @@ -48,20 +48,68 @@ type TypeMethod struct { Return TypeID } +type VariantFamily uint8 + +const ( + VariantFamilyInvalid VariantFamily = iota + VariantFamilyOptional + VariantFamilyNamed +) + +const ( + OptionalAbsentCase = iota + OptionalPresentCase +) + +type VariantCase struct { + Name string + Payload TypeID +} + // Type is a backend-independent runtime descriptor. Source-only aliases are // resolved before interning, so every child directly describes its ABI shape. type Type struct { - Kind TypeKind - Signed bool - Bits int - Mutable bool - Length string - Elem TypeID - Fields []TypeField - Methods []TypeMethod - Params []TypeID - Return TypeID - Name string + Kind TypeKind + Signed bool + Bits int + Mutable bool + Length string + Elem TypeID + Fields []TypeField + Methods []TypeMethod + Params []TypeID + Return TypeID + Name string + Family VariantFamily + Identity string + Cases []VariantCase +} + +// OptionalVariant owns optional's fixed case order. Flow facts, lowering, and +// backends use these case indexes instead of rebuilding optional conventions. +func OptionalVariant(payload TypeID) Type { + return Type{ + Kind: TypeVariant, Family: VariantFamilyOptional, + Cases: []VariantCase{{Name: "Absent"}, {Name: "Present", Payload: payload}}, + } +} + +func (t Type) VariantCase(index int) (VariantCase, bool) { + if t.Kind != TypeVariant || index < 0 || index >= len(t.Cases) { + return VariantCase{}, false + } + return t.Cases[index], true +} + +func (t Type) OptionalPayload() (TypeID, bool) { + if t.Kind != TypeVariant || t.Family != VariantFamilyOptional || len(t.Cases) != 2 { + return InvalidType, false + } + present := t.Cases[OptionalPresentCase] + if t.Cases[OptionalAbsentCase].Payload != InvalidType || present.Payload == InvalidType { + return InvalidType, false + } + return present.Payload, true } // TypeTable is owned by one CompilerContext. It is canonical storage for IR @@ -193,8 +241,14 @@ func (t *TypeTable) textLocked(id TypeID) string { prefix = "&mut " } return prefix + t.textLocked(typ.Elem) - case TypeOptional: - return "?" + t.textLocked(typ.Elem) + case TypeVariant: + if payload, optional := typ.OptionalPayload(); optional { + return "?" + t.textLocked(payload) + } + if typ.Family == VariantFamilyNamed && typ.Name != "" { + return typ.Name + } + return "" case TypeArray: if typ.Length == "" { return "[]" + t.textLocked(typ.Elem) @@ -237,11 +291,28 @@ func (t *TypeTable) textLocked(id TypeID) string { // ABIKey is stable only inside the compiler ABI model. Backends may use it for // symbol identity, never raw TypeID values. -func (t *TypeTable) ABIKey(id TypeID) string { return t.Text(id) } +func (t *TypeTable) ABIKey(id TypeID) string { + if t == nil { + return "" + } + t.mu.RLock() + defer t.mu.RUnlock() + if id == InvalidType || int(id) >= len(t.types) { + return "" + } + typ := t.types[id] + if typ.Kind == TypeVariant && typ.Family == VariantFamilyNamed { + return "variant:" + typ.Identity + } + return t.textLocked(id) +} func (t *TypeTable) key(typ Type) string { var b strings.Builder - fmt.Fprintf(&b, "%d|%t|%d|%t|%q|%d|%d|%q", typ.Kind, typ.Signed, typ.Bits, typ.Mutable, typ.Length, typ.Elem, typ.Return, typ.Name) + fmt.Fprintf(&b, "%d|%t|%d|%t|%q|%d|%d|%q|%d|%q", typ.Kind, typ.Signed, typ.Bits, typ.Mutable, typ.Length, typ.Elem, typ.Return, typ.Name, typ.Family, typ.Identity) + for _, variant := range typ.Cases { + fmt.Fprintf(&b, "|v:%q:%d", variant.Name, variant.Payload) + } for _, field := range typ.Fields { fmt.Fprintf(&b, "|f:%q:%d", field.Name, field.Type) } @@ -268,6 +339,7 @@ func (t *TypeTable) fieldsTextLocked(fields []TypeField, separator byte) string func cloneType(typ Type) Type { typ.Fields = append([]TypeField(nil), typ.Fields...) typ.Params = append([]TypeID(nil), typ.Params...) + typ.Cases = append([]VariantCase(nil), typ.Cases...) if len(typ.Methods) == 0 { return typ } diff --git a/internal/ir/types_test.go b/internal/ir/types_test.go index 0faa417..ea3fa17 100644 --- a/internal/ir/types_test.go +++ b/internal/ir/types_test.go @@ -70,3 +70,45 @@ func TestTypeTableUsesLanguageNamesForStringTypes(t *testing.T) { t.Fatalf("string type text = %q, want str", got) } } + +func TestTypeTableInternsTaggedVariantIdentityAndCases(t *testing.T) { + types := NewTypeTable() + i32 := types.Intern(Type{Kind: TypeInteger, Signed: true, Bits: 32}) + str := types.Intern(Type{Kind: TypeString}) + + optionalID := types.Intern(OptionalVariant(i32)) + optional, ok := types.Type(optionalID) + if !ok || optional.Kind != TypeVariant || optional.Family != VariantFamilyOptional { + t.Fatalf("optional variant = (%#v, %t)", optional, ok) + } + if len(optional.Cases) != 2 || optional.Cases[OptionalAbsentCase].Payload != InvalidType || + optional.Cases[OptionalPresentCase].Payload != i32 { + t.Fatalf("optional cases = %#v", optional.Cases) + } + if got := types.Text(optionalID); got != "?i32" { + t.Fatalf("optional text = %q, want ?i32", got) + } + + result := Type{ + Kind: TypeVariant, Family: VariantFamilyNamed, Name: "Result", Identity: "app::Result", + Cases: []VariantCase{ + {Name: "Ok", Payload: i32}, + {Name: "Error", Payload: str}, + {Name: "Pending"}, + }, + } + resultID := types.Intern(result) + otherID := types.Intern(Type{ + Kind: TypeVariant, Family: VariantFamilyNamed, Name: "Result", Identity: "other::Result", + Cases: result.Cases, + }) + if resultID == otherID { + t.Fatal("nominally distinct variants shared one TypeID") + } + if types.Text(resultID) != "Result" || types.Text(otherID) != "Result" { + t.Fatalf("named variant text = %q and %q", types.Text(resultID), types.Text(otherID)) + } + if types.ABIKey(resultID) == types.ABIKey(otherID) { + t.Fatal("nominally distinct variants shared one ABI key") + } +} diff --git a/internal/project/modules.go b/internal/project/modules.go index 0833654..2bdb4a8 100644 --- a/internal/project/modules.go +++ b/internal/project/modules.go @@ -130,6 +130,14 @@ func (m *Module) DefiningModuleKey() symbols.DefiningModuleKey { } } +// TypeDeclarationIdentity anchors nominal type identity at its declaring module. +func (m *Module) TypeDeclarationIdentity(name string) string { + if m == nil || m.Key == "" || name == "" { + return name + } + return m.Key + "::" + name +} + func NewSemanticInfo() *SemanticInfo { return &SemanticInfo{ BlockScopes: make(map[ast.NodeID]*symbols.Scope), diff --git a/internal/semantics/binder/binder.go b/internal/semantics/binder/binder.go index 9526f3d..8efa69d 100644 --- a/internal/semantics/binder/binder.go +++ b/internal/semantics/binder/binder.go @@ -101,10 +101,12 @@ func (b *binder) bindTypeDecl(decl ast.TypeDecl) { if defined, ok := sym.Type.(*typeinfo.DefinedType); ok && defined != nil { // Reuse same shell so self-references keep same type identity. defined.Name = name.Name + defined.Identity = b.module.TypeDeclarationIdentity(name.Name) defined.Underlying = underlying } else { sym.BindType(&typeinfo.DefinedType{ Name: name.Name, + Identity: b.module.TypeDeclarationIdentity(name.Name), Underlying: underlying, }) } diff --git a/internal/semantics/collector/collector.go b/internal/semantics/collector/collector.go index d5e31f3..04bf57f 100644 --- a/internal/semantics/collector/collector.go +++ b/internal/semantics/collector/collector.go @@ -111,7 +111,8 @@ func (c *collector) collectConcreteTypeDecl(name *ast.Ident, node ast.Node) { } sym := symbols.New(name.Name, symbols.SymbolType, node, ast.LocOf(name)) sym.Type = &typeinfo.DefinedType{ - Name: name.Name, + Name: name.Name, + Identity: c.module.TypeDeclarationIdentity(name.Name), // Underlying is filled by binder. } if err := c.module.ModuleScope.Declare(sym); err != nil { diff --git a/internal/semantics/collector/collector_test.go b/internal/semantics/collector/collector_test.go index 2cc1e91..139cf7b 100644 --- a/internal/semantics/collector/collector_test.go +++ b/internal/semantics/collector/collector_test.go @@ -10,6 +10,7 @@ import ( "compiler/internal/frontend/parser" "compiler/internal/project" "compiler/internal/semantics/symbols" + "compiler/internal/semantics/typeinfo" "compiler/pkg/peeper" ) @@ -52,6 +53,34 @@ fn (self: Counter) Read() -> i32 { return self.value; }` } } +func TestCollectedDefinedTypeKeepsDeclaringModuleIdentity(t *testing.T) { + const filePath = "collector_type_identity_test" + peeper.SourceExt + const src = `enum Status { Ready }` + diag := diagnostics.NewDiagnosticBag() + module := &project.Module{ + Key: project.ModuleKeyFor(project.ModuleOriginLocal, filePath), + FilePath: filePath, + Content: src, + AST: parser.New(filePath, lexer.New(filePath, src, diag).Tokenize(), diag).ParseModule(), + Imports: make(map[string]project.ResolvedImport), + } + ctx := project.New(".", peeper.SourceExt, diag) + Collect(ctx, module) + + sym, ok := module.ModuleScope.LookupLocal("Status") + if !ok || sym == nil { + t.Fatal("collected enum type missing") + } + defined, ok := sym.Type.(*typeinfo.DefinedType) + if !ok || defined == nil { + t.Fatalf("collected enum type = %T, want DefinedType", sym.Type) + } + want := module.Key + "::Status" + if defined.Identity != want { + t.Fatalf("collected enum identity = %q, want %q", defined.Identity, want) + } +} + func TestImportSymbolsKeepSourceLocation(t *testing.T) { const filePath = "collector_import_test" + peeper.SourceExt src := `import "external"; diff --git a/internal/semantics/flowresult/result.go b/internal/semantics/flowresult/result.go index 56486f5..b7d2047 100644 --- a/internal/semantics/flowresult/result.go +++ b/internal/semantics/flowresult/result.go @@ -11,35 +11,45 @@ import ( "compiler/internal/semantics/typeinfo" ) -type PresenceFact struct { +type VariantFact struct { CarrierOrigins []place.Origin - Depth int + Cases []int + CaseCount int Dependencies []symbols.SymbolID } type Facts struct { - Presence []PresenceFact + Variants []VariantFact ReferenceOrigins map[symbols.SymbolID][]place.Origin RawPointerOrigins map[symbols.SymbolID][]place.Origin } type PayloadAccess struct { CarrierOrigins []place.Origin - Depth int + Cases []int Direct bool } +// OptionalTest is base typechecker evidence for source `none` comparisons. +// Flow converts it into case-based VariantTest evidence. type OptionalTest struct { SubjectID ast.NodeID PresentWhenTrue bool - Depth int +} + +type VariantTest struct { + SubjectID ast.NodeID + Case int + CaseWhenTrue bool + CaseCount int + PayloadPath []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 + VariantTests map[ast.NodeID]VariantTest 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 7fc6a13..ac27a62 100644 --- a/internal/semantics/ownership/expr.go +++ b/internal/semantics/ownership/expr.go @@ -76,7 +76,7 @@ func (a *analyzer) checkExpr( return } if use != useRead && ownershipTrackedType(a.exprType(e)) { - if a.partialOptionalPayloadMove(e) { + if a.partialVariantPayloadMove(e) { a.ctx.Diagnostics.AddError(diagnostics.ErrInvalidCopy, "move-only optional payload cannot be moved from partial place; borrow it instead", ast.LocOf(e), "") return @@ -210,7 +210,7 @@ func (a *analyzer) checkSelector( return } if ownershipTrackedType(a.exprType(selector)) { - if a.partialOptionalPayloadMove(selector) { + if a.partialVariantPayloadMove(selector) { a.ctx.Diagnostics.AddError(diagnostics.ErrInvalidCopy, "move-only optional payload cannot be moved from partial place; borrow it instead", ast.LocOf(selector), "") return @@ -364,12 +364,12 @@ func (a *analyzer) exprType(expr ast.Expr) typeinfo.Type { return a.module.EffectiveExprType(expr.ID()) } -func (a *analyzer) partialOptionalPayloadMove(expr ast.Expr) bool { +func (a *analyzer) partialVariantPayloadMove(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 + return ok && len(payload.Cases) > 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_test.go b/internal/semantics/ownership/ownership_test.go index 83ea318..4f11708 100644 --- a/internal/semantics/ownership/ownership_test.go +++ b/internal/semantics/ownership/ownership_test.go @@ -618,7 +618,7 @@ fn inspect(value: ?Token) { storage := []place.Origin{{Root: value}} payload := []place.Origin{{ Root: value, - Projections: []place.OriginProjection{{Kind: place.OriginOptionalPayload}}, + Projections: []place.OriginProjection{{Kind: place.OriginVariantPayload, Case: ir.OptionalPresentCase}}, }} if got := result.module.Flow.ResolvedStorageOrigins[valueUse.ID()]; !place.SameOrigins(got, storage) { t.Fatalf("payload storage origins = %#v, want carrier %#v", got, storage) diff --git a/internal/semantics/place/origin.go b/internal/semantics/place/origin.go index ea17e11..0424387 100644 --- a/internal/semantics/place/origin.go +++ b/internal/semantics/place/origin.go @@ -13,7 +13,7 @@ const ( OriginField OriginIndex OriginBindingIndex - OriginOptionalPayload + OriginVariantPayload OriginWildcard ) @@ -22,6 +22,7 @@ type OriginProjection struct { Field string Index string Binding *symbols.Symbol + Case int } type Origin struct { @@ -36,7 +37,7 @@ type ResolveOptions struct { RawPointerOrigins func(*symbols.Symbol) []Origin CallOrigins func(*ast.CallExpr) []Origin ConstantIndex func(ast.Expr) (string, bool) - PayloadDepth func(ast.Expr) int + PayloadCases func(ast.Expr) []int } // Resolution keeps carrier storage distinct from referenced value storage. @@ -86,7 +87,7 @@ func Resolve(scope *symbols.Scope, expr ast.Expr, opts ResolveOptions) Resolutio } base := Resolve(scope, node.Expr, opts) origins := appendIndirectProjection(base.ValueOrigins, node.Expr, opts.ExprType) - origins = appendOptionalPayloadProjections(origins, node.Expr, opts.PayloadDepth) + origins = appendVariantPayloadProjections(origins, node.Expr, opts.PayloadCases) origins = appendOriginProjection(origins, OriginProjection{Kind: OriginField, Field: node.Name.Name}) return Resolution{ StorageOrigins: origins, @@ -97,7 +98,7 @@ func Resolve(scope *symbols.Scope, expr ast.Expr, opts ResolveOptions) Resolutio case *ast.IndexExpr: base := Resolve(scope, node.Expr, opts) origins := appendIndirectProjection(base.ValueOrigins, node.Expr, opts.ExprType) - origins = appendOptionalPayloadProjections(origins, node.Expr, opts.PayloadDepth) + origins = appendVariantPayloadProjections(origins, node.Expr, opts.PayloadCases) dependencies := append([]*symbols.Symbol(nil), base.Dependencies...) if _, rangeIndex := node.Index.(*ast.RangeExpr); rangeIndex { origins = appendOriginProjection(origins, OriginProjection{Kind: OriginWildcard}) @@ -223,18 +224,18 @@ 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 { +func appendVariantPayloadProjections(origins []Origin, base ast.Expr, payloadCases func(ast.Expr) []int) []Origin { + if payloadCases == nil { return origins } - return PayloadOrigins(origins, payloadDepth(base)) + return VariantPayloadOrigins(origins, payloadCases(base)) } -// PayloadOrigins projects carrier storage through exact proven optional layers. -func PayloadOrigins(origins []Origin, depth int) []Origin { +// VariantPayloadOrigins projects carrier storage through exact proven cases. +func VariantPayloadOrigins(origins []Origin, cases []int) []Origin { out := CloneOrigins(origins) - for range depth { - out = appendOriginProjection(out, OriginProjection{Kind: OriginOptionalPayload}) + for _, caseIndex := range cases { + out = appendOriginProjection(out, OriginProjection{Kind: OriginVariantPayload, Case: caseIndex}) } return out } diff --git a/internal/semantics/place/origin_test.go b/internal/semantics/place/origin_test.go index f41802b..db8f78f 100644 --- a/internal/semantics/place/origin_test.go +++ b/internal/semantics/place/origin_test.go @@ -345,3 +345,15 @@ func TestOriginsOverlap(t *testing.T) { }) } } + +func TestVariantPayloadOriginsPreserveExactCasePath(t *testing.T) { + root := symbols.New("value", symbols.SymbolVar, nil, nil) + origins := VariantPayloadOrigins([]Origin{{Root: root}}, []int{2, 1}) + want := []Origin{{Root: root, Projections: []OriginProjection{ + {Kind: OriginVariantPayload, Case: 2}, + {Kind: OriginVariantPayload, Case: 1}, + }}} + if !SameOrigins(origins, want) { + t.Fatalf("variant payload origins = %#v, want %#v", origins, want) + } +} diff --git a/internal/semantics/typechecker/flow.go b/internal/semantics/typechecker/flow.go index ab9f2f0..d8a3ca3 100644 --- a/internal/semantics/typechecker/flow.go +++ b/internal/semantics/typechecker/flow.go @@ -15,14 +15,16 @@ import ( "compiler/internal/semantics/typeinfo" ) -type presenceStateFact struct { +type variantStateFact struct { origins []place.Origin - depth int + cases []int + caseCount int dependencies []*symbols.Symbol } type flowState struct { - presence []presenceStateFact + reachable bool + variants []variantStateFact references map[*symbols.Symbol][]place.Origin rawPointers map[*symbols.Symbol][]place.Origin } @@ -45,9 +47,9 @@ type flowExpressionEvents struct { calls []flowCallEvent } -type edgePresenceFact struct { - presence presenceStateFact - order int +type edgeVariantFact struct { + variant variantStateFact + order int } type flowAnalyzer struct { @@ -68,7 +70,7 @@ func CheckFlow(ctx *project.CompilerContext, module *project.Module) *flowresult 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), + VariantTests: make(map[ast.NodeID]flowresult.VariantTest), ResolvedStorageOrigins: make(map[ast.NodeID][]place.Origin), ResolvedValueOrigins: make(map[ast.NodeID][]place.Origin), } @@ -152,9 +154,10 @@ func (c *checker) effectiveExpressionType(scope *symbols.Scope, expr ast.Expr, b return unwrapOptionalLayers(base, required) } - proven := presenceDepth(c.flow.state.presence, resolution.StorageOrigins) - resolved := unwrapOptionalLayers(base, proven) + payloadCases := provenOptionalPayloadCases(c.flow.state.variants, resolution.StorageOrigins) + resolved := unwrapOptionalLayers(base, len(payloadCases)) applied := optionalLayerCount(base) - optionalLayerCount(resolved) + payloadCases = payloadCases[:applied] if c.optionalTestContext == 0 { if _, explicitCarrier := typeinfo.Underlying(expected).(*typeinfo.OptionalType); explicitCarrier { c.recordFlowResolution(expr, resolution) @@ -162,9 +165,9 @@ func (c *checker) effectiveExpressionType(scope *symbols.Scope, expr ast.Expr, b } } if applied > 0 { - c.recordPayloadAccess(expr, resolution, applied) + c.recordPayloadAccess(expr, resolution, payloadCases) } - valueOrigins := place.PayloadOrigins(resolution.StorageOrigins, applied) + valueOrigins := place.VariantPayloadOrigins(resolution.StorageOrigins, payloadCases) if _, _, reference := typeinfo.ReferenceValueTarget(resolved); reference { valueOrigins = place.CloneOrigins(resolution.ValueOrigins) } else if _, raw := typeinfo.Underlying(resolved).(*typeinfo.RawPtrType); raw { @@ -241,24 +244,32 @@ func (c *checker) recordOptionalTest(node *ast.BinaryExpr, subject ast.Expr) { return } if payload, ok := c.flow.result.Payloads[subject.ID()]; ok { - test.Depth = payload.Depth + c.flow.result.VariantTests[node.ID()] = flowresult.VariantTest{ + SubjectID: subject.ID(), Case: ir.OptionalPresentCase, + CaseWhenTrue: test.PresentWhenTrue, CaseCount: 2, + PayloadPath: append([]int(nil), payload.Cases...), + } + } else { + c.flow.result.VariantTests[node.ID()] = flowresult.VariantTest{ + SubjectID: subject.ID(), Case: ir.OptionalPresentCase, + CaseWhenTrue: test.PresentWhenTrue, CaseCount: 2, + } } - 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 { +func (c *checker) recordPayloadAccess(expr ast.Expr, resolution place.Resolution, cases []int) { + if c == nil || c.flow == nil || expr == nil || len(cases) == 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, + Cases: append([]int(nil), cases...), Direct: direct, } } @@ -322,11 +333,11 @@ func (c *checker) resolveFlowPlace(scope *symbols.Scope, expr ast.Expr, st flowS } return integer.Text(), true }, - PayloadDepth: func(base ast.Expr) int { + PayloadCases: func(base ast.Expr) []int { if c.flow == nil || base == nil { - return 0 + return nil } - return c.flow.result.Payloads[base.ID()].Depth + return c.flow.result.Payloads[base.ID()].Cases }, }) } @@ -336,6 +347,7 @@ func (a *flowAnalyzer) run() { return } order := make([]cfg.SiteID, 0) + disconnected := make(map[cfg.SiteID]bool) for _, block := range a.graph.Blocks { if block == nil { continue @@ -344,6 +356,7 @@ func (a *flowAnalyzer) run() { if site != nil { a.sites[site.ID] = site order = append(order, site.ID) + disconnected[site.ID] = !block.Reachable } } } @@ -373,7 +386,7 @@ func (a *flowAnalyzer) run() { for { if len(queue) == 0 { for _, id := range order { - if _, visited := a.inStates[id]; visited { + if _, visited := a.inStates[id]; visited || !disconnected[id] { continue } a.inStates[id] = copyFlowState(entryState) @@ -407,6 +420,9 @@ func (a *flowAnalyzer) run() { if site.Kind == cfg.SiteTerminator { a.applyConditionEdge(site, edge.Kind, &out, events) } + if !out.reachable { + continue + } current, exists := a.inStates[edge.To] merged := out if exists { @@ -426,6 +442,7 @@ func (a *flowAnalyzer) run() { func newFlowState() flowState { return flowState{ + reachable: true, references: make(map[*symbols.Symbol][]place.Origin), rawPointers: make(map[*symbols.Symbol][]place.Origin), } @@ -433,9 +450,10 @@ func newFlowState() flowState { 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, + dst.reachable = src.reachable + for _, fact := range src.variants { + dst.variants = append(dst.variants, variantStateFact{ + origins: place.CloneOrigins(fact.origins), cases: append([]int(nil), fact.cases...), caseCount: fact.caseCount, dependencies: append([]*symbols.Symbol(nil), fact.dependencies...), }) } @@ -453,15 +471,16 @@ func snapshotFlowState(st flowState) flowresult.Facts { ReferenceOrigins: make(map[symbols.SymbolID][]place.Origin), RawPointerOrigins: make(map[symbols.SymbolID][]place.Origin), } - for _, fact := range st.presence { + for _, fact := range st.variants { 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, + facts.Variants = append(facts.Variants, flowresult.VariantFact{ + CarrierOrigins: place.CloneOrigins(fact.origins), Cases: append([]int(nil), fact.cases...), + CaseCount: fact.caseCount, Dependencies: dependencies, }) } for sym, origins := range st.references { @@ -526,9 +545,9 @@ func (a *flowAnalyzer) applyStatementEffects(c *checker, scope *symbols.Scope, s } case *ast.AssignStmt: resolution := c.resolveFlowPlace(scope, node.Target, *st) - invalidatePresenceOrigins(st, resolution.StorageOrigins) + invalidateVariantOrigins(st, resolution.StorageOrigins) if sym := a.assignedSymbol(scope, node.Target); sym != nil { - invalidatePresenceDependency(st, sym) + invalidateVariantDependency(st, sym) a.updateOriginBinding(c, scope, sym, node.Value, st) } } @@ -589,20 +608,20 @@ func (a *flowAnalyzer) invalidateCall(c *checker, scope *symbols.Scope, call *as 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) + invalidateVariantOrigins(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) + invalidateVariantOrigins(st, origins) } else { - st.presence = nil + st.variants = nil } } } } for _, sym := range a.module.ModuleScope.Symbols() { if sym != nil && sym.IsMutable() { - invalidatePresenceOrigins(st, []place.Origin{{Root: sym}}) + invalidateVariantOrigins(st, []place.Origin{{Root: sym}}) } } } @@ -644,35 +663,37 @@ func (a *flowAnalyzer) applyConditionEdge(site *cfg.Site, edge cfg.EdgeKind, st if scope == nil { scope = a.functionScope } - for _, implied := range a.impliedPresence(scope, condition, edge == cfg.EdgeTrue, *st, events) { + for _, implied := range a.impliedVariants(scope, condition, edge == cfg.EdgeTrue, *st, events) { filtered := copyFlowState(*st) - filtered.presence = []presenceStateFact{implied.presence} + filtered.variants = nil + restrictVariantFact(&filtered, implied.variant) 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]) + if !filtered.reachable { + st.reachable = false + return + } + if len(filtered.variants) > 0 { + restrictVariantFact(st, filtered.variants[0]) } } } -func (a *flowAnalyzer) impliedPresence( +func (a *flowAnalyzer) impliedVariants( scope *symbols.Scope, expr ast.Expr, truth bool, st flowState, events *flowExpressionEvents, -) []edgePresenceFact { +) []edgeVariantFact { if expr == nil { return nil } - if test, found := a.result.OptionalTests[expr.ID()]; found { - if truth != test.PresentWhenTrue { - return nil - } + if test, found := a.result.VariantTests[expr.ID()]; found { 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) @@ -680,118 +701,191 @@ func (a *flowAnalyzer) impliedPresence( a.ctx.Diagnostics.Add(unstableOptionalNarrowingError(subject)) return nil } - return []edgePresenceFact{{ - presence: presenceStateFact{ - origins: place.CloneOrigins(resolution.StorageOrigins), depth: test.Depth + 1, + cases := []int{test.Case} + if truth != test.CaseWhenTrue { + cases = variantCasesExcept(test.CaseCount, test.Case) + } + order := 0 + if events != nil { + order = events.tests[expr.ID()] + } + return []edgeVariantFact{{ + variant: variantStateFact{ + origins: place.VariantPayloadOrigins(resolution.StorageOrigins, test.PayloadPath), + cases: cases, caseCount: test.CaseCount, dependencies: append([]*symbols.Symbol(nil), resolution.Dependencies...), }, - order: events.tests[expr.ID()], + order: order, }} } switch node := expr.(type) { case *ast.UnaryExpr: if node.Op == "!" { - return a.impliedPresence(scope, node.Expr, !truth, st, events) + return a.impliedVariants(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 a.constrainEdgeVariantFacts(scope, st, events, + a.impliedVariants(scope, node.Left, true, st, events), + a.impliedVariants(scope, node.Right, true, st, events), ) } - return intersectEdgePresenceFacts( - a.impliedPresence(scope, node.Left, false, st, events), - a.impliedPresence(scope, node.Right, false, st, events), + return alternateEdgeVariantFacts( + a.impliedVariants(scope, node.Left, false, st, events), + a.impliedVariants(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 alternateEdgeVariantFacts( + a.impliedVariants(scope, node.Left, true, st, events), + a.impliedVariants(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 a.constrainEdgeVariantFacts(scope, st, events, + a.impliedVariants(scope, node.Left, false, st, events), + a.impliedVariants(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 +func provenOptionalPayloadCases(facts []variantStateFact, origins []place.Origin) []int { + path := make([]int, 0) + current := place.CloneOrigins(origins) + for { + fact, found := variantFact(facts, current) + if !found || !sameCaseSet(fact.cases, []int{ir.OptionalPresentCase}) { + return path } + path = append(path, ir.OptionalPresentCase) + current = place.VariantPayloadOrigins(current, []int{ir.OptionalPresentCase}) } - return 0 } -func addPresenceFact(st *flowState, added presenceStateFact) { - if st == nil || len(added.origins) == 0 || added.depth <= 0 { +func restrictVariantFact(st *flowState, added variantStateFact) { + if st == nil || !st.reachable || len(added.origins) == 0 || added.caseCount <= 0 { + return + } + if len(added.cases) == 0 { + st.reachable = false + st.variants = nil 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 + if len(added.cases) >= added.caseCount { + return + } + for index := range st.variants { + if place.SameOrigins(st.variants[index].origins, added.origins) { + if st.variants[index].caseCount != added.caseCount { + st.reachable = false + st.variants = nil + return + } + st.variants[index].cases = intersectCaseSets(st.variants[index].cases, added.cases) + if len(st.variants[index].cases) == 0 { + st.reachable = false + st.variants = nil + return } - st.presence[index].dependencies = mergeDependencies(st.presence[index].dependencies, added.dependencies) + st.variants[index].dependencies = mergeDependencies(st.variants[index].dependencies, added.dependencies) return } } - st.presence = append(st.presence, presenceStateFact{ - origins: place.CloneOrigins(added.origins), depth: added.depth, + st.variants = append(st.variants, variantStateFact{ + origins: place.CloneOrigins(added.origins), cases: append([]int(nil), added.cases...), caseCount: added.caseCount, dependencies: append([]*symbols.Symbol(nil), added.dependencies...), }) } -func unionEdgePresenceFacts(left, right []edgePresenceFact) []edgePresenceFact { - merged := append([]edgePresenceFact(nil), left...) +func (a *flowAnalyzer) constrainEdgeVariantFacts( + scope *symbols.Scope, + st flowState, + events *flowExpressionEvents, + left, right []edgeVariantFact, +) []edgeVariantFact { + merged := make([]edgeVariantFact, len(left)) + for i, fact := range left { + merged[i] = edgeVariantFact{variant: variantStateFact{ + origins: place.CloneOrigins(fact.variant.origins), cases: append([]int(nil), fact.variant.cases...), + caseCount: fact.variant.caseCount, dependencies: append([]*symbols.Symbol(nil), fact.variant.dependencies...), + }, order: fact.order} + } for _, candidate := range right { found := false for index := range merged { - if !place.SameOrigins(merged[index].presence.origins, candidate.presence.origins) { + if !place.SameOrigins(merged[index].variant.origins, candidate.variant.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) + if a.variantFactInvalidatedBetween(scope, st, events, merged[index], candidate.order) { + candidate.variant.origins = place.CloneOrigins(candidate.variant.origins) + candidate.variant.cases = append([]int(nil), candidate.variant.cases...) + candidate.variant.dependencies = append([]*symbols.Symbol(nil), candidate.variant.dependencies...) + merged[index] = candidate + break } + merged[index].variant.cases = intersectCaseSets(merged[index].variant.cases, candidate.variant.cases) + merged[index].variant.dependencies = mergeDependencies( + merged[index].variant.dependencies, candidate.variant.dependencies, + ) + 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...) + candidate.variant.origins = place.CloneOrigins(candidate.variant.origins) + candidate.variant.cases = append([]int(nil), candidate.variant.cases...) + candidate.variant.dependencies = append([]*symbols.Symbol(nil), candidate.variant.dependencies...) merged = append(merged, candidate) } } return merged } -func intersectEdgePresenceFacts(left, right []edgePresenceFact) []edgePresenceFact { - out := make([]edgePresenceFact, 0) +func (a *flowAnalyzer) variantFactInvalidatedBetween( + scope *symbols.Scope, + st flowState, + events *flowExpressionEvents, + fact edgeVariantFact, + before int, +) bool { + if a == nil || events == nil || before <= fact.order { + return false + } + filtered := copyFlowState(st) + filtered.variants = nil + restrictVariantFact(&filtered, fact.variant) + if !filtered.reachable { + return false + } + checker := &checker{ctx: a.ctx, module: a.module, flow: &flowCheck{result: a.result, state: &filtered}} + for _, call := range events.calls { + if call.order > fact.order && call.order < before { + a.invalidateCall(checker, scope, call.call, &filtered) + } + } + _, found := variantFact(filtered.variants, fact.variant.origins) + return !found +} + +func alternateEdgeVariantFacts(left, right []edgeVariantFact) []edgeVariantFact { + out := make([]edgeVariantFact, 0) for _, leftFact := range left { for _, rightFact := range right { - if !place.SameOrigins(leftFact.presence.origins, rightFact.presence.origins) { + if !place.SameOrigins(leftFact.variant.origins, rightFact.variant.origins) || leftFact.variant.caseCount != rightFact.variant.caseCount { continue } - out = append(out, edgePresenceFact{ - presence: presenceStateFact{ - origins: place.CloneOrigins(leftFact.presence.origins), - depth: min(leftFact.presence.depth, rightFact.presence.depth), + cases := unionCaseSets(leftFact.variant.cases, rightFact.variant.cases) + if len(cases) >= leftFact.variant.caseCount { + break + } + out = append(out, edgeVariantFact{ + variant: variantStateFact{ + origins: place.CloneOrigins(leftFact.variant.origins), cases: cases, caseCount: leftFact.variant.caseCount, dependencies: mergeDependencies( - leftFact.presence.dependencies, rightFact.presence.dependencies, + leftFact.variant.dependencies, rightFact.variant.dependencies, ), }, order: min(leftFact.order, rightFact.order), @@ -802,15 +896,19 @@ func intersectEdgePresenceFacts(left, right []edgePresenceFact) []edgePresenceFa return out } -func intersectPresenceFacts(left, right []presenceStateFact) []presenceStateFact { - out := make([]presenceStateFact, 0) +func mergeVariantFacts(left, right []variantStateFact) []variantStateFact { + out := make([]variantStateFact, 0) for _, leftFact := range left { for _, rightFact := range right { - if !place.SameOrigins(leftFact.origins, rightFact.origins) { + if !place.SameOrigins(leftFact.origins, rightFact.origins) || leftFact.caseCount != rightFact.caseCount { continue } - out = append(out, presenceStateFact{ - origins: place.CloneOrigins(leftFact.origins), depth: min(leftFact.depth, rightFact.depth), + cases := unionCaseSets(leftFact.cases, rightFact.cases) + if len(cases) >= leftFact.caseCount { + break + } + out = append(out, variantStateFact{ + origins: place.CloneOrigins(leftFact.origins), cases: cases, caseCount: leftFact.caseCount, dependencies: mergeDependencies(leftFact.dependencies, rightFact.dependencies), }) break @@ -820,8 +918,14 @@ func intersectPresenceFacts(left, right []presenceStateFact) []presenceStateFact } func mergeFlowStates(left, right flowState) flowState { + if !left.reachable { + return copyFlowState(right) + } + if !right.reachable { + return copyFlowState(left) + } merged := newFlowState() - merged.presence = intersectPresenceFacts(left.presence, right.presence) + merged.variants = mergeVariantFacts(left.variants, right.variants) for sym, origins := range left.references { merged.references[sym] = place.CloneOrigins(origins) } @@ -838,12 +942,13 @@ func mergeFlowStates(left, right flowState) flowState { } func sameFlowState(left, right flowState) bool { - if len(left.presence) != len(right.presence) || len(left.references) != len(right.references) || + if left.reachable != right.reachable || len(left.variants) != len(right.variants) || 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 { + for _, fact := range left.variants { + rightFact, found := variantFact(right.variants, fact.origins) + if !found || rightFact.caseCount != fact.caseCount || !sameCaseSet(rightFact.cases, fact.cases) { return false } } @@ -854,6 +959,66 @@ func sameFlowState(left, right flowState) bool { return true } +func variantFact(facts []variantStateFact, origins []place.Origin) (variantStateFact, bool) { + for _, fact := range facts { + if place.SameOrigins(fact.origins, origins) { + return fact, true + } + } + return variantStateFact{}, false +} + +func variantCasesExcept(caseCount, excluded int) []int { + cases := make([]int, 0, max(caseCount-1, 0)) + for caseIndex := range caseCount { + if caseIndex != excluded { + cases = append(cases, caseIndex) + } + } + return cases +} + +func sameCaseSet(left, right []int) bool { + if len(left) != len(right) { + return false + } + for _, candidate := range left { + if !containsCase(right, candidate) { + return false + } + } + return true +} + +func intersectCaseSets(left, right []int) []int { + out := make([]int, 0, min(len(left), len(right))) + for _, candidate := range left { + if containsCase(right, candidate) { + out = append(out, candidate) + } + } + return out +} + +func unionCaseSets(left, right []int) []int { + out := append([]int(nil), left...) + for _, candidate := range right { + if !containsCase(out, candidate) { + out = append(out, candidate) + } + } + return out +} + +func containsCase(cases []int, candidate int) bool { + for _, caseIndex := range cases { + if caseIndex == candidate { + return true + } + } + return false +} + func mergeDependencies(left, right []*symbols.Symbol) []*symbols.Symbol { merged := append([]*symbols.Symbol(nil), left...) for _, candidate := range right { @@ -871,12 +1036,12 @@ func mergeDependencies(left, right []*symbols.Symbol) []*symbols.Symbol { return merged } -func invalidatePresenceDependency(st *flowState, assigned *symbols.Symbol) { +func invalidateVariantDependency(st *flowState, assigned *symbols.Symbol) { if st == nil || assigned == nil { return } - kept := st.presence[:0] - for _, fact := range st.presence { + kept := st.variants[:0] + for _, fact := range st.variants { dependent := false for _, dependency := range fact.dependencies { if dependency == assigned { @@ -888,58 +1053,47 @@ func invalidatePresenceDependency(st *flowState, assigned *symbols.Symbol) { kept = append(kept, fact) } } - st.presence = kept + st.variants = kept } -func invalidatePresenceOrigins(st *flowState, mutated []place.Origin) { +func invalidateVariantOrigins(st *flowState, mutated []place.Origin) { if st == nil || len(mutated) == 0 { return } - kept := st.presence[:0] - for _, fact := range st.presence { + kept := st.variants[:0] + for _, fact := range st.variants { if !place.OriginsOverlap(fact.origins, mutated) { kept = append(kept, fact) continue } - preserved := fact.depth + preserved := true 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 !mutationPreservesVariantCase(carrier, mutation) { + preserved = false } } } - if preserved > 0 { - fact.depth = preserved + if preserved { kept = append(kept, fact) } } - st.presence = kept + st.variants = kept } -func payloadDescendantDepth(carrier, mutation place.Origin) int { +func mutationPreservesVariantCase(carrier, mutation place.Origin) bool { if carrier.Root == nil || carrier.Root != mutation.Root || len(mutation.Projections) <= len(carrier.Projections) { - return 0 + return false } 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 + return false } - depth++ } - return depth + return mutation.Projections[len(carrier.Projections)].Kind == place.OriginVariantPayload } func clearFlowScope(scope *symbols.Scope, st *flowState) { @@ -949,7 +1103,7 @@ func clearFlowScope(scope *symbols.Scope, st *flowState) { for _, sym := range scope.Symbols() { delete(st.references, sym) delete(st.rawPointers, sym) - invalidatePresenceDependency(st, sym) - invalidatePresenceOrigins(st, []place.Origin{{Root: sym}}) + invalidateVariantDependency(st, sym) + invalidateVariantOrigins(st, []place.Origin{{Root: sym}}) } } diff --git a/internal/semantics/typechecker/flow_test.go b/internal/semantics/typechecker/flow_test.go index 791f9c9..f9bbde0 100644 --- a/internal/semantics/typechecker/flow_test.go +++ b/internal/semantics/typechecker/flow_test.go @@ -25,10 +25,10 @@ func TestClearFlowScopeRemovesOnlyExitedBindingFacts(t *testing.T) { 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}}, + variants: []variantStateFact{ + {origins: outerOrigins, cases: []int{1}, caseCount: 2}, + {origins: innerOrigins, cases: []int{1}, caseCount: 2}, + {origins: outerOrigins, cases: []int{1}, caseCount: 2, dependencies: []*symbols.Symbol{inner}}, }, references: map[*symbols.Symbol][]place.Origin{outer: outerOrigins, inner: innerOrigins}, rawPointers: map[*symbols.Symbol][]place.Origin{outer: outerOrigins, inner: innerOrigins}, @@ -36,8 +36,8 @@ func TestClearFlowScopeRemovesOnlyExitedBindingFacts(t *testing.T) { 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 len(state.variants) != 1 || state.variants[0].origins[0].Root != outer { + t.Fatalf("variant facts after scope exit = %#v, want only outer fact", state.variants) } if _, exists := state.references[inner]; exists { t.Fatal("scope exit retained inner reference origin") @@ -56,7 +56,7 @@ func TestInvalidateCallClearsMutableModuleVariableFacts(t *testing.T) { if err := moduleScope.Declare(global); err != nil { t.Fatal(err) } - state := flowState{presence: []presenceStateFact{{origins: []place.Origin{{Root: global}}, depth: 1}}} + state := flowState{variants: []variantStateFact{{origins: []place.Origin{{Root: global}}, cases: []int{1}, caseCount: 2}}} analyzer := flowAnalyzer{ module: &project.Module{ModuleScope: moduleScope, Semantics: project.NewSemanticInfo()}, result: &flowresult.Result{ExprTypes: make(map[ast.NodeID]typeinfo.Type)}, @@ -64,7 +64,32 @@ func TestInvalidateCallClearsMutableModuleVariableFacts(t *testing.T) { 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) + if len(state.variants) != 0 { + t.Fatalf("variant facts after call = %#v, want mutable module fact invalidated", state.variants) + } +} + +func TestMergeVariantFactsUnionsPossibleCases(t *testing.T) { + root := symbols.New("value", symbols.SymbolVar, nil, nil) + origins := []place.Origin{{Root: root}} + left := flowState{reachable: true, variants: []variantStateFact{{origins: origins, cases: []int{0}, caseCount: 3}}} + right := flowState{reachable: true, variants: []variantStateFact{{origins: origins, cases: []int{1}, caseCount: 3}}} + + merged := mergeFlowStates(left, right) + if !merged.reachable || len(merged.variants) != 1 || !sameCaseSet(merged.variants[0].cases, []int{0, 1}) { + t.Fatalf("merged variant facts = %#v", merged) + } +} + +func TestInvalidateVariantFactsPreservesCaseForPayloadDescendant(t *testing.T) { + root := symbols.New("value", symbols.SymbolVar, nil, nil) + carrier := []place.Origin{{Root: root}} + state := flowState{variants: []variantStateFact{{origins: carrier, cases: []int{1}, caseCount: 2}}} + mutated := place.VariantPayloadOrigins(carrier, []int{1}) + mutated[0].Projections = append(mutated[0].Projections, place.OriginProjection{Kind: place.OriginField, Field: "field"}) + + invalidateVariantOrigins(&state, mutated) + if len(state.variants) != 1 || !sameCaseSet(state.variants[0].cases, []int{1}) { + t.Fatalf("payload mutation invalidated carrier case = %#v", state.variants) } } diff --git a/internal/semantics/typeinfo/types.go b/internal/semantics/typeinfo/types.go index d6c9375..2164e91 100644 --- a/internal/semantics/typeinfo/types.go +++ b/internal/semantics/typeinfo/types.go @@ -43,6 +43,7 @@ type NamedType struct { type DefinedType struct { Name string + Identity string Underlying Type } @@ -61,6 +62,25 @@ type OptionalType struct { Inner Type } +type VariantFamily uint8 + +const ( + VariantFamilyInvalid VariantFamily = iota + VariantFamilyOptional + VariantFamilyNamed +) + +type VariantCase struct { + Name string + Payload Type +} + +type VariantDescriptor struct { + Family VariantFamily + Identity string + Cases []VariantCase +} + type ArrayShape uint8 const ( @@ -206,6 +226,47 @@ func Underlying(t Type) Type { } } +// VariantDescriptorOf is source semantics' canonical variant classification. +// It preserves nominal identity before inspecting a defined enum's underlying +// representation, while optionals remain structural source types. +func VariantDescriptorOf(t Type) (VariantDescriptor, bool) { + identity := "" + if defined, ok := t.(*DefinedType); ok && defined != nil { + identity = defined.Identity + if identity == "" { + identity = defined.Name + } + t = defined.Underlying + } + switch variant := t.(type) { + case *OptionalType: + if variant == nil || variant.Inner == nil { + return VariantDescriptor{}, false + } + return VariantDescriptor{ + Family: VariantFamilyOptional, + Cases: []VariantCase{ + {Name: "Absent"}, + {Name: "Present", Payload: variant.Inner}, + }, + }, true + case *EnumType: + if variant == nil || len(variant.Variants) == 0 { + return VariantDescriptor{}, false + } + if identity == "" { + identity = variant.Text() + } + cases := make([]VariantCase, len(variant.Variants)) + for i, name := range variant.Variants { + cases[i].Name = name + } + return VariantDescriptor{Family: VariantFamilyNamed, Identity: identity, Cases: cases}, true + default: + return VariantDescriptor{}, false + } +} + func (t *OwnedPtrType) Text() string { if t == nil { return "" diff --git a/internal/semantics/typeinfo/types_test.go b/internal/semantics/typeinfo/types_test.go index 25011af..e4672db 100644 --- a/internal/semantics/typeinfo/types_test.go +++ b/internal/semantics/typeinfo/types_test.go @@ -412,3 +412,22 @@ func TestDynamicArrayRequiresRecursivelySizedElement(t *testing.T) { t.Fatalf("dynamic array of fixed arrays containing bare interfaces must be unsized") } } + +func TestVariantDescriptorUnifiesOptionalAndNamedEnumCases(t *testing.T) { + i32 := &IntegerType{Signed: true, Bits: 32} + optional, ok := VariantDescriptorOf(&OptionalType{Inner: i32}) + if !ok || optional.Family != VariantFamilyOptional || optional.Identity != "" || len(optional.Cases) != 2 || + optional.Cases[0].Name != "Absent" || optional.Cases[0].Payload != nil || + optional.Cases[1].Name != "Present" || TypeText(optional.Cases[1].Payload) != "i32" { + t.Fatalf("optional descriptor = %#v", optional) + } + + named, ok := VariantDescriptorOf(&DefinedType{ + Name: "Status", + Underlying: &EnumType{Variants: []string{"Ready", "Waiting"}}, + }) + if !ok || named.Family != VariantFamilyNamed || named.Identity != "Status" || len(named.Cases) != 2 || + named.Cases[0].Name != "Ready" || named.Cases[1].Name != "Waiting" { + t.Fatalf("named descriptor = %#v", named) + } +} diff --git a/x_test/runtime_optional_narrowing/src/main.peep b/x_test/runtime_optional_narrowing/src/main.peep index b869055..a320738 100644 --- a/x_test/runtime_optional_narrowing/src/main.peep +++ b/x_test/runtime_optional_narrowing/src/main.peep @@ -33,6 +33,32 @@ fn TakeOptional(value: ?Token) -> i32 { return Take(value); } +fn Clear(holder: &mut Holder) -> bool { + holder.field = none; + return true; +} + +fn KeepProofAcrossImpossibleEdge(value: ?i32, other: ?i32) -> i32 { + if value != none { + if other != none && other == none { + let ignored = 0; + } + return value; + } + return 0; +} + +fn KeepEagerMutationOrder(value: ?i32, other: Holder) -> i32 { + if value == none { + return 0; + } + let mut holder = other; + if holder.field != none && Clear(&mut holder) && holder.field == none { + return value; + } + return 0; +} + fn main() -> i32 { let maybe = Some(7); if maybe == none { @@ -105,5 +131,13 @@ fn main() -> i32 { return 8; } + if KeepProofAcrossImpossibleEdge(Some(29), Some(1)) != 29 { + return 9; + } + let eagerHolder = .Holder{field = Some(1), items = [2]?i32{none, none}}; + if KeepEagerMutationOrder(Some(31), eagerHolder) != 31 { + return 10; + } + return Guard(Some(17)) - 17; }