1 * Snobol4th - a Forth written in Snobol4
2 * Dave Gauer 2024 - ratfactor.com
3
4 * Data Stack
5
6 * Data stack and data head index
7 data = array(10,null)
8 * Note that "data head" always points to the
9 * top value and 0 is blank.
10 dh = 0
11
12 * Compile Mode Flag
13 compile_mode = null
14 compiling_name = ''
15
16 * Input state
17 dict_pos = 0
18 stdin_pos = 0
19 stdin_line = ''
20 readfrom = 'stdin_line'
21 storepos = 'stdin_pos'
22
23 * Infinite loop prevention while experimenting:
24 * max_tokens = 4000
25 * token_count = 0
26
27
28 * Dictionary
29 * *************************************
30 * dtab entries point to positions in
31 * the dict string (dictionary storage)
32 dict = ''
33 dtab = table()
34
35 * (Test for dictionary entries with
36 * differ(dtab['no-exist']) :f(nodict)
37 * |n|_| - native
38 * |_|i| - immediate
39 * |_|_|55 - 'compiled' word position in dict
40 * |_|_|dup - native function to call
41 dtab['.'] = 'n_' 'dot'
42 dtab['."'] = 'ni' 'dotquote'
43 dtab['.S'] = 'n_' 'dotstack'
44 dtab['"'] = 'n_' 'quote'
45 dtab['SPACE'] = 'n_' 'space'
46 dtab['CR'] = 'n_' 'cr'
47 dtab['DUP'] = 'n_' 'dup'
48 dtab['SWAP'] = 'n_' 'swap'
49 dtab['DROP'] = 'n_' 'drop'
50 dtab['+'] = 'n_' 'plus'
51 dtab['-'] = 'n_' 'minus'
52 dtab['1-'] = 'n_' 'oneminus'
53 dtab[':'] = 'n_' 'colon'
54 dtab[';'] = 'ni' 'semicolon'
55 dtab['EXIT'] = 'n_' 'exit'
56 dtab['HERE'] = 'n_' 'here'
57 dtab['('] = 'ni' 'comment'
58 dtab['?DUP'] = 'n_' 'ifdup'
59 dtab['IF'] = 'n_' 'if'
60 dtab['ELSE'] = 'n_' 'else'
61 dtab['THEN'] = 'n_' 'then'
62 dtab['DO'] = 'n_' 'do'
63 dtab['I'] = 'n_' 'i'
64 dtab['LOOP'] = 'n_' 'loop'
65 dtab['+LOOP'] = 'n_' 'plusloop'
66 dtab['-LOOP'] = 'n_' 'minusloop'
67
68
69 * Output line
70 * *************************************
71 * Since SNOBOL4 wants to print whole lines, let's
72 * collect printed strings and wait for "CR"
73 stdout = ''
74
75 * Return stack
76 * *************************************
77 * rs stores return positions in the
78 * dictionary string.
79 *
80 * dict: ddddddddddddddd
81 * ^ ^
82 * stdin: ss|s|ssssssssss
83 * ^ | |
84 * rs = 0 1 2 (rh=3)
85 *
86 * If rh is 0, read from stdin
87 rs = array(10,null)
88 rh = 0
89
90
91 * DUP duplicates head of stack
92 define('dup()') :(enddup)
93 dup
94 x = pop()
95 push(x)
96 push(x)
97 :(return)
98 enddup
99
100 * SWAP duplicates head of stack
101 define('swap()') :(endswap)
102 swap
103 x = pop()
104 y = pop()
105 push(x)
106 push(y)
107 :(return)
108 endswap
109
110 * DROP drops the head item of stack
111 define('drop()') :(enddrop)
112 drop
113 pop()
114 :(return)
115 enddrop
116
117 * '.' (dot) pops head of stack and prints it
118 define('dot()') :(enddot)
119 dot
120 x = pop()
121 stdout = stdout x ' '
122 :(return)
123 enddot
124
125 * CR prints whatever we've collected in stdout
126 * as a new line. (Snobol4 has no way to print
127 * without printing a whole line.)
128 define('cr()') :(endcr)
129 cr
130 output = stdout
131 stdout = ''
132 :(return)
133 endcr
134
135 * SPACE prints a space character
136 define('space()') :(endspace)
137 space
138 stdout = stdout ' '
139 :(return)
140 endspace
141
142 * '+' (plus) pops 2 digits, adds them, pushes result
143 define('plus()') :(endplus)
144 plus
145 x = pop()
146 y = pop()
147 push(x + y)
148 :(return)
149 endplus
150
151 * '-' (minus) pops 2 digits, - them, pushes result
152 define('minus()') :(endminus)
153 minus
154 x = pop()
155 y = pop()
156 push(y - x)
157 :(return)
158 endminus
159
160 * '1-' (oneminus) decrements top of stack by one
161 define('oneminus()') :(endoneminus)
162 oneminus
163 x = pop()
164 push(x - 1)
165 :(return)
166 endoneminus
167
168 * ":" (colon) starts compile mode, gathers name
169 define('colon()') :(endcolon)
170 colon
171 compiling_name = get_token()
172 dtab[compiling_name] = '__' size(dict)
173 compile_mode = 1
174 :(return)
175 endcolon
176
177 * ";" (semicolon) starts compile mode, gathers name
178 define('semicolon()') :(endsemicolon)
179 semicolon
180 dict = dict ' EXIT'
181 compile_mode = null
182 :(return)
183 endsemicolon
184
185 * EXIT starts compile mode, gathers name
186 define('exit()') :(endexit)
187 exit
188 rh = rh - 1
189 lt(rh,1) :s(return)
190 dict_pos = rs[rh]
191 :(return)
192 endexit
193
194 * HERE pushes current input pos
195 define('here()') :(endhere)
196 here
197 push(pos)
198 :(return)
199 endhere
200
201 in_comment = 0
202 * '(' is a comment...it throws away input until ')'
203 commentpat = tab(*stdin_pos) break(')') @stdin_pos
204 define('comment()') :(endcomment)
205 comment
206 stdin_line commentpat :f(cget_more)
207 stdin_pos = stdin_pos + 1
208 :(return)
209 cget_more
210 stdin_pos = 0
211 stdin_line = input :f(theend) s(comment)
212 endcomment
213
214 * '.S' prints the parameter stack
215 define('dotstack()') :(enddotstack)
216 dotstack
217 lt(dh,1) :s(printstackempty)
218 i = 0
219 next_si
220 lt(i,dh) :f(return)
221 i = i + 1
222 stdout = stdout data[i] ' '
223 :(next_si)
224 printstackempty
225 stdout = stdout '<Stack empty>'
226 :(return)
227 enddotstack
228
229 * '."' declares and prints a string!
230 * quotepat = tab(*$storepos) (break('"') @$storepos) . quotestr
231 quotepat = tab(*$storepos) (break('"') @epos) . quotestr
232 define('dotquote()') :(enddotquote)
233 dotquote
234 * output = $readfrom
235 * output = dupl(' ',epos) '^'
236 $readfrom quotepat :f(endquote_notfound)
237 $storepos = epos + 1
238 * output = '"' quotestr '"'
239 * output = 'storing pos ' storepos '=' $storepos
240 eq(1, compile_mode) :s(dotquote_compile)
241 stdout = stdout quotestr
242 :(return)
243 dotquote_compile
244 dict = dict ' ." ' quotestr '"'
245 :(return)
246 endquote_notfound
247 output = "ERROR: No matching end quote."
248 output = $readfrom
249 output = dupl(' ',$storepos) '^'
250 :(end)
251 enddotquote
252
253 * '?DUP' duplicates the top of stack if it's a 'truthy' value
254 define('ifdup()') :(endifdup)
255 ifdup
256 x = data[dh]
257 eq(x,0) :s(return)
258 dup()
259 :(return)
260 endifdup
261
262 * 'IF' skips instructions until THEN when top of stack is false
263 elsethenpat = tab(*dict_pos) arb ('ELSE' | 'THEN') @dict_pos
264 define('if()') :(endif)
265 if
266 * If return stack empty, we are not in a
267 * dictionary definition.
268 lt(rh,1) :s(no_stdin)
269 * If the top of stack value is true, then
270 * start executing the next instruction.
271 eq(0,pop()) :s(if_false)
272 :(return)
273 if_false
274 * If it's false, scan to an ELSE or THEN.
275 dict elsethenpat
276 dictpos = dictpos + 1
277 :(return)
278 no_stdin output = "ERROR: Cannot execute 'IF' outside of a word definition." :(theend)
279 endif
280
281 * 'ELSE' skips instructions until THEN
282 elsepat = tab(*dict_pos) break('THEN') @dict_pos
283 define('else()') :(endelse)
284 else
285 dict elsethenpat
286 dictpos = dictpos + 1
287 :(return)
288 endelse
289
290 * 'THEN' itself is a non-action in this Forth!
291 define('then()') :(endthen)
292 then
293 :(return)
294 endthen
295
296 * DO LOOP, I, and DO +LOOP
297 * This Forth doesn't do nested loops because
298 * it has just one variable for the "I" index.
299 * If desired, this could be fixed with a
300 * stack (traditionally the return stack).
301
302 * 'DO'
303 define('do()') :(enddo)
304 do
305 * If return stack empty, we are not in a
306 * dictionary definition.
307 lt(rh,1) :s(no_stdin2)
308 loop_startpos = dict_pos
309 loop_ivar = pop()
310 loop_endvar = pop()
311 :(return)
312 no_stdin2 output = "ERROR: Cannot execute 'DO...LOOP' outside of a word definition." :(theend)
313 enddo
314
315 * 'i'
316 define('i()') :(endi)
317 i
318 push(loop_ivar)
319 :(return)
320 endi
321
322 * 'loop'
323 define('loop()') :(endloop)
324 loop
325 loop_ivar = loop_ivar + 1
326 le(loop_endvar, loop_ivar) :s(return)
327 dict_pos = loop_startpos
328 :(return)
329 endloop
330
331 * 'plusloop'
332 define('plusloop()') :(endplusloop)
333 plusloop
334 loop_inc = pop()
335 loop_ivar = loop_ivar + loop_inc
336 le(loop_endvar, loop_ivar) :s(return)
337 dict_pos = loop_startpos
338 :(return)
339 endplusloop
340
341 * 'minusloop'
342 define('minusloop()') :(endminusloop)
343 minusloop
344 loop_dec = pop()
345 loop_ivar = loop_ivar - loop_dec
346 ge(loop_endvar, loop_ivar) :s(return)
347 dict_pos = loop_startpos
348 :(return)
349 endminusloop
350
351
352 * Internal functions
353 * *************************************
354
355 * pop() pops head of stack
356 define('pop()') :(endpop)
357 pop
358 lt(0,dh) :f(popunderflow)
359 pop = data[dh]
360 dh = dh - 1
361 :(return)
362 popunderflow output = 'Stack underflow.' :(theend)
363 endpop
364
365 * push(6) pushes 6 to head of stack
366 define('push(v)') :(endpush)
367 push
368 * output = "push starting with dh=" dh
369 dh = dh + 1
370 * output = 'pushing data[' dh ']:' v
371 data[dh] = v
372 :(return)
373 endpush
374
375
376
377 * Patterns for getting tokens
378 * Note the *p delays the evaluation of cursor position p until pattern used
379 anyspace = (span(' ') | '')
380 wordpat = tab(*pos) anyspace (break(' ') | rem) . token anyspace @pos
381 * get_token() returns next token from input
382 define('get_token()') :(endget_token)
383 get_token
384 * If return stack not empty, we are
385 * reading from the dictionary.
386 lt(rh,1) :s(read_stdin)
387 readfrom = 'dict'
388 storepos = 'dict_pos'
389 pos = dict_pos
390 :(read2)
391 read_stdin
392 readfrom = 'stdin_line'
393 storepos = 'stdin_pos'
394 pos = stdin_pos
395 lt(stdin_pos, size(stdin_line)) :s(read2)
396 * If the pos is not less than input size, read more
397 * And if reading from input fails, we're done!
398 &trim = 1
399 stdin_line = input :f(theend)
400 stdin_pos = 0
401 pos = 0
402 read2
403 $readfrom wordpat :f(fail)
404 $storepos = pos
405 get_token = token
406
407 * TEMP debug output:
408 * $readfrom tab(pos) rem . restofit
409 * output = "*** Read token '" token "', now pos(" storepos ")=" pos "(" restofit ")"
410
411 :(return)
412 endget_token
413
414
415 * Main - read tokens of input
416 * *************************************
417
418 next_token
419 * Infinite loop prevention while experimenting:
420 * token_count = token_count + 1
421 * lt(token_count, max_tokens) :f(theend)
422
423 token = get_token()
424
425 * Test if token is in dictionary
426 token_data = dtab[token]
427 differ(token_data) :f(nodict)
428
429 * It is, are we compiling? If not, execute it.
430 differ(compile_mode) :f(exec_word)
431
432 * Yes, we're compiling. Is token an immediate word?
433 token_data pos(1) 'i' :s(exec_word)
434
435 * Compiling and not immediate, 'compile' to word
436 dict = dict ' ' token
437 :(next_token)
438
439 exec_word
440 * Is it a native word or dictionary word?
441 token_data pos(0) 'n' :s(exec_native)
442
443 * Is a dictionary word, execute it...
444 token_data tab(2) rem . dict_pos
445 rs[rh] = pos
446 rh = rh + 1
447
448 pos = dict_pos
449 :(next_token)
450
451 exec_native
452 token_data tab(2) rem . native_fn
453 apply(native_fn, null) :(next_token)
454
455 nodict
456 * Test if token is an integer (or empty)
457 differ(token) :f(next_token)
458 integer(token) :f(bad_input)
459 * It is, are we compiling?
460 differ(compile_mode) :f(push_int)
461 * Compile int into dict word definition
462 dict = dict ' ' token
463 :(next_token)
464 push_int
465 push(token)
466 :(next_token)
467 fail output = "FAIL!" :(theend)
468 bad_input output = "ERROR: '" token "' not in dictionary." :(next_token)
469
470 theend
471 * Print any remaining buffered output
472 differ(stdout) :f(end)
473 output = stdout
474 end