diff --git a/asm/asm.fnl b/asm/asm.fnl index 0cf8450..b1f37fc 100644 --- a/asm/asm.fnl +++ b/asm/asm.fnl @@ -251,15 +251,16 @@ (set self.dbgfile nil)) self) :read-hotswap - (fn [self machine] + (fn [self machine prg-new] (let [addr-to-label {} addr-to-size {}] (each [_ block (pairs self.org-to-block)] (each [label pdat (pairs block.preserved)] (tset addr-to-label pdat.addr label) (tset addr-to-size pdat.addr pdat.size))) - (collect [addr bytes (pairs (machine:read-batch addr-to-size))] - (values (. addr-to-label addr) bytes)))) + (lume.merge (collect [addr bytes (pairs (machine:read-batch addr-to-size))] + (values (. addr-to-label addr) bytes)) + (if (?. self.source :read-hotswap) (self.source:read-hotswap machine prg-new) {})))) :write-hotswap (fn [self machine hotswap] (machine:write-batch diff --git a/link/mame.fnl b/link/mame.fnl index e362315..5660d7e 100644 --- a/link/mame.fnl +++ b/link/mame.fnl @@ -174,7 +174,7 @@ (self:set-bp addr #(util.in-coro (fn [] (self:clear-bp addr) - (local hotswap (prg-old:read-hotswap self)) + (local hotswap (prg-old:read-hotswap self prg-new)) (prg-new:upload self) (prg-new:write-hotswap self hotswap) (self:jump (prg-new:lookup-addr :on-hotswap)) diff --git a/ssc/hotswap.fnl b/ssc/hotswap.fnl new file mode 100644 index 0000000..6bb088f --- /dev/null +++ b/ssc/hotswap.fnl @@ -0,0 +1,119 @@ +; hotswap support +(local {: addr-parser} (require :asm.65816)) +(local util (require :lib.util)) +(local {: bytes-to-uint16 : bytes-to-uint24 : int16-to-bytes : int24-to-bytes} util) + +; Hotswap theory: +; The common case is code moving around in memory; even if you are not changing the content of code that +; is currently executing, any changes to anything mean that pointers to that code will need to change. +; If you try not to store pointers in globals (when everything is statically allocated, you should explicitly +; name things, like in Forth) then the main place that persistent pointers to code exist is in call stacks - +; specifically, the values on the stack consumed by "rts" or "rtl". When hotswapping, we need to walk the +; callstack and A. find these values, and B. patch them to their new values. For this, we need to be able to +; map any address that might be returned to from the old program to the new program - we can easily do this +; by generating symbols for each callsite and setting up a reverse lookup table. We _also_ need to know the +; stack layout at the time of each call so that we can find the next link in the chain, and to verify that +; this hasn't changed (if the function has changed enough, then we can't modify it mid-execution). + +(fn assert-local-matches [funcname loc-old loc-new] + (each [_ key (ipairs [:type :name :returnaddr])] + (assert (= (. loc-old key) (. loc-new key)) (.. "Stack mismatch when patching " funcname)))) + +(fn assert-locals-match [funcname locals-old locals-new] + (assert (= (length locals-old) (length locals-new)) (.. "Stack size mismatch when patching " funcname)) + (each [iloc loc-old (ipairs locals-old)] + (assert-local-matches funcname loc-old (. locals-new iloc)))) + +(fn next-callsite-offset [locals] + (var offset 0) + (var start-counting false) + (each [_ loc (ipairs locals)] + (if start-counting (let [size (match loc.type :placeholder 0 :word 2 :long 4)] (set offset (+ offset size))) + loc.returnaddr (set start-counting true))) + offset) + +(fn next-callsite-far? [locals] + (var far? false) + (each [_ loc (ipairs locals)] + (when (and loc.returnaddr (= loc.type :long)) + (set far? true))) + far?) + +(fn read-callsite-addr [stack bank far] + (if far (bytes-to-uint24 stack 1) ; lowest byte (top of stack) is preserved B register + (bit.bor (bytes-to-uint16 stack) (bit.lshift bank 16)))) + +(fn lookup-callsite [ssc addr] + (if (= addr (- (ssc.prg:lookup-addr :yield-forever) 1)) + {:callsite-sym :yield-forever :locals [] :calling :yield-forever :funcname ""} + (. ssc.addr-to-callsite addr))) + +(fn patch-stack [ssc-old ssc-new stack bank far] + (print (length stack) bank far) + (if (= (length stack) 0) stack + ; top-of-stack should be a callsite; look it up + (let [callsite-addr (read-callsite-addr stack bank far) + {: callsite-sym : locals : funcname} (assert (lookup-callsite ssc-old callsite-addr) + (.. "Top of stack value " callsite-addr " is not a recognized callsite")) + new-addr (- (ssc-new.prg:lookup-addr callsite-sym) 1) + {:locals new-locals} (lookup-callsite ssc-new new-addr) + new-bank (bit.rshift new-addr 16) + _ (when (not far) (assert (= (bit.band callsite-addr 0xff0000) (bit.band new-addr 0xff0000)) + (.. funcname " moved banks from " bank " to " new-bank))) + _ (print (.. "patching " callsite-sym " from " callsite-addr " to " new-addr)) + _ (assert-locals-match funcname locals new-locals) + new-top (if far (.. (stack:sub 1 1) (int24-to-bytes new-addr)) + (int16-to-bytes new-addr)) + iaftertop (if far 5 3) + inextstack (+ iaftertop (next-callsite-offset locals)) + _ (print (.. "locals " (fv locals) " offset " (next-callsite-offset locals)) inextstack)] + (.. new-top (stack:sub iaftertop (- inextstack 1)) + (if (= funcname ssc-old.prg.start-symbol) (stack:sub inextstack) ; stop when we hit the boot-up function + (patch-stack ssc-old ssc-new (stack:sub inextstack) new-bank (next-callsite-far? locals))))))) + +(fn split-equally [s size] + (values (fn [s iprev] (let [i (+ iprev 1) + istart (+ (* (- i 1) size) 1) + iend (* i size)] + (when (>= (length s) iend) (values i (s:sub istart iend))))) + s 0)) + +(fn read-stacks [link ssc] + (let [stack-bounds-addr (ssc.prg:lookup-addr :first-task) + stack-bounds-bytes (link:read stack-bounds-addr 4) + first-task (bytes-to-uint16 stack-bounds-bytes) + last-task (bytes-to-uint16 stack-bounds-bytes 2) + task-size 0x100 + read-size (+ (- last-task first-task) task-size) + task-count (/ read-size task-size) + task-bytes (link:read first-task read-size) + sp-offset (addr-parser ssc.TASK-STACK)] + (icollect [_ task (split-equally task-bytes task-size)] + (let [sp-addr (bytes-to-uint16 task sp-offset) + istackstart (bit.band sp-addr 0xff) + stack (task:sub (+ istackstart 2))] + {:sp-addr (+ sp-addr 1) : stack})))) + +(fn lookup-yield-bank [ssc] (bit.rshift (ssc.prg:lookup-addr :yield) 16)) +(fn filter-nonyielding-stacks [ssc stacks] + (let [yield-bank (lookup-yield-bank ssc)] + (print "yield-bank" yield-bank) + (icollect [_ stack (ipairs stacks)] + (let [callsite-addr (read-callsite-addr stack.stack yield-bank false) + callsite (lookup-callsite ssc callsite-addr) + funcname (?. callsite :calling)] + (print "top callsite:" (string.format "%X" callsite-addr) (fv callsite)) + (when (= funcname :yield) stack))))) + +(fn hotswap-stacks [link ssc-old ssc-new] + (print "hotswappin time" link ssc-old ssc-new) + (let [stacks (read-stacks link ssc-old) + _ (print "read stacks" (fv stacks)) + yielding-stacks (filter-nonyielding-stacks ssc-old stacks)] + (print "filtered stacks" (length stacks) (length yielding-stacks)) + (collect [_ {: sp-addr : stack} (ipairs yielding-stacks)] + (let [(success new-stack) (pcall #(patch-stack ssc-old ssc-new stack (lookup-yield-bank ssc-old) false))] + (if success (do (print "patched" (stack:tohex) "to" (new-stack:tohex)) (values (tostring sp-addr) new-stack)) + (error (.. new-stack ": stack at " (string.format "%X" sp-addr)))))))) + +{: hotswap-stacks} diff --git a/ssc/iigs/bootstub.fnl b/ssc/iigs/bootstub.fnl index 7503506..5dc4f93 100644 --- a/ssc/iigs/bootstub.fnl +++ b/ssc/iigs/bootstub.fnl @@ -3,13 +3,16 @@ #(compile $1 (start-symbol boot) + (predef-fn boot () void far) + (predef-fn [(or $2 :main)] () void far) + [(when (not= link.name :udpdebug) (! (do ;udpdebug boots into 16-bit mode (start-symbol boot-8) (org 0x1000) (fn boot-8 () - (asm (clc) (xce) (rep 0x30) ; disable emulation mode, 16-bit index registers - (jsr boot) - (sec) (xce))) ; re-enter emulation mode + (asm (clc) (xce) (rep 0x30)) ; disable emulation mode, 16-bit index registers + (boot) + (asm (sec) (xce))) ; re-enter emulation mode )))] (org 0x060000) @@ -42,7 +45,7 @@ (set! BootHandle-e0 (NewHandle 0x4000 BootUserID 0xb017 0xe02000)) (set! BootHandle-e1 (NewHandle 0x8000 BootUserID 0xb017 0xe12000)) - (asm (jsl [(or $2 :main)])) + ( [(or $2 :main)] ) (DisposeHandle BootHandle-e1) (DisposeHandle BootHandle-e0) diff --git a/ssc/init.fnl b/ssc/init.fnl index 0d2e982..29d395d 100644 --- a/ssc/init.fnl +++ b/ssc/init.fnl @@ -207,7 +207,7 @@ (fn Ssc.compile-function-generic [self name args body post-body returnaddr-type call-instruction] (let [arglocals (self:parse-parameters args)] - (self:define-fn name (lume.concat arglocals [{:type returnaddr-type :comment :returnaddr}]) + (self:define-fn name (lume.concat arglocals [{:type returnaddr-type :returnaddr true}]) #(let [(c-function etype) (self:expr-poly body)] (self.org:append name c-function (table.unpack post-body)) {:arity (length args) :args arglocals :org self.org :type etype : name : call-instruction})))) @@ -482,8 +482,9 @@ locals (lume.clone self.locals) callid (or (. self.callsites f.name) 0) _ (tset self.callsites f.name (+ callid 1)) - callsite-sym (.. "") - capture-addr (fn [addr] (tset self.addr-to-callsite addr {: callsite-sym : locals})) + funcname self.defining-fn + callsite-sym (.. "") + capture-addr (fn [addr] (tset self.addr-to-callsite (- addr 1) {: callsite-sym : locals : funcname :calling f.name})) post (icollect [_ (countiter (length args))] (self:drop))] (values (lume.concat [:block] pre [[f.call-instruction f.name] callsite-sym [:export callsite-sym] [:meta capture-addr]] post) f.type))) @@ -541,6 +542,11 @@ (fn Ssc.assemble [self] (self.prg:assemble) + (set self.prg.source self) self.prg) +(fn Ssc.read-hotswap [self machine prg-new] + (local {: hotswap-stacks} (require :ssc.hotswap)) + (hotswap-stacks machine self prg-new.source)) + Ssc diff --git a/ssc/task.fnl b/ssc/task.fnl index 0496c13..9f927e2 100644 --- a/ssc/task.fnl +++ b/ssc/task.fnl @@ -24,7 +24,8 @@ [:tsc] [:and 0xff] [:ora task-base] [:tcs]])]) (fn yield () - (asm (tsc) (sta [$1.TASK-STACK]) + (asm (tsc) (sta [$1.TASK-STACK]) ; + debug-stub on-hotswap (export debug-stub) (export on-hotswap) ; todo: cleanup mame hotswap logic (lda [$1.TASK-NEXT]) (tcd) (lda [$1.TASK-STACK]) (tcs)))