Replace skip with if ... then, and give it words to test
- author
- Marijn Besseling <njirambem@gmail.com> · 2026-09-12 19:37 UTC
- commit
- 7afeca15f1913039aec8eb6ab79c39c1c3766ac2
- parent
- a4d46838a4
- tree
- browse at this commit
2 files changed +177 -23
Blog/Components/Pages/Concat.razor +105 -12
| @@ -12,16 +12,46 @@ | ||
| 12 | 12 | <Panel Legend="Predefined operations"> |
| 13 | 13 | <StackOps/> |
| 14 | 14 | |
| 15 | - <StackOp Name="skip" | |
| 16 | - Description="Skips forward in the program by the amount on the top of the stack. If the value on the top of the stack is 0 or negative, the value is dropped and the program does not skip ahead."> | |
| 17 | - <StackStep Stack="[1, 2, 3]" Program="skip drop dup"/> | |
| 18 | - <StackStep Stack="[2, 3]" Program="dup"/> | |
| 19 | - <StackStep Stack="[2, 2, 3]"/> | |
| 15 | + <StackOp Name="if ... then" | |
| 16 | + Description="Drops the top value on the stack and tests it. If it is false, or 0, the program skips ahead past the matching 'then', otherwise the words in between are run. The 'then' itself is removed either way: it only marks the end of the branch, it never runs."> | |
| 17 | + <StackStep Stack="[1, 2, 3]" Program="if dup then drop"/> | |
| 18 | + <StackStep Stack="[2, 3]" Program="dup drop"/> | |
| 19 | + <StackStep Stack="[2, 2, 3]" Program="drop"/> | |
| 20 | + <StackStep Stack="[2, 3]"/> | |
| 20 | 21 | <p>Or</p> |
| 21 | - <StackStep Stack="[-1, 2, 3]" Program="skip drop dup"/> | |
| 22 | - <StackStep Stack="[2, 3]" Program="drop dup"/> | |
| 23 | - <StackStep Stack="[3]" Program="dup"/> | |
| 24 | - <StackStep Stack="[3, 3]"/> | |
| 22 | + <StackStep Stack="[0, 2, 3]" Program="if dup then drop"/> | |
| 23 | + <StackStep Stack="[2, 3]" Program="drop"/> | |
| 24 | + <StackStep Stack="[3]"/> | |
| 25 | + </StackOp> | |
| 26 | + | |
| 27 | + <StackOp Name="= <> < > <= >=" | |
| 28 | + Description="Compares the top two values and leaves a flag in their place, true or false, which 'if' can take straight off the stack. They read in the order they are written, so '2 3 <' asks whether 2 is below 3."> | |
| 29 | + <StackStep Stack="[]" Program="2 3 < if 10 then"/> | |
| 30 | + <StackStep Stack="[2]" Program="3 < if 10 then"/> | |
| 31 | + <StackStep Stack="[3, 2]" Program="< if 10 then"/> | |
| 32 | + <StackStep Stack="[true]" Program="if 10 then"/> | |
| 33 | + <StackStep Stack="[]" Program="10"/> | |
| 34 | + <StackStep Stack="[10] <=="/> | |
| 35 | + </StackOp> | |
| 36 | + | |
| 37 | + <StackOp Name="true false" Description="Puts a flag on the stack, the same one a comparison leaves behind."> | |
| 38 | + <StackStep Stack="[3]" Program="true"/> | |
| 39 | + <StackStep Stack="[true, 3]"/> | |
| 40 | + </StackOp> | |
| 41 | + | |
| 42 | + <StackOp Name="and" Description="True only if both flags on top of the stack are true."> | |
| 43 | + <StackStep Stack="[true, false]" Program="and"/> | |
| 44 | + <StackStep Stack="[false]"/> | |
| 45 | + </StackOp> | |
| 46 | + | |
| 47 | + <StackOp Name="or" Description="True if either of the two flags on top of the stack is true."> | |
| 48 | + <StackStep Stack="[true, false]" Program="or"/> | |
| 49 | + <StackStep Stack="[true]"/> | |
| 50 | + </StackOp> | |
| 51 | + | |
| 52 | + <StackOp Name="invert" Description="Turns the flag on top of the stack into its opposite."> | |
| 53 | + <StackStep Stack="[false]" Program="invert"/> | |
| 54 | + <StackStep Stack="[true]"/> | |
| 25 | 55 | </StackOp> |
| 26 | 56 | |
| 27 | 57 | <StackOp Name=": word ... ;" Description="Creates a new definition."> |
| @@ -36,20 +66,83 @@ | ||
| 36 | 66 | <StackStep Stack="[2, 3]" Program="*"/> |
| 37 | 67 | <StackStep Stack="[6] <=="/> |
| 38 | 68 | </StackOp> |
| 69 | + | |
| 70 | + <StackOp Name=",," | |
| 71 | + Description="Unquotes the string on top of the stack: its words are put back in front of the program and run as if they had been written there."> | |
| 72 | + <StackStep Stack="[5]" Program="" 2 * " ,,"/> | |
| 73 | + <StackStep Stack="[ 2 * , 5]" Program=",,"/> | |
| 74 | + <StackStep Stack="[5]" Program="2 *"/> | |
| 75 | + <StackStep Stack="[2, 5]" Program="*"/> | |
| 76 | + <StackStep Stack="[10] <=="/> | |
| 77 | + </StackOp> | |
| 39 | 78 | </Panel> |
| 40 | 79 | |
| 41 | 80 | <Panel Legend="Example programs"> |
| 42 | 81 | <details> |
| 82 | + <summary>Fahrenheit to Celsius</summary> | |
| 83 | + <NavLink href="/concat?in=FwAgZgfAxisMwCYQFoQFYQCoQE4QHoQBuAKAQEYlIog"> | |
| 84 | + Replace program | |
| 85 | + </NavLink> | |
| 86 | + <pre> | |
| 87 | + : f>c 32 - 5 * 9 / ; | |
| 88 | + 212 f>c | |
| 89 | + </pre> | |
| 90 | + </details> | |
| 91 | + | |
| 92 | + <details> | |
| 93 | + <summary>Countdown</summary> | |
| 94 | + <NavLink | |
| 95 | + href="/concat?in=FwAgxg9grgdgLgEwgdxidCoAcQEsBmIAjCALTjTxKohwAWApmgNwBQArBbIijEA"> | |
| 96 | + Replace program | |
| 97 | + </NavLink> | |
| 98 | + <p>A word that calls itself is the only loop there is, and 'if' is what stops it.</p> | |
| 99 | + <pre> | |
| 100 | + : countdown dup if 1 - countdown then ; | |
| 101 | + 5 countdown | |
| 102 | + </pre> | |
| 103 | + </details> | |
| 104 | + | |
| 105 | + <details> | |
| 43 | 106 | <summary>Factorial</summary> |
| 44 | 107 | <NavLink |
| 45 | - href="/concat?in=FwAgxg9grgdgLgUwE4lQEygBxAJhAZwHcBDbAWhAG4AoUAQlRA2wEYQLJZEV8BrAS2wMAVFWoB2EHWpA"> | |
| 108 | + href="/concat?in=FwAghCkCYK4A4gIwgHwgJYDMSwcgtOCAFQgAuAFgKYB2IA3AFADs4QA"> | |
| 46 | 109 | Replace program |
| 47 | 110 | </NavLink> |
| 48 | 111 | <pre> |
| 49 | - : counter dup 2 swap - ; | |
| 50 | - : ! dup 1 - counter skip ! * ; | |
| 112 | + : ! dup 1 > if dup 1 - ! * then ; | |
| 51 | 113 | 7 ! |
| 52 | 114 | </pre> |
| 53 | 115 | </details> |
| 116 | + | |
| 117 | + <details> | |
| 118 | + <summary>Fibonacci</summary> | |
| 119 | + <NavLink | |
| 120 | + href="/concat?in=FwAgZglgRisCYFcAOICMIB8IJhIl6AtONCAM4DuAhigEwjGQwDUIALgBYCmAdiANwAoAGwkoQA"> | |
| 121 | + Replace program | |
| 122 | + </NavLink> | |
| 123 | + <p> | |
| 124 | + Every number in the sequence 0, 1, 1, 2, 3, 5, 8 is the sum of the two before it, so 'fib' calls | |
| 125 | + itself twice: once for each of them, with 'swap' digging the original number back out from under the | |
| 126 | + first result. | |
| 127 | + </p> | |
| 128 | + <pre> | |
| 129 | + : fib dup 1 > if dup 1 - fib swap 2 - fib + then ; | |
| 130 | + 6 fib | |
| 131 | + </pre> | |
| 132 | + </details> | |
| 133 | + | |
| 134 | + <details> | |
| 135 | + <summary>Quoting</summary> | |
| 136 | + <NavLink | |
| 137 | + href="/concat?in=FwAgJg9grgRgNgUxMgRCATCAVCNAaPEAbgChQBHKAQzACcoAHRZcaeJSWZ0gVhEpr0mCIA"> | |
| 138 | + Replace program | |
| 139 | + </NavLink> | |
| 140 | + <p>A string is data sitting on the stack until ',,' puts it back into the program, where it is code again.</p> | |
| 141 | + <pre> | |
| 142 | + : double " 2 * " ,, ; | |
| 143 | + : quadruple double double ; | |
| 144 | + 5 quadruple | |
| 145 | + </pre> | |
| 146 | + </details> | |
| 54 | 147 | </Panel> |
| 55 | 148 | </main> |
Blog/Components/Pages/Concat.razor.js +72 -11
| @@ -58,7 +58,7 @@ function evalString(inputString) { | ||
| 58 | 58 | |
| 59 | 59 | function effect2(f) { |
| 60 | 60 | return (stack) => { |
| 61 | - if (stack.length <= 1) throw "stack underflow, need 2 numbers"; | |
| 61 | + if (stack.length <= 1) throw "stack underflow, need 2 values"; | |
| 62 | 62 | let [x, y, ...rest] = stack; |
| 63 | 63 | return [f(y, x), ...rest]; |
| 64 | 64 | }; |
| @@ -69,6 +69,18 @@ const subtract = effect2((a, b) => a - b); | ||
| 69 | 69 | const multiply = effect2((a, b) => a * b); |
| 70 | 70 | const divide = effect2((a, b) => a / b); |
| 71 | 71 | |
| 72 | +const equal = effect2((a, b) => a === b); | |
| 73 | +const notEqual = effect2((a, b) => a !== b); | |
| 74 | +const lessThan = effect2((a, b) => a < b); | |
| 75 | +const greaterThan = effect2((a, b) => a > b); | |
| 76 | +const lessOrEqual = effect2((a, b) => a <= b); | |
| 77 | +const greaterOrEqual = effect2((a, b) => a >= b); | |
| 78 | + | |
| 79 | +/* Booleans in, booleans out: anything is a flag to 'if', but these words only | |
| 80 | + ever leave a real one. */ | |
| 81 | +const and = effect2((a, b) => Boolean(a) && Boolean(b)); | |
| 82 | +const or = effect2((a, b) => Boolean(a) || Boolean(b)); | |
| 83 | + | |
| 72 | 84 | async function evalWords(inputWords) { |
| 73 | 85 | let words = inputWords; |
| 74 | 86 | let stack = []; |
| @@ -96,14 +108,34 @@ function evalWord(word, stack, rest) { | ||
| 96 | 108 | return [multiply(stack), rest]; |
| 97 | 109 | case "/": |
| 98 | 110 | return [divide(stack), rest]; |
| 111 | + case "=": | |
| 112 | + return [equal(stack), rest]; | |
| 113 | + case "<>": | |
| 114 | + return [notEqual(stack), rest]; | |
| 115 | + case "<": | |
| 116 | + return [lessThan(stack), rest]; | |
| 117 | + case ">": | |
| 118 | + return [greaterThan(stack), rest]; | |
| 119 | + case "<=": | |
| 120 | + return [lessOrEqual(stack), rest]; | |
| 121 | + case ">=": | |
| 122 | + return [greaterOrEqual(stack), rest]; | |
| 123 | + case "and": | |
| 124 | + return [and(stack), rest]; | |
| 125 | + case "or": | |
| 126 | + return [or(stack), rest]; | |
| 127 | + case "invert": | |
| 128 | + return [invert(stack), rest]; | |
| 99 | 129 | case "dup": |
| 100 | 130 | return [dup(stack), rest]; |
| 101 | 131 | case "drop": |
| 102 | 132 | return [drop(stack), rest]; |
| 103 | 133 | case "swap": |
| 104 | 134 | return [swap(stack), rest]; |
| 105 | - case "skip": | |
| 106 | - return skip(stack, rest); | |
| 135 | + case "if": | |
| 136 | + return branch(stack, rest); | |
| 137 | + case "then": | |
| 138 | + throw "'then' without a matching 'if'"; | |
| 107 | 139 | case ",,": |
| 108 | 140 | return unquote(stack, rest); |
| 109 | 141 | case ":": |
| @@ -122,17 +154,36 @@ function unquote(stack, rest) { | ||
| 122 | 154 | } |
| 123 | 155 | } |
| 124 | 156 | |
| 125 | -function skip(stack, words) { | |
| 126 | - if (stack.length === 0) throw "stack underflow, dont know how much to skip"; | |
| 127 | - let [amount, ...restStack] = stack; | |
| 157 | +function branch(stack, words) { | |
| 158 | + if (stack.length === 0) throw "stack underflow, nothing to test"; | |
| 159 | + let [condition, ...restStack] = stack; | |
| 160 | + let index = findThen(words); | |
| 161 | + | |
| 162 | + // The branch is taken: run the words in between, but drop the 'then' that | |
| 163 | + // closes them, it has nothing left to do but clutter the log. | |
| 164 | + if (condition) { | |
| 165 | + let restWords = [...words.slice(0, index), ...words.slice(index + 1)]; | |
| 166 | + return [restStack, restWords]; | |
| 167 | + } | |
| 128 | 168 | |
| 129 | - if (amount > words.length) | |
| 130 | - throw `program underflow, cant skip ${amount} words`; | |
| 131 | - if (amount <= 0) return [stack.slice(1), words]; // no skipping on <= 0 | |
| 169 | + return [restStack, words.slice(index + 1)]; | |
| 170 | +} | |
| 132 | 171 | |
| 133 | - let restWords = words.slice(amount); | |
| 172 | +/** The index of the 'then' closing an 'if', skipping over any nested ones. */ | |
| 173 | +function findThen(words) { | |
| 174 | + let depth = 1; | |
| 175 | + | |
| 176 | + for (let index = 0; index < words.length; index++) { | |
| 177 | + const word = words[index]; | |
| 178 | + if (word === "if") { | |
| 179 | + depth++; | |
| 180 | + } else if (word === "then") { | |
| 181 | + depth--; | |
| 182 | + if (depth === 0) return index; | |
| 183 | + } | |
| 184 | + } | |
| 134 | 185 | |
| 135 | - return [restStack, restWords]; | |
| 186 | + throw "expected 'then', found end of program"; | |
| 136 | 187 | } |
| 137 | 188 | |
| 138 | 189 | function define(words) { |
| @@ -174,6 +225,12 @@ function swap(stack) { | ||
| 174 | 225 | return [y, x, ...rest]; |
| 175 | 226 | } |
| 176 | 227 | |
| 228 | +function invert(stack) { | |
| 229 | + if (stack.length === 0) throw "stack underflow, nothing to invert"; | |
| 230 | + let [x, ...rest] = stack; | |
| 231 | + return [!x, ...rest]; | |
| 232 | +} | |
| 233 | + | |
| 177 | 234 | function parse(word, stack, rest) { |
| 178 | 235 | if (word.startsWith('"')) { |
| 179 | 236 | return parseString(word, stack, rest); |
| @@ -183,6 +240,10 @@ function parse(word, stack, rest) { | ||
| 183 | 240 | return [stack, [...definitions[word], ...rest]]; |
| 184 | 241 | } |
| 185 | 242 | |
| 243 | + if (word === "true" || word === "false") { | |
| 244 | + return [[word === "true", ...stack], rest]; | |
| 245 | + } | |
| 246 | + | |
| 186 | 247 | let num = Number(word); |
| 187 | 248 | if (isNaN(num)) { |
| 188 | 249 | throw `word '${word}' not recognised`; |