From 659e08b1110dbd6046a518be5303e626ad706b9d Mon Sep 17 00:00:00 2001 From: Fuad Hasan Date: Mon, 20 Apr 2026 23:57:49 +0600 Subject: [PATCH 1/4] Clean up LLVM backend state and MIR walking --- internal/backend/llvm/llvm.go | 314 ++++++---------------------- internal/backend/llvm/lower_test.go | 50 +++++ internal/ir/mir/walk.go | 178 ++++++++++++++++ internal/ir/mir/walk_test.go | 51 +++++ 4 files changed, 347 insertions(+), 246 deletions(-) create mode 100644 internal/ir/mir/walk.go create mode 100644 internal/ir/mir/walk_test.go diff --git a/internal/backend/llvm/llvm.go b/internal/backend/llvm/llvm.go index 975572b7..b0b95897 100644 --- a/internal/backend/llvm/llvm.go +++ b/internal/backend/llvm/llvm.go @@ -27,25 +27,29 @@ type moduleState struct { layout *layout.Module layouts map[string]*layout.Module modules map[string]*mir.Module - fn *mir.Function functions map[string]struct{} globals map[string]struct{} modulePrefix string - aggLocals map[int]*aggregateLocal - aggParams map[int]struct{} - scalarLocals map[int]*scalarAllocaLocal // mutable scalar locals mapped to alloca ptrs - nextTemp int nextPrivateGlobal int // counter for compiler-emitted private globals deferredB *strings.Builder // deferred global definitions (e.g. string literals used in functions) - pendingLines []string // extra load instructions to flush before each emitted line interfaceVTables map[becommon.InterfaceVTableKey]string interfaceWrappers map[becommon.InterfaceWrapperKey]struct{} runtimeTypes map[string]string - tempValues map[int]mir.Value debug *debugState // nil if debug info is disabled - fnScopeID int // DISubprogram metadata ID for the current function - debugLocalVarIDs map[int]int // local ID -> DILocalVariable metadata ID (for dbg.value updates) inGlobalInit bool + functionState +} + +type functionState struct { + fn *mir.Function + aggLocals map[int]*aggregateLocal + aggParams map[int]struct{} // aggParams are params that are aggregate types. For example, structs or arrays. + scalarLocals map[int]*scalarAllocaLocal // mutable scalar locals mapped to alloca ptrs + nextTemp int + pendingLines []string // extra load instructions to flush before each emitted line + tempValues map[int]mir.Value + fnScopeID int // DISubprogram metadata ID for the current function + debugLocalVarIDs map[int]int // local ID -> DILocalVariable metadata ID (for dbg.value updates) } // debugState accumulates LLVM debug-info metadata nodes (DWARF) while @@ -509,38 +513,11 @@ func LowerProgram(units []*backend.Unit, includeDebug bool) (string, error) { } // We need a temporary state only to resolve return types. tmpState := newProgramModuleState(unit, allLayouts, dbg, shared) - callDecls, err := collectExternCallDecls(tmpState, unit.Module, seenExterns) + externDecls, err := collectModuleExternDecls(tmpState, unit, seenExterns) if err != nil { return "", fmt.Errorf("collect extern call declarations [%s]: %w", unit.Module.ImportPath, err) } - declLines = append(declLines, callDecls...) - for _, fn := range externFunctionsForUnit(unit) { - if fn == nil || !fn.IsExtern || fn.LinkName == "" || isLoweredBuiltinExtern(fn) { - continue - } - sym := becommon.SanitizeLinkName(fn.LinkName) - if _, seen := seenExterns[sym]; seen { - continue - } - seenExterns[sym] = struct{}{} - // Determine return type string. - var retStr string - if isAggregateType(tmpState, fn.Result) { - if tn, err := llvmABITypeName(tmpState, fn.Result); err == nil { - retStr = tn - } else { - retStr = "ptr" - } - } else { - if rt, err := llvmBaseType(fn.Result); err == nil { - retStr = rt - } else { - retStr = "ptr" - } - } - // Use variadic signature so we don't need param type info. - declLines = append(declLines, fmt.Sprintf("declare %s @%s(...)", retStr, sym)) - } + declLines = append(declLines, externDecls...) } // Always declare Ferret runtime functions used by compiler-emitted code. @@ -585,20 +562,9 @@ func LowerProgram(units []*backend.Unit, includeDebug bool) (string, error) { if fn == nil || fn.IsExtern { continue } - // Capture function IR into a temp buffer so that any string constants - // accumulated in deferredB during lowering can be emitted BEFORE the - // function body (LLVM IR requires globals to be defined before use). - var fnB strings.Builder - if err := emitFunction(&fnB, state, fn); err != nil { + if err := emitFunctionWithDeferred(&b, state, fn, true); err != nil { return "", fmt.Errorf("lower program function %s [%s]: %w", fn.Name, unit.Module.ImportPath, err) } - b.WriteByte('\n') - if state.deferredB.Len() > 0 { - b.WriteString(state.deferredB.String()) - state.deferredB.Reset() - b.WriteByte('\n') - } - b.WriteString(fnB.String()) } } if len(ctorEntries) > 0 { @@ -798,180 +764,47 @@ func collectExternCallDecls(state *moduleState, mod *mir.Module, seenExterns map decls = append(decls, fmt.Sprintf("declare %s @%s(%s)", ret, sym, strings.Join(params, ", "))) return nil } - var walkValue func(value mir.Value) error - var walkPlace func(place mir.Place) error - var walkInstr func(instr mir.Instr) error - var walkTerm func(term mir.Terminator) error - walkValue = func(value mir.Value) error { - switch v := value.(type) { - case nil, *mir.NameValue, *mir.LocalValue, *mir.NumberValue, *mir.BoolValue, *mir.StringValue, *mir.NoneValue: - return nil - case *mir.UnaryValue: - return walkValue(v.Right) - case *mir.BinaryValue: - if err := walkValue(v.Left); err != nil { - return err - } - return walkValue(v.Right) - case *mir.PostfixValue: - return walkValue(v.Left) - case *mir.AddrOfValue: - return walkValue(v.Source) - case *mir.LoadValue: - return walkValue(v.Pointer) - case *mir.CallValue: - if _, _, ok := becommon.BuiltinMapCall(v); ok { - if err := walkValue(v.Callee); err != nil { - return err - } - for _, arg := range v.Args { - if err := walkValue(arg); err != nil { - return err - } - } - return nil - } - if callee, ok := v.Callee.(*mir.NameValue); ok && callee.LinkName != "" { - if err := addCall(becommon.SanitizeLinkName(callee.LinkName), v); err != nil { - return err - } - } - if err := walkValue(v.Callee); err != nil { - return err - } - for _, arg := range v.Args { - if err := walkValue(arg); err != nil { - return err - } - } - return nil - case *mir.FieldLoadValue: - return walkValue(v.Base) - case *mir.FieldValue: - return walkValue(v.Base) - case *mir.CastValue: - return walkValue(v.Left) - case *mir.TypeTestValue: - return walkValue(v.Left) - case *mir.CompositeValue: - for _, item := range v.Items { - if err := walkValue(item.Value); err != nil { - return err - } - } - return nil - case *mir.InterfaceValue: - return walkValue(v.Value) - case *mir.IndexValue: - if err := walkValue(v.Base); err != nil { - return err - } - return walkValue(v.Index) - default: + if err := mir.WalkModuleValues(mod, func(value mir.Value) error { + call, ok := value.(*mir.CallValue) + if !ok { return nil } - } - walkPlace = func(place mir.Place) error { - switch p := place.(type) { - case nil, *mir.LocalPlace: - return nil - case *mir.FieldPlace: - return walkPlace(p.Base) - case *mir.IndexPlace: - if err := walkPlace(p.Base); err != nil { - return err - } - return walkValue(p.Index) - case *mir.DerefPlace: - return walkValue(p.Pointer) - default: + if _, _, ok := becommon.BuiltinMapCall(call); ok { return nil } - } - walkInstr = func(instr mir.Instr) error { - switch i := instr.(type) { - case nil: - return nil - case *mir.AssignInstr: - return walkValue(i.Value) - case *mir.ComputeInstr: - return walkValue(i.Value) - case *mir.StoreInstr: - if err := walkPlace(i.Target); err != nil { - return err - } - return walkValue(i.Value) - case *mir.StoreFieldInstr: - if err := walkValue(i.Base); err != nil { - return err - } - return walkValue(i.Value) - case *mir.EvalInstr: - return walkValue(i.Value) - case *mir.BindInstr: - return walkValue(i.Value) - case *mir.LockInstr: - return walkValue(i.Value) - case *mir.DeferInstr: - for _, child := range i.Body { - if err := walkInstr(child); err != nil { - return err - } - } - return nil - default: + callee, ok := call.Callee.(*mir.NameValue) + if !ok || callee.LinkName == "" { return nil } + return addCall(becommon.SanitizeLinkName(callee.LinkName), call) + }); err != nil { + return nil, err } - walkTerm = func(term mir.Terminator) error { - switch t := term.(type) { - case nil: - return nil - case *mir.BranchTerm: - return walkValue(t.Cond) - case *mir.SwitchTerm: - if err := walkValue(t.Value); err != nil { - return err - } - for _, kase := range t.Cases { - if err := walkValue(kase.Expr); err != nil { - return err - } - } - return nil - case *mir.ReturnTerm: - return walkValue(t.Value) - case *mir.PanicTerm: - return walkValue(t.Value) - default: - return nil - } + return decls, nil +} + +func collectModuleExternDecls(state *moduleState, unit *backend.Unit, seenExterns map[string]struct{}) ([]string, error) { + if state == nil || unit == nil || unit.Module == nil { + return nil, nil + } + decls, err := collectExternCallDecls(state, unit.Module, seenExterns) + if err != nil { + return nil, err } - for _, global := range mod.Globals { - if global == nil { + for _, fn := range externFunctionsForUnit(unit) { + if fn == nil || !fn.IsExtern || fn.LinkName == "" || isLoweredBuiltinExtern(fn) { continue } - if err := walkValue(global.Init); err != nil { - return nil, err - } - } - for _, fn := range mod.Functions { - if fn == nil { + sym := becommon.SanitizeLinkName(fn.LinkName) + if _, seen := seenExterns[sym]; seen { continue } - for _, block := range fn.Blocks { - if block == nil { - continue - } - for _, instr := range block.Instructions { - if err := walkInstr(instr); err != nil { - return nil, err - } - } - if err := walkTerm(block.Terminator); err != nil { - return nil, err - } + decl, err := llvmExternDecl(state, fn) + if err != nil { + return nil, err } + seenExterns[sym] = struct{}{} + decls = append(decls, decl) } return decls, nil } @@ -1018,7 +851,6 @@ func newModuleState(unit *backend.Unit, allLayouts map[string]*layout.Module) *m interfaceVTables: shared.VTables, interfaceWrappers: shared.Wrappers, runtimeTypes: shared.RuntimeTypes, - tempValues: make(map[int]mir.Value), } } modulePrefix, functions, globals := becommon.BuildModuleSymbolTables(unit.Module) @@ -1039,7 +871,6 @@ func newModuleState(unit *backend.Unit, allLayouts map[string]*layout.Module) *m interfaceVTables: shared.VTables, interfaceWrappers: shared.Wrappers, runtimeTypes: shared.RuntimeTypes, - tempValues: make(map[int]mir.Value), } return state } @@ -1073,27 +904,11 @@ func (*lowerer) LowerModule(unit *backend.Unit) (*backend.Artifact, error) { b.WriteByte('\n') } seenExterns := seedExternDecls() - callDecls, err := collectExternCallDecls(state, unit.Module, seenExterns) + externDecls, err := collectModuleExternDecls(state, unit, seenExterns) if err != nil { return nil, err } - for _, decl := range callDecls { - b.WriteString(decl) - b.WriteByte('\n') - } - for _, fn := range externFunctionsForUnit(unit) { - if fn == nil || !fn.IsExtern || fn.LinkName == "" || isLoweredBuiltinExtern(fn) { - continue - } - sym := becommon.SanitizeLinkName(fn.LinkName) - if _, ok := seenExterns[sym]; ok { - continue - } - decl, err := llvmExternDecl(state, fn) - if err != nil { - return nil, err - } - seenExterns[sym] = struct{}{} + for _, decl := range externDecls { b.WriteString(decl) b.WriteByte('\n') } @@ -1117,22 +932,9 @@ func (*lowerer) LowerModule(unit *backend.Unit) (*backend.Artifact, error) { if fn == nil || fn.IsExtern { continue } - if written > 0 { - b.WriteByte('\n') - } - // Capture function IR into a temp buffer so that any string constants - // accumulated in deferredB during lowering can be emitted BEFORE the - // function body (LLVM IR requires globals to be defined before use). - var fnB strings.Builder - if err := emitFunction(&fnB, state, fn); err != nil { + if err := emitFunctionWithDeferred(&b, state, fn, written > 0); err != nil { return nil, err } - if state.deferredB.Len() > 0 { - b.WriteString(state.deferredB.String()) - state.deferredB.Reset() - b.WriteByte('\n') - } - b.WriteString(fnB.String()) written++ } if initName != "" { @@ -1164,6 +966,25 @@ func emitGlobalCtorTable(b *strings.Builder, entries []globalCtorEntry) { fmt.Fprintf(b, "@llvm.global_ctors = appending global [%d x { i32, ptr, ptr }] [%s]\n", len(parts), strings.Join(parts, ", ")) } +func emitFunctionWithDeferred(b *strings.Builder, state *moduleState, fn *mir.Function, leadingBlank bool) error { + // Capture function IR first so compiler-emitted globals collected in + // deferredB can be emitted before the function body, as LLVM requires. + var fnB strings.Builder + if err := emitFunction(&fnB, state, fn); err != nil { + return err + } + if leadingBlank { + b.WriteByte('\n') + } + if state.deferredB.Len() > 0 { + b.WriteString(state.deferredB.String()) + state.deferredB.Reset() + b.WriteByte('\n') + } + b.WriteString(fnB.String()) + return nil +} + // --------------------------------------------------------------------------- // Type declarations // --------------------------------------------------------------------------- @@ -1712,7 +1533,7 @@ func emitFunction(b *strings.Builder, state *moduleState, fn *mir.Function) erro // Attach DWARF subprogram metadata if debug state is active. dbgSuffix := "" - if state.debug != nil && *fn.Location.Filename != "" { + if state.debug != nil && fn.Location.Filename != nil && *fn.Location.Filename != "" { fileID := state.debug.getFile(*fn.Location.Filename) // Re-use the last added CU or add one for this file. cuID := -1 @@ -5499,6 +5320,7 @@ func prepareFunctionState(state *moduleState, fn *mir.Function) error { state.debugLocalVarIDs = make(map[int]int) state.pendingLines = nil state.nextTemp = 0 + state.fnScopeID = -1 for _, param := range fn.Params { if param == nil { diff --git a/internal/backend/llvm/lower_test.go b/internal/backend/llvm/lower_test.go index e42eab04..77ffba45 100644 --- a/internal/backend/llvm/lower_test.go +++ b/internal/backend/llvm/lower_test.go @@ -15,11 +15,61 @@ import ( "compiler/internal/core/abi" "compiler/internal/core/context" "compiler/internal/core/diagnostics" + "compiler/internal/core/source" compiler "compiler/internal/driver" "compiler/internal/ir/mir" "compiler/internal/testutils" ) +func TestLowerProgramDebugScopeDoesNotLeakToFunctionWithoutLocation(t *testing.T) { + loc := source.NewLocation("/tmp/ferret-debug-test.fer", source.Position{Line: 1, Column: 1}, source.Position{Line: 1, Column: 1}) + emptyFile := "" + voidType := &typeinfo.BuiltinType{Name: "void"} + mod := &mir.Module{ + Key: "main", + ImportPath: "main", + FilePath: "/tmp/ferret-debug-test.fer", + Functions: []*mir.Function{ + { + Name: "first", + Result: voidType, + EntryID: 0, + Blocks: []*mir.Block{{ID: 0, Terminator: &mir.ReturnTerm{}}}, + Location: loc, + }, + { + Name: "second", + Result: voidType, + EntryID: 0, + Blocks: []*mir.Block{{ID: 0, Terminator: &mir.ReturnTerm{}}}, + Location: source.Location{Filename: &emptyFile}, + }, + }, + } + layoutMod := &layout.Module{Key: "main"} + unit := &backend.Unit{ + Module: mod, + Layout: layoutMod, + Layouts: map[string]*layout.Module{"main": layoutMod}, + } + text, err := llvmbackend.LowerProgram([]*backend.Unit{unit}, true) + if err != nil { + t.Fatalf("LowerProgram: %v", err) + } + secondStart := strings.Index(text, "define void @main__second()") + if secondStart < 0 { + t.Fatalf("missing second function:\n%s", text) + } + secondEnd := strings.Index(text[secondStart:], "\n}") + if secondEnd < 0 { + t.Fatalf("malformed second function:\n%s", text[secondStart:]) + } + secondBody := text[secondStart : secondStart+secondEnd] + if strings.Contains(secondBody, "!dbg") { + t.Fatalf("debug metadata leaked into function without location:\n%s", secondBody) + } +} + func TestLowerInterfaceDispatchToLLVM(t *testing.T) { root := t.TempDir() mustWrite(t, filepath.Join(root, "main.fer"), ` diff --git a/internal/ir/mir/walk.go b/internal/ir/mir/walk.go new file mode 100644 index 00000000..94b7584c --- /dev/null +++ b/internal/ir/mir/walk.go @@ -0,0 +1,178 @@ +package mir + +// WalkModuleValues visits every value reachable from globals and function bodies. +func WalkModuleValues(mod *Module, visit func(Value) error) error { + if mod == nil { + return nil + } + for _, global := range mod.Globals { + if global == nil { + continue + } + if err := WalkValue(global.Init, visit); err != nil { + return err + } + } + for _, fn := range mod.Functions { + if err := WalkFunctionValues(fn, visit); err != nil { + return err + } + } + return nil +} + +func WalkFunctionValues(fn *Function, visit func(Value) error) error { + if fn == nil { + return nil + } + for _, block := range fn.Blocks { + if block == nil { + continue + } + for _, instr := range block.Instructions { + if err := WalkInstrValues(instr, visit); err != nil { + return err + } + } + if err := WalkTerminatorValues(block.Terminator, visit); err != nil { + return err + } + } + return nil +} + +func WalkInstrValues(instr Instr, visit func(Value) error) error { + switch i := instr.(type) { + case nil, *UnsafeInstr: + return nil + case *BindInstr: + return WalkValue(i.Value, visit) + case *AssignInstr: + return WalkValue(i.Value, visit) + case *ComputeInstr: + return WalkValue(i.Value, visit) + case *StoreInstr: + if err := WalkPlaceValues(i.Target, visit); err != nil { + return err + } + return WalkValue(i.Value, visit) + case *StoreFieldInstr: + if err := WalkValue(i.Base, visit); err != nil { + return err + } + return WalkValue(i.Value, visit) + case *EvalInstr: + return WalkValue(i.Value, visit) + case *LockInstr: + return WalkValue(i.Value, visit) + case *DeferInstr: + for _, child := range i.Body { + if err := WalkInstrValues(child, visit); err != nil { + return err + } + } + } + return nil +} + +func WalkTerminatorValues(term Terminator, visit func(Value) error) error { + switch t := term.(type) { + case nil, *JumpTerm, *ExitTerm: + return nil + case *BranchTerm: + return WalkValue(t.Cond, visit) + case *SwitchTerm: + if err := WalkValue(t.Value, visit); err != nil { + return err + } + for _, kase := range t.Cases { + if err := WalkValue(kase.Expr, visit); err != nil { + return err + } + } + case *ReturnTerm: + return WalkValue(t.Value, visit) + case *PanicTerm: + return WalkValue(t.Value, visit) + } + return nil +} + +func WalkPlaceValues(place Place, visit func(Value) error) error { + switch p := place.(type) { + case nil, *LocalPlace: + return nil + case *FieldPlace: + return WalkPlaceValues(p.Base, visit) + case *IndexPlace: + if err := WalkPlaceValues(p.Base, visit); err != nil { + return err + } + return WalkValue(p.Index, visit) + case *DerefPlace: + return WalkValue(p.Pointer, visit) + } + return nil +} + +func WalkValue(value Value, visit func(Value) error) error { + if value == nil { + return nil + } + if visit != nil { + if err := visit(value); err != nil { + return err + } + } + switch v := value.(type) { + case *NameValue, *LocalValue, *TempValue, *NumberValue, *BoolValue, *StringValue, *NoneValue: + return nil + case *UnaryValue: + return WalkValue(v.Right, visit) + case *AddrOfValue: + return WalkValue(v.Source, visit) + case *LoadValue: + return WalkValue(v.Pointer, visit) + case *BinaryValue: + if err := WalkValue(v.Left, visit); err != nil { + return err + } + return WalkValue(v.Right, visit) + case *PostfixValue: + return WalkValue(v.Left, visit) + case *CallValue: + if err := WalkValue(v.Callee, visit); err != nil { + return err + } + for _, arg := range v.Args { + if err := WalkValue(arg, visit); err != nil { + return err + } + } + case *FieldLoadValue: + return WalkValue(v.Base, visit) + case *FieldValue: + return WalkValue(v.Base, visit) + case *CastValue: + return WalkValue(v.Left, visit) + case *TypeTestValue: + return WalkValue(v.Left, visit) + case *CompositeValue: + for _, item := range v.Items { + if err := WalkValue(item.Key, visit); err != nil { + return err + } + if err := WalkValue(item.Value, visit); err != nil { + return err + } + } + case *InterfaceValue: + return WalkValue(v.Value, visit) + case *IndexValue: + if err := WalkValue(v.Base, visit); err != nil { + return err + } + return WalkValue(v.Index, visit) + } + return nil +} diff --git a/internal/ir/mir/walk_test.go b/internal/ir/mir/walk_test.go new file mode 100644 index 00000000..b28c52eb --- /dev/null +++ b/internal/ir/mir/walk_test.go @@ -0,0 +1,51 @@ +package mir + +import ( + "reflect" + "testing" + + "compiler/internal/analysis/semantics/typeinfo" +) + +func TestWalkModuleValuesVisitsNestedCompositeKeys(t *testing.T) { + strType := &typeinfo.StringType{} + i32Type := &typeinfo.BuiltinType{Name: "i32"} + keyCall := &CallValue{ + baseValue: baseValue{ExprType: strType}, + Callee: &NameValue{baseValue: baseValue{ExprType: &typeinfo.FuncType{Result: strType}}, Path: []string{"key"}}, + } + valueCall := &CallValue{ + baseValue: baseValue{ExprType: i32Type}, + Callee: &NameValue{baseValue: baseValue{ExprType: &typeinfo.FuncType{Result: i32Type}}, Path: []string{"value"}}, + } + mod := &Module{ + Globals: []*Global{ + { + Name: "items", + Init: &CompositeValue{ + Items: []CompositeItem{ + {Key: keyCall, Value: valueCall}, + }, + }, + }, + }, + } + var calls []string + if err := WalkModuleValues(mod, func(value Value) error { + call, ok := value.(*CallValue) + if !ok { + return nil + } + name, ok := call.Callee.(*NameValue) + if !ok || len(name.Path) == 0 { + return nil + } + calls = append(calls, name.Path[0]) + return nil + }); err != nil { + t.Fatalf("WalkModuleValues: %v", err) + } + if want := []string{"key", "value"}; !reflect.DeepEqual(calls, want) { + t.Fatalf("calls = %#v, want %#v", calls, want) + } +} From 42e13a54e6fe9771a059b0f8bba9b248f2c5496d Mon Sep 17 00:00:00 2001 From: Fuad Hasan Date: Tue, 21 Apr 2026 01:29:30 +0600 Subject: [PATCH 2/4] fix deref bug --- internal/backend/llvm/llvm.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/backend/llvm/llvm.go b/internal/backend/llvm/llvm.go index b0b95897..65f52436 100644 --- a/internal/backend/llvm/llvm.go +++ b/internal/backend/llvm/llvm.go @@ -1533,7 +1533,8 @@ func emitFunction(b *strings.Builder, state *moduleState, fn *mir.Function) erro // Attach DWARF subprogram metadata if debug state is active. dbgSuffix := "" - if state.debug != nil && fn.Location.Filename != nil && *fn.Location.Filename != "" { + filename := fn.Location.Filename + if state.debug != nil && filename != nil && *filename != "" { fileID := state.debug.getFile(*fn.Location.Filename) // Re-use the last added CU or add one for this file. cuID := -1 From 2f7c7ac615896c534c4a3b8bbaedc646dad43a75 Mon Sep 17 00:00:00 2001 From: Fuad Hasan Date: Tue, 21 Apr 2026 22:05:23 +0600 Subject: [PATCH 3/4] Fix LLVM debug decls for missing local locations --- internal/backend/llvm/llvm.go | 27 +++++++++-------- internal/backend/llvm/lower_test.go | 46 +++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 12 deletions(-) diff --git a/internal/backend/llvm/llvm.go b/internal/backend/llvm/llvm.go index 65f52436..c788c97b 100644 --- a/internal/backend/llvm/llvm.go +++ b/internal/backend/llvm/llvm.go @@ -1663,16 +1663,22 @@ func entryDebugDecls(state *moduleState) []string { locID := state.debug.addLocation(line, 1, state.fnScopeID) lines = append(lines, fmt.Sprintf("call void @llvm.dbg.declare(metadata ptr %s, metadata !%d, metadata !%d), !dbg !%d", ptrName, varID, exprID, locID)) } + localFilePath := func(filename *string) string { + if filename != nil && *filename != "" { + return *filename + } + if state.fn.Location.Filename != nil { + return *state.fn.Location.Filename + } + return "" + } for _, param := range state.fn.Params { if param == nil { continue } paramIDs[param.LocalID] = struct{}{} - filePath := param.Location.Filename - if *filePath == "" { - filePath = state.fn.Location.Filename - } + filePath := localFilePath(param.Location.Filename) line := 1 if param.Location.Start != nil { line = param.Location.Start.Line @@ -1680,12 +1686,12 @@ func entryDebugDecls(state *moduleState) []string { line = state.fn.Location.Start.Line } if _, ok := state.aggParams[param.LocalID]; ok { - emitDecl(param.LocalID, param.Name, param.Type, llvmLocalName(param.Name), *filePath, line, argIndex) + emitDecl(param.LocalID, param.Name, param.Type, llvmLocalName(param.Name), filePath, line, argIndex) argIndex++ continue } if sc, ok := state.scalarLocals[param.LocalID]; ok { - emitDecl(param.LocalID, param.Name, param.Type, sc.AllocaName, *filePath, line, argIndex) + emitDecl(param.LocalID, param.Name, param.Type, sc.AllocaName, filePath, line, argIndex) argIndex++ } } @@ -1697,10 +1703,7 @@ func entryDebugDecls(state *moduleState) []string { if _, isParam := paramIDs[local.ID]; isParam { continue } - filePath := local.Location.Filename - if *filePath == "" { - filePath = state.fn.Location.Filename - } + filePath := localFilePath(local.Location.Filename) line := 1 if local.Location.Start != nil { line = local.Location.Start.Line @@ -1708,14 +1711,14 @@ func entryDebugDecls(state *moduleState) []string { line = state.fn.Location.Start.Line } if sc, ok := state.scalarLocals[local.ID]; ok { - emitDecl(local.ID, local.Name, local.Type, sc.AllocaName, *filePath, line, 0) + emitDecl(local.ID, local.Name, local.Type, sc.AllocaName, filePath, line, 0) continue } if agg, ok := state.aggLocals[local.ID]; ok { if _, isParam := state.aggParams[local.ID]; isParam { continue } - emitDecl(local.ID, local.Name, local.Type, llvmLocalName(agg.PtrName), *filePath, line, 0) + emitDecl(local.ID, local.Name, local.Type, llvmLocalName(agg.PtrName), filePath, line, 0) } } diff --git a/internal/backend/llvm/lower_test.go b/internal/backend/llvm/lower_test.go index 77ffba45..c2242cd0 100644 --- a/internal/backend/llvm/lower_test.go +++ b/internal/backend/llvm/lower_test.go @@ -70,6 +70,52 @@ func TestLowerProgramDebugScopeDoesNotLeakToFunctionWithoutLocation(t *testing.T } } +func TestLowerProgramDebugDeclsUseFunctionFileForMissingLocalLocations(t *testing.T) { + loc := source.NewLocation("/tmp/ferret-debug-local.fer", source.Position{Line: 7, Column: 1}, source.Position{Line: 7, Column: 1}) + voidType := &typeinfo.BuiltinType{Name: "void"} + i32Type := &typeinfo.BuiltinType{Name: "i32"} + mod := &mir.Module{ + Key: "main", + ImportPath: "main", + FilePath: "/tmp/ferret-debug-local.fer", + Functions: []*mir.Function{ + { + Name: "debug_locals", + Result: voidType, + Params: []*mir.Param{ + {Name: "value", LocalID: 1, Type: i32Type, Location: source.Location{}}, + }, + Locals: []*mir.Local{ + {ID: 1, Name: "value", Type: i32Type, Mutable: true, Location: source.Location{}}, + {ID: 2, Name: "scratch", Type: i32Type, Mutable: true, Location: source.Location{}}, + }, + EntryID: 0, + Blocks: []*mir.Block{{ID: 0, Terminator: &mir.ReturnTerm{}}}, + Location: loc, + }, + }, + } + layoutMod := &layout.Module{Key: "main"} + unit := &backend.Unit{ + Module: mod, + Layout: layoutMod, + Layouts: map[string]*layout.Module{"main": layoutMod}, + } + text, err := llvmbackend.LowerProgram([]*backend.Unit{unit}, true) + if err != nil { + t.Fatalf("LowerProgram: %v", err) + } + for _, want := range []string{ + "call void @llvm.dbg.declare(metadata ptr %value_alloca", + "call void @llvm.dbg.declare(metadata ptr %scratch_alloca", + `!DIFile(filename: "ferret-debug-local.fer", directory: "/tmp")`, + } { + if !strings.Contains(text, want) { + t.Fatalf("expected %q in llvm output:\n%s", want, text) + } + } +} + func TestLowerInterfaceDispatchToLLVM(t *testing.T) { root := t.TempDir() mustWrite(t, filepath.Join(root, "main.fer"), ` From 0abff38d8d7ba4f8c60d5b19a43c0f6ab0de9aa8 Mon Sep 17 00:00:00 2001 From: Fuad Hasan Date: Wed, 22 Apr 2026 01:54:20 +0600 Subject: [PATCH 4/4] Reuse extern return fallback for declarations --- internal/backend/llvm/llvm.go | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/internal/backend/llvm/llvm.go b/internal/backend/llvm/llvm.go index c788c97b..bd45f664 100644 --- a/internal/backend/llvm/llvm.go +++ b/internal/backend/llvm/llvm.go @@ -679,22 +679,9 @@ func implicitExternSymbol(decl string) string { // llvmExternDecl builds a "declare" line for an extern function. func llvmExternDecl(state *moduleState, fn *mir.Function) (string, error) { sym := becommon.SanitizeLinkName(fn.LinkName) - - var retStr string - if fn.Result == nil || isVoidType(fn.Result) { - retStr = "void" - } else if isAggregateType(state, fn.Result) { - t, err := llvmABITypeName(state, fn.Result) - if err != nil { - return "", err - } - retStr = t - } else { - t, err := llvmBaseType(fn.Result) - if err != nil { - return "", err - } - retStr = t + retStr, err := llvmExternReturnType(state, fn.Result) + if err != nil { + return "", err } // Extern signatures may be unavailable or incomplete in some standalone