diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 92223b2..937adb8 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 @@ -74,7 +76,18 @@ 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: Upload coverage to Codecov + uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: coverage.out golangci-lint: runs-on: ubuntu-latest 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/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 { 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) + }) + } +} 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: