Download forth/Forth/Parse.lean from Snapkitty/ai-free: direct link, hf CLI and curl.
- Browser
- Download file 19.1 kB
-
https://huggingface.co/Snapkitty/ai-free/resolve/main/forth/Forth/Parse.lean
- Command line
-
hf download hf://Snapkitty/ai-free/forth/Forth/Parse.lean
-
curl -L -o Parse.lean https://huggingface.co/Snapkitty/ai-free/resolve/main/forth/Forth/Parse.lean
19.1 kB
| import Forth.Semantics | |
| /-! | |
| # Forth.Parse | |
| Text frontend: Forth source β `Program` (dictionary + main block + variable count). | |
| Lexing (`tokenize`): | |
| * tokens are separated by whitespace (space, tab, newline, carriage return); | |
| * `\` as a token starts a comment that runs to the end of the line; | |
| * `(` as a token starts a comment that ends with the first token ending in `)` | |
| (an unterminated `(` comment is an error). | |
| Parsing (`parse`), case-insensitive: | |
| * integer literals, optionally negative (`-12`), taken modulo `2^n` by `Op.lit`; | |
| * `+ - * / MOD AND OR XOR INVERT LSHIFT RSHIFT = <> < > U< U> 0= DUP DROP SWAP OVER ROT @ !`; | |
| * `>R R> R@ I J UNLOOP` (return-data stack words; `I`, `J` and `UNLOOP` are ordinary words, also | |
| accepted outside a `DO β¦ LOOP`, where they act on whatever the return-data stack holds, see | |
| `Forth.Semantics`); | |
| * `IF β¦ ELSE β¦ THEN`, `IF β¦ THEN`, `BEGIN β¦ UNTIL`, `BEGIN β¦ WHILE β¦ REPEAT`, `DO β¦ LOOP`, | |
| `DO β¦ +LOOP`, and `?DO` in place of `DO`. `?DO` is not a separate construct: `?DO body LOOP rest` parses as | |
| `OVER OVER = IF DROP DROP ELSE DO body LOOP THEN rest` (`Block.loopOf`), which skips the loop | |
| when limit and index are equal, as ANS `?DO` does; | |
| * `?DUP`, parsed as `DUP IF DUP THEN` (`Block.qdup`); | |
| * `CASE tβ OF bβ ENDOF β¦ tβ OF bβ ENDOF default ENDCASE`, parsed into nested `IF`s | |
| (`Block.ofClause`): an `OF` compares its test value with the selector, and the clauses after | |
| an `ENDOF` must reach `ENDCASE`; | |
| * `LEAVE`, which must occur inside a `DO` loop of the same definition or of the main block | |
| (otherwise `LEAVE outside DO β¦ LOOP`, checked on the parsed program, `Program.leaveOK`); | |
| * `: name β¦ ;` colon definitions at top level. A name is visible from the start of its own | |
| body (so a word may call itself recursively) and in everything after it; a later definition | |
| of the same name shadows the earlier one. Word `k` (in definition order) is `defs[k]`. | |
| * `RECURSE` (inside a definition) calls the word being defined; `EXIT` (inside a definition) | |
| leaves it. Both are errors outside a definition. | |
| * `VARIABLE name` (top level) allocates the next memory cell: the `k`-th variable (from 0) is | |
| cell `k`, and `name` pushes `k`. The program records how many cells it uses (`vars`). | |
| * `<number> CONSTANT name` (top level): the number literal immediately before `CONSTANT` is | |
| removed from the top-level code and `name` pushes it. (Standard Forth takes the value from | |
| the stack at run time; here it must be a literal, resolved at parse time.) | |
| * Data space, at top level, handled at parse time like `VARIABLE`: `CREATE name` makes `name` | |
| push the next free cell without allocating it; `n ALLOT` (`n` a literal just before it, like | |
| `CONSTANT`'s) allocates `n` cells (none if `n` is negative); `x ,` (`x` a literal) allocates the | |
| next cell and stores `x` in it when the main block reaches that point. | |
| * Top-level code outside definitions forms the main block, in source order. | |
| * Anything else is an error (`unknown word`), as are unbalanced control words, nested | |
| definitions, `VARIABLE`/`CONSTANT`/`CREATE`/`ALLOT`/`,` inside a definition and a missing `;`. A `DO` without | |
| its `LOOP` or `+LOOP` is an error, and so is a `LOOP` or `+LOOP` without a `DO`. | |
| Name lookup order: dictionary, then built-in words, then numbers. | |
| Everything is structurally recursive on `List Char` / fuel, so concrete parses are checked by | |
| `decide`/`rfl`. `Forth.Print` proves `parse (print P) = .ok P` for well-formed programs; | |
| `Forth.ParseProps` proves the converse direction (`parse_wf`: every successful parse is | |
| well formed) and the claims above about case, comments, `CONSTANT` and `RECURSE`. | |
| -/ | |
| namespace WordDialect | |
| namespace Forth | |
| /-- Concatenate two blocks: run `a`, then `b`. -/ | |
| def Block.append : Block β Block β Block | |
| | .nil, b => b | |
| | .op o r, b => .op o (r.append b) | |
| | .ite t e r, b => .ite t e (r.append b) | |
| | .untilL x r, b => .untilL x (r.append b) | |
| | .call i r, b => .call i (r.append b) | |
| | .exit r, b => .exit (r.append b) | |
| | .doLoop x r, b => .doLoop x (r.append b) | |
| | .plusLoop x r, b => .plusLoop x (r.append b) | |
| | .leave r, b => .leave (r.append b) | |
| | .whileL c x r, b => .whileL c x (r.append b) | |
| /-- Split off a trailing literal at the top level of a block (the token just before | |
| `CONSTANT`). -/ | |
| def Block.dropLastLit : Block β Option (Block Γ Int) | |
| | .nil => none | |
| | .op (.lit z) .nil => some (.nil, z) | |
| | .op o r => r.dropLastLit.map fun p => (.op o p.1, p.2) | |
| | .ite t e r => r.dropLastLit.map fun p => (.ite t e p.1, p.2) | |
| | .untilL x r => r.dropLastLit.map fun p => (.untilL x p.1, p.2) | |
| | .call i r => r.dropLastLit.map fun p => (.call i p.1, p.2) | |
| | .exit r => r.dropLastLit.map fun p => (.exit p.1, p.2) | |
| | .doLoop x r => r.dropLastLit.map fun p => (.doLoop x p.1, p.2) | |
| | .plusLoop x r => r.dropLastLit.map fun p => (.plusLoop x p.1, p.2) | |
| | .leave r => r.dropLastLit.map fun p => (.leave p.1, p.2) | |
| | .whileL c x r => r.dropLastLit.map fun p => (.whileL c x p.1, p.2) | |
| /-- `?DO`: run the loop `loop` unless limit and index are equal (then drop both); then `rest`. -/ | |
| def Block.qdo (loop rest : Block) : Block := | |
| .op .over (.op .over (.op .eq (.ite (.op .drop (.op .drop .nil)) loop rest))) | |
| /-- The loop `mk body` (`DO`, `q = false`) or its `?DO` form (`q = true`), followed by `rest`. -/ | |
| def Block.loopOf (q : Bool) (mk : Block β Block β Block) (body rest : Block) : Block := | |
| match q with | |
| | false => mk body rest | |
| | true => Block.qdo (mk body .nil) rest | |
| /-- `?DUP`: `DUP IF DUP THEN`, duplicating the top only when it is nonzero. -/ | |
| def Block.qdup (rest : Block) : Block := .op .dup (.ite (.op .dup .nil) .nil rest) | |
| /-- One `CASE` clause `test OF body ENDOF more`, after its test: if the test value equals the | |
| selector below it, drop the selector, run `body`, and push a placeholder (which `ENDCASE` | |
| drops, as it drops the selector on the path where no clause matched); otherwise go on with the | |
| remaining clauses `more`. -/ | |
| def Block.ofClause (body more : Block) : Block := | |
| .op .over (.op .eq (.ite (.op .drop (body.append (.op (.lit 0) .nil))) more .nil)) | |
| /-- Every `LEAVE` lies inside a loop body (`inLoop`: inside one already). -/ | |
| def Block.leaveOK : Bool β Block β Bool | |
| | _, .nil => true | |
| | l, .op _ r => r.leaveOK l | |
| | l, .ite t e r => t.leaveOK l && e.leaveOK l && r.leaveOK l | |
| | l, .untilL x r => x.leaveOK l && r.leaveOK l | |
| | l, .call _ r => r.leaveOK l | |
| | l, .exit r => r.leaveOK l | |
| | l, .doLoop x r => x.leaveOK true && r.leaveOK l | |
| | l, .plusLoop x r => x.leaveOK true && r.leaveOK l | |
| | l, .leave r => l && r.leaveOK l | |
| | l, .whileL c x r => c.leaveOK l && x.leaveOK l && r.leaveOK l | |
| /-- Every `LEAVE` of the program lies inside a loop of its own block. -/ | |
| def Program.leaveOK (P : Program) : Bool := | |
| P.defs.all (Block.leaveOK false) && P.main.leaveOK false | |
| namespace Parse | |
| abbrev Tok := List Char | |
| def isWs (c : Char) : Bool := c == ' ' || c == '\t' || c == '\n' || c == '\r' | |
| /-- Emit the pending token `cur` (kept reversed) in front of `rest`. -/ | |
| def flush (cur : List Char) (rest : List Tok) : List Tok := | |
| if cur = [] then rest else cur.reverse :: rest | |
| /-- Split into tokens. `cur` is the current token reversed; `skip` means "inside a `\` | |
| comment". -/ | |
| def lex : List Char β List Char β Bool β List Tok | |
| | [], cur, _ => if cur = ['\\'] then [] else flush cur [] | |
| | c :: cs, _, true => if c == '\n' then lex cs [] false else lex cs [] true | |
| | c :: cs, cur, false => | |
| if isWs c then | |
| if cur = ['\\'] then (if c == '\n' then lex cs [] false else lex cs [] true) | |
| else flush cur (lex cs [] false) | |
| else lex cs (c :: cur) false | |
| /-- Remove `( β¦ )` comments. `inside` means "inside a `(` comment". -/ | |
| def stripParens : Bool β List Tok β Except String (List Tok) | |
| | false, [] => .ok [] | |
| | true, [] => .error "unterminated ( comment" | |
| | false, t :: ts => | |
| if t = ['('] then stripParens true ts else (t :: Β·) <$> stripParens false ts | |
| | true, t :: ts => if t.getLast? = some ')' then stripParens false ts else stripParens true ts | |
| def tokenize (src : String) : Except String (List Tok) := | |
| stripParens false (lex src.toList [] false) | |
| def upper (t : Tok) : Tok := t.map Char.toUpper | |
| def opTable : List (Tok Γ Op) := | |
| [ (['+'], .add), (['-'], .sub), (['*'], .mul), (['/'], .div), (['M', 'O', 'D'], .mod), | |
| (['A', 'N', 'D'], .and), (['O', 'R'], .or), (['X', 'O', 'R'], .xor), | |
| (['I', 'N', 'V', 'E', 'R', 'T'], .invert), | |
| (['L', 'S', 'H', 'I', 'F', 'T'], .lshift), (['R', 'S', 'H', 'I', 'F', 'T'], .rshift), | |
| (['='], .eq), (['<', '>'], .ne), (['<'], .lt), (['>'], .gt), | |
| (['U', '<'], .ult), (['U', '>'], .ugt), (['0', '='], .zeq), | |
| (['D', 'U', 'P'], .dup), (['D', 'R', 'O', 'P'], .drop), (['S', 'W', 'A', 'P'], .swap), | |
| (['O', 'V', 'E', 'R'], .over), (['R', 'O', 'T'], .rot), | |
| (['@'], .fetch), (['!'], .store), | |
| (['>', 'R'], .tor), (['R', '>'], .fromr), (['R', '@'], .rfetch), | |
| (['I'], .loopI), (['J'], .loopJ), (['U', 'N', 'L', 'O', 'O', 'P'], .unloop), | |
| (['2', 'D', 'U', 'P'], .twoDup), (['2', 'D', 'R', 'O', 'P'], .twoDrop), | |
| (['2', 'S', 'W', 'A', 'P'], .twoSwap), (['/', 'M', 'O', 'D'], .divMod), | |
| (['N', 'E', 'G', 'A', 'T', 'E'], .negate), (['A', 'B', 'S'], .abs), | |
| (['M', 'I', 'N'], .min), (['M', 'A', 'X'], .max), | |
| (['U', 'M', '*'], .umStar), (['U', 'M', '/', 'M', 'O', 'D'], .umDivMod), | |
| (['M', '*'], .mStar), (['S', 'M', '/', 'R', 'E', 'M'], .smRem), | |
| (['*', '/', 'M', 'O', 'D'], .starSlashMod), (['*', '/'], .starSlash), | |
| (['F', 'M', '/', 'M', 'O', 'D'], .fmMod) ] | |
| def digitsAux : Nat β List Char β Option Nat | |
| | acc, [] => some acc | |
| | acc, c :: cs => if c.isDigit then digitsAux (10 * acc + (c.toNat - '0'.toNat)) cs else none | |
| /-- A nonempty string of decimal digits. -/ | |
| def digits : List Char β Option Nat | |
| | [] => none | |
| | cs => digitsAux 0 cs | |
| def number : Tok β Option Int | |
| | '-' :: ds => (digits ds).map fun k => -(k : Int) | |
| | ds => (digits ds).map Int.ofNat | |
| def kIF : Tok := ['I', 'F'] | |
| def kELSE : Tok := ['E', 'L', 'S', 'E'] | |
| def kTHEN : Tok := ['T', 'H', 'E', 'N'] | |
| def kBEGIN : Tok := ['B', 'E', 'G', 'I', 'N'] | |
| def kUNTIL : Tok := ['U', 'N', 'T', 'I', 'L'] | |
| def kCOLON : Tok := [':'] | |
| def kSEMI : Tok := [';'] | |
| def kVARIABLE : Tok := ['V', 'A', 'R', 'I', 'A', 'B', 'L', 'E'] | |
| def kCONSTANT : Tok := ['C', 'O', 'N', 'S', 'T', 'A', 'N', 'T'] | |
| def kRECURSE : Tok := ['R', 'E', 'C', 'U', 'R', 'S', 'E'] | |
| def kEXIT : Tok := ['E', 'X', 'I', 'T'] | |
| def kDO : Tok := ['D', 'O'] | |
| def kLOOP : Tok := ['L', 'O', 'O', 'P'] | |
| def kPLOOP : Tok := ['+', 'L', 'O', 'O', 'P'] | |
| def kQDO : Tok := ['?', 'D', 'O'] | |
| def kLEAVE : Tok := ['L', 'E', 'A', 'V', 'E'] | |
| def kWHILE : Tok := ['W', 'H', 'I', 'L', 'E'] | |
| def kREPEAT : Tok := ['R', 'E', 'P', 'E', 'A', 'T'] | |
| def kCREATE : Tok := ['C', 'R', 'E', 'A', 'T', 'E'] | |
| def kALLOT : Tok := ['A', 'L', 'L', 'O', 'T'] | |
| def kCOMMA : Tok := [','] | |
| def kQDUP : Tok := ['?', 'D', 'U', 'P'] | |
| def kCASE : Tok := ['C', 'A', 'S', 'E'] | |
| def kOF : Tok := ['O', 'F'] | |
| def kENDOF : Tok := ['E', 'N', 'D', 'O', 'F'] | |
| def kENDCASE : Tok := ['E', 'N', 'D', 'C', 'A', 'S', 'E'] | |
| /-- Words that end the block being parsed. -/ | |
| def stops : List Tok := | |
| [kELSE, kTHEN, kUNTIL, kCOLON, kSEMI, kVARIABLE, kCONSTANT, kLOOP, kPLOOP, kWHILE, kREPEAT, | |
| kENDOF, kENDCASE, kCREATE, kALLOT, kCOMMA] | |
| /-- Dictionary: (upper-cased name, what the name compiles to), most recent first. A colon | |
| definition `k` compiles to `Block.call k`; a variable or constant to a literal. -/ | |
| abbrev Dict := List (Tok Γ (Block β Block)) | |
| def showTok (t : Tok) : String := String.ofList t | |
| /-- What a token means where a word is expected. -/ | |
| inductive Item where | |
| | stop (u : Tok) | |
| | ifK | |
| | beginK | |
| | doK (q : Bool) | |
| | caseK | |
| | ofK | |
| | prim (w : Block β Block) | |
| | bad (msg : String) | |
| /-- Look up a (non-keyword) token `t`, upper-cased `u`: dictionary, built-ins, numbers. -/ | |
| def lookupWord (dict : Dict) (t u : Tok) : Item := | |
| match dict.lookup u with | |
| | some w => .prim w | |
| | none => | |
| match opTable.lookup u with | |
| | some o => .prim (.op o) | |
| | none => | |
| match number u with | |
| | some z => .prim (.op (.lit z)) | |
| | none => .bad s!"unknown word: {showTok t}" | |
| /-- Classify a token; `self` is the index of the word being defined, if any. -/ | |
| def classify (dict : Dict) (self : Option Nat) (t : Tok) : Item := | |
| let u := upper t | |
| if u β stops then .stop u | |
| else if u = kIF then .ifK | |
| else if u = kBEGIN then .beginK | |
| else if u = kRECURSE then | |
| match self with | |
| | some i => .prim (.call i) | |
| | none => .bad "RECURSE outside a definition" | |
| else if u = kEXIT then | |
| match self with | |
| | some _ => .prim .exit | |
| | none => .bad "EXIT outside a definition" | |
| else if u = kDO then .doK false | |
| else if u = kQDO then .doK true | |
| else if u = kLEAVE then .prim .leave | |
| else if u = kQDUP then .prim Block.qdup | |
| else if u = kCASE then .caseK | |
| else if u = kOF then .ofK | |
| else lookupWord dict t u | |
| /-- The error for running out of fuel in `parseSeq`. Never produced for the fuel `parse` uses | |
| (`Forth.Grammar.parseSeq_ne_deep`). -/ | |
| def msgDeep : String := "input too deeply nested" | |
| /-- The error for running out of fuel in `parseTop`. Never produced by `parse` | |
| (`Forth.Grammar.parse_ne_fuel`). -/ | |
| def msgLong : String := "input too long" | |
| /-- Parse a block up to the end of input or a stop word. Returns the block, the stop word | |
| met (`none` at end of input) and the tokens after it. -/ | |
| def parseSeq : Nat β Dict β Option Nat β List Tok β Except String (Block Γ Option Tok Γ List Tok) | |
| | 0, _, _, _ => .error msgDeep | |
| | _ + 1, _, _, [] => .ok (.nil, none, []) | |
| | fuel + 1, dict, self, t :: ts => | |
| match classify dict self t with | |
| | .stop u => .ok (.nil, some u, ts) | |
| | .bad msg => .error msg | |
| | .prim w => do | |
| let (rest, stop, ts') β parseSeq fuel dict self ts | |
| .ok (w rest, stop, ts') | |
| | .ifK => do | |
| let (tb, stop1, ts1) β parseSeq fuel dict self ts | |
| if stop1 = some kELSE then | |
| let (eb, stop2, ts2) β parseSeq fuel dict self ts1 | |
| if stop2 = some kTHEN then | |
| let (rest, stop3, ts3) β parseSeq fuel dict self ts2 | |
| .ok (.ite tb eb rest, stop3, ts3) | |
| else .error "IF β¦ ELSE without THEN" | |
| else if stop1 = some kTHEN then | |
| let (rest, stop3, ts3) β parseSeq fuel dict self ts1 | |
| .ok (.ite tb .nil rest, stop3, ts3) | |
| else .error "IF without THEN" | |
| | .beginK => do | |
| let (body, stop1, ts1) β parseSeq fuel dict self ts | |
| if stop1 = some kUNTIL then | |
| let (rest, stop2, ts2) β parseSeq fuel dict self ts1 | |
| .ok (.untilL body rest, stop2, ts2) | |
| else if stop1 = some kWHILE then | |
| let (wb, stop2, ts2) β parseSeq fuel dict self ts1 | |
| if stop2 = some kREPEAT then | |
| let (rest, stop3, ts3) β parseSeq fuel dict self ts2 | |
| .ok (.whileL body wb rest, stop3, ts3) | |
| else .error "WHILE without REPEAT" | |
| else .error "BEGIN without UNTIL or WHILE" | |
| | .doK q => do | |
| let (body, stop1, ts1) β parseSeq fuel dict self ts | |
| if stop1 = some kLOOP then | |
| let (rest, stop2, ts2) β parseSeq fuel dict self ts1 | |
| .ok (Block.loopOf q .doLoop body rest, stop2, ts2) | |
| else if stop1 = some kPLOOP then | |
| let (rest, stop2, ts2) β parseSeq fuel dict self ts1 | |
| .ok (Block.loopOf q .plusLoop body rest, stop2, ts2) | |
| else .error "DO without LOOP or +LOOP" | |
| | .caseK => do | |
| let (body, stop1, ts1) β parseSeq fuel dict self ts | |
| if stop1 = some kENDCASE then | |
| let (rest, stop2, ts2) β parseSeq fuel dict self ts1 | |
| .ok (body.append (.op .drop rest), stop2, ts2) | |
| else .error "CASE without ENDCASE" | |
| | .ofK => do | |
| let (body, stop1, ts1) β parseSeq fuel dict self ts | |
| if stop1 = some kENDOF then | |
| let (more, stop2, ts2) β parseSeq fuel dict self ts1 | |
| if stop2 = some kENDCASE then .ok (Block.ofClause body more, stop2, ts2) | |
| else .error "OF β¦ ENDOF without ENDCASE" | |
| else .error "OF without ENDOF" | |
| /-- Parse top-level code, definitions, variables and constants. `vars` counts the variables | |
| so far; `main` accumulates the top-level code so far. -/ | |
| def parseTop : Nat β Dict β List Block β Nat β Block β List Tok β Except String Program | |
| | 0, _, _, _, _, _ => .error msgLong | |
| | fuel + 1, dict, defs, vars, main, toks => do | |
| let (b, stop, rest) β parseSeq (toks.length + 1) dict none toks | |
| match stop with | |
| | none => .ok β¨defs, main.append b, varsβ© | |
| | some u => | |
| if u = kCOLON then | |
| match rest with | |
| | [] => .error "missing name after :" | |
| | name :: body => | |
| let dict' := (upper name, Block.call defs.length) :: dict | |
| let (bb, stop2, rest2) β parseSeq (body.length + 1) dict' (some defs.length) body | |
| if stop2 = some kSEMI then parseTop fuel dict' (defs ++ [bb]) vars (main.append b) rest2 | |
| else if stop2 = some kCOLON then .error s!"nested definition inside {showTok name}" | |
| else if stop2 = some kVARIABLE β¨ stop2 = some kCONSTANT β¨ stop2 = some kCREATE β¨ | |
| stop2 = some kALLOT β¨ stop2 = some kCOMMA then | |
| .error s!"VARIABLE, CONSTANT, CREATE, ALLOT or , inside definition of {showTok name}" | |
| else .error s!"definition of {showTok name} has no ;" | |
| else if u = kVARIABLE then | |
| match rest with | |
| | [] => .error "missing name after VARIABLE" | |
| | name :: rest' => | |
| parseTop fuel ((upper name, Block.op (.lit vars)) :: dict) defs (vars + 1) | |
| (main.append b) rest' | |
| else if u = kCONSTANT then | |
| match b.dropLastLit, rest with | |
| | none, _ => .error "CONSTANT needs a number literal immediately before it" | |
| | some _, [] => .error "missing name after CONSTANT" | |
| | some (b', z), name :: rest' => | |
| parseTop fuel ((upper name, Block.op (.lit z)) :: dict) defs vars (main.append b') rest' | |
| else if u = kCREATE then | |
| match rest with | |
| | [] => .error "missing name after CREATE" | |
| | name :: rest' => | |
| parseTop fuel ((upper name, Block.op (.lit vars)) :: dict) defs vars (main.append b) rest' | |
| else if u = kALLOT then | |
| match b.dropLastLit with | |
| | none => .error "ALLOT needs a number literal immediately before it" | |
| | some (b', z) => parseTop fuel dict defs (vars + z.toNat) (main.append b') rest | |
| else if u = kCOMMA then | |
| match b.dropLastLit with | |
| | none => .error ", needs a number literal immediately before it" | |
| | some (b', z) => | |
| parseTop fuel dict defs (vars + 1) | |
| ((main.append b').append (.op (.lit z) (.op (.lit vars) (.op .store .nil)))) rest | |
| else .error s!"unexpected {showTok u}" | |
| def parseTokens (toks : List Tok) : Except String Program := do | |
| let P β parseTop (toks.length + 1) [] [] 0 .nil toks | |
| if P.leaveOK then .ok P else .error "LEAVE outside DO β¦ LOOP" | |
| end Parse | |
| /-- Parse Forth source text into a program. -/ | |
| def parse (src : String) : Except String Program := do | |
| Parse.parseTokens (β Parse.tokenize src) | |
| end Forth | |
| end WordDialect | |