Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

TI-BASIC execution

TI-BASIC is a token-stream interpreter built around three kinds of state: a cursor over program bytes, a recursive expression evaluator whose result lives in OP1, and control records that remember where loops and subprograms resume. This page follows one statement through those layers before describing the special cases.

The addresses and byte-level decisions below refer to TI-84 Plus OS 2.55MP. Claims marked [confirmed] are tied to ROM bytes or calculator traces. A [hypothesis] marks the remaining interpretation of an incompletely decoded structure.

The execution pipeline

A stored program is a VAT object of type ProgObj (05h) or ProtProgObj (06h). Its data begins with a little-endian size word followed by exactly that many token bytes. There are no stored line numbers: 3Fh separates lines, and 3Eh separates colon-delimited statements. [confirmed]

Execution can be read as a pipeline:

flowchart LR
    V["VAT program object"] -->|"size selects token body"| C["parse cursor<br/>nextParseByte … basic_end"]
    C -->|"fetch and classify"| H["statement or expression handler"]
    H --> E["recursive expression"]
    H --> M["variable or list access"]
    H --> D["command or control transfer"]
    E --> R["OP1 result"]
    M --> R
    D --> R
    R -->|"advance or replace cursor"| C

The page-38 evaluator owns the cursor and grammar. Statement commands can cross to page 02, control-flow bodies to page 33, display code to pages 01/03/37, and variable lookup to the VAT routines. Those page changes are continuations of one interpreter, not separate parsers. [confirmed]

The token cursor

TIBasicParserState (0x9652) keeps the program identity and cursor interval contiguous in RAM: [confirmed]

#pragma pack(push, 1)
typedef struct {
    uint8_t basic_prog[9];
    uint16_t basic_start;
    uint16_t next_parse_byte;
    uint16_t basic_end;
    uint8_t num_arguments;
} TIBasicParserState;
#pragma pack(pop)

next_parse_byte is the current token position. basic_start begins the current body, and basic_end is its inclusive parser/refill boundary.

The small page-38 helpers are the useful way to reason about cursor movement:

RoutineAddressOperation
parse_cur_tok38:72DAFetch the current byte and classify 00h, 3Eh, and 3Fh
parse_advance38:7248Increment nextParseByte, compare it with basic_end, and refill when needed
parse_expect_or_err38:5CD8Require one token or restore the fault position and raise syntax error
parse_scan_tokens38:4180Scan to a statement delimiter without splitting a two-byte token or quoted string
parse_init38:5B7BReset parser-position bytes and parser flags

Encoded width matters whenever the interpreter skips rather than evaluates. _IsA2ByteTok (00:1FE8) searches the 11-byte two_byte_token_lead_table (00:1FF6); a match means the lead and following byte must move together. The scanner also treats a quoted string as one region, so Then, Else, or End bytes inside a string cannot terminate an outer scan. [confirmed]

scan_to_delimiter():
  loop:
    token = current_token()
    if token is end, colon, or EOL:
      return
    if token is quote:
      advance through the closing quote or EOL
    else if token is a two-byte lead:
      advance once for the second byte
    advance to the next token

This routine does not know the grammar of an expression. Its job is narrower: preserve token boundaries while another routine searches for a statement or block delimiter.

Expressions are nested productions

_ParseInp (38:5987) initializes parser state for a homescreen expression or formula and enters the shared evaluator. Stored-program execution arrives with the program body and parser frame already selected. Both converge on the recursive expression machinery around parse_eval_expr (38:5AB3) and the statement loop at 38:59C5. [confirmed]

The evaluator is not a flat “token to function” switch. It first maps the current token to a grammar class, selects a production family, and lets that handler recursively consume tighter-binding operands. The selector at 38:7010 chooses among three bases:

Selector CHandler familyRole
Other than 02h or 03hgrammar_handler_table (38:4000)Main grammar productions
02hcode at 38:478CPostfix/power production
03hleaf_production_handler_table (38:7175)Six leaf-production offsets

For the main family, the grammar class in A indexes grammar_handler_table. Its 87 page-local offsets contain 84 valid pointers and 81 distinct handler destinations. The bytes there are data — beginning 9F 41 F0 45 1C 42 ...—not executable Z80. The selector doubles the class, reads the pointer, and calls the chosen production. [confirmed]

