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
202 changes: 164 additions & 38 deletions compiler/air/lower.go

Large diffs are not rendered by default.

189 changes: 188 additions & 1 deletion compiler/air/lower_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,54 @@ import (
"github.com/akonwi/ard/parse"
)

type orderingTestModule struct {
path string
}

func (m orderingTestModule) Path() string { return m.path }
func (m orderingTestModule) Get(string) checker.Symbol { return checker.Symbol{} }
func (m orderingTestModule) Program() *checker.Program { return nil }
func (m orderingTestModule) Symbols() map[string]checker.Symbol { return nil }

func TestSortedModulesUsesCanonicalPathOrder(t *testing.T) {
modules := map[string]checker.Module{
"middle": orderingTestModule{path: "demo/middle"},
"last": orderingTestModule{path: "demo/z_last"},
"first": orderingTestModule{path: "demo/a_first"},
}

ordered := sortedModules(modules)
got := make([]string, len(ordered))
for i, module := range ordered {
got[i] = module.Path()
}
want := []string{"demo/a_first", "demo/middle", "demo/z_last"}
if strings.Join(got, ",") != strings.Join(want, ",") {
t.Fatalf("sorted modules = %v, want %v", got, want)
}
}

func TestSortedMethodDefinitionsUsesCanonicalKeyOrder(t *testing.T) {
first := &checker.FunctionDef{Name: "first"}
middle := &checker.FunctionDef{Name: "middle"}
last := &checker.FunctionDef{Name: "last"}
methods := map[string]*checker.FunctionDef{
"z_last": last,
"middle": middle,
"a_first": first,
}

ordered := sortedMethodDefinitions(methods)
got := make([]string, len(ordered))
for i, method := range ordered {
got[i] = method.Name
}
want := []string{"first", "middle", "last"}
if strings.Join(got, ",") != strings.Join(want, ",") {
t.Fatalf("sorted methods = %v, want %v", got, want)
}
}

