diff --git a/compiler/lowering.go b/compiler/lowering.go index bfed684aa..2b57a49a1 100644 --- a/compiler/lowering.go +++ b/compiler/lowering.go @@ -2452,6 +2452,8 @@ func lazyPackageVars(semPkg *semanticPackage) map[types.Object]bool { } funcRefs := packageVarsReferencedFromOtherFileTopLevelCalls(semPkg, declFiles) lazy := make(map[types.Object]bool) + otherFile := make(map[types.Object]bool) + var inits []packageVarInit for idx, file := range semPkg.source.Syntax { sourcePath := sourceFilePath(semPkg, idx, file) for _, decl := range file.Decls { @@ -2483,11 +2485,13 @@ func lazyPackageVars(semPkg *semanticPackage) map[types.Object]bool { if valueIdx < len(valueSpec.Values) && initializerReferencesOtherFileObject(semPkg, declFiles, sourcePath, valueSpec.Values[valueIdx]) { lazy[obj] = true + otherFile[obj] = true continue } if valueIdx < len(valueSpec.Values) && initializerCallsFunctionReferencingOtherFileObject(semPkg, declFiles, sourcePath, valueSpec.Values[valueIdx]) { lazy[obj] = true + otherFile[obj] = true continue } if valueIdx < len(valueSpec.Values) && @@ -2498,20 +2502,90 @@ func lazyPackageVars(semPkg *semanticPackage) map[types.Object]bool { if valueIdx >= len(valueSpec.Values) && zeroValueReferencesOtherFileObject(semPkg, declFiles, sourcePath, obj.Type()) { lazy[obj] = true + otherFile[obj] = true continue } if funcRefs[obj] && (valueIdx >= len(valueSpec.Values) || !initializerMayHaveRuntimeEffects(lowerFileContext{semPkg: semPkg}, valueSpec.Values[valueIdx])) { lazy[obj] = true + continue + } + + // The other initializers may read variables that need other files (see markLazyReaders). + if valueIdx < len(valueSpec.Values) && len(valueSpec.Names) == len(valueSpec.Values) { + inits = append(inits, packageVarInit{obj: obj, expr: valueSpec.Values[valueIdx]}) } } } } } + markLazyReaders(semPkg, lazy, otherFile, inits) return lazy } +// packageVarInit is a package variable and its initializer. +type packageVarInit struct { + obj *types.Var + expr ast.Expr +} + +// markLazyReaders marks as lazy the variables of the given initializers that +// read a variable of otherFile, the lazy variables whose initializers need +// declarations of other files, and adds them to otherFile, until no more are +// marked. Reading such a variable runs its initializer, which may need a +// declaration, such as a class, of a file that is not evaluated yet, when the +// file of the reader is evaluated first. Initializers with runtime effects +// stay eager, so they still run at package initialization, as do those of +// blank variables, which are never read. +func markLazyReaders(semPkg *semanticPackage, lazy, otherFile map[types.Object]bool, inits []packageVarInit) { + // Each pass marks the readers of the variables marked so far. + ctx := lowerFileContext{semPkg: semPkg} + for changed := true; changed; { + changed = false + for _, init := range inits { + // Skip the variables that are lazy, never read, or not readers. + if lazy[init.obj] || init.obj.Name() == "_" || !initializerReadsVar(semPkg, otherFile, init.expr) { + continue + } + + // Mark a reader whose initializer has no runtime effects. + if initializerMayHaveRuntimeEffects(ctx, init.expr) { + continue + } + lazy[init.obj] = true + otherFile[init.obj] = true + changed = true + } + } +} + +// initializerReadsVar returns whether expr reads a package variable of vars +// outside of function literals, which run later. +func initializerReadsVar(semPkg *semanticPackage, vars map[types.Object]bool, expr ast.Expr) bool { + reads := false + ast.Inspect(expr, func(node ast.Node) bool { + // Stop after a read, and skip function literals, which do not run + // when the variable is initialized. + if reads { + return false + } + if _, ok := node.(*ast.FuncLit); ok { + return false + } + + // An identifier may use a variable of vars. + ident, ok := node.(*ast.Ident) + if !ok { + return true + } + obj, ok := semPkg.source.TypesInfo.Uses[ident].(*types.Var) + reads = ok && vars[obj] + return !reads + }) + return reads +} + func packageVarsReferencedFromOtherFileTopLevelCalls( semPkg *semanticPackage, declFiles map[types.Object]string, diff --git a/tests/tests/cross_file_struct_typeinfo_zero/__goscript_package_init.gs.ts b/tests/tests/cross_file_struct_typeinfo_zero/__goscript_package_init.gs.ts new file mode 100644 index 000000000..4e2a2d8ed --- /dev/null +++ b/tests/tests/cross_file_struct_typeinfo_zero/__goscript_package_init.gs.ts @@ -0,0 +1,15 @@ +// Generated file based on __goscript_package_init.go +// Updated when compliance tests are re-run, DO NOT EDIT! + +import * as __goscript_file0 from "./c.gs.ts" + +let initialized = false + +export async function __goscript_package_init(): globalThis.Promise { + if (initialized) { + return + } + initialized = true + __goscript_file0.__goscript_init_cTable() + __goscript_file0.__goscript_init_cCopy() +} diff --git a/tests/tests/cross_file_struct_typeinfo_zero/b.go b/tests/tests/cross_file_struct_typeinfo_zero/b.go index d33d44942..5c3eee9e9 100644 --- a/tests/tests/cross_file_struct_typeinfo_zero/b.go +++ b/tests/tests/cross_file_struct_typeinfo_zero/b.go @@ -7,3 +7,7 @@ type bValue struct { func makeB() bValue { return bValue{} } + +func countC() int { + return len(cCopy) +} diff --git a/tests/tests/cross_file_struct_typeinfo_zero/b.gs.ts b/tests/tests/cross_file_struct_typeinfo_zero/b.gs.ts index 2c16b7f6d..66719b617 100644 --- a/tests/tests/cross_file_struct_typeinfo_zero/b.gs.ts +++ b/tests/tests/cross_file_struct_typeinfo_zero/b.gs.ts @@ -3,6 +3,7 @@ import * as $ from "@goscript/builtin/index.js" import * as __goscript_a from "./a.gs.ts" +import * as __goscript_c from "./c.gs.ts" export class bValue { public declare inner: __goscript_a.aValue @@ -33,3 +34,7 @@ export class bValue { export function makeB(): bValue { return new bValue(true) } + +export function countC(): number { + return $.sliceLen(__goscript_c.__goscript_get_cCopy()) +} diff --git a/tests/tests/cross_file_struct_typeinfo_zero/c.go b/tests/tests/cross_file_struct_typeinfo_zero/c.go new file mode 100644 index 000000000..4b20bee27 --- /dev/null +++ b/tests/tests/cross_file_struct_typeinfo_zero/c.go @@ -0,0 +1,8 @@ +package main + +// cTable is initialized lazily, as its values have a type of another file, +// and so must be cCopy, which reads it: b.go, which declares that type, +// uses cCopy, so its file is evaluated after this one. +var cTable = []bValue{{}, {}} + +var cCopy = cTable diff --git a/tests/tests/cross_file_struct_typeinfo_zero/c.gs.ts b/tests/tests/cross_file_struct_typeinfo_zero/c.gs.ts new file mode 100644 index 000000000..cd60cc946 --- /dev/null +++ b/tests/tests/cross_file_struct_typeinfo_zero/c.gs.ts @@ -0,0 +1,45 @@ +// Generated file based on c.go +// Updated when compliance tests are re-run, DO NOT EDIT! + +import * as $ from "@goscript/builtin/index.js" +import * as __goscript_a from "./a.gs.ts" +import * as __goscript_b from "./b.gs.ts" +import "./a.gs.ts" + +export var cTable: $.Slice<__goscript_b.bValue> + +export function __goscript_init_cTable(): void { + if (((cTable) as any) === undefined) { + cTable = $.arrayToSlice<__goscript_b.bValue>([new __goscript_b.bValue(true), new __goscript_b.bValue(true)]) + } +} + +export function __goscript_get_cTable(): $.Slice<__goscript_b.bValue> { + if (((cTable) as any) === undefined) { + __goscript_init_cTable() + } + return cTable +} + +export function __goscript_set_cTable(__goscriptValue: $.Slice<__goscript_b.bValue>): void { + cTable = __goscriptValue +} + +export var cCopy: $.Slice<__goscript_b.bValue> + +export function __goscript_init_cCopy(): void { + if (((cCopy) as any) === undefined) { + cCopy = __goscript_get_cTable() + } +} + +export function __goscript_get_cCopy(): $.Slice<__goscript_b.bValue> { + if (((cCopy) as any) === undefined) { + __goscript_init_cCopy() + } + return cCopy +} + +export function __goscript_set_cCopy(__goscriptValue: $.Slice<__goscript_b.bValue>): void { + cCopy = __goscriptValue +} diff --git a/tests/tests/cross_file_struct_typeinfo_zero/cross_file_struct_typeinfo_zero.go b/tests/tests/cross_file_struct_typeinfo_zero/cross_file_struct_typeinfo_zero.go index cf92af379..26abf95d8 100644 --- a/tests/tests/cross_file_struct_typeinfo_zero/cross_file_struct_typeinfo_zero.go +++ b/tests/tests/cross_file_struct_typeinfo_zero/cross_file_struct_typeinfo_zero.go @@ -4,4 +4,5 @@ func main() { a := makeA() b := makeB() println("ok:", a.next == nil, b.inner.next == nil) + println("table:", countC(), cCopy[1].inner.next == nil) } diff --git a/tests/tests/cross_file_struct_typeinfo_zero/cross_file_struct_typeinfo_zero.gs.ts b/tests/tests/cross_file_struct_typeinfo_zero/cross_file_struct_typeinfo_zero.gs.ts index 440ce618f..367e37a22 100644 --- a/tests/tests/cross_file_struct_typeinfo_zero/cross_file_struct_typeinfo_zero.gs.ts +++ b/tests/tests/cross_file_struct_typeinfo_zero/cross_file_struct_typeinfo_zero.gs.ts @@ -4,13 +4,18 @@ import * as $ from "@goscript/builtin/index.js" import * as __goscript_a from "./a.gs.ts" import * as __goscript_b from "./b.gs.ts" +import * as __goscript_c from "./c.gs.ts" +import * as __goscript_package_init from "./__goscript_package_init.gs.ts" export async function main(): globalThis.Promise { let a = __goscript_a.makeA() let b = __goscript_b.makeB() await $.println("ok:", a.next == null, b.inner.next == null) + await $.println("table:", __goscript_b.countC(), $.plainSliceIndex(__goscript_c.__goscript_get_cCopy()!, 1).inner.next == null) } +await __goscript_package_init.__goscript_package_init() + if ($.isMainScript(import.meta)) { await main() } diff --git a/tests/tests/cross_file_struct_typeinfo_zero/expected.log b/tests/tests/cross_file_struct_typeinfo_zero/expected.log index 9da6a3b67..f89b37d92 100644 --- a/tests/tests/cross_file_struct_typeinfo_zero/expected.log +++ b/tests/tests/cross_file_struct_typeinfo_zero/expected.log @@ -1 +1,2 @@ ok: true true +table: 2 true diff --git a/tests/tests/cross_file_struct_typeinfo_zero/index.ts b/tests/tests/cross_file_struct_typeinfo_zero/index.ts index c85f3f33b..186776c42 100644 --- a/tests/tests/cross_file_struct_typeinfo_zero/index.ts +++ b/tests/tests/cross_file_struct_typeinfo_zero/index.ts @@ -1,2 +1,5 @@ export { aValue } from "./a.gs.ts" export { bValue } from "./b.gs.ts" +import "./c.gs.ts" +import { __goscript_package_init } from "./__goscript_package_init.gs.ts" +await __goscript_package_init() diff --git a/tests/tests/cross_file_struct_typeinfo_zero/tsconfig.json b/tests/tests/cross_file_struct_typeinfo_zero/tsconfig.json index f95220d67..2bbae1250 100644 --- a/tests/tests/cross_file_struct_typeinfo_zero/tsconfig.json +++ b/tests/tests/cross_file_struct_typeinfo_zero/tsconfig.json @@ -19,8 +19,10 @@ }, "extends": "../../../tests/tsconfig.base.json", "include": [ + "__goscript_package_init.gs.ts", "a.gs.ts", "b.gs.ts", + "c.gs.ts", "cross_file_struct_typeinfo_zero.gs.ts", "index.ts" ]