From 9f92b6a51b81692635e05a981c19f1bc0cb5fa41 Mon Sep 17 00:00:00 2001 From: John Robbins Date: Tue, 10 Sep 2024 12:12:24 -0400 Subject: [PATCH 01/21] Developed and documented the %exit command For the `%exit` command: - Fixed `visit_end_statement` to handle the break state and end debugging when `END` is executed. - Added the `%exit` command work to driver.py. - Added tests for `%exit` command to driver_test.py. - Documented the `%exit` command. Additional changes: - Fixed the project links in the `%?` and `%help` commands. - Added https to the logo. - Forgot to update the regex in cmd_lang_test.py when I renamed `%openfile` to `%loadfile`. Fixed that and update the appropriate tests. - Added test to check for invalid line number when deleting a breakpoint. - Changed the error messages when executing debugger only commands to show the full command name. Closes #1. --- docs/docs/command-language.md | 11 +++++- src/tbp/driver.py | 30 ++++++++++++---- src/tbp/interpreter.py | 5 ++- tests/cmd_lang_test.py | 37 +++++++++++++------- tests/debugger_test.py | 64 ++++++++++++++++++++++++++++++++--- 5 files changed, 121 insertions(+), 26 deletions(-) diff --git a/docs/docs/command-language.md b/docs/docs/command-language.md index 372d50b..ff600da 100644 --- a/docs/docs/command-language.md +++ b/docs/docs/command-language.md @@ -315,7 +315,7 @@ C=3 I=1 J=26 N=10 S=256 DEBUG(200):> ``` -### Seeing the Call Stack +### Seeing the Call Stack: `%bt` | `%backtrace` While Tiny BASIC is not a language you are going use to write a recursive descent parser, it does support calling procedures with [`GOSUB`](tb-language#gosubreturn---call-toreturn-from-a-procedure) and [`RETURN`](tb-language#gosubreturn---call-toreturn-from-a-procedure). Using the [`deep.tbp`](https://github.com/John-Robbins/tbp/blob/main/examples/deep.tbp) test program here's an example of its output. @@ -333,6 +333,15 @@ DEBUG(200):>%bt DEBUG(200):> ``` +### Exiting the Debugger: `%e` | `%exit` + +If you want to stop debugging and end program execution while at a debugger prompt, use the `%e` command to return to the normal tbp prompt. + +```text +DEBUG(420):>%exit +tbp:> +``` + ### Debugging Tips and Tricks When stopped at the debugger prompt, if you want to change a variable, use the regular Tiny BASIC assignment statement. diff --git a/src/tbp/driver.py b/src/tbp/driver.py index d21226b..e8d4ee7 100644 --- a/src/tbp/driver.py +++ b/src/tbp/driver.py @@ -193,7 +193,8 @@ def _load_program_and_run(self: Driver, program: str) -> None: lint | savefile | \bsf\b | step | \bs\b | - vars | \bv\b + vars | \bv\b | + exit | \be\b ) \s* (?P @@ -271,15 +272,26 @@ def _process_command_language(self: Driver, cmd: str) -> Driver.CmdResult: # no self._command_stepper(Interpreter.BreakContinueType.STEP) case "backtrace" | "bt": self._command_stack() + case "exit" | "e": + self._command_exit_debugger() case _: # pragma: no cover pass # pragma: no cover return Driver.CmdResult.CONTINUE + def _command_exit_debugger(self: Driver) -> None: + """Exit the debugger and returns to the tbp prompt.""" + if self._interpreter.at_breakpoint() is False: + print_output("CLE #08: %exit command only works while debugging.\n") + else: + # The END statement already knows how to drop out of the debugger + # so I can use it here to do the work. + self._interpreter.interpret_line("END") + def _command_stack(self: Driver) -> None: """Show the call stack.""" if self._interpreter.at_breakpoint() is False: - print_output("CLE #08: %bt command only works while debugging.\n") + print_output("CLE #08: %backtrace command only works while debugging.\n") else: res: str = self._interpreter.stack_string() print_output(res) @@ -290,9 +302,9 @@ def _command_stepper( ) -> None: """Execute a single step.""" if self._interpreter.at_breakpoint() is False: - cmd: str = "%c" + cmd: str = "%continue" if step_type == Interpreter.BreakContinueType.STEP: - cmd = "%s" + cmd = "%step" print_output(f"CLE #08: {cmd} command only works while debugging.\n") return @@ -438,7 +450,7 @@ def _command_language_error(error: str) -> None: ########################################################################### _LOGO: str = f""" - Tiny BASIC in Python - github.com/John-Robbins/tbp + Tiny BASIC in Python - https://github.com/John-Robbins/tbp _______ ____ |__ __| _ \\ | | | |_) |_ __ @@ -517,10 +529,10 @@ def _logo_display() -> None: A complete Tiny BASIC interpreter and debugger. To learn more about the Tiny BASIC language, see the documentation at -http://github.com/john-robbins/tbp/docs/tinybasic/tinybasic-users-manual.html +https://john-robbins.github.io/tbp/tb-language To learn more about Tiny BASIC in Python, see the extensive documentation at -http://github.com/john-robbins/tbp/docs/ +https://john-robbins.github.io/tbp/ Command Line Options -------------------- @@ -584,6 +596,8 @@ def _logo_display() -> None: - Displays all the initialized variables. %bt | %backtrace - Display the call stack. +%e | %exit + - Exit the debugger and return to tbp prompt. """ _SHORTHELP: str = """ @@ -618,4 +632,6 @@ def _logo_display() -> None: - Displays all the initialized variables. %bt | %backtrace - Display the call stack. +%e | %exit + - Exit the debugger and return to tbp prompt. """ diff --git a/src/tbp/interpreter.py b/src/tbp/interpreter.py index 76b7f47..cca56c3 100644 --- a/src/tbp/interpreter.py +++ b/src/tbp/interpreter.py @@ -923,7 +923,10 @@ def visit_return_statement(self: Interpreter, ret: Return) -> LanguageItem: def visit_end_statement(self: Interpreter, end: End) -> LanguageItem: """Process an END statement.""" del end - if self._the_state == Interpreter.State.RUNNING_STATE: + if self._the_state in { + Interpreter.State.RUNNING_STATE, + Interpreter.State.BREAK_STATE, + }: # Clean up possible RUN parameters that were not used. self.initialize_runtime_state() return cast(End, None) # pragma: no cover diff --git a/tests/cmd_lang_test.py b/tests/cmd_lang_test.py index d5e5c02..333e0f1 100644 --- a/tests/cmd_lang_test.py +++ b/tests/cmd_lang_test.py @@ -18,7 +18,7 @@ %(?P help | \? | quit | \bq\b | - openfile | \bof\b | + loadfile | \blf\b | opt | break | \bbp\b | backtrace | \bbt\b | @@ -27,7 +27,8 @@ lint | savefile | \bsf\b | step | \bs\b | - vars | \bv\b + vars | \bv\b | + exit | \be\b ) \s* (?P @@ -74,15 +75,15 @@ def test_quit() -> None: assert m is None -def test_openfile() -> None: +def test_loadfile() -> None: """Test '%openfile'.""" - m = _CMD_REGEX.match('%openfile "somefile"') + m = _CMD_REGEX.match('%loadfile "somefile"') assert m is not None - assert m.group(_CMD_GROUP) == "openfile" + assert m.group(_CMD_GROUP) == "loadfile" assert m.group(_PARAM_GROUP) == '"somefile"' - m = _CMD_REGEX.match('%of "blah"') + m = _CMD_REGEX.match('%lf "blah"') assert m is not None - assert m.group(_CMD_GROUP) == "of" + assert m.group(_CMD_GROUP) == "lf" assert m.group(_PARAM_GROUP) == '"blah"' m = _CMD_REGEX.match("%open 100") assert m is None @@ -164,11 +165,21 @@ def test_step() -> None: assert m.group(_CMD_GROUP) == "s" -def test_info() -> None: - """Test '%step'.""" - m = _CMD_REGEX.match("%step") +def test_vars() -> None: + """Test '%vars'.""" + m = _CMD_REGEX.match("%vars") assert m is not None - assert m.group(_CMD_GROUP) == "step" - m = _CMD_REGEX.match("%s") + assert m.group(_CMD_GROUP) == "vars" + m = _CMD_REGEX.match("%v") assert m is not None - assert m.group(_CMD_GROUP) == "s" + assert m.group(_CMD_GROUP) == "v" + + +def test_exit() -> None: + """Test '%exit'.""" + m = _CMD_REGEX.match("%exit") + assert m is not None + assert m.group(_CMD_GROUP) == "exit" + m = _CMD_REGEX.match("%e") + assert m is not None + assert m.group(_CMD_GROUP) == "e" diff --git a/tests/debugger_test.py b/tests/debugger_test.py index 7f12bbd..aebb23b 100644 --- a/tests/debugger_test.py +++ b/tests/debugger_test.py @@ -196,15 +196,16 @@ def test_not_debugging_commands( monkeypatch: pytest.MonkeyPatch, ) -> None: """Test the all the debugger commands fail when not debugging.""" - cmds = iter(["%c", "%s", "%bt", "%q"]) + cmds = iter(["%c", "%s", "%bt", "%e", "%q"]) driver: Driver = Driver() monkeypatch.setattr("builtins.input", lambda _: next(cmds)) ret: int = driver.party_like_it_is_1976(empty_opts) output = capsys.readouterr() assert ret == 0 - assert "CLE #08: %c" in output.out - assert "CLE #08: %s" in output.out - assert "CLE #08: %bt" in output.out + assert "CLE #08: %continue" in output.out + assert "CLE #08: %step" in output.out + assert "CLE #08: %backtrace" in output.out + assert "CLE #08: %exit" in output.out def test_simple_breakpoint_commands( @@ -427,3 +428,58 @@ def test_open_file_while_debugging( output = capsys.readouterr() assert ret == 0 assert "CLE #15: %loadfile disabled while debugging." in output.out + + +def test_debugging_exit( + capsys: CaptureFixture[str], + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Test %exit command.""" + cmds = iter( + [ + "10 GOSUB 40", + "20 END", + '40 PR "Scout"', + "50 RETURN", + "%bp 10", + "RUN", + "%exit", + "%continue", + "%quit", + ], + ) + driver: Driver = Driver() + monkeypatch.setattr("builtins.input", lambda _: next(cmds)) + ret: int = driver.party_like_it_is_1976(empty_opts) + output = capsys.readouterr() + assert ret == 0 + assert "CLE #08: %continue" in output.out + + +def test_debugging_invalid_bp_delete( + capsys: CaptureFixture[str], + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Test %d command.""" + cmds = iter( + [ + "10 GOSUB 40", + "20 END", + '40 PR "Scout"', + "50 RETURN", + "%bp 10", + "%d log", + "%exit", + "%continue", + "%quit", + ], + ) + driver: Driver = Driver() + monkeypatch.setattr("builtins.input", lambda _: next(cmds)) + ret: int = driver.party_like_it_is_1976(empty_opts) + output = capsys.readouterr() + assert ret == 0 + assert ( + "CLE #05: %break and %delete commands require line numbers as parameters:" + in output.out + ) From ed58fc1b65763c981b276854aa6f7fa4c85a79f5 Mon Sep 17 00:00:00 2001 From: John Robbins Date: Tue, 10 Sep 2024 14:52:37 -0400 Subject: [PATCH 02/21] Fixed copyright MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Last I checked, it's 2024 so the copyright message on the top of all Python files should be correct. Geez, I'm an idiot.šŸ¤£ Closes #43. --- src/tbp/__init__.py | 2 +- src/tbp/__main__.py | 2 +- src/tbp/astprinter.py | 2 +- src/tbp/driver.py | 2 +- src/tbp/errors.py | 2 +- src/tbp/helpers.py | 2 +- src/tbp/interpreter.py | 2 +- src/tbp/languageitems.py | 2 +- src/tbp/linter.py | 2 +- src/tbp/memory.py | 2 +- src/tbp/parser.py | 2 +- src/tbp/scanner.py | 2 +- src/tbp/symboltable.py | 2 +- src/tbp/tokens.py | 2 +- tests/__init__.py | 2 +- tests/cmd_lang_test.py | 2 +- tests/debugger_test.py | 2 +- tests/driver_test.py | 2 +- tests/helpers_test.py | 2 +- tests/interpreter_test.py | 2 +- tests/lang_test.py | 2 +- tests/linter_test.py | 2 +- tests/memory_test.py | 2 +- tests/parser_test.py | 2 +- tests/programs.py | 2 +- tests/scanner_test.py | 2 +- tests/symboltable_test.py | 2 +- tests/tokens_test.py | 2 +- tools/num_pytest_tests.py | 2 +- 29 files changed, 29 insertions(+), 29 deletions(-) diff --git a/src/tbp/__init__.py b/src/tbp/__init__.py index 1a34429..e80f131 100644 --- a/src/tbp/__init__.py +++ b/src/tbp/__init__.py @@ -3,7 +3,7 @@ ############################################################################### # Tiny BASIC in Python # Licensed under the MIT License. -# Copyright (c) 2004 John Robbins +# Copyright (c) 2024 John Robbins ############################################################################### from __future__ import annotations diff --git a/src/tbp/__main__.py b/src/tbp/__main__.py index 1b2ff08..93832a6 100644 --- a/src/tbp/__main__.py +++ b/src/tbp/__main__.py @@ -3,7 +3,7 @@ ############################################################################### # Tiny BASIC in Python # Licensed under the MIT License. -# Copyright (c) 2004 John Robbins +# Copyright (c) 2024 John Robbins ############################################################################### from __future__ import annotations diff --git a/src/tbp/astprinter.py b/src/tbp/astprinter.py index ea0daf0..b4706ee 100644 --- a/src/tbp/astprinter.py +++ b/src/tbp/astprinter.py @@ -3,7 +3,7 @@ ############################################################################### # Tiny BASIC in Python # Licensed under the MIT License. -# Copyright (c) 2004 John Robbins +# Copyright (c) 2024 John Robbins ############################################################################### from __future__ import annotations diff --git a/src/tbp/driver.py b/src/tbp/driver.py index e8d4ee7..adaa461 100644 --- a/src/tbp/driver.py +++ b/src/tbp/driver.py @@ -3,7 +3,7 @@ ############################################################################### # Tiny BASIC in Python # Licensed under the MIT License. -# Copyright (c) 2004 John Robbins +# Copyright (c) 2024 John Robbins ############################################################################### from __future__ import annotations diff --git a/src/tbp/errors.py b/src/tbp/errors.py index 990160e..bf0902b 100644 --- a/src/tbp/errors.py +++ b/src/tbp/errors.py @@ -3,7 +3,7 @@ ############################################################################### # Tiny BASIC in Python # Licensed under the MIT License. -# Copyright (c) 2004 John Robbins +# Copyright (c) 2024 John Robbins ############################################################################### from __future__ import annotations diff --git a/src/tbp/helpers.py b/src/tbp/helpers.py index 92830e0..7995ed3 100644 --- a/src/tbp/helpers.py +++ b/src/tbp/helpers.py @@ -3,7 +3,7 @@ ############################################################################### # Tiny BASIC in Python # Licensed under the MIT License. -# Copyright (c) 2004 John Robbins +# Copyright (c) 2024 John Robbins ############################################################################### from __future__ import annotations diff --git a/src/tbp/interpreter.py b/src/tbp/interpreter.py index cca56c3..da4064d 100644 --- a/src/tbp/interpreter.py +++ b/src/tbp/interpreter.py @@ -3,7 +3,7 @@ ############################################################################### # Tiny BASIC in Python # Licensed under the MIT License. -# Copyright (c) 2004 John Robbins +# Copyright (c) 2024 John Robbins ############################################################################### from __future__ import annotations diff --git a/src/tbp/languageitems.py b/src/tbp/languageitems.py index f08bae9..84d430c 100644 --- a/src/tbp/languageitems.py +++ b/src/tbp/languageitems.py @@ -3,7 +3,7 @@ ############################################################################### # Tiny BASIC in Python # Licensed under the MIT License. -# Copyright (c) 2004 John Robbins +# Copyright (c) 2024 John Robbins ############################################################################### from __future__ import annotations diff --git a/src/tbp/linter.py b/src/tbp/linter.py index 3635f34..d3710aa 100644 --- a/src/tbp/linter.py +++ b/src/tbp/linter.py @@ -3,7 +3,7 @@ ############################################################################### # Tiny BASIC in Python # Licensed under the MIT License. -# Copyright (c) 2004 John Robbins +# Copyright (c) 2024 John Robbins ############################################################################### from __future__ import annotations diff --git a/src/tbp/memory.py b/src/tbp/memory.py index de8ad9b..415d8d9 100644 --- a/src/tbp/memory.py +++ b/src/tbp/memory.py @@ -3,7 +3,7 @@ ############################################################################### # Tiny BASIC in Python # Licensed under the MIT License. -# Copyright (c) 2004 John Robbins +# Copyright (c) 2024 John Robbins ############################################################################### from __future__ import annotations diff --git a/src/tbp/parser.py b/src/tbp/parser.py index edbf551..0e44f6f 100644 --- a/src/tbp/parser.py +++ b/src/tbp/parser.py @@ -3,7 +3,7 @@ ############################################################################### # Tiny BASIC in Python # Licensed under the MIT License. -# Copyright (c) 2004 John Robbins +# Copyright (c) 2024 John Robbins ############################################################################### from __future__ import annotations diff --git a/src/tbp/scanner.py b/src/tbp/scanner.py index 760e1b7..1d7e5ad 100644 --- a/src/tbp/scanner.py +++ b/src/tbp/scanner.py @@ -3,7 +3,7 @@ ############################################################################### # Tiny BASIC in Python # Licensed under the MIT License. -# Copyright (c) 2004 John Robbins +# Copyright (c) 2024 John Robbins ############################################################################### from __future__ import annotations diff --git a/src/tbp/symboltable.py b/src/tbp/symboltable.py index 42ea63c..de6ccf8 100644 --- a/src/tbp/symboltable.py +++ b/src/tbp/symboltable.py @@ -3,7 +3,7 @@ ############################################################################### # Tiny BASIC in Python # Licensed under the MIT License. -# Copyright (c) 2004 John Robbins +# Copyright (c) 2024 John Robbins ############################################################################### from __future__ import annotations diff --git a/src/tbp/tokens.py b/src/tbp/tokens.py index e75d051..e208239 100644 --- a/src/tbp/tokens.py +++ b/src/tbp/tokens.py @@ -3,7 +3,7 @@ ############################################################################### # Tiny BASIC in Python # Licensed under the MIT License. -# Copyright (c) 2004 John Robbins +# Copyright (c) 2024 John Robbins ############################################################################### from __future__ import annotations diff --git a/tests/__init__.py b/tests/__init__.py index 9a3b053..aec3156 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -3,5 +3,5 @@ ############################################################################### # Tiny BASIC in Python # Licensed under the MIT License. -# Copyright (c) 2004 John Robbins +# Copyright (c) 2024 John Robbins ############################################################################### diff --git a/tests/cmd_lang_test.py b/tests/cmd_lang_test.py index 333e0f1..e7974c2 100644 --- a/tests/cmd_lang_test.py +++ b/tests/cmd_lang_test.py @@ -3,7 +3,7 @@ ############################################################################### # Tiny BASIC in Python # Licensed under the MIT License. -# Copyright (c) 2004 John Robbins +# Copyright (c) 2024 John Robbins ############################################################################### from __future__ import annotations diff --git a/tests/debugger_test.py b/tests/debugger_test.py index aebb23b..034e276 100644 --- a/tests/debugger_test.py +++ b/tests/debugger_test.py @@ -3,7 +3,7 @@ ############################################################################### # Tiny BASIC in Python # Licensed under the MIT License. -# Copyright (c) 2004 John Robbins +# Copyright (c) 2024 John Robbins ############################################################################### from __future__ import annotations diff --git a/tests/driver_test.py b/tests/driver_test.py index 491eb21..8674266 100644 --- a/tests/driver_test.py +++ b/tests/driver_test.py @@ -3,7 +3,7 @@ ############################################################################### # Tiny BASIC in Python # Licensed under the MIT License. -# Copyright (c) 2004 John Robbins +# Copyright (c) 2024 John Robbins ############################################################################### from __future__ import annotations diff --git a/tests/helpers_test.py b/tests/helpers_test.py index 8cae4b9..342856a 100644 --- a/tests/helpers_test.py +++ b/tests/helpers_test.py @@ -3,7 +3,7 @@ ############################################################################### # Tiny BASIC in Python # Licensed under the MIT License. -# Copyright (c) 2004 John Robbins +# Copyright (c) 2024 John Robbins ############################################################################### from __future__ import annotations diff --git a/tests/interpreter_test.py b/tests/interpreter_test.py index 8a38c12..7bfce9b 100644 --- a/tests/interpreter_test.py +++ b/tests/interpreter_test.py @@ -3,7 +3,7 @@ ############################################################################### # Tiny BASIC in Python # Licensed under the MIT License. -# Copyright (c) 2004 John Robbins +# Copyright (c) 2024 John Robbins ############################################################################### from __future__ import annotations diff --git a/tests/lang_test.py b/tests/lang_test.py index 0907477..aa36ebc 100644 --- a/tests/lang_test.py +++ b/tests/lang_test.py @@ -3,7 +3,7 @@ ############################################################################### # Tiny BASIC in Python # Licensed under the MIT License. -# Copyright (c) 2004 John Robbins +# Copyright (c) 2024 John Robbins ############################################################################### from __future__ import annotations diff --git a/tests/linter_test.py b/tests/linter_test.py index c4952ab..05f1a41 100644 --- a/tests/linter_test.py +++ b/tests/linter_test.py @@ -3,7 +3,7 @@ ############################################################################### # Tiny BASIC in Python # Licensed under the MIT License. -# Copyright (c) 2004 John Robbins +# Copyright (c) 2024 John Robbins ############################################################################### from __future__ import annotations diff --git a/tests/memory_test.py b/tests/memory_test.py index 52c867b..2745acf 100644 --- a/tests/memory_test.py +++ b/tests/memory_test.py @@ -3,7 +3,7 @@ ############################################################################### # Tiny BASIC in Python # Licensed under the MIT License. -# Copyright (c) 2004 John Robbins +# Copyright (c) 2024 John Robbins ############################################################################### from __future__ import annotations diff --git a/tests/parser_test.py b/tests/parser_test.py index 31ef780..7aae9af 100644 --- a/tests/parser_test.py +++ b/tests/parser_test.py @@ -3,7 +3,7 @@ ############################################################################### # Tiny BASIC in Python # Licensed under the MIT License. -# Copyright (c) 2004 John Robbins +# Copyright (c) 2024 John Robbins ############################################################################### from __future__ import annotations diff --git a/tests/programs.py b/tests/programs.py index 7460319..835a824 100644 --- a/tests/programs.py +++ b/tests/programs.py @@ -3,7 +3,7 @@ ############################################################################### # Tiny BASIC in Python # Licensed under the MIT License. -# Copyright (c) 2004 John Robbins +# Copyright (c) 2024 John Robbins ############################################################################### from __future__ import annotations diff --git a/tests/scanner_test.py b/tests/scanner_test.py index 76e7d0a..06f6ade 100644 --- a/tests/scanner_test.py +++ b/tests/scanner_test.py @@ -3,7 +3,7 @@ ############################################################################### # Tiny BASIC in Python # Licensed under the MIT License. -# Copyright (c) 2004 John Robbins +# Copyright (c) 2024 John Robbins ############################################################################### from __future__ import annotations diff --git a/tests/symboltable_test.py b/tests/symboltable_test.py index 0b08078..2c6af9b 100644 --- a/tests/symboltable_test.py +++ b/tests/symboltable_test.py @@ -3,7 +3,7 @@ ############################################################################### # Tiny BASIC in Python # Licensed under the MIT License. -# Copyright (c) 2004 John Robbins +# Copyright (c) 2024 John Robbins ############################################################################### from __future__ import annotations diff --git a/tests/tokens_test.py b/tests/tokens_test.py index eda64a5..6dff955 100644 --- a/tests/tokens_test.py +++ b/tests/tokens_test.py @@ -3,7 +3,7 @@ ############################################################################### # Tiny BASIC in Python # Licensed under the MIT License. -# Copyright (c) 2004 John Robbins +# Copyright (c) 2024 John Robbins ############################################################################### from __future__ import annotations diff --git a/tools/num_pytest_tests.py b/tools/num_pytest_tests.py index c5d0334..e7982c9 100644 --- a/tools/num_pytest_tests.py +++ b/tools/num_pytest_tests.py @@ -3,7 +3,7 @@ ############################################################################### # Tiny BASIC in Python # Licensed under the MIT License. -# Copyright (c) 2004 John Robbins +# Copyright (c) 2024 John Robbins ############################################################################### # Run pytest with the '--junit-xml=.test-results.xml' command line option to From ceae39d9b9d5caf8fbd042b325971bebecabfb98 Mon Sep 17 00:00:00 2001 From: John Robbins Date: Wed, 11 Sep 2024 15:02:17 -0400 Subject: [PATCH 03/21] read_input fixes It no longer eats exceptions and just returns a string. --- src/tbp/driver.py | 4 +--- src/tbp/helpers.py | 12 ++---------- src/tbp/interpreter.py | 8 +------- 3 files changed, 4 insertions(+), 20 deletions(-) diff --git a/src/tbp/driver.py b/src/tbp/driver.py index adaa461..baa0ac4 100644 --- a/src/tbp/driver.py +++ b/src/tbp/driver.py @@ -108,9 +108,7 @@ def party_like_it_is_1976(self: Driver, options: Options) -> int: # Run away! while continue_running is True: prompt: str = self._build_prompt() - cmd_result, cmd_to_do = read_input(prompt) - if cmd_result is False: - break + cmd_to_do = read_input(prompt) continue_running = self._execute_line(cmd_to_do.strip()) return 0 diff --git a/src/tbp/helpers.py b/src/tbp/helpers.py index 7995ed3..14b98ed 100644 --- a/src/tbp/helpers.py +++ b/src/tbp/helpers.py @@ -118,17 +118,9 @@ def print_output(message: str) -> None: ############################################################################### -def read_input(prompt: str) -> tuple[bool, str]: +def read_input(prompt: str) -> str: """Read input from the user.""" - result_str: str = "" - result_flag: bool = True - - try: - result_str = input(prompt) - except (EOFError, KeyboardInterrupt): - result_flag = False - - return result_flag, result_str + return input(prompt) # The minimum number of entries in the valid_input list for limit_input. diff --git a/src/tbp/interpreter.py b/src/tbp/interpreter.py index da4064d..200f3e3 100644 --- a/src/tbp/interpreter.py +++ b/src/tbp/interpreter.py @@ -1130,13 +1130,7 @@ def visit_input_statement( # Build up the prompt. prompt_text = self._build_input_prompt(curr_index, input_stmt.variables) # Ask the user for input. - input_good, raw_text = read_input(prompt_text) - if input_good is False: - # Indicate to the rest of the interpreter that the user hit - # CTRL+C or CTRL+D. - self.initialize_runtime_state() - print_output("Error #350: Aborting RUN from INPUT entry.\n") - return cast(Input, None) + raw_text = read_input(prompt_text) # Split the input string on commas. raw_list = raw_text.split(",") From 6b070bb41e8cfdd794ac98a4ec1bfff4048c5f13 Mon Sep 17 00:00:00 2001 From: John Robbins Date: Wed, 11 Sep 2024 17:21:28 -0400 Subject: [PATCH 04/21] pyright update Pyright now wants this in .TOML files instead of in the user workspace. --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index e56bfd4..5cf1e46 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -250,6 +250,7 @@ reportUnusedImport = false reportUnnecessaryComparison = false reportUnknownArgumentType = false reportUnknownLambdaType = false +typeCheckingMode = "strict" #################### From ba0aa40a662fa1bba5fa460e097efac9f2e8f9c1 Mon Sep 17 00:00:00 2001 From: John Robbins Date: Wed, 11 Sep 2024 21:55:01 -0400 Subject: [PATCH 05/21] Added `Interpreter.current_state` The `Interpreter` class now has the `current_state` property. Removed the old `at_breakpoint` method. Also updated some driver tests to remove the `StringIO` usage. --- src/tbp/driver.py | 15 +++++++++------ src/tbp/interpreter.py | 12 ++++++++---- tests/driver_test.py | 19 ++++++++----------- 3 files changed, 25 insertions(+), 21 deletions(-) diff --git a/src/tbp/driver.py b/src/tbp/driver.py index baa0ac4..6349bee 100644 --- a/src/tbp/driver.py +++ b/src/tbp/driver.py @@ -122,7 +122,7 @@ def _build_prompt(self: Driver) -> str: # This is what the prompt will be 99% of the time. prompt_str: str = Driver._DEFAULT_PROMPT - if self._interpreter.at_breakpoint() is True: + if self._interpreter.current_state == Interpreter.State.BREAK_STATE: # What source line have we stopped on? curr_line: int = self._interpreter.current_line_number() prompt_str = f"DEBUG({curr_line}):>" @@ -138,7 +138,10 @@ def _execute_line(self: Driver, cmd: str) -> bool: if cmd[0] == Driver._CMD_LANG_PREFIX: if self._process_command_language(cmd) == Driver.CmdResult.QUIT: return False - elif self._interpreter.at_breakpoint() and cmd[0:3].lower() == "run": + elif ( + self._interpreter.current_state == Interpreter.State.BREAK_STATE + and cmd[0:3].lower() == "run" + ): # We have one more check. If we are at a breakpoint and the user # entered the RUN statement, we don't want to do that because it # restarts the program. We want the user to use %c instead. @@ -279,7 +282,7 @@ def _process_command_language(self: Driver, cmd: str) -> Driver.CmdResult: # no def _command_exit_debugger(self: Driver) -> None: """Exit the debugger and returns to the tbp prompt.""" - if self._interpreter.at_breakpoint() is False: + if self._interpreter.current_state != Interpreter.State.BREAK_STATE: print_output("CLE #08: %exit command only works while debugging.\n") else: # The END statement already knows how to drop out of the debugger @@ -288,7 +291,7 @@ def _command_exit_debugger(self: Driver) -> None: def _command_stack(self: Driver) -> None: """Show the call stack.""" - if self._interpreter.at_breakpoint() is False: + if self._interpreter.current_state != Interpreter.State.BREAK_STATE: print_output("CLE #08: %backtrace command only works while debugging.\n") else: res: str = self._interpreter.stack_string() @@ -299,7 +302,7 @@ def _command_stepper( step_type: Interpreter.BreakContinueType, ) -> None: """Execute a single step.""" - if self._interpreter.at_breakpoint() is False: + if self._interpreter.current_state != Interpreter.State.BREAK_STATE: cmd: str = "%continue" if step_type == Interpreter.BreakContinueType.STEP: cmd = "%step" @@ -383,7 +386,7 @@ def _command_savefile(self: Driver, filename: str) -> None: def _command_loadfile(self: Driver, filename: str) -> None: """Load a program from disk.""" # If we are debugging, %openfile can't be used. - if self._interpreter.at_breakpoint() is True: + if self._interpreter.current_state == Interpreter.State.BREAK_STATE: self._command_language_error("CLE #15: %loadfile disabled while debugging.") return # Is the filename empty? diff --git a/src/tbp/interpreter.py b/src/tbp/interpreter.py index 200f3e3..021fa3f 100644 --- a/src/tbp/interpreter.py +++ b/src/tbp/interpreter.py @@ -143,6 +143,14 @@ def __init__(self: Interpreter) -> None: # The one-shot breakpoints list. self._one_shot_breakpoints: list[int] = [] + ########################################################################### + # PUBLIC: Interpret Properties + ########################################################################### + @property + def current_state(self: Interpreter) -> Interpreter.State: + """Return the current state.""" + return self._the_state + ########################################################################### # PUBLIC: Interpret Methods ########################################################################### @@ -341,10 +349,6 @@ def delete_breakpoint(self: Interpreter, line_number: int) -> tuple[bool, str]: self._breakpoints.remove(line_number) return True, "" - def at_breakpoint(self: Interpreter) -> bool: - """Public method the driver can call to see the state.""" - return self._the_state == Interpreter.State.BREAK_STATE - def break_continue(self: Interpreter, step: Interpreter.BreakContinueType) -> None: """Tell the interpreter how to continue from a breakpoint.""" # We have to be very careful here. The debugger evaluation is called diff --git a/tests/driver_test.py b/tests/driver_test.py index 8674266..5c33044 100644 --- a/tests/driver_test.py +++ b/tests/driver_test.py @@ -34,8 +34,8 @@ def test_empty_cmd_lang( ) -> None: """Test '%'.""" driver: Driver = Driver() - input_data = StringIO("%") - monkeypatch.setattr("sys.stdin", input_data) + cmds = iter(["%", "%q"]) + monkeypatch.setattr("builtins.input", lambda _: next(cmds)) ret: int = driver.party_like_it_is_1976(empty_opts) assert ret == 0 output = capsys.readouterr() @@ -61,10 +61,9 @@ def test_cmd_short_help( monkeypatch: pytest.MonkeyPatch, ) -> None: """Test '%?^%q'.""" - commands = r"%?\n%q\n" + cmds = iter(["%?", "%q"]) driver: Driver = Driver() - input_data = StringIO(commands) - monkeypatch.setattr("sys.stdin", input_data) + monkeypatch.setattr("builtins.input", lambda _: next(cmds)) ret: int = driver.party_like_it_is_1976(empty_opts) assert ret == 0 output = capsys.readouterr() @@ -76,10 +75,9 @@ def test_cmd_long_help( monkeypatch: pytest.MonkeyPatch, ) -> None: """Test '%help^%q'.""" - commands = r"%help\n%q\n" + cmds = iter(["%help", "%q"]) driver: Driver = Driver() - input_data = StringIO(commands) - monkeypatch.setattr("sys.stdin", input_data) + monkeypatch.setattr("builtins.input", lambda _: next(cmds)) ret: int = driver.party_like_it_is_1976( Driver.Options(nologo=False, file="", commands=""), ) @@ -94,10 +92,9 @@ def test_cmd_log_alone( monkeypatch: pytest.MonkeyPatch, ) -> None: """Test '%opt log'.""" - commands = r"%opt log\n" + cmds = iter(["%opt log", "%q"]) driver: Driver = Driver() - input_data = StringIO(commands) - monkeypatch.setattr("sys.stdin", input_data) + monkeypatch.setattr("builtins.input", lambda _: next(cmds)) ret: int = driver.party_like_it_is_1976(empty_opts) assert ret == 0 output = capsys.readouterr() From 040bec15037466fd0753b2130b226f3b077456f8 Mon Sep 17 00:00:00 2001 From: John Robbins Date: Wed, 11 Sep 2024 22:57:00 -0400 Subject: [PATCH 06/21] Bumped up Ruff defaults I know I've got too much complexity with returns, branches, and McCabe. --- pyproject.toml | 5 +++++ src/tbp/interpreter.py | 2 +- src/tbp/parser.py | 6 ++---- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 5cf1e46..e267de6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -138,6 +138,11 @@ strict = true [tool.ruff.lint.pylint] max-args = 6 max-public-methods = 39 +max-branches = 16 +max-returns = 17 + +[tool.ruff.lint.mccabe] +max-complexity = 16 [tool.ruff.lint.per-file-ignores] # S101: https://docs.astral.sh/ruff/rules/assert/ diff --git a/src/tbp/interpreter.py b/src/tbp/interpreter.py index 021fa3f..e7f4ea5 100644 --- a/src/tbp/interpreter.py +++ b/src/tbp/interpreter.py @@ -534,7 +534,7 @@ def _hit_breakpoint(self: Interpreter) -> bool: return False - def _set_one_shots(self: Interpreter) -> None: # noqa: C901 pylint: disable=too-complex + def _set_one_shots(self: Interpreter) -> None: # pylint: disable=too-complex """ Find the address to step into. diff --git a/src/tbp/parser.py b/src/tbp/parser.py index 0e44f6f..883aae2 100644 --- a/src/tbp/parser.py +++ b/src/tbp/parser.py @@ -123,9 +123,8 @@ def _line(self: Parser) -> LanguageItem: # TODO@John-Robbins: Is the dictionary of method pointers the way to go? # Doing this lookup with a bunch of if statements or case statements will # yield a high McCabe complexity number. - # PLR0911: Too many return statements (10 > 6) # pylint: disable=too-complex, too-many-branches - def _statement(self: Parser) -> LanguageItem: # noqa: PLR0911, C901, PLR0912 + def _statement(self: Parser) -> LanguageItem: """Parse a statement on the line.""" if self._match(TokenType.PRINT): return self._print_statement() @@ -214,8 +213,7 @@ def _unary(self: Parser) -> LanguageItem: expression: LanguageItem = self._primary() return expression - # TODO@John-Robbins: Keeping this here as a note to myself. - def _primary(self: Parser) -> LanguageItem: # noqa: PLR0911 + def _primary(self: Parser) -> LanguageItem: """Parse the terminals from the line.""" curr_token: Token = self._peek() if self._match(TokenType.NUMBER): From 0934173038bc4fef03b0584dbcaea50c2c9c2db8 Mon Sep 17 00:00:00 2001 From: John Robbins Date: Thu, 12 Sep 2024 21:52:05 -0400 Subject: [PATCH 07/21] Proper CTRL+C/CTRL+D Handling Full implementation of the CTRL+C and CTRL+D processing as outlined in issue #3. A few other small fixes to tests and better output. --- .vscode/settings.json | 1 + src/tbp/driver.py | 72 ++++++++--- src/tbp/helpers.py | 4 + src/tbp/interpreter.py | 53 ++++---- tests/controlkeys_test.py | 251 ++++++++++++++++++++++++++++++++++++++ tests/driver_test.py | 4 +- 6 files changed, 336 insertions(+), 49 deletions(-) create mode 100644 tests/controlkeys_test.py diff --git a/.vscode/settings.json b/.vscode/settings.json index d7871fe..849b00c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -51,6 +51,7 @@ "Dependa", "Diffie", "dorny", + "ELIT", "EspaƱa", "Esser", "ETURN", diff --git a/src/tbp/driver.py b/src/tbp/driver.py index 6349bee..d1930f5 100644 --- a/src/tbp/driver.py +++ b/src/tbp/driver.py @@ -62,6 +62,7 @@ def __init__(self: Driver) -> None: # user can change this with: %opt run_file_load t|f self._run_after_file_load: bool = False + # pylint: disable=too-complex, too-many-branches def party_like_it_is_1976(self: Driver, options: Options) -> int: """ Entry point for the Tree Walking Interpreter. @@ -97,7 +98,9 @@ def party_like_it_is_1976(self: Driver, options: Options) -> int: input_list = options.commands.split("^") cmds_to_run.extend(input_list) - # Run the commands starting with the %of if present. + exit_code: int = 0 + + # Run the commands starting with the %lf if present. for cmd in cmds_to_run: if self._execute_line(cmd) is False: return 0 @@ -107,11 +110,52 @@ def party_like_it_is_1976(self: Driver, options: Options) -> int: # Run away! while continue_running is True: - prompt: str = self._build_prompt() - cmd_to_do = read_input(prompt) - continue_running = self._execute_line(cmd_to_do.strip()) + try: + prompt: str = self._build_prompt() + cmd_to_do = read_input(prompt) + continue_running = self._execute_line(cmd_to_do.strip()) + except (KeyboardInterrupt, EOFError) as exp: + # No matter what, we need to force a newline so the prompt + # doesn't stay on the same line looking ugly. + print_output("\n") + + # Reminder: + # CTRL+C generates a KeyboardInterrupt exception. + # CTRL+D generates a EOFError exception. + # How we process these is all dependent on the current + # state. + if self._interpreter.current_state == Interpreter.State.RUNNING_STATE: + if isinstance(exp, KeyboardInterrupt): + print_output( + "Keyboard Interrupt: Breaking out of program at line " + f"{self._interpreter.current_line_number()}.\n", + ) + self._interpreter.initialize_runtime_state() + elif self._interpreter.current_state in { + Interpreter.State.LINE_STATE, + Interpreter.State.BREAK_STATE, + }: + if isinstance(exp, EOFError): + print_output("EOF interrupt: exiting tbp.\n") + exit_code = 1 + continue_running = False + elif self._interpreter.current_state in { + Interpreter.State.FILE_STATE, + Interpreter.State.ERROR_FILE_STATE, + } and isinstance(exp, KeyboardInterrupt): + print_output( + "Keyboard Interrupt: Aborting file loading, " + "no program in memory.\n", + ) + # It's a FILE_STATE or ERROR_FILE_STATE so cancel everything. + self._interpreter.initialize_runtime_state() + self._interpreter.clear_program() + + print_output( + "\nThank you for using tbp! Your patronage is appreciated.\n", + ) - return 0 + return exit_code ########################################################################### # Private Helper Methods @@ -216,21 +260,14 @@ def _load_program_and_run(self: Driver, program: str) -> None: _CMD_REGEX = re.compile(_CMD_REGEX_STRING, re.IGNORECASE | re.VERBOSE) - # The circularity of the tools is weird. The Ruff warning suppression has - # to appear at the end of the line, but that makes the line too long for - # pylint. šŸ¤·šŸ¾ā€ā™€ļø - - # C901: https://docs.astral.sh/ruff/rules/complex-structure/ - # PLR0912: https://docs.astral.sh/ruff/rules/too-many-branches/ - + # I've bumped up the Ruff defaults to branches and returns. # While it would be easy to disable these complexity rules, I still think # they are valuable as a reminder to keep an eye on the procedure. There's # probably some table driven way to simplify this code, but I think that # would veer into the maintenance hell mode. Sometimes case statements are # the best way to go. - # pylint: disable=line-too-long - def _process_command_language(self: Driver, cmd: str) -> Driver.CmdResult: # noqa: C901, PLR0912 + def _process_command_language(self: Driver, cmd: str) -> Driver.CmdResult: """Do the '%' commands.""" # Pull out what the user want's to do. if (m := self._CMD_REGEX.match(cmd)) is None: @@ -241,9 +278,6 @@ def _process_command_language(self: Driver, cmd: str) -> Driver.CmdResult: # no match m.group(Driver._CMD_GROUP).lower(): case "q" | "quit": - print_output( - "\nThank you for using tbp! Your patronage is appreciated.\n", - ) return Driver.CmdResult.QUIT case "?": print_output(Driver._SHORTHELP) @@ -402,9 +436,7 @@ def _command_loadfile(self: Driver, filename: str) -> None: if program := load_program(filename): self._load_program_and_run(program) - # PLR0912: https://docs.astral.sh/ruff/rules/too-many-branches/ - # C901: https://docs.astral.sh/ruff/rules/complex-structure/ - def _command_opt(self: Driver, option: str, value: str) -> None: # noqa: C901, PLR0912 + def _command_opt(self: Driver, option: str, value: str) -> None: """Change run on load and timing options.""" if not (option := option.lower()): self._command_language_error("CLE #04: Required option is missing.") diff --git a/src/tbp/helpers.py b/src/tbp/helpers.py index 14b98ed..99b03cf 100644 --- a/src/tbp/helpers.py +++ b/src/tbp/helpers.py @@ -179,6 +179,8 @@ def limit_input(prompt: str, valid_input: list[str]) -> tuple[bool, str]: except (EOFError, KeyboardInterrupt): result_flag = False keep_asking = False + # Add a newline so we don't have the prompt looking weird. + print_output("\n") return False, "" @@ -202,8 +204,10 @@ def save_program(filename: str, program: str) -> bool: yes_no, ) if (overwrite is False) or (res == "n"): + print_output("Program not saved.\n") return False with the_file.open(mode="w", encoding="utf-8") as f: + print_output("Program saved.\n") f.write(program) except (RuntimeError, FileNotFoundError): print_output(f"CLE #12: Filename is invalid '{filename}'.\n") diff --git a/src/tbp/interpreter.py b/src/tbp/interpreter.py index e7f4ea5..35bbe7c 100644 --- a/src/tbp/interpreter.py +++ b/src/tbp/interpreter.py @@ -230,33 +230,32 @@ def interpret_buffer(self: Interpreter, source: str) -> bool: """ final_return: bool = True - try: - # Set the state flag so other methods know we are parsing a buffer. - self._the_state = Interpreter.State.FILE_STATE - # Set to False if there was an error parsing. - self._file_line = 1 - - # Fake reading this memory buffer as a file. I like this trick. - file = StringIO(source) - - current_line: str = file.readline() - while current_line: - # It's perfectly fine to have empty lines. - if (current_line != "\n") and ( - self.interpret_line(current_line) is False - ): - self._the_state = Interpreter.State.ERROR_FILE_STATE - final_return = False - current_line = file.readline() - self._file_line += 1 - - if self._the_state == Interpreter.State.ERROR_FILE_STATE: - # Clear out any loaded program so we don't have half programs - # floating around. - self.clear_program() - self.initialize_runtime_state() - finally: - self._the_state = Interpreter.State.LINE_STATE + + # Set the state flag so other methods know we are parsing a buffer. + self._the_state = Interpreter.State.FILE_STATE + # Set to False if there was an error parsing. + self._file_line = 1 + + # Fake reading this memory buffer as a file. I like this trick. + file = StringIO(source) + + current_line: str = file.readline() + while current_line: + # It's perfectly fine to have empty lines. + if (current_line != "\n") and (self.interpret_line(current_line) is False): + self._the_state = Interpreter.State.ERROR_FILE_STATE + final_return = False + current_line = file.readline() + self._file_line += 1 + + if self._the_state == Interpreter.State.ERROR_FILE_STATE: + # Clear out any loaded program so we don't have half programs + # floating around. + self.clear_program() + self.initialize_runtime_state() + + # We are done processing the file so go back to the normal state. + self._the_state = Interpreter.State.LINE_STATE return final_return diff --git a/tests/controlkeys_test.py b/tests/controlkeys_test.py new file mode 100644 index 0000000..8ebf947 --- /dev/null +++ b/tests/controlkeys_test.py @@ -0,0 +1,251 @@ +"""Unit tests handling CTRL+C and CTRL+D in the Driver class.""" + +############################################################################### +# Tiny BASIC in Python +# Licensed under the MIT License. +# Copyright (c) 2024 John Robbins +############################################################################### + +from __future__ import annotations + +import signal +import time +from threading import Thread +from typing import TYPE_CHECKING, Any + +from tbp.driver import Driver + +if TYPE_CHECKING: + from collections.abc import Generator + + import pytest + from pytest import CaptureFixture # noqa: PT013 + +empty_opts: Driver.Options = Driver.Options(nologo=True, file="", commands="") + +############################################################################### +# What you are about to see in this file is a crime against Python, testing, +# and all programming best practices. Heed my advice! Don't do this at home. šŸ˜¹ +# +# When developing the CTRL+C and CTRL+D (CTRL+Z, ENTER on Windows) handling for +# tbp, I needed a way to do those keystrokes as they would occur with the +# Python built in input function. Traversing windswept mountain ranges of basic +# testing content and wading through the ancient swamps of outdated forum +# posts, I've found nothing that talks about how to test your app with those +# keystrokes under pytest. +# +# In tbp, there's hundreds of tests that use the very cool Monkey Patching to +# replace the built in Python input function. However, those tests, and any +# discussion of testing the input function, assume you're pumping in nice and +# normal text and not any interesting keystrokes. After much trial and error, +# I found a way to "pretend" CTRL+C and CTRL+D occur so my testing covers those +# cases. +# +# The key (HAH!) to CTRL+C and CTRL+D is that they generate exceptions, +# KeyboardInterrupt and EOFError, respectively. The question is how to get +# those generated in the proper context of the input function while testing. +# With the traditional Monkey Patching, you provide an iterator that loops +# through an array of text, which returns each line as though the user entered +# it in the input function. +# +# My idea was to build a generator, fake_input below, that takes a list of +# those strings, and when it sees special characters, it would raise the +# exception, just like occurs when using the input function. Turns out, that +# works! However, and this is super important, you must be sure your code +# handles the exception you raised and does not let it propagate out of your +# code. Also, because the generator called by pytest can get in a weird state +# when raising a KeyboardInterrupt for CTRL+C, always pass a default value to +# next. In my case, I use the tbp command language %q to quit. +# +# Like all hacks, I have no idea if this will keep working in the future. +# +# ELIT CAVE! - Developer beware! +############################################################################### + + +CTRL_C = "`" +CTRL_D = "^" + + +def fake_input(items: list[str]) -> Generator[str, Any, None]: + """Fake input, but force exceptions to occur.""" + for i in items: + if i == CTRL_C: + raise KeyboardInterrupt + if i == CTRL_D: + raise EOFError + yield i + + +def test_ctrl_d_from_debug_prompt( + capsys: CaptureFixture[str], + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Test CTRL+D from a breakpoint prompt.""" + cmds = [ + "10 LET C = 200", + "20 LET H = 200", + "700 END", + "%bp 10", + "RUN", + CTRL_D, + "%q", + ] + + driver: Driver = Driver() + # Create the generator. + thing = fake_input(cmds) + ret = 0 + monkeypatch.setattr("builtins.input", lambda _: next(thing, "%q")) + ret = driver.party_like_it_is_1976(empty_opts) + output = capsys.readouterr() + assert ret == 1 + assert "EOF interrupt: exiting tbp." in output.out + + +def test_ctrl_d_from_normal_prompt( + capsys: CaptureFixture[str], + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Test CTRL+D from a normal prompt.""" + cmds = [ + CTRL_D, + "%q", + ] + + driver: Driver = Driver() + thing = fake_input(cmds) + ret = 0 + monkeypatch.setattr("builtins.input", lambda _: next(thing, "%q")) + ret = driver.party_like_it_is_1976(empty_opts) + output = capsys.readouterr() + assert ret == 1 + assert "EOF interrupt: exiting tbp." in output.out + + +def test_ctrl_c_from_debug_prompt( + capsys: CaptureFixture[str], + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Test CTRL+C from a breakpoint prompt.""" + cmds = [ + "10 LET C = 200", + "20 LET H = 200", + "700 END", + "%bp 10", + "RUN", + CTRL_C, + "%q", + ] + + driver: Driver = Driver() + # Create the generator. + thing = fake_input(cmds) + monkeypatch.setattr("builtins.input", lambda _: next(thing, "%q")) + ret = driver.party_like_it_is_1976(empty_opts) + output = capsys.readouterr() + assert ret == 0 + assert "\n\n" in output.out + + +def test_ctrl_c_from_normal_prompt( + capsys: CaptureFixture[str], + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Test CTRL+C from a normal prompt.""" + cmds = [ + CTRL_C, + "%q", + ] + + driver: Driver = Driver() + thing = fake_input(cmds) + monkeypatch.setattr("builtins.input", lambda _: next(thing, "%q")) + ret = driver.party_like_it_is_1976(empty_opts) + output = capsys.readouterr() + assert ret == 0 + assert "\n\n" in output.out + + +def test_ctrl_c_from_running_code( + capsys: CaptureFixture[str], + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Test CTRL+C from a program.""" + cmds = [ + "10 INPUT A", + "20 END", + "RUN", + CTRL_C, + "%q", + ] + + # Note that this works on the running state because the program is sitting + # at 10 INPUT A, which gets the fake CTRL+C. + driver: Driver = Driver() + thing = fake_input(cmds) + monkeypatch.setattr("builtins.input", lambda _: next(thing, "%q")) + ret = driver.party_like_it_is_1976(empty_opts) + output = capsys.readouterr() + assert ret == 0 + assert "Keyboard Interrupt: Breaking out of program at line 10." in output.out + + +class AsyncControlC(Thread): + """A class who's job is to signal CTRL+C.""" + + def run(self: AsyncControlC) -> None: + """Fakes a CTRL+C after a minor delay.""" + time.sleep(0.01) + signal.raise_signal(signal.SIGINT) + + +def test_ctrl_c_from_program_load( + capsys: CaptureFixture[str], + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Test CTRL+C from loading a file.""" + cmds = [ + "%opt log t", + '%lf "./examples/adventure.tbp"', + "%opt log f", + "%q", + ] + + driver: Driver = Driver() + thing = fake_input(cmds) + ret = 0 + # In Python a CTRL+C signal is handled on the main thread, which is where + # this method is running. I'll signal a CTRL+C from another thread so + # control transfers back here where it will interrupt the large file + # loading. I've also slowed down the loading by turning on logging. + my_thread = AsyncControlC() + my_thread.start() + + monkeypatch.setattr("builtins.input", lambda _: next(thing, "%q")) + ret = driver.party_like_it_is_1976(empty_opts) + output = capsys.readouterr() + assert ret == 0 + assert "Aborting file loading" in output.out + + +def test_ctrl_c_saving_file( + capsys: CaptureFixture[str], + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Test CTRL+C when saving a file.""" + cmds = [ + "10 INPUT A", + "20 END", + '%sf "./examples/tbp.tbp"', + CTRL_C, + "%q", + ] + + driver: Driver = Driver() + thing = fake_input(cmds) + monkeypatch.setattr("builtins.input", lambda _: next(thing, "%q")) + ret = driver.party_like_it_is_1976(empty_opts) + output = capsys.readouterr() + assert ret == 0 + assert "Program not saved." in output.out diff --git a/tests/driver_test.py b/tests/driver_test.py index 5c33044..8de03df 100644 --- a/tests/driver_test.py +++ b/tests/driver_test.py @@ -347,12 +347,12 @@ def test_commands_quit( capsys: CaptureFixture[str], ) -> None: """Test `tbp -c stuff'.""" - opts = Driver.Options(nologo=True, file="", commands="%q") + opts = Driver.Options(nologo=True, file="", commands="%opt log^%q") driver: Driver = Driver() ret: int = driver.party_like_it_is_1976(opts) output = capsys.readouterr() assert ret == 0 - assert "Thank you for using tbp! Your patronage is appreciated." in output.out + assert "Option: logging is False." in output.out def test_line_timer( From 02c2726297b1962c5bc5bbd64cdce50d37452cce Mon Sep 17 00:00:00 2001 From: John Robbins Date: Thu, 12 Sep 2024 22:46:36 -0400 Subject: [PATCH 08/21] Added CTRL key and edits Added CTRL+C/CTRL+D usage to FAQ. Minor edits. --- .vscode/ltex.dictionary.en-US.txt | 4 + .vscode/ltex.hiddenFalsePositives.en-US.txt | 1 + docs/docs/faq.md | 16 +++- docs/docs/thoughts.md | 92 +++++++++------------ 4 files changed, 59 insertions(+), 54 deletions(-) diff --git a/.vscode/ltex.dictionary.en-US.txt b/.vscode/ltex.dictionary.en-US.txt index c10a939..5080b38 100644 --- a/.vscode/ltex.dictionary.en-US.txt +++ b/.vscode/ltex.dictionary.en-US.txt @@ -77,3 +77,7 @@ monorepo %endraw% hack-arounds mergeable +KeyBoardInterrupt +EOFError +controlkeys +uv diff --git a/.vscode/ltex.hiddenFalsePositives.en-US.txt b/.vscode/ltex.hiddenFalsePositives.en-US.txt index f4f5663..1719302 100644 --- a/.vscode/ltex.hiddenFalsePositives.en-US.txt +++ b/.vscode/ltex.hiddenFalsePositives.en-US.txt @@ -33,3 +33,4 @@ {"rule":"TOO_LONG_PARAGRAPH","sentence":"^\\Q^8: And pointing the fickle finger of blame!\\E$"} {"rule":"UNLIKELY_OPENING_PUNCTUATION","sentence":"^\\Q:crossed_fingers: (Thanks for your patience!)\\E$"} {"rule":"WORD_CONTAINS_UNDERSCORE","sentence":"^\\Q:crossed_fingers: (Thanks for your patience!)\\E$"} +{"rule":"NON_STANDARD_WORD","sentence":"^\\QHow does tbp handle CTRL+C and CTRL+D (CTRL+Z, ENTER on Windows)?\\E$"} diff --git a/docs/docs/faq.md b/docs/docs/faq.md index 942fc72..1e570cd 100644 --- a/docs/docs/faq.md +++ b/docs/docs/faq.md @@ -14,9 +14,23 @@ permalink: faq --- +## General Usage + +- How does tbp handle `CTRL+C` and `CTRL+D` (`CTRL+Z`, ENTER on Windows)? + +For tbp, the Python REPL served as a model. As a reminder, when you enter these keys, they are not entries, but generate exceptions. `CTRL+C` is [KeyBoardInterrupt](https://docs.python.org/3/library/exceptions.html#KeyboardInterrupt) and `CTRL+D` is [EOFError](https://docs.python.org/3/library/exceptions.html#EOFError). + +When you are at a normal tbp prompt, `tbp:>` or a breakpoint prompt, `DEBUG(410:>`, `CTRL+C` does nothing, but `CTRL+D` will immediately terminate the application. + +If a Tiny BASIC program is running, both `CTRL+C` and `CTRL+D` will break the program and reset tbp's internal state as though it was typed in or loaded. + +If you are in the middle of loading a file with [%loadfile](tbp-command-language#loading-files-loadfile--lf), a `CTRL+C` will abort file loading, reset the tbp internal state, and clear any loaded program from memory. + +Figuring out a way to test these keystrokes was quite the adventure. You can check out the hack by reading [controlkeys_test.py](https://github.com/John-Robbins/tbp/blob/main/tests/controlkeys_test.py). + ## Tiny BASIC -- Why is `Syntax Error: Error #020: LET is missing an '=', but found 'o` the most common error when entering code at the tbp prompt/? +- Why is `Syntax Error: Error #020: LET is missing an '=', but found '{var}'` the most common error when entering code at the tbp prompt? Tiny BASIC allows two forms of `LET` statements: diff --git a/docs/docs/thoughts.md b/docs/docs/thoughts.md index 12bfada..9ed2583 100644 --- a/docs/docs/thoughts.md +++ b/docs/docs/thoughts.md @@ -87,12 +87,12 @@ Runtime Error: Error #336: Accessing uninitialized variable 'D'. -------------------------------------------------------^ ``` -Another area where I thought I did well was the unit tests and code coverage. With 280-unit tests and 99% coverage overall, I flushed a ton of bugs out with the tests. Additionally, I have mypy, ruff, and pylint cranked up to their equivalent strict mode with only 13 rules disabled in pyproject.toml. I always got a little thrill running `make` and seeing all those checks and tests. +Another area where I thought I did well was the unit tests and code coverage. With 290-unit tests and 99.88% coverage overall, I flushed a ton of bugs out with those tests. Additionally, I have mypy, ruff, and pylint cranked up to their equivalent strict mode with only 13 rules disabled in pyproject.toml. I also bumped up a few of the complexity numbers in pyproject.toml. I always got a little thrill running `make` in my macOS terminal and seeing all those checks and tests. ```text % make mypy --config-file pyproject.toml src/ tests/ -Success: no issues found in 28 source files +Success: no issues found in 29 source files ruff check --config ./pyproject.toml src/ tests/ All checks passed! pylint --rcfile pyproject.toml src/ tests/ @@ -100,64 +100,48 @@ pylint --rcfile pyproject.toml src/ tests/ -------------------------------------------------------------------- Your code has been rated at 10.00/10 (previous run: 10.00/10, +0.00) -coverage run -m pytest --maxfail=1 -console_output_style=classic -=================== test session starts =================== +coverage run -m pytest --maxfail=1 -console_output_style=classic --junit-xml=.test-results.xml +============================= test session starts ============================== platform darwin -- Python 3.12.1, pytest-8.3.2, pluggy-1.5.0 rootdir: /Users/johnrobbins/Code/tbp configfile: onsole_output_style=classic plugins: anyio-4.4.0 -collected 280 items - -tests/cmd_lang_test.py .......... [ 3%] -tests/debugger_test.py ..................... [ 11%] -tests/driver_test.py ........................ [ 19%] -tests/helpers_test.py ........... [ 23%] -tests/interpreter_test.py ......................... [ 32%] -................................................ [ 49%] -tests/lang_test.py .. [ 50%] -tests/linter_test.py .......................... [ 59%] -tests/memory_test.py . [ 60%] -tests/parser_test.py .............................. [ 70%] -...................................... [ 84%] -tests/scanner_test.py ............................. [ 94%] -...... [ 96%] -tests/symboltable_test.py ........ [ 99%] -tests/tokens_test.py . [100%] - -=================== 280 passed in 0.95s =================== -coverage report --precision=2 --show-missing --sort=Cover +collected 290 items + +tests/cmd_lang_test.py ........... [ 3%] +tests/controlkeys_test.py ....... [ 6%] +tests/debugger_test.py ....................... [ 14%] +tests/driver_test.py ........................ [ 22%] +tests/helpers_test.py ........... [ 26%] +tests/interpreter_test.py .............................................. [ 42%] +........................... [ 51%] +tests/lang_test.py .. [ 52%] +tests/linter_test.py .......................... [ 61%] +tests/memory_test.py . [ 61%] +tests/parser_test.py ................................................... [ 78%] +................. [ 84%] +tests/scanner_test.py ................................... [ 96%] +tests/symboltable_test.py ........ [ 99%] +tests/tokens_test.py . [100%] + +------ generated xml file: /Users/johnrobbins/Code/tbp/.test-results.xml ------- +============================= 290 passed in 1.06s ============================== +coverage report --precision=2 --show-missing --sort=Cover --skip-covered Name Stmts Miss Branch BrPart Cover Missing ------------------------------------------------------------------------ -src/tbp/helpers.py 78 6 20 1 92.86% 175-179, 223-224 -src/tbp/driver.py 216 4 108 6 96.91% 86->88, 89->91, 138, 166, 311-312, 389->exit, 421->exit +src/tbp/helpers.py 79 4 22 1 95.05% 21-23, 231-232 src/tbp/astprinter.py 142 3 28 2 97.06% 111-112, 277 -src/tbp/interpreter.py 499 5 186 7 98.25% 201->205, 356->360, 415->420, 578->exit, 600->603, 1047-1048, 1134-1136 -src/tbp/parser.py 275 3 116 2 98.72% 359->364, 486-488, 509->526 +src/tbp/driver.py 237 2 120 8 97.20% 87->89, 90->92, 128->112, 142->112, 180, 211, 436->exit, 466->exit +src/tbp/parser.py 275 3 116 2 98.72% 357->362, 484-486, 507->524 +src/tbp/interpreter.py 495 2 186 6 98.83% 209->213, 359->363, 418->423, 581->exit, 603->606, 1053-1054 src/tbp/languageitems.py 170 1 8 1 98.88% 192 src/tbp/scanner.py 242 1 128 3 98.92% 211->exit, 320, 356->360 +tests/controlkeys_test.py 88 0 20 1 99.07% 72->exit tests/interpreter_test.py 510 0 14 2 99.62% 885->exit, 904->exit -src/tbp/__init__.py 3 0 0 0 100.00% -src/tbp/errors.py 17 0 0 0 100.00% -src/tbp/linter.py 107 0 32 0 100.00% -src/tbp/memory.py 16 0 4 0 100.00% -src/tbp/symboltable.py 30 0 16 0 100.00% -src/tbp/tokens.py 51 0 2 0 100.00% -tests/cmd_lang_test.py 101 0 0 0 100.00% -tests/debugger_test.py 177 0 42 0 100.00% -tests/driver_test.py 203 0 34 0 100.00% -tests/helpers_test.py 85 0 2 0 100.00% -tests/lang_test.py 10 0 0 0 100.00% -tests/linter_test.py 217 0 52 0 100.00% -tests/memory_test.py 11 0 4 0 100.00% -tests/parser_test.py 348 0 44 0 100.00% -tests/programs.py 4 0 0 0 100.00% -tests/scanner_test.py 430 0 18 0 100.00% -tests/symboltable_test.py 43 0 10 0 100.00% -tests/tokens_test.py 5 0 0 0 100.00% ------------------------------------------------------------------------ -TOTAL 3990 23 868 24 99.03% +TOTAL 4117 16 914 26 99.17% -1 empty file skipped. +19 files skipped due to complete coverage. coverage lcov Wrote LCOV report to .coverage.lcov ``` @@ -228,7 +212,7 @@ One of the first things I learned on my Python journey is that there is far more Another key trick I employed simply trying to start learning Python pay for the [Kagi](https://kagi.com) search engine because it lets you easily [block sites](https://help.kagi.com/kagi/features/website-info-personalized-results.html#personalized-results) with low quality content from appearing in your search results. Additionally, Kagi has through its [Lenses](https://help.kagi.com/kagi/features/lenses.html) feature a fantastic way to limit searches to forums. -The best way I can sum up the state of trying to learn Python in 2024 is to reference that meme of [How to Draw an Owl](https://www.reddit.com/r/pics/comments/d3zhx/how_to_draw_an_owl/). Step one is to draw a circle, step two is to draw the rest of the owl. This is **not** a joke. Once you understand [list comprehension](https://docs.python.org/3/glossary.html#term-list-comprehension), where do you go? As you can hopefully see with tbp, I think I've developed a passable understanding of the Python language, but the next step is missing. Where's the introduction to infrastructure? Where is the walk-through content on setting up your development environment? How do imports work? Why are there seemingly millions of package installers? Which one should I use? How do I set up projects with multiple modules? Why are there so many build/packaging systems? These are the questions I still have about *after* finishing tbp. +The best way I can sum up the state of trying to learn Python in 2024 is to reference that meme of [How to Draw an Owl](https://www.reddit.com/r/pics/comments/d3zhx/how_to_draw_an_owl/). Step one is to draw a circle, step two is to draw the rest of the owl. This is **not** a joke. Once you understand [list comprehension](https://docs.python.org/3/glossary.html#term-list-comprehension), where do you go? As you can hopefully see with tbp, I think I've developed a passable understanding of the Python language, but the next step is missing. Where's the introduction to infrastructure? Where is the walk-through content on setting up your development environment? How do imports work? Why are there seemingly millions of package installers? Which one should I use? How do I set up projects with multiple modules? Why are there so many build systems? These are the questions I still have about *after* finishing tbp. Earlier I mentioned that I started tbp with all my code in a single directory and used pytest to run my code. That worked well, and my focus was completely on the problem domain of programming languages. As I get the scanner finished, and I am getting close to finishing the parser, I figure it's time for me to look at getting tbp set up as a module. Looking at projects on GitHub, I saw the source layout they used so moved the tbp source (`./src/tbp`) and test code (`./test`) to mimic those. That's when I got to find the unique Python joy of import problems. Visual Studio Code was happily showing me IntelliSense when I added a new test, but pytest just gave me errors whenever I tried to run tests. I spent hours trying to figure out the import problems until I gave up and put all code back in the same directory. At least I knew I could work on my problem, even though I knew I'm not being Pythonic. @@ -250,6 +234,8 @@ Obviously, the Python community has done and will continue to do wonderful thing Please understand I'm just trying to report my story of trying to develop my first Python module as a novice Python developer in 2024. The problems might be all on me, but it felt much harder than it should have been. Python desperately needs an equivalent to Rust's glorious [Cargo Book](https://doc.rust-lang.org/cargo/). +After a couple of weeks in GitHub Actions land, see below, I see our friends at [Astral](https://astral.sh) have released [uv](https://docs.astral.sh/uv/), which based on what I'm seeing on blogs and social media, is garnering massive amounts of excitement. Sadly, I still can't find any overarching explanation of how I, a Python novice, is supposed to create a project following best practices in order to take advantage of uv's speed and completeness. It feels like the Python community has all of this information in their bones, but why can't it be shared for novices like me? + ## GitHub and GitHub Actions While I started development before git was a gleam in Linus Torvalds' eye, I'd used git and GitHub quite a bit back in the olden days. However, in the time I've been away from the world of coding, there's been some huge changes at GitHub. Like, :sparkles:WOW:sparkles: levels of changes! As keeping with my obsessive note-taking inclination, I wrote up my thoughts and bumps with the new amazing GitHub Actions, the security, and project management features. I'll also post this in the GitHub Actions discussion area in the hopes it helps the product team. Especially as I'm a first-time user of GitHub Actions, which must make me a unicorn in today's dev world. @@ -270,7 +256,7 @@ The documentation has a [Use cases and examples](https://docs.github.com/en/acti In the overall [documentation](https://docs.github.com/en/actions) I found the organization a little confusing. For example, the [Writing workflows](https://docs.github.com/en/actions/writing-workflows) list what I assumed was a table of contents for 27 articles about workflows that you should read in order. For example, it lists [Using workflow templates](https://docs.github.com/en/actions/writing-workflows/using-workflow-templates) followed by [Choosing when your workflow runs](https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs). However, reading [Using workflow templates](https://docs.github.com/en/actions/writing-workflows/using-workflow-templates), does not have any link for the [Choosing when your workflow runs](https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs), those it does have links for other parts of the documentation. My goal was to read a logical flow through learning about workflows starting from zero, but I was wandering to places that assumed I'd read something else previously. -The workflow documentation got into details quickly, when I was simply trying to get a handle on [Building and testing Python](https://docs.github.com/en/actions/use-cases-and-examples/building-and-testing/building-and-testing-python). Personally, I feel it would be better to have a step-by-step tutorial of a simple calculator program. You start with the code, and you build the action step by step. The idea is that by the end the entire YAML file shows building, linting, and testing. The repository has everything in place so that PRs trigger the proper workflow and so on. That's what I ended up doing on a throw away repository to see how everything fit together. If this is something that GitHub doesn't feel is a good fit for the documentation, someone should do this in their blog as they would get a lot of traffic. +The workflow documentation got into details quickly, when I was simply trying to get a handle on [Building and testing Python](https://docs.github.com/en/actions/use-cases-and-examples/building-and-testing/building-and-testing-python). Personally, I feel it would be better to have a step-by-step tutorial of a simple calculator program. You start with the code, and you build the action step by step. The idea is that by the end the entire YAML file shows building, linting, and testing. The repository has everything in place so that PRs trigger the proper workflow and so on. That's what I ended up doing on a throw away repository to see how everything fit together. If this is something that GitHub doesn't feel is a good fit for the documentation, someone should do this in their blog as they would get a lot of traffic. KEY POINT: It has to be kept up to date! In all the documentation is pretty good, but a good walk through at the start would be great to have. The other issue is that the docs go deep fast, and I felt there's a lot of middle ground missing. I ended up with a monolithic workflow [file](https://github.com/John-Robbins/tbp/blob/b14ea797cb76c26c33005a4ce7ec768be4ec7a92/.github/workflows/Code-CI.yml) where I was installing, type checking, linting, testing, and producing code coverage on each operating system in the matrix. I knew that wasn't what I wanted in the long run but wasn't sure what the best practices were for a GitHub Action. @@ -329,7 +315,7 @@ jobs: In the above example, I'm repeating the exact same setup each time. Does the `test-cov-job` always have to repeat the setup? Can the job assume the project is ready to go? I never found anything definitive in the documentation and looking through many examples, it seems you must repeat the setup every time. As I mentioned in the comment above, Always Repeat Yourself feels wrong to me. -Thinking that [reusable workflows](https://docs.github.com/en/actions/sharing-automations/reusing-workflows) would be the answer, I went down that path. I wanted to encapsulate the common setup work, and pass in the `runs-on` value. Although it's been almost three years since someone asked, there's no official support for [array input type support](https://github.com/orgs/community/discussions/11692). I fumbled around with an [idea](https://github.com/orgs/community/discussions/11692#discussioncomment-3541856) I found in the actions discussions, but I could never get it to work. I gave up on reusable workflows and repeated myself because working code always wins. +Thinking that [reusable workflows](https://docs.github.com/en/actions/sharing-automations/reusing-workflows) would be the answer, I went down that path. I wanted to encapsulate the common setup work, and pass in the `runs-on` value. Although it's been almost three years since someone asked, there's no official support for [array input type support](https://github.com/orgs/community/discussions/11692). I fumbled around with an [idea](https://github.com/orgs/community/discussions/11692#discussioncomment-3541856) I found in the actions discussions, but I could never get it to work. I gave up on reusable workflows and repeatedly repeated steps because working code always wins. After a bit of trial and error, I had two workflows, one for code and one for docs. They were working great where I could run them manually on branches, and automatically on pull requests and pushes to main. Life was good, and then my code workflow stopped working. My workflow worked on a Sunday and died on a Monday. Being brand new to workflows, I struggled for several hours as to why my artifact uploads no longer worked. Yeah, you know where this is going: [Upcoming breaking change: Hidden Files will be excluded by default](https://github.com/actions/upload-artifact/issues/602). Why did a breaking change not involve a major version bump? I'm old enough to remember when a [GitHub co-founder](http://tom.preston-werner.com/) first proposed [SemVer](https://semver.org/spec/v2.0.0.html), which I thought was a very good attempt at bringing sanity to version numbers. Is SemVer merely a suggestion at GitHub these days? @@ -525,9 +511,9 @@ My original goal for tbp was to do a project with Tiny BASIC in three phases wit 2. A virtual machine interpreter. 3. A Tiny BASIC compiler to a virtual machine. -I think tbp part 1 turned out OK, but there's plenty of room for improvement. Going into the project, I thought 60% of my problems would be on the programming languages side. I had no idea that the majority of problems would be basic infrastructure issues with Python. If I had started in C, I know I would have finished with phases 1 and a big chunk of 2 in the time it took me to get tbp done. I expected to be developing slower as I learned Python, but not this slow. Being completely truthful, Python has left a little bitterness in my mouth. I think, but don't know for sure, I've gotten over the first major Python development hurdle developing a module, but will I get over the next one as a novice Python developer? Given what I've gone through so far, I don't know whether I'll have the patience or energy to go through this again. +I think tbp part 1 turned out OK, but there's plenty of room for improvement. Going into the project, I thought 60% of my problems would be on the programming languages side. I had no idea that the majority of problems would be basic infrastructure issues with Python. If I had started in C, I know I would have finished with phases 1 and a big chunk of 2 in the time it took me to get tbp done. I expected to be developing slower as I learned Python, but not this slow. I think, but don't know for sure, I've gotten over the first major Python development hurdle developing a module, but will I get over the next one as a novice Python developer? Given what I've gone through so far, I don't know whether I'll have the patience or energy to go through something like that again. -When I tackle phases two and three, I'll probably use another language. +When I tackle phases two and three, I'll most likely use another language. --- From 9b461a0bcf566860dcf9ab0c556a20a5c4e5ec9f Mon Sep 17 00:00:00 2001 From: John Robbins Date: Fri, 13 Sep 2024 10:54:59 -0400 Subject: [PATCH 09/21] Better error reporting For `INPUT` added error string on what failed. For unexpected values scanning and parsing, now reports the escaped character so '\n' instead of an actual CRLF. --- src/tbp/interpreter.py | 3 ++- src/tbp/parser.py | 2 +- src/tbp/scanner.py | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/tbp/interpreter.py b/src/tbp/interpreter.py index 35bbe7c..2f13a52 100644 --- a/src/tbp/interpreter.py +++ b/src/tbp/interpreter.py @@ -1091,8 +1091,9 @@ def _assign_input_expression( tokens = self._parser.parse_tokens(lex_tokens) for token in tokens: self._evaluate(token) - except TbpBaseError: + except TbpBaseError as err: # The rhs value is invalid. + print_output(f"{err.friendly_name}: {err.message}\n") return_value = False msg = ( f"Error #351: Invalid value in INPUT: '{var_value}'. Setting " diff --git a/src/tbp/parser.py b/src/tbp/parser.py index 883aae2..11f81f1 100644 --- a/src/tbp/parser.py +++ b/src/tbp/parser.py @@ -244,7 +244,7 @@ def _primary(self: Parser) -> LanguageItem: # If we get here, we have a problem. self._report_error( "Error #293: Syntax error - " - f"unexpected expression '{self._peek().lexeme!s}'.", + f"unexpected expression {self._peek().lexeme!r}.", ) return cast(LanguageItem, None) # pragma: no cover diff --git a/src/tbp/scanner.py b/src/tbp/scanner.py index 1d7e5ad..3a9a804 100644 --- a/src/tbp/scanner.py +++ b/src/tbp/scanner.py @@ -162,7 +162,7 @@ def _scan_token(self: Scanner) -> None: # noqa: C901, PLR0912 else: self._report_error( "Error #293: Syntax error - unexpected expression : " - f"'{curr_character}'", + f"{curr_character!r}", ) ########################################################################### From b6983625c58a39d9baf18e7d3753440337ebe847 Mon Sep 17 00:00:00 2001 From: John Robbins Date: Fri, 13 Sep 2024 17:57:46 -0400 Subject: [PATCH 10/21] Forgot to include these! In moving repositories, I forgot to add these files. Whoops! --- grammar/grammar_tests.txt | 73 +++++++++++++++++++++++++++++++ grammar/tiny_basic_grammar.ebnf | 77 +++++++++++++++++++++++++++++++++ 2 files changed, 150 insertions(+) create mode 100644 grammar/grammar_tests.txt create mode 100644 grammar/tiny_basic_grammar.ebnf diff --git a/grammar/grammar_tests.txt b/grammar/grammar_tests.txt new file mode 100644 index 0000000..d99b74c --- /dev/null +++ b/grammar/grammar_tests.txt @@ -0,0 +1,73 @@ +(* +Test strings for the tbp grammar. + +You can test the grammar at http://instaparse.mojombo.com. +*) + +100REM +REM +100RETURN +RETURN +100CLEAR +CLEAR +100END +END +INPUTA +INPUTA,B +100INPUTA +100INPUTZ,Y,X +GOTO100 +100GOTO100 +100GOTOA +GOSUB100 +100GOSUB100 +100GOSUBA+B/10 +LETA=10 +LETA=(A+B) +100LETA=10 +100LETA=(A+B) +A=10 +100A=10 +LIST +100LIST +100LIST10 +100LIST10,11 +100LISTA+B,B+A +RUN +RUN100 +100RUN +100RUN100 +100RUN100,200 +100RUNA +100RUNA,B +100RUN(A+B),B+A +PR +100PR +PRINT +100PRINT +100PRA +100PRINTA +100PRINTA+B +100PRINT(A+B) +100PRINTA,B +100PRINT(A+B),200 +100PR200,(B/A) +100PRA,A,A +100PRINT10+20;11,A +PRINT"CHARLIE" +100PRINT"SAMMY" +100PRINTA,"HAROLD" +100PRINT"SCOUT",10+13;E*2 +IFA=1THENGOTO100 +IFA=1GOTO100 +100IFRND(10)>100THENGOSUB20 +IFRND(10)>100THENGOSUB20 +100IFRND(10)>100GOSUB20 +IFRND(10)>100GOSUB20 +IFA+B+C+D+E+F+G+H+I0IFI<10GOTO3270 +PRINT-(A+B) +100PRINT-A+(-B)-(+C) +100 diff --git a/grammar/tiny_basic_grammar.ebnf b/grammar/tiny_basic_grammar.ebnf new file mode 100644 index 0000000..3ec2c15 --- /dev/null +++ b/grammar/tiny_basic_grammar.ebnf @@ -0,0 +1,77 @@ +(* The Extended Bacus-Naur Form grammar for Tiny BASIC in Python. *) +(* You can test this grammar at http://instaparse.mojombo.com. *) +(* This can also be tested here: https://mdkrajnak.github.io/ebnftest/. *) + +(* A Tiny BASIC program consists of lines. *) + ::= + EOF ; + +(* There's three types of lines, direct execution, program line, and *) +(* just a line number (to delete a program line.*) + ::= ( | | ) CRLF + +(* All the statements for the Tiny BASIC language. I used the Tiny BASIC *) +(* User's Manual and looking at a lot of BASIC source code to validate how *) +(*statements are used. I think this is close to the original specifications. *) + ::= "REM" * + | "RETURN" + | "CLEAR" + | "END" + | "INPUT" + | "GOTO" + | "GOSUB" + | ("LET")? "=" + | "LIST" ()? + | "RUN" ()? + | ("PRINT" | "PR") ()? + | "IF" ("THEN")? + ; + +(* Tiny BASIC supports two functions. A random number generator and a *) +(* function to invoke an assembly language subroutine. *) + ::= "RND" "(" ")" + | "USR" "(" ")" + ; + +(* The usual expressions. *) + ::= ; + ::= (("+"|"-") )* ; + ::= (("*"|"/") )* ; + ::= ("+"|"-") + | ; + ::= + | + | + | "(" ")" + ; + +(* All the relational operations supported by the IF statement.*) + ::= ("<" (">" | "=")?) + | (">" ("<" | "=")?) + | "=" + ; + +(* The list of identifiers the INPUT command supports. *) + ::= ("," )* ; + +(* LIST and RUN support multiple expressions as arguments. For RUN, any *) +(* arguments are cached and on the first INPUT statement, those values are *) +(* placed in the identifiers specified as arguments to INPUT. *) + ::= ("," )* + +(* PRINT has some special consideration as it is the only statement that can *) +(* work with strings. *) + ::= ( | ) ((","|";") ( | ))* ; + +(* All the terminals. *) + ::= #'[A-Z]' + ::= + ; + ::= #'[0-9]' ; + ::= '"' ()+ '"' + +(* This regex is here so https://github.com/Engelberg/instaparse is happy. *) +(* It returns all the characters in the printable ASCII range. *) + ::= #'[^~,]' ; +(* The terminator for all lines *) +CRLF ::= "\n" ; +(* Finally, we are done parsing! *) +EOF ::= epsilon ; From 8c147b82638118ec7716a0899fe8d68b407e6c2b Mon Sep 17 00:00:00 2001 From: John Robbins Date: Fri, 13 Sep 2024 18:08:33 -0400 Subject: [PATCH 11/21] Adding cov report to output. Closes #44. --- .github/workflows/CI.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 5a2f0e4..03d8fe2 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -276,11 +276,11 @@ jobs: # Get the code coverage data. run: | coverage combine - coverage report --precision=2 --show-missing --sort=Cover --skip-covered + coverage report --precision=2 --show-missing --sort=Cover --skip-covered >> $GITHUB_STEP_SUMMARY coverage json --fail-under=98 export TOTAL_COVERAGE=$(python -c "import json;print(round(float(json.load(open('coverage.json'))['totals']['percent_covered']),2))") echo "total_coverage=$TOTAL_COVERAGE" >> $GITHUB_ENV - echo "## Total coverage: :fire: ${TOTAL_COVERAGE}% :fireworks:" >> $GITHUB_STEP_SUMMARY + echo "## Total Coverage: :fire: ${TOTAL_COVERAGE}% :fire:" >> $GITHUB_STEP_SUMMARY - name: "Make Coverage Badge" # Code coverage is only updated on main branch. if: (github.ref == 'refs/heads/main') From f79d64f2d448647a39120fc5ddcf1a08c56e7248 Mon Sep 17 00:00:00 2001 From: John Robbins Date: Fri, 13 Sep 2024 18:15:12 -0400 Subject: [PATCH 12/21] Tweaking output Needed to add ``test...``` around the output to get it rendered correctly. --- .github/workflows/CI.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 03d8fe2..083edaa 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -273,10 +273,14 @@ jobs: pattern: covdata-* merge-multiple: true - name: "Combine and Report" - # Get the code coverage data. + # Get the code coverage data. Since GITHUB_STEP_SUMMARY is markdown, I + # need to add the ```test...``` around the report output so it's rendered + # as text and looks normal. run: | coverage combine + echo "```text" coverage report --precision=2 --show-missing --sort=Cover --skip-covered >> $GITHUB_STEP_SUMMARY + echo "```" coverage json --fail-under=98 export TOTAL_COVERAGE=$(python -c "import json;print(round(float(json.load(open('coverage.json'))['totals']['percent_covered']),2))") echo "total_coverage=$TOTAL_COVERAGE" >> $GITHUB_ENV From 850621e5c1730abad4ab6f3629626ed637b7c010 Mon Sep 17 00:00:00 2001 From: John Robbins Date: Fri, 13 Sep 2024 18:20:20 -0400 Subject: [PATCH 13/21] Doh! Need to direct text correctly. echo without >> $GITHUB_STEP_SUMMARY isn't helpful. --- .github/workflows/CI.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 083edaa..832fa95 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -278,9 +278,9 @@ jobs: # as text and looks normal. run: | coverage combine - echo "```text" + echo "```text" >> $GITHUB_STEP_SUMMARY coverage report --precision=2 --show-missing --sort=Cover --skip-covered >> $GITHUB_STEP_SUMMARY - echo "```" + echo "```" >> $GITHUB_STEP_SUMMARY coverage json --fail-under=98 export TOTAL_COVERAGE=$(python -c "import json;print(round(float(json.load(open('coverage.json'))['totals']['percent_covered']),2))") echo "total_coverage=$TOTAL_COVERAGE" >> $GITHUB_ENV From 88ee0657c80cbe88d5177f818d80f0ae86cdd509 Mon Sep 17 00:00:00 2001 From: John Robbins Date: Fri, 13 Sep 2024 19:55:31 -0400 Subject: [PATCH 14/21] Edit-CRY-Repeat --- .github/workflows/CI.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 832fa95..ad753ea 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -278,9 +278,10 @@ jobs: # as text and looks normal. run: | coverage combine - echo "```text" >> $GITHUB_STEP_SUMMARY - coverage report --precision=2 --show-missing --sort=Cover --skip-covered >> $GITHUB_STEP_SUMMARY - echo "```" >> $GITHUB_STEP_SUMMARY + cat "```text" >> summary.txt + coverage report --precision=2 --show-missing --sort=Cover --skip-covered >> summary.txt + echo "```" >> summary.txt + cat summary.txt >> $GITHUB_STEP_SUMMARY coverage json --fail-under=98 export TOTAL_COVERAGE=$(python -c "import json;print(round(float(json.load(open('coverage.json'))['totals']['percent_covered']),2))") echo "total_coverage=$TOTAL_COVERAGE" >> $GITHUB_ENV From dab7312f356588ee86b01ad4508b7419ae62403b Mon Sep 17 00:00:00 2001 From: John Robbins Date: Fri, 13 Sep 2024 20:04:18 -0400 Subject: [PATCH 15/21] Edit-CRY-Repeat #2 --- .github/workflows/CI.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index ad753ea..b0ffee9 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -278,9 +278,9 @@ jobs: # as text and looks normal. run: | coverage combine - cat "```text" >> summary.txt + cat "\`\`\`text" >> summary.txt coverage report --precision=2 --show-missing --sort=Cover --skip-covered >> summary.txt - echo "```" >> summary.txt + echo "\`\`\`" >> summary.txt cat summary.txt >> $GITHUB_STEP_SUMMARY coverage json --fail-under=98 export TOTAL_COVERAGE=$(python -c "import json;print(round(float(json.load(open('coverage.json'))['totals']['percent_covered']),2))") From 4b0dd8fb2f5811e786e5c0ebe90e3fc6ad31eb1b Mon Sep 17 00:00:00 2001 From: John Robbins Date: Sat, 14 Sep 2024 14:25:53 -0400 Subject: [PATCH 16/21] More CI stuff. --- .github/workflows/CI.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index b0ffee9..64883f9 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -278,10 +278,10 @@ jobs: # as text and looks normal. run: | coverage combine - cat "\`\`\`text" >> summary.txt - coverage report --precision=2 --show-missing --sort=Cover --skip-covered >> summary.txt - echo "\`\`\`" >> summary.txt - cat summary.txt >> $GITHUB_STEP_SUMMARY + SURROUND_PART=$'\`\`\`\n' + echo $SURROUND_PART >> $GITHUB_STEP_SUMMARY + coverage report --precision=2 --show-missing --sort=Cover --skip-covered >> $GITHUB_STEP_SUMMARY + echo $SURROUND_PART >> $GITHUB_STEP_SUMMARY coverage json --fail-under=98 export TOTAL_COVERAGE=$(python -c "import json;print(round(float(json.load(open('coverage.json'))['totals']['percent_covered']),2))") echo "total_coverage=$TOTAL_COVERAGE" >> $GITHUB_ENV From 26dcfc56e423c533b65a951a4a09b74bf5267623 Mon Sep 17 00:00:00 2001 From: John Robbins Date: Sat, 14 Sep 2024 14:35:44 -0400 Subject: [PATCH 17/21] More CI fiddling. --- .github/workflows/CI.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 64883f9..edf4ebb 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -278,10 +278,9 @@ jobs: # as text and looks normal. run: | coverage combine - SURROUND_PART=$'\`\`\`\n' - echo $SURROUND_PART >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY coverage report --precision=2 --show-missing --sort=Cover --skip-covered >> $GITHUB_STEP_SUMMARY - echo $SURROUND_PART >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY coverage json --fail-under=98 export TOTAL_COVERAGE=$(python -c "import json;print(round(float(json.load(open('coverage.json'))['totals']['percent_covered']),2))") echo "total_coverage=$TOTAL_COVERAGE" >> $GITHUB_ENV From e9610e18a73887b549cca00f9adf1b3078516abe Mon Sep 17 00:00:00 2001 From: John Robbins Date: Sat, 14 Sep 2024 20:12:41 -0400 Subject: [PATCH 18/21] Final editing pass One last pass before releasing 1.0.0. --- .vscode/ltex.hiddenFalsePositives.en-US.txt | 1 + README.md | 6 +- docs/_config.yml | 2 +- docs/docs/command-language.md | 13 +-- docs/docs/contributing.md | 15 ++- docs/docs/faq.md | 22 ++-- docs/docs/getting-started.md | 14 +-- docs/docs/tb-language.md | 34 +++--- docs/docs/thoughts.md | 111 +++++--------------- docs/index.md | 14 +-- 10 files changed, 88 insertions(+), 144 deletions(-) diff --git a/.vscode/ltex.hiddenFalsePositives.en-US.txt b/.vscode/ltex.hiddenFalsePositives.en-US.txt index 1719302..0212779 100644 --- a/.vscode/ltex.hiddenFalsePositives.en-US.txt +++ b/.vscode/ltex.hiddenFalsePositives.en-US.txt @@ -34,3 +34,4 @@ {"rule":"UNLIKELY_OPENING_PUNCTUATION","sentence":"^\\Q:crossed_fingers: (Thanks for your patience!)\\E$"} {"rule":"WORD_CONTAINS_UNDERSCORE","sentence":"^\\Q:crossed_fingers: (Thanks for your patience!)\\E$"} {"rule":"NON_STANDARD_WORD","sentence":"^\\QHow does tbp handle CTRL+C and CTRL+D (CTRL+Z, ENTER on Windows)?\\E$"} +{"rule":"EN_A_VS_AN","sentence":"^\\QIf you do forget to end on an \\E(?:Dummy|Ina|Jimmy-)[0-9]+\\Q statement, tbp handles that case correctly.\\E$"} diff --git a/README.md b/README.md index 00f695a..7946700 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ It seemed nuts to put this up on PyPI, which is for important modules, not learn ### Cloning and Installing -Download the code from the [Latest](https://github.com/John-Robbins/tbp/releases/latest) release. Expand the compressed file into a directory, then execute the following. +Download the code from the [Latest Releases](https://github.com/John-Robbins/tbp/releases/latest). Expand the compressed file into a directory, then execute the following. ```bash % pip install . @@ -70,7 +70,7 @@ Download the code from the [Latest](https://github.com/John-Robbins/tbp/releases ## :book: Documentation -Everything about tbp is lovingly documented to death in the GitHub Pages for this repository: [https://John-Robbins.github.io/tbp](https://John-Robbins.github.io/tbp). +Everything about tbp is lovingly documented to death in over 17,000 words in the GitHub Pages for this repository: [https://John-Robbins.github.io/tbp](https://John-Robbins.github.io/tbp). ## :clap: Acknowledgements @@ -92,7 +92,7 @@ Marco's [TinyBasicBlazor](https://retrobits.altervista.org/tinybasicblazor/), a ### :heart: YOU! :heart: -Thank you so much for looking at tbp. It's my first Python project and first time using GitHub Actions so any feedback on what I can do better, or what I did wrong, greatly is appreciated. Hit me up in the [Issues](https://github.com/John-Robbins/tbp/issues). +Thank you so much for looking at tbp. It's my first Python project and first time using GitHub Actions so any feedback on what I can do better, or what I did wrong, is greatly appreciated. Hit me up in the [Issues](https://github.com/John-Robbins/tbp/issues). *To all of you, thanks! I'm a Python beginner standing on the shoulders of giants!* diff --git a/docs/_config.yml b/docs/_config.yml index c35faf9..fc49368 100644 --- a/docs/_config.yml +++ b/docs/_config.yml @@ -61,4 +61,4 @@ liquid: # Footer content appears at the bottom of every page's main content # Note: The footer_content option is deprecated and will be removed in a future major release. # Please use `_includes/footer_custom.html` for more robust markup / liquid-based content. -footer_content: "Copyright © 2024 John Robbins. Distributed by an MIT license." +footer_content: "Copyright © 2024 John Robbins. Distributed by an MIT license." diff --git a/docs/docs/command-language.md b/docs/docs/command-language.md index ff600da..c580dc3 100644 --- a/docs/docs/command-language.md +++ b/docs/docs/command-language.md @@ -92,7 +92,7 @@ LINT #04: Potentially uninitialized variable 'B'. ---------------------------------^ ``` -Ideally, the next step would be to build an engine that would walk the program looking for the execution flow to find if the program initializes a variable during a run. That's something I do want to look at. +Ideally, the next step would be to build an engine that would walk the program looking for the execution flow to find if the program initializes a variable during a run. That's something I do want to look at in future versions of tbp. #### `%lint` Command Error Messages @@ -109,11 +109,6 @@ Loads the specified file from disk. You must surround the filename by quote char When loading a file, tbp assumes all lines in the file are [Tiny BASIC language](tb-language) statements, so any tbp command language commands will be reported as an error. -```text -tbp:>%lf "./examples/srps.tbp" -tbp:> -``` - If the `run_on_load` option is true, the loaded file will automatically execute a [`RUN`](tb-language#run---execute-the-program-in-memory) statement. The default for `run_on_load` is false. Obviously, you cannot use the `%loadfile` command if stopped at a breakpoint.[^3] @@ -207,11 +202,11 @@ tbp:>run ## The Tiny BASIC in Python Debugger -The tbp debugger is a full-featured debugger that allows breakpoints on lines, stepping, variable display, and call stack display. What more could a developer want? For this section, I will be debugging the [`pas.tbp`](https://github.com/John-Robbins/tbp/blob/main/examples/pas.tbp) program, which you can find in the tbp examples folder, if you would like to practice as you read along. +The tbp debugger is a full-featured debugger that allows breakpoints on lines, stepping, variable display, and call stack display. What more could a developer want? For this section, I will be debugging [Winston (Winny) Weinert's](https://github.com/winny-/tinybasic.rkt) wonderful Pascal Triangle program, [`pas.tbp`](https://github.com/John-Robbins/tbp/blob/main/examples/pas.tbp), which you can find in the tbp examples folder, if you would like to practice as you read along. ### Setting and Listing Breakpoints: `%bp` | `%break` -To start a debugging session, start by setting a breakpoint on the line number where you want to stop. In the example below, I wanted to set breakpoints on several lines of a loaded program. +To enter a debugging session, start by setting a breakpoint on the line number where you want to stop. In the example below, I wanted to set breakpoints on several lines of a loaded program. ```text tbp:>list 180,220 @@ -275,7 +270,7 @@ Breakpoint: 200 DEBUG(200):> ``` -When you hit a breakpoint, tbp displays the line in square brackets and changes the prompt to show you are in the debugger with `DEBUG` and the line number of the breakpoint. In this case, the prompt is `DEBUG(200):>`. +When you hit a breakpoint, tbp displays the breakpoint line in square brackets and changes the prompt to show you are in the debugger with `DEBUG` and the line number of the breakpoint. In this case, the prompt is `DEBUG(200):>`. ### Show Variables: `%v` | `%vars` diff --git a/docs/docs/contributing.md b/docs/docs/contributing.md index 019ecc8..f21ffc7 100644 --- a/docs/docs/contributing.md +++ b/docs/docs/contributing.md @@ -20,8 +20,7 @@ Thank you so much for any and all contributions to Tiny BASIC in Python! As this Before submitting a pull request, please help me out by doing the following. - File an issue explaining the bug or mistake in the code. -- If the issue is not following best practices, please explain what I did wrong or include a link to a website/repository showing why I need to make the change. -- This is all about me learning after all, so I appreciate the pointers. +- If the issue is not following best practices, please explain what I did wrong or include a link to a website/repository showing why I need to make the change. This is all about me learning after all, so I really appreciate the pointers. ## Setting Up Your Development Environment @@ -29,7 +28,7 @@ All my development occurred on Python version 3.12.1, but any higher versions wi ### macOS Sonoma and Linux -Note: I haven't tested these scripts on Linux, but they should work. Let me know if they don't. +*Note: I haven't tested these scripts on Linux, but they should work. Let me know if they don't.* In the `./tools` directory are two shell scripts I used to jump set up my environment. First ensure that you don't have a virtual environment active. If you do, execute `deactivate`. In the tbp code root directory, run the following three commands in your terminal. @@ -53,7 +52,7 @@ Note that I don't have a Windows computer, so I am going off memory and reading For Windows, you will need to do the script steps manually in the tbp code root directory at a PowerShell command prompt. -First look to see if the `VIRTUAL_ENV` is set, which says you have a virtual environment already active. Run the Windows equivalent to `deactivate` to disable that virtual environment. +First look to see if the `VIRTUAL_ENV` environment variable exists, which indicates you have a virtual environment already active. Run the Windows equivalent to `deactivate` to disable that virtual environment. In your PowerShell command prompt, execute the following commands to create a virtual environment for tbp. @@ -67,9 +66,9 @@ You may have to shut down and restart PowerShell. In your PowerShell command prompt, in the tbp code root directory, execute the following commands. ```powershell -python -m pip install $dryrun --upgrade pip -python -m pip install $dryrun .[dev] -python -m pip install $dryrun --editable . +python -m pip install --upgrade pip +python -m pip install .[dev] +python -m pip install --editable . ``` ## Key Development Notes @@ -77,7 +76,7 @@ python -m pip install $dryrun --editable . **UNIT TESTS AND CODE COVERAGE ARE EVERYTHING!** -At the time of this writing, there are 280 unit tests and the combined operating system code coverage of 99.92%. For any pull requests, I'll obviously be checking that any code changes have tests that execute the code. _No coverage, no merge._ +At the time of this writing, there are 290 unit tests and the combined operating system code coverage of 99.88% with 100% coverage for the tbp code. For any pull requests, I'll obviously be checking that any code changes have tests that execute the code. *No coverage, no merge!* If you have a version of `make` installed, the root directory has the `Makefile` that automates a ton of work for you. Simply running `make` will do the work of running `mypy`, `ruff`, `pylint`, `coverage.py`, and `pytest`. diff --git a/docs/docs/faq.md b/docs/docs/faq.md index 1e570cd..c4043a2 100644 --- a/docs/docs/faq.md +++ b/docs/docs/faq.md @@ -16,21 +16,21 @@ permalink: faq ## General Usage -- How does tbp handle `CTRL+C` and `CTRL+D` (`CTRL+Z`, ENTER on Windows)? +- **How does tbp handle `CTRL+C` and `CTRL+D` (`CTRL+Z`, `ENTER` on Windows)?** -For tbp, the Python REPL served as a model. As a reminder, when you enter these keys, they are not entries, but generate exceptions. `CTRL+C` is [KeyBoardInterrupt](https://docs.python.org/3/library/exceptions.html#KeyboardInterrupt) and `CTRL+D` is [EOFError](https://docs.python.org/3/library/exceptions.html#EOFError). + For tbp, the Python REPL served as a model. As a reminder, when you enter these keys, they are not entries, but generate exceptions. `CTRL+C` is [KeyBoardInterrupt](https://docs.python.org/3/library/exceptions.html#KeyboardInterrupt) and `CTRL+D` is [EOFError](https://docs.python.org/3/library/exceptions.html#EOFError). -When you are at a normal tbp prompt, `tbp:>` or a breakpoint prompt, `DEBUG(410:>`, `CTRL+C` does nothing, but `CTRL+D` will immediately terminate the application. + When you are at a normal tbp prompt, `tbp:>` or a breakpoint prompt, `DEBUG(420):>`, `CTRL+C` does nothing, but `CTRL+D` will immediately terminate the application. -If a Tiny BASIC program is running, both `CTRL+C` and `CTRL+D` will break the program and reset tbp's internal state as though it was typed in or loaded. + If a Tiny BASIC program is running, both `CTRL+C` and `CTRL+D` will break the program and take you back to the normal tbp prompt. -If you are in the middle of loading a file with [%loadfile](tbp-command-language#loading-files-loadfile--lf), a `CTRL+C` will abort file loading, reset the tbp internal state, and clear any loaded program from memory. + If you are in the middle of loading a file with [%loadfile](tbp-command-language#loading-files-loadfile--lf), a `CTRL+C` will abort file loading and clear any loaded program from memory. -Figuring out a way to test these keystrokes was quite the adventure. You can check out the hack by reading [controlkeys_test.py](https://github.com/John-Robbins/tbp/blob/main/tests/controlkeys_test.py). + Figuring out a way to test these keystrokes was quite the adventure. You can check out the hack by reading [controlkeys_test.py](https://github.com/John-Robbins/tbp/blob/main/tests/controlkeys_test.py). ## Tiny BASIC -- Why is `Syntax Error: Error #020: LET is missing an '=', but found '{var}'` the most common error when entering code at the tbp prompt? +- **Why is `Syntax Error: Error #020: LET is missing an '=', but found '...'` the most common error when entering code at the tbp prompt?** Tiny BASIC allows two forms of `LET` statements: @@ -42,19 +42,19 @@ Figuring out a way to test these keystrokes was quite the adventure. You can che When you enter a statement like `oto`, the tbp scanner sees the first character, it looks to see if the entire statement is a keyword and if it isn't, the scanner assumes the `o` is the variable `O`, and the assumes it's the second form of assignment above. -- What are some good BASIC resources if I want to learn more? +- **What are some good BASIC resources if I want to learn more?** - Tom Pittman's [Tiny BASIC](http://www.ittybittycomputers.com/IttyBitty/TinyBasic/) page has loads of content and example programs. - [Marco's Retrobits](https://retrobits.altervista.org/blog/) has lots of cool games he has written for other BASIC platforms such as the [Sinclair ZX Spectrum](https://en.wikipedia.org/wiki/ZX_Spectrum) computer. While many of his programs don't run in Tiny BASIC, I learned a lot from them. - Ronald Nicholson has a great [list](http://www.nicholson.com/rhn/basic/basic.info.html) of BASIC resources. Additionally, he developed the free, cross-platform Chipmunk BASIC interpreter. - - [Hans Otten](http://retro.hansotten.nl) has a ton of awesome retro computing information. His [KIM-1 Simulator](http://retro.hansotten.nl/6502-sbc/kim-1-manuals-and-software/kim-1-simulator/) is one of the computers that ran the original Tiny BASIC implementations. - - J. Alan Henning has written many nice [articles](https://troypress.com/category/programming-languages/) about BASIC. My absolute favorite article he wrote is [The Tiny BASIC Interpretive Language ILā€”and Onions.](https://troypress.com/the-tiny-basic-interpretive-language-il-and-onions/) He presents his intermediate language (IL) version of Tiny BASIC from the original specifications [Dennis Allison](https://en.wikipedia.org/wiki/Dennis_Allison) wrote in the [January 1976](https://archive.org/details/dr_dobbs_journal_vol_01/page/n89/mode/2up) issue of Dr. Dobbā€™s Journal of Computer Calisthenics & Orthodontia. + - [Hans Otten](http://retro.hansotten.nl) has a ton of awesome retro computing information. His [KIM-1 Simulator](http://retro.hansotten.nl/6502-sbc/kim-1-manuals-and-software/kim-1-simulator/) recreates one of the computers that ran the original Tiny BASIC implementations. + - J. Alan Henning has written many nice [articles](https://troypress.com/category/programming-languages/) about BASIC. My absolute favorite article he wrote is [The Tiny BASIC Interpretive Language ILā€”and Onions.](https://troypress.com/the-tiny-basic-interpretive-language-il-and-onions/) He presents his intermediate language (IL) version of Tiny BASIC from the original specifications [Dennis Allison](https://en.wikipedia.org/wiki/Dennis_Allison) wrote in the [January 1976](https://archive.org/details/dr_dobbs_journal_vol_01/page/n89/mode/2up) issue of Dr. Dobbā€™s Journal of Computer Calisthenics & Orthodontia. My goal for future versions of tbp is to be able to run his IL version. - [Damian Walker's](http://damian.cyningstan.org.uk/posts/150/the-tiny-basic-interpreter-and-compiler-project) implementation of [Tiny BASIC](https://github.com/cyningstan/TinyBASIC). It's all in C, and he added some neat features. - [Rosetta Code's Tiny BASIC](https://rosettacode.org/wiki/Category:Tiny_BASIC) examples. ## Code/Development -- What is the overview of the source code files? +- **What is the overview of the source code files?** | Filename | Description | |----------|-------------| diff --git a/docs/docs/getting-started.md b/docs/docs/getting-started.md index 5cdce1c..35e38d4 100644 --- a/docs/docs/getting-started.md +++ b/docs/docs/getting-started.md @@ -49,7 +49,7 @@ As tbp is a command line program, once you have it installed, you can simply run | | | |_) | |_) | |_| |____/| .__/ | | - |_| version 0.9.0.0 + |_| version 1.0.0 Party like it's 1976! Look at that cool CN tower in Toronto! @@ -80,7 +80,7 @@ options: ### File to Load -To start tbp with a program file ready to run, pass it on the command line. +To start tbp with a program file ready to run, pass it on the command line. After tbp started and loaded the program, the example shows using the [`RUN`](tb-language#run---execute-the-program-in-memory) command to execute the loaded program. ```text % python -m tbp ./examples/rand.tbp @@ -93,11 +93,11 @@ To start tbp with a program file ready to run, pass it on the command line. | | | |_) | |_) | |_| |____/| .__/ | | - |_| version 0.9.0.0 + |_| version 1.0.0 Party like it's 1976! What do I do with a $2.00 bill? -tbp:>run +tbp:>RUN 69 12 83 53 59 16 62 36 7 80 13 92 93 46 32 44 36 54 71 26 58 0 56 28 @@ -113,10 +113,10 @@ tbp:> The `--commands` option allows you to specify any Tiny BASIC language statements or tbp command language commands you want to run at startup. Separate the commands with the `^` character. -In the following example, it shows starting up with the [tbp.tbp](https://github.com/John-Robbins/tbp/blob/main/examples/tbp.tbp) example program but using `--commands` to run the [`LIST`](tb-language#list---display-the-program-in-memory) statement followed by the [`%q`](tbp-command-language#quit-q) command. +In the following example, it shows starting up with the [tbp.tbp](https://github.com/John-Robbins/tbp/blob/main/examples/tbp.tbp) example program but using `--commands` to run the Tiny BASIC [`LIST`](tb-language#list---display-the-program-in-memory) statement followed by the command language [`%quit`](tbp-command-language#quit-q) command. ```text -% python -m tbp ./examples/tbp.tbp --commands LIST^%q +% python -m tbp ./examples/tbp.tbp --commands LIST^%quit Tiny BASIC in Python - github.com/John-Robbins/tbp _______ ____ @@ -126,7 +126,7 @@ In the following example, it shows starting up with the [tbp.tbp](https://github | | | |_) | |_) | |_| |____/| .__/ | | - |_| version 0.9.0.0 + |_| version 1.0.0 Party like it's 1976! Star Wars Episode IV started filming. May the Force be with them. diff --git a/docs/docs/tb-language.md b/docs/docs/tb-language.md index f0ecc47..0c0df20 100644 --- a/docs/docs/tb-language.md +++ b/docs/docs/tb-language.md @@ -20,9 +20,9 @@ This document describes the dialect for the Tiny BASIC language implemented by T Tiny Basic is, well, tiny! Compared to something like [Python's language reference](https://docs.python.org/3/reference/index.html), Tiny BASIC is 12 statements, two functions, and 26 succulent variables. Even that small, Tiny BASIC can still produce some fantastic programs, some of which you can see in the project's [examples](https://github.com/John-Robbins/tbp/tree/main/examples) folder. In this documentation, I want to cover salient points on how tbp implements the language. The original documentation of Dr. Tom Pittman's version is [The TINY BASIC User Manuel](http://www.ittybittycomputers.com/IttyBitty/TinyBasic/TBuserMan.htm) and is worth looking at for a deeper background. It is also interesting to learn about all the contortions necessary to smash a full interpreter into 4 kilobytes of memory. -In tbp, I've mostly tried to stay to the size limitations of the original. Integers are 16-bit, so that's a number range of -32,768 to +32,767. Another limitation I've kept is "spaces are optional." Back in 1976, Tiny BASIC cared deeply about space and one of the ways the accomplished that was removing spaces in your programs. Thus, in Tiny BASIC, spaces are optional. By stripping out spaces, you could save considerable amounts of space. Pun intended! In the project [examples](https://github.com/John-Robbins/tbp/tree/main/examples) folder, you can find two Tic-Tac-Toe programs. The [readable one](https://github.com/John-Robbins/tbp/blob/main/examples/tic-tac-toe.tbp) has spaces, but [ttt-c.tbp](https://github.com/John-Robbins/tbp/blob/main/examples/ttt-c.tbp) is the compressed version with spaces removed. +In tbp, I've mostly tried to stay to the size limitations of the original. Integers are 16-bit, so that's a number range of -32,768 to +32,767. Another limitation I've kept is "spaces are optional." Back in 1976, Tiny BASIC cared deeply about space and one of the ways the accomplished that was removing spaces in your programs. Thus, in Tiny BASIC, spaces are optional. By stripping out spaces, you could save considerable amounts of space. Pun intended! In the project [examples](https://github.com/John-Robbins/tbp/tree/main/examples) folder, you can find two Tic-Tac-Toe programs. The [readable one](https://github.com/John-Robbins/tbp/blob/main/examples/tic-tac-toe.tbp) has spaces, but [ttt-c.tbp](https://github.com/John-Robbins/tbp/blob/main/examples/ttt-c.tbp) is the compressed version with spaces removed. The readable version is 4,226 bytes where the compressed is 1,710 bytes. -Why did they care about space so much back then? Tom Pittman uploaded a Tiny BASIC [programming manual](http://www.ittybittycomputers.com/IttyBitty/TinyBasic/ProgTB/ProgTB0.htm) for the [Netronics ELF](https://en.wikipedia.org/wiki/ELF_II), whose base model came with 256 bytes of RAM.[^2] Read that number in the last sentence slowly. +Why did they care about space so much back then? Tom Pittman uploaded a Tiny BASIC [programming manual](http://www.ittybittycomputers.com/IttyBitty/TinyBasic/ProgTB/ProgTB0.htm) for the [Netronics ELF](https://en.wikipedia.org/wiki/ELF_II), whose base model came with 256 bytes of RAM.[^2] Read that number in the last sentence slowly. Every byte was sacred in the programming days of yesteryear! Below is an example of a valid Tiny BASIC source code line. @@ -36,7 +36,7 @@ As spaces are optional, the line below is also valid in the language and tbp sto 2 4 4 5 0 i F V / X *2 * Q >0 G o T o 4 6 1 0 ``` -If you looked closely at the second example, you might have noticed that Tiny BASIC is case-insensitive as well. You may not want to write code using either of the above as examples. Talk about a maintenance hell! [^3] +If you looked closely at the second example, you might have noticed that Tiny BASIC is case-insensitive as well. You may not want to write code using either of the above as examples. Talk about a maintenance heck! [^3] ## Direct Execution vs. A Statement vs. Command Language @@ -57,7 +57,7 @@ A * (I - J) / J The functions, `RND` and `USR` can also appear in expressions. See the discussion of those in the [Functions](#functions) section below. -One very important difference between the original Tiny BASIC and the tbp implementation is that in tbp, it checks all variable access at runtime and if you did not initialize a variable, it's reported as a runtime error and your program terminates. The example below shows this checking. +One very important difference between the original Tiny BASIC and the tbp implementation is that tbp checks all variable access at runtime and if you did not initialize a variable, it's reported as a runtime error. Your program execution terminates and at the tbp prompt, you can fix your program. The example below shows this checking. ```text tbp:>10 INPUT A @@ -77,9 +77,9 @@ Additionally, the [`%lint`](tbp-command-language#linting-lint) command language ### `REM` - Remark/Comment -Documenting your code is crucial, and the `REM` statement is your tool. You can use `REM` as a direct execution statement if you want to leave notes to yourself at the tbp prompt. In your program, putting a line number in front of a comment and that comment will stay with the program even when saved and loaded from files. +Documenting your code is crucial, and the `REM` statement is your tool. You can use `REM` as a direct execution statement if you want to leave notes to yourself at the tbp prompt, like I did in the awesome sizzle GIF in the project [README](https://github.com/John-Robbins/tbp/blob/main/README.md). In your program, putting a line number in front of a REM make it part of your program even when saved and loaded from files. -In the original implementation of Tiny BASIC, when it loaded a line into memory, the interpreter would look to see if a line started with a number. If it did, the interpreter loaded the line into memory and didn't parse it until it executed. If a [`GOTO`](#goto---jump-to-line) jumped over that line, it never gets parsed. If you look at some original programs from the 1970s, there are lines like the following. +In the original implementation of Tiny BASIC, when it loaded a line into memory, the interpreter would look to see if a line started with a number. If it did, the interpreter loaded the line into memory and didn't parse it until it executed. If a [`GOTO`](#goto---jump-to-line) jumped over that line, it was never parsed. If you look at some original programs from the 1970s, there are lines like the following. ```text 130 . 0 IS EMPTY, 1 IS X. 3 TS O @@ -88,7 +88,7 @@ In the original implementation of Tiny BASIC, when it loaded a line into memory, 160 P IS POKE ROUTINE ADDRESS ``` -In tbp, I made the decision to parse lines when entered before storing them memory. Therefore, the above lines are syntax errors in tbp instead of runtime errors. +In tbp, I made the decision to parse lines when entered before storing them memory. Therefore, the above lines are syntax errors in tbp instead of runtime errors. To fix the problem, add `REM` statements after the line number. ### `LET` - Assignment @@ -104,7 +104,7 @@ The `LET` is optional for assignment, but I think it is good practice to include While you are debugging in tbp, you can use either form as direct execution statements to change variable values for testing and analysis as shown below. ```text -tbp:>A=420 +tbp:>LET A=420 tbp:>Z=999 ``` @@ -117,7 +117,7 @@ INPUT A INPUT X, Y, Z ``` -After the `INPUT` statement, you specify the variables you want filled in by the user. The only values allowed are integers. However, you can build expressions as input so at a `INPUT` prompt, you could enter `(A*2/(X+3))`. The result of that expression evaluation becomes the value for the specified `INPUT` variable. +After the `INPUT` statement, you specify the variables you want filled in by the user. The only values allowed for input are integers. However, you can build expressions as input so at a `INPUT` prompt, you could enter `(A*2/(X+3))`. The result of that expression evaluation becomes the value for the specified `INPUT` variable. The original Tiny BASIC simply shows a `?` when asking for input. In tbp, I show the variable the program is asking for because I found it helpful for debugging Tiny BASIC programs. In most cases you will see a single character in brackets like the following where I'm running the [prime-decomp.tbp](https://github.com/John-Robbins/tbp/blob/main/examples/prime-decomp.tbp) program. The `[N]?` says the program is asking you for the `N` variable. @@ -168,7 +168,7 @@ I treat that as a warning. The original Tiny BASIC had a scheme where it stored ### `PRINT` - Output -It might be nice show data to the user of your awesome Tiny BASIC program and that's your `PRINT` statement, which you can abbreviate to `PR` for space savings. This is the one statement that can work with string data. You may pass any sort of mix of strings, expressions, or the two print separators,`,` and `;`. +It might be nice show data to the user of your awesome Tiny BASIC program and that's your `PRINT` statement, which you can abbreviate to `PR` for space savings. This is the one statement that can work with string data. You may pass any sort of mix of strings, expressions, or the two print separators,`,` (comma) and `;` (semicolon). ```text tbp:>PRINT A @@ -288,9 +288,11 @@ IF expression rel_op expression statement Below are examples of valid `IF` statements. ```text -IF A > 3 THEN PRINT "A > 3" -IF N/P*P=N GOTO 350 -IF M * N > Q IF V <> (B/W) GOSUB 9540 +110 IF A > 3 THEN PRINT "A > 3" + +420 IF N/P*P=N GOTO 350 + +1105 IF M * N > Q IF V <> (B/W) GOSUB 9540 ``` The last example is equivalent to the following in Python. @@ -302,7 +304,7 @@ if ((M * N) > Q) and (V != (B/W)): ### `END` - End and Clean Up -The `END` statement must be the last statement executed by your program as that tells the interpreter the program finished. That way the interpreter cleans up the call stack, runtime state, memory, and any one-shot breakpoints (used by the [`%step`](tbp-command-language#step-s--step) command language command) You may have multiple `END` statements in your program as needed. +The `END` statement should be the last statement executed by your program as that tells the interpreter the program finished. That way the interpreter cleans up the call stack, runtime state, memory, and any one-shot breakpoints (used by the [`%step`](tbp-command-language#step-s--step) command language command). If you do forget to end on an `END` statement, tbp handles that case correctly. You may have multiple `END` statements as exit points in your program as needed. The [`%lint`](tbp-command-language#linting-lint) command built into tbp will ensure you have at least one `END` statement in your program but does not ensure it executes. @@ -333,7 +335,7 @@ In the latter two `RUN` commands above, you see the parameter passing the statem ### `CLEAR` - Clear and Reset the Interpreter -After executing a `CLEAR`, the entire program, its state, one-shot breakpoints, and call stacks return to the default state. Like the original Tiny BASIC, tbp **does not** reset any variables. +After executing a `CLEAR`, the entire program, its state, one-shot breakpoints, and call stacks return to the default state. Like the original Tiny BASIC, tbp **does not** change any of the `A`-`Z` variables. The [`%lint`](tbp-command-language#linting-lint) command built into tbp will check the program in memory for `CLEAR` statements in a program, because you just don't want to do that.[^5] @@ -417,7 +419,7 @@ The parameter to `RND` is the range and the random number returned is between $$ Tiny BASIC, as you can see, has limitations. To allow features like manipulating memory and talking to hardware, the `USR` function let you incorporate machine code into the environment. You can read more about how you could use the original `USR` function in the [Tiny BASIC Experimenter's Kit](http://www.ittybittycomputers.com/IttyBitty/TinyBasic/TBEK.txt). -The `USR` function depends heavily on the memory layout of Tiny BASIC. Since tbp is a tree walking interpreter, supporting the same would have been difficult and, honestly, pointless. With my goal of running as many of the original programs as I could in tbp, I implemented just enough to allow of `USR` to run the original [Adventure](https://github.com/John-Robbins/tbp/blob/main/examples/adventure.tbp), Conway's game of [Life](https://github.com/John-Robbins/tbp/blob/main/examples/life.tbp), and Tic-Tac-Toe games, but [readable](https://github.com/John-Robbins/tbp/blob/main/examples/tic-tac-toe.tbp) and [compressed](https://github.com/John-Robbins/tbp/blob/main/examples/ttt-c.tbp). In tbp, I supported the two key routines, reading a byte from memory, and writing a byte to memory. The read routine is at address 276 (0x114) and the write routine is at 280 (0x118). Those are the only two routine addresses allowed, and others cause an error. +The `USR` function depends heavily on the memory layout of Tiny BASIC. Since tbp is a tree walking interpreter, supporting the same would have been difficult and, honestly, pointless. With my goal of running as many of the original programs as I could in tbp, I implemented just enough to allow of `USR` to run the original [Adventure](https://github.com/John-Robbins/tbp/blob/main/examples/adventure.tbp), Conway's game of [Life](https://github.com/John-Robbins/tbp/blob/main/examples/life.tbp), and Tic-Tac-Toe games, both [readable](https://github.com/John-Robbins/tbp/blob/main/examples/tic-tac-toe.tbp) and [compressed](https://github.com/John-Robbins/tbp/blob/main/examples/ttt-c.tbp). In tbp, I supported the two key routines, reading a byte from memory, and writing a byte to memory. The read routine is at address 276 (0x114) and the write routine is at 280 (0x118). Those are the only two routine addresses allowed, and others cause an error. By convention, Tiny BASIC programs used the `S` variable to hold the start of the Tiny BASIC interpreter in memory. For the Motorola 6800 CPU, that is 265 (0x100), and tbp does the same. There were different start addresses for different CPUs. Thus, `S+20` is the read byte routine, and `S+24` is routine to write a byte. diff --git a/docs/docs/thoughts.md b/docs/docs/thoughts.md index 9ed2583..d63e733 100644 --- a/docs/docs/thoughts.md +++ b/docs/docs/thoughts.md @@ -17,21 +17,25 @@ permalink: project-notes ## Overview -Through the development of Tiny BASIC in Python (tbp), I kept notes on where I screwed up, got confused, loved something, explained my thinking, or got frustrated. These are not necessary to enjoy the Tiny BASIC life, but I thought they might be interesting to others. +Why did I develop Tiny BASIC in Python (tbp)? Earlier this year, I randomly bumped into Peter Norvig's [(How to Write a (Lisp) Interpreter (in Python))](http://norvig.com/lispy.html) and found it fascinating. Out of a growing curiosity, I got the [Dragon Book](https://www.malaprops.com/book/9780321486813), but that was a fire hose of information and theory. Coming from the software industry with oodles of experience working on debuggers, I needed a book where I could get my hands dirty with an interpreter/compiler to get the feel for how the various parts fit together. Once I had something concrete to relate to, I felt I would get much more out of the big textbook. That's when I bumped into Bob Nystrom's outstanding [Crafting Interpreters](http://www.craftinginterpreters.com), which is *exactly* the book I envisioned I needed to start my learning journey. + +After getting through Bob's masterpiece, I wanted to do more learning, but wasn't ready to go down the path of designing my own language yet. With that idea, I wanted a small, but useful language. Tiny BASIC is both of those. Also, I wanted to learn Python because that's what all the cool kids are doing these days. + +Through the development of tbp, I kept notes on where I screwed up, got confused, loved something, explained my thinking, or got frustrated. These are not necessary to enjoy the Tiny BASIC life, but I thought they might be interesting to others. ## Code and Problem Thoughts ### My Kingdom for an EBNF Grammar Checker -The very first task I wanted to complete was to build a solid Extended Backus-Naur form (EBNF) grammar for the Tiny BASIC language by hand. The file is [tiny_basic_grammar.ebnf](https://github.com/John-Robbins/tbp/blob/main/docs/tbp/tiny_basic_grammar.ebnf) in the tbp repository. Building robust grammars for a language is hard. There are a million edge cases and surprises that are waiting around dark corners to scare you. All the compiler textbooks discuss creating grammars extensively, and you start to realize that if you screw up the grammar in the beginning, youā€™ve made your life terrible. +The very first task I wanted to complete was to build a solid Extended Backus-Naur form (EBNF) grammar for the Tiny BASIC language by hand. The file is [tiny_basic_grammar.ebnf](https://github.com/John-Robbins/tbp/blob/main/docs/tbp/grammar/tiny_basic_grammar.ebnf) in the tbp repository. Building robust grammars for a language is hard. There are a million edge cases and surprises that are waiting around dark corners to scare you. All the compiler textbooks discuss creating grammars extensively, and you start to realize that if you screw up the grammar in the beginning, youā€™ve made your life terrible. This correctness was especially important to me because I am a programming language beginner. All the compiler text books I read stressed the importance of grammar correctness, avoiding [left recursion](https://en.wikipedia.org/wiki/Left_recursion), and the like. Given how many pages these textbooks spent on defining grammars, I was looking forward to working with tools that would help me in finding all the mistakes I was making. One would think EBNF grammar validators would be a keen area of development to help language designers avoid mistakes. -Yes, I am gobsmacked that EBNF validators arenā€™t a dime a dozen. [^1] I found a few academic websites that offered validators for experimental LR(0) grammars and other esoteric parsers. It seems like all the textbooks on compilers leave checking and validating your grammar is an "exercise for the reader." Why is that? Is that because all these programming language researchers and experienced compiler developers can cast a side eye across an EBNF grammar and just know itā€™s perfect? Is writing an EBNF validator as hard as solving the Riemann hypothesis? This really is a very strange situation to me, and I'd love to know why EBNF validators aren't common. +Yes, I am gobsmacked that EBNF validators arenā€™t a dime a dozen. [^1] I found a few academic websites that offered validators for experimental LR(0) grammars and other esoteric parsers. It seems like all the textbooks on compilers leave checking and validating your grammar is an "exercise for the reader." Why is that? Is that because all these programming language researchers and experienced compiler developers can cast a side eye across an EBNF grammar and just know itā€™s perfect? Is writing an EBNF validator as hard as solving the [Riemann hypothesis](https://en.wikipedia.org/wiki/Riemann_hypothesis)? This really is a very strange situation to me, and I'd love to know why EBNF validators aren't common. Years ago, I'd heard of the parser generator [ANTLR]( https://www.antlr.org), but I'm dealing with a language that has 12 statements and two functions. I wanted to do this by hand to learn and while ANTLR is very cool, but extraordinary overkill for my little project. Fortunately, ANTLR has its wonderful [Lab](http://lab.antlr.org) where you can interactively play around with rules and example text to see how they parse. I used it to get a clue about how some grammar rules would work and since the ANTLR grammar is close enough to EBNF, it helped a lot. -I spent my time hand walking the grammar I built, and it all looked OK. Here's where the narrator jumps in and says in that gorgeous, succulent, deep baritone, say "And John was wrong. Completely wrong." +I spent my time hand walking the grammar I built, and it all looked OK. Here's where the narrator jumps in and says in that gorgeous, deep baritone, say "And John was wrong. Completely wrong." Somehow, through internet magic, I saw a reference someplace on a blog where the writer said they tested their grammar on [http://instaparse.mojombo.com](http://instaparse.mojombo.com). Oh, my! An EBNF checker that works! It's not perfect, but if you refresh the page and paste in your grammar in after major changes, it works well. I found many, many problems in my first attempt at the grammar under Instaparse. A huge thank you and shout out to [Mark Engleberg](https://github.com/Engelberg) for writing [instaparse](https://github.com/Engelberg/instaparse), and [Matt Huebert](https://github.com/mhuebert) for hosting it. Much later, I found another tool, [EBNF Test](https://mdkrajnak.github.io/ebnftest/) by [mdkrajnak](https://github.com/mdkrajnak) which was also extremely helpful. @@ -43,9 +47,9 @@ Obviously, I don't have a complete grasp of grammar development. My plan is to w Bob Nystrom's jlox implementation in his book [Crafting Interpreters](http://www.craftinginterpreters.com) *highly* influenced tbp's design. [^3] His was the first implementation I fully understood, so it was obvious I should go down the same path. However, I think I should have done more thinking and designing on the scanner before jumping into the implementation. I'll own the screw-up. -In some cases, like the [Scanner._number](https://github.com/John-Robbins/tbp/blob/main/src/tbp/scanner.py#L377-L387) method, I got myself into situations where scanning for the token left the current pointer into the next lexeme, and I'd have to back up. +In some cases, like the [Scanner._number](https://github.com/John-Robbins/tbp/blob/main/src/tbp/scanner.py#L369-L379) method, I got myself into situations where scanning for the token left the current pointer into the next lexeme, and I'd have to back up. -Another area where I did a bad job was in scanning for keywords and identifiers (those `A`-`Z` variables). Bob's language, lox, allows spaces, but not inside keywords, so his implementation could use string methods like `substring` to extract a keyword. Since I am working with a language where `GOTO` and `G O T O` are identical, that added a level of complexity that I didn't appreciate at first. I decided to go with a solution that looked at a letter, say `R`, and then looked if the next characters were `UN`, `ETURN`, `ND` or `EM`. Of course, skipping any whitespace between them. In all, my [Scanner._handle_keyword_or_identifier](https://github.com/John-Robbins/tbp/blob/main/src/tbp/scanner.py#L406-L471) method is embarrassing. +Another area where I did a bad job was in scanning for keywords and identifiers (those `A`-`Z` variables). Bob's language, lox, allows spaces, but not inside keywords, so his implementation could use string methods like `substring` to extract a keyword. Since I am working with a language where `GOTO` and `G O T O` are identical, that added a level of complexity that I didn't appreciate at first. I decided to go with a solution that looked at a letter, say `R`, and then looked if the next characters were `UN`, `ETURN`, `ND` or `EM`. Of course, skipping any whitespace between them. In all, my [Scanner._handle_keyword_or_identifier](https://github.com/John-Robbins/tbp/blob/main/src/tbp/scanner.py#L398-L463) method is embarrassing. With my huge amounts of unit tests, I thought I had flushed out all the problems, until a week or two ago. I was running as many Tiny BASIC programs as I could find and produce through tbp, and I ran across the following line. @@ -71,7 +75,7 @@ In the end, with too much special casing, I have a scanner that works, so that's Again, if you've read [Crafting Interpreters](http://www.craftinginterpreters.com) the tbp parse and interpreter should feel very familiar. As they stand now, both are solid, but it was some work to get there for both. -The main problem with the parser was that I had declared separate expression and statement types, and it got to be a mess coordinating them while parsing. After enough pain, I rethought everything and decided to go with a single base class I called a [`LanguageItem`](https://github.com/John-Robbins/tbp/blob/main/src/tbp/languageitems.py#L160-L202). That helped a lot, but I still feel that there's more simplification possible. For example, I have a [`Assignment`](https://github.com/John-Robbins/tbp/blob/main/src/tbp/languageitems.py#L258-L279) type and a [`LET`](https://github.com/John-Robbins/tbp/blob/main/src/tbp/languageitems.py#L489-L503) type, which are really the same thing. +The main problem with the parser was that I had declared separate expression and statement types, and it got to be a mess coordinating them while parsing. After enough pain, I rethought everything and decided to go with a single base class I called a [`LanguageItem`](https://github.com/John-Robbins/tbp/blob/main/src/tbp/languageitems.py#L159). That helped a lot, but I still feel that there's more simplification possible. For example, I have a [`Assignment`](https://github.com/John-Robbins/tbp/blob/main/src/tbp/languageitems.py#L257) type and a [`LET`](https://github.com/John-Robbins/tbp/blob/main/src/tbp/languageitems.py#L488) type, which are really the same thing. The interpreter bothers me a little because the class is so big. I don't know if there's a way to move functionality other places, but it is doing a lot of work. It's the execution engine and debugger, which makes up a lot of the code. Something I considered was making the `LanguageItem` derived types, like for `RND` or `PRINT` have ways to do their own work. That way as the interpreter is executing through the code, it could tell the [`Print`](https://github.com/John-Robbins/tbp/blob/main/src/tbp/languageitems.py#L451-L470) type, "Hey! Do your thing." passing in a callback if the `Print` type needed to evaluate its parameters. @@ -87,72 +91,13 @@ Runtime Error: Error #336: Accessing uninitialized variable 'D'. -------------------------------------------------------^ ``` -Another area where I thought I did well was the unit tests and code coverage. With 290-unit tests and 99.88% coverage overall, I flushed a ton of bugs out with those tests. Additionally, I have mypy, ruff, and pylint cranked up to their equivalent strict mode with only 13 rules disabled in pyproject.toml. I also bumped up a few of the complexity numbers in pyproject.toml. I always got a little thrill running `make` in my macOS terminal and seeing all those checks and tests. - -```text -% make -mypy --config-file pyproject.toml src/ tests/ -Success: no issues found in 29 source files -ruff check --config ./pyproject.toml src/ tests/ -All checks passed! -pylint --rcfile pyproject.toml src/ tests/ - --------------------------------------------------------------------- -Your code has been rated at 10.00/10 (previous run: 10.00/10, +0.00) - -coverage run -m pytest --maxfail=1 -console_output_style=classic --junit-xml=.test-results.xml -============================= test session starts ============================== -platform darwin -- Python 3.12.1, pytest-8.3.2, pluggy-1.5.0 -rootdir: /Users/johnrobbins/Code/tbp -configfile: onsole_output_style=classic -plugins: anyio-4.4.0 -collected 290 items - -tests/cmd_lang_test.py ........... [ 3%] -tests/controlkeys_test.py ....... [ 6%] -tests/debugger_test.py ....................... [ 14%] -tests/driver_test.py ........................ [ 22%] -tests/helpers_test.py ........... [ 26%] -tests/interpreter_test.py .............................................. [ 42%] -........................... [ 51%] -tests/lang_test.py .. [ 52%] -tests/linter_test.py .......................... [ 61%] -tests/memory_test.py . [ 61%] -tests/parser_test.py ................................................... [ 78%] -................. [ 84%] -tests/scanner_test.py ................................... [ 96%] -tests/symboltable_test.py ........ [ 99%] -tests/tokens_test.py . [100%] - ------- generated xml file: /Users/johnrobbins/Code/tbp/.test-results.xml ------- -============================= 290 passed in 1.06s ============================== -coverage report --precision=2 --show-missing --sort=Cover --skip-covered -Name Stmts Miss Branch BrPart Cover Missing ------------------------------------------------------------------------- -src/tbp/helpers.py 79 4 22 1 95.05% 21-23, 231-232 -src/tbp/astprinter.py 142 3 28 2 97.06% 111-112, 277 -src/tbp/driver.py 237 2 120 8 97.20% 87->89, 90->92, 128->112, 142->112, 180, 211, 436->exit, 466->exit -src/tbp/parser.py 275 3 116 2 98.72% 357->362, 484-486, 507->524 -src/tbp/interpreter.py 495 2 186 6 98.83% 209->213, 359->363, 418->423, 581->exit, 603->606, 1053-1054 -src/tbp/languageitems.py 170 1 8 1 98.88% 192 -src/tbp/scanner.py 242 1 128 3 98.92% 211->exit, 320, 356->360 -tests/controlkeys_test.py 88 0 20 1 99.07% 72->exit -tests/interpreter_test.py 510 0 14 2 99.62% 885->exit, 904->exit ------------------------------------------------------------------------- -TOTAL 4117 16 914 26 99.17% - -19 files skipped due to complete coverage. -coverage lcov -Wrote LCOV report to .coverage.lcov -``` +Another area where I thought I did well was the unit tests and code coverage. With 290-unit tests and 99.88% coverage overall, I flushed a ton of bugs out with those tests. Additionally, I have mypy, ruff, and pylint cranked up to their equivalent strict mode with only 13 rules disabled in pyproject.toml (in addition to bumping up a few of the complexity numbers in pyproject.toml). I always got a little thrill running `make` in my macOS terminal and seeing all those checks and tests. ## Python First, some backstory: Not only is tbp my first foray into the world of programming languages, but it is also my very first Python project. A year and a half ago, I took a Numerical Analysis class and that was the first time I ever had seen Python. [^4] We used Jupyter notebooks, and I learned barely enough Python to survive the class as I was far more interested in the mathematics of Runga-Kutta methods and Fourier transforms. -A few months ago, I randomly bumped into Peter Norvig's [(How to Write a (Lisp) Interpreter (in Python))](http://norvig.com/lispy.html) and found it fascinating. Out of a growing curiosity, I got the [Dragon Book](https://www.malaprops.com/book/9780321486813), but that was a fire hose of information and theory. Coming from the software industry, I needed a book where I could get my hands dirty with an interpreter/compiler to get the feel for how the various parts fit together. Once I had something concrete to relate to, I felt I would get much more out of the big textbook. That's when I bumped into Bob Nystrom's outstanding [Crafting Interpreters](http://www.craftinginterpreters.com), which is *exactly* the book I envisioned I needed to start my learning journey. - -My first inclination when I thought about doing a project about Tiny BASIC was to use C, which I can do in my sleep. However, I wanted to get into programming languages quickly, so I thought I'd do my implementation in Python. With the automatic memory management, the dynamic language, and given Python's popularity I could also finally be one of the cool kids. +My first inclination when I thought about doing this project with Tiny BASIC was to use C, which I can do in my sleep. However, I wanted to get into programming languages quickly, so the dynamic language, garbage collector, and free cross-platform appeal, pushed me to Python. Hence, a whole new problem domain and a whole new development language at the same time. I thought I'd write a bit about my time as an extremely experienced developer learning Python, and its ecosystem, as a **COMPLETE PYTHON NOVICE** in 2024. After finishing tbp, I'd only call myself a mostly novice Python developer. Python has been around a very long time, and I have the feeling that the Python community has forgotten that there are still a few people who have never used Python before. If you're feeling a little foreshadowing, I'll go ahead and tell you that my first Python experience wasn't completely positive. @@ -196,7 +141,7 @@ def foo(value: str | int) -> None: I do love what mypy is doing, but I think this is a result of bolting on a good type system to an old and successful dynamic language. When I first started tbp, I had different AST types for expressions and statements, and I spent a lot of time fiddling with declarations and casting to ensure mypy was OK with the code. What mypy is trying to solve is a very hard problem. I don't know if this is something better tooling in editors could help with but mypy is 95% great but that last 5% ends up being a little work. Iā€™m sure Iā€™m misunderstanding something about the use of mypy. -Another small stumbling block was that while Python has easy to use [`list`](https://docs.python.org/3/tutorial/datastructures.html#more-on-lists) and [`dict`](https://docs.python.org/3/tutorial/datastructures.html#dictionaries) data structures, I was surprised that sorted lists and dictionaries were not part of the Python Standard Library. I wanted to keep the Tiny BASIC program in a dictionary with the key being the line number and the associated item the AST types. From the Python documentation, it looked like I could use the [OrderedDict](https://docs.python.org/3/library/collections.html#collections.OrderedDict), but that only does insertion order. If someone has a thousand-line program in tbp, and they want to insert a line in the middle, it looks like I'd be inserting and calling [sorted](https://docs.python.org/3/library/functions.html#sorted) constantly which returns a new dictionary every time. I've done a ton of C# programming in the past and have an intimate knowledge of memory management, both native and managed, as well as garbage collection algorithms. I knew using the standard library wasn't correct or scalable for my problem domain. +Another small stumbling block was that while Python has easy to use [`list`](https://docs.python.org/3/tutorial/datastructures.html#more-on-lists) and [`dict`](https://docs.python.org/3/tutorial/datastructures.html#dictionaries) data structures, I was surprised that sorted lists and dictionaries were not part of the Python Standard Library. I wanted to keep the user's Tiny BASIC program in a dictionary with the key being the line number and the associated item the AST types. From the Python documentation, it looked like I could use the [OrderedDict](https://docs.python.org/3/library/collections.html#collections.OrderedDict), but that only does insertion order. If someone has a thousand-line program in tbp, and they want to insert a line in the middle, it looks like I'd be inserting and calling [sorted](https://docs.python.org/3/library/functions.html#sorted) constantly which returns a new dictionary every time. I've done a ton of C# programming in the past and have an intimate knowledge of memory management, both native and managed, as well as garbage collection algorithms. I knew using the standard library wasn't correct or scalable for my problem domain. Fortunately, I found [Grant Jenkins](https://github.com/grantjenks)' [Python Sorted Containers](https://grantjenks.com/docs/sortedcontainers/), which is massive overkill for what I needed, but it did allow me to focus on my problem domain instead of me spending a lot of time developing my own implementation. If you use Sorted Containers, [Hal Blackburn](https://github.com/h4l) did a great job building the necessary [type stubs](https://github.com/h4l/sortedcontainers-stubs), so you get all the mypy goodness. @@ -210,17 +155,17 @@ I want to make very clear I deeply appreciate everyone who has ever worked on Py One of the first things I learned on my Python journey is that there is far more "content" about Python on the internet than I could have ever imagine. Since Python has been around a while, it makes sense, but that's a weakness, not a Python strength. Several times when I first started when I had an issue, I'd find some content that would say it solved the problem, but when I tried, it wouldn't work, and I'd figure out it was for Python 2. With the avalanche of content on the internet I've found the signal-to-noise ratio is 99% noise and 1% signal. There are billions of articles on how to slice an array but finding anything useful past never-programmed-before-beginner-content is difficult. I quickly figured out that to have any hope of being successful with Python, the only content I could really trust was at [https://docs.python.org/3/](https://docs.python.org/3/). If I did venture outside that place, I made sure to search only content written in the last year or two at most. -Another key trick I employed simply trying to start learning Python pay for the [Kagi](https://kagi.com) search engine because it lets you easily [block sites](https://help.kagi.com/kagi/features/website-info-personalized-results.html#personalized-results) with low quality content from appearing in your search results. Additionally, Kagi has through its [Lenses](https://help.kagi.com/kagi/features/lenses.html) feature a fantastic way to limit searches to forums. +Another key trick I employed simply trying to start learning Python was to pay for the [Kagi](https://kagi.com) search engine because it lets you easily [block sites](https://help.kagi.com/kagi/features/website-info-personalized-results.html#personalized-results) with low quality content from appearing in your search results. Additionally, Kagi has through its [Lenses](https://help.kagi.com/kagi/features/lenses.html) feature a fantastic way to limit searches to forums. The best way I can sum up the state of trying to learn Python in 2024 is to reference that meme of [How to Draw an Owl](https://www.reddit.com/r/pics/comments/d3zhx/how_to_draw_an_owl/). Step one is to draw a circle, step two is to draw the rest of the owl. This is **not** a joke. Once you understand [list comprehension](https://docs.python.org/3/glossary.html#term-list-comprehension), where do you go? As you can hopefully see with tbp, I think I've developed a passable understanding of the Python language, but the next step is missing. Where's the introduction to infrastructure? Where is the walk-through content on setting up your development environment? How do imports work? Why are there seemingly millions of package installers? Which one should I use? How do I set up projects with multiple modules? Why are there so many build systems? These are the questions I still have about *after* finishing tbp. -Earlier I mentioned that I started tbp with all my code in a single directory and used pytest to run my code. That worked well, and my focus was completely on the problem domain of programming languages. As I get the scanner finished, and I am getting close to finishing the parser, I figure it's time for me to look at getting tbp set up as a module. Looking at projects on GitHub, I saw the source layout they used so moved the tbp source (`./src/tbp`) and test code (`./test`) to mimic those. That's when I got to find the unique Python joy of import problems. Visual Studio Code was happily showing me IntelliSense when I added a new test, but pytest just gave me errors whenever I tried to run tests. I spent hours trying to figure out the import problems until I gave up and put all code back in the same directory. At least I knew I could work on my problem, even though I knew I'm not being Pythonic. +Earlier I mentioned that I started tbp with all my code in a single directory and used pytest to run my code. That worked well, and my focus was completely on the problem domain of programming languages. As I get the scanner finished, and I am getting close to finishing the parser, I figure it's time for me to look at getting tbp set up as a module. Looking at projects on GitHub, I saw the source layout they used so moved the tbp source (`./src/tbp`) and test code (`./test`) to mimic those. That's when I got to find the unique Python joy of import problems. Visual Studio Code was happily showing me IntelliSense for tbp types when I added a new test, but pytest just gave me errors whenever I tried to run tests. I spent hours trying to figure out the import problems until I gave up and put all code back in the same directory. At least I knew I could work on my problem, even though I knew I'm not being Pythonic. A while later, I have the `Interpreter` class finished, and I really need to figure out this module and associated import problems. I'm happy to end up exposing myself as a complete idiot, but if with all my development experience, I am flailing as a novice Python developer, how must a new developer feel? Coming from the Windows world, if I want a new project, I could fire up Visual Studio, and in the New Project dialog, I pick what I want, and poof, a project appears. This project has all the basic settings, so I can start developing. For example, if I want a project that builds a C DLL, I'm not worrying too much about the infrastructure as a new developer, and one can start focusing on the problem they want to solve. I would love to see the exact same thing with Python. It's worth mentioning that I found several project templates on GitHub and in blog posts, but whenever I tried them, I still had the same import problems. -It took me 10 frustrating hours of grinding through tons of documentation, web searches, and trial and error on top of the three months I'd already been looking for a solution. I started at the [Installing Python Modules](https://docs.python.org/3/installing/index.html) page where there's a link to [Packaging Python Projects](https://packaging.python.org/en/latest/tutorials/packaging-projects/), which says "This tutorial walks you through how to package a simple Python project." As a Python novice, I thought this was the guide I need to get my imports working. It was not. Also, it used [Hatchling](https://packaging.python.org/en/latest/key_projects/#hatch) as what looked like the default, so I chose that. +It took me ten frustrating hours of grinding through tons of documentation, web searches, and trial and error, on top of the three months I'd already been looking, to find a solution. I started at the [Installing Python Modules](https://docs.python.org/3/installing/index.html) page where there's a link to [Packaging Python Projects](https://packaging.python.org/en/latest/tutorials/packaging-projects/), which says "This tutorial walks you through how to package a simple Python project." As a Python novice, I thought this was the guide I need to get my imports working. It was not. Also, it used [Hatchling](https://packaging.python.org/en/latest/key_projects/#hatch) as what looked like the default, so I chose that. After hours of fruitless searching and reading, I randomly ended up in the Python Packaging User Guide on the [Packaging and distributing projects](https://packaging.python.org/en/latest/guides/distributing-packages-using-setuptools/), which talks about Setuptools. Also, the page says it is outdated, but didn't give any links on where to go for the latest information. As a Python novice, I'm thinking packaging is build systems. Also, I'm nowhere near the point of being ready to package and distribute anything because I can't even get tests working, so I almost didn't read it. However, in the middle of that page is [Working in "development mode"](https://packaging.python.org/en/latest/guides/distributing-packages-using-setuptools/#working-in-development-mode), and for the first time in three months I learn about `python -m pip install -e .` and "Editable Installs". Wow! I was gobsmacked.[^1] That section links to a page in the Setuptools, [Development Mode (a.k.a. ā€œEditable Installsā€)](https://setuptools.pypa.io/en/latest/userguide/development_mode.html) with even more information. @@ -228,17 +173,17 @@ Why was something this important buried in the Setuptools documentation instead Weirdly, in trying to get the proper steps written down for how to create and develop a new module with imports working, I see different behaviors. With a brand-new virtual environment, sometimes I can have an empty `__init__.py` in the `src/modname` directory. Other times I must put in `from .modname import thing1, thing2` for imports to work over in the test code. Now I see why this search, [https://www.google.com/search?q=developer+python+imports+errors+how+to+fix+site%3Astackoverflow.com](https://www.google.com/search?q=developer+python+imports+errors+how+to+fix+site%3Astackoverflow.com), has about "About 22,600,000 results (0.36 seconds)." Again, my misunderstandings about imports could be all me. -When I first ran into import problems, out of desperation I was even looking in the [CPython](https://github.com/python/cpython) source code for hints. That gave me an idea. How does one go about proposing a PEP? Can we get two new keywords: `import_that_doesn't_hate_you` and `from_that_doesn't_hate_you` that have a well-known, documented, and easily understandable process for Python imports? I'm only half joking. +At one point, out of shear desperation I was even looking in the [CPython](https://github.com/python/cpython) source code for ideas about how imports work. That gave me an idea. How does one go about proposing a PEP? Can we get two new keywords: `import_that_doesn't_hate_you` and `from_that_doesn't_hate_you` that have a well-known, documented, and easily understandable process for Python imports? I'm only half joking. -Obviously, the Python community has done and will continue to do wonderful things. Python is everywhere and obviously works in a plethora of domains and operating systems. But as a complete Python novice, I seemed to spend a tremendous amount of time trying to figure out basic project setup and usage issues in Python. The Python community is working on huge and important problems given the breadth of Python usage, but there needs to be an emphasis on getting novice developers productive. I haven't even mentioned the large amount of time spent on other things like pyproject.toml settings and so on. +Obviously, the Python community has done and will continue to do wonderful things. Python is everywhere and obviously works in a plethora of domains and operating systems. But as a complete Python novice, I seemed to spend a tremendous amount of time trying to figure out basic project setup and usage issues in Python. The Python community is working on huge and important problems given the breadth of Python usage, but there needs to be some emphasis on getting novice developers productive. I haven't even mentioned the large amount of time spent struggling on other things like pyproject.toml settings, building modules, and so on. -Please understand I'm just trying to report my story of trying to develop my first Python module as a novice Python developer in 2024. The problems might be all on me, but it felt much harder than it should have been. Python desperately needs an equivalent to Rust's glorious [Cargo Book](https://doc.rust-lang.org/cargo/). +Please understand I'm just trying to report my story of trying to develop my first Python module as a novice Python developer in 2024. The problems might be all on me, but it felt much harder than it should have been. Python desperately needs an equivalent to Rust's glorious [Cargo Book](https://doc.rust-lang.org/cargo/). It's the one place that shows a novice developer the best practices for creating packages and being productive. -After a couple of weeks in GitHub Actions land, see below, I see our friends at [Astral](https://astral.sh) have released [uv](https://docs.astral.sh/uv/), which based on what I'm seeing on blogs and social media, is garnering massive amounts of excitement. Sadly, I still can't find any overarching explanation of how I, a Python novice, is supposed to create a project following best practices in order to take advantage of uv's speed and completeness. It feels like the Python community has all of this information in their bones, but why can't it be shared for novices like me? +During the couple of weeks I spent in GitHub Actions land, see below, our friends at [Astral](https://astral.sh) have released [uv](https://docs.astral.sh/uv/), which based on what I'm seeing on blogs and social media, is garnering massive amounts of excitement. Sadly, I still can't find any overarching explanation of how I, a Python novice, is supposed to create a project following best practices in order to take advantage of uv's speed and completeness. It feels like the Python community has all of this information in their bones, but why can't it be shared for novices like me? ## GitHub and GitHub Actions -While I started development before git was a gleam in Linus Torvalds' eye, I'd used git and GitHub quite a bit back in the olden days. However, in the time I've been away from the world of coding, there's been some huge changes at GitHub. Like, :sparkles:WOW:sparkles: levels of changes! As keeping with my obsessive note-taking inclination, I wrote up my thoughts and bumps with the new amazing GitHub Actions, the security, and project management features. I'll also post this in the GitHub Actions discussion area in the hopes it helps the product team. Especially as I'm a first-time user of GitHub Actions, which must make me a unicorn in today's dev world. +While I started development before git was a gleam in Linus Torvalds' eye, I'd used git and GitHub quite a bit back in my younger days. However, in the time I've been away from the world of coding, there's been some huge changes at GitHub. Like, :sparkles:WOW:sparkles: levels of changes! As keeping with my obsessive note-taking inclination, I wrote up my thoughts and bumps with the new amazing GitHub Actions, the security, and project management features. Just to make clear, I **love** the new to me features in GitHub! Like I mentioned in the [Python](#python) section, this feedback comes from a place of love in my heart. I want to help make GitHub better. @@ -246,7 +191,7 @@ Just to make clear, I **love** the new to me features in GitHub! Like I mentione My original plan was to post the tbp code, and setup up just enough GitHub Pages for this documentation. I'm using [Jekyll](https://jekyllrb.com) as the site generator. [^6] The truly incredible [Just the Docs](https://just-the-docs.com) theme hand walks you through everything you need to do. When in doubt, I looked and followed what they did in their repository. Totally worth the [donation](https://opencollective.com/just-the-docs)! (I've already donated to pytest and coverage.py). -After getting my [first](https://github.com/John-Robbins/tbp/blob/5bc6d9a43a74f8c6d5ec254103dd26953a2f15f2/.github/workflows/Build%20and%20Deploy%20Site%20to%20Pages.yml) GitHub Action to build the pages, I thought I'd look a deeper to see what these things were all about. The fact that at no cost I could run all my tests on macOS, Windows, and Linux, was mind-blowing to me! What a wonderful gift to the open-source community. After I got my cross-platform tests with coverage working, I couldn't believe my 280-unit test combined code coverage was 99.92%![^7] Even more, the 2,000 minutes for Actions on personal accounts is huge. I've been pounding on my GitHub Actions for the entire first week of September, and I've used a whole 1 minute so far. I've just got to give a huge thanks to GitHub/Microsoft for giving us this support. +After getting my [first](https://github.com/John-Robbins/tbp/blob/5bc6d9a43a74f8c6d5ec254103dd26953a2f15f2/.github/workflows/Build%20and%20Deploy%20Site%20to%20Pages.yml) GitHub Action to build the pages, I thought I'd look deeper to see what these things were all about. The fact that at no cost I could run all my tests on macOS, Windows, and Linux, was mind-blowing to me! What a wonderful gift to the open-source community. After I got my cross-platform tests with coverage working, I couldn't believe my 290-unit test combined code coverage was 99.88%![^7] Even more, the 2,000 minutes for Actions on personal accounts is huge. I've been pounding on my GitHub Actions for the entire first week of September, and I've used a whole 1 minute so far. I've just got to give a huge thanks to GitHub/Microsoft for giving us this support. ### Reading the Documentation @@ -256,7 +201,7 @@ The documentation has a [Use cases and examples](https://docs.github.com/en/acti In the overall [documentation](https://docs.github.com/en/actions) I found the organization a little confusing. For example, the [Writing workflows](https://docs.github.com/en/actions/writing-workflows) list what I assumed was a table of contents for 27 articles about workflows that you should read in order. For example, it lists [Using workflow templates](https://docs.github.com/en/actions/writing-workflows/using-workflow-templates) followed by [Choosing when your workflow runs](https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs). However, reading [Using workflow templates](https://docs.github.com/en/actions/writing-workflows/using-workflow-templates), does not have any link for the [Choosing when your workflow runs](https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs), those it does have links for other parts of the documentation. My goal was to read a logical flow through learning about workflows starting from zero, but I was wandering to places that assumed I'd read something else previously. -The workflow documentation got into details quickly, when I was simply trying to get a handle on [Building and testing Python](https://docs.github.com/en/actions/use-cases-and-examples/building-and-testing/building-and-testing-python). Personally, I feel it would be better to have a step-by-step tutorial of a simple calculator program. You start with the code, and you build the action step by step. The idea is that by the end the entire YAML file shows building, linting, and testing. The repository has everything in place so that PRs trigger the proper workflow and so on. That's what I ended up doing on a throw away repository to see how everything fit together. If this is something that GitHub doesn't feel is a good fit for the documentation, someone should do this in their blog as they would get a lot of traffic. KEY POINT: It has to be kept up to date! +The workflow documentation got into details quickly, when I was simply trying to get a handle on [Building and testing Python](https://docs.github.com/en/actions/use-cases-and-examples/building-and-testing/building-and-testing-python). Personally, I feel it would be better to have a step-by-step tutorial of a simple calculator program. You start with the code, and you build the action step by step. The idea is that by the end the entire YAML file shows building, linting, and testing. The repository has everything in place so that PRs trigger the proper workflow and so on. That's what I ended up doing on a throw away repository to see how everything fit together. If this is something that GitHub doesn't feel is a good fit for the documentation, someone should do this in their blog as they would get a lot of traffic. **KEY POINT:** *It must be kept up to date!* In all the documentation is pretty good, but a good walk through at the start would be great to have. The other issue is that the docs go deep fast, and I felt there's a lot of middle ground missing. I ended up with a monolithic workflow [file](https://github.com/John-Robbins/tbp/blob/b14ea797cb76c26c33005a4ce7ec768be4ec7a92/.github/workflows/Code-CI.yml) where I was installing, type checking, linting, testing, and producing code coverage on each operating system in the matrix. I knew that wasn't what I wanted in the long run but wasn't sure what the best practices were for a GitHub Action. @@ -264,7 +209,7 @@ What I would have *loved* to have had in the docs were links to real world Pytho ### Yet Another Misstep Looms -Or is Yesterday's Actions Mislead Later? Either way, YAML is not a configuration language that I enjoyed, but I'll deal with it. These [two](https://ruudvanasseldonk.com/2023/01/11/the-yaml-document-from-hell) different [posts](https://noyaml.com), do a good job of explaining the pain. My plan was to break up my giant job into multiple discrete jobs. For example, I didn't need to type check and lint on every operating system where one would suffice. Like all folks starting out in YAML, I have many, many commits in the edit, debug/find-stupid-error, repeat cycle. I just loved the joy of indention errors. +Or is Yesterday's Actions Mislead Later? Either way, YAML is not a configuration language that I enjoyed, but I'll deal with it. These [two](https://ruudvanasseldonk.com/2023/01/11/the-yaml-document-from-hell) different [posts](https://noyaml.com), do a good job of explaining the pain. My plan was to break up my giant job into multiple discrete jobs. For example, I didn't need to type check and lint on every operating system where one would suffice. Like all folks starting out in YAML, I have many, many commits in the edit, find-stupid-error, repeat cycle. I just loved the joy of indention errors. In the middle of learning GitHub Actions, I was surprised that there wasn't any graphical editor to help you build GitHub Actions. It feels like a tool that helps produce correct YAML limited to GitHub Actions doesn't seem like it would be at the complexity of a compiler back end. When I looked for such a tool on the Visual Studio Code Market Place, I didn't find any, but did find the [GitHub Actions](https://marketplace.visualstudio.com/items?itemName=GitHub.vscode-github-actions) extension. While it's nice to be able to run workflows from your IDE, I was more interested in the help with authoring. I don't know if I didn't have it installed correctly, or what, but the extension never reported obvious errors like indenting problems, so I uninstalled it. Fortunately, I found the cool [actionlint playground](https://rhysd.github.io/actionlint/) from [rhysd](https://github.com/rhysd), which validates your workflow files. It saved me innumerable mistakes! The ability to run workflows locally for testing would be very useful as well. I looked at the interesting [act](https://nektosact.com) project, but I only found it towards the end of my tribulations. When I work on bigger workflows, I will certainly install and use act. @@ -313,9 +258,9 @@ jobs: . . . ``` -In the above example, I'm repeating the exact same setup each time. Does the `test-cov-job` always have to repeat the setup? Can the job assume the project is ready to go? I never found anything definitive in the documentation and looking through many examples, it seems you must repeat the setup every time. As I mentioned in the comment above, Always Repeat Yourself feels wrong to me. +In the above example, I'm repeating the exact same setup each time. Does the `test-cov-job` always have to repeat the setup? Can the next job in a YAML file assume the project is ready to go? I never found anything definitive in the documentation and looking through many examples, it seems you must repeat the setup every time. As I mentioned in the comment above, Always Repeat Yourself feels wrong to me. -Thinking that [reusable workflows](https://docs.github.com/en/actions/sharing-automations/reusing-workflows) would be the answer, I went down that path. I wanted to encapsulate the common setup work, and pass in the `runs-on` value. Although it's been almost three years since someone asked, there's no official support for [array input type support](https://github.com/orgs/community/discussions/11692). I fumbled around with an [idea](https://github.com/orgs/community/discussions/11692#discussioncomment-3541856) I found in the actions discussions, but I could never get it to work. I gave up on reusable workflows and repeatedly repeated steps because working code always wins. +Thinking that [reusable workflows](https://docs.github.com/en/actions/sharing-automations/reusing-workflows) would be the answer, I went down that path. I wanted to encapsulate the common setup work, and pass in the `runs-on` value. Although it's been almost three years since someone asked, there's no official support for [array input type support](https://github.com/orgs/community/discussions/11692). I fumbled around with an [idea](https://github.com/orgs/community/discussions/11692#discussioncomment-3541856) I found in the GitHub Actions discussions, but I could never get it to work. I gave up on reusable workflows and repeatedly repeated steps because working code always wins. After a bit of trial and error, I had two workflows, one for code and one for docs. They were working great where I could run them manually on branches, and automatically on pull requests and pushes to main. Life was good, and then my code workflow stopped working. My workflow worked on a Sunday and died on a Monday. Being brand new to workflows, I struggled for several hours as to why my artifact uploads no longer worked. Yeah, you know where this is going: [Upcoming breaking change: Hidden Files will be excluded by default](https://github.com/actions/upload-artifact/issues/602). Why did a breaking change not involve a major version bump? I'm old enough to remember when a [GitHub co-founder](http://tom.preston-werner.com/) first proposed [SemVer](https://semver.org/spec/v2.0.0.html), which I thought was a very good attempt at bringing sanity to version numbers. Is SemVer merely a suggestion at GitHub these days? @@ -511,7 +456,7 @@ My original goal for tbp was to do a project with Tiny BASIC in three phases wit 2. A virtual machine interpreter. 3. A Tiny BASIC compiler to a virtual machine. -I think tbp part 1 turned out OK, but there's plenty of room for improvement. Going into the project, I thought 60% of my problems would be on the programming languages side. I had no idea that the majority of problems would be basic infrastructure issues with Python. If I had started in C, I know I would have finished with phases 1 and a big chunk of 2 in the time it took me to get tbp done. I expected to be developing slower as I learned Python, but not this slow. I think, but don't know for sure, I've gotten over the first major Python development hurdle developing a module, but will I get over the next one as a novice Python developer? Given what I've gone through so far, I don't know whether I'll have the patience or energy to go through something like that again. +I think tbp part 1 turned out OK, but there's plenty of room for improvement. Going into the project, I thought 60% of my problems would be on the programming languages side. I had no idea that the majority of problems would be basic infrastructure issues with Python. If I had started in C, I know I would have finished with phases 1 and a big chunk of 2 in the time it took me to get tbp done. I expected to be developing slower as I learned Python, but not this slow. Given what I've gone through so far, I don't know whether I'll have the patience or energy to spend countless hours searching the internet for solutions for Python infrastructure problems. When I tackle phases two and three, I'll most likely use another language. diff --git a/docs/index.md b/docs/index.md index 41d59f5..2493bcb 100644 --- a/docs/index.md +++ b/docs/index.md @@ -25,15 +25,17 @@ Any problems/confusion? Please file an [issue](https://github.com/John-Robbins/t The links on the left of this page have detailed discussion of installation, usage, the [Tiny BASIC language](tb-language), and the [tbp command language](tbp-command-language) that controls the programming environment. +When using tbp, use the `%help` command to get links to the documentation, and help on the tbp command language. + ### Prompts and Command Line Editing In tbp you will see various command line prompts. - `tbp:>` - The normal tbp prompt where you can enter both the Tiny BASIC language and the tbp command language. - `[A]?` - This prompt says a program is asking you for input with the [`INPUT`](tb-language#input---asking-the-user) Tiny BASIC statement. -- `DEBUG(213):>` - When you stop at a breakpoint, this prompt shows the line number you stopped on. In this case, it is line `213` in your program. See the [tbp debugger](tbp-command-language#the-tiny-basic-in-python-debugger) documentation for details about the debugger commands and help debugging. +- `DEBUG(420):>` - When you stop at a breakpoint, this prompt shows the line number you stopped on. In this case, it is line `420` in your Tiny BASIC program. See the [tbp debugger](tbp-command-language#the-tiny-basic-in-python-debugger) documentation for details about the debugger commands and help debugging. -The tbp command line uses Python's [readline](https://docs.python.org/3/library/readline.html#module-readline) module so all the normal editing, copy, paste, and arrow keys work. +The tbp command line uses Python's [readline](https://docs.python.org/3/library/readline.html#module-readline) module on macOS and Linux, and [pyreadline3](https://pypi.org/project/pyreadline3/) on Windows, so all the normal editing, copy, paste, and arrow keys work. ## Example Programs @@ -43,14 +45,14 @@ The [examples](https://github.com/John-Robbins/tbp/blob/main/examples) directory - Tiny Adventure game from [The First Book of Tiny BASIC Programs](https://www.retrotechnology.com/memship/Son_of_TFBOTBAS.HTM#chapter6) by Tom Pittman. This is a complicated game so read the documentation before playing. - [deep.tbp](https://github.com/John-Robbins/tbp/blob/main/examples/deep.tbp) - A test program for the tbp debugger that generates a deep call stack. -- [eurphoria.tbp](https://github.com/John-Robbins/tbp/blob/main/examples/eurphoria.tbp) +- [eurphoria.tbp](https://github.com/John-Robbins/tbp/blob/main/examples/euphoria.tbp) - Kingdom of Euphoria game from [The First Book of Tiny BASIC Programs](https://www.retrotechnology.com/memship/Son_of_TFBOTBAS.HTM#chapter3) by Tom Pittman. - [fib.tbp](https://github.com/John-Robbins/tbp/blob/main/examples/fib.tbp) - Generates Fibonacci numbers. From the excellent [Marco's Retrobits](HTTPS://RETROBITS.ALTERVISTA.ORG). - [fizzbuzz.tbp](https://github.com/John-Robbins/tbp/blob/main/examples/fizzbuzz.tbp) - An implementation of [Fizz Buzz](https://en.wikipedia.org/wiki/Fizz_buzz) from the always fascinating [Rosetta Code](https://rosettacode.org/wiki/Rosetta_Code). The original implementation is [here](https://rosettacode.org/wiki/FizzBuzz/Basic#Tiny_BASIC). I updated the code to use line numbers. - [fizzbuzz2.tbp](https://github.com/John-Robbins/tbp/blob/main/examples/fizzbuzz2.tbp) - - Another implementation of [Fizz Buzz](https://en.wikipedia.org/wiki/Fizz_buzz) from [Winston (Winny) Weinert's](https://github.com/winny-/tinybasic.rkt/blob/master/tinybasic-examples/examples/fizzbuzz.rkt) very cool implementation of Tiny BASIC in Racket. + - Another implementation of [Fizz Buzz](https://en.wikipedia.org/wiki/Fizz_buzz) from [Winston (Winny) Weinert's](https://github.com/winny-/tinybasic.rkt/blob/master/tinybasic-examples/examples/fizzbuzz.rkt) very cool implementation of [Tiny BASIC in Racket](https://github.com/winny-/tinybasic.rkt). - [gotoheck.tbp](https://github.com/John-Robbins/tbp/blob/main/examples/gotoheck.tbp) - A test program for the tbp debugger that implements all sorts of `GOTO`/`GOSUB` statements. - [life.tbp](https://github.com/John-Robbins/tbp/blob/main/examples/life.tbp) @@ -58,7 +60,7 @@ The [examples](https://github.com/John-Robbins/tbp/blob/main/examples) directory - [lintdemo.tbp](https://github.com/John-Robbins/tbp/blob/main/examples/lintdemo.tbp) - A modified version of the Sum of Squares and Cube Digits (see below) to demonstrate tbp's `%lint` command. - [pas.tbp](https://github.com/John-Robbins/tbp/blob/main/examples/pas.tbp) - - A fantastic implementation of [Pascal's Triangle](https://en.wikipedia.org/wiki/Pascal's_triangle) by [Winston (Winny) Weinert's](https://github.com/winny-/tinybasic.rkt/blob/master/tinybasic-examples/examples/pascals-triangle.rkt) for his implementation of Tiny BASIC in Racket. + - A fantastic implementation of [Pascal's Triangle](https://en.wikipedia.org/wiki/Pascal's_triangle) by [Winston (Winny) Weinert's](https://github.com/winny-/tinybasic.rkt/blob/master/tinybasic-examples/examples/pascals-triangle.rkt) for his implementation of [Tiny BASIC in Racket](https://github.com/winny-/tinybasic.rkt). - [prime-decomp.tbp](https://github.com/John-Robbins/tbp/blob/main/examples/prime-decomp.tbp) - An implementation if [integer factorization](https://en.wikipedia.org/wiki/Integer_factorization) originally from [Rosetta Code](https://rosettacode.org/wiki/Prime_decomposition#Tiny_BASIC). I changed the program to make the output cooler. - [rand.tbp](https://github.com/John-Robbins/tbp/blob/main/examples/rand.tbp) @@ -77,6 +79,6 @@ The [examples](https://github.com/John-Robbins/tbp/blob/main/examples) directory - The *compressed* version of the [tic-tac-toe game](http://www.ittybittycomputers.com/IttyBitty/TinyBasic/TicTac.htm) from the January 19, 1977, issue of the Homebrew Computer Club Newsletter by Tom Pittman. -[^1]: Note that I am not a lawyer but feel it was OK to share other's work like this. If that's not a correct interpretation, let me know as soon as possible, and I will take these programs down. +[^1]: Note that I am not a lawyer but feel it was OK to share other's work like this. If that's not a correct interpretation, let me know as soon as possible, and I will remove any requested by the authors. From 1bbedec90a7fef0d3a19c03ec6e3cab91ce8d48e Mon Sep 17 00:00:00 2001 From: John Robbins Date: Sat, 14 Sep 2024 22:12:41 -0400 Subject: [PATCH 19/21] Removed dead code --- src/tbp/astprinter.py | 7 +------ src/tbp/parser.py | 5 ----- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/src/tbp/astprinter.py b/src/tbp/astprinter.py index b4706ee..c2023ec 100644 --- a/src/tbp/astprinter.py +++ b/src/tbp/astprinter.py @@ -16,7 +16,6 @@ ) from tbp.parser import Parser from tbp.scanner import Scanner -from tbp.tokens import Token if TYPE_CHECKING: from tbp.languageitems import ( @@ -43,6 +42,7 @@ Usr, Variable, ) + from tbp.tokens import Token class AstPrinter(Visitor): @@ -107,9 +107,6 @@ def visit_literal_expression( expression: Literal, ) -> LanguageItem: """Process a hard coded number.""" - if isinstance(expression.value, str) is True: - self._add_to_buffer(f'"{expression.value}"') - return self._common_return self._add_to_buffer(str(expression.value)) return self._common_return @@ -273,8 +270,6 @@ def _process_pieces( for curr_arg, piece in enumerate(args): if isinstance(piece, LanguageItem): piece.accept(self) - elif isinstance(piece, Token): - self._add_to_buffer(piece.lexeme) elif isinstance(piece, list): if (length := len(piece)) > 0: self._add_to_buffer("(") diff --git a/src/tbp/parser.py b/src/tbp/parser.py index 11f81f1..9ac6ef7 100644 --- a/src/tbp/parser.py +++ b/src/tbp/parser.py @@ -480,11 +480,6 @@ def _if_statement(self: Parser) -> LanguageItem: return If(previous.line, previous.column, lhs, operator, rhs, branch) - def _clear_statement(self: Parser) -> LanguageItem: - current = self._previous() - self._verify_line_finished() - return Clear(current.line, current.column) - def _input_statement(self: Parser) -> LanguageItem: previous = self._previous() curr_var: Token = self._peek() From 685dd5c961ffd9505254867d23a242245c5398b2 Mon Sep 17 00:00:00 2001 From: John Robbins Date: Sat, 14 Sep 2024 22:16:02 -0400 Subject: [PATCH 20/21] Latest make output. --- docs/docs/contributing.md | 41 ++++++++++++++++++++------------------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/docs/docs/contributing.md b/docs/docs/contributing.md index f21ffc7..c4a18b8 100644 --- a/docs/docs/contributing.md +++ b/docs/docs/contributing.md @@ -85,7 +85,7 @@ Here's what the output looks like on macOS. `Makefile` is your best friend when ```text % make mypy --config-file pyproject.toml src/ tests/ -Success: no issues found in 28 source files +Success: no issues found in 29 source files ruff check --config ./pyproject.toml src/ tests/ All checks passed! pylint --rcfile pyproject.toml src/ tests/ @@ -99,17 +99,18 @@ platform darwin -- Python 3.12.1, pytest-8.3.2, pluggy-1.5.0 rootdir: /Users/johnrobbins/Code/tbp configfile: onsole_output_style=classic plugins: anyio-4.4.0 -collected 280 items - -tests/cmd_lang_test.py .......... [ 3%] -tests/debugger_test.py ..................... [ 11%] -tests/driver_test.py ........................ [ 19%] -tests/helpers_test.py ........... [ 23%] -tests/interpreter_test.py .............................................. [ 40%] -........................... [ 49%] -tests/lang_test.py .. [ 50%] -tests/linter_test.py .......................... [ 59%] -tests/memory_test.py . [ 60%] +collected 290 items + +tests/cmd_lang_test.py ........... [ 3%] +tests/controlkeys_test.py ....... [ 6%] +tests/debugger_test.py ....................... [ 14%] +tests/driver_test.py ........................ [ 22%] +tests/helpers_test.py ........... [ 26%] +tests/interpreter_test.py .............................................. [ 42%] +........................... [ 51%] +tests/lang_test.py .. [ 52%] +tests/linter_test.py .......................... [ 61%] +tests/memory_test.py . [ 61%] tests/parser_test.py ................................................... [ 78%] ................. [ 84%] tests/scanner_test.py ................................... [ 96%] @@ -117,22 +118,22 @@ tests/symboltable_test.py ........ [ 99%] tests/tokens_test.py . [100%] ------ generated xml file: /Users/johnrobbins/Code/tbp/.test-results.xml ------- -============================= 280 passed in 1.05s ============================== +============================= 290 passed in 0.93s ============================== coverage report --precision=2 --show-missing --sort=Cover --skip-covered Name Stmts Miss Branch BrPart Cover Missing ------------------------------------------------------------------------ -src/tbp/helpers.py 82 8 22 2 90.38% 21-23, 187-191, 235-236 -src/tbp/driver.py 216 4 108 6 96.91% 86->88, 89->91, 138, 166, 311-312, 389->exit, 421->exit -src/tbp/astprinter.py 142 3 28 2 97.06% 111-112, 277 -src/tbp/interpreter.py 499 5 186 7 98.25% 201->205, 356->360, 415->420, 578->exit, 600->603, 1047-1048, 1134-1136 -src/tbp/parser.py 275 3 116 2 98.72% 359->364, 486-488, 509->526 +src/tbp/helpers.py 79 4 22 1 95.05% 21-23, 231-232 +src/tbp/driver.py 237 2 120 8 97.20% 87->89, 90->92, 128->112, 142->112, 180, 211, 436->exit, 466->exit +src/tbp/interpreter.py 496 2 186 6 98.83% 209->213, 359->363, 418->423, 581->exit, 603->606, 1053-1054 src/tbp/languageitems.py 170 1 8 1 98.88% 192 src/tbp/scanner.py 242 1 128 3 98.92% 211->exit, 320, 356->360 +tests/controlkeys_test.py 88 0 20 1 99.07% 72->exit +src/tbp/parser.py 271 0 116 2 99.48% 357->362, 502->519 tests/interpreter_test.py 510 0 14 2 99.62% 885->exit, 904->exit ------------------------------------------------------------------------ -TOTAL 3994 25 870 25 98.97% +TOTAL 4108 10 910 24 99.32% -19 files skipped due to complete coverage. +20 files skipped due to complete coverage. coverage lcov Wrote LCOV report to .coverage.lcov ``` From 65b939de8671c700d9aa16198d694b4e539ef83b Mon Sep 17 00:00:00 2001 From: John Robbins Date: Sat, 14 Sep 2024 23:04:48 -0400 Subject: [PATCH 21/21] Updates for 1.0.0 --- CHANGELOG.md | 30 ++++++++++++++++++++++++++++++ docs/_data/version.yml | 2 +- src/tbp/__init__.py | 2 +- 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c7ef1cb..7e0e6d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,34 @@ --- +## 1.0.0 (2024-09-15) + +- Added the `%exit` command language command to exit the tbp debugger. Documented [here](https://john-robbins.github.io/tbp/tbp-command-language#exiting-the-debugger-e--exit). Closes [#1](https://github.com/John-Robbins/tbp/issues/1). +- Now tbp behaves like a normal command line application when the user hits `CTRL+C` and `CTRL+D`. Documented [here](https://john-robbins.github.io/tbp/faq#general-usage). Closes [#3](https://github.com/John-Robbins/tbp/issues/3). +- Fixed the copyright on top of all Python files. For some reason I thought it was 2004. Closes [#43](https://github.com/John-Robbins/tbp/issues/43). +- Added the combined coverage report summary to the CI.yml output. That makes it easier to see what didn't have coverage. Closes [#44](https://github.com/John-Robbins/tbp/issues/44). Sorry, the below is just too sexy not to show. :joy_cat: + + ```text + Name Stmts Miss Branch BrPart Cover Missing + + ------------------------------------------------------------------------ + + tests/controlkeys_test.py 88 0 20 1 99.07% 72->exit + tests/interpreter_test.py 510 0 14 2 99.62% 885->exit, 904->exit + ------------------------------------------------------------------------ + + TOTAL 2253 0 252 3 99.88% + + 13 files skipped due to complete coverage. + ``` + +- Added the `tiny_basic_grammar.ebnf` and `grammar_tests.txt` that I forgot to bring over from the dead repository. Closes [#45](https://github.com/John-Robbins/tbp/issues/45). +- Added better error reporting on `INPUT` entry errors and escaped syntax error strings, so characters like `\n` are displayed correctly. Closes [#46](https://github.com/John-Robbins/tbp/issues/46). +- Did a pass to eliminate any dead code. Closes [#47](https://github.com/John-Robbins/tbp/issues/47). +- Did a final editing pass on all documentation. + +--- + ## 0.9.1 (2024-09-09) --- @@ -12,6 +40,8 @@ - Updated the README and [Getting Started Installation](https://john-robbins.github.io/tbp/getting-started#installation) section to point to the Latest releases. - This is basically a practice release to ensure I have the steps down. :crossed_fingers: (Thanks for your patience!) +--- + ## 0.9.0 (2024-09-06) --- diff --git a/docs/_data/version.yml b/docs/_data/version.yml index 5f36498..155f763 100644 --- a/docs/_data/version.yml +++ b/docs/_data/version.yml @@ -1 +1 @@ -number: 0.9.1 \ No newline at end of file +number: 1.0.0 diff --git a/src/tbp/__init__.py b/src/tbp/__init__.py index e80f131..290aa6d 100644 --- a/src/tbp/__init__.py +++ b/src/tbp/__init__.py @@ -12,5 +12,5 @@ # Module information. ############################################################################### -__version__ = "0.9.1" +__version__ = "1.0.0" __author__ = "John Robbins"