Forthy Friday (Macro)

Hi, Racket Discourse.

I was feeling a bit antsy last night and thought it would be good idea to take a look at forsp, which I'd glanced at before, but forgotten about again in the meantime.

Anthony presents a very interesting semantics with his fusion of forth and lisp characteristics.

The evaluation is call-by-push-value (CBPV), which I find quite pleasing to use.

I translated some of his code into a Racket macro, substituting his use of $ and ^ with quasiquote and unquote. I also took the liberty of referring to pop as pull, although this is immaterial.

'foo
; pushes the value 'foo onto the stack

`foo
; pulls a value off the stack and binds it to 'foo in the environment

,foo
; refs the value of 'foo in the environment and pushes it onto the stack

 foo
; calls the value of 'foo if it is a procedure, or pushes it onto the stack otherwise

(foo ...)
; a closure which captures the environment, and pushes the thunk of `foo ...` onto the stack

foo bar
; call foo, then call bar

I won't claim to be 100% faithful to the author's explanation, since it was merely an exploratory dive into the system, and forths in general. Very charming, nonetheless.

Just look at this Y-combinator:

(`f (`x (,x x) f) dup force) `Y

And the obligatory list?:

(`g (,g Y)) `rec ; a helper for working with Y

 (`self
  `_
  (#false (,_ cdr self) (,_ pair?) if) #true (,_ nil?) if)
 rec
 `list?

The implementation is reasonably short, and could probably be streamlined some more:

#lang racket/base

(require
  (only-in
   racket/function disjoin)
  (only-in
   racket/dict dict-ref dict-set dict?)
  (only-in
   racket/bool nor)
  (for-syntax
   racket/base
   syntax/parse))

(define stack-item?
  (disjoin
   symbol? string? char? exact-integer? rational? boolean?
   null?   pair?
   procedure?))

(define stack (make-parameter #false))
(define table (make-parameter #false))

(define (pull)
  (unless (pair? {stack})
    (error 'forsp "cannot pull from an empty stack"))
  (let ([ex (car {stack})])
    {stack (cdr {stack})}
    ex))

(define (push ex)
  (unless (stack-item? ex)
    (error 'forsp "expected a stack-item? found: ~a" ex))
  {stack (cons ex {stack})})

(define (pull/set ex)
  {table (dict-set {table} ex (pull))})

(define (unknown-handler ex)
  (ฮป _ (error 'forsp "unknown reference: ~a" ex)))

(define (ref/push ex)
  (push (dict-ref {table} ex (unknown-handler ex))))

(define (ref/call ex)
  (define ref (dict-ref {table} ex (unknown-handler ex)))
  (if (procedure? ref) (ref) (push ref)))

(define (nop/push) (push void))

(define (clo/push proc)
  (define tbl {table})
  (push (ฮป _ (parameterize ([table tbl]) (proc)))))

(define-syntax (forsp stx)
  (syntax-parse stx
    #:datum-literals (nil quote quasiquote unquote unquote-splicing)
    
    [(_ (~or* 'ex:id ex:string ex:char ex:exact-integer ex:boolean nil) exs ...)
     #;quote-push
     #'(begin (push (~? 'ex null)) (forsp exs ...))]

    [(_ `ex:id exs ...)
     #;quote-pull-set
     #'(begin (pull/set 'ex) (forsp exs ...))]

    [(_ ,ex:id exs ...)
     #;quote-ref-push
     #'(begin (ref/push 'ex) (forsp exs ...))]

    #;[(_ ,@ex:expr exs ...)
     #;quote-ref-push-xeno
     #'(begin (push ex) (forsp exs ...))]

    [(_ ex:id exs ...)
     #;quote-ref-call
     #'(begin (ref/call 'ex) (forsp exs ...))]

    [(_ () exs ...)
     #;void-push
     #'(begin (nop/push) (forsp exs ...))]

    [(_ (ex:expr ...) exs ...)
     #;closure-push
     #'(begin (clo/push (ฮป _ (forsp ex ...))) (forsp exs ...))]

    [(_) #''ok]))

And here we have some examples of its usage:

(define โˆ˜ compose1)

; some built-in functions available to the user
(define builtins
  `([symbol?   . ,(โˆ˜ push symbol?        pull)]
    [string?   . ,(โˆ˜ push string?        pull)]
    [char?     . ,(โˆ˜ push char?          pull)]
    [integer?  . ,(โˆ˜ push exact-integer? pull)]
    [rational? . ,(โˆ˜ push rational?      pull)]
    [boolean?  . ,(โˆ˜ push boolean?       pull)]
    
    [nil?      . ,(โˆ˜ push null?          pull)]
    [pair?     . ,(โˆ˜ push pair?          pull)]
    [dict?     . ,(โˆ˜ push dict?          pull)]
    [closure?  . ,(โˆ˜ push procedure?     pull)]
    
    [not       . ,(โˆ˜ push not pull)]
    [nor       . ,(โˆ˜ push (ฮป _ (let ([b (pull)] [a (pull)]) (nor a b))))]
    [eq?       . ,(โˆ˜ push (ฮป _ (let ([b (pull)] [a (pull)]) (equal? a b))))]
    
    [cons      . ,(โˆ˜ push (ฮป _ (cons (pull) (pull))))]
    [car       . ,(โˆ˜ push car pull)]
    [cdr       . ,(โˆ˜ push cdr pull)]
    
    [stack     . ,(โˆ˜ push stack)]
    [table     . ,(โˆ˜ push table)]
    
    [cswap     . ,(ฮป _ (when (pull) (let ([a (pull)] [b (pull)]) (push a) (push b))))]

    [<         . ,(โˆ˜ push (ฮป _ (let ([b (pull)] [a (pull)]) (<  a b))))]
    [โ‰ค         . ,(โˆ˜ push (ฮป _ (let ([b (pull)] [a (pull)]) (<= a b))))]
    [>         . ,(โˆ˜ push (ฮป _ (let ([b (pull)] [a (pull)]) (>  a b))))]
    [โ‰ฅ         . ,(โˆ˜ push (ฮป _ (let ([b (pull)] [a (pull)]) (>= a b))))]
    [=         . ,(โˆ˜ push (ฮป _ (let ([b (pull)] [a (pull)]) (=  a b))))]
    
    [+         . ,(โˆ˜ push (ฮป _ (let ([b (pull)] [a (pull)]) (+ a b))))]
    [-         . ,(โˆ˜ push (ฮป _ (let ([b (pull)] [a (pull)]) (- a b))))]
    [*         . ,(โˆ˜ push (ฮป _ (let ([b (pull)] [a (pull)]) (* a b))))]
    [/         . ,(โˆ˜ push (ฮป _ (let ([b (pull)] [a (pull)]) (/ a b))))]
    [ยซ         . ,(โˆ˜ push (ฮป _ (let ([b (pull)] [a (pull)]) (arithmetic-shift a b))))]
    [ยป         . ,(โˆ˜ push (ฮป _ (let ([b (pull)] [a (pull)]) (arithmetic-shift a (- b)))))]
    
    [chars     . ,(โˆ˜ push string->list pull)]
    
    [print     . ,(โˆ˜ println pull)]))

(define-syntax-rule
  (fresh-forsp ex ...)
  (parameterize ([stack null]
                 [table builtins])
    (forsp ex ...)))

(fresh-forsp
 ;------------------------------------------------------------
 ; utility procedures
 ;------------------------------------------------------------
 (`_ ,_ ,_)                   `dup
 (#true cswap)                `swap
 (`_ _)                       `force
 (`a `b `c ,c ,b ,a ,c)       `over2
 (`a `b `c ,b ,a ,c)          `rot
 
 (force cswap `_ force)       `if
 (`f `t `c `fn ,f ,t ,c fn)   `endif

 (`f (`x (,x x) f) dup force) `Y
 (`g (,g Y))                  `rec

 (`self
  `_
  (#false (,_ cdr self) (,_ pair?) if) #true (,_ nil?) if)
 rec
 `list?
 ;------------------------------------------------------------

 '-----------------------------
 `dashed-line

 'say-hello      print
 "Hello, world!" print
 dashed-line     print

 'make-a-list             print
 nil 3 cons 2 cons 1 cons print
 dashed-line              print

 'is-this-a-list?  print
 2 2 cons dup      print
 list?             print
 dashed-line       print

 'get-the-car print
 3 2 cons dup print
 car          print
 dashed-line  print
 
 'get-the-cdr print
 3 2 cons dup print
 cdr          print
 dashed-line  print

 'is-3=4?     print
 (#false print) (#true print) 3 4 eq? if
 dashed-line  print

 'is-1=1?     print
 ,if (1 1 eq?)
 (#true  print)
 (#false print)
 endif
 dashed-line  print

 '1+2=        print
 1 2 +        print
 dashed-line  print
 
 '2-1=        print
 2 1 -        print
 dashed-line  print
 
 '2*2=        print
 2 2 *        print
 dashed-line  print
 
 '2/2=        print
 2 2 /        print
 dashed-line  print
 
 (nor not)                 `or
 (`x `y ,y not ,x not nor) `and
 (and not)                 `nand

 'false-or-false   print
 #f #f or          print
 dashed-line       print
 
 'true-and-true    print
 #t #t and         print
 dashed-line       print
 
 'false-nand-false print
 #f #f nand        print
 dashed-line       print
 
 (`self
  `n
  (,n 1 - self ,n *) 1 (0 ,n eq?) if)
 rec
 `factorial

 'factorial-5 print
 5 factorial  print
 dashed-line  print
 
 'factorial-7 print
 7 factorial  print
 dashed-line  print
 
 (`self
  `n `fn
  (fn ,fn ,n 1 - self) () (0 ,n eq?) if)
 rec
 `repeat

 1 (dup) 2 repeat
 'print-1-times-3 print
 (print) 3 repeat
 dashed-line      print

 (stack nil?) `stack-nil?
 'is-the-stack-empty? print
 stack-nil?           print
 dashed-line          print
 
 '1-jumps-over-the-others print
 1 2 3 stack              print
 over2
 stack                    print
 dashed-line              print

 'rotate-3   print
 (rot stack print)
 3 repeat
 dashed-line print)
'say-hello
"Hello, world!"
'-----------------------------
'make-a-list
'(1 2 3)
'-----------------------------
'is-this-a-list?
'(2 . 2)
#f
'-----------------------------
'get-the-car
'(2 . 3)
2
'-----------------------------
'get-the-cdr
'(2 . 3)
3
'-----------------------------
'is-3=4?
#f
'-----------------------------
'is-1=1?
#t
'-----------------------------
'1+2=
3
'-----------------------------
'2-1=
1
'-----------------------------
'2*2=
4
'-----------------------------
'2/2=
1
'-----------------------------
'false-or-false
#f
'-----------------------------
'true-and-true
#t
'-----------------------------
'false-nand-false
#t
'-----------------------------
'factorial-5
120
'-----------------------------
'factorial-7
5040
'-----------------------------
'print-1-times-3
1
1
1
'-----------------------------
'is-the-stack-empty?
#t
'-----------------------------
'1-jumps-over-the-others
'(3 2 1)
'(1 3 2 1)
'-----------------------------
'rotate-3
'(2 1 3 1)
'(3 2 1 1)
'(1 3 2 1)
'-----------------------------
'ok
> 

Have a good weekend everyone!


Edit: I thought of a cute way to incorporate keywords:

20 'john #: man
21 'mary #: woman

#:woman `name `age
,name 'woman cons print
,age  'woman cons print

#:man `name `age
,name 'man cons print
,age  'man cons print

'flounder #: fish
#:bird `type

;=> '(woman . mary)
;=> '(woman . 21)
;=> '(man . john)
;=> '(man . 20)

;=> forsp: expected the keyword #:bird, found: #:fish

When the macro encounters the empty keyword, #:, it "quotes" the next identifier as a keyword, and pushes it onto the stack.

Then, when the macro encounters any other keyword, it pulls the next item from the stack, which must be a keyword equal to it. If it is not, an error is raised.

4 Likes

Named stacks aren't a bad use for keywords, either:

(sporklet
 (`_)              `pop
 (`_,_โ‚‚)           `dup
 [`_ ,@(abs _)]    `abs ; I try to use square brackets for "foreign" functions
 ,+                `add
 [`y `x ,@(- x y)] `sub
 [`y `x ,@(* x y)] `*
 (dup *)           `sqr
 
 (`#:x `#:y `#:z
  #:z pop
  #:y dup sqr `#:
  #:x sqr     `#:
  #:  add
  #:y abs     `#:
  #:  sub)
 `f

 3 2 1 f
 stack print)
;=> '(3)

I came across the idea from this blog, although it seems as though the author has been inactive on the project for a while (speculation).

P.S. aren't let and friends just grand?
The above becomes:

(let* ((out (โ†’ (mk-clos (let* ((_ (โ†))) (void)))))
       (pop (โ†))
       (out (โ†’ (mk-clos (let* ((_ (โ†)) (out (โ†’ _)) (out (โ†’ _))) (void)))))
       (dup (โ†))
       (out (โ†’ (mk-clos (let* ((_ (โ†)) (out (โ†’ (abs _)))) (void)))))
       (abs (โ†))
       (out (โ†’ core:add))
       (add (โ†))
       (out (โ†’ (mk-clos (let* ((y (โ†)) (x (โ†)) (out (โ†’ (- x y)))) (void)))))
       (sub (โ†))
       (out (โ†’ (mk-clos (let* ((y (โ†)) (x (โ†)) (out (โ†’ (* x y)))) (void)))))
       (* (โ†))
       (out (โ†’ (mk-clos (let* ((out (โ‡’ dup)) (out (โ‡’ *))) (void)))))
       (sqr (โ†))
       (out
        (โ†’
         (mk-clos
          (let* ((pin (hosts))
                 (who (โ‰ซ '#:x))
                 (who (โ‰ซ '#:y))
                 (who (โ‰ซ '#:z))
                 (who (โ™ฏ '#:z))
                 (out (โ‡’ pop))
                 (who (โ™ฏ '#:y))
                 (out (โ‡’ dup))
                 (out (โ‡’ sqr))
                 (who (โ‰ซ '#:))
                 (who (โ™ฏ '#:x))
                 (out (โ‡’ sqr))
                 (who (โ‰ซ '#:))
                 (who (โ™ฏ '#:))
                 (out (โ‡’ add))
                 (who (โ™ฏ '#:y))
                 (out (โ‡’ abs))
                 (who (โ‰ซ '#:))
                 (who (โ™ฏ '#:))
                 (out (โ‡’ sub)))
            (โˆ pin (hosts))))))
       (f (โ†))
       (out (โ†’ '3))
       (out (โ†’ '2))
       (out (โ†’ '1))
       (out (โ‡’ f))
       (out (โ‡’ core:stack))
       (out (โ‡’ core:print)))
  (void))

where the repeated out identifier, for example, is created with generate-temporary.


Edit: one could also write the above without "pushing", and instead "pulling":

(`#:x `#:y `#:z
  #:z pop
  #:y dup sqr swap
  #:x sqr
  #:y abs swap
  #: ,#:y ,#:x add ,#:y sub)

Although the uses of swap are kind of meh.

1 Like