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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
415 changes: 0 additions & 415 deletions architecture-audit-masterprompt.md

This file was deleted.

42 changes: 22 additions & 20 deletions docs/architecture/backend.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,32 +30,34 @@ and an optional source location. Each block contains MIR instructions and one
terminator. `Instr` and `Terminator` are sealed interfaces; the emitter uses
exhaustive type switches and panics if a new MIR node is not classified.

The result is LLVM module text. Invalid target or invalid runtime-symbol use
returns an empty string after diagnostics. Unsupported layouts and lowering
errors set emitter invalid state; `finalLLVMText` then returns an empty string.
The result is LLVM module text. Invalid target, malformed MIR, or invalid
runtime-symbol use returns an empty string after diagnostics. Unsupported
layouts and lowering errors set emitter invalid state; `finalLLVMText` then
returns an empty string.

## Module emission job

`GenerateLLVMIR` emits in this order:

1. Validate non-nil module and `target.Info.Valid()`.
2. Validate reserved runtime symbols with `ValidateRuntimeSymbols`.
3. Create one `llvmEmitter` for the module.
4. Emit `source_filename` and `target triple`.
5. Resolve and emit identified named type definitions.
6. Emit print format globals when print is used.
7. Emit MIR static data: raw byte arrays or typed constants.
8. Discover interface constructions and emit unique itab globals.
9. Emit declared MIR functions.
10. Emit print, allocator, free, and overflow-intrinsic declarations as needed.
11. Emit default allocator descriptor thunks when allocator runtime is needed.
12. Collect and emit direct-call declarations.
13. If no function has blocks, finalize.
14. Emit named drop helpers.
15. Emit MIR interface thunks.
16. Emit interface payload drop/release thunks.
17. Emit each defined function and its blocks.
18. Append late external globals and debug metadata.
2. Validate backend-independent MIR invariants with `mir.Module.Validate`.
3. Validate reserved runtime symbols with `ValidateRuntimeSymbols`.
4. Create one `llvmEmitter` for the module.
5. Emit `source_filename` and `target triple`.
6. Resolve and emit identified named type definitions.
7. Emit print format globals when print is used.
8. Emit MIR static data: raw byte arrays or typed constants.
9. Discover interface constructions and emit unique itab globals.
10. Emit declared MIR functions.
11. Emit print, allocator, free, and overflow-intrinsic declarations as needed.
12. Emit default allocator descriptor thunks when allocator runtime is needed.
13. Collect and emit direct-call declarations.
14. If no function has blocks, finalize.
15. Emit named drop helpers.
16. Emit MIR interface thunks.
17. Emit interface payload drop/release thunks.
18. Emit each defined function and its blocks.
19. Append late external globals and debug metadata.

Static strings are already interned by `mir.Module.InternString`; raw entries are
escaped by `llvmEscapeString`. Typed constants are lowered by `staticConstant`.
Expand Down
6 changes: 5 additions & 1 deletion docs/architecture/ir.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,11 @@ signed indexes also reject negative values independently of unsigned length.
- effect and ownership result validators reject inconsistent analysis evidence.
- `mir.Module.Validate` rejects invalid runtime types, places, instructions,
terminators, calls, and return shapes before backend emission.
- LLVM emission treats validated MIR violations as internal invariants.
- The pipeline validates MIR before entering backend phase, and LLVM emission
revalidates at its direct entry boundary so callers that bypass the pipeline
receive diagnostics instead of sending malformed MIR into lowering.
- After these validation boundaries, LLVM emission treats unclassified backend
invariants as internal failures.

## Adding a language construct

Expand Down
6 changes: 6 additions & 0 deletions internal/backend/llvm/emitter.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ func GenerateLLVMIR(mod *mir.Module, diag *diagnostics.DiagnosticBag, targetInfo
}
return ""
}
if err := mod.Validate(); err != nil {
if diag != nil {
diag.AddError(diagnostics.ErrInvalidEvidence, "MIR is malformed: "+err.Error(), nil, "")
}
return ""
}
if !ValidateRuntimeSymbols([]*mir.Module{mod}, diag) {
return ""
}
Expand Down
27 changes: 12 additions & 15 deletions internal/backend/llvm/emitter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -351,11 +351,7 @@ func TestTypedLLVMBuilderRejectsOperandMismatches(t *testing.T) {
}
}

