git @ Cat's Eye Technologies Whothm / 771c8d8
Merge branch 'implement-in-lua' into develop-2023 Chris Pressey 1 year, 11 months ago
6 changed file(s) with 836 addition(s) and 0 deletion(s). Raw diff Collapse all Expand all
0 r := (0, 0, 1, 2);
1 s := (0, 0, 1, 2);
2 XOR := TF/FT;
3
4 begin
5 r.x += r.w;
6 r.x += -1;
7 r.w += 1;
8 r.h += 1;
9 draw r, XOR;
10 s.x += s.w;
11 s.x += -1;
12 s.w += 1;
13 s.h += 2;
14 draw s, XOR;
15 end
0 `whothm.lua`
1 ============
2
3 This directory contains an implementation of Whothm in Lua 5.3,
4 called `whothm.lua`. Its source resides in the `src` subdirectory.
5
6 It also contains a demonstration of running `whothm.lua` in a
7 web browser, under [Fengari][], in the `demo` subdirectory.
8
9 In order to for this demonstration to work locally, you'll need
10 to run a local webserver from the *root* directory of this
11 repository. For example, if you have Python 3 installed,
12
13 python3 -m http.server
14
15 Then open
16
17 http://127.0.0.1:8000/impl/whothm.lua/demo/whothm.html
18
19 in your web browser. If you don't have Python, other options
20 (and more information on running web installations locally)
21 can be found here: [how to run things locally][].
22
23 Additionally, in the `bin` directory in the root of this repo,
24 there is a driver script called `whothm` whose purpose is to
25 properly run `whothm.lua` regardless of the current directory.
26 In other words, you can just put that bin dir on your `PATH`
27 and type `whothm` to run it.
28
29 [Fengari]: https://fengari.io/
30 [how to run things locally]: https://github.com/mrdoob/three.js/wiki/How-to-run-things-locally#run-local-server
0 /*
1 * fengari-web.js and whothm.lua must be loaded before this source.
2 * After loading this source, call launch() to create and start the interpreter.
3 */
4
5 function launch(config) {
6 config.container.innerHTML = `
7 <canvas id="canvas" style="border: 1px solid black; float: right" width=400 height=400></canvas>
8 <textarea id="editor" rows="10" cols="80"></textarea>
9 <div id="control-panel"></div>
10 <button onclick="run()">Run</button>
11 <pre id="output"></pre>
12 `;
13
14 function makeSelect(container, labelText, optionsArray, fun) {
15 var label = document.createElement('label');
16 label.innerHTML = labelText;
17 container.appendChild(label);
18 var select = document.createElement("select");
19 for (var i = 0; i < optionsArray.length; i++) {
20 var op = document.createElement("option");
21 op.text = optionsArray[i].filename;
22 op.value = optionsArray[i].contents;
23 select.options.add(op);
24 }
25 select.onchange = function(e) {
26 fun(optionsArray[select.selectedIndex]);
27 };
28 select.selectedIndex = 0;
29 label.appendChild(select);
30 return select;
31 };
32
33 function selectOptionByText(selectElem, text) {
34 var optElem;
35 for (var i = 0; optElem = selectElem.options[i]; i++) {
36 if (optElem.text === text) {
37 selectElem.selectedIndex = i;
38 selectElem.dispatchEvent(new Event('change'));
39 return;
40 }
41 }
42 }
43
44 var controlPanel = document.getElementById('control-panel');
45 examplePrograms = [{
46 filename: "hello-world.whothm",
47 contents: `r := (0, 0, 1, 2);
48 s := (0, 0, 1, 2);
49 XOR := TF/FT;
50
51 begin
52 r.x += r.w;
53 r.x += -1;
54 r.w += 1;
55 r.h += 1;
56 draw r, XOR;
57 s.x += s.w;
58 s.x += -1;
59 s.w += 1;
60 s.h += 2;
61 draw s, XOR;
62 end`
63 }
64 ];
65 var select = makeSelect(controlPanel, "example program:", examplePrograms, function(option) {
66 document.getElementById('editor').value = option.contents;
67 });
68 selectOptionByText(select, "hello-world.whothm");
69 }
70
71 function setLuaGlobal(name, value) {
72 fengari.interop.push(fengari.L, value);
73 fengari.lua.lua_setglobal(fengari.L, name);
74 }
75
76 function run() {
77 // set up debug function
78 setLuaGlobal("debug", function() {
79 var s = fengari.interop.tojs(fengari.L, 2);
80 console.log(s);
81 });
82
83 // set up print function
84 var outputElem = document.getElementById("output");
85 outputElem.innerHTML = '';
86 setLuaGlobal("print", function() {
87 var s = fengari.interop.tojs(fengari.L, 2);
88 outputElem.innerHTML += s + "\n";
89 });
90
91 // set up canvas drawing
92 var canvas = document.getElementById("canvas");
93 var ctx = canvas.getContext('2d');
94 var cellWidth = 5;
95 var cellHeight = 5;
96 setLuaGlobal("plot_on_canvas", function() {
97 var x = fengari.interop.tojs(fengari.L, 2);
98 var y = fengari.interop.tojs(fengari.L, 3);
99 var c = fengari.interop.tojs(fengari.L, 4);
100 ctx.fillStyle = c ? "black" : "white";
101 ctx.fillRect(x * cellWidth, y * cellHeight, cellWidth, cellHeight);
102 /*
103 ctx.strokeStyle = c ? "black" : "white";
104 ctx.strokeRect(x * cellWidth, y * cellHeight, cellWidth, cellHeight);
105 */
106 });
107
108 // set whothm program
109 var progText = document.getElementById("editor").value;
110 setLuaGlobal("whothm_prog", progText);
111
112 // run whothm program
113 fengari.load(`
114 local parser = Parser.new(whothm_prog)
115 local machine = parser.parse()
116 local bitmap = BitMap.new(128, 128)
117 debug("Running machine")
118 machine.run(bitmap)
119 debug("Plotting on canvas")
120 bitmap.foreach(plot_on_canvas)
121 debug("Done")
122 `)();
123 }
0 <!DOCTYPE html>
1 <head>
2 <meta charset="utf-8">
3 <title>whothm.lua (running under Fengari)</title>
4 <style>
5 #installation-container { }
6 </style>
7 </head>
8 <body>
9
10 <h1>Whothm</h1>
11
12 <p>(whothm.lua running under Fengari)</p>
13
14 <div id="installation-container">
15 <div id="installation">
16 </div>
17 </div>
18
19 <script src="https://catseye.tc/contrib/fengari-web-v0.1.4/fengari-web.js"></script>
20 <!-- script src="../../../eg/examplePrograms.jsonp.js"></script> -->
21 <script src="whothm-fengari-launcher.js"></script>
22 <script type="application/lua" src="../src/whothm.lua" async></script>
23 <script>
24 launch({ container: document.getElementById('installation') });
25 </script>
26
27 </body>
0 -- Usage:
1 -- LUA_PATH="?.lua" lua test.lua
2
3 table = require "table"
4
5 r = require "whothm"
6
7 local t = TruthTable.new()
8 print(t.to_s())
9 t.map_to_true("FF")
10 print(t.to_s())
11 print(t.apply(false, false))
12 print(t.apply(false, true))
13
14 local r = Rectangle.new(5, 1, 6, 10)
15 print(r.to_s())
16
17 local b = BitMap.new(40, 20)
18 local t = TruthTable.new()
19 t.map_to_true("FT")
20 t.map_to_true("TF")
21 r.draw(b, t)
22 print(b.to_s())
23
24 local source = [[
25 r := (0, 0, 1, 2);
26 s := (0, 0, 1, -987654321);
27 XOR := TF/FT;
28
29 begin
30 r.x += r.w;
31 r.x += -1;
32 r.w += 1;
33 r.h += 1;
34 draw r, XOR;
35 s.x += s.w;
36 s.x += -1;
37 s.w += 1;
38 s.h += 2;
39 draw s, XOR;
40 end
41 ]]
42 local s = Scanner.new(source)
43 local scanned = {}
44 token = s.scan()
45 while token ~= "EOF" do
46 table.insert(scanned, token)
47 token = s.scan()
48 end
49 print(table.concat(scanned, "|"))
50
51
52 local source = [[
53 r := (0, 0, 1, 2);
54 s := (0, 0, 1, 1);
55 XOR := TF/FT;
56 begin
57 r.x += r.w;
58 r.x += -1;
59 r.w += 1;
60 r.h += 1;
61 draw r, XOR;
62 s.x += s.w;
63 s.x += -1;
64 s.w += 1;
65 s.h += 2;
66 draw s, XOR;
67 end
68 ]]
69 local p = Parser.new(source)
70 local machine = p.parse()
71 p.dump_state()
72 print(machine.to_s())
0 --
1 -- whothm.lua
2 --
3
4 table = require "table"
5
6
7 --[[ ========== UTILS ========= ]]--
8
9 function table_size(t)
10 local count = 0
11 for _ in pairs(t) do count = count + 1 end
12 return count
13 end
14
15 render_table = function(t)
16 local s = "{"
17 for key,value in pairs(t) do
18 if type(value) == "table" then
19 s = s .. key .. ": " .. render_table(value) .. ","
20 else
21 s = s .. key .. ": " .. tostring(value) .. ","
22 end
23 end
24 return s .. "}"
25 end
26
27
28 --[[ ========== DEBUG ========= ]]--
29
30 local do_debug = false
31
32 debug = function(s)
33 if do_debug then
34 print("--> (" .. s .. ")")
35 end
36 end
37
38
39 --[[ ========== MODEL ========= ]]--
40
41 TruthTable = {}
42 TruthTable.new = function()
43 local tt = {}
44 local methods = {}
45
46 methods.map_to_true = function(truth_pair)
47 tt[truth_pair] = true
48 end
49
50 methods.apply = function(a, b)
51 local t1
52 if a then t1 = "T" else t1 = "F" end
53 local t2
54 if b then t2 = "T" else t2 = "F" end
55 return tt[t1 .. t2] or false
56 end
57
58 methods.to_s = function()
59 local text = "TruthTable(["
60 local i = 0
61 local size = table_size(tt)
62 for key,value in pairs(tt) do
63 text = text .. key
64 i = i + 1
65 if i < size then
66 text = text .. ", "
67 end
68 end
69 return text .. "])"
70 end
71
72 return methods
73 end
74
75
76 Rectangle = {}
77 Rectangle.new = function(x, y, w, h)
78 local methods = {}
79
80 methods.get_property = function(name)
81 local data = {
82 ["x"] = x,
83 ["y"] = y,
84 ["w"] = w,
85 ["h"] = h
86 }
87 return data[name]
88 end
89
90 methods.change_property = function(name, delta)
91 if name == "x" then x = x + delta
92 elseif name == "y" then y = y + delta
93 elseif name == "w" then w = w + delta
94 elseif name == "h" then h = h + delta
95 else
96 error("Cannot modify property '" .. name .. "' of rectangle")
97 end
98 end
99
100 methods.draw = function(bitmap, tt)
101 --debug("drawing: " .. methods.to_s() .. " with " .. tt.to_s())
102
103 local b_w = bitmap.get_width()
104 local b_h = bitmap.get_height()
105
106 if x > b_w and x+w > b_w and y > b_h and y+h > b_h then
107 return
108 end
109 local right = x + w
110 if right > b_w then right = b_w end
111 local bottom = y + h
112 if bottom > b_h then bottom = b_h end
113
114 local px, py
115 for py = y,bottom-1 do
116 for px = x,right-1 do
117 bitmap.modify_pixel(px, py, tt)
118 end
119 end
120
121 --debug("bitmap now:\n" .. bitmap.to_s())
122 end
123
124 methods.to_s = function()
125 return "Rectangle(" .. x .. "," .. y .. "," .. w .. "," .. h .. ")"
126 end
127
128 return methods
129 end
130
131
132 BitMap = {}
133 BitMap.new = function(width, height)
134 local methods = {}
135 local data
136
137 methods.clear = function()
138 local i
139 data = {}
140 for i=1, width * height do
141 data[i] = false
142 end
143 end
144
145 methods.get_height = function() return height end
146 methods.get_width = function() return width end
147
148 -- x, y here are zero-based
149 methods.get_pixel = function(x, y)
150 local pos = y * width + x + 1
151 return data[pos]
152 end
153
154 -- x, y here are zero-based
155 methods.modify_pixel = function(x, y, tt)
156 if x >= 0 and x < width and y >= 0 and y < height then
157 local pos = y * width + x + 1
158 data[pos] = tt.apply(data[pos], true)
159 end
160 end
161
162 methods.foreach = function(callback)
163 local px, py
164 for py = 0,height - 1 do
165 for px = 0,width - 1 do
166 callback(px, py, methods.get_pixel(px, py))
167 end
168 end
169 end
170
171 methods.to_s = function()
172 local buffer = {}
173 local px, py
174 local c
175 for py = 0,height - 1 do
176 for px = 0,width - 1 do
177 if methods.get_pixel(px, py) then
178 table.insert(buffer, "*")
179 else
180 table.insert(buffer, " ")
181 end
182 end
183 table.insert(buffer, "\n")
184 end
185 return table.concat(buffer)
186 end
187
188 -- init
189 methods.clear()
190
191 return methods
192 end
193
194
195 --[[ ========== SCANNER ========== ]]--
196
197 function isdigit(s)
198 return string.find("0123456789", s, 1, true) ~= nil
199 end
200
201 function islower(s)
202 return string.find("abcdefghijklmnopqrstuvwxyz", s, 1, true) ~= nil
203 end
204
205 function isupper(s)
206 return string.find("ABCDEFGHIJKLMNOPQRSTUVWXYZ", s, 1, true) ~= nil
207 end
208
209 function isalpha(s)
210 return islower(s) or isupper(s)
211 end
212
213 function isalnum(s)
214 return isalpha(s) or isdigit(s)
215 end
216
217 function issep(s)
218 return string.find("(),.;=", s, 1, true) ~= nil
219 end
220
221 function isspace(s)
222 return string.find(" \t\n\r", s, 1, true) ~= nil
223 end
224
225 function iseol(s)
226 return string.find("\n\r", s, 1, true) ~= nil
227 end
228
229 Scanner = {}
230 Scanner.new = function(s)
231 local string = s
232 local _text = nil
233 local _type = nil
234
235 local methods = {}
236
237 methods.text = function() return _text end
238 methods.type = function() return _type end
239
240 methods.is_eof = function()
241 return _type == "EOF"
242 end
243
244 methods.set_token = function(text, type)
245 _text = text
246 _type = type
247 debug("set_token " .. text .. " (" .. type .. ")")
248 end
249
250 methods.scan = function()
251 methods.scan_impl()
252 debug("scanned '" .. _text .. "' (" .. _type .. ")")
253 return _text
254 end
255
256 methods.scan_impl = function()
257 -- discard leading whitespace
258 while isspace(string:sub(1,1)) and string ~= "" do
259 string = string:sub(2)
260 -- TODO: count pos and line
261 end
262
263 -- check for end of input
264 if string == "" then
265 methods.set_token("EOF", "EOF")
266 return
267 end
268
269 -- else check for identifiers
270 if isalpha(string:sub(1,1)) then
271 local len = 0
272 while isalpha(string:sub(1+len,1+len)) and len <= string:len() do
273 len = len + 1
274 end
275 local word = string:sub(1, 1+len-1)
276 string = string:sub(1+len)
277 methods.set_token(word, "ident")
278 return
279 end
280
281 -- else check for literal decimal number
282 if string:sub(1,1) == "-" or isdigit(string:sub(1,1)) then
283 local len = 0
284 if string:sub(1,1) == "-" then
285 len = len + 1
286 end
287 while isdigit(string:sub(1+len,1+len)) and len <= string:len() do
288 len = len + 1
289 end
290 methods.set_token(string:sub(1, len), "numlit")
291 string = string:sub(len + 1)
292 return
293 end
294
295 -- else check for certain two-character tokens
296 local c = string:sub(1,2)
297 if c == "+=" or c == ":=" then
298 string = string:sub(3)
299 methods.set_token(c, "operator")
300 return
301 end
302
303 -- anything else => one character token
304 local c = string:sub(1,1)
305 string = string:sub(2)
306 methods.set_token(c, "operator")
307 end
308
309 methods.consume = function(s)
310 if _text == s then
311 methods.scan()
312 return true
313 else
314 return false
315 end
316 end
317
318 methods.consume_type = function(t)
319 if _type == t then
320 methods.scan()
321 return true
322 else
323 return false
324 end
325 end
326
327 methods.expect = function(s)
328 if _text == s then
329 methods.scan()
330 else
331 error(
332 "expected '" .. s ..
333 "', found '" .. _text .. "'"
334 )
335 end
336 end
337
338 debug("created scanner with string '" .. string .. "'")
339
340 return methods
341 end
342
343
344 Machine = {}
345 Machine.new = function()
346 local methods = {}
347 local bitmap = nil
348 local commands = {}
349
350 methods.add_draw_command = function(rect, tt)
351 local command = {}
352 command.type = "draw"
353 command.rect = rect
354 command.tt = tt
355 table.insert(commands, command)
356 end
357
358 methods.add_delta_command = function(rect, property, delta)
359 local command = {}
360 command.type = "delta"
361 command.rect = rect
362 command.property = property
363 command.delta = delta
364 table.insert(commands, command)
365 end
366
367 methods.add_delta_indirect_command = function(dest_rect, dest_property, src_rect, src_property)
368 local command = {}
369 command.type = "delta_indirect"
370 command.dest_rect = dest_rect
371 command.dest_property = dest_property
372 command.src_rect = src_rect
373 command.src_property = src_property
374 table.insert(commands, command)
375 end
376
377 methods.execute = function(command)
378 --debug("executing command: " .. render_table(command))
379 if command.type == "draw" then
380 --print("DrawCommand(r=" .. command.rect.to_s() .. ",tt=" .. command.tt.to_s() .. ")")
381 command.rect.draw(bitmap, command.tt)
382 elseif command.type == "delta" then
383 --print("DeltaCommand(r=" .. command.rect.to_s() .. ",m=" .. command.property .. ",d=" .. command.delta .. ")")
384 command.rect.change_property(command.property, command.delta)
385 elseif command.type == "delta_indirect" then
386 --print(
387 -- "DeltaIndirectCommand(srcR=" .. command.src_rect.to_s() .. ",srcM=" .. command.src_property ..
388 -- ",destR=" .. command.dest_rect.to_s() .. ",destM=" .. command.dest_property .. ")"
389 --)
390 local delta = command.src_rect.get_property(command.src_property)
391 command.dest_rect.change_property(command.dest_property, delta)
392 else
393 error("Malformed command: " .. render_table(command))
394 end
395 end
396
397 methods.run = function(given_bitmap)
398 local i, j, command
399 bitmap = given_bitmap
400 for i = 0,100 do
401 --print(i)
402 for j,command in ipairs(commands) do
403 methods.execute(command)
404 end
405 end
406 end
407
408 methods.to_s = function()
409 return "Machine(" .. render_table(commands) .. ")"
410 end
411
412 return methods
413 end
414
415
416 Parser = {}
417 Parser.new = function(source)
418 local methods = {}
419 local line = 1
420 local pos = 0
421 local token
422 local rect_map = {}
423 local tt_map = {}
424 local m -- machine
425 local scanner = Scanner.new(source)
426
427 local token_is = function(s) return s == scanner.text() end
428
429 methods.parse = function()
430 m = Machine.new()
431 while not token_is("begin") do
432 methods.parse_decl()
433 scanner.expect(";")
434 end
435 scanner.expect("begin")
436 while not token_is("end") do
437 methods.parse_command()
438 scanner.expect(";")
439 end
440 scanner.expect("end")
441 return m
442 end
443
444 methods.parse_decl = function()
445 local name = scanner.text()
446 scanner.scan()
447 scanner.expect(":=")
448 if scanner.consume("(") then
449 -- it's a rectangle
450 local x = tonumber(scanner.text())
451 scanner.scan()
452 scanner.expect(",")
453 local y = tonumber(scanner.text())
454 scanner.scan()
455 scanner.expect(",")
456 local w = tonumber(scanner.text())
457 scanner.scan()
458 scanner.expect(",")
459 local h = tonumber(scanner.text())
460 scanner.scan()
461 scanner.expect(")")
462 rect_map[name] = Rectangle.new(x, y, w, h)
463 else
464 -- it's a truthtable
465 local tt = TruthTable.new()
466 local truth_pair = scanner.text()
467 scanner.scan()
468 tt.map_to_true(truth_pair)
469 while scanner.consume("/") do
470 truth_pair = scanner.text()
471 scanner.scan()
472 tt.map_to_true(truth_pair)
473 end
474 tt_map[name] = tt
475 end
476 end
477
478 methods.parse_command = function()
479 if scanner.consume("draw") then
480 -- it's a draw command
481 local rect = methods.parse_rect()
482 scanner.expect(",")
483 local tt_name = scanner.text()
484 scanner.scan()
485 local tt = tt_map[tt_name]
486 if tt == nil then
487 -- throw new ParseException(line, "Undefined truth table '" + ttName + "'");
488 print("Undefined truth table") -- FIXME accumulate error
489 else
490 m.add_draw_command(rect, tt)
491 end
492 else
493 -- it's a delta command
494 local rect = methods.parse_rect()
495 scanner.expect(".")
496 local property = scanner.text()
497 scanner.scan()
498 scanner.expect("+=")
499 if scanner.type() == "numlit" then
500 local value = tonumber(scanner.text())
501 scanner.scan()
502 m.add_delta_command(rect, property, value)
503 else
504 local src_rect = methods.parse_rect()
505 scanner.expect(".")
506 local src_property = scanner.text()
507 scanner.scan()
508 m.add_delta_indirect_command(rect, property, src_rect, src_property)
509 end
510 end
511 end
512
513 methods.parse_rect = function()
514 local name = scanner.text()
515 scanner.scan()
516 local rect = rect_map[name]
517 if rect == nil then
518 -- throw new ParseException(line, "Undefined rectangle '" + rectName + "'");
519 print("Undefined rectangle")
520 end
521 return rect
522 end
523
524 methods.dump_state = function()
525 for key,value in pairs(rect_map) do
526 print(key, value.to_s())
527 end
528 for key,value in pairs(tt_map) do
529 print(key, value.to_s())
530 end
531 end
532
533 -- init
534 scanner.scan()
535
536 return methods
537 end
538
539
540 --[[ ================== MAIN =============== ]]--
541
542 function main(arg)
543 while #arg > 0 do
544 if arg[1] == "--debug" then
545 do_debug = true
546 else
547 local f = assert(io.open(arg[1], "r"))
548 local whothm_prog = f:read("*all")
549 f:close()
550
551 local parser = Parser.new(whothm_prog)
552 local machine = parser.parse()
553 local bitmap = BitMap.new(80, 30)
554 machine.run(bitmap)
555 print(bitmap.to_s())
556 end
557 table.remove(arg, 1)
558 end
559 end
560
561 if arg ~= nil then
562 main(arg)
563 end