Last active
September 1, 2025 09:35
-
-
Save sma/ff7dd4ae2a3b136a315c537df5646368 to your computer and use it in GitHub Desktop.
Revisions
-
sma revised this gist
Sep 1, 2025 . 4 changed files with 218 additions and 53 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal 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. ## 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";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 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 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 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 `%` 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, bool)>[]; Input get line => stack.last.$1; // final definition @@ -371,25 +371,21 @@ void run(String input) { execute(); } 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, 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) { 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. 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': final (target, g) = _target(); _addLine(_index(target), g); // ... rest of cases ... } } } (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'), 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) { } ``` 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().$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().$1]; while (line.matches(',')) { 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)); ``` This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -3,10 +3,18 @@ import 'dart:math'; final lines = <String>[]; 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) { 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; } @@ -42,19 +52,23 @@ void execute() { if (input == null) return; variables[name] = num.parse(input); case 'D': final (target, g) = _target(); _addLine(_index(target), g); case 'G': _setLine(_index(_target().$1)); case 'I': final value = expr(); final targets = [_target().$1]; while (line.matches(',')) { 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 -_power(); } else if (line.matches('(')) { final result = expr(); 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 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('%')) { 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)); } else { break; } } } 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, 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'), 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+(\.\d+)?|\.\d+'); final _variables = RegExp(r'[A-EG-Z]'); This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -1,4 +1,4 @@ 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";R 07.20 D 6;T !"GOODBYE!"!!;Q 08.10 S C=FITR(5*FRAN())+1 '''; void main() { focal.run(hamurabi); } This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal 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); } -
sma created this gist
Aug 28, 2025 .There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal 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. This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal 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]'); This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal 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); }