Skip to content

Instantly share code, notes, and snippets.

@sma
Last active September 1, 2025 09:35
Show Gist options
  • Select an option

  • Save sma/ff7dd4ae2a3b136a315c537df5646368 to your computer and use it in GitHub Desktop.

Select an option

Save sma/ff7dd4ae2a3b136a315c537df5646368 to your computer and use it in GitHub Desktop.

Revisions

  1. sma revised this gist Sep 1, 2025. 4 changed files with 218 additions and 53 deletions.
    139 changes: 110 additions & 29 deletions 0tutorial.md
    Original file line number Diff line number Diff line change
    @@ -1,6 +1,6 @@
    # Building a FOCAL Interpreter in Dart

    [The Sumerian Game](https://en.wikipedia.org/wiki/The_Sumerian_Game) is one of the oldest computer games, created in 1964 as an education game. Its source code is unfortunately lost, but a simplified version was ported in 1969 to [FOCAL](https://en.wikipedia.org/wiki/FOCAL_(programming_language)) for the PDP8 under the name King of Sumeria and then to BASIC around 1971 and eventually published as Hamurabi in David Ahl's 101 _BASIC Computer Games_ book.
    [The Sumerian Game](https://en.wikipedia.org/wiki/The_Sumerian_Game) is one of the oldest computer games, created in 1964 as an education game. Its source code is unfortunately lost, but a simplified version was ported in 1969 to [FOCAL](https://en.wikipedia.org/wiki/FOCAL_(programming_language)) for the PDP8 under the name King of Sumeria and then to BASIC around 1971 and eventually published as Hamurabi in David Ahl's _101 BASIC Computer Games_ book.

    ## What's This All About?

    @@ -52,25 +52,25 @@ const hamurabi = '''
    06.10 T !!"HAMURABI: "%5
    07.10 I (C)7.2;S C=C-1;D 6;T "BUT YOU HAVE ONLY"
    07.10 I (C)7.2;S C=C-1;D 6;T "BUT YOU HAVE ONLY";R
    07.20 D 6;T !"GOODBYE!"!!;Q
    08.10 S C=FITR(5*FRAN())+1
    ''';
    ```

    \* I replaced the last `!` with `;` in `04.80`, removed a superfluous `;R` in `07.10`, removed the superflous `FABS` and `ABS` calls in `05.40` and `08.10`, especially as `ABS` seems to be missing an `F`.
    \* I replaced the last `!` with `;` in `04.80`, removed the superflous `FABS` and `ABS` calls in `05.40` and `08.10`, especially as `ABS` seems to be missing an `F`.

    ### Explaination

    FOCAL programs have line numbers split into block numbers (01-31) and line numbers within that block (01-99). Within a line, commands are separated with `;` and start with a single letter.
    FOCAL programs have line numbers split into group numbers (01-31) and step numbers within that group (01-99). Within a line, commands are separated with `;` and start with a single letter.
    * ASK prompts for a numeric input and stores it into a variable.
    * DO jumps to a subroutine, automatically returning at the end of that line.
    * GOTO jumps to another line. In both cases, the line number part can be omitted and if that value is less than 10, it is multiplied by 10.
    * IF jumps conditionally whether a numeric value is less than zero, zero, or greater than zero.
    * DO jumps to a subroutine, automatically returning at the end of that group or line.
    * GOTO jumps to another line. In both cases, the step number part can be omitted and if that value is less than 10, it is multiplied by 10.
    * IF jumps conditionally if a numeric value is less than zero, zero, or greater than zero.
    * QUIT stops the execution of the program.
    * SET sets a variable to some numeric value. There are 26 variables A to Z that can store numbers.
    * TYPE outputs text and numeric values. A `!` stands for a newline, a `,` concatenates expressions, and `%N` sets the padding for numeric values.
    * TYPE outputs text and numeric values. A `!` stands for a newline, a `,` concatenates expressions, and `%` sets the padding for numeric values.

    ## Plan

    @@ -356,7 +356,7 @@ The `run()` function parses multi-line programs and manages execution:

    ```dart
    final lines = <String>[];
    final stack = <(Input, int)>[];
    final stack = <(Input, int, bool)>[];
    Input get line => stack.last.$1; // final definition
    @@ -371,25 +371,21 @@ void run(String input) {
    execute();
    }
    void _addLine(int index) =>
    stack.add((Input(lines[index].substring(6)), index + 1));
    void _addLine(int index, [bool g = false]) =>
    stack.add((Input(lines[index].substring(6)), index + 1, g));
    ```

    The `stack` manages execution context - each entry contains the current line's `Input` parser and the index of the next line to execute. The global `line` reference always points to the topmost stack entry. The `_addLine()` helper strips the line number prefix and pushes a new execution context together with the index of the next line.
    The `stack` manages execution context - each entry contains the current line's `Input` parser, the index of the next line to execute, and a flag whether `DO` shall execute a group or a single line. The global `line` reference always points to the topmost stack entry. The `_addLine()` helper strips the line number prefix and pushes a new execution context together with the index of the next line and the optional "group" flag.

    Enhanced `execute()` for multi-line programs:

    ```dart
    void execute() {
    for (;;) {
    if (line.isEmpty) {
    if (stack.length == 1) {
    final (_, next) = stack.removeLast();
    if (next == lines.length) break;
    _addLine(next);
    } else {
    stack.removeLast();
    }
    final (_, next, g) = stack.removeLast();
    if (next == lines.length) return;
    _addLine(next, g);
    continue;
    }
    if (line.matches(';')) continue;
    @@ -399,7 +395,7 @@ void execute() {
    }
    ```

    When a line finishes, execution continues with the next sequential line. If we're in a subroutine (stack depth > 1), we return to the caller instead.
    When a line finishes, execution continues with the next sequential line.

    Test with a simple multi-line program:
    ```dart
    @@ -418,22 +414,35 @@ Add commands as needed when running Hamurabi. Start with DO (subroutines):
    ```dart
    void execute() {
    for (;;) {
    if (line.isEmpty) {
    final (_, next, g) = stack.removeLast();
    if (stack.isEmpty) {
    if (next == lines.length) return;
    _addLine(next, g);
    } else {
    if (g && next < lines.length && lines[next].substring(0, 2) == lines[next - 1].substring(0, 2)) {
    _addLine(next, g);
    }
    }
    continue;
    }
    // ... existing code ...
    switch (line.next()) {
    // ... existing cases ...
    case 'D':
    _addLine(_index(_target()));
    final (target, g) = _target();
    _addLine(_index(target), g);
    // ... rest of cases ...
    }
    }
    }
    String _target() {
    (String, bool) _target() {
    var t = line.match(RegExp(r'\d\d?(\.\d\d?)?')) ?? (throw 'missing jump target');
    final parts = t.split('.');
    if (parts.length == 1) return parts.single.padLeft(2, '0');
    return '${parts[0].padLeft(2, '0')}.${parts[1].padRight(2, '0')}';
    if (parts.length == 1) return (parts.single.padLeft(2, '0'), true);
    return ('${parts[0].padLeft(2, '0')}.${parts[1].padRight(2, '0')}', false);
    }
    int _index(String target) {
    @@ -444,20 +453,41 @@ int _index(String target) {
    }
    ```

    DO calls a subroutine by pushing the target line onto the stack - each FOCAL subroutine is exactly one line that automatically returns when finished. The `_target()` function normalizes line numbers (e.g., "6" becomes "06.10", "4.3" becomes "04.30") for consistent prefix matching. The `_index()` function then searches for the matching line number in our program.
    If we're in a subroutine (stack depth > 1), we return to the caller if the end of the current group was reached or the end of the current line.

    DO calls a subroutine by pushing the target line onto the stack. The `_target()` function normalizes line numbers (e.g., "6" becomes "06", "4.3" becomes "04.30") for consistent prefix matching. It also returns whether a group call or a single line call shall be done.

    The `_index()` function then searches for the matching line number in our program.

    Here is a test:
    ```dart
    run('''
    01.10 D 2;D 2.2
    02.10 T"Hello
    02.20 T"World
    ''');
    '''); // prints HelloWorldWorldHelloWorld
    ```

    Add RETURN:
    ```dart
    case 'R':
    if (stack.length == 1) throw 'no subroutine to return from';
    stack.removeLast();
    ```

    Here is a test:
    ```dart
    run('''
    01.10 D 2;D 2.2
    02.10 T"Hello;R
    02.20 T"World
    '''); // prints HelloWorldHello before crashing
    ```

    Add GOTO (unconditional jump):
    ```dart
    case 'G':
    _setLine(_index(_target()));
    _setLine(_index(_target().$1));
    void _setLine(int index) {
    stack.removeLast();
    @@ -470,14 +500,15 @@ GOTO simply jumps to another line using the existing target resolution and line
    Here is the classic endless print loop:
    ```dart
    run('01.10 T!"Hello, World!";G 1');
    ```

    Add IF (conditional jumps):
    ```dart
    case 'I':
    final value = expr();
    final targets = [_target()];
    final targets = [_target().$1];
    while (line.matches(',')) {
    targets.add(_target());
    targets.add(_target().$1);
    }
    final index = value.compareTo(0) + 1;
    if (index < targets.length) _setLine(_index(targets[index]));
    @@ -539,3 +570,53 @@ We built a complete FOCAL interpreter by:
    7. Adding built-in mathematical functions

    The bottom-up approach let us test each component independently before integration, making debugging easier and ensuring correctness at each step.

    ## Addendum

    Let's also run the [lunar lander](https://www.pdp8online.com/ftp/software/games/focal/lunar.fcl) program.

    * `S G=.001` is not valid because `_factor` can only deal with integers so far. We have to change `_digits`:

    ```dart
    final _digits = RegExp(r'\d+(\.\d+)?|\.\d+');
    ```
    * `F X=1,51` is an unknown command. According to [the manual](http://bitsavers.informatik.uni-stuttgart.de/pdf/dec/pdp8/focal/DEC-08-AJAB-D_FOCAL_Programming_Manual_Jan70.pdf), this a FOR loop which is used to type 51x `.` Because that line is only reached on invalid input, we can ignore this.
    * `Q^2` is not supported. Let's rename `_factor` to `_power` and add a new `_factor` implementation:
    ```dart
    num _factor() {
    for (var result = _power(); ;) {
    if (line.matches('^')) {
    result = pow(result, _power());
    } else {
    return result;
    }
    }
    }
    num _power() {
    if (line.matches('-')) {
    return -_power();
    // rest of the old _factor
    ```
    * The `FSQT` function is missing. It can be added to `_factor`:
    ```dart
    } else if (line.matches('FSQT')) {
    return sqrt(expr());
    ```
    * I also need to support `%4.02` meaning that a number should be formatted as `xx.yy`. Also, FOCAL will prefix each number with `= ` for whatever reason. I need to add this or the tabular display breaks. So let's add this to `output`:
    ```dart
    } else if (line.matches('%')) {
    padding = expr();
    } else if (withExpr) {
    final p = padding.toInt();
    final f = ((padding - p) * 100).round();
    stdout.write('= ');
    stdout.write(expr().toStringAsFixed(f).padLeft(p + f.sign));
    ```
    73 changes: 52 additions & 21 deletions 1focal.dart
    Original file line number Diff line number Diff line change
    @@ -3,10 +3,18 @@ import 'dart:math';

    final lines = <String>[];

    final stack = <(Input, int)>[];
    final stack = <(Input, int, bool)>[];

    Input get line => stack.last.$1;

    set line(Input value) {
    lines
    ..clear()
    ..add('00.00 ${value.string}');
    stack.clear();
    _addLine(0);
    }

    final variables = <String, num>{};

    void run(String input) {
    @@ -23,12 +31,14 @@ void run(String input) {
    void execute() {
    for (;;) {
    if (line.isEmpty) {
    if (stack.length == 1) {
    final (_, next) = stack.removeLast();
    if (next == lines.length) break;
    _addLine(next);
    final (_, next, g) = stack.removeLast();
    if (stack.isEmpty) {
    if (next == lines.length) return;
    _addLine(next, g);
    } else {
    stack.removeLast();
    if (g && next < lines.length && lines[next].substring(0, 2) == lines[next - 1].substring(0, 2)) {
    _addLine(next, g);
    }
    }
    continue;
    }
    @@ -42,19 +52,23 @@ void execute() {
    if (input == null) return;
    variables[name] = num.parse(input);
    case 'D':
    _addLine(_index(_target()));
    final (target, g) = _target();
    _addLine(_index(target), g);
    case 'G':
    _setLine(_index(_target()));
    _setLine(_index(_target().$1));
    case 'I':
    final value = expr();
    final targets = [_target()];
    final targets = [_target().$1];
    while (line.matches(',')) {
    targets.add(_target());
    targets.add(_target().$1);
    }
    final index = value.compareTo(0) + 1;
    if (index < targets.length) _setLine(_index(targets[index]));
    case 'Q':
    return;
    case 'R':
    if (stack.length == 1) throw 'no subroutine to return from';
    stack.removeLast();
    case 'S':
    final name = line.match(_variables) ?? (throw 'missing variable');
    line.match('=') ?? (throw 'missing =');
    @@ -119,20 +133,32 @@ num _term() {
    }

    num _factor() {
    for (var result = _power(); ;) {
    if (line.matches('^')) {
    result = pow(result, _power());
    } else {
    return result;
    }
    }
    }

    num _power() {
    if (line.matches('-')) {
    return -_factor();
    return -_power();
    } else if (line.matches('(')) {
    final result = expr();
    line.match(')') ?? (throw 'missing )');
    line.match(')') ?? (throw 'missing ) in $line');
    return result;
    } else if (line.match(_digits) case final number?) {
    return num.tryParse(number) ?? (throw 'invalid number: $number');
    } else if (line.match(_variables) case final name?) {
    return variables[name] ?? (throw 'unset variable: $name');
    return variables[name] ?? (throw 'unset variable: $name in $line');
    } else if (line.matches('FITR')) {
    return expr().toInt();
    } else if (line.matches('FRAN()')) {
    return Random().nextDouble();
    } else if (line.matches('FSQT')) {
    return sqrt(expr());
    }
    throw 'syntax error: $line';
    }
    @@ -150,29 +176,34 @@ void output(bool withExpr) {
    stdout.write(ch);
    }
    } else if (line.matches('%')) {
    expr(); // padding value (ignored)
    padding = expr();
    } else if (withExpr) {
    stdout.write('${expr()}'.padLeft(5));
    final p = padding.toInt();
    final f = ((padding - p) * 100).round();
    stdout.write('= ');
    stdout.write(expr().toStringAsFixed(f).padLeft(p + f.sign));
    } else {
    break;
    }
    }
    }

    void _addLine(int index) {
    stack.add((Input(lines[index].substring(6)), index + 1));
    num padding = 8.04;

    void _addLine(int index, [bool g = false]) {
    stack.add((Input(lines[index].substring(6)), index + 1, g));
    }

    void _setLine(int index) {
    stack.removeLast();
    _addLine(index);
    }

    String _target() {
    (String, bool) _target() {
    var t = line.match(RegExp(r'\d\d?(\.\d\d?)?')) ?? (throw 'missing jump target');
    final parts = t.split('.');
    if (parts.length == 1) return parts.single.padLeft(2, '0');
    return '${parts[0].padLeft(2, '0')}.${parts[1].padRight(2, '0')}';
    if (parts.length == 1) return (parts.single.padLeft(2, '0'), true);
    return ('${parts[0].padLeft(2, '0')}.${parts[1].padRight(2, '0')}', false);
    }

    int _index(String target) {
    @@ -182,5 +213,5 @@ int _index(String target) {
    throw 'no such line: $target';
    }

    final _digits = RegExp(r'\d+');
    final _digits = RegExp(r'\d+(\.\d+)?|\.\d+');
    final _variables = RegExp(r'[A-EG-Z]');
    6 changes: 3 additions & 3 deletions 2hamurabi.dart
    Original file line number Diff line number Diff line change
    @@ -1,4 +1,4 @@
    import 'package:focal/focal.dart';
    import 'package:focal/focal.dart' as focal;

    const hamurabi = '''
    01.10 S P=95;S S=2800;S H=3000;S E=200;S Y=3;S A=1000;S I=5;S Q=1
    @@ -43,12 +43,12 @@ const hamurabi = '''
    06.10 T !!"HAMURABI: "%5
    07.10 I (C)7.2;S C=C-1;D 6;T "BUT YOU HAVE ONLY"
    07.10 I (C)7.2;S C=C-1;D 6;T "BUT YOU HAVE ONLY";R
    07.20 D 6;T !"GOODBYE!"!!;Q
    08.10 S C=FITR(5*FRAN())+1
    ''';

    void main() {
    run(hamurabi);
    focal.run(hamurabi);
    }
    53 changes: 53 additions & 0 deletions 3lander.dart
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,53 @@
    import 'package:focal/focal.dart' as focal;

    const lander = '''
    01.04 T "CONTROL CALLING LUNAR MODULE.MANUAL CONTROL IS NECESSARY"!
    01.06 T "YOU MAY RESET FUEL RATE K EACH 10 SECS TO 0 OR ANY VALUE"!
    01.08 T "BETWEEN 8&200 LBS/SEC.YOU'VE 16000 LBS FUEL.ESTIMATED"!
    01.11 T "FREE FALL IMPACT TIME-120 SECS.CAPSULE WEIGHT-32500 LBS"!
    01.20 T "FIRST RADAR CHECK COMING UP"!!!
    01.30 T "COMMENCE LANDING PROCEDURE"!"TIME,SECS ALTITUDE,"
    01.40 T "MILES+FEET VELOCITY,MPH FUEL,LBS FUEL RATE"!
    02.05 S L=0;S A=120;S V=1;S M=33000;S N=16500;S G=.001;S Z=1.8
    02.10 T " ",%3,L," ",FITR(A)," ",%4,5280*(A-FITR(A))
    02.20 T %6.02," ",3600*V," ",%6.01,M-N," K=";A K;S T=10
    02.70 T %7.02;I (K)2.72;I (200-K)2.72;I (K-8)2.71,3.1,3.1
    02.71 I (K-0)2.72,3.1,2.72
    02.72 T "NOT POSSIBLE";F X=1,51;T "."
    02.73 T "K=";A K;G 2.7
    03.10 I ((M-N)-.001)4.1;I (T-.001)2.1;S S=T
    03.40 I ((N+S*K)-M)3.5,3.5;S S=(M-N)/K
    03.50 D 9;I (I)7.1,7.1;I (V)3.8,3.8;I (J)8.1
    03.80 D 6;G 3.1
    04.10 T "FUEL OUT AT",L," SECS"!
    04.40 S S=(-V+FSQT(V*V+2*A*G))/G;S V=V+G*S;S L=L+S
    05.10 T "ON THE MOON AT",L," SECS"!;S W=3600*V
    05.20 T "IMPACT VELOCITY OF",W," M.P.H."!,"FUEL LEFT:"
    05.30 T M-N," LBS."!;I (-W+1)5.5,5.5
    05.40 T "PERFECT LANDING !-(LUCKY)"!;G 5.9
    05.50 I (-W+10)5.6,5.6;T "GOOD LANDING-(COULD BE BETTER)"!;G 5.90
    05.60 I (-W+25)5.7,5.7;T "CONGRATULATIONS ON A POOR LANDING"!;G 5.9
    05.70 I (-W+60)5.8,5.8;T "CRAFT DAMAGE.GOOD LUCK"!;G 5.9
    05.80 T "SORRY,BUT THERE WERE NO SURVIVORS-YOU BLEW IT!"!"IN"
    05.81 T "FACT YOU BLASTED A NEW LUNAR CRATER",W*.277777,"FT.DEEP.
    05.90 T "CONTROL OUT";Q
    06.10 S L=L+S;S T=T-S;S M=M-S*K;S A=I;S V=J
    07.10 I (S-.005)5.1;S S=2*A/(V+FSQT(V*V+2*A*(G-Z*K/M)))
    07.30 D 9;D 6;G 7.1
    08.10 S W=(1-M*G/(Z*K))/2;S S=M*V/(Z*K*(W+FSQT(W*W+V/Z)))+.05;D 9
    08.30 I (I)7.1,7.1;D 6;I (-J)3.1,3.1;I (V)3.1,3.1,8.1
    09.10 S Q=S*K/M;S J=V+G*S+Z*(-Q-Q^2/2-Q^3/3-Q^4/4-Q^5/5)
    09.40 S I=A-G*S*S/2-V*S+Z*S*(Q/2+Q^2/6+Q^3/12+Q^4/20+Q^5/30)
    ''';

    void main() {
    focal.run(lander);
    }
  2. sma created this gist Aug 28, 2025.
    541 changes: 541 additions & 0 deletions 0tutorial.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,541 @@
    # Building a FOCAL Interpreter in Dart

    [The Sumerian Game](https://en.wikipedia.org/wiki/The_Sumerian_Game) is one of the oldest computer games, created in 1964 as an education game. Its source code is unfortunately lost, but a simplified version was ported in 1969 to [FOCAL](https://en.wikipedia.org/wiki/FOCAL_(programming_language)) for the PDP8 under the name King of Sumeria and then to BASIC around 1971 and eventually published as Hamurabi in David Ahl's 101 _BASIC Computer Games_ book.

    ## What's This All About?

    This tutorial shows how to build a FOCAL interpreter from scratch.

    Here's the original* Hamurabi program we'll run:

    ```dart
    const hamurabi = '''
    01.10 S P=95;S S=2800;S H=3000;S E=200;S Y=3;S A=1000;S I=5;S Q=1
    02.10 S D=0
    02.20 D 6;T !!!"LAST YEAR"!D," STARVED,
    02.25 T !I," ARRIVED,";S P=P+I;I (-Q)2.3
    02.27 S P=FITR(P/2);T !"**PLAGUE**"!
    02.30 T !"POPULATION IS"P,!!"THE CITY OWNS
    02.35 T A," ACRES."!!;I (H-1)2.5;T "WE HARVESTED
    02.40 D 3.2
    02.50 T !" RATS ATE "E," BUSHELS, YOU NOW HAVE
    02.60 T !S," BUSHELS IN STORE."!
    03.10 D 6; D 8;S Y=C+17;T "LAND IS TRADING AT
    03.20 T Y," BUSHELS PER ACRE;";S C=1
    03.30 D 4.3;A " BUY?"!Q;I (Q)7.2,3.8
    03.40 I (Y*Q-S)3.9,3.6;D 4.6;G 3.3
    03.50 D 4.5;G 3.3
    03.60 D 3.9:G 4.8
    03.70 S A=A+Q;S S=Y*Q;S C=0
    03.80 A !"TO SELL?"!Q;I (Q)7.2,3.9;S Q=-Q;I (A+Q)3.5
    03.90 S A=A+Q;S S=S-Y*Q;S C=0
    04.10 T !"BUSHELS TO USE
    04.11 A " AS FOOD?"!Q;I (Q)7.7;I (Q-S)4.2,4.7;D 4.6;G 4.1
    04.20 S S=S-Q;S C=1
    04.30 A !"HOW MANY ACRES OF LAND DO YOU WISH TO
    04.35 A !"PLANT WITH SEED? "D
    04.40 I (D)7.2;I (A-D)4.45;I (FITR(D/2)-S-1)4.65;D 4.6;G 4.3
    04.45 D 4.5;G 4.3
    04.50 D 7;T A," ACRES."!
    04.60 D 7;D 2.6
    04.65 I (D-10*P-1)5.1;D 7;T P," PEOPLE."!;G 4.3
    04.70 D 4.2
    04.80 D 6;T "YOU HAVE NO GRAIN LEFT AS SEED !!";S D=0
    05.10 S S=S-FITR(D/2);D 8;S Y=C;S H=D*Y
    05.20 D 8;S E=0;I (FITR(C/2)-C/2)5.3;S E=FITR(S/C)
    05.30 S S=S-E+H;D 8;S I=FITR(C*(20*A+S)/P/100+1);S C=FITR(Q/20)
    05.40 S Q=FITR(10*FRAN());I (P-C)2.1;S D=P-C;S P=C;G 2.2
    06.10 T !!"HAMURABI: "%5
    07.10 I (C)7.2;S C=C-1;D 6;T "BUT YOU HAVE ONLY"
    07.20 D 6;T !"GOODBYE!"!!;Q
    08.10 S C=FITR(5*FRAN())+1
    ''';
    ```

    \* I replaced the last `!` with `;` in `04.80`, removed a superfluous `;R` in `07.10`, removed the superflous `FABS` and `ABS` calls in `05.40` and `08.10`, especially as `ABS` seems to be missing an `F`.

    ### Explaination

    FOCAL programs have line numbers split into block numbers (01-31) and line numbers within that block (01-99). Within a line, commands are separated with `;` and start with a single letter.
    * ASK prompts for a numeric input and stores it into a variable.
    * DO jumps to a subroutine, automatically returning at the end of that line.
    * GOTO jumps to another line. In both cases, the line number part can be omitted and if that value is less than 10, it is multiplied by 10.
    * IF jumps conditionally whether a numeric value is less than zero, zero, or greater than zero.
    * QUIT stops the execution of the program.
    * SET sets a variable to some numeric value. There are 26 variables A to Z that can store numbers.
    * TYPE outputs text and numeric values. A `!` stands for a newline, a `,` concatenates expressions, and `%N` sets the padding for numeric values.

    ## Plan

    We'll build the interpreter bottom-up:

    1. **Input Class** - Pattern matching foundation
    2. **Expression Parser** - Math expressions with precedence
    3. **Output System** - Text and number formatting
    4. **Minimal Executor** - Basic command loop with TYPE
    5. **User Input & Semicolons** - ASK command and statement separators
    6. **Single-line Programs** - Practical calculator examples
    7. **Variables** - SET command for storage
    8. **Program Structure** - Multi-line program loading
    9. **Control Flow** - DO, GOTO, IF commands as needed
    10. **Built-ins** - FITR, FRAN functions
    11. **Integration** - Complete Hamurabi game

    Each step builds on the previous and can be tested immediately.

    ## Input Class

    The `Input` class handles pattern matching and tokenization:

    ```dart
    class Input {
    Input(this.string);
    final String string;
    var index = 0;
    bool get isEmpty => index == string.length;
    String get peek => isEmpty ? '' : string[index];
    String next() => isEmpty ? '' : string[index++];
    String? match(Pattern pattern) {
    while (peek == ' ' || peek == '\t') {
    next();
    }
    final match = pattern.matchAsPrefix(string, index);
    if (match != null) {
    index = match.end;
    return match[0];
    }
    return null;
    }
    bool matches(Pattern pattern) => match(pattern) != null;
    @override
    String toString() => '${string.substring(0, index)} →${string.substring(index)}';
    }
    ```

    Key methods:
    - `isEmpty` checks if all input is consumed
    - `match()` consumes matching patterns, skipping whitespace
    - `matches()` tests patterns without consuming
    - `toString()` shows current position with arrow

    Test it:
    ```dart
    final input = Input(' 123 + 456');
    print(input.match(RegExp(r'\d+'))); // "123"
    print(input.matches('+')); // true
    print(input); // " 123 + →456"
    ```

    ## Expression Parser

    FOCAL expressions support `+`, `-`, `*`, `/`, parentheses, numbers, and variables. We use recursive descent parsing:

    ```dart
    final _digits = RegExp(r'\d+');
    final _variables = RegExp(r'[A-EG-Z]');
    final variables = <String, num>{};
    var line = Input(''); // will be changed later
    num expr() {
    for (var value = _term(); ;) {
    if (line.matches('+')) {
    value += _term();
    } else if (line.matches('-')) {
    value -= _term();
    } else {
    return value;
    }
    }
    }
    num _term() {
    for (var value = _factor(); ;) {
    if (line.matches('*')) {
    value *= _factor();
    } else if (line.matches('/')) {
    value /= _factor();
    } else {
    return value;
    }
    }
    }
    num _factor() {
    if (line.matches('-')) {
    return -_factor();
    } else if (line.matches('(')) {
    final result = expr();
    line.match(')') ?? (throw 'missing )');
    return result;
    } else if (line.match(_digits) case final number?) {
    return num.tryParse(number) ?? (throw 'invalid number: $number');
    } else if (line.match(_variables) case final name?) {
    return variables[name] ?? (throw 'unset variable: $name');
    }
    throw 'syntax error: $line';
    }
    ```

    Note: FOCAL uses F for function prefix, so variable F is reserved.

    Test expressions:
    ```dart
    line = Input('2 + 3 * 4');
    print(expr()); // 14
    variables['A'] = 10;
    line = Input('A * 2 + -5');
    print(expr()); // 15
    line = Input('(2 + 3) * 4');
    print(expr()); // 20
    ```

    ## Output System

    The `output()` function handles FOCAL's text formatting:
    - `!` = newline
    - `,` = concatenates expressions
    - `"text"` = literal strings
    - `%N` = number padding (parsed but ignored)
    - expressions = numeric output (padded to 5 characters)

    ```dart
    import 'dart:io';
    void output(bool withExpr) {
    while (!line.isEmpty && !line.matches(';')) {
    if (line.matches('!')) {
    stdout.write('\n');
    } else if (line.matches(',')) {
    } else if (line.matches('"')) {
    for (;;) {
    if (line.isEmpty) return;
    final ch = line.next();
    if (ch == '"') break;
    stdout.write(ch);
    }
    } else if (line.matches('%')) {
    expr(); // padding value (ignored)
    } else if (withExpr) {
    stdout.write('${expr()}'.padLeft(5));
    } else {
    break;
    }
    }
    }
    ```

    Test output:
    ```dart
    line = Input('"Hello"!');
    output(false); // Hello\n
    variables['X'] = 42;
    line = Input('"Result:"X');
    output(true); // Result: 42
    ```

    ## Minimal Executor

    Basic command loop that handles TYPE commands:

    ```dart
    void execute() {
    for (;;) {
    if (line.isEmpty) break;
    if (line.matches(';')) continue;
    switch (line.next()) {
    case 'T':
    output(true);
    default:
    throw 'syntax error: $line';
    }
    }
    }
    ```

    Test TYPE command:
    ```dart
    line = Input('T "2 + 3 ="2+3!!');
    execute(); // 2 + 3 = 5\n
    ```

    ## User Input & Semicolons

    Add ASK command for reading numbers:

    ```dart
    import 'dart:io';
    void execute() {
    for (;;) {
    if (line.isEmpty) break;
    if (line.matches(';')) continue;
    switch (line.next()) {
    case 'A':
    output(false);
    final name = line.match(_variables);
    if (name == null) break;
    final input = stdin.readLineSync();
    if (input == null) return;
    variables[name] = num.parse(input);
    case 'T':
    output(true);
    default:
    throw 'syntax error: $line';
    }
    }
    }
    ```

    Now you can run interactive single-line programs.

    ## Single-line Programs

    Test with a practical example - convert Celsius to Fahrenheit:
    ```dart
    line = Input('A "Celsius: "C; T "Fahrenheit: "C*9/5+32!');
    execute();
    ```

    ## Variables

    Add SET command for storing intermediate results:

    ```dart
    void execute() {
    for (;;) {
    if (line.isEmpty) break;
    if (line.matches(';')) continue;
    switch (line.next()) {
    case 'A':
    output(false);
    final name = line.match(_variables);
    if (name == null) break;
    final input = stdin.readLineSync();
    if (input == null) return;
    variables[name] = num.parse(input);
    case 'S':
    final name = line.match(_variables) ?? (throw 'missing variable');
    line.match('=') ?? (throw 'missing =');
    variables[name] = expr();
    case 'T':
    output(true);
    default:
    throw 'syntax error: $line';
    }
    }
    }
    ```

    Test with intermediate calculations:
    ```dart
    line = Input('A "Base: "B; A "Height: "H; S A=B*H/2; T "Area: "A!');
    execute();
    ```

    ## Program Structure

    The `run()` function parses multi-line programs and manages execution:

    ```dart
    final lines = <String>[];
    final stack = <(Input, int)>[];
    Input get line => stack.last.$1; // final definition
    void run(String input) {
    final validLine = RegExp(r'^\d\d\.\d\d ').hasMatch;
    lines
    ..clear()
    ..addAll(input.split('\n').where(validLine));
    stack.clear();
    variables.clear();
    _addLine(0);
    execute();
    }
    void _addLine(int index) =>
    stack.add((Input(lines[index].substring(6)), index + 1));
    ```

    The `stack` manages execution context - each entry contains the current line's `Input` parser and the index of the next line to execute. The global `line` reference always points to the topmost stack entry. The `_addLine()` helper strips the line number prefix and pushes a new execution context together with the index of the next line.

    Enhanced `execute()` for multi-line programs:

    ```dart
    void execute() {
    for (;;) {
    if (line.isEmpty) {
    if (stack.length == 1) {
    final (_, next) = stack.removeLast();
    if (next == lines.length) break;
    _addLine(next);
    } else {
    stack.removeLast();
    }
    continue;
    }
    if (line.matches(';')) continue;
    // ... existing switch cases ...
    }
    }
    ```

    When a line finishes, execution continues with the next sequential line. If we're in a subroutine (stack depth > 1), we return to the caller instead.

    Test with a simple multi-line program:
    ```dart
    const program = '''
    01.10 S A=5
    01.20 S B=3
    01.30 T "Sum: "A+B!
    ''';
    run(program); // Sum: 8
    ```

    ## Control Flow Commands

    Add commands as needed when running Hamurabi. Start with DO (subroutines):

    ```dart
    void execute() {
    for (;;) {
    // ... existing code ...
    switch (line.next()) {
    // ... existing cases ...
    case 'D':
    _addLine(_index(_target()));
    // ... rest of cases ...
    }
    }
    }
    String _target() {
    var t = line.match(RegExp(r'\d\d?(\.\d\d?)?')) ?? (throw 'missing jump target');
    final parts = t.split('.');
    if (parts.length == 1) return parts.single.padLeft(2, '0');
    return '${parts[0].padLeft(2, '0')}.${parts[1].padRight(2, '0')}';
    }
    int _index(String target) {
    for (var i = 0; i < lines.length; i++) {
    if (lines[i].startsWith(target)) return i;
    }
    throw 'no such line: $target';
    }
    ```

    DO calls a subroutine by pushing the target line onto the stack - each FOCAL subroutine is exactly one line that automatically returns when finished. The `_target()` function normalizes line numbers (e.g., "6" becomes "06.10", "4.3" becomes "04.30") for consistent prefix matching. The `_index()` function then searches for the matching line number in our program.

    Here is a test:
    ```dart
    run('''
    01.10 D 2;D 2.2
    02.10 T"Hello
    02.20 T"World
    ''');
    Add GOTO (unconditional jump):
    ```dart
    case 'G':
    _setLine(_index(_target()));
    void _setLine(int index) {
    stack.removeLast();
    _addLine(index);
    }
    ```

    GOTO simply jumps to another line using the existing target resolution and line indexing functions.

    Here is the classic endless print loop:
    ```dart
    run('01.10 T!"Hello, World!";G 1');
    Add IF (conditional jumps):
    ```dart
    case 'I':
    final value = expr();
    final targets = [_target()];
    while (line.matches(',')) {
    targets.add(_target());
    }
    final index = value.compareTo(0) + 1;
    if (index < targets.length) _setLine(_index(targets[index]));
    ```

    IF evaluates an expression and conditionally jumps based on its sign. It takes 1-3 arguments: the line to jump to if negative, optionally if zero, and optionally if positive. For example, `I (X-5)10,20,30` jumps to line 10 if X<5, line 20 if X=5, or line 30 if X>5. If fewer targets are provided or the condition doesn't match any target, execution continues normally.

    ## Built-in Functions

    Add FITR (truncate) and FRAN (random) functions to `_factor()` because they are used by the Hamurabi code:

    ```dart
    num _factor() {
    // ... existing code ...
    } else if (line.matches('FITR')) {
    return expr().toInt();
    } else if (line.matches('FRAN()')) {
    return Random().nextDouble();
    }
    throw 'syntax error: $line';
    }
    ```

    ## Program Termination

    Last but not least, add QUIT command to stop execution:

    ```dart
    switch (line.next()) {
    // ... existing cases ...
    case 'Q':
    return;
    // ... rest of cases ...
    }
    ```

    ## Final Integration

    Your complete interpreter can now run the Hamurabi game:

    ```dart
    void main() {
    focal.run(hamurabi);
    }
    ```

    Hopefully, the interpreter correctly handles all FOCAL commands and preserves the original behavior of the game.

    ## Summary

    We built a complete FOCAL interpreter by:

    1. Creating a flexible input tokenizer
    2. Implementing expression parsing with operator precedence
    3. Building output formatting for text and numbers
    4. Adding interactive commands (ASK, SET, TYPE)
    5. Supporting multi-line programs with line numbers
    6. Implementing control flow (DO, GOTO, IF)
    7. Adding built-in mathematical functions

    The bottom-up approach let us test each component independently before integration, making debugging easier and ensuring correctness at each step.
    186 changes: 186 additions & 0 deletions 1focal.dart
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,186 @@
    import 'dart:io';
    import 'dart:math';

    final lines = <String>[];

    final stack = <(Input, int)>[];

    Input get line => stack.last.$1;

    final variables = <String, num>{};

    void run(String input) {
    final validLine = RegExp(r'^\d\d\.\d\d ').hasMatch;
    lines
    ..clear()
    ..addAll(input.split('\n').where(validLine));
    stack.clear();
    variables.clear();
    _addLine(0);
    execute();
    }

    void execute() {
    for (;;) {
    if (line.isEmpty) {
    if (stack.length == 1) {
    final (_, next) = stack.removeLast();
    if (next == lines.length) break;
    _addLine(next);
    } else {
    stack.removeLast();
    }
    continue;
    }
    if (line.matches(';')) continue;
    switch (line.next()) {
    case 'A':
    output(false);
    final name = line.match(_variables);
    if (name == null) break;
    final input = stdin.readLineSync();
    if (input == null) return;
    variables[name] = num.parse(input);
    case 'D':
    _addLine(_index(_target()));
    case 'G':
    _setLine(_index(_target()));
    case 'I':
    final value = expr();
    final targets = [_target()];
    while (line.matches(',')) {
    targets.add(_target());
    }
    final index = value.compareTo(0) + 1;
    if (index < targets.length) _setLine(_index(targets[index]));
    case 'Q':
    return;
    case 'S':
    final name = line.match(_variables) ?? (throw 'missing variable');
    line.match('=') ?? (throw 'missing =');
    variables[name] = expr();
    case 'T':
    output(true);
    default:
    throw 'syntax error: $line';
    }
    }
    }

    class Input {
    Input(this.string);
    final String string;
    var index = 0;

    bool get isEmpty => index == string.length;
    String get peek => isEmpty ? '' : string[index];
    String next() => isEmpty ? '' : string[index++];

    String? match(Pattern pattern) {
    while (peek == ' ' || peek == '\t') {
    next();
    }
    final match = pattern.matchAsPrefix(string, index);
    if (match != null) {
    index = match.end;
    return match[0];
    }
    return null;
    }

    bool matches(Pattern pattern) => match(pattern) != null;

    @override
    String toString() => '${string.substring(0, index)} →${string.substring(index)}';
    }

    num expr() {
    for (var value = _term(); ;) {
    if (line.matches('+')) {
    value += _term();
    } else if (line.matches('-')) {
    value -= _term();
    } else {
    return value;
    }
    }
    }

    num _term() {
    for (var value = _factor(); ;) {
    if (line.matches('*')) {
    value *= _factor();
    } else if (line.matches('/')) {
    value /= _factor();
    } else {
    return value;
    }
    }
    }

    num _factor() {
    if (line.matches('-')) {
    return -_factor();
    } else if (line.matches('(')) {
    final result = expr();
    line.match(')') ?? (throw 'missing )');
    return result;
    } else if (line.match(_digits) case final number?) {
    return num.tryParse(number) ?? (throw 'invalid number: $number');
    } else if (line.match(_variables) case final name?) {
    return variables[name] ?? (throw 'unset variable: $name');
    } else if (line.matches('FITR')) {
    return expr().toInt();
    } else if (line.matches('FRAN()')) {
    return Random().nextDouble();
    }
    throw 'syntax error: $line';
    }

    void output(bool withExpr) {
    while (!line.isEmpty && !line.matches(';')) {
    if (line.matches('!')) {
    stdout.write('\n');
    } else if (line.matches(',')) {
    } else if (line.matches('"')) {
    for (;;) {
    if (line.isEmpty) return;
    final ch = line.next();
    if (ch == '"') break;
    stdout.write(ch);
    }
    } else if (line.matches('%')) {
    expr(); // padding value (ignored)
    } else if (withExpr) {
    stdout.write('${expr()}'.padLeft(5));
    } else {
    break;
    }
    }
    }

    void _addLine(int index) {
    stack.add((Input(lines[index].substring(6)), index + 1));
    }

    void _setLine(int index) {
    stack.removeLast();
    _addLine(index);
    }

    String _target() {
    var t = line.match(RegExp(r'\d\d?(\.\d\d?)?')) ?? (throw 'missing jump target');
    final parts = t.split('.');
    if (parts.length == 1) return parts.single.padLeft(2, '0');
    return '${parts[0].padLeft(2, '0')}.${parts[1].padRight(2, '0')}';
    }

    int _index(String target) {
    for (var i = 0; i < lines.length; i++) {
    if (lines[i].startsWith(target)) return i;
    }
    throw 'no such line: $target';
    }

    final _digits = RegExp(r'\d+');
    final _variables = RegExp(r'[A-EG-Z]');
    54 changes: 54 additions & 0 deletions 2hamurabi.dart
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,54 @@
    import 'package:focal/focal.dart';

    const hamurabi = '''
    01.10 S P=95;S S=2800;S H=3000;S E=200;S Y=3;S A=1000;S I=5;S Q=1
    02.10 S D=0
    02.20 D 6;T !!!"LAST YEAR"!D," STARVED,
    02.25 T !I," ARRIVED,";S P=P+I;I (-Q)2.3
    02.27 S P=FITR(P/2);T !"**PLAGUE**"!
    02.30 T !"POPULATION IS"P,!!"THE CITY OWNS
    02.35 T A," ACRES."!!;I (H-1)2.5;T "WE HARVESTED
    02.40 D 3.2
    02.50 T !" RATS ATE "E," BUSHELS, YOU NOW HAVE
    02.60 T !S," BUSHELS IN STORE."!
    03.10 D 6; D 8;S Y=C+17;T "LAND IS TRADING AT
    03.20 T Y," BUSHELS PER ACRE;";S C=1
    03.30 D 4.3;A " BUY?"!Q;I (Q)7.2,3.8
    03.40 I (Y*Q-S)3.9,3.6;D 4.6;G 3.3
    03.50 D 4.5;G 3.3
    03.60 D 3.9:G 4.8
    03.70 S A=A+Q;S S=Y*Q;S C=0
    03.80 A !"TO SELL?"!Q;I (Q)7.2,3.9;S Q=-Q;I (A+Q)3.5
    03.90 S A=A+Q;S S=S-Y*Q;S C=0
    04.10 T !"BUSHELS TO USE
    04.11 A " AS FOOD?"!Q;I (Q)7.7;I (Q-S)4.2,4.7;D 4.6;G 4.1
    04.20 S S=S-Q;S C=1
    04.30 A !"HOW MANY ACRES OF LAND DO YOU WISH TO
    04.35 A !"PLANT WITH SEED? "D
    04.40 I (D)7.2;I (A-D)4.45;I (FITR(D/2)-S-1)4.65;D 4.6;G 4.3
    04.45 D 4.5;G 4.3
    04.50 D 7;T A," ACRES."!
    04.60 D 7;D 2.6
    04.65 I (D-10*P-1)5.1;D 7;T P," PEOPLE."!;G 4.3
    04.70 D 4.2
    04.80 D 6;T "YOU HAVE NO GRAIN LEFT AS SEED !!"!;S D=0
    05.10 S S=S-FITR(D/2);D 8;S Y=C;S H=D*Y
    05.20 D 8;S E=0;I (FITR(C/2)-C/2)5.3;S E=FITR(S/C)
    05.30 S S=S-E+H;D 8;S I=FITR(C*(20*A+S)/P/100+1);S C=FITR(Q/20)
    05.40 S Q=FITR(10*FRAN());I (P-C)2.1;S D=P-C;S P=C;G 2.2
    06.10 T !!"HAMURABI: "%5
    07.10 I (C)7.2;S C=C-1;D 6;T "BUT YOU HAVE ONLY"
    07.20 D 6;T !"GOODBYE!"!!;Q
    08.10 S C=FITR(5*FRAN())+1
    ''';

    void main() {
    run(hamurabi);
    }