-
Notifications
You must be signed in to change notification settings - Fork 5
/
asm-blox.el
3724 lines (3436 loc) · 156 KB
/
asm-blox.el
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
;;; asm-blox.el --- Programming game involving WAT -*- lexical-binding: t -*-
;; Copyright © 2021-2022 Zachary Romero <[email protected]>
;; Author: Zachary Romero
;; Maintainer: Zachary Romero
;; Version: 0.0.1
;; Package-Requires: ((emacs "26.1") (yaml "0.5.1"))
;; Homepage: https://github.com/zkry/asm-blox
;; Keywords: games
;; This file is not part of GNU Emacs
;; This program is free software: you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with this program. If not, see <https://www.gnu.org/licenses/>.
;;; Commentary:
;; Asm-blox is a programming game where you are tasked with writing
;; code to complete puzzles. Code is written on a 4x3 grid of cells
;; where your room to write code and memory is limited. Complex
;; programs must be written by combining code cells, passing messages
;; between them. The command `asm-blox' will open the puzzle listing
;; buffer where you can select a puzzle to work on. For detains on
;; how to program the board please refer to the reference material
;; provided by this package.
;;; Code:
(require 'cl-lib)
(require 'seq)
(require 'yaml)
(defgroup asm-blox nil
"Programming game involving tiled WAT and YAML code cells."
:prefix "asm-blox-"
:group 'games)
(defcustom asm-blox-save-directory-name (expand-file-name ".asm-blox" user-emacs-directory)
"The directory in which all puzzles will be saved and searched for."
:group 'asm-blox
:type 'directory)
(defconst asm-blox-box-height 12
"Height of an individual code block.")
(defconst asm-blox-box-width 20
"Width of an individual code block.")
(defconst asm-blox--gameboard-col-ct 4
"Amount of columns on a Asm-blox gameboard.")
(defconst asm-blox--gameboard-row-ct 3
"Amount of rows on a Asm-blox gameboard.")
(defvar-local asm-blox-box-contents nil
"Hashtable containing the textual contents of the gameboard.
Keys are in the form '(ROW COL).")
(defvar asm-blox--gameboard nil
"Vector of combiled code-cells.")
(defvar-local asm-blox--extra-gameboard-cells nil
"List of sources (ie puzzle input) and sinks (ie win conditions) of a game.")
(defvar asm-blox--gameboard-state nil
"Contains the state of the board whether it be victory or error.")
(defvar asm-blox--parse-depth nil
"Dynamic variable used when parsing WAT to determine label references.")
(defvar asm-blox--branch-labels nil
"Dynamic variable to store where lables which labels refer to which depths.")
(defvar asm-blox-runtime-error nil
"If non-nil, contains the runtime error encountered.
The format of the error is (list message row column).")
(defvar-local asm-blox-execution-origin-buffer nil
"The buffer where the execution buffer was created from.")
(cl-defstruct (asm-blox-code-node
(:constructor asm-blox--code-node-create)
(:copier nil))
"Parsed sexp of code. Used to keep track of start and end position in text."
children start-pos end-pos)
(defun asm-blox--parse-error-p (err)
"Return non-nil if ERR is a parse error."
(and (listp err) (eql 'error (car err))))
(defun asm-blox--parse-cell (coords code)
"Parse a the CODE of a text box at COORDS, returning a cell runtime."
(let* ((first-char
(and (not (string-empty-p (string-trim code)))
(substring-no-properties (string-trim-left code) 0 1)))
;; There is currently no switch the user can use to indicate
;; filetype, thus the need of heuristic.
(sexp-p (eql (ignore-errors (car (read code))) 'module))
(wat-p (or (not first-char)
(string= first-char "(")
(string= first-char ")")
(string= first-char ";"))))
(cond
(sexp-p
(asm-blox--create-sexp-code-node (car coords) (cadr coords) code))
(wat-p
(let ((parse-tree (asm-blox--parse-assembly code)))
(if (asm-blox--parse-error-p parse-tree)
parse-tree
(let ((row (car coords))
(col (cadr coords))
(asm (asm-blox--parse-tree-to-asm parse-tree)))
(if (asm-blox--parse-error-p asm)
asm
(asm-blox--cell-runtime-create
:instructions asm
:pc 0
:stack '()
:row row
:col col))))))
(t
(asm-blox--create-yaml-code-node (car coords) (cadr coords) code)))))
(defun asm-blox--parse-assembly (code)
"Parse ASM CODE returning a list of instructions."
(with-temp-buffer
(erase-buffer)
(insert code)
(goto-char (point-min))
(cl-labels
((whitespace-p
(c)
(or (eql c ?\s)
(eql c ?\t)
(eql c ?\n)))
(current-char
()
(char-after (point)))
(consume-space
()
(while (and (whitespace-p (current-char))
(not (eobp)))
(forward-char)))
(symbol-char-p
(c)
(or (<= ?a c ?z)
(<= ?A c ?Z)
(= ?_ c)))
(parse-element
(&optional top-level)
(let ((elements '()))
(catch 'end
(while t
(consume-space)
(if (eobp)
(if top-level
(throw 'end nil)
(throw 'error `(error ,(point) "SYNTAX ERROR")))
(let ((at-char (current-char)))
(cond
;; Start of children list
((eql at-char ?\()
(let ((start (point)))
(forward-char 1)
(let* ((children (parse-element))
(node (asm-blox--code-node-create
:children children
:start-pos start
:end-pos (point))))
(push node elements))))
;; End of children list
((eql at-char ?\) )
(if top-level
(throw 'error `(error ,(point) "SYNTAX ERROR"))
(forward-char 1)
(throw 'end nil)))
((eql at-char ?\?)
(forward-char 1)
(let ((c))
(if (looking-at "\\\\")
(progn
(forward-char 1)
(let ((escape-c (char-after (point))))
(pcase escape-c
(?n (setq c ?\n))
(?s (setq c ?\s))
(?b (setq c ?\b))
(_ (throw 'error `(error ,(point) "BAD ESCAPE CODE"))))))
(setq c (char-after (point)))
(when (or (= c ?\s) (= c ?\n))
(throw 'error `(error ,(point) "INVALID CHAR"))))
(forward-char 1)
(push c elements)))
;; Symbol
((eql at-char ?\;)
(while (and (not (looking-at "\n"))
(not (eobp)))
(forward-char 1)))
((symbol-char-p at-char)
(let ((start (point)))
(forward-char 1)
(while (and (not (eobp))
(symbol-char-p (current-char)))
(forward-char 1))
(let ((symbol (intern (upcase
(buffer-substring-no-properties
start
(point))))))
(push symbol elements))))
;; digit
((or (cl-digit-char-p at-char) (eql at-char ?-))
(let ((start (point)))
(forward-char 1)
(while (and (not (eobp))
(cl-digit-char-p (current-char)))
(forward-char 1))
(let ((number (string-to-number
(buffer-substring-no-properties
start
(point)))))
(when (< number -999)
(throw 'error `(error ,(point) "TOO LOW NUMBER")))
(when (> number 999)
(throw 'error `(error ,(point) "TOO HIGH NUMBER")))
(push number elements))))
;; Unknown character
(t (throw 'error `(error ,(point) "unexpected character"))))))))
(reverse elements))))
(catch 'error (parse-element t)))))
(defconst asm-blox-base-operations
'(GET SET TEE CONST NULL IS_NULL DROP
NOP ADD INC DEC SUB MUL DIV REM AND OR EQZ GZ LZ
EQ NE LT GT GE LE SEND PUSH POP
CLR NOT DUP ABS))
(defconst asm-blox-command-specs
'((SET integerp asm-blox--subexpressions)
(CLR)
(CONST integerp)
(DUP asm-blox--subexpressions)
(ABS asm-blox--subexpressions)
(ADD asm-blox--subexpressions)
(SUB asm-blox--subexpressions)
(MUL asm-blox--subexpressions)
(DIV asm-blox--subexpressions)
(NEG asm-blox--subexpressions)
(REM asm-blox--subexpressions)
(AND asm-blox--subexpressions)
(NOT asm-blox--subexpressions)
(OR asm-blox--subexpressions)
(EQ asm-blox--subexpressions)
(NE asm-blox--subexpressions)
(LT asm-blox--subexpressions)
(LE asm-blox--subexpressions)
(GT asm-blox--subexpressions)
(GE asm-blox--subexpressions)
(GZ asm-blox--subexpressions)
(LZ asm-blox--subexpressions)
(EQZ asm-blox--subexpressions)
(BLOCK asm-blox--subexpressions)
(LOOP asm-blox--subexpressions)
(INC integerp)
(DEC integerp)
(BR_IF integerp)
(BR integerp)
(NOP)
(DROP asm-blox--subexpressions)
(SEND asm-blox--portp asm-blox--subexpressions)
(GET (lambda (x) (or (asm-blox--portp x) (integerp x))))
(LEFT)
(RIGHT)
(UP)
(DOWN)
(FN t) ;; FN needs special verification code
)
"List of commands and specifications for command arguments.")
(defun asm-blox--portp (x)
"Return non-nil if X is a port direction."
(if (stringp x)
(memq (intern x) '(UP DOWN LEFT RIGHT))
(memq x '(UP DOWN LEFT RIGHT))))
(defun asm-blox--code-node-validate (code-node)
"Determine if CODE-NODE adheres to the corresponding specification."
(let* ((children (asm-blox-code-node-children code-node))
(start-pos (asm-blox-code-node-start-pos code-node))
;; (end-pos (asm-blox-code-node-end-pos code-node))
(first-child (car children))
(cmd-spec (assoc first-child asm-blox-command-specs))
(banned-commands
(and asm-blox--extra-gameboard-cells
(asm-blox--problem-spec-banned-commands
asm-blox--extra-gameboard-cells))))
(cond
((not first-child)
`(error ,start-pos "No command found"))
((not cmd-spec)
`(error ,start-pos "Command not found"))
((member first-child banned-commands)
`(error ,start-pos "Command banned"))
(t
(let* ((specs (cdr cmd-spec))
(rest-children (cdr children))
(at-spec (car specs)))
(catch 'error
(while (or specs rest-children)
(cond
((eql at-spec 'asm-blox--subexpressions)
(if (seq-every-p #'asm-blox-code-node-p rest-children)
(setq rest-children nil)
(throw 'error `(error ,start-pos "bad end expressions"))))
((and rest-children (not specs))
(throw 'error `(error ,start-pos "too many args")))
((and specs (not rest-children))
(throw 'error `(error ,start-pos "not enough args")))
(t
(let* ((at-child (car rest-children))
(ok-p (funcall at-spec at-child)))
(if ok-p
(setq rest-children (cdr rest-children))
(let ((msg (format "bad arg to '%s'" first-child)))
(throw 'error `(error ,start-pos ,msg)))))))
(setq specs (cdr specs))
(setq at-spec (car specs)))))))))
(defun asm-blox--make-label ()
"Depending on the parse-depth create a label for the various goto statements."
(intern (concat "L_"
(number-to-string (random 100000))
"_"
(number-to-string asm-blox--parse-depth))))
(defun asm-blox--parse-tree-to-asm* (parse)
"Convert PARSE into a list of ASM instructions recursively."
(let ((asm-blox--parse-depth (if asm-blox--parse-depth
(1+ asm-blox--parse-depth)
0)))
(cond
((listp parse)
(let ((asm-stmts (mapcar #'asm-blox--parse-tree-to-asm* parse)))
(apply #'append asm-stmts)))
((asm-blox-code-node-p parse)
(let ((err (asm-blox--code-node-validate parse)))
(if err
(throw 'error err)
(let* ((children (asm-blox-code-node-children parse))
(start-pos (asm-blox-code-node-start-pos parse))
(end-pos (asm-blox-code-node-end-pos parse))
(first-child (car children))
(rest-children (cdr children)))
(cond
((not first-child)
(throw 'error `(error ,start-pos "No cmd found")))
((eql first-child 'BLOCK)
(let* ((label-symbol (asm-blox--make-label))
(asm-blox--branch-labels
(cons (cons asm-blox--parse-depth label-symbol)
asm-blox--branch-labels))
(rest-asm-stmts (mapcar #'asm-blox--parse-tree-to-asm*
rest-children)))
(append rest-asm-stmts
(list (asm-blox--code-node-create
:children (list 'LABEL label-symbol)
:start-pos nil
:end-pos nil)))))
((eql first-child 'LOOP)
(let* ((label-symbol (asm-blox--make-label))
(asm-blox--branch-labels
(cons (cons asm-blox--parse-depth label-symbol)
asm-blox--branch-labels))
(rest-asm-stmts
(mapcar #'asm-blox--parse-tree-to-asm* rest-children)))
(append (list (asm-blox--code-node-create
:children (list 'LABEL label-symbol)
:start-pos nil
:end-pos nil))
rest-asm-stmts)))
((eql first-child 'BR)
(let* ((br-num (car rest-children))
(lbl-ref-level (- asm-blox--parse-depth br-num 1))
(label-symbol
(or (cdr (assoc lbl-ref-level
asm-blox--branch-labels))
(throw 'error `(error ,start-pos "Label not found")))))
(asm-blox--code-node-create
:children (list 'JMP label-symbol)
:start-pos start-pos
:end-pos end-pos)))
((eql first-child 'BR_IF)
(let* ((br-num (car rest-children))
(lbl-ref-level (- asm-blox--parse-depth br-num 1))
(label-symbol
(or (cdr (assoc lbl-ref-level asm-blox--branch-labels))
(throw 'error `(error ,start-pos "Label not found")))))
(asm-blox--code-node-create
:children (list 'JMP_IF label-symbol)
:start-pos start-pos
:end-pos end-pos)))
((eql first-child 'IF)
(let* ((then-case (car rest-children))
(else-case (cadr rest-children))
(then-label (asm-blox--make-label))
(end-label (asm-blox--make-label)))
`(,(asm-blox--code-node-create
:children (list 'JMP_IF_NOT then-label)
:start-pos nil
:end-pos nil)
,@(if then-case
(seq-map #'asm-blox--parse-tree-to-asm*
(cdr (asm-blox-code-node-children then-case)))
nil)
,(asm-blox--code-node-create
:children (list 'JMP end-label)
:start-pos nil
:end-pos nil)
,(asm-blox--code-node-create
:children (list 'LABEL then-label)
:start-pos nil
:end-pos nil)
,@(if else-case
(seq-map #'asm-blox--parse-tree-to-asm*
(cdr (asm-blox-code-node-children else-case)))
nil)
,(asm-blox--code-node-create
:children (list 'LABEL end-label)
:start-pos nil
:end-pos nil))))
((assoc first-child asm-blox-command-specs)
(let* ((cmd-spec (assoc first-child asm-blox-command-specs))
(spec (cdr cmd-spec))
(rest-children (cdr children))
(children-cmds '()))
;; Determine which children are commands
;; that run before the command we're at.
(while (and spec rest-children)
(when (eql (car spec) 'asm-blox--subexpressions)
(setq children-cmds (seq-map #'asm-blox--parse-tree-to-asm*
rest-children)))
(setq spec (cdr spec))
(setq rest-children (cdr rest-children)))
(append children-cmds (list parse))))
(t `(error ,start-pos ,(format "Bad cmd: %s " first-child)))))))))))
(defun asm-blox--flatten-list (tree)
"Return a \"flattened\" copy of TREE. Copied from Emacs 27.1."
(let (elems)
(while (consp tree)
(let ((elem (pop tree)))
(while (consp elem)
(push (cdr elem) tree)
(setq elem (car elem)))
(if elem (push elem elems))))
(if tree (push tree elems))
(nreverse elems)))
(defun asm-blox--parse-tree-to-asm (parse)
"Generate game bytecode from tree of PARSE, resolving labels."
(catch 'error
(let ((asm (asm-blox--flatten-list (asm-blox--parse-tree-to-asm* parse))))
(asm-blox--resolve-labels asm)
asm)))
(defun asm-blox--resolve-labels (asm)
"Change each label reference in ASM to index in program."
(let ((idxs '())
(idx 0))
(dolist (code-node asm)
(let* ((code-data (asm-blox-code-node-children code-node))
(cmd (car code-data)))
(when (eql cmd 'LABEL)
(let ((label-name (cadr code-data)))
(setq idxs (cons (cons label-name idx) idxs)))))
(setq idx (1+ idx)))
(dolist (code-node asm)
(let* ((code-data (asm-blox-code-node-children code-node))
(cmd (car code-data)))
(when (or (eql cmd 'JMP)
(eql cmd 'JMP_IF)
(eql cmd 'JMP_IF_NOT))
(let* ((label-name (cadr code-data))
(jmp-to-idx (cdr (assoc label-name idxs))))
(setcdr code-data (list jmp-to-idx))))))))
;;; RUNTIME ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(cl-defstruct (asm-blox--cell-runtime
(:constructor asm-blox--cell-runtime-create)
(:copier nil))
"Structure that contains the runtime for a cell on the board.
A cell can be either for ASM or YAML. An ASM cell will have
instructions, a PC, and a stack, while a YAML runtime will have
run-function, run-spec, and run-state.
Both have directional ports and staging ports. The staging ports
are a way to let all of the cells run without the cells that run
previously change what values the later cells read. After all
cells have moved, the staging port becomes the current port."
instructions
pc
stack
row col
staging-up staging-down staging-left staging-right
up down left right
run-function message-function
run-spec
run-state)
(defun asm-blox--cell-message-at-pos (row col)
"Return cell runtimes display message at ROW COL if exists."
(let* ((cell-runtime (asm-blox--cell-at-row-col row col))
(msg-fn (asm-blox--cell-runtime-message-function cell-runtime)))
(when msg-fn
(funcall msg-fn cell-runtime))))
(defun asm-blox--cell-runtime-current-instruction (cell-runtime)
"Return the current instruction of CELL-RUNTIME based in pc."
(let* ((pc (asm-blox--cell-runtime-pc cell-runtime))
(instrs (asm-blox--cell-runtime-instructions cell-runtime))
(instrs (if (not (listp instrs)) (list instrs) instrs)))
(if (not instrs)
(asm-blox--code-node-create :children '(_EMPTY))
(and (>= pc (length instrs)) (error "End of program error"))
(car (nthcdr pc instrs)))))
(defun asm-blox--gameboard-in-final-state-p ()
"Return non-nil if the gameboard is in a finalized state."
;; If asm-blox--gameboard-state is not nil then it is in finalized state.
asm-blox--gameboard-state)
;; TODO - consolidate this function with asm-blox--cell-at-moved-row-col
(defun asm-blox--cell-at-row-col (row col)
"Return the cell at index ROW COL from the gameboard."
(if (and (<= 0 row (1- asm-blox--gameboard-row-ct))
(<= 0 col (1- asm-blox--gameboard-col-ct)))
(aref asm-blox--gameboard
(+ (* row asm-blox--gameboard-col-ct)
col))
;; if reference to out of bounds, return empty cell runtime.
(asm-blox--cell-runtime-create)))
(defun asm-blox--set-cell-at-row-col (row col cell-runtime)
"Set board cell at ROW, COL to CELL-RUNTIME."
(when (not asm-blox--gameboard)
(setq asm-blox--gameboard
(make-vector (* asm-blox--gameboard-col-ct asm-blox--gameboard-row-ct)
nil)))
(setf (aref asm-blox--gameboard (+ (* row asm-blox--gameboard-col-ct) col))
cell-runtime))
(defun asm-blox--cell-at-moved-row-col (row col dir)
"Return the item at the cell in the gameboard at position DIR from ROW,COL."
(let* ((d-row (cond ((eql dir 'UP) -1)
((eql dir 'DOWN) 1)
(t 0)))
(d-col (cond ((eql dir 'LEFT) -1)
((eql dir 'RIGHT) 1)
(t 0)))
(row* (+ row d-row))
(col* (+ col d-col)))
(asm-blox--cell-at-row-col row* col*)))
(defun asm-blox--mirror-direction (direction)
"Retunr the opposite of DIRECTION."
(pcase direction
('UP 'DOWN)
('DOWN 'UP)
('LEFT 'RIGHT)
('RIGHT 'LEFT)))
(defun asm-blox--get-value-from-direction (cell-runtime direction)
"Dynamically look up and return value at DIRECTION on CELL-RUNTIME."
(pcase direction
('UP (asm-blox--cell-runtime-up cell-runtime))
('RIGHT (asm-blox--cell-runtime-right cell-runtime))
('DOWN (asm-blox--cell-runtime-down cell-runtime))
('LEFT (asm-blox--cell-runtime-left cell-runtime))))
(defun asm-blox--get-value-from-staging-direction (cell-runtime direction)
"Dynamically look up and return value at DIRECTION on CELL-RUNTIME."
(pcase direction
('UP (asm-blox--cell-runtime-staging-up cell-runtime))
('RIGHT (asm-blox--cell-runtime-staging-right cell-runtime))
('DOWN (asm-blox--cell-runtime-staging-down cell-runtime))
('LEFT (asm-blox--cell-runtime-staging-left cell-runtime))))
(defun asm-blox--gameboard-source-at-pos (row col &optional dir)
"Return non-nil if a source exists at ROW, COL (at offset DIR)."
(let* ((d-row (cond ((eql dir 'UP) -1)
((eql dir 'DOWN) 1)
(t 0)))
(d-col (cond ((eql dir 'LEFT) -1)
((eql dir 'RIGHT) 1)
(t 0)))
(row* (+ row d-row))
(col* (+ col d-col))
(sources (asm-blox--problem-spec-sources asm-blox--extra-gameboard-cells)))
(seq-find (lambda (source)
(and (= (asm-blox--cell-source-row source) row*)
(= (asm-blox--cell-source-col source) col*)))
sources)))
(defun asm-blox--valid-position (row col &optional dir)
"Return non-nil if cell exists at ROW, COL (plus optional DIR)."
(let* ((d-row (cond ((eql dir 'UP) -1)
((eql dir 'DOWN) 1)
(t 0)))
(d-col (cond ((eql dir 'LEFT) -1)
((eql dir 'RIGHT) 1)
(t 0)))
(row* (+ row d-row))
(col* (+ col d-col)))
(and (<= 0 row* (1- asm-blox--gameboard-row-ct))
(<= 0 col* (1- asm-blox--gameboard-col-ct)))))
(defun asm-blox--cell-runtime-merge-ports-with-staging (cell-runtime)
"For CELL-RUNTIME, if the staging region has a value, move it to port.
If the port does't have a value, set staging to nil."
;; This function is needed to prevent execution order from tampering with the
;; execution results.
(dolist (direction '(UP DOWN LEFT RIGHT))
(let ((staging-value
(asm-blox--get-value-from-staging-direction cell-runtime direction))
(value (asm-blox--get-value-from-direction cell-runtime direction)))
(when (and (eql staging-value 'sent) (not value))
(asm-blox--cell-runtime-set-staging-value-from-direction cell-runtime direction nil)
(setq staging-value nil))
(when (and staging-value (not value))
(asm-blox--cell-runtime-set-value-from-direction cell-runtime direction staging-value)
(asm-blox--cell-runtime-set-staging-value-from-direction cell-runtime direction 'sent)))))
(defun asm-blox--remove-value-from-staging-direction (cell-runtime direction)
"Dynamically look up and return value at staging DIRECTION on CELL-RUNTIME."
(pcase direction
('UP (setf (asm-blox--cell-runtime-staging-up cell-runtime) nil))
('RIGHT (setf (asm-blox--cell-runtime-staging-right cell-runtime) nil))
('DOWN (setf (asm-blox--cell-runtime-staging-down cell-runtime) nil))
('LEFT (setf (asm-blox--cell-runtime-staging-left cell-runtime) nil))))
(defun asm-blox--remove-value-from-direction (cell-runtime direction)
"Dynamically look up and return value at DIRECTION on CELL-RUNTIME."
(pcase direction
('UP (setf (asm-blox--cell-runtime-up cell-runtime) nil))
('RIGHT (setf (asm-blox--cell-runtime-right cell-runtime) nil))
('DOWN (setf (asm-blox--cell-runtime-down cell-runtime) nil))
('LEFT (setf (asm-blox--cell-runtime-left cell-runtime) nil))))
;; TODO don't repeat this logic elsewhere
(defun asm-blox--cell-runtime-set-value-from-direction (cell-runtime direction value)
"Dynamically set the DIRECTION port of CELL-RUNTIME to VALUE."
(pcase direction
('UP (setf (asm-blox--cell-runtime-up cell-runtime) value))
('RIGHT (setf (asm-blox--cell-runtime-right cell-runtime) value))
('DOWN (setf (asm-blox--cell-runtime-down cell-runtime) value))
('LEFT (setf (asm-blox--cell-runtime-left cell-runtime) value))))
(defun asm-blox--cell-runtime-set-staging-value-from-direction (cell-runtime direction value)
"Dynamically set the staging DIRECTION port of CELL-RUNTIME to VALUE."
(pcase direction
('UP (setf (asm-blox--cell-runtime-staging-up cell-runtime) value))
('RIGHT (setf (asm-blox--cell-runtime-staging-right cell-runtime) value))
('DOWN (setf (asm-blox--cell-runtime-staging-down cell-runtime) value))
('LEFT (setf (asm-blox--cell-runtime-staging-left cell-runtime) value))))
(defun asm-blox--cell-runtime-instructions-length (cell-runtime)
"Return the length of CELL-RUNTIME."
(length (asm-blox--cell-runtime-instructions cell-runtime)))
(defun asm-blox--cell-runtime-pc-inc (cell-runtime)
"Return the length of CELL-RUNTIME."
(let ((instr-ct (asm-blox--cell-runtime-instructions-length cell-runtime))
(pc (asm-blox--cell-runtime-pc cell-runtime)))
(if (= (1+ pc) instr-ct)
(setf (asm-blox--cell-runtime-pc cell-runtime) 0)
(setf (asm-blox--cell-runtime-pc cell-runtime) (1+ pc)))))
(defun asm-blox--cell-runtime-push (cell-runtime value)
"Add VALUE to the stack of CELL-RUNTIME."
;; TODO: Handle stack overflow error.
(let* ((stack (asm-blox--cell-runtime-stack cell-runtime)))
(when (>= (length stack) 4)
(let ((row (asm-blox--cell-runtime-row cell-runtime))
(col (asm-blox--cell-runtime-col cell-runtime)))
(throw 'runtime-error `(error "Stack overflow" ,row ,col))))
(setf (asm-blox--cell-runtime-stack cell-runtime) (cons value stack))))
(defun asm-blox--cell-runtime-pop (cell-runtime)
"Pop and return a value from the stack of CELL-RUNTIME."
(let* ((stack (asm-blox--cell-runtime-stack cell-runtime))
(val (car stack)))
;; TODO: Handle stack underflow error.
(when (not val)
(let ((row (asm-blox--cell-runtime-row cell-runtime))
(col (asm-blox--cell-runtime-col cell-runtime)))
(throw 'runtime-error `(error "Stack underflow" ,row ,col))))
(prog1 val
(setf (asm-blox--cell-runtime-stack cell-runtime) (cdr stack)))))
(defun asm-blox--binary-operation (cell-runtime function)
"Perform binary operation FUNCTION on the top two items of CELL-RUNTIME."
(let* ((v1 (asm-blox--cell-runtime-pop cell-runtime))
(v2 (asm-blox--cell-runtime-pop cell-runtime))
(res (funcall function v2 v1)))
(when (> res 999)
(setq res (- res (* 2 999))))
(when (< res -999)
(setq res (+ res (* 2 999))))
(asm-blox--cell-runtime-push cell-runtime res)))
(defun asm-blox--cell-runtime-set-stack (cell-runtime offset &optional op)
"Set the stack at a given OFFSET of CELL-RUNTIME to top stack value.
If OP is a symbol, perform special logic."
(let* ((row (asm-blox--cell-runtime-row cell-runtime))
(col (asm-blox--cell-runtime-col cell-runtime))
(stack (asm-blox--cell-runtime-stack cell-runtime))
(offset (if (< offset 0) (+ offset (length stack)) offset))
(curr-val (nth (- (length stack) offset 1) stack))
(v (cond
((eql op 'INC) (1+ curr-val))
((eql op 'DEC) (1- curr-val))
(t (asm-blox--cell-runtime-pop cell-runtime)))))
(when (or (< offset 0) (>= offset (length stack)))
(setq asm-blox-runtime-error ;; TODO: extract this logic
(list "Idx out of bounds" row col))
(setq asm-blox--gameboard-state 'error))
(setcar (nthcdr (- (length stack) offset 1) stack) v)))
(defun asm-blox--unary-operation (cell-runtime function)
"Perform binary operation FUNCTION on the top two items of CELL-RUNTIME."
(let* ((v (asm-blox--cell-runtime-pop cell-runtime))
(res (funcall function v)))
(asm-blox--cell-runtime-push cell-runtime res)))
(defun asm-blox--cell-runtime-send (cell-runtime direction)
"Put the top value of CELL-RUNTIME's stack on the DIRECTION register."
(let ((v (asm-blox--cell-runtime-pop cell-runtime))
(current-val))
(pcase direction
('UP (setq current-val (asm-blox--cell-runtime-staging-up cell-runtime)))
('DOWN (setq current-val (asm-blox--cell-runtime-staging-down cell-runtime)))
('LEFT (setq current-val (asm-blox--cell-runtime-staging-left cell-runtime)))
('RIGHT (setq current-val (asm-blox--cell-runtime-staging-right cell-runtime))))
(let ((result
(if current-val
;; item is blocked
'blocked
(asm-blox--cell-runtime-set-staging-value-from-direction cell-runtime direction v))))
(when (eql result 'blocked)
(asm-blox--cell-runtime-push cell-runtime v))
result)))
(defun asm-blox--cell-runtime-get-extra (cell-runtime direction)
"Perform the GET command on CELL-RUNTIME outside the gameboard at DIRECTION."
(let* ((at-row (asm-blox--cell-runtime-row cell-runtime))
(at-col (asm-blox--cell-runtime-col cell-runtime))
(source (asm-blox--gameboard-source-at-pos at-row at-col direction)))
(if (or (not source) (not (asm-blox--cell-source-current-value source)))
'blocked
(let ((v (asm-blox--cell-source-pop source)))
(asm-blox--cell-runtime-push cell-runtime v)))))
(defun asm-blox--cell-runtime-stack-get (cell-runtime loc)
"Perform GET command variant, grabbing the LOC value from CELL-RUNTIME's stack."
(let ((row (asm-blox--cell-runtime-row cell-runtime))
(col (asm-blox--cell-runtime-col cell-runtime))
(stack (seq-reverse (asm-blox--cell-runtime-stack cell-runtime)))
(val))
;; Error checking
(if (>= loc 0)
(progn
(when (>= loc (length stack))
(setq asm-blox-runtime-error ;; TODO: extract the logic here to separate function
(list (format "Bad idx %d/%d" loc (length stack)) row col)))
(setq val (nth loc stack)))
(when (> (- loc) (length stack))
(setq asm-blox-runtime-error ;; TODO: extract the logic here to separate function
(list (format "Bad idx %d/%d" loc (length stack)) row col)))
(setq val (nth (+ (length stack) loc) stack)))
(asm-blox--cell-runtime-push cell-runtime val)))
(defun asm-blox--cell-runtime-get (cell-runtime direction)
"Perform the GET command running from CELL-RUNTIME, recieving from DIRECTION."
(if (integerp direction)
(asm-blox--cell-runtime-stack-get cell-runtime direction)
(let* ((at-row (asm-blox--cell-runtime-row cell-runtime))
(at-col (asm-blox--cell-runtime-col cell-runtime)))
(if (not (asm-blox--valid-position at-row at-col direction))
(asm-blox--cell-runtime-get-extra cell-runtime direction)
(let* ((opposite-direction (asm-blox--mirror-direction direction))
(from-cell (asm-blox--cell-at-moved-row-col at-row at-col direction))
(recieve-val (asm-blox--get-value-from-direction from-cell opposite-direction)))
(if recieve-val
(progn
(asm-blox--cell-runtime-push cell-runtime recieve-val)
(asm-blox--remove-value-from-direction from-cell opposite-direction))
'blocked))))))
(defun asm-blox--true-p (v)
"Return non-nil if V is truthy."
(not (= 0 v)))
(defun asm-blox--cell-runtime-skip-labels (cell-runtime)
"Skip pc over any label instructions for CELL-RUNTIME.
This logic is needed to display current command properly."
(while (let* ((current-instr (asm-blox--cell-runtime-current-instruction cell-runtime))
(code-data (asm-blox-code-node-children current-instr))
(cmd (car code-data)))
(eql cmd 'LABEL))
;; TODO: check case of all LABEL commands
(asm-blox--cell-runtime-pc-inc cell-runtime)))
(defun asm-blox--cell-runtime-step (cell-runtime)
"Perform one step of CELL-RUNTIME."
(let* ((current-instr (asm-blox--cell-runtime-current-instruction cell-runtime))
(code-data (asm-blox-code-node-children current-instr))
(cmd (car code-data))
(status
(pcase cmd
('_EMPTY 'blocked)
('CONST (let ((const (cadr code-data)))
(asm-blox--cell-runtime-push cell-runtime const)))
('SET (let ((stack-offset (cadr code-data)))
(asm-blox--cell-runtime-set-stack cell-runtime stack-offset)))
('INC (let ((stack-offset (cadr code-data)))
(asm-blox--cell-runtime-set-stack cell-runtime stack-offset 'INC)))
('DEC (let ((stack-offset (cadr code-data)))
(asm-blox--cell-runtime-set-stack cell-runtime stack-offset 'DEC)))
('CLR (setf (asm-blox--cell-runtime-stack cell-runtime) nil))
('DUP (let ((stack (asm-blox--cell-runtime-stack cell-runtime)))
(setf (asm-blox--cell-runtime-stack cell-runtime) (append stack stack))))
('ADD (asm-blox--binary-operation cell-runtime #'+))
('SUB (asm-blox--binary-operation cell-runtime #'-))
('MUL (asm-blox--binary-operation cell-runtime #'*))
('DIV (asm-blox--binary-operation cell-runtime #'/))
('REM (asm-blox--binary-operation cell-runtime #'%))
('AND (asm-blox--binary-operation cell-runtime(lambda (a b) (if (and (asm-blox--true-p a)
(asm-blox--true-p b))
1 0))))
('NOT (asm-blox--unary-operation cell-runtime (lambda (x) (if (asm-blox--true-p x) 0 1))))
('NEG (asm-blox--unary-operation cell-runtime (lambda (x) (- x))))
('ABS (asm-blox--unary-operation cell-runtime (lambda (x) (abs x))))
('OR (asm-blox--binary-operation cell-runtime (lambda (a b) (if (or (asm-blox--true-p a)
(asm-blox--true-p b))
1 0))))
('EQZ (asm-blox--unary-operation cell-runtime (lambda (x) (if (= 0 x) 1 0))))
('LZ (asm-blox--unary-operation cell-runtime (lambda (x) (if (< x 0) 1 0))))
('GZ (asm-blox--unary-operation cell-runtime (lambda (x) (if (> x 0) 1 0))))
('EQ (asm-blox--binary-operation cell-runtime (lambda (a b) (if (= a b) 1 0))))
('NE (asm-blox--binary-operation cell-runtime (lambda (a b) (if (not (= a b)) 1 0))))
('LT (asm-blox--binary-operation cell-runtime (lambda (a b) (if (< a b) 1 0))))
('LE (asm-blox--binary-operation cell-runtime (lambda (a b) (if (<= a b) 1 0))))
('GT (asm-blox--binary-operation cell-runtime (lambda (a b) (if (> a b) 1 0))))
('GE (asm-blox--binary-operation cell-runtime (lambda (a b) (if (>= a b) 1 0))))
('NOP (ignore))
('DROP (asm-blox--cell-runtime-pop cell-runtime))
('SEND (asm-blox--cell-runtime-send cell-runtime (cadr code-data)))
('GET (asm-blox--cell-runtime-get cell-runtime (cadr code-data)))
('RIGHT (asm-blox--cell-runtime-get cell-runtime 'RIGHT))
('LEFT (asm-blox--cell-runtime-get cell-runtime 'LEFT))
('UP (asm-blox--cell-runtime-get cell-runtime 'UP))
('DOWN (asm-blox--cell-runtime-get cell-runtime 'DOWN))
('JMP (let ((position (cadr code-data)))
(setf (asm-blox--cell-runtime-pc cell-runtime) position)))
('LABEL 'label)
('JMP_IF (let ((position (cadr code-data))
(top-value (asm-blox--cell-runtime-pop cell-runtime)))
(when (asm-blox--true-p top-value)
(setf (asm-blox--cell-runtime-pc cell-runtime) position))))
('JMP_IF_NOT (let ((position (cadr code-data))
(top-value (asm-blox--cell-runtime-pop cell-runtime)))
(when (not (asm-blox--true-p top-value))
(setf (asm-blox--cell-runtime-pc cell-runtime) position)))))))
;; handle PC movement
(pcase status
('blocked nil)
('jump nil)
('label (progn
(asm-blox--cell-runtime-pc-inc cell-runtime)
(asm-blox--cell-runtime-step cell-runtime)))
(_ (asm-blox--cell-runtime-pc-inc cell-runtime)))
(asm-blox--cell-runtime-skip-labels cell-runtime)))
(defun asm-blox--step ()
"Perform the operations needed to progress the game one step."
(asm-blox--gameboard-step)
(asm-blox--resolve-port-values)
(asm-blox--extra-gameboard-step))
(defun asm-blox--extra-gameboard-step ()
"Perform step on all things not on the gameboard."
(let ((sinks (asm-blox--problem-spec-sinks asm-blox--extra-gameboard-cells)))
(dolist (sink sinks)
(asm-blox--cell-sink-get sink))))
(defun asm-blox--gameboard-step ()
"Perform step on all cells on the gameboard."
(let ((res
(catch 'runtime-error
(let ((last-cell-fns '()))
(dotimes (idx (length asm-blox--gameboard))
(let ((cell (aref asm-blox--gameboard idx)))
(let ((fn (asm-blox--cell-runtime-run-function cell)))
(if (functionp fn)
(setq last-cell-fns (cons (cons fn cell) last-cell-fns))
(asm-blox--cell-runtime-step cell)))))
;; We need to run the non-code cells last because they directly
;; manipulate their ports.
(dolist (fn+cell last-cell-fns)
(funcall (car fn+cell) (cdr fn+cell)))))))
(when (eql (car res) 'error)
(setq asm-blox--gameboard-state 'error)
(setq asm-blox-runtime-error (cdr res)))))
(defun asm-blox--resolve-port-values ()
"Move staging port values to main, propogate nils up to staging."
(dotimes (idx (length asm-blox--gameboard))
(let ((cell (aref asm-blox--gameboard idx)))
(asm-blox--cell-runtime-merge-ports-with-staging cell))))
(defun asm-blox-check-winning-conditions ()
"Return non-nil if all sinks are full."
(when (not (asm-blox--gameboard-in-final-state-p))
(let ((sinks (asm-blox--problem-spec-sinks asm-blox--extra-gameboard-cells))
(win-p t))
(while (and sinks win-p)
(let* ((sink (car sinks))
(expected-data (asm-blox--cell-sink-expected-data sink))
(idx (asm-blox--cell-sink-idx sink))
(err-val (asm-blox--cell-sink-err-val sink)))
(if (asm-blox--cell-sink-expected-text sink)
;; If expected text exists we are dealing with an editor sink
(let* ((expected-text (string-trim-right (asm-blox--cell-sink-expected-text sink)))
(expected-lines (split-string expected-text "\n"))
(text (string-trim-right (asm-blox--cell-sink-editor-text sink)))
(text-lines (split-string text "\n")))
(if (not (equal (length text-lines) (length expected-lines)))
(setq win-p nil)
(unless (cl-loop for expected-line in expected-lines
for line in text-lines
always (equal (string-trim-right expected-line) (string-trim-right line)))
(setq win-p nil))))
(when (or (< idx (length expected-data))
err-val)
(setq win-p nil)))
(setq sinks (cdr sinks))))
(when win-p
(asm-blox--win-file-for-current-buffer)
;; TODO: Do something else for the victory.
(setq asm-blox--gameboard-state 'win)
(message "Congragulations, you won!")))))
;;; Gameboard Display Helpers ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Functions that map the domain of the gameboard to that of the
;; display.
(defun asm-blox--get-direction-col-registers (row col direction)
"Return the register value for the DIRECTION registers at ROW, COL.
ROW and COL here do not refer to the coordinates of a
cell-runtime but rather the in-between row/col."
(cl-assert (or (eql 'LEFT direction) (eql 'RIGHT direction)))
(cl-assert (<= 0 row (1- asm-blox--gameboard-row-ct)))
(cl-assert (<= 0 col asm-blox--gameboard-col-ct))
(let ((cell-col (if (eql 'RIGHT direction) (1- col) col)))
(cond
;; outputs are never displayed on the board
((or (and (= col 0) (eql direction 'LEFT))
(and (= row asm-blox--gameboard-col-ct) (eql direction 'RIGHT)))
nil)
((or (= col 0)
(= col asm-blox--gameboard-col-ct))
(let* ((source (asm-blox--gameboard-source-at-pos row cell-col)))
(if (not source)
nil
(asm-blox--cell-source-current-value source))))
(t
(let* ((cell-runtime (asm-blox--cell-at-row-col row cell-col)))
(asm-blox--get-value-from-direction cell-runtime direction))))))
(defun asm-blox--get-direction-row-registers (row col direction)
"Return the register value for the DIRECTION registers at ROW, COL.