Skip to content
This repository was archived by the owner on Jun 19, 2026. It is now read-only.
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
81 changes: 64 additions & 17 deletions internal/analysis/semantics/ownership/ownership.go
Original file line number Diff line number Diff line change
Expand Up @@ -618,7 +618,7 @@ func (a *ownershipAnalyzer) checkDirectValueBorrowConflict(scope *valueScope, va
return
}
if a.hasActiveMutableBorrowOf(scope, root) {
a.reportBorrowConflict(value.Loc(), root, "cannot use value while a mutable borrow is live")
a.reportBorrowConflict(scope, value.Loc(), root, "cannot use value while a mutable borrow is live")
}
}

Expand Down Expand Up @@ -687,12 +687,12 @@ func (a *ownershipAnalyzer) checkAddrOfValue(scope *valueScope, value *mir.AddrO
}
if value.Mutable {
if owner.frozen > 0 {
a.reportBorrowConflict(value.Loc(), root.localID, "cannot create mutable borrow while another borrow is live")
a.reportBorrowConflict(scope, value.Loc(), root.localID, "cannot create mutable borrow while another borrow is live")
}
return
}
if owner.mutBorrow {
a.reportBorrowConflict(value.Loc(), root.localID, "cannot create immutable borrow while a mutable borrow is live")
a.reportBorrowConflict(scope, value.Loc(), root.localID, "cannot create immutable borrow while a mutable borrow is live")
}
}

