honeylisp/ssc/init.fnl

403 lines
19 KiB
Plaintext
Raw Normal View History

; ssc: the sufficiently simple compiler
; The goal of ssc is to allow simple prefix expressions to be compiled into 65816 code that
; would run at least as fast or faster than the equivalent threaded Forth code. Complex
; optimizations are a non-goal; if you want to tune the generated code, go ahead and write
; the assembly directly.
; * 3 data types: word (2 bytes), long (4 bytes), void (0 bytes).
; * Expressions return their results in different places depending on type - word values are stored in the A register,
; long values are stored in the direct page at LONG_LO / LONG_HI.
; * Data and return addresses are mixed on one stack, unlike Forth.
; * Function calls take a fixed number of arguments, and return 0 or 1 results. The compiler enforces arity checking.
; * To call a function taking arguments [arg1 arg2 arg3], all 3 arguments should be pushed to the stack before calling.
; When the function takes control, the stack should look like this:
; arg1 arg2 arg3 return-address
; * The caller is responsible for removing all arguments from the stack once the function returns.
; * The caller is responsible for preserving the A, X and Y registers, if this is desirable.
; * If the function returns a value, it is stored in the A/LONG register, like any expression.
; * If a function returns no result, it is not obliged to preserve the A/LONG register.
; * Multitasking is achieved by overlapping the D and S registers on the same 256-byte page of memory.
; Yielding to a new task involves saving the S register, setting the D register to the new task's page,
; then setting the S register to the saved value in the old task.
; * Useful task-local "registers" are kept at the beginning of the page, and the stack grows down from the end of the page.
; * DP register list:
; * LONG (32-bit "register")
; * Last suspended value of S
; * Mailbox
; * Pointer to next task
; Compiler notes:
; Expressions are of the form [:function arg1 arg2 arg3]
; args are either strings (symbols) or numbers
; TODO:
; * implement global definitions
; * fix comparisons
2021-08-02 03:26:51 +00:00
; * implement loops
; * implement "getters" (subroutine that runs when referenced by name without an explicit call)
(import-macros {:sss ! : compile} :ssc.macros)
(local Object (require :core.object))
(local lume (require :lib.lume))
(local Ssc (Object:extend))
(local Prg (require :asm.asm))
(local util (require :lib.util))
2021-08-09 18:43:24 +00:00
(local {: loword : hiword} util)
(set Ssc.LONG_LO :d0x00)
(set Ssc.LONG_HI :d0x02)
(fn Ssc.new [self ?opts]
(local opts (or ?opts {}))
(set self.prg (or opts.prg (Prg.new nil :65816)))
(set self.forms (lume.clone (or opts.forms self.__index.forms)))
(set self.functions {})
(set self.locals [])
(set self.modules {})
(set self.globals {})
(if opts.boot (self:compile (table.unpack opts.boot))
(self:compile (!
(start-symbol boot)
(org [(or opts.boot-org 0)])
(asm
boot
(clc) (xce) (rep 0x30) ; disable emulation mode, 16-bit index registers
(jsr main)
(sec) (xce) ;re-enter emulation mode
(rts))))))
2021-08-09 18:43:24 +00:00
(fn Ssc.push [self name expr ?etype]
(local etype (or ?etype :word))
(table.insert self.locals {: name :type etype})
(match etype
2021-08-09 18:43:24 +00:00
:word [:block (or expr [:flatten]) [:pha]]
:long [:block (or expr [:flatten]) [:lda self.LONG_HI] [:pha] [:lda self.LONG_LO] [:pha]]
_ (error (.. "Unknown stack type " (tostring etype)))))
(fn Ssc.remove-local [self ?name]
(let [loc (. self.locals (length self.locals))]
(when (not= loc.name ?name) (error (.. "Internal stack error: expected " (or ?name "temporary") ", got " (or loc.name "temporary"))))
(tset self.locals (length self.locals) nil)
loc))
(fn Ssc.drop [self ?name]
(match (. (self:remove-local ?name) :type)
:word [:ply]
:long [:block [:ply] [:ply]]))
(fn Ssc.pop [self ?name]
(let [{:type etype} (self:remove-local ?name)]
(values (match etype
:word [:pla]
:long [:block [:pla] [:sta self.LONG_LO] [:pla] [:sta self.LONG_HI]])
etype)))
(fn Ssc.was-dropped [self localcount]
(set self.locals (lume.slice self.locals 1 (- (length self.locals) localcount))))
(fn Ssc.defining? [self] (> (length self.locals) 0))
(fn countiter [minmax ?max]
(let [min (if ?max minmax 1)
max (or ?max minmax)]
(fn [_ iprev]
(let [i (if iprev (+ iprev 1) min)]
(when (<= i max) i)))))
(fn pairoff [l]
(fn [_ iprev] (let [i (if iprev (+ iprev 2) 1)]
(when (< i (length l)) (values i (. l i) (. l (+ i 1)))))))
; Comparison theory:
; word x word constant -> word
; long x long constant -> word
; word x word -> word
; long x long -> word
; Any combination of word and long gets promoted to long (we have to compare both words anyway).
; this should hold for beq and bpl, but not bmi and bne
; optimized cases:
; reg + immediate
; reg + stack? nope, reg + stack isn't really possible when the operator isn't commutative
; stack + reg
(fn boolop [self left right branch]
(let [compiled-left (self:expr-word left)
2021-08-02 03:26:51 +00:00
push-left (when (not= (type right) :number) (self:push))
compiled-compare (if (not push-left) [:cmp right]
[:block push-left (self:expr-word right) [:cmp 1 :s]])
2021-08-02 03:26:51 +00:00
drop-left (when push-left (self:drop))]
[:block
compiled-left
compiled-compare
[branch :-true-]
2021-08-02 03:26:51 +00:00
[:lda 0]
[:bra :-finished-]
:-true-
[:lda 0xffff]
2021-08-02 03:26:51 +00:00
:-finished-
drop-left]))
; operations that work on the accumulator, like adc or sbc
; optimization strategy: keep the current result in the accumulator, work from the stack or immediate values
; 1. take "right" arguments and push them (unless already on stack, immediate, or absolute)
; 2. load left into accumulator
; 3. apply until done
(fn Ssc.accumulation-op [self op first ...]
(var etype (self:type-expr first))
(for [i 1 (select :# ...)] (when (= (self:type-expr (select i ...)) :long) (set etype :long)))
(let [args (icollect [_ val (ipairs [...])] (self:push-opgen val))
setup (icollect [_ {: setup} (ipairs args)] (when setup (setup)))
acc (: self (.. :expr- etype) first)
operations (icollect [i addr (ipairs args)] (op etype addr i))
cleanup (icollect [_ {: cleanup} (ipairs args)] (when cleanup (cleanup)))]
(values (lume.concat [:block] setup [acc] operations cleanup) etype)))
(set Ssc.forms
{:asm (fn [self ...] (if (self:defining?) [:block ...] (self.org:append ...)))
:asm-long (fn [self ...] (values [:block ...] :long))
2021-08-02 03:26:51 +00:00
:org (lambda [self org] (set self.org (self.prg:org org)))
:start-symbol (lambda [self symbol] (set self.prg.start-symbol symbol))
:form (lambda [self name func] (tset self.forms name func))
:require (lambda [self name]
(when (= (. self.modules name) nil)
(let [mod (util.reload name)
func (if (= (type mod) :function) mod mod.module)]
(tset self.modules name mod)
(func self))))
:do (fn [self ...]
(var etype-body :void)
(local c-body (lume.concat [:block] (icollect [i (countiter (select :# ...))]
(let [(expr etype) (self:expr-poly (select i ...))]
(set etype-body etype)
expr))))
(values c-body etype-body))
:let (fn [self bindings ...]
(let [compiled-bindings (icollect [_ symbol expr (pairoff bindings)] (self:push symbol (self:expr-poly expr)))
(compiled-body etype) (self:expr-poly [:do ...])
compiled-cleanup (icollect [i-half (countiter (/ (length bindings) 2))]
(self:drop (. bindings (- (length bindings) (* i-half 2) -1))))]
(values (lume.concat [:block] compiled-bindings [compiled-body] compiled-cleanup) etype)))
:fn (lambda [self name args ...]
(assert (not (self:defining?)) "Can't nest function definitions")
2021-08-09 18:43:24 +00:00
(local arglocals (self:parse-parameters args))
(set self.locals (lume.concat arglocals [{:type :word :comment :returnaddr}]))
; todo: maybe handle mutually recursive functions? (compile-expr only has access to currently-defined functions)
(local (c-function etype) (self:expr-poly [:do ...]))
(self.org:append name c-function [:rts])
(tset self.functions name {:arity (length args) :args arglocals :org self.org :type etype})
(assert (= (length self.locals) (+ (length args) 1))
(.. "Left locals on stack?? Expected " (tostring (+ (length args) 1)) " got " (tostring (length self.locals))))
(set self.locals []))
:if (lambda [self test iftrue ?iffalse]
(let [c-test (self:expr-word test)
(c-true truetype) (self:expr-poly iftrue)
(c-false falsetype) (when ?iffalse (self:expr-poly ?iffalse))
etype (if (and falsetype (not= truetype falsetype)) :void truetype)]
(values (lume.concat [:block
c-test
[:cmp 0] [:beq (if ?iffalse :-elseblock- :-finished-)]
c-true]
(when ?iffalse [
[:bra :-finished-]
:-elseblock- c-false])
[:-finished-]) etype)))
:+ (lambda [self first ...]
(self:accumulation-op
(fn [etype {: lo : hi : const}]
(match etype
:word (if (= const 1) [:inc] (= const 2) [:block [:inc] [:inc]]
(= const -1) [:dec] (= const -2) [:block [:dec] [:dec]]
[:block [:clc] (lo :adc)])
:long [:block [:clc] (lo :lda) [:adc self.LONG_LO] [:sta self.LONG_LO]
(if hi (hi :lda) [:lda 0]) [:adc self.LONG_HI] [:sta self.LONG_HI]]))
first ...))
:- (lambda [self first ...]
(self:accumulation-op
(fn [etype {: lo : hi : const}]
(match etype
:word (if (= const 1) [:dec] (= const 2) [:block [:dec] [:dec]]
(= const -1) [:inc] (= const -2) [:block [:inc] [:inc]]
[:block [:sec] (lo :sbc)])
:long [:block [:sec] (lo :lda) [:sbc self.LONG_LO] [:sta self.LONG_LO]
(if hi (hi :lda) [:lda 0]) [:sbc self.LONG_HI] [:sta self.LONG_HI]]))
first ...))
:= (lambda [self lhs rhs] (boolop self lhs rhs :beq))
:not= (lambda [self lhs rhs] (boolop self lhs rhs :bne))
2021-08-02 03:26:51 +00:00
:< (lambda [self lhs rhs] (boolop self lhs rhs :bmi))
:> (lambda [self lhs rhs] (boolop self rhs lhs :bmi))
2021-08-02 03:26:51 +00:00
:>= (lambda [self lhs rhs] (boolop self lhs rhs :bpl))
:<= (lambda [self lhs rhs] (boolop self rhs lhs :bpl))
:not (lambda [self bool] (self:expr-poly [:if bool 0 0xffff]))
:or (lambda [self lhs rhs] (self:expr-poly [:if lhs 0xffff [:if rhs 0xffff 0]]))
:and (lambda [self lhs rhs] (self:expr-poly [:if lhs [:if rhs 0xffff 0] 0]))
2021-08-09 18:43:24 +00:00
:loword (lambda [self long] [:block (self:expr-long long) [:lda self.LONG_LO]])
:hiword (lambda [self long] [:block (self:expr-long long) [:lda self.LONG_HI]])
:ref (lambda [self label] [:lda #(loword ($1:lookup-addr label))])
:far-ref (lambda [self label] (values [:block [:lda #(loword ($1:lookup-addr label))] [:sta self.LONG_LO]
[:lda #(hiword ($1:lookup-addr label))] [:sta self.LONG_HI]] :long))
; TODO: maybe handle a few different addressing modes here? re-use if the value is already on the stack?
; TODO: automatically handle far-ref
:word! (lambda [self ref value] [:block (self:push nil (self:expr-word ref) :word) (self:expr-word value) [:ldy 0] [:sta [1 :s] :y] (self:drop)])
:long! (lambda [self ref value] [:block (self:push nil (self:expr-word ref) :word)
(self:expr-long value) [:ldy 0] [:lda self.LONG_LO] [:sta [1 :s] :y] [:iny] [:iny] [:lda self.LONG_HI] [:sta [1 :s] :y]
(self:drop)])
:word-at (lambda [self ref]
(local (c-ref etype) (self:expr-poly ref))
(if (= etype :word)
[:block (self:push nil c-ref :word) [:ldy 0] [:lda [1 :s] :y] (self:drop)]
(= etype :long)
[:block c-ref [:ldy 0] [:lda [[self.LONG_LO]] :y]]))
:long-at (lambda [self ref]
(local (c-ref etype) (self:expr-poly ref))
(if (= etype :word)
[:block (self:push nil c-ref :word) [:ldy 0] [:lda [1 :s] :y] [:sta self.LONG_LO] [:iny] [:iny] [:lda [1 :s] :y] [:sta self.LONG_HI] (self:drop)]
(= etype :long)
[:block c-ref [:ldy 0] [:lda [[self.LONG_LO]] :y]] [:tax] [:iny] [:iny] [:lda [[self.LONG_LO]] [:sta self.LONG_HI] [:stx self.LONG_LO]]))
:set! (lambda [self lhs value]
(let [(c-value etype) (self:expr-poly value)
{: lo : hi} (self:opgen-lhs lhs)
c-lo (match etype
:word (lo :sta)
:long [:block [:lda self.LONG_LO] (lo :sta)])
c-hi (when hi (match etype
:word [:block [:lda 0] (hi :sta)]
:long [:block [:lda self.LONG_HI] (hi :sta)]))
block [:block]]
(pp c-value)
(lume.push block c-value c-lo c-hi)
block))
})
(fn Ssc.local-offset [self name-or-index]
(var offset nil)
2021-08-09 18:43:24 +00:00
(var stacklen 0)
(for [i 1 (length self.locals)]
2021-08-09 18:43:24 +00:00
(let [loc (. self.locals i)
size (match loc.type :word 2 :long 4 _ (error (.. "how big is this local??" (fv loc))))]
(set stacklen (+ stacklen size))
(when (or (= i name-or-index) (= loc.name name-or-index))
(set offset stacklen))))
(when offset (+ (- stacklen offset) 1)))
(fn Ssc.local-type [self name-or-index]
(var etype nil)
(for [i 1 (length self.locals)]
(when (or (= i name-or-index) (= (. self.locals i :name) name-or-index))
(set etype (. self.locals i :type))))
etype)
(fn Ssc.set-long [self loexpr hiexpr]
[:block loexpr [:sta self.LONG_LO] hiexpr [:sta self.LONG_HI]])
(fn Ssc.set-long-local [self loc]
(self:set-long [:lda (self:local-offset loc) :s] [:lda (+ (self:local-offset loc) 2) :s]))
(fn Ssc.type-expr [self expr] (let [(_ etype) (self:expr-poly expr)] etype))
; opgen - a small structure that allows for reading a value with many different addressing modes
; :lo and :hi keys are functions that, when called with an opcode, returns that opcode with the appropriate argument to work on
; either the low or high word. If :hi does not exist in the structure, then the value represented by the opgen is only word-sized.
; :setup and :cleanup keys are used by push-opgen to handle generation of the necessary stack manipulation instructions.
; opgen-const makes the constant available in the :const key so it can be checked and potentially optimized further (+1 -> inc)
(fn Ssc.opgen-const [self const]
{:lo #[$1 (bit.band const 0xffff)] :hi (let [hi (bit.rshift (bit.band const 0xffff0000) 16)] (if (or (= hi 0) (= hi 0xffff)) nil #[$1 hi])) : const})
(fn Ssc.opgen-local [self loc]
{:lo #[$1 (self:local-offset loc) :s] :hi (when (= (self:local-type loc) :long) #[$1 (+ (self:local-offset loc) 2) :s])})
(fn Ssc.opgen-symbol [self name etype]
{:lo #[$1 name] :hi (when (= etype :long) #[:block [:ldy 2] [$1 name :y]])}) ; this is stupid - the assembler should be able to calculate addr + 2
(fn Ssc.opgen-global [self name] (self:opgen-symbol name (. self.globals name :type)))
(fn Ssc.opgen-ref-loc [self name etype]
(when (= (self:local-type name) :word) ; long pointer deref is not possible directly from the stack; have to eval and move to LONG register
{:lo #[:block [:ldy 0] [$1 [(self:local-offset name) :s] :y]]
:hi (when (= etype :long) #[:block [:ldy 2] [$1 [(self:local-offset name) :s] :y]])}))
(fn Ssc.opgen-ref-global [self name etype]
(match (. self.globals name :type)
:word {:lo #[:block [:ldy 0] [$1 [name] :y]] :hi (when (= etype :long) #[:block [:ldy 2] [$1 [name] :y]])}
:long {:lo #[:block [:ldy 0] [$1 [[name]] :y]] :hi (when (= etype :long) #[:block [:ldy 2] [$1 [[name]] :y]])}))
(fn string? [v] (= (type v) :string))
(fn Ssc.opgen-lhs [self expr]
(match [(type expr) expr]
[:string _] (if (self:local-offset expr) (self:opgen-local expr)
(. self.globals expr) (self:opgen-global expr))
(where [_ [:word-at [:ref name]]] (string? name)) (self:opgen-symbol name :word)
(where [_ [:long-at [:ref name]]] (string? name)) (self:opgen-symbol name :long)
(where [_ [:word-at name]] (string? name)) (if (self:local-offset name) (self:opgen-ref-loc name :word)
(. self.globals name) (self:opgen-ref-global name :word))
(where [_ [:long-at name]] (string? name)) (if (self:local-offset name) (self:opgen-ref-loc name :long)
(. self.globals name) (self:opgen-ref-global name :long))))
(fn Ssc.opgen [self expr]
(if (= (type expr) :number) (self:opgen-const expr) (self:opgen-lhs expr)))
(fn Ssc.push-opgen [self expr]
(or (self:opgen expr)
(let [c (self:push nil (self:expr-poly expr))
iloc (length (self.locals))]
(lume.merge (self:opgen-local iloc) {:setup #c :cleanup #(self:drop)}))))
2021-08-09 18:43:24 +00:00
(fn Ssc.parse-parameters [self params]
(icollect [_ param (ipairs params)] (match param
[:long pname] {:name pname :type :long}
pname {:name pname :type :word})))
(fn Ssc.push-arguments [self paramdefs args]
(icollect [iarg arg (ipairs args)]
(let [atype (. paramdefs iarg :type)
c-arg (: self (.. :expr- atype) arg)
c-push (self:push nil c-arg atype)]
c-push)))
(fn Ssc.expr-poly [self expr]
(match expr
(where lit (?. (self:opgen lit) :hi)) (let [{: lo : hi} (self:opgen lit)]
(values [:block (lo :lda) [:sta self.LONG_LO] (hi :lda) [:sta self.LONG_HI]] :long))
(where lit (?. (self:opgen lit) :lo)) (let [{: lo} (self:opgen lit)] (values (lo :lda) :word))
; TODO: Global scope
(where [func & args] (= (?. self.functions func :arity) (length args)))
(let [f (. self.functions func)
2021-08-09 18:43:24 +00:00
pre (self:push-arguments f.args args)
post (icollect [_ (countiter (length args))] (self:drop))]
(print (fv pre) (fv post) (fv args))
(values (lume.concat [:block] pre [[:jsr func]] post) f.type))
(where [form & args] (. self.forms form))
(let [f (. self.forms form)
(cexpr etype) (f self (table.unpack args))]
(values cexpr (or etype :word)))
nil (values [:block] :void)
_ (error (.. "Unrecognized expression " (fv expr)))))
(fn Ssc.expr-word [self expr]
(let [(c etype) (self:expr-poly expr)]
(when (not= etype :word) (error "Unexpected long or void in " (fv expr) " - please wrap in explicit truncation form"))
c))
(fn Ssc.expr-long [self expr]
(let [(c etype) (self:expr-poly expr)]
(match etype
:long c
:word [:block c [:sta self.LONG_LO] [:lda 0] [:sta self.LONG_HI]]
_ (error "Unexpected type " etype " in " (fv expr) " - wanted long or word"))))
(fn Ssc.compile [self ...]
(for [i 1 (select :# ...)]
(self:expr-poly (select i ...)))
self)
2021-08-02 03:26:51 +00:00
(fn Ssc.assemble [self]
(self.prg:assemble)
self.prg)
Ssc