From fef7c0b5ed831dba70761ffb8aeafcfbd57d5974 Mon Sep 17 00:00:00 2001 From: Cold Fry Date: Thu, 9 Apr 2026 00:31:04 +0000 Subject: [PATCH 1/5] initial --- internal/luatest/labeled_loops_test.go | 146 +++++++++++ internal/luatest/map_set_forof_test.go | 340 +++++++++++++++++++++++++ 2 files changed, 486 insertions(+) create mode 100644 internal/luatest/labeled_loops_test.go create mode 100644 internal/luatest/map_set_forof_test.go diff --git a/internal/luatest/labeled_loops_test.go b/internal/luatest/labeled_loops_test.go new file mode 100644 index 0000000..9173001 --- /dev/null +++ b/internal/luatest/labeled_loops_test.go @@ -0,0 +1,146 @@ +package luatest + +import ( + "strings" + "testing" +) + +func TestLabeledBreak(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + body string + want string + }{ + { + "labeled break exits outer loop", + `let result = ""; + outer: for (let i = 0; i < 3; i++) { + for (let j = 0; j < 3; j++) { + if (j === 1) break outer; + result += i + "," + j + " "; + } + } + return result.trim();`, + `"0,0"`, + }, + { + "labeled break on while", + `let count = 0; + outer: while (true) { + let inner = 0; + while (true) { + inner++; + if (inner > 2) break outer; + } + count++; + } + return count;`, + `0`, + }, + { + "labeled break skips else branch", + `let result = 0; + outer: for (let i = 0; i < 5; i++) { + for (let j = 0; j < 5; j++) { + if (i * j > 6) break outer; + result += 1; + } + } + return result;`, + `14`, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + ExpectFunction(t, tc.body, tc.want, Opts{}) + }) + } +} + +func TestLabeledContinue(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + body string + want string + }{ + { + "labeled continue skips to outer loop", + `let result = ""; + outer: for (let i = 0; i < 3; i++) { + for (let j = 0; j < 3; j++) { + if (j === 1) continue outer; + result += i + "," + j + " "; + } + } + return result.trim();`, + `"0,0 1,0 2,0"`, + }, + { + "labeled continue on while loop", + `let sum = 0; + let i = 0; + outer: while (i < 3) { + i++; + let j = 0; + while (j < 3) { + j++; + if (j === 2) continue outer; + sum += j; + } + } + return sum;`, + `3`, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + ExpectFunction(t, tc.body, tc.want, Opts{}) + }) + } +} + +func TestLabeledStatement_NoBreakOrContinue(t *testing.T) { + t.Parallel() + // Label that isn't targeted by break/continue should just emit the inner statement + ExpectFunction(t, ` + let x = 0; + myLabel: for (let i = 0; i < 3; i++) { + x += i; + } + return x; + `, `3`, Opts{}) +} + +func TestLabeledStatement_Codegen(t *testing.T) { + t.Parallel() + // Verify labeled break emits goto labels + code := `export function __main() { + let result = 0; + outer: for (let i = 0; i < 3; i++) { + for (let j = 0; j < 3; j++) { + if (j === 1) break outer; + result += 1; + } + } + return result; + }` + results := TranspileTS(t, code, Opts{}) + for _, r := range results { + if strings.HasSuffix(r.FileName, "main.ts") { + if !strings.Contains(r.Lua, "::__break_outer::") { + t.Errorf("expected goto break label in output, got:\n%s", r.Lua) + } + if !strings.Contains(r.Lua, "goto __break_outer") { + t.Errorf("expected goto statement in output, got:\n%s", r.Lua) + } + } + } +} diff --git a/internal/luatest/map_set_forof_test.go b/internal/luatest/map_set_forof_test.go new file mode 100644 index 0000000..493a12c --- /dev/null +++ b/internal/luatest/map_set_forof_test.go @@ -0,0 +1,340 @@ +package luatest + +import ( + "testing" + + "github.com/realcoldfry/tslua/internal/transpiler" +) + +func TestOptimizedMapForOf(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + body string + want string + }{ + { + "for-of map entries", + `const m = new Map(); + m.set("a", 1); + m.set("b", 2); + let result = ""; + for (const [k, v] of m) { + result += k + v; + } + return result;`, + `"a1b2"`, + }, + { + "for-of map.entries()", + `const m = new Map(); + m.set("x", 10); + m.set("y", 20); + let result = ""; + for (const [k, v] of m.entries()) { + result += k + v; + } + return result;`, + `"x10y20"`, + }, + { + "for-of map.keys()", + `const m = new Map(); + m.set("a", 1); + m.set("b", 2); + const keys: string[] = []; + for (const k of m.keys()) { + keys.push(k); + } + return keys.join(",");`, + `"a,b"`, + }, + { + "for-of map.values()", + `const m = new Map(); + m.set("a", 1); + m.set("b", 2); + let sum = 0; + for (const v of m.values()) { + sum += v; + } + return sum;`, + `3`, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + ExpectFunction(t, tc.body, tc.want, Opts{}) + }) + } +} + +func TestOptimizedSetForOf(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + body string + want string + }{ + { + "for-of set", + `const s = new Set(); + s.add(1); + s.add(2); + s.add(3); + let sum = 0; + for (const v of s) { + sum += v; + } + return sum;`, + `6`, + }, + { + "for-of set.values()", + `const s = new Set(); + s.add("a"); + s.add("b"); + const vals: string[] = []; + for (const v of s.values()) { + vals.push(v); + } + return vals.join(",");`, + `"a,b"`, + }, + { + "for-of set.keys()", + `const s = new Set(); + s.add(10); + s.add(20); + let sum = 0; + for (const k of s.keys()) { + sum += k; + } + return sum;`, + `30`, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + ExpectFunction(t, tc.body, tc.want, Opts{}) + }) + } +} + +func TestOptimizedArrayEntriesForOf(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + body string + want string + }{ + { + "for-of array.entries() with index and value", + `const arr = ["a", "b", "c"]; + let result = ""; + for (const [i, v] of arr.entries()) { + result += i + ":" + v + " "; + } + return result.trim();`, + `"0:a 1:b 2:c"`, + }, + { + "for-of array.entries() index is 0-based", + `const arr = [10, 20, 30]; + let sum = 0; + for (const [i, v] of arr.entries()) { + sum += i; + } + return sum;`, + `3`, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + ExpectFunction(t, tc.body, tc.want, Opts{}) + }) + } +} + +// Optimized emit mode tests - these hit the tryOptimizedMapSetForOf +// and tryOptimizedArrayEntriesForOf code paths. + +var optimizedOpts = Opts{EmitMode: transpiler.EmitModeOptimized} + +func TestOptimizedEmit_MapForOf(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + body string + want string + }{ + { + "for-of map entries", + `const m = new Map(); + m.set("a", 1); + m.set("b", 2); + let result = ""; + for (const [k, v] of m) { + result += k + v; + } + return result;`, + `"a1b2"`, + }, + { + "for-of map.entries()", + `const m = new Map(); + m.set("x", 10); + m.set("y", 20); + let result = ""; + for (const [k, v] of m.entries()) { + result += k + v; + } + return result;`, + `"x10y20"`, + }, + { + "for-of map.keys()", + `const m = new Map(); + m.set("a", 1); + m.set("b", 2); + const keys: string[] = []; + for (const k of m.keys()) { + keys.push(k); + } + return keys.join(",");`, + `"a,b"`, + }, + { + "for-of map.values()", + `const m = new Map(); + m.set("a", 1); + m.set("b", 2); + let sum = 0; + for (const v of m.values()) { + sum += v; + } + return sum;`, + `3`, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + ExpectFunction(t, tc.body, tc.want, optimizedOpts) + }) + } +} + +func TestOptimizedEmit_SetForOf(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + body string + want string + }{ + { + "for-of set", + `const s = new Set(); + s.add(1); + s.add(2); + s.add(3); + let sum = 0; + for (const v of s) { + sum += v; + } + return sum;`, + `6`, + }, + { + "for-of set.values()", + `const s = new Set(); + s.add("a"); + s.add("b"); + const vals: string[] = []; + for (const v of s.values()) { + vals.push(v); + } + return vals.join(",");`, + `"a,b"`, + }, + { + "for-of set.keys()", + `const s = new Set(); + s.add(10); + s.add(20); + let sum = 0; + for (const k of s.keys()) { + sum += k; + } + return sum;`, + `30`, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + ExpectFunction(t, tc.body, tc.want, optimizedOpts) + }) + } +} + +func TestOptimizedEmit_ArrayEntriesForOf(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + body string + want string + }{ + { + "for-of array.entries() with index and value", + `const arr = ["a", "b", "c"]; + let result = ""; + for (const [i, v] of arr.entries()) { + result += i + ":" + v + " "; + } + return result.trim();`, + `"0:a 1:b 2:c"`, + }, + { + "for-of array.entries() index is 0-based", + `const arr = [10, 20, 30]; + let sum = 0; + for (const [i, v] of arr.entries()) { + sum += i; + } + return sum;`, + `3`, + }, + { + "for-of array.entries() value only (unused index)", + `const arr = [10, 20, 30]; + let sum = 0; + for (const [, v] of arr.entries()) { + sum += v; + } + return sum;`, + `60`, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + ExpectFunction(t, tc.body, tc.want, optimizedOpts) + }) + } +} From 571d1cdde79a313fc2931fc9f6137d263c0fb6fd Mon Sep 17 00:00:00 2001 From: Cold Fry Date: Thu, 9 Apr 2026 01:05:33 +0000 Subject: [PATCH 2/5] _ --- .github/workflows/ci.yml | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 92223b2..ab4c7c4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -74,7 +74,24 @@ jobs: - name: Tests run: | export PATH="$PWD/.lua-runtimes/bin:$PATH" - FORCE_COLOR=1 gotestsum --format short ./internal/transpiler/ ./internal/lua/... ./internal/tstltest/ -skip TestCodegen_ + FORCE_COLOR=1 gotestsum --format short -- \ + -coverpkg=./internal/transpiler/,./internal/lua/ \ + -coverprofile=coverage.out \ + -covermode=atomic \ + ./internal/transpiler/ ./internal/lua/... ./internal/luatest/ ./internal/tstltest/ \ + -skip TestCodegen_ + + - name: Coverage summary + run: | + echo '### Coverage' >> "$GITHUB_STEP_SUMMARY" + total=$(go tool cover -func=coverage.out | grep '^total:' | awk '{print $NF}') + echo "**Total: ${total}**" >> "$GITHUB_STEP_SUMMARY" + echo '' >> "$GITHUB_STEP_SUMMARY" + echo '| File | Coverage |' >> "$GITHUB_STEP_SUMMARY" + echo '|------|----------|' >> "$GITHUB_STEP_SUMMARY" + go tool cover -func=coverage.out | grep -oP 'internal/\S+\.go.*\d+\.\d+%' | \ + sed 's|internal/||' | \ + awk '{printf "| `%s` | %s |\n", $1, $NF}' >> "$GITHUB_STEP_SUMMARY" golangci-lint: runs-on: ubuntu-latest From 85796647000e3dfd34ec6887e170b7bc7e2c55cf Mon Sep 17 00:00:00 2001 From: Cold Fry Date: Thu, 9 Apr 2026 01:10:48 +0000 Subject: [PATCH 3/5] _ --- justfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/justfile b/justfile index 6479114..5fc43e9 100644 --- a/justfile +++ b/justfile @@ -71,7 +71,7 @@ bench-lua: # Run all 100%-passing Go tests testall: tstlgen - FORCE_COLOR=1 gotestsum --format short ./internal/transpiler/ ./internal/lua/... ./internal/tstltest/ -skip TestCodegen_ + FORCE_COLOR=1 gotestsum --format short ./internal/transpiler/ ./internal/lua/... ./internal/luatest/ ./internal/tstltest/ -skip TestCodegen_ # Format code (Go + TS) fmt: From a162158b40ff73b852e26f89abec82e8985f78c1 Mon Sep 17 00:00:00 2001 From: Cold Fry Date: Thu, 9 Apr 2026 01:18:55 +0000 Subject: [PATCH 4/5] _ --- .github/workflows/ci.yml | 4 +++- internal/luatest/lualib_transpile_test.go | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ab4c7c4..1ebeaeb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -52,7 +52,9 @@ jobs: go install gotest.tools/gotestsum@latest - name: Install npm dependencies - run: npm ci + run: | + npm ci + cd extern/tstl && npm ci - name: Cache Lua runtimes id: lua-cache diff --git a/internal/luatest/lualib_transpile_test.go b/internal/luatest/lualib_transpile_test.go index bc2ebb4..7ff4942 100644 --- a/internal/luatest/lualib_transpile_test.go +++ b/internal/luatest/lualib_transpile_test.go @@ -111,7 +111,8 @@ io.write(type(lb) .. ":" .. type(lb.Map)) for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { - luaCode := fmt.Sprintf(`package.path = %q .. "/?.lua;" .. package.path + luaCode := fmt.Sprintf(`package.loaded["lualib_bundle"] = nil +package.path = %q .. "/?.lua;" .. package.path %s`, tmpDir, tc.lua) if e, ok := Evaluators[luaRuntime]; ok { From a563ce44316faa4169a1f3b8943610893391eacd Mon Sep 17 00:00:00 2001 From: Cold Fry Date: Thu, 9 Apr 2026 01:33:52 +0000 Subject: [PATCH 5/5] codecov? --- .github/workflows/ci.yml | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1ebeaeb..937adb8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -83,17 +83,11 @@ jobs: ./internal/transpiler/ ./internal/lua/... ./internal/luatest/ ./internal/tstltest/ \ -skip TestCodegen_ - - name: Coverage summary - run: | - echo '### Coverage' >> "$GITHUB_STEP_SUMMARY" - total=$(go tool cover -func=coverage.out | grep '^total:' | awk '{print $NF}') - echo "**Total: ${total}**" >> "$GITHUB_STEP_SUMMARY" - echo '' >> "$GITHUB_STEP_SUMMARY" - echo '| File | Coverage |' >> "$GITHUB_STEP_SUMMARY" - echo '|------|----------|' >> "$GITHUB_STEP_SUMMARY" - go tool cover -func=coverage.out | grep -oP 'internal/\S+\.go.*\d+\.\d+%' | \ - sed 's|internal/||' | \ - awk '{printf "| `%s` | %s |\n", $1, $NF}' >> "$GITHUB_STEP_SUMMARY" + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: coverage.out golangci-lint: runs-on: ubuntu-latest