Expand Down Expand Up @@ -890,7 +890,7 @@ func (a *ownershipAnalyzer) consumeLocalPath(scope *valueScope, root int, path s
return
}
if info.frozen > 0 {
a.reportBorrowConflict(loc, root, "cannot move a value while a borrow is live")
a.reportBorrowConflict(scope, loc, root, "cannot move a value while a borrow is live")
return
}
if path == "" && len(info.movedSubs) > 0 {
Expand Down Expand Up @@ -1001,17 +1001,22 @@ func (a *ownershipAnalyzer) bindBorrowValue(scope *valueScope, slot *valueInfo,
}
if info.mutable {
if owner.frozen > 0 {
a.reportBorrowConflict(info.loc, info.owner.localID, "cannot create mutable borrow while another borrow is live")
a.reportBorrowConflict(scope, info.loc, info.owner.localID, "cannot create mutable borrow while another borrow is live")
return
}
} else if owner.mutBorrow {
a.reportBorrowConflict(info.loc, info.owner.localID, "cannot create immutable borrow while a mutable borrow is live")
a.reportBorrowConflict(scope, info.loc, info.owner.localID, "cannot create immutable borrow while a mutable borrow is live")
return
}
slot.borrowOf = info.owner.localID
slot.borrowMut = info.mutable
slot.borrowLoc = info.loc
owner.frozen++
if owner.activeBorrowLoc.Start == nil {
if loc, ok := normalizeBorrowLabelLocation(info.loc, value.Loc()); ok {
owner.activeBorrowLoc = loc
}
}
if info.mutable {
owner.mutBorrow = true
}
Expand All @@ -1023,15 +1028,35 @@ func (a *ownershipAnalyzer) releaseBorrowValue(scope *valueScope, slot *valueInf
}
if owner, ok := scope.Lookup(slot.borrowOf); ok && owner != nil && owner.frozen > 0 {
owner.frozen--
if slot.borrowMut && owner.frozen == 0 {
if owner.frozen == 0 {
owner.mutBorrow = false
owner.activeBorrowLoc = source.Location{}
} else if owner.activeBorrowLoc == slot.borrowLoc {
refreshOwnerActiveBorrowLoc(scope, slot.borrowOf, owner)
}
}
slot.borrowOf = -1
slot.borrowMut = false
slot.borrowLoc = source.Location{}
}

func refreshOwnerActiveBorrowLoc(scope *valueScope, ownerID int, owner *valueInfo) {
if scope == nil || owner == nil {
return
}
fallback := owner.activeBorrowLoc
owner.activeBorrowLoc = source.Location{}
for _, candidate := range scope.values {
if candidate == nil || candidate.borrowOf != ownerID {
continue
}
if loc, ok := normalizeBorrowLabelLocation(candidate.borrowLoc, fallback); ok {
owner.activeBorrowLoc = loc
return
}
}
}

func (a *ownershipAnalyzer) rebindBorrowAssignment(scope *valueScope, left mir.Place, right mir.Value) {
if scope == nil {
return
Expand All @@ -1057,7 +1082,7 @@ func (a *ownershipAnalyzer) checkAssignmentTarget(scope *valueScope, left mir.Pl
return
}
if info.frozen > 0 {
a.reportBorrowConflict(left.Loc(), root, "this value is currently borrowed")
a.reportBorrowConflict(scope, left.Loc(), root, "this value is currently borrowed")
}
if path == "" {
a.releaseBorrowValue(scope, info)
Expand Down Expand Up @@ -1210,7 +1235,7 @@ func (a *ownershipAnalyzer) requireActivePath(scope *valueScope, root int, path
return
}
if info.mutBorrow || a.hasActiveMutableBorrowOf(scope, root) {
a.reportBorrowConflict(loc, root, "cannot use value while a mutable borrow is live")
a.reportBorrowConflict(scope, loc, root, "cannot use value while a mutable borrow is live")
return
}
if path == "" {
Expand Down Expand Up @@ -1376,8 +1401,8 @@ func (a *ownershipAnalyzer) reportBorrowEscapeIfNeeded(scope *valueScope, value
diag := diagnostics.NewError(message).
WithCode(diagnostics.ErrBorrowEscape).
WithPrimaryLabel(&loc, "this borrow escapes its allowed scope")
if info.loc.Start != nil {
diag.WithSecondaryLabel(&info.loc, "borrow created here")
if borrowLoc, ok := normalizeBorrowLabelLocation(info.loc, loc); ok {
diag.WithSecondaryLabel(&borrowLoc, "borrow created here")
}
a.addDiagnostic(diag)
}
Expand Down Expand Up @@ -1419,16 +1444,38 @@ func (a *ownershipAnalyzer) borrowValueInfo(scope *valueScope, value mir.Value)
return borrowInfo{}, false
}

func (a *ownershipAnalyzer) reportBorrowConflict(loc source.Location, localID int, message string) {
func (a *ownershipAnalyzer) reportBorrowConflict(scope *valueScope, loc source.Location, localID int, message string) {
name := a.localName(localID)
if name == "" {
name = fmt.Sprintf("local#%d", localID)
}
a.addDiagnostic(
diagnostics.NewError(fmt.Sprintf("cannot use %q here", name)).
WithCode(diagnostics.ErrBorrowConflict).
WithPrimaryLabel(&loc, message),
)
diag := diagnostics.NewError(fmt.Sprintf("cannot use %q here", name)).
WithCode(diagnostics.ErrBorrowConflict).
WithPrimaryLabel(&loc, message)
if scope != nil {
if owner, ok := scope.Lookup(localID); ok && owner != nil {
if borrowLoc, ok := normalizeBorrowLabelLocation(owner.activeBorrowLoc, loc); ok {
diag.WithSecondaryLabel(&borrowLoc, "borrow created here")
}
}
}
a.addDiagnostic(diag)
}

func normalizeBorrowLabelLocation(origin, fallback source.Location) (source.Location, bool) {
if origin.Start == nil {
return source.Location{}, false
}
normalized := origin
if normalized.Filename == nil || (normalized.Filename != nil && *normalized.Filename == "") {
if fallback.Filename != nil && *fallback.Filename != "" {
normalized.Filename = fallback.Filename
}
}
if normalized.Filename == nil || *normalized.Filename == "" {
return source.Location{}, false
}
return normalized, true
}

func (a *ownershipAnalyzer) fieldSelectorName(value *mir.FieldValue) string {
Expand Down
37 changes: 37 additions & 0 deletions internal/analysis/semantics/ownership/ownership_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,43 @@ fn run() -> void {
t.Fatalf("expected %s diagnostic, got %#v", diagnostics.ErrBorrowConflict, result.Diagnostics.Diagnostics())
}

func TestOwnershipPhaseBorrowConflictShowsBorrowOriginLabel(t *testing.T) {
root := t.TempDir()
mustWriteOwnership(t, filepath.Join(root, "main.fer"), `
fn run() -> void {
let mut x = 10
let y = &mut x
print(x)
print(*y)
}
`)

result := compiler.New(root, ".fer", diagnostics.NewDiagnosticBag("")).ParseEntry(filepath.Join(root, "main.fer"))
if result.Entry == nil || result.Entry.Phase < phase.PhaseOwnershipAnalyzed {
t.Fatalf("expected ownership analyzed phase, got %#v", result.Entry)
}
for _, diag := range result.Diagnostics.Diagnostics() {
if diag.Code != diagnostics.ErrBorrowConflict {
continue
}
if len(diag.Labels) < 2 {
t.Fatalf("expected borrow conflict to include secondary borrow-origin label, got %#v", diag)
}
found := false
for _, label := range diag.Labels {
if label.Message == "borrow created here" && label.Location != nil && label.Location.Filename != nil {
found = true
break
}
}
if !found {
t.Fatalf("expected borrow-origin secondary label with file location, got %#v", diag)
}
return
}
t.Fatalf("expected %s diagnostic, got %#v", diagnostics.ErrBorrowConflict, result.Diagnostics.Diagnostics())
}

func TestOwnershipPhaseAllowsMultipleImmutableBorrows(t *testing.T) {
root := t.TempDir()
mustWriteOwnership(t, filepath.Join(root, "main.fer"), `
Expand Down
38 changes: 25 additions & 13 deletions internal/analysis/semantics/ownership/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,20 @@ import (
)

type valueInfo struct {
typ typeinfo.Type
concrete typeinfo.Type
mutable bool
constant bool
moved bool
moveLoc source.Location
movedPath string
movedSubs map[string]source.Location
frozen int
mutBorrow bool
borrowOf int
borrowMut bool
borrowLoc source.Location
typ typeinfo.Type
concrete typeinfo.Type
mutable bool
constant bool
moved bool
moveLoc source.Location
movedPath string
movedSubs map[string]source.Location
frozen int
mutBorrow bool
activeBorrowLoc source.Location
borrowOf int
borrowMut bool
borrowLoc source.Location
}

type valueScope struct {
Expand Down Expand Up @@ -94,6 +95,7 @@ func (s *valueScope) TrimToLiveOut(live cfg.LocalSet) {
info.movedSubs = nil
info.frozen = 0
info.mutBorrow = false
info.activeBorrowLoc = source.Location{}
info.borrowOf = -1
info.borrowMut = false
info.borrowLoc = source.Location{}
Expand Down Expand Up @@ -139,6 +141,7 @@ func equalValueInfo(a, b *valueInfo) bool {
equalMovedSubs(a.movedSubs, b.movedSubs) &&
a.frozen == b.frozen &&
a.mutBorrow == b.mutBorrow &&
a.activeBorrowLoc == b.activeBorrowLoc &&
a.borrowOf == b.borrowOf &&
a.borrowMut == b.borrowMut &&
a.borrowLoc == b.borrowLoc
Expand Down Expand Up @@ -179,6 +182,15 @@ func mergeValueInfo(a, b *valueInfo) *valueInfo {
out.frozen = b.frozen
}
out.mutBorrow = a.mutBorrow || b.mutBorrow
if out.frozen > 0 {
if a.activeBorrowLoc.Start != nil {
out.activeBorrowLoc = a.activeBorrowLoc
} else {
out.activeBorrowLoc = b.activeBorrowLoc
}
} else {
out.activeBorrowLoc = source.Location{}
}
if a.borrowOf == b.borrowOf {
out.borrowOf = a.borrowOf
out.borrowMut = a.borrowMut || b.borrowMut
Expand Down
10 changes: 10 additions & 0 deletions tests/repro/borrow_conflict_active_borrow_label.fer
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
fn run() -> void {
let mut x = 10
let y = &mut x
print(x)
print(*y)
}

fn main() -> void {
run()
}
Loading