// mir.Instr and mir.Terminator are sealed, so a node this package could pass to
// emission no longer exists: the earlier fabricated one cannot implement either
// interface. What remains reachable is a block that carries no terminator at
// all, which would otherwise emit an unterminated basic block with no signal.
func TestGenerateLLVMIRPanicsForMalformedMIRBlocks(t *testing.T) {
func TestGenerateLLVMIRRejectsMalformedMIRBlocks(t *testing.T) {
for _, tt := range []struct {
name string
block *mir.Block
Expand All @@ -364,24 +360,25 @@ func TestGenerateLLVMIRPanicsForMalformedMIRBlocks(t *testing.T) {
{
name: "missing terminator",
block: &mir.Block{ID: 7},
want: "LLVM emission: block b7 has no terminator",
want: "MIR is malformed: function test block b7 has no terminator",
},
} {
t.Run(tt.name, func(t *testing.T) {
defer func() {
if recovered := recover(); recovered != tt.want {
t.Fatalf("GenerateLLVMIR panic = %#v; want %q", recovered, tt.want)
}
}()
GenerateLLVMIR(&mir.Module{
diag := diagnostics.NewDiagnosticBag()
if out := GenerateLLVMIR(&mir.Module{
Name: "unknown-node",
Types: llvmTypes.table,
Funcs: []*mir.Function{{
Name: "test",
ReturnType: llvmTypes.void,
Blocks: []*mir.Block{tt.block},
}},
}, diagnostics.NewDiagnosticBag(), testLinuxAMD64, false)
}, diag, testLinuxAMD64, false); out != "" {
t.Fatalf("GenerateLLVMIR() = %q, want empty output", out)
}
if !diag.HasErrors() || !strings.Contains(diag.EmitAllToString(), tt.want) {
t.Fatalf("diagnostics = %q, want %q", diag.EmitAllToString(), tt.want)
}
})
}
}
Expand Down Expand Up @@ -1477,7 +1474,7 @@ func TestGenerateLLVMIRRejectsIncompleteNamedType(t *testing.T) {
if out := GenerateLLVMIR(mod, diag, testLinuxAMD64, false); out != "" {
t.Fatalf("incomplete named type emitted LLVM:\n%s", out)
}
if !diag.HasErrors() || !strings.Contains(diag.EmitAllToString(), "unsupported llvm type: Pending") {
if !diag.HasErrors() || !strings.Contains(diag.EmitAllToString(), "MIR is malformed: function use_pending parameter 0 has invalid type#") {
t.Fatalf("incomplete named type diagnostic missing:\n%s", diag.EmitAllToString())
}
}
Expand Down Expand Up @@ -3583,7 +3580,7 @@ func TestGenerateLLVMIRRejectsIncompleteVariantSwitch(t *testing.T) {
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") {
if !diag.HasErrors() || !strings.Contains(diag.EmitAllToString(), "MIR is malformed: function select block b0 variant switch has 1 targets, want 2") {
t.Fatalf("incomplete variant switch diagnostic missing:\n%s", diag.EmitAllToString())
}
}
Expand Down
44 changes: 25 additions & 19 deletions internal/ir/mir/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ func validateInstruction(types *ir.TypeTable, fn *Function, block *Block, index
case *Store:
problems := validatePlace(types, node.Place, where+" store")
problems = append(problems, validateValueRef(types, node.Value, where+" store value")...)
if node.Place != nil && node.Value != nil && node.Place.Type != node.Value.TypeID() {
if node.Place != nil && !typednil.IsNil(node.Value) && node.Place.Type != node.Value.TypeID() {
problems = append(problems, fmt.Sprintf("%s stores type#%d into place type#%d", where, node.Value.TypeID(), node.Place.Type))
}
return problems
Expand All @@ -150,7 +150,7 @@ func validateInstruction(types *ir.TypeTable, fn *Function, block *Block, index
if !arrayOK || arrayType.Kind != ir.TypeArray || arrayType.Length != "" {
problems = append(problems, fmt.Sprintf("%s operates on non-dynamic-array type#%d", where, node.ArrayType))
}
if node.Array != nil {
if !typednil.IsNil(node.Array) {
carrier, carrierOK := types.Type(node.Array.TypeID())
if !carrierOK || carrier.Kind != ir.TypeReference || carrier.Elem != node.ArrayType {
problems = append(problems, fmt.Sprintf("%s array value type#%d does not reference array type#%d", where, node.Array.TypeID(), node.ArrayType))
Expand Down Expand Up @@ -179,14 +179,14 @@ func validateTerminator(types *ir.TypeTable, fn *Function, block *Block, blocks
switch term := block.Term.(type) {
case *Branch:
problems = append(problems, validateValueRef(types, term.Cond, where+" branch condition")...)
if term.Cond != nil && types != nil {
if !typednil.IsNil(term.Cond) && types != nil {
if typ, ok := types.Type(term.Cond.TypeID()); !ok || typ.Kind != ir.TypeBool {
problems = append(problems, fmt.Sprintf("%s branches on non-bool type#%d", where, term.Cond.TypeID()))
}
}
case *SwitchVariant:
problems = append(problems, validateValueRef(types, term.Value, where+" variant switch value")...)
if term.Value != nil && types != nil {
if !typednil.IsNil(term.Value) && types != nil {
if variant, ok := types.Type(term.Value.TypeID()); !ok || variant.Kind != ir.TypeVariant {
problems = append(problems, fmt.Sprintf("%s switches on non-variant type#%d", where, term.Value.TypeID()))
} else {
Expand Down Expand Up @@ -215,6 +215,9 @@ func validateTerminator(types *ir.TypeTable, fn *Function, block *Block, blocks
break
}
problems = append(problems, validateValueRef(types, term.Value, where+" return value")...)
if typednil.IsNil(term.Value) {
break
}
if returnType.Kind == ir.TypeVoid {
problems = append(problems, fmt.Sprintf("%s returns a value from void function", where))
} else if term.Value.TypeID() != fn.ReturnType {
Expand Down Expand Up @@ -326,7 +329,7 @@ func validateValueExpr(types *ir.TypeTable, expr ValueExpr, where string) []stri
}
case *Field:
problems = append(problems, validateValueRef(types, node.Base, where+" field base")...)
if node.Base != nil && types != nil {
if !typednil.IsNil(node.Base) && types != nil {
if base, ok := types.Type(node.Base.TypeID()); !ok || base.Kind != ir.TypeStruct || node.Index < 0 || node.Index >= len(base.Fields) {
problems = append(problems, fmt.Sprintf("%s selects invalid field %d from type#%d", where, node.Index, node.Base.TypeID()))
} else if base.Fields[node.Index].Type != node.Type {
Expand All @@ -347,7 +350,7 @@ func validateValueExpr(types *ir.TypeTable, expr ValueExpr, where string) []stri
problems = append(problems, validateValueRef(types, node.Allocator, where+" allocator")...)
}
if types != nil {
if allocated, ok := types.Type(node.Type); ok && allocated.Kind == ir.TypeOwnedPtr && node.Value != nil && allocated.Elem != node.Value.TypeID() {
if allocated, ok := types.Type(node.Type); ok && allocated.Kind == ir.TypeOwnedPtr && !typednil.IsNil(node.Value) && allocated.Elem != node.Value.TypeID() {
problems = append(problems, fmt.Sprintf("%s owns type#%d but allocates type#%d", where, allocated.Elem, node.Value.TypeID()))
}
}
Expand All @@ -368,7 +371,7 @@ func validateValueExpr(types *ir.TypeTable, expr ValueExpr, where string) []stri
for index, slot := range node.Slots {
problems = append(problems, validateValueRef(types, slot, fmt.Sprintf("%s interface slot %d", where, index))...)
method, ok := types.InterfaceMethod(node.Type, index)
if ok && slot != nil && slot.TypeID() != method.SlotType {
if ok && !typednil.IsNil(slot) && slot.TypeID() != method.SlotType {
problems = append(problems, fmt.Sprintf("%s interface slot %d has type#%d, want published type#%d", where, index, slot.TypeID(), method.SlotType))
}
}
Expand All @@ -391,7 +394,7 @@ func validateCall(types *ir.TypeTable, call *Call, where string) []string {
for index, arg := range call.Args {
problems = append(problems, validateValueRef(types, arg, fmt.Sprintf("%s argument %d", where, index))...)
}
if call.Callee == nil || types == nil {
if typednil.IsNil(call.Callee) || types == nil {
return problems
}
function, ok := types.Type(call.Callee.TypeID())
Expand All @@ -406,7 +409,7 @@ func validateCall(types *ir.TypeTable, call *Call, where string) []string {
return problems
}
for index, arg := range call.Args {
if arg != nil && arg.TypeID() != function.Params[index] {
if !typednil.IsNil(arg) && arg.TypeID() != function.Params[index] {
problems = append(problems, fmt.Sprintf("%s argument %d has type#%d, want type#%d", where, index, arg.TypeID(), function.Params[index]))
}
}
Expand All @@ -422,7 +425,7 @@ func validateInterfaceCall(types *ir.TypeTable, call *InterfaceCall, where strin
for index, arg := range call.Args {
problems = append(problems, validateValueRef(types, arg, fmt.Sprintf("%s interface argument %d", where, index))...)
}
if call.Base == nil || types == nil {
if typednil.IsNil(call.Base) || types == nil {
return problems
}
method, ok := types.InterfaceMethod(call.Base.TypeID(), call.Slot)
Expand All @@ -446,7 +449,7 @@ func validateInterfaceCall(types *ir.TypeTable, call *InterfaceCall, where strin
}
for index, arg := range call.Args {
param := index + 1
if param < len(slotType.Params) && arg != nil && arg.TypeID() != slotType.Params[param] {
if param < len(slotType.Params) && !typednil.IsNil(arg) && arg.TypeID() != slotType.Params[param] {
problems = append(problems, fmt.Sprintf("%s interface argument %d has type#%d, want type#%d", where, index, arg.TypeID(), slotType.Params[param]))
}
}
Expand Down Expand Up @@ -491,7 +494,7 @@ func validateStructLiteral(types *ir.TypeTable, value *StructLit, where string)
}
for index, field := range value.Fields {
problems = append(problems, validateValueRef(types, field, fmt.Sprintf("%s field %d", where, index))...)
if index < len(structure.Fields) && field != nil && field.TypeID() != structure.Fields[index].Type {
if index < len(structure.Fields) && !typednil.IsNil(field) && field.TypeID() != structure.Fields[index].Type {
problems = append(problems, fmt.Sprintf("%s field %d has type#%d, want type#%d", where, index, field.TypeID(), structure.Fields[index].Type))
}
}
Expand All @@ -509,7 +512,7 @@ func validateArrayLiteral(types *ir.TypeTable, value *ArrayLit, where string) []
}
for index, element := range value.Values {
problems = append(problems, validateValueRef(types, element, fmt.Sprintf("%s element %d", where, index))...)
if element != nil && element.TypeID() != array.Elem {
if !typednil.IsNil(element) && element.TypeID() != array.Elem {
problems = append(problems, fmt.Sprintf("%s element %d has type#%d, want type#%d", where, index, element.TypeID(), array.Elem))
}
}
Expand All @@ -529,21 +532,24 @@ func validateVariantMake(types *ir.TypeTable, value *VariantMake, where string)
return []string{fmt.Sprintf("%s constructs invalid case %d of type#%d", where, value.Case, value.Type)}
}
if variantCase.Payload == ir.InvalidType {
if value.Payload != nil {
return []string{fmt.Sprintf("%s gives payload to payloadless case %d", where, value.Case)}
if value.Payload == nil {
return nil
}
return nil
if typednil.IsNil(value.Payload) {
return []string{where + " variant payload is nil"}
}
return []string{fmt.Sprintf("%s gives payload to payloadless case %d", where, value.Case)}
}
problems := validateValueRef(types, value.Payload, where+" variant payload")
if value.Payload != nil && value.Payload.TypeID() != variantCase.Payload {
if !typednil.IsNil(value.Payload) && value.Payload.TypeID() != variantCase.Payload {
problems = append(problems, fmt.Sprintf("%s payload has type#%d, want type#%d", where, value.Payload.TypeID(), variantCase.Payload))
}
return problems
}

func validateVariantIs(types *ir.TypeTable, value *VariantIs, where string) []string {
problems := validateValueRef(types, value.Value, where+" variant value")
if types == nil || value.Value == nil {
if types == nil || typednil.IsNil(value.Value) {
return problems
}
variant, ok := types.Type(value.Value.TypeID())
Expand All @@ -565,7 +571,7 @@ func validatePlace(types *ir.TypeTable, place *Place, where string) []string {
}
problems := validateValueRef(types, place.Root, where+" root")
problems = append(problems, validateKnownType(types, place.Type, where+" result type")...)
if types == nil || place.Root == nil {
if types == nil || typednil.IsNil(place.Root) {
return problems
}
current := place.Root.TypeID()
Expand Down
Loading
Loading