Input: a line of text from the terminal (with or without line number). Output: a byte sequence of tokens in the token buffer.
Steps:
- Skip leading whitespace
- If first characters are digits, parse line number (stored separately)
- For each remaining token:
- Skip whitespace
- Match keywords (longest match)
- Match operators and delimiters
- Match variable names (letter, optionally followed by digit)
- Match integer literals (decimal digits)
- Match string literals (quoted with
") - Emit token byte(s) to token buffer
- Emit end-of-line token (0x00)
Keywords start at 0x80. The token layout reserves 0x80-0xAF
for up to 48 keywords before delimiter, operator, and variable tokens.
This reserve is intentional: adding INKEY exposed that a one- or
two-token gap is too easy to exhaust.
| Byte | Keyword |
|---|---|
| 0x80 | LET |
| 0x81 | |
| 0x82 | INPUT |
| 0x83 | IF |
| 0x84 | THEN |
| 0x85 | GOTO |
| 0x86 | GOSUB |
| 0x87 | RETURN |
| 0x88 | FOR |
| 0x89 | TO |
| 0x8A | STEP |
| 0x8B | NEXT |
| 0x8C | STOP |
| 0x8D | END |
| 0x8E | REM |
| 0x8F | LIST |
| 0x90 | RUN |
| 0x91 | NEW |
| 0x92 | AND |
| 0x93 | OR |
| 0x94 | BYE |
| 0x95 | PEEK |
| 0x96 | POKE |
| 0x97 | ABS |
| 0x98 | CHR$ |
| 0x99 | DATA |
| 0x9A | READ |
| 0x9B | RESTORE |
| 0x9C | DIM |
| 0x9D | ON |
| 0x9E | MOD |
| 0x9F | BAND |
| 0xA0 | BOR |
| 0xA1 | BXOR |
| 0xA2 | SHL |
| 0xA3 | SHR |
| 0xA4 | CONT |
| 0xA5 | INKEY |
Keywords are case-insensitive. The tokenizer uppercases input before matching.
After REM, the rest of the line is stored verbatim as a string token
(for faithful listing).
Delimiter tokens are fixed immediately after the keyword reserve. Keep
them below the operator range so new keywords can be added up to
0xAF without colliding with expression parsing.
| Byte | Delimiter |
|---|---|
| 0xB0 | ( |
| 0xB1 | ) |
| 0xB2 | , |
| 0xB3 | ; |
| Byte | Operator |
|---|---|
| 0xB4 | + |
| 0xB5 | - |
| 0xB6 | * |
| 0xB7 | / |
| 0xB8 | = |
| 0xB9 | <> |
| 0xBA | < |
| 0xBB | <= |
| 0xBC | > |
| 0xBD | >= |
Single-letter variables A-Z map to 0xC0-0xD9 (26 tokens).
If extended variables (A0-Z9) are supported, they use a two-byte encoding: 0xDA + index byte (0-259), where index = (letter-'A')*10 + digit. This reserves 0xC0-0xD9 for the common single-letter case.
Integer literal (0xE0):
0xE0 byte2 byte1 byte0 (24-bit value, big-endian)
String literal (0xE1):
0xE1 length char0 char1 ... charN
Length is a single byte (max 255 characters).
Terminates the token stream for a line.
The expression parser uses precedence climbing, which naturally handles:
- Binary operators with varying precedence
- Unary prefix operators
- Parenthesized sub-expressions
- Function calls (PEEK, ABS)
| Precedence | Operators | Associativity |
|---|---|---|
| 1 (lowest) | AND, OR (logical) |
Left |
| 2 | BAND, BOR, BXOR, SHL, SHR (bitwise) |
Left |
| 3 | =, <>, <, <=, >, >= |
Left |
| 4 | +, - |
Left |
| 5 | *, /, MOD |
Left |
| 6 (highest) | unary -, unary + |
Right (prefix) |
expr = comparison
comparison = addition ( ( "=" | "<>" | "<" | "<=" | ">" | ">=" ) addition )*
addition = term ( ( "+" | "-" ) term )*
term = unary ( ( "*" | "/" | "MOD" ) unary )*
unary = ( "-" | "+" ) unary | primary
primary = INTEGER
| VARIABLE
| "(" expr ")"
| "PEEK" "(" expr ")"
| "ABS" "(" expr ")"
The parser evaluates directly from the token stream, returning an integer result. It maintains a token pointer into the current line's token buffer.
For the p-code implementation, expression evaluation uses the VM's eval stack naturally: each operand is pushed, each operator pops operands and pushes the result.
The executor reads the first token of a line and dispatches to the corresponding handler. Each handler consumes tokens from the line and may call the expression parser.
LET var = expr (or implicit: var = expr)
- Read variable token
- Expect
=operator - Evaluate expression
- Store result in variable table
PRINT expr [sep expr ...]
- Loop:
- If string literal: print string
- Else: evaluate expression, print as decimal integer
- If
,: advance to next tab column (every 14 chars) - If
;: no separator - If end of line: print newline
- Trailing
;suppresses newline
INPUT var or INPUT "prompt"; var
- If string literal present: print it; else print
? - Read line from terminal
- Parse integer from input
- Store in variable
IF expr THEN line
- Evaluate expression
- If non-zero: execute
GOTO line - If zero: advance to next line
GOTO line
- Parse line number from token stream
- Search program area for that line
- Set
current_line_ptrto found line - Error if not found:
BAD LINE NUMBER
GOSUB line
- Push current
next_line_ptronto GOSUB stack - Execute as GOTO
RETURN
- Pop from GOSUB stack
- Set
current_line_ptrto popped value - Error if stack empty:
RETURN WITHOUT GOSUB
FOR var = expr TO expr [STEP expr]
- Parse variable, start, limit, optional step (default 1)
- Store start value in variable
- Push FOR entry: variable, limit, step, restart pointer
- If step > 0 and start > limit: skip to matching NEXT
- If step < 0 and start < limit: skip to matching NEXT
NEXT var
- Find matching FOR entry on stack (by variable)
- Add step to variable
- If step > 0 and variable > limit: pop FOR entry, continue
- If step < 0 and variable < limit: pop FOR entry, continue
- Else: set
current_line_ptrto restart pointer (loop back) - Error if no match:
NEXT WITHOUT FOR
STOP
- Print
STOPPED IN nnn - Save the resume pointer (the next-line offset) so a subsequent
CONTcan pick up here. - Return to REPL.
CONT
- If a resume pointer is saved (i.e. the most recent execution ended via STOP and the program has not been edited since), resume execution from the saved pointer; clear the pointer so a second CONT errors.
- Otherwise raise
CAN'T CONTINUE(code 16). - Variables, GOSUB stack, and FOR stack are preserved across STOP — only RUN and NEW reset them.
- Editing any line (or RUN, or NEW) clears the saved resume pointer.
END
- Clear running flag
- Return to REPL
REM
- Skip rest of line (already stored as string token)
DATA [, ]...
- No-op at execution time. The data values stay embedded in the stored line and are consumed by READ.
READ [, ]...
- For each target: read one signed integer from the data pool, advance the read pointer, store in variable.
- The data pool is the concatenation of every DATA line's values in line-number order. The read pointer survives across statements until reset by RESTORE or RUN.
- Out-of-data raises
OUT OF DATA(code 13).
RESTORE [line]
- With no argument: rewind the read pointer; the next READ scans from the first DATA line.
- With a line number: position the read pointer at that line; the next READ scans forward from there for DATA values.
- RUN and NEW also rewind the read pointer.
DIM () [, ()]...
- For each declaration: allocate
size+1integer slots from a shared 1024-element array pool, store the base offset and length per letter, and zero-fill the new slots. - Re-DIM allocates fresh from the pool (old slots leak); the new array starts zero-cleared.
- Single-letter names A..Z; the array namespace is distinct from
the scalar namespace (
AandA()coexist). OUT OF MEMORY(code 4) if the pool can't fit the new array.
Subscripted variable references (in expressions and LET targets)
<var>(<expr>)reads or writes the array element at indexexpr.- Bounds check: 0 <= index < size; otherwise
BAD ADDRESS(code 8). - Reading or writing an un-DIMmed array also raises
BAD ADDRESS.
ON GOTO [, ]... / ON GOSUB [, ]...
- Evaluate
<expr>as a 1-based index into the comma-separated line list. - If the index is in range, branch to the selected line; for GOSUB, push the return pointer first (mirroring plain GOSUB).
- If the index is
<= 0or greater than the number of targets, fall through to the next line (no error). This matches MS/Dartmouth BASIC behaviour. BAD LINE NUMBER(code 3) if the selected line doesn't exist.
Variables A through Z. Each is a signed 24-bit integer word.
Variable token byte directly indexes the table: vars[token - 0xC0].
A-Z plus A0-A9, B0-B9, ... Z0-Z9. Two-letter+digit names common in 1970s BASIC. Index: single-letter = 0-25, extended = 26 + (letter*10 + digit).
Decision: A-Z only (26 variables) for v1. Extend to A0-Z9 later if needed.
PEEK(addr): read one byte from VM address space, return as integer (0-255)POKE addr, val: write low 8 bits of val to VM address space
Addresses are against the COR24 MMIO byte address space. Primary v1 use cases:
- LED D2: write via
POKEto LED port (0xFF0000) - Button SW2: read via
PEEKof switch port (0xFF0000) - UART data/status: 0xFF0100 / 0xFF0101
Full COR24 memory map:
- SRAM: 0x000000 - 0x0FFFFF
- MMIO: 0xFF0000+ (LED/switch at 0xFF0000, UART at 0xFF0100)
v1: no protection. PEEK/POKE can read/write anything, including the VM's own state. This is intentional for hardware bring-up use. Document the danger.
v2 consideration: optional SAFE/UNSAFE mode flag.
PEEK uses the VM's loadb instruction.
POKE uses the VM's storeb instruction.
Both operate on the full COR24 address space.
The primary I/O device. Maps to UART:
- Output:
sys PUTC(one byte at a time) - Input:
sys GETC(one byte, blocking) - Line-oriented: interpreter assembles chars into lines
- Echo: characters echoed as typed
- CR/LF normalization: accept CR, LF, or CR+LF as line end
SAVE/LOAD is deferred from v1. The future approach will use MMIO and I2C emulated virtual tape reader/punch devices. A Yew/Rust/WASM browser UI may wrap the interpreter+emulator and provide tape I/O through the browser interface.
When implemented, the save format will be plain text BASIC source (one line per stored line, detokenized from internal storage).
Short, teletype-era messages printed to console.
Format: ERROR_TEXT or ERROR_TEXT IN LINE nnn
Each error has a numeric code for debugger use:
| Code | Message | Condition |
|---|---|---|
| 1 | SYNTAX ERROR | Unexpected token |
| 2 | WHAT? | Unrecognized command |
| 3 | BAD LINE NUMBER | Line not found |
| 4 | OUT OF MEMORY | Program or stack full |
| 5 | DIVISION BY ZERO | Divide/mod by zero |
| 6 | RETURN WITHOUT GOSUB | GOSUB stack empty |
| 7 | NEXT WITHOUT FOR | No matching FOR |
| 8 | BAD ADDRESS | Invalid PEEK/POKE address |
| 9 | STOPPED | STOP statement executed |
| 10 | TYPE MISMATCH | Wrong value type |
| 11 | OVERFLOW | Arithmetic overflow (if checked) |
| 12 | STRING TOO LONG | String exceeds buffer |
| 13 | OUT OF DATA | READ with no remaining DATA values |
| 14 | END OF TAPE | Reader/punch exhausted |
| 15 | DEVICE ERROR | I/O failure |
| 16 | CAN'T CONTINUE | CONT with no valid resume point |
On error during RUN:
- Print error message with line number
- Stop execution
- Return to REPL
- Program and variables preserved (user can inspect/fix)
Conceptually a struct; implemented as parallel arrays or a memory block depending on implementation language:
InterpreterState:
; Program storage
program_start ; pointer to start of program area
program_end ; pointer to end of used program area
program_limit ; pointer to end of program area (max)
; Execution state
current_line_ptr ; pointer to line being executed
next_line_ptr ; pointer to next line after current
token_ptr ; current position in token buffer
; Variable table
vars[26] ; A-Z integer values
; GOSUB stack
gosub_stack[64] ; return line pointers
gosub_sp ; stack pointer (0 = empty)
; FOR stack
for_var[16] ; variable index
for_limit[16] ; limit value
for_step[16] ; step value
for_restart[16] ; restart line pointer
for_sp ; stack pointer (0 = empty)
; Buffers
input_buffer[256] ; raw input line
token_buffer[256] ; tokenized current line
; Flags
running ; 1 = executing program, 0 = immediate
stopped ; 1 = STOP executed
error_code ; last error (0 = none)
On startup:
- Clear all state
- Set program area pointers
- Print banner:
COR24 BASIC V1
On NEW:
- Clear program area
- Clear variables
- Clear stacks
On RUN:
- Clear variables
- Clear GOSUB and FOR stacks
- Set
current_line_ptrtoprogram_start - Set
running = 1
Fixed-size stack of word-sized entries (line pointers).
gosub_push(line_ptr):
if gosub_sp >= GOSUB_MAX: error OUT OF MEMORY
gosub_stack[gosub_sp] = line_ptr
gosub_sp += 1
gosub_pop() -> line_ptr:
if gosub_sp == 0: error RETURN WITHOUT GOSUB
gosub_sp -= 1
return gosub_stack[gosub_sp]
Maximum depth: 64 (configurable at build time).
Each entry is 4 words:
for_push(var_idx, limit, step, restart_ptr):
; First check if this variable already has a FOR entry
; If so, overwrite it (re-entering a FOR loop)
for i = for_sp-1 downto 0:
if for_var[i] == var_idx:
for_limit[i] = limit
for_step[i] = step
for_restart[i] = restart_ptr
return
; New entry
if for_sp >= FOR_MAX: error OUT OF MEMORY
for_var[for_sp] = var_idx
for_limit[for_sp] = limit
for_step[for_sp] = step
for_restart[for_sp] = restart_ptr
for_sp += 1
for_find(var_idx) -> index or -1:
for i = for_sp-1 downto 0:
if for_var[i] == var_idx: return i
return -1
Maximum depth: 16 (configurable at build time).
Based on reading sw-cor24-pcode, these language-neutral primitives are not present but would benefit interpreter implementations:
These were added to sw-cor24-pcode as language-neutral opcodes:
| Opcode | Primitive | Stack Effect | Used For |
|---|---|---|---|
| 0x70 | MEMCPY | ( src dst len -- ) | Line insertion/deletion, string copy |
| 0x71 | MEMSET | ( dst val len -- ) | Clear program area, zero-fill buffers |
| 0x72 | MEMCMP | ( a b len -- result ) | Keyword matching in tokenizer |
| 0x73 | JMP_IND | ( addr -- ) | Dispatch table for statement handlers |
MEMCPY uses memmove semantics (overlapping-safe). MEMCMP returns 0 (equal), -1 (a<b), or 1 (a>b).
| Primitive | Status |
|---|---|
| CALL_IND | Not needed unless virtual dispatch required |
| FIND_BYTE | Not needed unless token scanning is a bottleneck |
Everything BASIC-specific: line lookup, tokenization, FOR/GOSUB management, number parsing, PRINT formatting. These belong in the interpreter/runtime layer.