From c84efda6df7651a1d0bf53cd7e751704332c1e3f Mon Sep 17 00:00:00 2001 From: "Randall C. O'Reilly" Date: Sat, 24 Oct 2026 16:39:53 +0200 Subject: [PATCH] fix(compiler): alias shadowed names in switch init statements A short variable declaration in the init statement of a switch or type switch that redeclares a name its initializer reads, as in switch pos, err := d.consumeWhitespace(pos); err { lowered the new let declaration in the same TypeScript block as the initializer, so the initializer read the new, uninitialized binding and threw a ReferenceError. encoding/json/jsontext does this in checkEOF. If statements, for statements and plain blocks already save the outer value and rename the new binding (lowerShortDeclShadowAliases). Do the same for the init statements of switch and type switch statements, and add a compliance test covering each of these forms. Signed-off-by: Randall C. O'Reilly --- compiler/lowering.go | 32 ++++- tests/tests/shadow_define_init/expected.log | 6 + tests/tests/shadow_define_init/index.ts | 0 .../shadow_define_init/shadow_define_init.go | 69 +++++++++++ .../shadow_define_init.gs.ts | 114 ++++++++++++++++++ tests/tests/shadow_define_init/tsconfig.json | 25 ++++ 6 files changed, 241 insertions(+), 5 deletions(-) create mode 100644 tests/tests/shadow_define_init/expected.log create mode 100644 tests/tests/shadow_define_init/index.ts create mode 100644 tests/tests/shadow_define_init/shadow_define_init.go create mode 100644 tests/tests/shadow_define_init/shadow_define_init.gs.ts create mode 100644 tests/tests/shadow_define_init/tsconfig.json diff --git a/compiler/lowering.go b/compiler/lowering.go index 4e10c45fa..be0e078a7 100644 --- a/compiler/lowering.go +++ b/compiler/lowering.go @@ -9275,10 +9275,11 @@ const taglessSwitchValue = "(true as boolean)" func (o *LoweringOwner) lowerSwitchStmt(ctx lowerFileContext, stmt *ast.SwitchStmt) ([]loweredStmt, []Diagnostic) { var diagnostics []Diagnostic var init []loweredStmt + var prelude []loweredStmt if stmt.Init != nil { - lowered, initDiagnostics := o.lowerStmt(ctx, stmt.Init) + var initDiagnostics []Diagnostic + ctx, prelude, init, initDiagnostics = o.lowerSwitchInit(ctx, stmt.Init) diagnostics = append(diagnostics, initDiagnostics...) - init = append(init, lowered...) } value := taglessSwitchValue @@ -9361,7 +9362,25 @@ func (o *LoweringOwner) lowerSwitchStmt(ctx lowerFileContext, stmt *ast.SwitchSt return []loweredStmt{lowered}, diagnostics } init = append(init, lowered) - return []loweredStmt{{children: init}}, diagnostics + return append(prelude, loweredStmt{children: init}), diagnostics +} + +// lowerSwitchInit lowers the init statement of a switch or type switch, as +// lowerStmt does for an if init. A short declaration that redeclares a name +// its initializer reads, as in switch pos, err := f(pos), saves the outer +// value in prelude, to emit before the block of the switch. It returns the +// context for the rest of the switch, in which the new names are visible. +func (o *LoweringOwner) lowerSwitchInit( + ctx lowerFileContext, + init ast.Stmt, +) (lowerFileContext, []loweredStmt, []loweredStmt, []Diagnostic) { + initCtx := ctx + var prelude []loweredStmt + if stmtCtx, nextCtx, aliasPrelude, ok := o.lowerShortDeclStatementContext(ctx, init); ok { + initCtx, ctx, prelude = stmtCtx, nextCtx, aliasPrelude + } + lowered, diagnostics := o.lowerStmt(initCtx, init) + return ctx, prelude, lowered, diagnostics } // splitSwitchFallthrough returns the statements of a case body without its @@ -9429,8 +9448,11 @@ func constantComparableType(ctx lowerFileContext, expr ast.Expr) string { func (o *LoweringOwner) lowerTypeSwitchStmt(ctx lowerFileContext, stmt *ast.TypeSwitchStmt) ([]loweredStmt, []Diagnostic) { var lowered []loweredStmt var diagnostics []Diagnostic + var prelude []loweredStmt if stmt.Init != nil { - init, initDiagnostics := o.lowerStmt(ctx, stmt.Init) + var init []loweredStmt + var initDiagnostics []Diagnostic + ctx, prelude, init, initDiagnostics = o.lowerSwitchInit(ctx, stmt.Init) diagnostics = append(diagnostics, initDiagnostics...) lowered = append(lowered, init...) } @@ -9500,7 +9522,7 @@ func (o *LoweringOwner) lowerTypeSwitchStmt(ctx lowerFileContext, stmt *ast.Type switchIR.site = o.lowerTypeSwitchSite(ctx, switchIR) } lowered = append(lowered, loweredStmt{typeSwitch: switchIR}) - return lowered, diagnostics + return append(prelude, lowered...), diagnostics } // lowerTypeSwitchSite returns the hoisted runtime site of a type switch whose diff --git a/tests/tests/shadow_define_init/expected.log b/tests/tests/shadow_define_init/expected.log new file mode 100644 index 000000000..d97064f99 --- /dev/null +++ b/tests/tests/shadow_define_init/expected.log @@ -0,0 +1,6 @@ +switchInit: 2 +ifInit: 2 +block: 2 +single: 4 +forInit: 3 +typeSwitchInit: 5 diff --git a/tests/tests/shadow_define_init/index.ts b/tests/tests/shadow_define_init/index.ts new file mode 100644 index 000000000..e69de29bb diff --git a/tests/tests/shadow_define_init/shadow_define_init.go b/tests/tests/shadow_define_init/shadow_define_init.go new file mode 100644 index 000000000..138bba888 --- /dev/null +++ b/tests/tests/shadow_define_init/shadow_define_init.go @@ -0,0 +1,69 @@ +package main + +func next(n int) (int, error) { + return n + 1, nil +} + +// switchInit redeclares pos in a switch init from the outer pos. +func switchInit(pos int) int { + switch pos, err := next(pos); err { + case nil: + return pos + } + return -1 +} + +// ifInit redeclares pos in an if init from the outer pos. +func ifInit(pos int) int { + if pos, err := next(pos); err == nil { + return pos + } + return -1 +} + +// block redeclares pos in a nested block from the outer pos. +func block(pos int) int { + { + pos, err := next(pos) + if err == nil { + return pos + } + } + return -1 +} + +// single redeclares pos with a single value in a switch init. +func single(pos int) int { + switch pos := pos * 2; pos { + case 4: + return pos + } + return -1 +} + +// forInit redeclares i in a for init from the outer i. +func forInit(i int) int { + sum := 0 + for i := i; i < 3; i++ { + sum += i + } + return sum +} + +// typeSwitchInit redeclares v in a type switch init from the outer v. +func typeSwitchInit(v any) int { + switch v, ok := v.(int); any(ok).(type) { + case bool: + return v + } + return -1 +} + +func main() { + println("switchInit:", switchInit(1)) + println("ifInit:", ifInit(1)) + println("block:", block(1)) + println("single:", single(2)) + println("forInit:", forInit(1)) + println("typeSwitchInit:", typeSwitchInit(5)) +} diff --git a/tests/tests/shadow_define_init/shadow_define_init.gs.ts b/tests/tests/shadow_define_init/shadow_define_init.gs.ts new file mode 100644 index 000000000..b89c72bd2 --- /dev/null +++ b/tests/tests/shadow_define_init/shadow_define_init.gs.ts @@ -0,0 +1,114 @@ +// Generated file based on shadow_define_init.go +// Updated when compliance tests are re-run, DO NOT EDIT! + +import * as $ from "@goscript/builtin/index.js" + +export function next(n: number): [number, $.GoError] { + return [n + 1, null] +} + +export function switchInit(pos: number): number { + let __goscriptShadow0 = pos + { + let __goscriptTuple0: any = next(__goscriptShadow0) + let __goscriptShadow1: number = __goscriptTuple0[0] + let err: $.GoError = __goscriptTuple0[1] + let __goscriptSwitch0 = err + switch ((true as boolean)) { + case __goscriptSwitch0 == null: + { + return __goscriptShadow1 + break + } + } + } + return -1 +} + +export function ifInit(pos: number): number { + let __goscriptShadow2 = pos + { + let __goscriptTuple1: any = next(__goscriptShadow2) + let __goscriptShadow3: number = __goscriptTuple1[0] + let err: $.GoError = __goscriptTuple1[1] + if (err == null) { + return __goscriptShadow3 + } + } + return -1 +} + +export function block(pos: number): number { + { + let __goscriptShadow4 = pos + let __goscriptTuple2: any = next(__goscriptShadow4) + let __goscriptShadow5: number = __goscriptTuple2[0] + let err: $.GoError = __goscriptTuple2[1] + if (err == null) { + return __goscriptShadow5 + } + } + return -1 +} + +export function single(pos: number): number { + let __goscriptShadow6 = pos + { + let __goscriptShadow7 = __goscriptShadow6 * 2 + switch (__goscriptShadow7) { + case 4: + { + return __goscriptShadow7 + break + } + } + } + return -1 +} + +export function forInit(i: number): number { + let sum = 0 + { + let __goscriptShadow8 = i + for (let __goscriptShadow9 = __goscriptShadow8; __goscriptShadow9 < 3; __goscriptShadow9++) { + sum += __goscriptShadow9 + } + } + return sum +} + +export function typeSwitchInit(v: any): number { + let __goscriptShadow10 = v + let __goscriptTuple3: any = $.typeAssertTuple(__goscriptShadow10, /* @__PURE__ */ $.basicType("int")) + let __goscriptShadow11: number = __goscriptTuple3[0] + let ok: boolean = __goscriptTuple3[1] + { + const __goscriptTypeSwitchValue = ok + switch ($.typeSwitchCase(__goscriptTypeSwitchValue, __goscriptStatic0())) { + case 0: + { + return __goscriptShadow11 + } + break + } + } + return -1 +} + +export async function main(): globalThis.Promise { + await $.println("switchInit:", switchInit(1)) + await $.println("ifInit:", ifInit(1)) + await $.println("block:", block(1)) + await $.println("single:", single(2)) + await $.println("forInit:", forInit(1)) + await $.println("typeSwitchInit:", typeSwitchInit($.basicInterfaceValue(5, "int"))) +} + +var __goscriptStatic0$value: any +function __goscriptStatic0(): any { + return __goscriptStatic0$value ??= ($.typeSwitchSite([/* @__PURE__ */ $.basicType("bool")])) +} + +if ($.isMainScript(import.meta)) { + await main() +} diff --git a/tests/tests/shadow_define_init/tsconfig.json b/tests/tests/shadow_define_init/tsconfig.json new file mode 100644 index 000000000..4cbc5f6eb --- /dev/null +++ b/tests/tests/shadow_define_init/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "paths": { + "*": [ + "./*" + ], + "@go/github.com/s4wave/goscript/*": [ + "../../../*" + ], + "@goscript/*": [ + "../../../tests/tests/shadow_define_init/run/output/@goscript/*", + "../../../gs/*", + "../../../tests/deps/*" + ], + "@goscript/github.com/s4wave/goscript/tests/tests/shadow_define_init/*": [ + "./*" + ] + } + }, + "extends": "../../../tests/tsconfig.base.json", + "include": [ + "index.ts", + "shadow_define_init.gs.ts" + ] +}