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
23 changes: 15 additions & 8 deletions compiler/air/lower.go
Original file line number Diff line number Diff line change
Expand Up @@ -437,8 +437,9 @@ func (l *lowerer) declareFunction(module ModuleID, def *checker.FunctionDef) (Fu
Return: returnType,
ReturnReference: returnReference,
},
IsTest: def.IsTest,
Private: def.Private,
IsTest: def.IsTest,
Private: def.Private,
ForeignABI: def.ForeignABI,
})
l.program.Modules[module].Functions = appendUniqueFunction(l.program.Modules[module].Functions, id)
if def.Name == "main" {
Expand Down Expand Up @@ -484,12 +485,13 @@ func (l *lowerer) declareFunctionSpecializationWithSignatureAndGenericKey(module
id := FunctionID(len(l.program.Functions))
l.functions[key] = id
l.program.Functions = append(l.program.Functions, Function{
ID: id,
Module: module,
Name: def.Name,
Signature: signature,
IsTest: def.IsTest,
Private: def.Private,
ID: id,
Module: module,
Name: def.Name,
Signature: signature,
IsTest: def.IsTest,
Private: def.Private,
ForeignABI: def.ForeignABI,
})
l.program.Modules[module].Functions = appendUniqueFunction(l.program.Modules[module].Functions, id)
return id, nil
Expand Down Expand Up @@ -573,6 +575,7 @@ func (l *lowerer) declareGenericFunctionDef(module ModuleID, callDef *checker.Fu
TypeParams: goParams,
IsTest: def.IsTest,
Private: def.Private,
ForeignABI: def.ForeignABI,
})
l.program.Modules[module].Functions = appendUniqueFunction(l.program.Modules[module].Functions, id)
typeVars := make(map[string]TypeID, len(params))
Expand Down Expand Up @@ -1787,6 +1790,7 @@ func (l *lowerer) declareMethodFunction(module ModuleID, owner checker.Type, tra
Name: owner.String() + "." + traitName + "." + def.Name,
Receiver: ownerType,
MethodName: def.Name,
ForeignABI: def.ForeignABI,
Signature: Signature{
Params: params,
Return: returnType,
Expand Down Expand Up @@ -1862,6 +1866,7 @@ func (l *lowerer) declareInstanceMethodFunction(module ModuleID, ownerName strin
Name: ownerName + "." + def.Name,
Receiver: ownerType,
MethodName: def.Name,
ForeignABI: def.ForeignABI,
Signature: signature,
})
l.program.Modules[module].Functions = appendUniqueFunction(l.program.Modules[module].Functions, id)
Expand Down Expand Up @@ -1915,6 +1920,7 @@ func (fl *functionLowerer) declareInstanceMethodFunction(module ModuleID, ownerN
Name: ownerName + "." + def.Name,
Receiver: ownerType,
MethodName: def.Name,
ForeignABI: def.ForeignABI,
Signature: signature,
})
fl.l.program.Modules[module].Functions = appendUniqueFunction(fl.l.program.Modules[module].Functions, id)
Expand Down Expand Up @@ -2054,6 +2060,7 @@ func (fl *functionLowerer) declareGenericInstanceMethodFunction(module ModuleID,
Name: structDef.Name + "." + callDef.Name,
Receiver: recvType,
MethodName: callDef.Name,
ForeignABI: orig.ForeignABI,
Signature: signature,
TypeParams: paramNames,
})
Expand Down
3 changes: 3 additions & 0 deletions compiler/air/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ type Function struct {
IsTest bool
IsScript bool
Private bool
// ForeignABI preserves an exact foreign Go method signature. Mutable
// descriptor parameters remain value-shaped at this boundary.
ForeignABI bool

// TypeParams names the generic parameters for a generic function definition
// (ADR 0031). When set, the function is emitted as `func Name[T any](...)`
Expand Down
108 changes: 93 additions & 15 deletions compiler/checker/checker.go
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,7 @@ func derefTypeSeen(t Type, seen map[Type]bool) Type {
Parameters: newParams,
ReturnType: derefReturnType,
ForeignResultShape: typ.ForeignResultShape,
ForeignABI: typ.ForeignABI,
InferReturnTypeFromBody: typ.InferReturnTypeFromBody,
Body: typ.Body,
Mutates: typ.Mutates,
Expand Down Expand Up @@ -391,6 +392,7 @@ type Checker struct {
spans *SpanIndex
nextCallInferenceID uint64
expectedCallExpectation *typeExpectation
foreignABIFunctions map[*parse.FunctionDeclaration]bool
moduleFiles map[string]string
}

Expand All @@ -415,8 +417,9 @@ func New(filePath string, input *parse.Program, moduleResolver *ModuleResolver,
StructMethods: map[MethodOwner]map[string]*FunctionDef{},
ForeignInterfaceImpls: map[MethodOwner][]*ForeignType{},
},
scope: &rootScope,
goTypesContext: gotypes.NewContext(),
scope: &rootScope,
goTypesContext: gotypes.NewContext(),
foreignABIFunctions: map[*parse.FunctionDeclaration]bool{},
}
if checkOptions.RecordSpans {
c.spans = &SpanIndex{}
Expand Down Expand Up @@ -1997,7 +2000,7 @@ func (c *Checker) checkForeignInterfaceImplementation(s *parse.TraitImplementati
c.addTypeMismatch(expectedType, paramType, param.GetLocation())
valid = false
}
if paramMutable && mutableParamNeedsGoPointer(paramType) {
if paramMutable && mutableForeignParamChangesGoABI(paramType) {
legacy := fmt.Sprintf("Go interface method '%s' parameter '%s' cannot be mutable because it would change the Go ABI", method.Name, param.Name)
c.addDiagnostic(implementationParameterMutabilityDiagnostic{
Method: method.Name, Parameter: param.Name, ExpectedMutable: false, Span: c.sourceSpan(param.GetLocation()), LegacyMessage: legacy,
Expand Down Expand Up @@ -2031,6 +2034,7 @@ func (c *Checker) checkForeignInterfaceImplementation(s *parse.TraitImplementati
continue
}
c.pushMethodGenericAllowlist(receiverGenerics)
c.foreignABIFunctions[&method] = true
fnDef := c.checkFunction(&method, func() {
c.scope.add(s.Receiver.Name, targetType, method.Mutates)
}, receiverGenerics...)
Expand All @@ -2048,6 +2052,7 @@ func (c *Checker) checkForeignInterfaceImplementation(s *parse.TraitImplementati
}
fnDef.Receiver = s.Receiver.Name
fnDef.Mutates = method.Mutates
fnDef.ForeignABI = true
fnDef.Name = goMethodNameToArdName(interfaceMethodName)
if _, exists := c.structMethod(targetType, fnDef.Name); exists {
validImpl = false
Expand Down Expand Up @@ -2102,20 +2107,30 @@ func (c *Checker) structImplementsForeignInterface(def *StructDef, iface *Foreig
// callee mutating the temporary is sound. Idiomatic Go passes container
// literals to such parameters directly.
func freshContainerSatisfiesMutable(paramType Type, arg Expression) bool {
if mutableParamNeedsGoPointer(paramType) {
return false
}
base, _ := mutableRefBase(paramType)
switch arg.(type) {
case *ListLiteral, *MapLiteral:
return true
case *ListLiteral:
_, ok := base.(*List)
if ok {
return true
}
foreign, ok := base.(*ForeignType)
return ok && foreign.Elem != nil
case *MapLiteral:
_, ok := base.(*Map)
if ok {
return true
}
foreign, ok := base.(*ForeignType)
return ok && foreign.MapKey != nil && foreign.MapValue != nil
}
return false
}

func mutableParamNeedsGoPointer(t Type) bool {
base, _ := mutableRefBase(t)
switch typ := base.(type) {
case *List, *Map, *Chan, *Receiver, *Sender:
case *Map, *Chan, *Receiver, *Sender:
return false
case *ForeignType:
// Named Go map and slice types are descriptors like their unnamed
Expand All @@ -2132,6 +2147,35 @@ func mutableParamNeedsGoPointer(t Type) bool {
}
}

func expressionUsesForeignDescriptor(expr Expression) bool {
switch value := expr.(type) {
case Variable:
return value.sym.foreignDescriptor
case *Variable:
return value.sym.foreignDescriptor
case *MutableRefExpr:
return expressionUsesForeignDescriptor(value.Operand)
default:
return false
}
}

func isDescriptorBackedMutableType(t Type) bool {
base, _ := mutableRefBase(t)
switch typ := base.(type) {
case *List, *Map, *Chan, *Receiver, *Sender:
return true
case *ForeignType:
return typ.Pointer || typ.Interface || typ.MapKey != nil || typ.Elem != nil
default:
return false
}
}

func mutableForeignParamChangesGoABI(t Type) bool {
return !isDescriptorBackedMutableType(t)
}

func (c *Checker) foreignInterfaceArgUpcast(expected Type, actual Expression) (Expression, bool) {
iface, ok := expected.(*ForeignType)
if !ok || !iface.Interface || actual == nil {
Expand Down Expand Up @@ -2768,6 +2812,8 @@ func (c *Checker) checkStmt(stmt *parse.Statement) *Statement {
__type: __type,
}
bound := c.scope.add(v.Name, v.__type, v.Mutable)
_, bindingIsReference := mutableRefBase(v.__type)
bound.foreignDescriptor = bindingIsReference && expressionUsesForeignDescriptor(val)
c.recordBindingWithSpan(s.NameLocation, s.GetLocation(), bound)
if c.spans != nil && c.scope.parent == nil {
// Module-level values are importable; give them a canonical
Expand All @@ -2794,6 +2840,15 @@ func (c *Checker) checkStmt(stmt *parse.Statement) *Statement {
return nil
}

if target.foreignDescriptor {
c.addDiagnostic(unreachableReferentAssignmentDiagnostic{
Name: target.Name,
Span: c.sourceSpan(s.Target.GetLocation()),
DeclarationSpan: sourceSpanIfPresent(target.declaredAt),
}.build())
return nil
}

// Assignment through a mutable-reference binding writes the
// referent; it does not rebind, so the binding's own
// (im)mutability does not gate it (ADR 0045).
Expand Down Expand Up @@ -5437,10 +5492,10 @@ func (c *Checker) createPrimitiveMethodNode(subject Expression, methodName strin

// Check for collection types
if _, isList := subject.Type().(*List); isList {
return c.createListMethod(subject, methodName, args, fnDef)
return c.createListMethod(subject, methodName, args, fnDef, loc)
}
if _, isFixedArray := subject.Type().(*FixedArray); isFixedArray {
return c.createListMethod(subject, methodName, args, fnDef)
return c.createListMethod(subject, methodName, args, fnDef, loc)
}
if _, isMap := subject.Type().(*Map); isMap {
return c.createMapMethod(subject, methodName, args, fnDef)
Expand All @@ -5449,11 +5504,11 @@ func (c *Checker) createPrimitiveMethodNode(subject Expression, methodName strin
return c.createMapMethod(subject, methodName, args, fnDef)
}
if foreign, isForeign := subject.Type().(*ForeignType); isForeign && foreign.Elem != nil {
return c.createListMethod(subject, methodName, args, fnDef)
return c.createListMethod(subject, methodName, args, fnDef, loc)
}
if foreign, isForeign := subject.Type().(*ForeignType); isForeign {
if _, ok := foreign.Underlying.(*FixedArray); ok {
return c.createListMethod(subject, methodName, args, fnDef)
return c.createListMethod(subject, methodName, args, fnDef, loc)
}
}
if _, isMaybe := subject.Type().(*Maybe); isMaybe {
Expand Down Expand Up @@ -5724,7 +5779,7 @@ func (c *Checker) createBoolMethod(subject Expression, methodName string) Expres
}
}

func (c *Checker) createListMethod(subject Expression, methodName string, args []Expression, fnDef *FunctionDef) Expression {
func (c *Checker) createListMethod(subject Expression, methodName string, args []Expression, fnDef *FunctionDef, loc parse.Location) Expression {
var elemType Type
if listType, ok := subject.Type().(*List); ok {
elemType = listType.of
Expand Down Expand Up @@ -5760,6 +5815,23 @@ func (c *Checker) createListMethod(subject Expression, methodName string, args [
default:
panic(fmt.Sprintf("Unknown List method: %s", methodName))
}
if kind == ListPush || kind == ListPrepend {
var sym Symbol
switch variable := subject.(type) {
case Variable:
sym = variable.sym
case *Variable:
sym = variable.sym
}
if sym.foreignDescriptor {
c.addDiagnostic(foreignDescriptorRebindingDiagnostic{
Name: sym.Name,
Operation: methodName,
Span: c.sourceSpan(loc),
DeclarationSpan: sourceSpanIfPresent(sym.declaredAt),
}.build())
}
}
return &ListMethod{
Subject: subject,
Kind: kind,
Expand Down Expand Up @@ -9907,6 +9979,8 @@ func (c *Checker) checkFunctionWithSignature(def *parse.FunctionDeclaration, ini
}
}

fn.ForeignABI = c.foreignABIFunctions[def]

if def.IsTest {
if init != nil {
c.addDiagnostic(invalidTestFunctionDiagnostic{
Expand Down Expand Up @@ -9956,7 +10030,9 @@ func (c *Checker) checkFunctionWithSignature(def *parse.FunctionDeclaration, ini
body := c.checkBlockWithExpected(def.Body, func() {
c.scope.expectReturn(returnType)
for _, param := range params {
c.recordBinding(param.Loc, c.scope.add(param.Name, param.Type, param.Mutable))
sym := c.scope.add(param.Name, param.Type, param.Mutable)
sym.foreignDescriptor = fn.ForeignABI && param.Mutable && isDescriptorBackedMutableType(param.Type)
c.recordBinding(param.Loc, sym)
}
}, returnType, true)
c.popFunctionGenericContext()
Expand Down Expand Up @@ -10030,6 +10106,7 @@ func substituteType(t Type, typeMap map[string]Type) Type {
Parameters: substitutedParams,
ReturnType: substituteType(typ.ReturnType, typeMap),
ForeignResultShape: typ.ForeignResultShape,
ForeignABI: typ.ForeignABI,
InferReturnTypeFromBody: typ.InferReturnTypeFromBody,
Body: typ.Body,
Mutates: typ.Mutates,
Expand Down Expand Up @@ -10833,6 +10910,7 @@ func (c *Checker) checkAndProcessArguments(fnDef *FunctionDef, resolvedExprs []p
Parameters: make([]Parameter, len(fnDefCopy.Parameters)),
ReturnType: substituteType(fnDefCopy.ReturnType, bindings),
ForeignResultShape: fnDefCopy.ForeignResultShape,
ForeignABI: fnDefCopy.ForeignABI,
InferReturnTypeFromBody: fnDefCopy.InferReturnTypeFromBody,
Body: fnDefCopy.Body,
Mutates: fnDefCopy.Mutates,
Expand Down
21 changes: 21 additions & 0 deletions compiler/checker/diagnostics.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ const (
DiagnosticCodeUnsupportedMutableReference DiagnosticCode = "unsupported_mutable_reference"
DiagnosticCodeInvalidForeignPointerBinding DiagnosticCode = "invalid_foreign_pointer_binding"
DiagnosticCodeUnreachableReferentAssignment DiagnosticCode = "unreachable_referent_assignment"
DiagnosticCodeForeignDescriptorRebinding DiagnosticCode = "foreign_descriptor_rebinding"
DiagnosticCodeReferenceRebinding DiagnosticCode = "reference_rebinding"
DiagnosticCodeImmutablePropertyAssignment DiagnosticCode = "immutable_property_assignment"
DiagnosticCodeImmutableReceiver DiagnosticCode = "immutable_receiver"
Expand Down Expand Up @@ -465,6 +466,26 @@ func (d unreachableReferentAssignmentDiagnostic) build() Diagnostic {
)
}

type foreignDescriptorRebindingDiagnostic struct {
Name string
Operation string
Span SourceSpan
DeclarationSpan *SourceSpan
}

func (d foreignDescriptorRebindingDiagnostic) build() Diagnostic {
legacy := fmt.Sprintf("Cannot call '%s' on foreign descriptor reference '%s': the Go ABI cannot propagate list growth", d.Operation, d.Name)
return mutationDiagnostic(
DiagnosticCodeForeignDescriptorRebinding,
legacy,
"Foreign descriptor cannot be replaced",
"This Go ABI parameter shares list elements, but list growth cannot update the caller's slice descriptor.",
DiagnosticLabel{Span: d.Span, Message: fmt.Sprintf("`.%s()` would only update a local descriptor", d.Operation)},
d.DeclarationSpan,
"foreign descriptor reference declared here",
)
}

type referenceRebindingDiagnostic struct {
Span SourceSpan
DeclarationSpan *SourceSpan
Expand Down
2 changes: 1 addition & 1 deletion compiler/checker/diagnostics_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1198,7 +1198,7 @@ func TestReferenceRebindingHasStructuredLabels(t *testing.T) {
}

func TestUnreachableReferentAssignmentHasStructuredLabels(t *testing.T) {
source := "mut items = [1, 2]\nlet ref = mut items\nref = [9, 9]\n"
source := "mut items = [1: 2]\nlet ref = mut items\nref = [9: 9]\n"
result := parse.Parse([]byte(source), "main.ard")
if len(result.Errors) > 0 {
t.Fatalf("parse errors: %v", result.Errors)
Expand Down
Loading
Loading