evaluate_production(class, level):
  if level == 2:
    return postfix_production_478C(class)
  else if level == 3:
    handler = leaf_production_handler_table[class]
  else:
    handler = grammar_handler_table[class]

  return handler(parse_cursor, OP1)

This nesting is what gives ^, multiplication, and addition their precedence. Binary productions move operands through the OS floating-point stack and apply operations such as _FPAdd, _FPMult, or _BinOPExec; the completed value is left in OP1. 38:6FB7–6FC2 also folds token classes F2h and above by adding 12h before dispatch. [confirmed]

The other indirect jumps in the declared interpreter graph also have bounded ROM-owned destinations:

JumpSelector sourceValid destinations
38:439014 entry wrappers load literal continuations14
38:724449-class table at 38:4FDB; five rows are zero/invalid27 distinct
02:5675Five preceding token comparisons load literal targets5
33:4380Bounds-checked 13-row table at 33:438113

The CFG follows those destinations without treating adjacent table bytes as instructions. The bounds describe valid interpreter state, not arbitrary register or stack corruption. [confirmed]

The shared statement loop can then do one of three things with the result:

  • store it through a parsed variable name;
  • write it to Ans through _StoAns (38:6251); or
  • pass it to a command or control-flow continuation.

_AnsName (38:74B7) constructs the internal name with class byte 72h; _RclAns (38:679F) recalls it through the ordinary variable machinery. [confirmed]

Variable identity and value payload are separate

The parser first builds a variable-name descriptor in OP1. The descriptor’s type byte selects a VAT object class and the following bytes encode the token or name. findsym_scan (07:565F) resolves that identity to a VAT entry and data pointer; only then does the recall path copy or address the value payload. [confirmed]

Object classTypePayload used by BASIC
Real00hOne 9-byte TIFloat
Real list01h2-byte length, then 9-byte elements
Matrix02hTwo dimensions, then 9-byte elements
String04h2-byte length, then token/character bytes
Program05h2-byte length, then token bytes
Protected program06hProgram payload with protected edit semantics
Complex list0Dh2-byte length, then complex elements
flowchart LR
    T["name token"] --> N["OP1 name descriptor"]
    N --> V["07:565F<br/>VAT scan"]
    V -->|"recall"| P["typed payload"]
    P --> R["OP1 value or element address"]
    R --> A["FP/list/matrix operation"]
    A -->|"store"| S["create, resize, or replace VAT payload"]

Scalar arithmetic copies a 9-byte value into the OP registers. List and matrix access instead checks the container type and dimensions, computes one element address, and moves that element through OP1. Stores can therefore fail before arithmetic runs: name lookup, type compatibility, dimensions, and allocation are distinct boundaries. [confirmed]

Statements end locally; blocks scan structurally

