Parsers and evaluators: who reads text, and how the language reaches them (BASIC II)

← Acorn BBC BASIC

Parsers and evaluators in BBC BASIC II: who reads text, and how the language reaches them

Scope. This article maps the BBC BASIC II text-reading machinery — the tokeniser, the statement dispatcher, the expression evaluator and its factor-level readers, the lvalue parser, and the two string→value front doors VAL and EVAL. Every routine address is specific to this version and was read from the disassembly (which reassembles byte-identically to the ROM). It is a road-map: the deep dives on tokenising, control-flow stacks, floating point and indirection lvalues cover the individual pieces.

A recurring source of confusion about BBC BASIC — why does VAL("&FF") give 0 but EVAL("&FF") give 255? why is TOP not TO+P here but is there? why can an indirection be a procedure parameter? — dissolves once you see that BASIC II is not one parser. It is several distinct text-reading machines, each reached from the language in different places. Knowing which machine runs where is the whole game.

The machines at a glance

Layer Routine(s) Reads → produces Reached from BASIC by
Tokeniser (crunch) tokenise_line (&8951) source text → token bytes every line as typed/edited; EVAL re-runs it on a string
Statement dispatch next_statement (&8BA3) / dispatch_token (&8BB1) a token → a handler RUN, immediate mode, each :-separated statement
Expression evaluator eval_expr (&9B1D) → the precedence ladder token stream → a typed value anywhere an expression is wanted (PRINT, =, IF, arguments…)
Factor readers (level 1) eval_factor (&ADEC) and its sub-readers one atom → a value reached only through the evaluator
Lvalue parser parse_lvalue (&9582) / parse_var_ref (&95C9) text → an assignable location + type LET, FOR, LOCAL, PROC/FN parameters
String→value front doors fn_val (&AC2F) (VAL), fn_eval (&ABE9) (EVAL) a string → a value the VAL and EVAL functions

The rest of this article walks the three layers that most often surprise people: the evaluator ladder, the factor-level readers (where the number/hex/string/variable splits live), and the two front doors.

The expression evaluator: a precedence ladder

eval_expr (&9B1D) is the single entry point. It copies the program pointer PtrA into the working pointer PtrB and enters the lowest-precedence level. Each level evaluates a higher-precedence operand, then loops consuming its own operators — a classic recursive-descent precedence climb:

Level Routine Operators
7 (lowest) eval_or_eor (&9B29) OR, EOR
6 eval_and (&9B72) AND
5 eval_relational (&9B9C) < <= = >= > <>
4 eval_add_sub (&9C42) + - (and string concatenation)
3 eval_mul_div (&9DD1) * / DIV MOD
1 eval_factor (&ADEC) the atom

eval_factor (&ADEC) is "level 1" — the highest-precedence rung and the one that actually reads things. It handles unary -, + and NOT; a parenthesised sub-expression; the ?, !, $ and | indirection operators; string literals; and the built-in functions. Unlike the higher levels it does not read the trailing operator — its caller does. Everything below is a sub-reader dispatched from here.

The factor readers — where the splits live

This is the layer that explains the surface quirks. eval_factor looks at the first character of the atom and dispatches:

The decisive fact: the decimal reader and the hex reader are different routines, and & is reachable only through eval_factor. parse_number never sees a &. That single split is the root of the VAL/EVAL difference below.

The lvalue parser: one routine, four exposures

Assignment targets are read by a separate machine, parse_lvalue (&9582) (via parse_var_ref (&95C9)). It returns a storage address plus a type byte, and — crucially — it is shared verbatim by four language constructs: the left of LET, the FOR control variable, each LOCAL, and each PROC/FN formal parameter. Because they all call the same routine they accept the same grammar, which is why a ?/!/$ indirection or an array element is a legal FOR variable, LOCAL, and formal parameter — see indirection lvalues as parameters.

Two string→value front doors: VAL and EVAL

Both turn a string into a value, but through completely different machines:

That is the entire reason for the classic contrast:

Input VAL(x$) EVAL(x$) Why
"1E3" 1000 1000 both reach parse_number, which reads the E exponent
"&FF" 0 255 VAL's parse_number has no & branch; EVAL tokenises and reaches factor_hex
"12AB" 12 (depends) VAL stops at the first non-digit; EVAL evaluates 12 then sees AB as a variable/operator context
"2+3" 2 5 VAL reads one number; EVAL evaluates the whole expression

VAL is "read a literal"; EVAL is "be the interpreter". A compiler matching the ROM must model them as two readers, not one with a flag.

Surface consequences, and where they're documented

Most of BASIC II's "vexing parse" folklore is just the machine boundaries showing through:

Appendix: routine cross-reference

Concern Routine
Tokenise a line tokenise_line (&8951) &8951
Dispatch a statement token next_statement (&8BA3) &8BA3, dispatch_token (&8BB1) &8BB1
Evaluate an expression eval_expr (&9B1D) &9B1D
Precedence levels 7→3 eval_or_eor (&9B29), eval_and (&9B72), eval_relational (&9B9C), eval_add_sub (&9C42), eval_mul_div (&9DD1)
Evaluate a factor (level 1) eval_factor (&ADEC) &ADEC
Decimal/float literal reader parse_number (&A07B) &A07B
Hex (&) literal reader factor_hex (&AE6D) &AE6D
String literal reader read_string_literal (&ADAD) &ADAD
Variable / indirection scan parse_var_ref (&95C9) &95C9
Assignment-target (lvalue) parse parse_lvalue (&9582) &9582
VAL fn_val (&AC2F) &AC2Fascii_to_number (&AC34) &AC34
EVAL fn_eval (&ABE9) &ABE9