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 a page-38 OPS record

The public page-33 routine behind bcall grf_435f = 5140h subtracts 20h, accepts 13 indices, and jumps through the table at 33:4381. Three ABI probes confirm both bounds outcomes at 33:436D and 33:4372, but natural stored programs do not enter it. It is not the For(/End transition. [confirmed]

Natural For( execution reaches parse_for_production (38:41E5). Natural End execution reaches parse_end_ops_record (38:4200), which consumes a 5-byte loop record from the operator stack at OPS + 1. All three structured loops share the shape — sentinel byte, continuation word, state word: [confirmed]

#pragma pack(push, 1)
typedef struct {
    uint8_t sentinel;       /* 00h */
    uint16_t continuation;  /* where the End token jumps back to */
    uint16_t state;         /* varies per fixture; meaning open */
} TILoopOpsRecord;
#pragma pack(pop)

A shadow-memory replay of the headless trace records the loop state for a program that runs For(θ,1,3), While θ<3, and Repeat θ≥2 in sequence. The reproduction macro is tools/macros/run-loops-typed.macro.

LoopRecord at EndContinuationState
For( first End00 36 58 07 00for_first_update (38:5836)0007h
For( later Ends00 7D 58 07 00for_steady_update (38:587D)0007h
Repeat End00 E7 57 23 0038:57E70023h

The continuation field selects the loop mechanics. Observed state words include 0012h and 0007h, so the field is not a fixed constant; its exact role is still open. [confirmed]

While and Repeat push their records in a parse-time form with continuation 38:5AC1. Three observed pushes carry marker bytes F0 58, 11 58, and 2A 58. The first condition evaluation rewrites the record to the runtime form above. That evaluation runs through 38:41CC or 38:41D9 — each guards with CALL 7203, calls 71B4, pops the saved value into HL, and jumps to 38:57A8 or 38:57E1 respectively; 38:57E1 sits immediately before the Repeat continuation 38:57E7. [confirmed]

A single trace does not establish which loop re-enters through parse_end_ops_record on each iteration and which jumps directly from its continuation. [hypothesis]

flowchart LR
    F["For( token"] --> P["parse_for_production<br/>create production state"]
    P --> B["execute loop body"]
    B --> E["End token<br/>parse_end_ops_record"]
    E --> O["pop sentinel + continuation + state word"]
    O --> I["for_first_update"]
    O --> S["for_steady_update"]
    I --> B
    S --> B

The continuation path resolves the loop variable through the VAT, applies the floating-point increment, compares the updated value, and either revisits the body or removes the record. The paired trace confirms the record bytes and the two continuations. The complete layout of the associated limit, step, and temporary floating-point values is not yet decoded. [confirmed]

The optional closing ) in For( changes marker-to-marker work and parser buffer state. Neither spelling reaches the page-02 finalization gate in the paired trace, so the exact causal transition remains open. 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, _ParseInp, 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. Menu( parses a title followed by option-string and label pairs. These argument-order boundaries are [confirmed]; the entry editor’s internal cursor and redraw state are not yet mapped.

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 OP1 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.