A pixel art image of a mushroom

December Adventure

This will be my log journal for December Adventure. I have several plans for it:

  • Continue my Common Lisp Vera implementation.
    • Check my vastly outdated nova page. Will update soon.
    • I would like to test the idea of a Vera/Mira OS and how that would look like overlayed over the Common Lisp version.
  • Work on my rewriting videogame.
    • The idea is to be a wizard, and spells are small visual rewriting programs that allow you to understand the fabric of reality and change it to your whim. As you advance you get small reagents that you can add and modify in your spellbook.
  • Work on a realistic japanese calligraphy generative rendering.
    • As a 尺八 - shakuhachi player, I’d like to have a nice way to practice my songs, I wanted to create a way for people to practice songs online, in a minimalistic setting, possibly with microphone recording to record your sessions and compare with the originals (?). Trying to mimic the way shakuhachi has been tought for centuries from your home.

I’m currently in Japan for a month with my family (partner and 10-month old daughter), so I don’t know how much I’ll be able to do. But I’ll try to get things done and write about them here and there while on downtime from visiting Japan.

Thanks to the Vera community on Discord for encouraging this December Adventure!

Day 1

Today I’ve been developing a Common Lisp implementation of Vera, which I started yesterday. The idea was to leverage Common Lisp codegen capabilities and the fairly good SBCL compiler. With these two tools we can have a native code compiler working in very few lines of code.

I’m gonna walk you through the current implementation, since it’s just a few lines of code. I’m gonna assume that you already have a working Vera parser, if you don’t you can use mine here.

A Vera program consists of a series of rules, each rule has two parts what is called the left hand side and the right hand side. When the Vera state contains the facts in the left hand side that rule triggers, those facts are removed and the facts from the right hand side are added to the state.

For example, consider this simple rule:

|a, b| c

When a and b exist, then those are consumed and replaced by c, after that rule triggers we start again from the top of the ruleset. There is a bit more to it and you can read other posts on Vera to get a bigger picture.

We are gonna write a normal Common Lisp function that does this for a very simple ruleset:

|| a  <- the empty rule means that it will be executed on start
|a| b
|b| c
|c| d
|d|
(defun some-ruleset ()
  (let ((running t)
        (start 1)
        (a 0)
        (b 0)
        (c 0)
        (d 0))
    (loop while running
      do (cond
           ((and (> start 0))
             (decf start)
             (incf a))
           ((and (> a 0))
             (decf a)
             (incf b))
           ((and (> b 0))
             (decf b)
             (incf c))
           ((and (> c 0))
             (decf c)
             (incf d))
           ((and (> d 0))
             (decf d))
           (t (setf running nil))))))

It’s a fairly simple function, we initialize one counter variable for each of the facts and run a loop where we check sequentially if the rule matches, if it does we decrement the left hand side and increment the right hand side. If we matched no rule we stop the loop by setting running to false.

Now that we have seen how we can write one of these functions by hand, let’s look into generating it programmatically.

Let’s say we have a list of rules, where we have already parsed the right and left hand sides.

(defun codegen-vera-program (rules)
  (let* ((unique-facts (find-unique-facts rules))
         (running-var (gensym))
         (counter-var (gensym)))
    `(lambda ()
       (let ((,running-var t) (start 1) (,counter-var 0))
         (let ,(mapcar (lambda (k) `(,k 0)) unique-facts)
           (loop while ,running-var
              do (cond
                   ,@(mapcar #'make-rule-body rules)
                   (t (setf ,running-var nil)))))))))

We are basically generating a template of the function above, we first find all the unique facts that are in the vera program and we use that to generate the list of variables, then we create the loop and we will populate it by calling make-rule-body for each rule in the ruleset. Let’s take a look at that function

(defun make-rule-body (rule)
  (let ((lhs (car rule))
        (rhs (cadr rule)))
    `((and ,@(mapcar (lambda (fact) `(> ,fact 0)) lhs))
      ,@(mapcar (lambda (f) `(decf ,f)) lhs)
      ,@(mapcar (lambda (f) `(incf ,f)) rhs))))

In this function we generate the condition and the decrements and increments for each rule, these will be injected into the cond of codegen-vera-program.

After we codegen a vera program we will have a quoted lambda expression that we can then pass to (compile nil <expr>) in order to get native code. With very little amount of lines we can now get a vera compiler that produces fairly good native code. This can be used for other cases when we have a custom language we wanna test out.

This post leaves out some details, I’ve used a slightly simplified version of vera without min semantics (I’ll expand on this at some other point, it’s a way to execute a rule several times on the same match if we have enough values for the left hand side) and the real common lisp code has some extra declarations and optimizations using FIXNUMs so the compiler uses registers and native operations to perform arithmetic.

You can see the full version here

With this implementation, this simple ruleset:

|a, b| c
|c, a| d

Ends up being 256 bytes of arm64 machine code:

