Skip to content

Latest commit

 

History

History
131 lines (89 loc) · 10.8 KB

File metadata and controls

131 lines (89 loc) · 10.8 KB

BasiCoCo

The main interpreter module is emulator/core.py (CoCoBasic class). All emulator source lives under emulator/. The conftest with test fixtures (basic, helpers, temp_programs_dir) is at conftest.py (project root).

Command Dispatch

process_statement() in core.py tries these in order:

  1. Multi-line IF — bare IF cond THEN (no action). Must be first because the AST parser can't parse it.
  2. File I/O intercepts — PRINT#, INPUT#, LINE INPUT# (and console LINE INPUT). Intercepted before AST because the AST parser doesn't understand # syntax.
  3. _try_ast_execute() — anything not in the CommandRegistry. The parser knows registry command keywords via registry_commands set and raises RegistryCommandError for them. Handles: END, GOTO, LET, PRINT, GOSUB, RETURN, FOR, EXIT FOR, WHILE, DO, IF, INPUT, ON (GOTO/GOSUB and ERROR GOTO), and implicit assignment (X = 5).
  4. CommandRegistry — everything else: NEXT, WEND, LOOP, ELSE, ENDIF, LOCAL, DIM, STOP, CONT, DATA, READ, RESTORE, SOUND, PAUSE, etc.

New control flow → AST visitor in ast_evaluator.py. New utility command → registry via execute_*. New BASIC function → functions.py only.

Execution Engine

All program execution goes through _execute_statements_loop() — the shared engine used by run_program(), continue_program_execution(), and execute_cont(). Flow-control is handled by _handle_flow_control() which dispatches jump/skip/pause directives. Helper methods: _find_line_position(), _skip_to_keyword(), _skip_to_next(), _skip_if_or_else_block().

Expression Evaluation

evaluate_expression(expr) on CoCoBasic parses and evaluates a BASIC expression string via the AST (ASTParser.parse_expression() + ASTEvaluator.visit()). evaluate_condition(cond) does the same, returning a boolean. Both cache parsed AST nodes in _expr_cache (keyed by expression string) so repeated evaluations in loops skip the tokenizer and parser. FunctionRegistry (in function_registry.py) maps BASIC function names to handlers in functions.py. Function handlers receive the CoCoBasic instance as their first argument.

Stack Ownership

AST visitors push; registry closing commands pop:

Stack Pushed by Popped by
for_stack visit_for_statement execute_next (control_flow.py)
call_stack visit_gosub_statement (4-tuple: line, sub_line, if_depth, for_depth) visit_return_statement (also restores if_stack/for_stack depth)
local_stack visit_gosub_statement (empty frame) visit_return_statement (restores variables); LOCAL/PRIVATE append entries
while_stack visit_while_statement execute_wend (control_flow.py)
do_stack visit_do_statement execute_loop (control_flow.py)
if_stack visit_if_statement / multi-line IF handler execute_else, execute_endif (control_flow.py)

IF/THEN/ELSE Handling

Three paths handle IF statements:

  1. Single-line IF (IF cond THEN action) — AST parser evaluates directly in visit_if_statement(). No stack involved.
  2. Multi-line IF (bare IF cond THEN) — detected in process_statement(), pushes to if_stack. skip_if_block directive tells executor to skip via _skip_if_or_else_block().
  3. AST-converted single-line (e.g., IF A=1 THEN B=2: C=3) — AST converter expands to IF cond THEN / body / ENDIF sublines, which then follow the multi-line path.

Nesting: _skip_if_or_else_block() counts nested IFs by checking stmt.startswith('IF ') (not substring match, to avoid false positives from strings like PRINT "IF THEN"). GOTO out of an IF block leaves a stale if_stack entry — cleared on next RUN by clear_all_stacks().

ON ERROR GOTO / RESUME

ON ERROR GOTO <line> registers an error handler; ON ERROR GOTO 0 disables it. When a runtime error occurs during program execution, _handle_flow_control() in program_executor.py intercepts the error (if a handler is registered and not already in an error handler), saves error state, and jumps to the handler line.

State fields on CoCoBasic: on_error_goto_line, error_number (ERR), error_line (ERL), error_resume_position, in_error_handler. All reset by clear_interpreter_state().

RESUME / RESUME NEXT / RESUME <line> return resume / resume_next / jump directives handled by _handle_flow_control(). ERR and ERL are read-only pseudo-variables exposed in visit_variable().

LOCAL and PRIVATE Variables

LOCAL var1, var2, ... inside a GOSUB subroutine saves the listed variables' current values. On RETURN, saved values are restored. This prevents subroutine variable collisions — the main pain point when all variables are global.

PRIVATE var1, var2, ... works like LOCAL but additionally initializes each variable to 0 (numeric) or "" (string). This creates clean scratch space — variables start fresh and don't leak after RETURN.

  • GOSUB pushes an empty frame onto local_stack; LOCAL/PRIVATE append (name, saved_value) entries to the current frame
  • PRIVATE also sets each variable to its default value (0 or "") after saving
  • RETURN pops the frame and restores variables in reverse order; variables that didn't exist before LOCAL/PRIVATE are removed
  • LOCAL/PRIVATE outside GOSUB is a runtime error
  • local_stack is managed alongside call_stack in clear_all_stacks(), save_execution_state(), restore_execution_state()
  • execute_local and execute_private live in control_flow.py (registry commands)

