Replace skip with if ... then, and give it words to test

skip took a count off the stack and jumped that many words forward, which made every branch a matter of counting the words on the other side of it - the factorial example needed a whole helper word, counter, to work out the number to skip. Take Forth's if ... then instead: if drops the top value and, when it is false, scans forward to the then that closes it, counting nested pairs on the way. The taken branch drops that then as well. Nothing is left for it to do, and leaving it in the word list means every frame of a recursion parks one in the program column - a factorial of 7 ended up dragging * then * then * then behind it. Reaching a then therefore means it never had an if, so it throws rather than passing silently, and an if without one throws the same way the false branch already did. Branching is worth little without something to branch on, so: = <> < > <= and >= go through the existing effect2, which already hands the deeper value in first and makes them read in the order they are written. and, or and invert follow, and true and false parse as literals next to numbers and strings, after the definitions lookup so they can be shadowed like any other word. Flags are plain booleans - if tests truthiness, so a number still works as one, which the countdown example leans on. The examples grew with the language: temperature conversion, a countdown, fibonacci and a quoting example join the factorial, and both recursive ones now ask dup 1 > instead of testing n - 1 against zero. That also stops 0 ! from recursing forever, since 0 - 1 is -1 and -1 was truthy. ,, is documented too - the quoting example was the first place it appears. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

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="&quot; 2 * &quot; ,,"/>
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`;