From 682eb4127c6eb218d7da53e6e8da4aad04271bcd Mon Sep 17 00:00:00 2001 From: degory Date: Mon, 4 Mar 2024 10:27:44 +0100 Subject: [PATCH] Fix exception handler and scoped disposal issues Bugs fixed: - Returning from finally doesn't work reliably (closes #1100) - Scoped disposal within an exception handler generates incorrect IL (closes #1101) --- .config/dotnet-tools.json | 2 +- Directory.Build.props | 2 +- .../execution/let-use-3/.vscode/tasks.json | 23 + .../execution/let-use-3/err.expected | 0 .../execution/let-use-3/file.txt | 1 + .../execution/let-use-3/ghul.json | 6 + .../execution/let-use-3/ghulflags | 1 + .../execution/let-use-3/il.expected | 0 .../execution/let-use-3/run.expected | 107 +++++ .../execution/let-use-3/test.ghul | 428 ++++++++++++++++++ .../execution/let-use-3/warn.expected | 0 .../execution/try-return/run.expected | 36 +- .../execution/try-return/test.ghul | 142 ++++-- .../execution/try-return/warn.expected | 1 - .../il/empty-try-finally/il.expected | 6 +- .../il/empty-try-finally/test.ghul | 8 - .../il/try-break-continue/il.expected | 10 +- .../il/try-break-continue/test.ghul | 8 - .../il/try-catch-exception/il.expected | 10 +- .../try-catch-exceptions-finally/il.expected | 14 +- .../il/try-catch-exceptions-finally/test.ghul | 8 - .../il/try-catch-exceptions/il.expected | 14 +- .../il/try-catch-exceptions/test.ghul | 8 - integration-tests/il/try-finally/il.expected | 6 +- integration-tests/il/try-finally/test.ghul | 10 - .../parse/incomplete-function-3/il.expected | 0 .../.vscode/tasks.json | 23 + .../namespace-and-globals-1/err.expected | 1 + .../namespace-and-globals-1/fail.expected | 1 + .../parse/namespace-and-globals-1/ghul.json | 6 + .../parse/namespace-and-globals-1/ghulflags | 1 + .../parse/namespace-and-globals-1/il.expected | 0 .../parse/namespace-and-globals-1/test.ghul | 9 + .../namespace-and-globals-1/warn.expected | 0 .../.vscode/tasks.json | 23 + .../namespace-and-globals-2/err.expected | 1 + .../namespace-and-globals-2/fail.expected | 1 + .../parse/namespace-and-globals-2/ghul.json | 6 + .../parse/namespace-and-globals-2/ghulflags | 1 + .../parse/namespace-and-globals-2/il.expected | 0 .../parse/namespace-and-globals-2/test.ghul | 11 + .../namespace-and-globals-2/warn.expected | 0 src/syntax/process/generate_il.ghul | 128 +++--- 43 files changed, 876 insertions(+), 187 deletions(-) create mode 100644 integration-tests/execution/let-use-3/.vscode/tasks.json create mode 100644 integration-tests/execution/let-use-3/err.expected create mode 100644 integration-tests/execution/let-use-3/file.txt create mode 100644 integration-tests/execution/let-use-3/ghul.json create mode 100644 integration-tests/execution/let-use-3/ghulflags create mode 100644 integration-tests/execution/let-use-3/il.expected create mode 100644 integration-tests/execution/let-use-3/run.expected create mode 100644 integration-tests/execution/let-use-3/test.ghul create mode 100644 integration-tests/execution/let-use-3/warn.expected create mode 100644 integration-tests/parse/incomplete-function-3/il.expected create mode 100644 integration-tests/parse/namespace-and-globals-1/.vscode/tasks.json create mode 100644 integration-tests/parse/namespace-and-globals-1/err.expected create mode 100644 integration-tests/parse/namespace-and-globals-1/fail.expected create mode 100644 integration-tests/parse/namespace-and-globals-1/ghul.json create mode 100644 integration-tests/parse/namespace-and-globals-1/ghulflags create mode 100644 integration-tests/parse/namespace-and-globals-1/il.expected create mode 100644 integration-tests/parse/namespace-and-globals-1/test.ghul create mode 100644 integration-tests/parse/namespace-and-globals-1/warn.expected create mode 100644 integration-tests/parse/namespace-and-globals-2/.vscode/tasks.json create mode 100644 integration-tests/parse/namespace-and-globals-2/err.expected create mode 100644 integration-tests/parse/namespace-and-globals-2/fail.expected create mode 100644 integration-tests/parse/namespace-and-globals-2/ghul.json create mode 100644 integration-tests/parse/namespace-and-globals-2/ghulflags create mode 100644 integration-tests/parse/namespace-and-globals-2/il.expected create mode 100644 integration-tests/parse/namespace-and-globals-2/test.ghul create mode 100644 integration-tests/parse/namespace-and-globals-2/warn.expected diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index b4dcde713..d30317ef7 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -15,7 +15,7 @@ ] }, "ghul.compiler": { - "version": "0.8.8", + "version": "0.8.9", "commands": [ "ghul-compiler" ] diff --git a/Directory.Build.props b/Directory.Build.props index 7d7d57cde..095ab68bb 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,6 +1,6 @@ - 0.8.9-alpha.2 + 0.8.10-alpha.10 $(NoWarn);NU1507 diff --git a/integration-tests/execution/let-use-3/.vscode/tasks.json b/integration-tests/execution/let-use-3/.vscode/tasks.json new file mode 100644 index 000000000..49063613c --- /dev/null +++ b/integration-tests/execution/let-use-3/.vscode/tasks.json @@ -0,0 +1,23 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "Run test", + "command": "dotnet ghul-test \"${workspaceFolder}\"", + "type": "shell", + "group": { + "kind": "test", + "isDefault": true + } + }, + { + "label": "Capture test expectation", + "command": "../../../tasks/capture.sh \"${workspaceFolder}\"", + "type": "shell", + "group": { + "kind": "build", + "isDefault": true + } + } + ] +} \ No newline at end of file diff --git a/integration-tests/execution/let-use-3/err.expected b/integration-tests/execution/let-use-3/err.expected new file mode 100644 index 000000000..e69de29bb diff --git a/integration-tests/execution/let-use-3/file.txt b/integration-tests/execution/let-use-3/file.txt new file mode 100644 index 000000000..c57eff55e --- /dev/null +++ b/integration-tests/execution/let-use-3/file.txt @@ -0,0 +1 @@ +Hello World! \ No newline at end of file diff --git a/integration-tests/execution/let-use-3/ghul.json b/integration-tests/execution/let-use-3/ghul.json new file mode 100644 index 000000000..84539d122 --- /dev/null +++ b/integration-tests/execution/let-use-3/ghul.json @@ -0,0 +1,6 @@ +{ + "compiler": "dotnet ../../../publish/ghul.dll", + "source": [ + "." + ] +} diff --git a/integration-tests/execution/let-use-3/ghulflags b/integration-tests/execution/let-use-3/ghulflags new file mode 100644 index 000000000..489577f22 --- /dev/null +++ b/integration-tests/execution/let-use-3/ghulflags @@ -0,0 +1 @@ +--dotnet \ No newline at end of file diff --git a/integration-tests/execution/let-use-3/il.expected b/integration-tests/execution/let-use-3/il.expected new file mode 100644 index 000000000..e69de29bb diff --git a/integration-tests/execution/let-use-3/run.expected b/integration-tests/execution/let-use-3/run.expected new file mode 100644 index 000000000..38accefde --- /dev/null +++ b/integration-tests/execution/let-use-3/run.expected @@ -0,0 +1,107 @@ + +testing let use in try +Check created +Hello World! +finished let use inside try. expect dispose after +>>> expect dispose after +disposing Check... +disposed Check +<<< expect dispose before. finished let use in try +test_let_use_in_try done + +testing let use inside catch clause +in catch have exception 'expected' +Check created +Hello World! +finished let use inside catch clause +>>> expect dispose after +disposing Check... +disposed Check +<<< expect dispose before +test_let_use_in_catch done + +testing let use inside finally without throw +no exception +in finally +Check created +Hello World! +finished let use inside finally clause. expect dispose after +>>> expect dispose after +disposing Check... +disposed Check +<<< expect dispose before. +test_let_use_in_finally_no_throw done + +testing let use inside finally clause with throw +in finally +Check created +Hello World! +finished let use inside finally clause with throw. expect dispose after +>>> expect dispose after +disposing Check... +disposed Check +expected: caught exception + +testing let use inside try in catch clause +in inner catch have exception 'Exception of type 'System.Exception' was thrown.' +Check created +Hello World! +finished let use inside try in catch clause. expect dispose after +>>> expect dispose after +disposing Check... +disposed Check +<<< expect dispose before. in outer finally +test_let_use_in_catch_inside_try done + +testing let use inside catch in catch clause +in outer catch have exception 'outer expected' +in inner catch have exception 'inner expected' +Check created +Hello World! +finished let use inside catch in catch clause. expect dispose after +>>> expect dispose after +disposing Check... +disposed Check +<<< expect dispose before. in outer finally +test_let_use_in_catch_inside_catch done + +testing let use inside catch in finally clause +in inner catch have exception 'inner expected' +Check created +Hello World! +finished let use inside catch in finally clause. expect dispose after +>>> expect dispose after +disposing Check... +disposed Check +<<< expect dispose before. in outer finally +expected: caught exception + +testing return from catch with let use +Check created +Hello World! +finished let use inside catch. expect dispose after +>>> expect dispose after +disposing Check... +disposed Check +<<< expect dispose before. finished return from catch with let use +test_return_from_catch_with_let_use done + +testing return from try with let use +Check created +Hello World! +finished let use inside try. expect dispose after +>>> expect dispose after +disposing Check... +disposed Check +<<< expect dispose before. finished return from try with let use +test_return_from_try_with_let_use done + +testing return from finally with let use +no exception +Check created +Hello World! +finished let use inside finally in finally clause. expect dispose after +>>> expect dispose after +disposing Check... +disposed Check +done diff --git a/integration-tests/execution/let-use-3/test.ghul b/integration-tests/execution/let-use-3/test.ghul new file mode 100644 index 000000000..9996dc54a --- /dev/null +++ b/integration-tests/execution/let-use-3/test.ghul @@ -0,0 +1,428 @@ +use IO.Std.write_line; + +// check we can call dispose() on Disposable structs +// both manually and using the let use pattern +// disposable structs not a particularly good idea, +// but they should work if they are used correctly +// i.e. created on the stack and never copied + +entry() is + test_let_use_in_try(); + test_let_use_in_catch(); + + test_let_use_in_finally_no_throw(); + + try + test_let_use_in_finally_with_throw(); + catch ex: System.Exception + write_line("expected: caught exception"); + yrt + + test_let_use_in_catch_inside_try(); + test_let_use_in_catch_inside_catch(); + + try + test_let_use_in_catch_inside_finally(); + catch ex: System.Exception + write_line("expected: caught exception"); + yrt + + assert test_return_from_catch_with_let_use() =~ "test_return_from_catch_with_let_use done"; + assert test_return_from_try_with_let_use() =~ "test_return_from_try_with_let_use done"; + assert test_return_from_finally_with_let_use() =~ "test_return_from_finally_with_let_use done"; + + write_line("done"); +si + +test_let_use_in_try() is + write_line(); + write_line("testing let use in try"); + try + let use file = IO.File.open_read("file.txt"); + let use reader = new IO.StreamReader(file); + let use check = new Check(); + + let line = reader.read_line(); + while line? do + write_line(line); + line = reader.read_line(); + od + + write_line("finished let use inside try. expect dispose after"); + write_line(">>> expect dispose after"); + finally + write_line("<<< expect dispose before. finished let use in try"); + yrt + + write_line("test_let_use_in_try done"); +si + +test_let_use_in_catch() is + write_line(); + write_line("testing let use inside catch clause"); + try + throw new System.Exception("expected"); + catch ex: System.Exception + write_line("in catch have exception '{ex.message}'"); + let use file = IO.File.open_read("file.txt"); + let use reader = new IO.StreamReader(file); + let use check = new Check(); + + let line = reader.read_line(); + + while line? do + write_line(line); + line = reader.read_line(); + od + + write_line("finished let use inside catch clause"); + write_line(">>> expect dispose after"); + yrt + + write_line("<<< expect dispose before"); + write_line("test_let_use_in_catch done"); +si + +test_let_use_in_finally_no_throw() is + write_line(); + write_line("testing let use inside finally without throw"); + try + write_line("no exception"); + finally + write_line("in finally"); + + let use file = IO.File.open_read("file.txt"); + let use reader = new IO.StreamReader(file); + let use check = new Check(); + + let line = reader.read_line(); + while line? do + write_line(line); + line = reader.read_line(); + od + + write_line("finished let use inside finally clause. expect dispose after"); + write_line(">>> expect dispose after"); + yrt + + write_line("<<< expect dispose before."); + write_line("test_let_use_in_finally_no_throw done"); +si + +test_let_use_in_finally_with_throw() is + write_line(); + write_line("testing let use inside finally clause with throw"); + try + throw new System.Exception("expected"); + finally + write_line("in finally"); + + let use file = IO.File.open_read("file.txt"); + let use reader = new IO.StreamReader(file); + let use check = new Check(); + + let line = reader.read_line(); + while line? do + write_line(line); + line = reader.read_line(); + od + + write_line("finished let use inside finally clause with throw. expect dispose after"); + write_line(">>> expect dispose after"); + yrt + + write_line("<<< expect dispose before"); + write_line("test_let_use_in_finally_with_throw done"); +si + +test_let_use_in_catch_inside_try() is + write_line(); + write_line("testing let use inside try in catch clause"); + + try + try + throw new System.Exception(); + catch ex: System.Exception + write_line("in inner catch have exception '{ex.message}'"); + + let use file = IO.File.open_read("file.txt"); + let use reader = new IO.StreamReader(file); + let use check = new Check(); + + let line = reader.read_line(); + while line? do + write_line(line); + line = reader.read_line(); + od + + write_line("finished let use inside try in catch clause. expect dispose after"); + write_line(">>> expect dispose after"); + yrt + finally + write_line("<<< expect dispose before. in outer finally"); + yrt + + write_line("test_let_use_in_catch_inside_try done"); +si + +test_let_use_in_catch_inside_catch() is + write_line(); + write_line("testing let use inside catch in catch clause"); + + try + throw new System.Exception("outer expected"); + catch ex: System.Exception + write_line("in outer catch have exception '{ex.message}'"); + + try + throw new System.Exception("inner expected"); + catch ex: System.Exception + write_line("in inner catch have exception '{ex.message}'"); + + let use file = IO.File.open_read("file.txt"); + let use reader = new IO.StreamReader(file); + let use check = new Check(); + + let line = reader.read_line(); + while line? do + write_line(line); + line = reader.read_line(); + od + + write_line("finished let use inside catch in catch clause. expect dispose after"); + write_line(">>> expect dispose after"); + yrt + finally + write_line("<<< expect dispose before. in outer finally"); + yrt + + write_line("test_let_use_in_catch_inside_catch done"); +si + +test_let_use_in_catch_inside_finally() is + write_line(); + write_line("testing let use inside catch in finally clause"); + + try + throw new System.Exception("outer expected"); + finally + try + throw new System.Exception("inner expected"); + catch ex: System.Exception + write_line("in inner catch have exception '{ex.message}'"); + + let use file = IO.File.open_read("file.txt"); + let use reader = new IO.StreamReader(file); + let use check = new Check(); + + let line = reader.read_line(); + while line? do + write_line(line); + line = reader.read_line(); + od + + write_line("finished let use inside catch in finally clause. expect dispose after"); + write_line(">>> expect dispose after"); + yrt + + write_line("<<< expect dispose before. in outer finally"); + yrt + + write_line("test_let_use_in_catch_inside_finally done"); +si + +test_let_use_in_finally_in_finally() is + write_line(); + write_line("testing let use inside finally in finally clause"); + + try + write_line("no exception"); + finally + try + write_line("in inner finally"); + finally + let use file = IO.File.open_read("file.txt"); + let use reader = new IO.StreamReader(file); + let use check = new Check(); + + let line = reader.read_line(); + while line? do + write_line(line); + line = reader.read_line(); + od + + write_line("finished let use inside finally in finally clause. expect dispose after"); + write_line(">>> expect dispose after"); + yrt + + write_line("<<< expect dispose before. in outer finally"); + yrt + + write_line("test_let_use_in_finally_in_finally done"); +si + +test_return_from_try_with_let_use() -> string is + write_line(); + write_line("testing return from try with let use"); + + try + let use file = IO.File.open_read("file.txt"); + let use reader = new IO.StreamReader(file); + let use check = new Check(); + + let line = reader.read_line(); + while line? do + write_line(line); + line = reader.read_line(); + od + + write_line("finished let use inside try. expect dispose after"); + write_line(">>> expect dispose after"); + + return "test_return_from_try_with_let_use done"; + + throw new System.Exception("not expected: should have returned A"); + catch ex: System.Exception + write_line("not expected: caught exception"); + finally + write_line("<<< expect dispose before. finished return from try with let use"); + write_line("test_return_from_try_with_let_use done"); + yrt + + throw new System.Exception("not expected: should have returned B"); +si + +test_return_from_catch_with_let_use() -> string is + write_line(); + write_line("testing return from catch with let use"); + + try + throw new System.Exception(); + catch ex: System.Exception + let use file = IO.File.open_read("file.txt"); + let use reader = new IO.StreamReader(file); + let use check = new Check(); + + let line = reader.read_line(); + while line? do + write_line(line); + line = reader.read_line(); + od + + write_line("finished let use inside catch. expect dispose after"); + write_line(">>> expect dispose after"); + + return "test_return_from_catch_with_let_use done"; + + throw new System.Exception("not expected: should have returned A"); + finally + write_line("<<< expect dispose before. finished return from catch with let use"); + write_line("test_return_from_catch_with_let_use done"); + yrt + + throw new System.Exception("not expected: should have returned B"); +si + + +test_return_from_finally_with_let_use() -> string is + write_line(); + write_line("testing return from finally with let use"); + + try + write_line("no exception"); + finally + let use file = IO.File.open_read("file.txt"); + let use reader = new IO.StreamReader(file); + let use check = new Check(); + + let line = reader.read_line(); + while line? do + write_line(line); + line = reader.read_line(); + od + + write_line("finished let use inside finally in finally clause. expect dispose after"); + write_line(">>> expect dispose after"); + + return "test_return_from_finally_with_let_use done"; + yrt + + throw new System.Exception("not expected: should have returned"); +si + + +struct Check: Disposable is + _disposed: bool; + + init() is + write_line("Check created"); + si + + dispose() is + write_line("disposing Check..."); + + if _disposed then + write_line("Check already disposed"); + return; + fi + + _disposed = true; + write_line("disposed Check"); + si +si + +struct Test: Disposable is + _disposed: bool; + + _unmanged_resource: int ptr; + _managed_resource: IO.FileStream; + + init() is + _managed_resource = IO.File.open_read("file.txt"); + + write_line("Test created"); + si + + dispose() is + write_line("disposing Test..."); + _dispose(true); + write_line("suppresing finalize..."); + System.GC.suppress_finalize(self); + write_line("disposed Test"); + si + + finalize() is + write_line("finalizing Test..."); + _dispose(false); + write_line("finalized Test"); + si + + _dispose(disposing: bool) is + write_line("disposing Test: disposing {disposing} disposed {_disposed}..."); + + if _disposed then + write_line("already disposed"); + return; + fi + + if disposing then + write_line("disposing managed resource..."); + _managed_resource.close(); + fi + + if _unmanged_resource != null then + write_line("disposing unmanaged resource..."); + _free_unmanaged_resource(_unmanged_resource); + _unmanged_resource = null; + fi + + write_line("mark as disposed done"); + _disposed = true; + + write_line("disposing Test done"); + si + + _free_unmanaged_resource(resource: int ptr) is + // do nothing + si +si \ No newline at end of file diff --git a/integration-tests/execution/let-use-3/warn.expected b/integration-tests/execution/let-use-3/warn.expected new file mode 100644 index 000000000..e69de29bb diff --git a/integration-tests/execution/try-return/run.expected b/integration-tests/execution/try-return/run.expected index ebcd6133f..09c178749 100644 --- a/integration-tests/execution/try-return/run.expected +++ b/integration-tests/execution/try-return/run.expected @@ -1,16 +1,20 @@ -will return: 246 -finally: 123 -result 1 is: 246 -will return: 246 -finally: 123 -result 2 is: 0 -will return: 246 -finally: 123 -result 3 is: 369 -will return: 246 -finally: 123 -finally 1 -finally 2 -result 5 is: 123 -result 6 is: 666 -result 7 is: 999 +finally_should_run_after_return_value will return: 246 +finally_should_run_after_return_value finally: 123 +finally should return after return value: 246 +finally_should_run_after_return_default_value will return: 246 +finally_should_run_after_return_default_value finally: 123 +finally should run after return default value: 456 +return_from_finally_should_override_return_from_try_body will return: 246 +return_from_finally_should_override_return_from_try_body finally: 123 +return from finally should override return from try body: 369 +finally_should_run_after_return_void value is 246 will return void +finally_should_run_after_return_void finally: 123 +return_value_should_pass_through_nested_trys finally A +return_value_should_pass_through_nested_trys finally B +return value should pass through nested trys: 123 +return from catch: 666 +return from finally should override return from catch: 999 +return_from_finally_with_no_prior_return_should_return_supplied_value will return: 123 +return from finally with no prior return should return supplied value: 123 +return_from_finally_with_catch_but_no_prior_return_should_return_supplied_value caught: oops: 123 from return_from_finally_with_catch_but_no_prior_return_should_return_supplied_value +return from finally with catch but no prior return should return supplied value: 123 diff --git a/integration-tests/execution/try-return/test.ghul b/integration-tests/execution/try-return/test.ghul index 52b5a0d82..8706cfd2b 100644 --- a/integration-tests/execution/try-return/test.ghul +++ b/integration-tests/execution/try-return/test.ghul @@ -1,84 +1,141 @@ namespace Test is + use IO.Std; + use System.Exception; + class Main is + entry() static is + let finally_should_run_after_return_value_result = + finally_should_run_after_return_value(123); + Std.out.write_line( + "finally should return after return value: {finally_should_run_after_return_value_result}"); + assert finally_should_run_after_return_value_result == 246; + let finally_should_run_after_return_default_value_result = + finally_should_run_after_return_default_value(123); + Std.out.write_line( + "finally should run after return default value: {finally_should_run_after_return_default_value_result}"); + assert finally_should_run_after_return_default_value_result == 456; - use Std = IO.Std; + let return_from_finally_should_override_return_from_try_body_result = + return_from_finally_should_override_return_from_try_body(123); - use System.Exception; + Std.out.write_line( + "return from finally should override return from try body: {return_from_finally_should_override_return_from_try_body_result}"); + assert return_from_finally_should_override_return_from_try_body_result == 369; - class Main is - entry() static is - - let v1 = finally_should_run_after_return_value(123); - Std.out.write_line("result 1 is: " + v1); + finally_should_run_after_return_void(123); - let v2 = finally_should_run_after_return_default_value(123); - Std.out.write_line("result 2 is: " + v2); - let v3 = return_from_finally_should_override_return_from_try_body(123); - Std.out.write_line("result 3 is: " + v3); + let return_value_should_pass_through_nested_trys_result = + return_value_should_pass_through_nested_trys(); - finally_should_run_after_return_void(123); + Std.out.write_line( + "return value should pass through nested trys: {return_value_should_pass_through_nested_trys_result}"); + assert return_value_should_pass_through_nested_trys_result == 123; + + + let return_from_catch_result = + return_from_catch(333); + Std.out.write_line( + "return from catch: {return_from_catch_result}"); + assert return_from_catch_result == 666; - let v5 = return_value_should_pass_through_nested_trys(); - Std.out.write_line("result 5 is: " + v5); - let v6 = return_from_catch(333); - Std.out.write_line("result 6 is: " + v6); + let return_from_finally_should_override_return_from_catch_result = + return_from_finally_should_override_return_from_catch(333); - let v7 = return_from_finally_should_override_return_from_catch(333); - Std.out.write_line("result 7 is: " + v7); + Std.out.write_line( + "return from finally should override return from catch: {return_from_finally_should_override_return_from_catch_result}"); + assert return_from_finally_should_override_return_from_catch_result == 999; + + + let return_from_finally_with_no_prior_return_should_return_supplied_value_result = + return_from_finally_with_no_prior_return_should_return_supplied_value(123); + + Std.out.write_line( + "return from finally with no prior return should return supplied value: {return_from_finally_with_no_prior_return_should_return_supplied_value_result}"); + assert return_from_finally_with_no_prior_return_should_return_supplied_value_result == 123; + + + let return_from_finally_with_catch_but_no_prior_return_should_return_supplied_value_result = + return_from_finally_with_catch_but_no_prior_return_should_return_supplied_value(123); + + Std.out.write_line( + "return from finally with catch but no prior return should return supplied value: {return_from_finally_with_catch_but_no_prior_return_should_return_supplied_value_result}"); + assert return_from_finally_with_catch_but_no_prior_return_should_return_supplied_value_result == 123; si finally_should_run_after_return_value(value: int) -> int static is try - Std.out.write_line("will return: " + value * 2); + Std.out.write_line("finally_should_run_after_return_value will return: {value * 2}"); return value * 2; finally - Std.out.write_line("finally: " + value); + Std.out.write_line("finally_should_run_after_return_value finally: {value}"); yrt - throw new System.Exception("should not be reachable"); // not reachable + throw new System.Exception("finally_should_run_after_return_value should not be reachable"); // not reachable si finally_should_run_after_return_default_value(value: int) -> int static is try - Std.out.write_line("will return: " + value * 2); - return; + Std.out.write_line("finally_should_run_after_return_default_value will return: {value * 2}"); + return 456; finally - Std.out.write_line("finally: " + value); + Std.out.write_line("finally_should_run_after_return_default_value finally: {value}"); yrt - throw new System.Exception("should not be reachable"); // not reachable + throw new System.Exception("finally_should_run_after_return_default_value should not be reachable"); // not reachable + si + + return_from_finally_with_no_prior_return_should_return_supplied_value(value: int) -> int static is + try + Std.out.write_line("return_from_finally_with_no_prior_return_should_return_supplied_value will return: {value}"); + finally + return value; + yrt + + throw new System.Exception("return_from_finally_with_no_prior_return_should_return_supplied_value should not be reachable"); // not reachable + si + + return_from_finally_with_catch_but_no_prior_return_should_return_supplied_value(value: int) -> int static is + try + throw new Exception("oops: {value} from return_from_finally_with_catch_but_no_prior_return_should_return_supplied_value"); + catch ex: Exception + Std.out.write_line("return_from_finally_with_catch_but_no_prior_return_should_return_supplied_value caught: {ex.message}"); + finally + return value; + yrt + + throw new System.Exception("return_from_finally_with_catch_but_no_prior_return_should_return_supplied_value should not be reachable"); // not reachable si return_from_finally_should_override_return_from_try_body(value: int) -> int static is try - Std.out.write_line("will return: " + value * 2); + Std.out.write_line("return_from_finally_should_override_return_from_try_body will return: {value * 2}"); return value * 2; finally - Std.out.write_line("finally: " + value); + Std.out.write_line("return_from_finally_should_override_return_from_try_body finally: {value}"); return value * 3; yrt - throw new System.Exception("should not be reachable"); // not reachable + throw new System.Exception("return_from_finally_should_override_return_from_try_body should not be reachable"); // not reachable si finally_should_run_after_return_void(value: int) static is try - Std.out.write_line("will return: " + value * 2); + Std.out.write_line("finally_should_run_after_return_void value is {value * 2} will return void"); return; finally - Std.out.write_line("finally: " + value); + Std.out.write_line("finally_should_run_after_return_void finally: {value}"); yrt - throw new System.Exception("should not be reachable"); // not reachable + throw new System.Exception("finally_should_run_after_return_void should not be reachable"); // not reachable si return_value_should_pass_through_nested_trys() -> int static is @@ -86,39 +143,42 @@ namespace Test is try return 123; catch ex: Exception + Std.out.write_line("return_value_should_pass_through_nested_trys caught: {ex.message} A"); finally - Std.out.write_line("finally 1"); + Std.out.write_line("return_value_should_pass_through_nested_trys finally A"); yrt - - catch ex: Exception + Std.out.write_line("return_value_should_pass_through_nested_trys should not be reachable A"); + catch ex: Exception + Std.out.write_line("return_value_should_pass_through_nested_trys caught: {ex.message} B"); finally - Std.out.write_line("finally 2"); + Std.out.write_line("return_value_should_pass_through_nested_trys finally B"); yrt - - throw new System.Exception("should not be reachable"); // not reachable + + Std.out.write_line("return_value_should_pass_through_nested_trys should not be reachable B"); + throw new System.Exception("return_value_should_pass_through_nested_trys should not be reachable B"); // not reachable si return_from_catch(value: int) -> int static is try - throw new Exception("oops"); + throw new Exception("oops: {value} from return_from_catch"); catch ex: Exception return value * 2; yrt - throw new System.Exception("should not be reachable"); // not reachable + throw new System.Exception("return_from_catch should not be reachable"); // not reachable si return_from_finally_should_override_return_from_catch(value: int) -> int static is try - throw new Exception("oops"); + throw new Exception("oops: {value} from return_from_finally_should_override_return_from_catch"); catch ex: Exception return value * 2; finally return value * 3; yrt - throw new System.Exception("should not be reachable"); // not reachable + throw new System.Exception("return_from_finally_should_override_return_from_catch should not be reachable"); // not reachable si si si \ No newline at end of file diff --git a/integration-tests/execution/try-return/warn.expected b/integration-tests/execution/try-return/warn.expected index 9d6f334b4..e69de29bb 100644 --- a/integration-tests/execution/try-return/warn.expected +++ b/integration-tests/execution/try-return/warn.expected @@ -1 +0,0 @@ -test.ghul: 53,17..53,23: warn: return without value from non void function returns default value of type Ghul.int diff --git a/integration-tests/il/empty-try-finally/il.expected b/integration-tests/il/empty-try-finally/il.expected index 2954babe6..672fefdc1 100644 --- a/integration-tests/il/empty-try-finally/il.expected +++ b/integration-tests/il/empty-try-finally/il.expected @@ -1,13 +1,13 @@ -.locals init (bool '.temp.0.1') +.locals init (bool '.temp.0.1') .try { -leave L_2 +leave L_0 } finally { L_1: endfinally } +L_0: ldloc '.temp.0.1' brfalse L_2 -L_0: ret L_2: diff --git a/integration-tests/il/empty-try-finally/test.ghul b/integration-tests/il/empty-try-finally/test.ghul index 7c0bd2914..6e2745d26 100644 --- a/integration-tests/il/empty-try-finally/test.ghul +++ b/integration-tests/il/empty-try-finally/test.ghul @@ -1,12 +1,4 @@ namespace Test is - - - - - - - - use Std = IO.Std; class Main is diff --git a/integration-tests/il/try-break-continue/il.expected b/integration-tests/il/try-break-continue/il.expected index e8d08aba1..14ecec39e 100644 --- a/integration-tests/il/try-break-continue/il.expected +++ b/integration-tests/il/try-break-continue/il.expected @@ -1,16 +1,16 @@ -.locals init (bool '.temp.0.1') +.locals init (bool '.temp.0.1') .try { leave L_1 -leave L_4 +leave L_2 } -catch class ['System.Runtime']'System'.'Exception' { +catch class ['System.Runtime']'System'.'Exception' { .locals init (class ['System.Runtime']'System'.'Exception' 'ex.0') stloc 'ex.0' leave L_0 -leave L_4 +leave L_2 } +L_2: ldloc '.temp.0.1' brfalse L_4 -L_2: ret L_4: diff --git a/integration-tests/il/try-break-continue/test.ghul b/integration-tests/il/try-break-continue/test.ghul index 8e9f7844e..818b51544 100644 --- a/integration-tests/il/try-break-continue/test.ghul +++ b/integration-tests/il/try-break-continue/test.ghul @@ -1,12 +1,4 @@ namespace Test is - - - - - - - - use Std = IO.Std; use System.Exception; diff --git a/integration-tests/il/try-catch-exception/il.expected b/integration-tests/il/try-catch-exception/il.expected index 73b59c4ef..ab894ad2d 100644 --- a/integration-tests/il/try-catch-exception/il.expected +++ b/integration-tests/il/try-catch-exception/il.expected @@ -1,14 +1,14 @@ -.locals init (bool '.temp.0.1') +.locals init (bool '.temp.0.1') .try { -leave L_2 +leave L_0 } -catch class ['System.Runtime']'System'.'Exception' { +catch class ['System.Runtime']'System'.'Exception' { .locals init (class ['System.Runtime']'System'.'Exception' 'ex.0') stloc 'ex.0' -leave L_2 +leave L_0 } +L_0: ldloc '.temp.0.1' brfalse L_2 -L_0: ret L_2: diff --git a/integration-tests/il/try-catch-exceptions-finally/il.expected b/integration-tests/il/try-catch-exceptions-finally/il.expected index 72f317918..c4e1a8960 100644 --- a/integration-tests/il/try-catch-exceptions-finally/il.expected +++ b/integration-tests/il/try-catch-exceptions-finally/il.expected @@ -1,25 +1,25 @@ -.locals init (bool '.temp.0.1') +.locals init (bool '.temp.0.1') .try { .try { -leave L_2 +leave L_0 } -catch class ['System.Runtime']'System'.'Exception' { +catch class ['System.Runtime']'System'.'Exception' { .locals init (class ['System.Runtime']'System'.'Exception' 'ex.0') stloc 'ex.0' -leave L_2 +leave L_0 } -catch class 'Test'.'MyException' { +catch class 'Test'.'MyException' { .locals init (class 'Test'.'MyException' 'ex.1') stloc 'ex.1' -leave L_2 +leave L_0 } } finally { L_1: endfinally } +L_0: ldloc '.temp.0.1' brfalse L_2 -L_0: ret L_2: diff --git a/integration-tests/il/try-catch-exceptions-finally/test.ghul b/integration-tests/il/try-catch-exceptions-finally/test.ghul index b8e591316..b6c7b31e1 100644 --- a/integration-tests/il/try-catch-exceptions-finally/test.ghul +++ b/integration-tests/il/try-catch-exceptions-finally/test.ghul @@ -1,12 +1,4 @@ namespace Test is - - - - - - - - use Std = IO.Std; class Main is diff --git a/integration-tests/il/try-catch-exceptions/il.expected b/integration-tests/il/try-catch-exceptions/il.expected index b289c9b32..e8e956758 100644 --- a/integration-tests/il/try-catch-exceptions/il.expected +++ b/integration-tests/il/try-catch-exceptions/il.expected @@ -1,19 +1,19 @@ -.locals init (bool '.temp.0.1') +.locals init (bool '.temp.0.1') .try { -leave L_2 +leave L_0 } -catch class ['System.Runtime']'System'.'Exception' { +catch class ['System.Runtime']'System'.'Exception' { .locals init (class ['System.Runtime']'System'.'Exception' 'ex.0') stloc 'ex.0' -leave L_2 +leave L_0 } -catch class 'Test'.'MyException' { +catch class 'Test'.'MyException' { .locals init (class 'Test'.'MyException' 'ex.1') stloc 'ex.1' -leave L_2 +leave L_0 } +L_0: ldloc '.temp.0.1' brfalse L_2 -L_0: ret L_2: diff --git a/integration-tests/il/try-catch-exceptions/test.ghul b/integration-tests/il/try-catch-exceptions/test.ghul index dd54a2f9d..f2d039af7 100644 --- a/integration-tests/il/try-catch-exceptions/test.ghul +++ b/integration-tests/il/try-catch-exceptions/test.ghul @@ -1,12 +1,4 @@ namespace Test is - - - - - - - - use Std = IO.Std; class Main is diff --git a/integration-tests/il/try-finally/il.expected b/integration-tests/il/try-finally/il.expected index 2954babe6..672fefdc1 100644 --- a/integration-tests/il/try-finally/il.expected +++ b/integration-tests/il/try-finally/il.expected @@ -1,13 +1,13 @@ -.locals init (bool '.temp.0.1') +.locals init (bool '.temp.0.1') .try { -leave L_2 +leave L_0 } finally { L_1: endfinally } +L_0: ldloc '.temp.0.1' brfalse L_2 -L_0: ret L_2: diff --git a/integration-tests/il/try-finally/test.ghul b/integration-tests/il/try-finally/test.ghul index 7c0bd2914..e9b2e6cb9 100644 --- a/integration-tests/il/try-finally/test.ghul +++ b/integration-tests/il/try-finally/test.ghul @@ -1,18 +1,8 @@ namespace Test is - - - - - - - - use Std = IO.Std; class Main is entry() static is - - @IL.output("il.out") try finally diff --git a/integration-tests/parse/incomplete-function-3/il.expected b/integration-tests/parse/incomplete-function-3/il.expected new file mode 100644 index 000000000..e69de29bb diff --git a/integration-tests/parse/namespace-and-globals-1/.vscode/tasks.json b/integration-tests/parse/namespace-and-globals-1/.vscode/tasks.json new file mode 100644 index 000000000..49063613c --- /dev/null +++ b/integration-tests/parse/namespace-and-globals-1/.vscode/tasks.json @@ -0,0 +1,23 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "Run test", + "command": "dotnet ghul-test \"${workspaceFolder}\"", + "type": "shell", + "group": { + "kind": "test", + "isDefault": true + } + }, + { + "label": "Capture test expectation", + "command": "../../../tasks/capture.sh \"${workspaceFolder}\"", + "type": "shell", + "group": { + "kind": "build", + "isDefault": true + } + } + ] +} \ No newline at end of file diff --git a/integration-tests/parse/namespace-and-globals-1/err.expected b/integration-tests/parse/namespace-and-globals-1/err.expected new file mode 100644 index 000000000..d7e67348c --- /dev/null +++ b/integration-tests/parse/namespace-and-globals-1/err.expected @@ -0,0 +1 @@ +test.ghul: 7,1..9,3: error: cannot mix global definitions and namespaces in the same file diff --git a/integration-tests/parse/namespace-and-globals-1/fail.expected b/integration-tests/parse/namespace-and-globals-1/fail.expected new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/integration-tests/parse/namespace-and-globals-1/fail.expected @@ -0,0 +1 @@ + diff --git a/integration-tests/parse/namespace-and-globals-1/ghul.json b/integration-tests/parse/namespace-and-globals-1/ghul.json new file mode 100644 index 000000000..84539d122 --- /dev/null +++ b/integration-tests/parse/namespace-and-globals-1/ghul.json @@ -0,0 +1,6 @@ +{ + "compiler": "dotnet ../../../publish/ghul.dll", + "source": [ + "." + ] +} diff --git a/integration-tests/parse/namespace-and-globals-1/ghulflags b/integration-tests/parse/namespace-and-globals-1/ghulflags new file mode 100644 index 000000000..4d095b972 --- /dev/null +++ b/integration-tests/parse/namespace-and-globals-1/ghulflags @@ -0,0 +1 @@ +--type-check \ No newline at end of file diff --git a/integration-tests/parse/namespace-and-globals-1/il.expected b/integration-tests/parse/namespace-and-globals-1/il.expected new file mode 100644 index 000000000..e69de29bb diff --git a/integration-tests/parse/namespace-and-globals-1/test.ghul b/integration-tests/parse/namespace-and-globals-1/test.ghul new file mode 100644 index 000000000..58b7c1c17 --- /dev/null +++ b/integration-tests/parse/namespace-and-globals-1/test.ghul @@ -0,0 +1,9 @@ +namespace Test is + +si + +// expect error can't mix namespace and +// global definitions in the same file +entry() is + "this is a plain string literal"; +si diff --git a/integration-tests/parse/namespace-and-globals-1/warn.expected b/integration-tests/parse/namespace-and-globals-1/warn.expected new file mode 100644 index 000000000..e69de29bb diff --git a/integration-tests/parse/namespace-and-globals-2/.vscode/tasks.json b/integration-tests/parse/namespace-and-globals-2/.vscode/tasks.json new file mode 100644 index 000000000..49063613c --- /dev/null +++ b/integration-tests/parse/namespace-and-globals-2/.vscode/tasks.json @@ -0,0 +1,23 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "Run test", + "command": "dotnet ghul-test \"${workspaceFolder}\"", + "type": "shell", + "group": { + "kind": "test", + "isDefault": true + } + }, + { + "label": "Capture test expectation", + "command": "../../../tasks/capture.sh \"${workspaceFolder}\"", + "type": "shell", + "group": { + "kind": "build", + "isDefault": true + } + } + ] +} \ No newline at end of file diff --git a/integration-tests/parse/namespace-and-globals-2/err.expected b/integration-tests/parse/namespace-and-globals-2/err.expected new file mode 100644 index 000000000..75196080a --- /dev/null +++ b/integration-tests/parse/namespace-and-globals-2/err.expected @@ -0,0 +1 @@ +test.ghul: 4,1..6,3: error: cannot mix global definitions and namespaces in the same file diff --git a/integration-tests/parse/namespace-and-globals-2/fail.expected b/integration-tests/parse/namespace-and-globals-2/fail.expected new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/integration-tests/parse/namespace-and-globals-2/fail.expected @@ -0,0 +1 @@ + diff --git a/integration-tests/parse/namespace-and-globals-2/ghul.json b/integration-tests/parse/namespace-and-globals-2/ghul.json new file mode 100644 index 000000000..84539d122 --- /dev/null +++ b/integration-tests/parse/namespace-and-globals-2/ghul.json @@ -0,0 +1,6 @@ +{ + "compiler": "dotnet ../../../publish/ghul.dll", + "source": [ + "." + ] +} diff --git a/integration-tests/parse/namespace-and-globals-2/ghulflags b/integration-tests/parse/namespace-and-globals-2/ghulflags new file mode 100644 index 000000000..4d095b972 --- /dev/null +++ b/integration-tests/parse/namespace-and-globals-2/ghulflags @@ -0,0 +1 @@ +--type-check \ No newline at end of file diff --git a/integration-tests/parse/namespace-and-globals-2/il.expected b/integration-tests/parse/namespace-and-globals-2/il.expected new file mode 100644 index 000000000..e69de29bb diff --git a/integration-tests/parse/namespace-and-globals-2/test.ghul b/integration-tests/parse/namespace-and-globals-2/test.ghul new file mode 100644 index 000000000..c1badcf67 --- /dev/null +++ b/integration-tests/parse/namespace-and-globals-2/test.ghul @@ -0,0 +1,11 @@ + +// expect error can't mix namespace and +// global definitions in the same file +entry() is + "this is a plain string literal"; +si + +namespace Test is + +si + \ No newline at end of file diff --git a/integration-tests/parse/namespace-and-globals-2/warn.expected b/integration-tests/parse/namespace-and-globals-2/warn.expected new file mode 100644 index 000000000..e69de29bb diff --git a/src/syntax/process/generate_il.ghul b/src/syntax/process/generate_il.ghul index 15470df2e..f65f933e5 100644 --- a/src/syntax/process/generate_il.ghul +++ b/src/syntax/process/generate_il.ghul @@ -640,9 +640,10 @@ namespace Syntax.Process is if value? then current_try.return_value.store(value); fi + + current_try.return_needed.store(new Literal.NUMBER("1", _innate_symbol_lookup.get_bool_type(), "i4")); if current_try.is_in_finally then - current_try.return_needed.store(new Literal.NUMBER("1", _innate_symbol_lookup.get_bool_type(), "i4")); _brancher.branch(current_try.middle); else _brancher.leave(current_try.start); @@ -733,13 +734,16 @@ namespace Syntax.Process is let return_value: TEMP; let label: LOOP_LABELS; + let outer_try: LOOP_LABELS; + if list.want_dispose then - return_needed = new TEMP(_context, 1, _innate_symbol_lookup.get_bool_type()); - if return_type? /\ return_type !~ _innate_symbol_lookup.get_void_type() then - return_value = new TEMP(_context, return_type); - fi + let ot_rn_rv = get_exception_handler_temps(); + outer_try = ot_rn_rv.outer_try; + return_needed = ot_rn_rv.return_needed; + return_value = ot_rn_rv.return_value; + for v in list.variables_to_dispose do let buffer = new System.Text.StringBuilder(); v.gen_definition_header(buffer); @@ -768,7 +772,7 @@ namespace Syntax.Process is label.is_in_finally = true; - _brancher.leave(label.end); + _brancher.leave(label.start); _context.outdent(); _context.write_line("}}"); @@ -802,7 +806,7 @@ namespace Syntax.Process is i = i - 1; od - // TODO probably not needed + // TODO this is duplicated from try statement handling - needs to be refactored _brancher.label(label.middle); _context.write_line("endfinally"); @@ -811,18 +815,8 @@ namespace Syntax.Process is _context.write_line("}}"); _loops.leave_loop(); - - _brancher.branch(BRANCH.Z, label.return_needed.load(), label.end); - - _brancher.label(label.start); - - if return_value? then - return_value.load().gen(_context); - fi - - _context.write_line("ret"); - - _brancher.label(label.end); + + gen_exception_handler_exit(outer_try, label, return_value); fi if list.value? then @@ -983,6 +977,55 @@ namespace Syntax.Process is return true; si + get_exception_handler_temps() -> (outer_try: LOOP_LABELS, return_needed: TEMP, return_value: TEMP) is + let return_type = current_function.return_type; + + let return_needed: TEMP; + let return_value: TEMP; + + let outer_try = _loops.get_current_try(); + + if outer_try? then + return_needed = outer_try.return_needed; + return_value = outer_try.return_value; + else + return_needed = new TEMP(_context, 1, _innate_symbol_lookup.get_bool_type()); + + if return_type? /\ return_type !~ _innate_symbol_lookup.get_void_type() then + return_value = new TEMP(_context, return_type); + fi + fi + + return (outer_try, return_needed, return_value); + si + + gen_exception_handler_exit(outer_try: LOOP_LABELS, label: LOOP_LABELS, return_value: TEMP) is + _brancher.label(label.start); + _brancher.branch(BRANCH.Z, label.return_needed.load(), label.end); + + if outer_try? then + if return_value? then + assert outer_try.return_value? else "outer try has no return value temporary"; + outer_try.return_value.store(return_value.load()); + fi + + if outer_try.is_in_finally then + outer_try.return_needed.store(new Literal.NUMBER("1", _innate_symbol_lookup.get_bool_type(), "i4")); + _brancher.branch(outer_try.middle); + else + _brancher.leave(outer_try.start); + fi + else + if return_value? then + return_value.load().gen(_context); + fi + + _context.write_line("ret"); + fi + + _brancher.label(label.end); + si + visit(`try: Statements.TRY) is if (!`try.catches? \/ `try.catches.count == 0) /\ @@ -996,14 +1039,13 @@ namespace Syntax.Process is fi let return_type = current_function.return_type; - - let return_needed = new TEMP(_context, 1, _innate_symbol_lookup.get_bool_type()); - let return_value: TEMP; - - if return_type? /\ return_type !~ _innate_symbol_lookup.get_void_type() then - return_value = new TEMP(_context, return_type); - fi + let ot_rn_rv = get_exception_handler_temps(); + + let outer_try = ot_rn_rv.outer_try; + let return_needed = ot_rn_rv.return_needed; + let return_value = ot_rn_rv.return_value; + let label = _loops.enter_try(return_needed, return_value); let need_double_try = `try.catches? /\ `try.catches.count > 0 /\ `try.`finally?; @@ -1018,7 +1060,7 @@ namespace Syntax.Process is `try.body.walk(self); - _brancher.leave(label.end); + _brancher.leave(label.start); _context.outdent(); @@ -1033,6 +1075,7 @@ namespace Syntax.Process is _context.write_line("}}"); fi + // TODO a lot of this is duplicated in the list statement handling - needs to be refactored if `try.`finally? then label.is_in_finally = true; @@ -1049,34 +1092,9 @@ namespace Syntax.Process is fi _loops.leave_loop(); - - _brancher.branch(BRANCH.Z, label.return_needed.load(), label.end); - - _brancher.label(label.start); - let outer_try = _loops.get_current_try(); - - if outer_try? then - if return_value? then - assert outer_try.return_value? else "outer try has no return value temporary"; - outer_try.return_value.store(return_value.load()); - fi - - if outer_try.is_in_finally then - outer_try.return_needed.store(new Literal.NUMBER("1", _innate_symbol_lookup.get_bool_type(), "i4")); - _brancher.branch(outer_try.middle); - else - _brancher.leave(outer_try.start); - fi - else - if return_value? then - return_value.load().gen(_context); - fi - - _context.write_line("ret"); - fi - - _brancher.label(label.end); + // let outer_try = _loops.get_current_try(); + gen_exception_handler_exit(outer_try, label, return_value); super.visit(`try); si @@ -1099,7 +1117,7 @@ namespace Syntax.Process is `catch.body.walk(self); - _brancher.leave(_loops.get_current_try().end); + _brancher.leave(_loops.get_current_try().start); _context.outdent(); _context.write_line("}}");