CL-USER> (disassemble 'basic-rewrite)
; disassembly for BASIC-REWRITE
; Size: 256 bytes. Origin: #x7008F708E4                       ; BASIC-REWRITE
; 8E4:       AA0A40F9         LDR R0, [THREAD, #16]           ; binding-stack-pointer
; 8E8:       4A0B00F9         STR R0, [CFP, #16]
; 8EC:       7A0300F9         STR CFP, [CSP]
; 8F0:       4A0F40F9         LDR R0, [CFP, #24]
; 8F4:       2BFCFF58         LDR R1, #x7008F70878            ; '(A B)
; 8F8:       56FBFF58         LDR LEXENV, #x7008F70860        ; #<SB-KERNEL:FDEFN HAS-KEYS>
; 8FC:       970080D2         MOVZ NARGS, #4
; 900:       FA031BAA         MOV CFP, CSP
; 904:       DE9240F8         LDR LR, [LEXENV, #9]
; 908:       C0033FD6         BLR LR
; 90C:       3B039B9A         CSEL CSP, OCFP, CSP, EQ
; 910:       5F011DEB         CMP R0, NULL
; 914:       01050054         BNE L3
; 918:       7A0300F9         STR CFP, [CSP]
; 91C:       4A0F40F9         LDR R0, [CFP, #24]
; 920:       0BFBFF58         LDR R1, #x7008F70880            ; '(C A)
; 924:       F6F9FF58         LDR LEXENV, #x7008F70860        ; #<SB-KERNEL:FDEFN HAS-KEYS>
; 928:       970080D2         MOVZ NARGS, #4
; 92C:       FA031BAA         MOV CFP, CSP
; 930:       DE9240F8         LDR LR, [LEXENV, #9]
; 934:       C0033FD6         BLR LR
; 938:       3B039B9A         CSEL CSP, OCFP, CSP, EQ
; 93C:       5F011DEB         CMP R0, NULL
; 940:       41020054         BNE L2
; 944:       AB4E43F9         LDR R1, [THREAD, #1688]         ; tls: *STANDARD-OUTPUT*
; 948:       6B00F8B6         TBZ R1, #63, L0
; 94C:       EAF9FF58         LDR R0, #x7008F70888            ; '*STANDARD-OUTPUT*
; 950:       4B1140F8         LDR R1, [R0, #1]
; 954: L0:   7A0300F9         STR CFP, [CSP]
; 958:       CAF9FF58         LDR R0, #x7008F70890            ; "No rewrite
                                                              ; "
; 95C:       970080D2         MOVZ NARGS, #4
; 960:       FA031BAA         MOV CFP, CSP
; 964:       29F880D2         MOVZ TMP, #1985
; 968:       BE6B69F8         LDR LR, [NULL, TMP]             ; WRITE-STRING
; 96C:       C0033FD6         BLR LR
; 970: L1:   4A0F40F9         LDR R0, [CFP, #24]
; 974:       B6F7FF58         LDR LEXENV, #x7008F70868        ; #<SB-KERNEL:FDEFN PRINT-COUNTERS>
; 978:       570080D2         MOVZ NARGS, #2
; 97C:       DE9240F8         LDR LR, [LEXENV, #9]
; 980:       DE130091         ADD LR, LR, #4
; 984:       C0031FD6         BR LR
; 988: L2:   7A0300F9         STR CFP, [CSP]
; 98C:       4A0F40F9         LDR R0, [CFP, #24]
; 990:       8BF7FF58         LDR R1, #x7008F70880            ; '(C A)
; 994:       2CF8FF58         LDR R2, #x7008F70898            ; '(D)
; 998:       D6F6FF58         LDR LEXENV, #x7008F70870        ; #<SB-KERNEL:FDEFN APPLY-RULE>
; 99C:       D70080D2         MOVZ NARGS, #6
; 9A0:       FA031BAA         MOV CFP, CSP
; 9A4:       DE9240F8         LDR LR, [LEXENV, #9]
; 9A8:       C0033FD6         BLR LR
; 9AC:       3B039B9A         CSEL CSP, OCFP, CSP, EQ
; 9B0:       F0FFFF17         B L1
; 9B4: L3:   7A0300F9         STR CFP, [CSP]
; 9B8:       4A0F40F9         LDR R0, [CFP, #24]
; 9BC:       EBF5FF58         LDR R1, #x7008F70878            ; '(A B)
; 9C0:       0CF7FF58         LDR R2, #x7008F708A0            ; '(C)
; 9C4:       76F5FF58         LDR LEXENV, #x7008F70870        ; #<SB-KERNEL:FDEFN APPLY-RULE>
; 9C8:       D70080D2         MOVZ NARGS, #6
; 9CC:       FA031BAA         MOV CFP, CSP
; 9D0:       DE9240F8         LDR LR, [LEXENV, #9]
; 9D4:       C0033FD6         BLR LR
; 9D8:       3B039B9A         CSEL CSP, OCFP, CSP, EQ
; 9DC:       E5FFFF17         B L1
; 9E0:       E00120D4         BRK #15                         ; Invalid argument count trap

Getting around 750 rewrites per microsecond (or 750M per second).

Backlinks