From e087a928a6e0b8441bf8407e082165a2f8e3d9ec Mon Sep 17 00:00:00 2001 From: Luxferre Date: Sat, 13 Jun 2026 19:01:45 +0300 Subject: [PATCH] added implementation notes --- README.md | 2 + lttb-impl-notes.md | 274 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 276 insertions(+) create mode 100644 lttb-impl-notes.md diff --git a/README.md b/README.md index 5160adc..d148a01 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,8 @@ The LTTB specification expands upon the original Design Notes in the following m See the [manual](lttb-manual.md) for more details on every supported command and the [examples](examples/) directory for working examples. +If you want to create your own implementation of LTTB, see the [Implementation Notes](lttb-impl-notes.md) document for the details on the internal interpreter workings. + ## Implementations Here are all currently known implementations of LTTB: diff --git a/lttb-impl-notes.md b/lttb-impl-notes.md new file mode 100644 index 0000000..b0dc0c7 --- /dev/null +++ b/lttb-impl-notes.md @@ -0,0 +1,274 @@ +# Luxferre's Truly Tiny BASIC (LTTB) implementation notes + +This document serves as the definite reference for everyone intending to create a new and fully compatible LTTB language interpreter implementation. It describes all the algorithms and data structures necessary to recreate a fully functional LTTB interpreter from scratch. + +## Data structures required for LTTB implementations + +* Integer numbers (signed) +* Strings +* Lists (arrays that can store numbers and strings) +* Associative arrays (key-value objects) +* Stacks (or any other linear structure that can emulate them, including lists) + +## Globals + +An LTTB interpreter requires the following globals: + +* Program working memory `PROGMEM` (associative array) +* Variable storage area `VARS` (associative array) +* Subroutine call stack `CALLSTACK` +* Instruction pointer `IP` (integer) + +Initial values: + +* `PROGMEM` = empty +* `VARS["A"]..VARS["Z"] = 0` +* `CALLSTACK` = empty +* `IP` = 0 + +## Overall interpreter session flow + +1. Print the welcome banner. +2. If the implementation is CLI-based and there is a file name passed into the command-line argument, run the `LOAD` routine on this file. +3. Output the prompt `> ` and expect user input on the same line. +4. Read the user input, trim leading/trailing whitespace and uppercase it. Save it as `CMD`. +5. If `CMD` equals exactly to `BYE`, print the "Bye!" message and exit the interpreter. +6. If `CMD` is exactly one of these `RUN LIST NEW CLEAR LOAD SAVE`, execute the corresponding interactive routine and go to step 3. For `NEW` and `CLEAR`, the interactive routine must be the same. +7. Execute program line input routine `PROGINPUT` with `CMD` as a parameter and then go to step 3. + +## Program line input routine (`PROGINPUT`) + +Accepts the statement string `STMT` as the input line argument. + +1. Trim leading/trailing whitespace from `STMT`. +2. Return from the routine if the `STMT` is an empty string. +3. Initialize line number variable `LNO` as 0. +4. Iterate through characters of `STMT`. If the current character `C` is a digit, set `LNO = LNO * 10 + C`. Repeat this step as long as `C` is a digit or a whitespace character. +5. Starting from the first non-digit and non-whitespace character, save the rest of the `STMT` string as the statement body `BODY`. +6. If `LNO > 0`, set `PROGMEM[LNO] = BODY`. Otherwise, run statement execution routine `PROGEXEC` with `BODY` as a parameter. + +## Program statement execution routine (`PROGEXEC`) + +Accepts the statement string `STMT` as the input line argument. + +1. Read the keyword identifier `KW` from the first two non-whitespace characters in `STMT`. +2. If the second character in `KW` is equal to `=`, go to step 3, otherwise go to step 5. +3. Set `KW` to the value `LE`. +4. Prepend the value `LE ` (two characters `LE` and then a whitespace) to the beginning of `STMT`. +5. If `KW` is exactly one of these `EN GO IF IN LE PR RE`, go to the next step, otherwise return. +6. Find the position of the first whitespace in `STMT`, then copy everything from this position to the end of `STMT` into `BODY`. +7. Trim leading/trailing whitespace from `BODY`. +8. If `KW` is equal to `GO` and the **third** non-whitespace character of `STMT` is equal to `S`, execute core language routine named `GOS` with `BODY` as a parameter and return. +9. Execute core language routine with the name equal to the value of `KW` with `BODY` as a parameter and return. +## Number normalization function (`NORM`) + +Accepts any number-like value `V` as the argument. + +1. If `V` is an empty string, return 0 immediately. +2. Convert `V` into an integer (by truncating the decimal part if necessary). +3. If `V` is inside the allowed range (-32768 to 32767), return `V`. +4. Set `V = V mod 65536`. +5. If `V` exceeds 32767, set `V = V - 65536`. +6. Return `V`. + +## Evaluation helper function (`EVALHELPER`) + +Simple two-value evaluator. Accepts two stacks, value stack `VALSTACK` and operator stack `OPSTACK`. + +1. Set the integer result `RES` to 0. +2. Pop operation `OP` from `OPSTACK`. +3. Pop operand `V2` from `VALSTACK`. +4. Pop operand `V1` from `VALSTACK`. +5. If `OP` is equal to `$`, set `RES` to a random value between 0 and `(V2 + 32768) % 32768`. +6. If `OP` is equal to `+`, set `RES = V1 + V2`. +7. If `OP` is equal to `-`, set `RES = V1 - V2`. +8. If `OP` is equal to `*`, set `RES = V1 * V2`. +9. If `OP` is equal to `/` and `V2` is not 0, set `RES = V1 / V2`. +10. Push the value of `NORM(RES)` to `VALSTACK`. +11. Return `VALSTACK` and `OPSTACK`. + +## Expression evaluation function (`EXPREVAL`) + +Mathematical expression and intrinsic function application engine. Accepts a single `EXPR` string as a parameter. + +1. Trim any leading/trailing whitespace from `EXPR`. +2. If `EXPR` is empty, return 0 as a result. +3. If `EXPR` only consists of digits, return `NORM(EXPR)` as a result. +4. Substitute all occurrences of the string `RND` with the string `0$` inside `EXPR`. +5. [Phase 1 start] Initialize a `PRE_EXPR` (prepared expression) string buffer with three opening parens `(((`. +6. Initialize a previous character holder `PREVC` with an empty string. +7. Fetch the next character `C` in `EXPR`. Go to step 15 if there are no more characters. +8. If `C` is a digit or `$` character, append it to `PRE_EXPR` and go to step 14. +9. If `C` is a variable name `A`-`Z`, append the value `VARS[C]` to `PRE_EXPR` and go to step 14. +10. If `C` is an opening parenthesis `(`, append `(((` to `PRE_EXPR` and go to step 14. +11. If `C` is a closing parenthesis `)`, append `)))` to `PRE_EXPR` and go to step 14. +12. If `C` is either `*` or `/`, append `)`, then the value of `C`, then `(` to `PRE_EXPR` and go to step 14. +13. If `C` is either `+` or `-`, check for the `PREVC` value. If `PREVC` is either an empty string or belongs to the `( + - * /` set, then append `0` and the value of `C` to `PRE_EXPR`, otherwise append `))`, then the value of `C`, then `((` to `PRE_EXPR`. +14. Save the current value of `C` into `PREVC` and go to step 7. +15. Append three closing parens `)))` to the end of `PRE_EXPR`. +16. [Phase 2 start] Initialize a value buffer `VALBUF` as an empty string, and two empty stacks: `VALSTACK` and `OPSTACK`. +17. Initialize precedence map `PRECMAP` with string keys and integer values. The map can be represented in JSON as follows: `{"+": 1, "-": 1, "*": 2, "/": 2, "$": 3}`. +18. Fetch the next non-whitespace character `C` in `PRE_EXPR`. Go to step 33 if there are no more characters. +19. If `C` is a digit, append it to `VALBUF` and go to step 18. +20. If `VALBUF` is not empty, push its value onto `VALSTACK` and clear the `VALBUF` buffer. +21. If `C` is an opening parenthesis `(`, push it onto `OPSTACK` and go to step 18. +22. If `C` is a closing parenthesis `)`, go to step 23, otherwise go to step 25. +23. While `OPSTACK` is not empty and the top of `OPSTACK` is not `(`, execute `EVALHELPER(VALSTACK,OPSTACK)`. +24. If the top of `OPSTACK` is `(`, pop it from there. Go to step 18. +25. If `C` is one of these `+ - * / $`, go to step 26, otherwise go to step 18. +26. Set the current precedence value `PREC = PRECMAP[C]`. +27. If `OPSTACK` is empty, go to step 32. +28. Get the top operation on the `OPSTACK` without popping it. Assign it to `TOP_OP`. +29. If `TOP_OP` is `(`, go to step 32. +30. If `PRECMAP[TOP_OP] >= PREC`, execute `EVALHELPER(VALSTACK,OPSTACK)`, otherwise go to step 32. +31. Go to step 27. +32. Push `C` into `OPSTACK` and go to step 18. +33. While `OPSTACK` is not empty, execute `EVALHELPER(VALSTACK,OPSTACK)`. +34. Pop the value `RES` from `VALSTACK` and return `NORM(RES)` as the expression result. + +## Core language routines + +### `EN` + +Program end routine. + +1. Set `IP = -1`. +2. Clear `CALLSTACK`. + +### `GO` + +Direct jump routine. Accepts `EXPR` string as a parameter. + +Set `IP = EXPREVAL(EXPR) - 1`. + +### `GOS` + +Subroutine caller. Accepts `EXPR` string as a parameter. + +1. Push `IP` to `CALLSTACK`. +2. Set `IP = EXPREVAL(EXPR) - 1`. + +### `RE` + +Return from a subroutine. + +Pop the value from `CALLSTACK` and set `IP` to it. + +### `LE` + +Assign a value to the variable. Accepts `STMT` string as a parameter. + +1. Treat the first Latin letter in `STMT` as the variable name `VARNAME`. +2. Treat everything after the first `=` character in `STMT` as the expression `EXPR`. +3. Set `VARS[VARNAME] = EXPREVAL(EXPR)`. + +### `IF` + +Conditionally execute the statement that follows the expressions with the relative operator. Accepts `STMT` string as a parameter. + +1. Find the first position of either of these characters `> = <` inside `STMT`. Record the position into two integer variables, `ROPOS` and `ROEND`, and the first found character itself into the string variable `RELOP`. +2. If the character at `ROPOS + 1` position inside `STMT` also is either of `> = <`, then append this character to `RELOP` and increment `ROEND` by 1. +3. Set the string `EXPR1` to everything from the start of `STMT` to the character at position `ROPOS - 1` inclusively. +4. Set the string `REST` to everything from the position `ROEND + 1` to the end of `STMT` inclusively. +5. Initialize the integer position variable `THENPOS` to -1. +6. Pick the next keyword `KW` from this list in this particular order: `THEN LET = RETURN GOSUB INPUT PRINT GOTO END IF`. Go to step 11 when the list is exhausted. +7. Set `THENPOS` to the first position of the currently iterated keyword `KW` inside `REST`. If not found, go to step 6. +8. If `KW` is equal exactly to `=`, decrement `THENPOS` by 1. +9. Copy the value of `THENPOS` to another integer variable, `RESTPOS`. +10. If `KW` is equal exactly to `THEN`, increment `RESTPOS` by 4. +11. Set the string `EXPR2` to everything from the start of `REST` to `THENPOS - 1` inclusively. +12. Set `REST` to everything from the position `RESTPOS` to the end of the `REST` string inclusively. +13. Perform comparison operation denoted by `RELOP` on `EXPREVAL(EXPR1)` and `EXPREVAL(EXPR2)`. If the result of comparison is true, execute `PROGINPUT(REST)`. + +### `IN` + +Value input routine. Accepts `STMT` string as a parameter. + +1. Remove all whitespace inside `STMT`. +2. Split the value of `STMT` by a comma `,` into a list `VARLIST`. +3. Output the prompt `? ` and expect user input on the same line. +4. Read the user input into a buffer string, remove all whitespace inside it and uppercase it. Save it as `INPUTBUF`. +5. Split the value of `INPUTBUF` by a comma `,` into a list `INPUTLIST`. +6. Iterate over `VARLIST`. For each 1-character variable `VAR` inside `VARLIST`, take the corresponding expression `VAREXPR` from `INPUTLIST` (under the same index) and set `VARS[VAR] = EXPREVAL(VAREXPR)`. + +### `PR` + +Value and string printing routine. Accepts `STMT` string as a parameter. + +1. Initialize an empty list `PRINTLIST` and an empty string buffer `ITEM`. +2. Set the `QUOTING` flag to 0. +3. Fetch the next character `C` from `STMT`. Go to step 10 if there are no more characters. +4. If `C` is a double quote `"`, then flip the `QUOTING` flag (set `QUOTING = 1 - QUOTING`), append the value of `C` to `ITEM` and go to step 3. Otherwise, go to step 6. +5. If the `QUOTING` flag is set, append the value of `C` to `ITEM` and go to step 3. +6. If `C` is either a comma (`,`) or a semicolon (`;`), go to step 7, otherwise go to step 9. +7. If the current `ITEM` value is not empty, append it to the `PRINTLIST` and clear the `ITEM` variable. +8. Append the value of `C` to `PRINTLIST` and go to step 3. +9. If `C` is not a whitespace, append its value to `ITEM` and go to step 3. +10. If the current `ITEM` value is not empty, append it to the `PRINTLIST` and clear the `ITEM` variable. +11. Fetch the next item string `ITEM` from `PRINTLIST`. Go to step 15 if there are no more items. +12. If `ITEM` begins with a double quote `"`, trim all double quotes from the beginning and end and output its value verbatim (without a newline). Go to step 11. +13. If `ITEM` is equal exactly to a comma `,`, then output a TAB character (`\t`, ASCII 9) and go to step 11. +14. If `ITEM` is not equal to a semicolon `;`, then output the value of `EXPREVAL(ITEM)` verbatim, without a newline. Go to step 11. +15. If the last `ITEM` value is not a comma `,` or a semicolon `;`, output a newline. + +## Interactive routines + +### `RUN` + +Run the program currently loaded into the working memory. + +1. Set `IP = 1`. +2. If `PROGMEM[IP]` exists and is not empty, execute `PROGEXEC(PROGMEM[IP])`. +3. Increment `IP` by 1. +4. If `IP > 0` and `IP < 32768`, go to step 2. + +### `LIST` + +Output program listing. + +1. Set `I = 1`. +2. If `PROGMEM[I]` exists and is not empty, output the value of `I`, TAB character (ASCII 9) and `PROGMEM[I]` with a newline at the end. +3. Increment `I` by 1. +4. If `I > 0` and `I < 32768`, go to step 2. + +### `NEW` / `CLEAR` + +Memory clear routine. + +Restore all globals to their initial values: + +* `PROGMEM` = empty +* `VARS["A"]..VARS["Z"] = 0` +* `CALLSTACK` = empty +* `IP` = 0 + +### `LOAD` + +Program loading routine from external storage media. The provided algorithm works with disk file I/O as the most popular form of interaction. + +1. Prompt the user for a file name/path `FNAME`. +2. If the file does not exist, notify the user about this error and return. +3. Execute the `CLEAR` interactive routine. +4. Open a file handle `FH` for reading from the file under the path `FNAME`. +5. Read the next program line `PROGLINE` from the file handle `FH`. Go to step 9 upon end-of-file condition. +6. Trim the leading and trailing whitespace from `PROGLINE`. +7. If `PROGLINE` is not empty, execute `PROGINPUT(PROGLINE)`. +8. Go to step 5. +9. Close the handle `FH` and notify the user about successful program loading. + +### `SAVE` + +Program saving routine into external storage media. The provided algorithm works with disk file I/O as the most popular form of interaction. + +1. Prompt the user for a file name/path `FNAME`. +2. If the file name/path isn't writable, notify the user about this error and return. +3. Open a file handle `FH` for writing into the file under the path `FNAME`. +4. Set `I = 1`. +5. If `PROGMEM[I]` exists and is not empty, write the value of `I`, space character (ASCII 32) and `PROGMEM[I]` with a newline at the end to the file handle `FH`. +6. Increment `I` by 1. +7. If `I > 0` and `I < 32768`, go to step 5. +8. Close the handle `FH` and notify the user about successful program saving. + +## Closing notes + +The set of algorithms in this document should be enough to create a complete and functional LTTB interpreter in any modern programming language. If you have any corrections or optimizations regarding the algorithms, feel free to create an issue in this repository.