diff --git a/tslang/lib/TypeScript/MLIRGenImpl.h b/tslang/lib/TypeScript/MLIRGenImpl.h index f6b854957..9b8e88f9d 100644 --- a/tslang/lib/TypeScript/MLIRGenImpl.h +++ b/tslang/lib/TypeScript/MLIRGenImpl.h @@ -1232,7 +1232,10 @@ class MLIRGenImpl if (genContext->usingVars != nullptr) { - for (auto vi : *genContext->usingVars) + // reverse declaration order, as JS disposes a scope (and as mlirGenReleaseOwned releases + // one): a later `using` may still need an earlier one while its `[Symbol.dispose]()` runs. + // The unwind cleanup region comes through here too, so it unwinds in the same order. + for (auto vi : llvm::reverse(*genContext->usingVars)) { auto varInTable = symbolTable.lookup(vi->getName()); if (!varInTable.first) diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index a26bc0ba6..092ff24c6 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -335,6 +335,7 @@ tslang_add_test(NAME test-compile-00-break-continue COMMAND test-runner "${PROJE tslang_add_test(NAME test-compile-00-label-chain-loop COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00label_chain_loop.ts") tslang_add_test(NAME test-compile-00-labelled-scope-exit COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00labelled_scope_exit.ts") tslang_add_test(NAME test-compile-00-switch-case-scope-exit COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00switch_case_scope_exit.ts") +tslang_add_test(NAME test-compile-00-using-dispose-order COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00using_dispose_order.ts") tslang_add_test(NAME test-compile-00-vars COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00vars.ts") tslang_add_test(NAME test-compile-00-var-bindings COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00var_bindings.ts") tslang_add_test(NAME test-compile-00-globals COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00globals.ts") @@ -876,6 +877,7 @@ tslang_add_test(NAME test-jit-00-break-continue COMMAND test-runner -jit "${PROJ tslang_add_test(NAME test-jit-00-label-chain-loop COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00label_chain_loop.ts") tslang_add_test(NAME test-jit-00-labelled-scope-exit COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00labelled_scope_exit.ts") tslang_add_test(NAME test-jit-00-switch-case-scope-exit COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00switch_case_scope_exit.ts") +tslang_add_test(NAME test-jit-00-using-dispose-order COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00using_dispose_order.ts") tslang_add_test(NAME test-jit-00-vars COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00vars.ts") tslang_add_test(NAME test-jit-00-var-bindings COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00var_bindings.ts") tslang_add_test(NAME test-jit-00-globals COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00globals.ts") @@ -1828,6 +1830,7 @@ set(TSLANG_CORPUS 00label_chain_loop.ts 00labelled_scope_exit.ts 00switch_case_scope_exit.ts + 00using_dispose_order.ts 00capture_in_new_arguments.ts 00cast_to_f32.ts 00catch_untyped_value.ts diff --git a/tslang/test/tester/tests/00switch_case_scope_exit.ts b/tslang/test/tester/tests/00switch_case_scope_exit.ts index ea5aa6206..c609538c6 100644 --- a/tslang/test/tester/tests/00switch_case_scope_exit.ts +++ b/tslang/test/tester/tests/00switch_case_scope_exit.ts @@ -47,6 +47,22 @@ function fallThrough(i: number) { trail += "after;"; } +// the cases share the switch's scope, so a `using` in each of two cases are two `using`s in one +// scope: disposed in reverse declaration order, the one the path skipped not at all +function usingInTwoCases(i: number) { + switch (i) { + case 0: + using a = new Res("a"); + trail += "body0;"; + case 1: + using b = new Res("b"); + trail += "body1;"; + break; + } + + trail += "after;"; +} + function bracedCases(i: number) { switch (i) { case 0: { @@ -178,6 +194,8 @@ function main() { check(() => caseWithBreak(5), "after;", "no case matched"); check(() => fallThrough(0), "body0;body1;a;after;", "falling through"); check(() => fallThrough(7), "default;after;", "default"); + check(() => usingInTwoCases(0), "body0;body1;b;a;after;", "a using in each of two cases"); + check(() => usingInTwoCases(1), "body1;b;after;", "a using in each of two cases, first skipped"); check(() => bracedCases(0), "body0;a;body1;b;after;", "braced cases falling through"); check(() => breakFromIf(0), "a;after;fn;", "break from an if in a case"); check(() => breakFromIf(1), "body1;after;fn;", "break from an if, case not taken"); diff --git a/tslang/test/tester/tests/00using_dispose_order.ts b/tslang/test/tester/tests/00using_dispose_order.ts new file mode 100644 index 000000000..bbc7c8f17 --- /dev/null +++ b/tslang/test/tester/tests/00using_dispose_order.ts @@ -0,0 +1,141 @@ +// The `using`s of one scope are disposed in reverse declaration order, as JS disposes them: the +// last one declared goes first, so a later resource may still use an earlier one while it is +// disposed. It used to be declaration order - `{ using a; using b; }` disposed `a` then `b`. +// +// The unwind cleanup region is generated from the same list, so a throw disposes in the same +// order as falling off the end, a `return`, a `break` or a `continue`. + +let trail = ""; + +class Res { + constructor(public name: string) {} + + [Symbol.dispose]() { + trail += this.name + ";"; + } +} + +function plainBlock() { + { + using a = new Res("a"); + using b = new Res("b"); + using c = new Res("c"); + } + + trail += "after;"; +} + +function functionBody() { + using a = new Res("a"); + using b = new Res("b"); + trail += "body;"; +} + +function earlyReturn(f: boolean) { + using a = new Res("a"); + { + using b = new Res("b"); + using c = new Res("c"); + if (f) { + return; + } + } + + trail += "after;"; +} + +function throwsInBlock() { + { + using a = new Res("a"); + using b = new Res("b"); + using c = new Res("c"); + throw 1; + } +} + +function throwsInTryBody() { + try { + using a = new Res("a"); + using b = new Res("b"); + throw 1; + } + catch (e: TypeOf<1>) { + trail += "caught;"; + } +} + +class Throws { + constructor(public name: string) {} + + [Symbol.dispose]() { + trail += this.name + ";"; + throw 1; + } +} + +// `b` is disposed first and throws; `a` is still disposed, by the cleanup the throw unwinds to +function throwingDispose() { + try { + using a = new Res("a"); + using b = new Throws("b"); + trail += "body;"; + } + catch (e: TypeOf<1>) { + trail += "caught;"; + } +} + +function loopBody() { + for (let i = 0; i < 2; i++) { + using a = new Res("a" + i); + using b = new Res("b" + i); + if (i == 0) { + continue; + } + + break; + } +} + +function switchCase(n: number) { + switch (n) { + case 1: + using a = new Res("a"); + using b = new Res("b"); + break; + } + + trail += "after;"; +} + +function expectThrow(f: () => void) { + try { + f(); + } + catch (e: TypeOf<1>) { + return true; + } + + return false; +} + +function check(f: () => void, expected: string, what: string) { + trail = ""; + f(); + print(what + ": " + trail); + assert(trail == expected, what); +} + +function main() { + check(plainBlock, "c;b;a;after;", "plain block"); + check(functionBody, "body;b;a;", "function body"); + check(() => earlyReturn(true), "c;b;a;", "return out of a nested block"); + check(() => earlyReturn(false), "c;b;after;a;", "falling off a nested block"); + check(() => assert(expectThrow(throwsInBlock), "must throw"), "c;b;a;", "throw out of a block"); + check(throwsInTryBody, "b;a;caught;", "throw out of a try body"); + check(throwingDispose, "body;b;a;caught;", "a dispose that throws"); + check(loopBody, "b0;a0;b1;a1;", "continue and break out of a loop body"); + check(() => switchCase(1), "b;a;after;", "break out of a switch case"); + + print("done."); +}