INPUT Protocol

INPUT pauses execution by returning {'type': 'input_request', ...}. Variable targets are described by dicts: {'name': 'V', 'array': True, 'indices': [1]} or {'name': 'X', 'array': False}. After storing the value via store_input_value(var_desc, value), call continue_program_execution() to resume.

Statement Splitting

StatementSplitter in text_utils.py owns all statement-splitting and argument-splitting logic:

  • split_on_delimiter() — split on colons (or custom delimiter), respecting quoted strings.
  • split_args() — split on commas, respecting parentheses and quotes. The single shared comma-splitter for the entire codebase (replaces all former per-module _split_args variants).
  • split_on_delimiter_paren_aware() — generalized version of split_args() for non-comma delimiters.
  • is_rem_line() — guards against splitting REM comments.
  • has_control_keyword() — detects IF/FOR/WHILE/DO at the start of a line.
  • CONTROL_KEYWORDS — canonical tuple of control-flow keyword prefixes.

Never duplicate this logic — always call through to StatementSplitter.

Immediate mode flow

process_command() intercepts LINE INPUT before the registry (since LINE would otherwise match the graphics command), then detects multi-statement lines early and routes them to process_line(). process_line() then takes one of three paths:

  1. REMprocess_statement() directly (never split).
  2. Control structure + colons (IF/FOR/WHILE/DO) → AST conversion → _execute_converted_as_temporary_program()run_program().
  3. Plain multi-statement → sequential process_statement() loop, stopping on jump or error.

Program storage flow

expand_line_to_sublines() in core.py splits stored lines once at entry time. REM lines and single-line IF/THEN are kept whole; everything else is split via StatementSplitter.split_on_delimiter(). Each subline is then pre-compiled by _store_subline():

  1. DATA → values pre-collected into data_values inline, then compiled as CompiledCommand (handler is a no-op at runtime)
  2. Multi-line IF (IF cond THEN) → condition pre-parsed to AST, stored as CompiledMultiLineIf(condition_ast) — executor evaluates condition and pushes to if_stack
  3. File I/O (PRINT#, INPUT#, LINE INPUT) → stays as string (AST drops #)
  4. AST-parseable (PRINT, FOR, GOTO, assignments, etc.) → stored as AST node
  5. Registry command (NEXT, WEND, LOOP, ELSE, ENDIF, DIM, SOUND, etc.) → stored as CompiledCommand(handler, args, keyword) — skips tokenization and registry lookup at runtime

At runtime, _execute_statements_loop() dispatches four ways: CompiledMultiLineIf → condition eval + if_stack, CompiledCommand → direct handler call, AST node → ast_evaluator.visit(), string → process_statement() (only file I/O reaches this path). Skip methods (_skip_to_next, _skip_to_keyword, _skip_if_or_else_block) check CompiledCommand.keyword, CompiledMultiLineIf, and strings.

On RUN, data_statements is built from data_values in sorted order — no re-parsing needed. Parsing logic lives in DataCommands.parse_data_values() (static method).

Naming

  • process_*: internal system (process_command, process_statement)
  • execute_*: registry commands (execute_next, execute_wend)
  • visit_*: AST execution (visit_for_statement, visit_goto_statement)

Rules

  • functions.py owns all BASIC functions — never duplicate elsewhere
  • Graphics commands go in graphics.py, DIM/arrays in variables.py, control-flow closing commands in control_flow.py, DATA/READ/RESTORE in data_commands.py
  • GPRINT(x,y),"text"[,color] draws text on the graphics screen using a 4x6 pixel font. Implemented as registry command in graphics.py (server emits {'type': 'gtext', ...}), rendered client-side by drawText() using GPRINT_FONT bitmap data in dual_monitor.js
  • Graphics helpers: self.emulator.eval_int(expr) for expression→int, _syntax_error(msg, suggestions) for error responses
  • LINE coordinate pair syntax: CommandRegistry.is_coordinate_pair_syntax() and parse_line_coordinates() handle (x1,y1)-(x2,y2) with optional spaces around the dash
  • System OK messages use _system_ok() (tagged with 'source': 'system')
  • File-creating tests must use autouse temp directory fixtures — never write to real programs/
  • All errors use error_context.syntax_error() / runtime_error() with 2-3 suggestions. syntax_error() auto-prefixes "SYNTAX ERROR:" — don't add it manually.
  • Use error_response(error) and text_response(text) from error_context to build response lists — never hand-build [{'type': 'error', ...}] or [{'type': 'text', ...}]
  • Use basic_truthy(value) from ast_nodes for BASIC boolean conversion — never inline the isinstance check
  • Use check_reserved_name(name) on CoCoBasic to guard against reserved function names in assignments and DIM
  • Use clear_all_stacks(), save_execution_state(), restore_execution_state() for stack/state management
  • source venv/bin/activate before running anything
  • Tests: python -m pytest (slow tests excluded by default via -m "not slow" in pytest.ini)
  • Run slow tests explicitly: python -m pytest -m slow
  • Run all tests: python -m pytest -m ""
  • Server logs: ./monitor_server_logs.sh tails logs/server_latest.log (created by ./start_server_with_logging.sh)