func TestLowerFunctionParameterWithIndirectMutuallyRecursiveStructs(t *testing.T) {
const helperEnv = "ARD_TEST_AIR_RECURSIVE_TYPE_BINDING"
const input = `
Expand Down Expand Up @@ -1617,8 +1665,147 @@ func TestLowerGenericStructMethodBodyUsesReceiverBindings(t *testing.T) {
t.Fatalf("get return kind = %v, want TypeParam", typeKind(t, program, get.Signature.Return))
}
}

func TestLowererCachesCrossModuleMethodLookupsByOwner(t *testing.T) {
owner := checker.MethodOwner{ModulePath: "dependency", TypeName: "Widget"}
trait := &checker.Trait{Name: "Printable", ModulePath: "dependency"}
traitOwner := checker.TraitMethodOwner{
MethodOwner: owner,
TraitModulePath: trait.ModulePath,
TraitName: trait.Name,
}
structMethod := &checker.FunctionDef{Name: "render"}
traitMethod := &checker.FunctionDef{Name: "print"}
required := &checker.FunctionDef{Name: "string", RequiredGoMethodName: "String"}
inherent := &checker.FunctionDef{Name: "label"}
dependency := &countingMethodModule{
path: "dependency",
program: &checker.Program{
StructMethods: map[checker.MethodOwner]map[string]*checker.FunctionDef{
owner: {"render": structMethod},
},
TraitMethods: map[checker.TraitMethodOwner]map[string]*checker.FunctionDef{
traitOwner: {"print": traitMethod},
},
RequiredGoMethods: map[checker.MethodOwner]map[string]*checker.FunctionDef{
owner: {"String": required},
},
InherentMethods: map[checker.MethodOwner]map[string]*checker.FunctionDef{
owner: {"label": inherent},
},
},
}
root := &countingMethodModule{
path: "root",
program: &checker.Program{Imports: map[string]checker.Module{"dependency": dependency}},
}
lowerer := newLowerer(LowerOptions{}, 1)
lowerer.moduleByName[root.path] = root
def := &checker.StructDef{Name: owner.TypeName, ModulePath: owner.ModulePath}

for i := 0; i < 2; i++ {
if got := lowerer.requiredGoMethods(def); got["String"] != required {
t.Fatalf("required lookup %d = %#v, want String method", i, got)
}
}
if root.programCalls != 1 || dependency.programCalls != 1 {
t.Fatalf("required lookup traversed modules more than once: root=%d dependency=%d", root.programCalls, dependency.programCalls)
}

for i := 0; i < 2; i++ {
if got := lowerer.inherentMethods(def); got["label"] != inherent {
t.Fatalf("inherent lookup %d = %#v, want label method", i, got)
}
}
if root.programCalls != 2 || dependency.programCalls != 2 {
t.Fatalf("inherent lookup traversed modules more than once: root=%d dependency=%d", root.programCalls, dependency.programCalls)
}

for i := 0; i < 2; i++ {
if got := lowerer.structMethods(def); got["render"] != structMethod {
t.Fatalf("struct lookup %d = %#v, want render method", i, got)
}
}
if root.programCalls != 3 || dependency.programCalls != 3 {
t.Fatalf("struct lookup traversed modules more than once: root=%d dependency=%d", root.programCalls, dependency.programCalls)
}

for i := 0; i < 2; i++ {
if got := lowerer.traitMethods(def, trait); got["print"] != traitMethod {
t.Fatalf("trait lookup %d = %#v, want print method", i, got)
}
}
if root.programCalls != 4 || dependency.programCalls != 4 {
t.Fatalf("trait lookup traversed modules more than once: root=%d dependency=%d", root.programCalls, dependency.programCalls)
}
}

func TestLowererCachesMissingCrossModuleMethods(t *testing.T) {
dependency := &countingMethodModule{path: "dependency", program: &checker.Program{}}
root := &countingMethodModule{
path: "root",
program: &checker.Program{Imports: map[string]checker.Module{"dependency": dependency}},
}
lowerer := newLowerer(LowerOptions{}, 1)
lowerer.moduleByName[root.path] = root
def := &checker.StructDef{Name: "Missing", ModulePath: "dependency"}

for i := 0; i < 2; i++ {
if got := lowerer.requiredGoMethods(def); got != nil {
t.Fatalf("required lookup %d = %#v, want nil", i, got)
}
}
if root.programCalls != 1 || dependency.programCalls != 1 {
t.Fatalf("missing lookup traversed modules more than once: root=%d dependency=%d", root.programCalls, dependency.programCalls)
}
}

func TestLowererDisablesMethodLookupCachingForMultipleRoots(t *testing.T) {
owner := checker.MethodOwner{ModulePath: "dependency", TypeName: "Widget"}
required := &checker.FunctionDef{Name: "string", RequiredGoMethodName: "String"}
dependency := &countingMethodModule{
path: "dependency",
program: &checker.Program{RequiredGoMethods: map[checker.MethodOwner]map[string]*checker.FunctionDef{
owner: {"String": required},
}},
}
root := &countingMethodModule{
path: "root",
program: &checker.Program{Imports: map[string]checker.Module{"dependency": dependency}},
}
lowerer := newLowerer(LowerOptions{}, 2)
lowerer.moduleByName[root.path] = root
def := &checker.StructDef{Name: owner.TypeName, ModulePath: owner.ModulePath}

for i := 0; i < 2; i++ {
if got := lowerer.requiredGoMethods(def); got["String"] != required {
t.Fatalf("required lookup %d = %#v, want String method", i, got)
}
}
if root.programCalls != 2 || dependency.programCalls != 2 {
t.Fatalf("uncached lookup did not retraverse modules: root=%d dependency=%d", root.programCalls, dependency.programCalls)
}
}

type countingMethodModule struct {
path string
program *checker.Program
programCalls int
}

func (m *countingMethodModule) Path() string { return m.path }

func (m *countingMethodModule) Get(string) checker.Symbol { return checker.Symbol{} }

func (m *countingMethodModule) Program() *checker.Program {
m.programCalls++
return m.program
}

func (m *countingMethodModule) Symbols() map[string]checker.Symbol { return nil }

func TestReferenceSyntheticIdentityUsesReferentTypeID(t *testing.T) {
lowerer := newLowerer(LowerOptions{})
lowerer := newLowerer(LowerOptions{}, 0)
left := TypeID(len(lowerer.program.Types) + 1)
lowerer.program.Types = append(lowerer.program.Types, TypeInfo{ID: left, Kind: TypeStruct, Name: "Item", ModulePath: "left"})
right := TypeID(len(lowerer.program.Types) + 1)
Expand Down
25 changes: 25 additions & 0 deletions compiler/checker/go_import_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,31 @@ func TestReadonlyModuleCompletionErrorClassification(t *testing.T) {
}
}

func TestDependencyReplaceOverlayUsesCanonicalModuleOrder(t *testing.T) {
root := t.TempDir()
if err := os.WriteFile(filepath.Join(root, "go.mod"), []byte("module example.com/app\n\ngo 1.27\n"), 0o644); err != nil {
t.Fatal(err)
}
resolver := NewGoPackagesResolver(root, nil)
resolver.DependencyModuleRoots = map[string]string{
"example.com/z_last": t.TempDir(),
"example.com/a_first": t.TempDir(),
}

overlay := resolver.dependencyReplaceOverlay()
got := string(overlay[filepath.Join(root, "go.mod")])
firstRequire := strings.Index(got, "example.com/a_first v0.0.0")
lastRequire := strings.Index(got, "example.com/z_last v0.0.0")
firstReplace := strings.Index(got, "replace example.com/a_first")
lastReplace := strings.Index(got, "replace example.com/z_last")
if firstRequire < 0 || lastRequire < 0 || firstRequire > lastRequire {
t.Fatalf("dependency requirements are not canonical:\n%s", got)
}
if firstReplace < 0 || lastReplace < 0 || firstReplace > lastReplace {
t.Fatalf("dependency replacements are not canonical:\n%s", got)
}
}

func TestDependencyModfilesTrustOnlyProjectChecksums(t *testing.T) {
cacheRoot := t.TempDir()
t.Setenv("HOME", cacheRoot)
Expand Down
31 changes: 26 additions & 5 deletions compiler/checker/go_packages_resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -686,8 +686,14 @@ func (r *GoPackagesResolver) dependencyReplaceOverlay() map[string][]byte {
if err != nil {
return nil
}
modulePaths := make([]string, 0, len(r.DependencyModuleRoots))
for modulePath := range r.DependencyModuleRoots {
modulePaths = append(modulePaths, modulePath)
}
sort.Strings(modulePaths)
changed := false
for modulePath, dir := range r.DependencyModuleRoots {
for _, modulePath := range modulePaths {
dir := r.DependencyModuleRoots[modulePath]
if modulePath == "" || dir == "" {
continue
}
Expand Down Expand Up @@ -759,24 +765,39 @@ func DependencyGoModuleRoots(info *ProjectInfo) map[string]string {
}
roots[modulePath] = root
}
dependencyAliases := make([]string, 0, len(info.Dependencies))
for alias := range info.Dependencies {
dependencyAliases = append(dependencyAliases, alias)
}
sort.Strings(dependencyAliases)
packageIDs := make([]string, 0, len(info.Packages))
for packageID := range info.Packages {
packageIDs = append(packageIDs, packageID)
}
sort.Strings(packageIDs)

// Add path dependencies first so a locked Git checkout retains precedence
// if malformed dependency metadata names the same Go module from both.
for _, dep := range info.Dependencies {
for _, alias := range dependencyAliases {
dep := info.Dependencies[alias]
if dep.Git == "" {
add(dep.RootPath)
}
}
for packageID, pkg := range info.Packages {
for _, packageID := range packageIDs {
pkg := info.Packages[packageID]
if packageID != info.RootPackageID && pkg.Git == "" && pkg.Path != "" {
add(pkg.RootPath)
}
}
for _, dep := range info.Dependencies {
for _, alias := range dependencyAliases {
dep := info.Dependencies[alias]
if dep.Git != "" {
add(dep.RootPath)
}
}
for packageID, pkg := range info.Packages {
for _, packageID := range packageIDs {
pkg := info.Packages[packageID]
if packageID != info.RootPackageID && pkg.Git != "" {
add(pkg.RootPath)
}
Expand Down
Loading
Loading