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
28 changes: 25 additions & 3 deletions compiler/checker/checker.go
Original file line number Diff line number Diff line change
Expand Up @@ -1990,7 +1990,7 @@ func (c *Checker) resolveType(t parse.DeclaredType) Type {
if !c.genericAllowedInCurrentMethod(ty.Name) {
c.addMethodIntroducedGeneric(ty.Name, methodGenericInvalidOccurrence, ty.GetLocation())
}
if existing := c.scope.findGeneric(ty.Name); existing != nil {
if existing := c.scope.findDeclarationGeneric(ty.Name); existing != nil {
baseType = existing
} else {
baseType = &TypeVar{name: ty.Name}
Expand Down Expand Up @@ -2038,6 +2038,28 @@ func (c *Checker) pushFunctionGenericContext(fnDef *FunctionDef, extraParams ...
c.genericContextStack = append(c.genericContextStack, context)
}

func (c *Checker) pushAnonymousFunctionGenericContext() {
if len(c.genericContextStack) == 0 {
c.genericContextStack = append(c.genericContextStack, nil)
return
}

// Anonymous functions inherit only the immediate lexical context used to
// validate explicit generic call arguments. Named functions still push a
// fresh frame, so copying the top frame keeps those declarations as generic
// boundaries while allowing nested closures to inherit transitively.
current := c.genericContextStack[len(c.genericContextStack)-1]
if len(current) == 0 {
c.genericContextStack = append(c.genericContextStack, nil)
return
}
inherited := make(map[string]bool, len(current))
for name := range current {
inherited[name] = true
}
c.genericContextStack = append(c.genericContextStack, inherited)
}

func (c *Checker) popFunctionGenericContext() {
if len(c.genericContextStack) == 0 {
return
Expand Down Expand Up @@ -9076,7 +9098,7 @@ func (c *Checker) checkExprInner(expr parse.Expression, expectedReturn Type) Exp
c.recordBinding(param.Loc, sym)
}
}
c.pushFunctionGenericContext(fn)
c.pushAnonymousFunctionGenericContext()
c.pushConstraintFunction(fn, s.GetLocation())
previousDeferredWorkDepth := c.deferredWorkDepth
c.deferredWorkDepth = 0
Expand Down Expand Up @@ -11354,7 +11376,7 @@ func (c *Checker) checkExprAsInner(expr parse.Expression, expectedType Type, exp
}

// Check body
c.pushFunctionGenericContext(fn)
c.pushAnonymousFunctionGenericContext()
previousDeferredWorkDepth := c.deferredWorkDepth
c.deferredWorkDepth = 0
body := c.checkBlockWithExpected(s.Body, func() {
Expand Down
245 changes: 245 additions & 0 deletions compiler/checker/closure_generic_context_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
package checker

import (
"testing"

"github.com/akonwi/ard/parse"
)

func checkGenericContextSource(t *testing.T, source string) *Checker {
t.Helper()
result := parse.Parse([]byte(source), "test.ard")
if len(result.Errors) > 0 {
t.Fatalf("parse error: %s", result.Errors[0].Message)
}
checker := New("test.ard", result.Program, nil)
checker.Check()
return checker
}

func TestAnonymousClosuresInheritEnclosingGenericContext(t *testing.T) {
tests := []struct {
name string
source string
}{
{
name: "contextual closure in generic receiver method",
source: `
fn identity(value: $T) $T { value }

struct Box<$T> { value: $T }

impl Box {
fn callback() fn() {
fn() {
let value = identity<$T>(self.value)
let _ = value
}
}
}
`,
},
{
name: "explicit receiver generic absent from fields",
source: `
fn empty() $T? { Maybe::new() }

struct Marker<$T> {}

impl Marker {
fn callback() fn() {
fn() {
let value = empty<$T>()
let _ = value
}
}
}
`,
},
{
name: "local closure in generic function",
source: `
fn identity(value: $T) $T { value }

fn invoke(value: $T) {
let callback = fn() {
let copy = identity<$T>(value)
let _ = copy
}
callback()
}
`,
},
{
name: "nested anonymous closures",
source: `
fn identity(value: $T) $T { value }

fn callback(value: $T) fn() fn() {
fn() fn() {
fn() {
let copy = identity<$T>(value)
let _ = copy
}
}
}
`,
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
checker := checkGenericContextSource(t, test.source)
if checker.HasErrors() {
t.Fatalf("checker diagnostics: %v", checker.Diagnostics())
}
})
}
}

func TestAnonymousClosureDoesNotExposeContextualCallGenericAsExplicitTypeArg(t *testing.T) {
checker := checkGenericContextSource(t, `
fn identity(value: $T) $T { value }
fn consume(callback: fn($T)) {}

fn main() {
consume(fn(value) {
let copy = identity<$T>(value)
let _ = copy
})
}
`)

for _, diagnostic := range checker.Diagnostics() {
if diagnostic.Code == DiagnosticCodeUnboundGenericTypeArg {
return
}
}
t.Fatalf("diagnostics = %v, want unbound generic type argument", checker.Diagnostics())
}

func TestAnonymousClosurePrefersOuterGenericOverSameNamedContextualGeneric(t *testing.T) {
checker := checkGenericContextSource(t, `
fn identity(value: $T) $T { value }
fn consume(callback: fn($T)) {}

fn outer(value: $T) {
consume(fn(_contextual) {
let copy = identity<$T>(value)
let _ = copy
})
}
`)

diagnostics := checker.Diagnostics()
if len(diagnostics) != 1 || diagnostics[0].Code != DiagnosticCodeUnresolvedGeneric {
t.Fatalf("diagnostics = %v, want only the unresolved consume generic", diagnostics)
}
}

func TestAnonymousClosureExplicitTypeArgUsesOuterGenericInsteadOfContextualGeneric(t *testing.T) {
checker := checkGenericContextSource(t, `
fn identity(value: $T) $T { value }
fn consume(callback: fn($T) $T, seed: $T) $T { callback(seed) }

fn outer(value: $T) Int {
consume(fn(_contextual) Int {
let copy = identity<$T>(value)
let _ = copy
1
}, 1)
}
`)
if checker.HasErrors() {
t.Fatalf("checker diagnostics: %v", checker.Diagnostics())
}

var outer *FunctionDef
for _, statement := range checker.program.Statements {
if function, ok := statement.Expr.(*FunctionDef); ok && function.Name == "outer" {
outer = function
break
}
}
if outer == nil {
t.Fatal("outer declaration not found")
}
outerGeneric, ok := outer.Parameters[0].Type.(*TypeVar)
if !ok {
t.Fatalf("outer parameter type = %T, want declaration TypeVar", outer.Parameters[0].Type)
}
consumeCall, ok := outer.Body.Stmts[0].Expr.(*FunctionCall)
if !ok {
t.Fatalf("outer body expression = %T, want FunctionCall", outer.Body.Stmts[0].Expr)
}
closure, ok := consumeCall.Args[0].(*FunctionDef)
if !ok {
t.Fatalf("consume callback = %T, want FunctionDef", consumeCall.Args[0])
}
if len(closure.CallGenericParams) != 0 {
t.Fatalf("closure call generics = %v, want inherited generics to remain outer-owned", closure.CallGenericParams)
}
binding, ok := closure.Body.Stmts[0].Stmt.(*VariableDef)
if !ok {
t.Fatalf("closure statement = %T, want VariableDef", closure.Body.Stmts[0].Stmt)
}
identityCall, ok := binding.Value.(*FunctionCall)
if !ok {
t.Fatalf("binding value = %T, want FunctionCall", binding.Value)
}
explicitGeneric, ok := identityCall.TypeArgs[0].(*TypeVar)
if !ok {
t.Fatalf("explicit type argument = %T, want TypeVar", identityCall.TypeArgs[0])
}
if explicitGeneric != outerGeneric {
t.Fatalf("explicit type argument = %p, want outer declaration generic %p", explicitGeneric, outerGeneric)
}
if explicitGeneric.owner != 0 || explicitGeneric.provisional {
t.Fatalf("explicit type argument owner = %d provisional = %t, want declaration-owned", explicitGeneric.owner, explicitGeneric.provisional)
}
}

func TestAnonymousClosureExplicitTypeArgDoesNotCrossNamedFunctionGenericBoundary(t *testing.T) {
checker := checkGenericContextSource(t, `
fn identity(value: $T) $T { value }

fn outer(value: $T) {
fn inner() {
let copy = identity<$T>(value)
let _ = copy
}
inner()
}
`)

for _, diagnostic := range checker.Diagnostics() {
if diagnostic.Code == DiagnosticCodeUnboundGenericTypeArg {
return
}
}
t.Fatalf("diagnostics = %v, want named function boundary to reject outer generic", checker.Diagnostics())
}

func TestFindDeclarationGenericIgnoresInferenceVariables(t *testing.T) {
declaration := &TypeVar{name: "T"}
outer := makeScope(nil)
outer.add("declaration", declaration, false)

tests := []struct {
name string
inference *TypeVar
}{
{name: "call-owned", inference: &TypeVar{name: "T", owner: 1}},
{name: "provisional", inference: &TypeVar{name: "T", provisional: true}},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
inner := makeScope(&outer)
inner.add("inference", test.inference, false)
if got := inner.findDeclarationGeneric("T"); got != declaration {
t.Fatalf("findDeclarationGeneric(T) = %p, want declaration generic %p", got, declaration)
}
})
}
}
16 changes: 9 additions & 7 deletions compiler/checker/scope.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,18 +100,20 @@ func (st SymbolTable) get(name string) (*Symbol, bool) {
return nil, false
}

// findGeneric looks for an existing generic type with the given name in the scope chain
func (st *SymbolTable) findGeneric(genericName string) *TypeVar {
// Check current scope
// findDeclarationGeneric looks for a declaration generic with the given name.
// Call-owned and provisional variables belong to inference and must not be
// reused when resolving a generic written in source.
func (st *SymbolTable) findDeclarationGeneric(genericName string) *TypeVar {
for _, symbol := range st.symbols {
if typeVar, ok := symbol.Type.(*TypeVar); ok && typeVar.name == genericName {
return typeVar
typeVar, ok := symbol.Type.(*TypeVar)
if !ok || typeVar.name != genericName || typeVar.owner != 0 || typeVar.provisional {
continue
}
return typeVar
}

// Check parent scopes
if st.parent != nil {
return st.parent.findGeneric(genericName)
return st.parent.findDeclarationGeneric(genericName)
}

return nil
Expand Down
33 changes: 33 additions & 0 deletions compiler/go/parity_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3032,6 +3032,39 @@ func TestGoTargetParityGenericMethodClosureCapturesInferredLocal(t *testing.T) {
}
}

func TestGoTargetParityAnonymousClosureInheritsReceiverGenericForExplicitCall(t *testing.T) {
program := lowerParitySource(t, `
fn identity(value: $T) $T { value }

mut observed = 0

struct Box<$T> {
value: $T,
}

impl Box {
fn callback() fn() {
fn() {
let value = identity<$T>(self.value)
let _ = value
observed = observed + 1
}
}
}

fn main() Int {
let int_box = Box<Int>{value: 42}
int_box.callback()()
let str_box = Box<Str>{value: "forty-two"}
str_box.callback()()
observed
}
`)
if got := runGoTargetParityJSON(t, program); got != "2" {
t.Fatalf("got %s, want 2", got)
}
}

func TestGoTargetParityNestedGenericClosuresPreserveNamedTypeIdentity(t *testing.T) {
program := lowerParitySource(t, `
private struct Value {
Expand Down
Loading
Loading