A false single-line If only has to skip one statement. A false If ... Then, While, Repeat, or For( must find a matching structural boundary without executing the intervening tokens. That is the purpose of blockmatch_end_else (38:4130). [confirmed]

find_matching_boundary():
  depth = 0
  loop:
    token = current_token()

    if token == Else and depth == 0: return Else
    if token == End  and depth == 0: return End
    if token == End:                    depth -= 1
    if token in {For, While, Repeat}:   depth += 1

    if token == If:
      scan_to_delimiter()
      if current_token() == Then:       depth += 1

    scan_to_delimiter()

The distinction between If condition and If condition:Then is structural: only the latter opens a block. Nested Else tokens are ignored until the depth returns to zero. The comparisons and counter changes are visible at 38:4137–417E; the counter is the 16-bit DE register. [confirmed]

Natural loops use FPS and OPS records

Natural For( execution reaches parse_for_production (38:41E5). Natural End execution reaches parse_end_ops_record (38:4200). Each loop owns floating-point operands on the FPS and three control words on the OPS. [confirmed]

LoopOffset below body FPSMeaningOPS continuation
For(-36loop-variable descriptor38:5836
For(-27initial value—
For(-18limit—
For(-9step—
While-9condition operand38:57C3
Repeat-9condition operand38:57E7

Each OPS record contains three little-endian words. Offset +0 is the continuation address. Offset +2 is the one-based parse offset. Offset +4 is the previous cleanTmp depth. OPS grows downward, so transient evaluator objects can precede the record in an ascending RAM dump. [confirmed]

38:595A computes nextParseByte - basic_start + 1. The path at 38:5965 restores the cursor as basic_start + offset, then pushes the offset and continuation through 38:7511. A For(I,1,3) trace records offset 7; the tracked While and Repeat cases record offset 8. [confirmed]

38:58CE pushes the condition for While and Repeat, then saves the cleanup link. For( enters the same save path at 38:58D1 because its parsed operands already occupy the FPS. 38:58DF restores the previous cleanup depth. [confirmed]

A terminating For( removes 0x24 FPS bytes at 38:582A. Its complete FPS record is 36 bytes, but the handler grows FPS by 27 bytes because it adopts the parsed loop-variable descriptor. A false While takes 38:57B0, scans to the matching End, drops one FPS slot at 38:57B7, and restores the cleanup link. Repeat reverses the condition at 38:57F8. [confirmed]

Nested-loop traces observe saved cleanTmp depths 1, 2, and 3. A Return inside For( reaches 38:5836, removes the 36-byte FPS record, and restores the cleanup link. Stop inside While bypasses 38:57C3 and exits the program chain. Divide-by-zero inside Repeat bypasses 38:57E7; the OS error cleanup resets cleanTmp, FPS, and OPS before displaying the error. [confirmed]

Six TilEm cases cover normal For(, While, and Repeat, nesting, Return/Stop, and an OS error. [confirmed]

The optional closing ) in For( changes parser work. The measured effect is covered in the For( parenthesis trap.

Labels rescan instead of indexing

goto_lbl_name_scanner (38:4870) reads the label name after Goto or Lbl. The search path at 38:7600 rescans the program body for a matching Lbl, then moves nextParseByte to it. This explains both the linear cost of Goto and why jumping out of structured loops can bypass normal loop cleanup. The token and name-scanning path is [confirmed]; the complexity and stack consequence are standard TI-BASIC behavior.

Program calls share data but preserve parser control

prgmNAME resolves another ProgObj, saves the caller’s interpreter state, and evaluates the callee body. It does not create a local variable frame. Scalars, lists, strings, and Ans remain global, so they form the practical calling convention. [confirmed]

EventEffect
prgmNAMEEnter the callee with a nested parser/control frame
ReturnUnwind one BASIC program frame and resume the caller
End of bodyReturn through the same program-frame machinery
StopTerminate the whole BASIC program chain

The run-confirmed callee transition passes through 38:6910, calls at 38:6914, and enters the body evaluator at 38:778F. The public parser bcalls are not substitutes for that prepared state: calling _ParseInpLastEnt or _Find_Parse_Formula from an arbitrary AsmPrgm reaches parser setup but not a working BASIC call frame. The negative fixtures end at ERR:INVALID and ERR:UNDEFINED, respectively. [confirmed]

For source-level conventions and the Ans/scalar/list calling fixture, see TI-BASIC examples and ASM interop.

Commands parse arguments, then hand off

Commands use the same expression evaluator for arguments and then cross to a specialized subsystem. The important boundary is between argument parsing and the operation itself:

CommandParse/dispatch evidenceDownstream operation
Disppage-38 statement handler_Disp at 37:51D3, then _NewLine
Output(38:6AE6, page-02 handler_OutputExpr at 03:4AF2
Input02:54EFentry editor, _ParseInpGraphReset, variable store
Prompt02:562Frepeated named-variable entry and store
Menu(02:555D_DispMenuTitle at 39:4D21, then label transfer
Pause02:55E7display and key-wait loop
getKeyexpression token ADhnon-blocking _GetKey bcall 4972h

Input accepts either an optional prompt string or a row/column prefix before one store target. Prompt loops over comma-separated variables and generates the NAME= labels itself. Accepted text enters _ParseInpGraphReset (bcall 4B04h, body 38:5978) and continues through the parser handoff at 38:5B2B. It does not enter _ParseInp at 38:5987 in the tracked cases. [confirmed]

The entry editor stores tokenized text in a split buffer: [confirmed]

WordAddressBoundary
editTop0x96F4start of the left span
editCursor0x96F6end of the left span and start of the gap
editTail0x96F8end of the gap and start of the right span
editBtm0x96FAend of the right span

Insertion at 06:4341 writes at the cursor. Cursor movement transfers tokens across the gap through 06:441E/06:445C; 06:43CA and 06:472F repack the two sides. Delete and redraw run through 06:45F8 and 06:46AF. The EDINPUT trace submits an empty line, retries, edits an expression that wraps from row 0 to row 1, and displays 45 after parser handoff. [confirmed]

The [ON] cancellation fixture reaches _ErrBreak at ram:273D and the error cleanup at ram:2793. It never reaches the parser handoff or the later Disp 99. Both editOpen (0x89F1 bit 2) and promptEdit (0x8A01 bit 0) are clear after cleanup. Menu( uses its separate title/key path at 39:4D21/39:5955; the tracked numeric selection reaches the label scanner at 38:4870 and displays 22. [confirmed]

Four TilEm cases cover Input, Prompt, cancellation, and Menu(. [confirmed]

getKey is an expression value, not a statement. The table at 37:6700 is a token-attribute table; returned key codes come from _GetKey on page 06. This distinction prevents a common false inference from the nearby CP ADh bytes. [confirmed]

Errors unwind saved relative stack state

The error entries first select an error code, then join the common path at 00:270A. For example, _ErrDivBy0 at 00:26EC selects 82h, while the syntax entry at 00:2700 selects 88h. Natural Disp 1/0 and Disp 1+ traces reach those entries, respectively. [confirmed]

The entry identifies the error message, but its incoming guard identifies the cause. Twelve natural programs separate all six numeric error entries into 12 guard paths. This table groups related paths so the mechanism stays visible:

ProgramOriginating guardPredicateError shim
Disp 1/000:2548–254Bdivisor in OP2 is zero_ErrDivBy0 at 00:26EC
Disp 10^10002:7076–7078, then 02:7053–7059positive exponent argument is at least 100_ErrOverflow at 00:26E8
Disp 1E99*1E9900:2513–251Dadjusted sum of biased decimal exponents overflows_ErrOverflow at 00:26E8
Disp ln(0)02:6F1E, then 00:212D–2131logarithm operand in OP1 is zero_ErrDomain at 00:26F4
Disp sin⁻¹(2) / cos⁻¹(2)02:76F1–76F5 / 02:76DF–76E2operand lies outside $[-1,1]$_ErrDomain at 00:26F4
Disp (-1)! / (-1) nCr 135:79CF–79D2 / 02:4FC8, then 00:2125–211Doperand fails the operation’s sign or integer check_ErrDomain at 00:26F4
Disp sqrt(-1)00:1B8F–1B93a complex result reaches the real-mode guard_ErrNon_Real at 00:26FC
Disp [[1,2][2,4]]⁻¹02:439C–43A5the pivot helper rejects the rank-deficient matrix_ErrSingularMat at 00:26F0
For(I,1,3,0) / For(I,1E99,1E99)37:4268–426B / 38:586D–5876the step is zero / adding it makes no progress_ErrIncrement at 00:26F8

The two OVERFLOW rows reach the same shim through different predicates. A trace that records only 00:26E8 therefore merges distinct numeric behavior. The compact numeric-error report retains the ordered guard path, the register state after each guard instruction, and the final error code in A. [confirmed]

A whole-ROM direct-reference scan finds 114 candidate CALL or JP operands to the six shims. The natural corpus reaches 11 distinct direct callers:

Error entryDirect-reference candidatesWitnessed callers
_ErrOverflow at 00:26E892
_ErrDivBy0 at 00:26EC21
_ErrSingularMat at 00:26F031
_ErrDomain at 00:26F4914
_ErrIncrement at 00:26F862
_ErrNon_Real at 00:26FC31

These 114 sites are linear-disassembly candidates, not 114 established predicates. The scan can decode data as instructions. It also omits indirect transfers and helpers that load an error code before entering the common path. The report preserves that distinction. [confirmed]

flowchart LR
    S["TI-BASIC expression"] --> E["operator evaluator"]
    E --> G["numeric guard<br/>zero, range, or exponent"]
    G --> R["shared error shim<br/>00:26E8–2708"]
    R --> C["00:270A<br/>common error path"]
    C --> U["restore OPS, FPS,<br/>error SP, and page"]

The shared context wrapper at 00:27DA does not save absolute FPS and OPS pointers. It saves each pointer as a delta from the corresponding base at 0x9822 or 0x9826, together with the previous error stack and mapped page. The unwind path at 00:27BB–27D9 restores them in reverse order. [confirmed]

save_error_context(target):
  push current_flash_page
  push previous_error_stack
  push FPS - word_at(0x9822)
  push OPS - word_at(0x9826)
  error_stack = SP
  jump target

unwind_error(error_code):
  SP = error_stack
  OPS = word_at(0x9826) + pop_word()
  FPS = word_at(0x9822) + pop_word()
  error_stack = pop_word()
  restore_flash_page(pop_word())
  return error_code
flowchart LR
    E["error entry<br/>82h or 88h"] --> C["00:270A<br/>common error path"]
    C --> U["00:27BB<br/>load saved error SP"]
    U --> O["restore OPS delta"]
    O --> F["restore FPS delta"]
    F --> P["restore previous error SP and page"]
    P --> H["error UI / caller continuation"]

The traces confirm the entry, common unwind, and pointer restoration. The error-screen Goto editor and every nested-error caller remain outside the current interpreter model.

What the coverage model establishes

tools/ti84re/tibasic/analyze_coverage.py ties eight finite models to byte signatures in the pinned ROM. It exhausts 591,360 states across token width, delimiters, one scan step, one block-depth transition, the extended-class fold, precedence family selection, command finalization, and page-33 table bounds. Z3 proves a minimum representative set for the semantic outcomes. [confirmed]

Dynamic evidence is a separate layer. Natural programs reach 38 of 52 outcomes at 26 selected branch sites. Public-bcall and internal-entry probes bring the declared outcome set to 52 of 52 while preserving provenance. The report records only trace hashes and compact counts; raw traces remain outside the repository. See TI-BASIC dynamic tracing for commands and exact boundaries. [confirmed]

The broader saturation audit starts at all 81 grammar-handler destinations and selected command, control-flow, value-storage, and numeric-error entries. It reaches 8,490 ROM instructions and 1,351 conditional branches, or 2,702 possible branch outcomes. The retained traces observe 924 outcomes; natural TI-BASIC programs account for 898. [confirmed]

This is deliberately not a claim of complete interpreter coverage. The four declared computed jumps are expanded over their ROM-defined valid domains, but corrupted or otherwise out-of-domain dispatch state is not modeled. Calls into display, graphing, and other ROM pages leave the declared regions, and arbitrary token streams, recursion depths, VAT layouts, and floating-point values remain open. The compact tools/oracles/tibasic/tibasic-saturation.json report records those boundaries explicitly.

Address map

AddressRole
00:1FE8_IsA2ByteTok
38:4000Grammar-handler pointer table
38:4130Matching End/Else scanner
38:4180Token-aware skip scanner
38:41E5Natural For( production entry
38:4200Natural End record consumer
38:4870Goto/Lbl name scanner
38:5987_ParseInp
38:5AB3Recursive expression evaluator
38:6251_StoAns
38:6910Stored-program statement-body entry
38:6FB7Grammar-class validation and high-token fold
38:7010Production-family selector
38:7248Cursor advance/refill
38:72DACurrent-token fetch and delimiter classification
38:758A_Find_Parse_Formula
38:7600Store/label name scanning region
38:778FNested stored-program body evaluator
02:5676Command finalization gate
33:435FBounded control-flow command dispatcher

The local finite-model evidence is tools/oracles/tibasic/tibasic-coverage.json. The broader direct-CFG evidence is tools/oracles/tibasic/tibasic-saturation.json. The selected backward error slices are in tools/oracles/tibasic/tibasic-numeric-errors.json. All three generators verify the pinned ROM before producing a report.