Skip to content

Instantly share code, notes, and snippets.

@deplinenoise
Forked from emoon/Asm
Created July 30, 2014 21:36
Show Gist options
  • Save deplinenoise/791b00b6124e62706582 to your computer and use it in GitHub Desktop.
Save deplinenoise/791b00b6124e62706582 to your computer and use it in GitHub Desktop.
; Pure Lisp based assembler
(def my-fun (input1 :in d1
input2 :in d2
output :out d0)
(move.l input1 output)
(add.l input2 output))
----------------------------------------------------------------------------------------
; We can also have loop constructions by doing a macro (no need for it to be 'built-in')
(def my-fun (input1 :in d1
input2 :in d2
output :out d0)
(loop for input1 to input2 do ; macro can in compile time look at body and figure out how to do the loop in the best way
(add.l input output)))
-------------------------------------------------------------------------------------------------------------------------
; This works quite well for simple instructions (would fit great far 6502) but 68k has more complex addressing modes which
; makes this a bit annoying
;
; Example: move.l d0,(a0)+ ; move register d0, to where a0 is pointing and bump the pointer according to the move size (here 4)
(move.l d0 a0+) ; maybe (move.l d0 (a0+)) would work but it becomes a bit ackward.
---------------------------------------------------------------------------------------------------------
; maybe a mixed approach is better (notice that syntax may not be 100% valid, just really playing around)
(def my-fun (input1 :in d1
input2 :in d2
array :in a0
output :out d0)
add.l input1,input2
move.l input2,output
(loop for input1 to input2 do
move.l input,(array)+))
---------------------------------------------------------------------------------------------------------------------------
; thoughs:
; Julia has some nice metaprogramming stuff which comes from lisp but isn't s-exp http://docs.julialang.org/en/latest/manual/metaprogramming/ maybe ideas can be used from here)
; KickAssembler can be used as insperation as well http://www.theweb.dk/KickAssembler/webhelp/content/cpt_FunctionsAndMacros.html
---------------------------------------------------------------------------------------------------------------------------
; Structs
; using structs in lisp is a bit tire some because the way you write the access. GOAL uses -> which is fine I guess. Here is some GOAL code (a bit formated in more Lisp style)
(defun sparticle-red-2-converge ((sp-system sparticle-system) (particle sparticle-cpuinfo) (spvec sprite-vec-data-2d))
"Red gun 2 charge glow effect"
(let ((control (the sparticle-launch-control (-> particle key))))
;; figure out the offset from the player's position
(let ((origin-vec (stack-vector))
(offset-vec (stack-vector)))
;; retrieve offset vec off of userdata, omega. We only had 8 bytesfor data storage,
;; but needed to store a full vec3, so we packed it into fixed point.
(let ((combined-vec (new 'stack 'vector4w)))
(set! (-> combined-vec x) (-> particle user-uint32))
(set! (-> combined-vec y) (-> particle data 0))
(rlet ((offset :reg vf1)
(preconverted :reg vf2)
(iconv0)
(src))
(l.q src combined-vec) ;; GOAL->EE
(mer.vh lo iconv0 src r0) ;; EE
(sr.vw iconv0 iconv0 16) ;; EE
(m preconverted iconv0) ;; EE->VU0
;; (format *stderr* "Value is ~d~%" iconv0) ;; GOAL
;; VU ftoi15
(cvt.s.w offset preconverted :fixed 12) ;; VU0
(s.q offset offset-vec))) ;; VU0->GOAL
;; figure out distance
(let ((time-remaining (-> particle timer))
(distance 0.0)
(tt 0.0))
(set! tt (/ (the float (-> particle timer)) (the float (frame-time90))))
(*! tt tt )
(set! tt (- 1.0 tt))
(vector-normalize! offset-vec (lerp PARTICLE_RED_2_CHARGE_SIZE (meters 0) tt))
(vector-copy! origin-vec (-> *target* gun fire-point))
(set! (-> spvec trans x) (+ (-> offset-vec x) (-> origin-vec x)))
(set! (-> spvec trans y) (+ (-> offset-vec y) (-> origin-vec y)))
(set! (-> spvec trans z) (+ (-> offset-vec z) (-> origin-vec z)))))))
--------------------------------------------------------------------------------
I would actually prefer some other way here to do structs and access data in a nice fasihon, something along the line:
struct Foo
{
.long meh
.long foo
}
register a0 is of type Foo;
move.l (a0).foo,d0 ; would translate to move.l 4(a0),d0
; also very basic syntax transforms like this can be used if wanted then
(a0).foo = d0
@deplinenoise
Copy link
Author

Couple of notes:

What about constants? :in, :out could be regs (or more likely implied by ABI if not set) and :const could be template-like constant to hardwire into loops etc.

+a0 and a0+ are both valid lisp symbols; option would be to use (move.l a1 (++ a0)) which is more flexible and would also work better with auto-regs:

(move.l foo (++ bar))

Same thing with movem.l:

(movem.l (foo bar baz a7) (-- stash))

Struct offsets could be done similarily:

(move.l (fooptr :bar) outreg)

So I guess cooking the rules down:

basic-reg ::= a0 | a1 | a2 | ...
auto-reg ::= <any symbol>
register-ref ::= basic-reg | auto-reg
incdec-op ::= ++ | --
incdec-reg ::= ( incdec-op register-ref )
deref ::= ( register-ref ) | ( register-ref number ) | (register-ref kw-symbol )

You could say things like:

((move.l a0 a1) ;; basic-reg basic-reg
 (move.l foo a7) ;; auto-reg basic-reg
 (move.l (a0) (++ a1)) ;; deref incdec-reg
 (move.l (ptr :foo) d3) ;; deref basic-reg
 (move.l (ptr 8) (d3))) ;; deref basic-reg (same thing as above if foo is at offset 8)

@emoon
Copy link

emoon commented Jul 31, 2014

Cool.

This actually doesn't look that bad. I think I will write some code with it and see how well it works. Might be interesting to look at some of the more complex instructions also. Now when I think about it I actually may just skip them (those advance 020+ things you can do with { ... } and such as they are Slower/emulated on 060 anyway)

@emoon
Copy link

emoon commented Jul 31, 2014

Also good point about constants. And yeah if you don't specify anything as input it should use the 68k ABI but quite quite often when you write (pure) assembly code it's very convenient (similar to vbcc)

void foo(__reg("a2") void* meh)

To specify the input.

@emoon
Copy link

emoon commented Jul 31, 2014

Implemented a "pseudo" integer vertex rot:

(defun-68k integer-rotate-verts (input :reg a0
                                 output :reg a1
                                 matrix :reg a2 as MatrixInt
                                 count :reg d7)
    (loop dec count
        (move.w (++ input) x)
        (move.w (++ input) y)
        (move.w (++ input) z)

        (move.l x temp_x)
        (move.l y temp_y)
        (move.l z temp_z)

        ; Rotate Z

        (muls.w (matrix :row2-x) x)
        (muls.w (matrix :row2-y) y)
        (muls.w (matrix :row2-z) z)

        (add.l x z)
        (add.l y z)
        (add.w (matrix :trans-z) z) 
        (move.l z rot_z)

        ; Rotate Y

        (move.l temp_x x)
        (move.l temp_y y)
        (move.l temp_y z)

        (muls.w (matrix :row1-x) x)
        (muls.w (matrix :row1-y) y)
        (muls.w (matrix :row1-z) z)

        (add.l x y)
        (add.l z y)
        (add.w (matrix :trans-y) y) 
        (move.l y temp_y) 

        ; Rotate X

        (move.l temp_x x)
        (move.l temp_y y)
        (move.l temp_y z)

        (muls.w (matrix :row0-x) x)
        (muls.w (matrix :row0-y) y)
        (muls.w (matrix :row0-z) z)

        (add.l z x)
        (add.l y x)
        (add.w (matrix :trans-x) x) 
        (move.l x temp_x) 

        (divs.l rot_z temp_x)
        (divs.l rot_z temp_y)

        (add.l 160 temp_x)
        (add.l 100 temp_y)

        (move.w temp_y (++ output))
        (move.w temp_z (++ output)))

    (rts))

@deplinenoise
Copy link
Author

Looks plenty readable to me. Only comment I have on the example is that for practical purposes the loop macro should probably take a body-style last parameter, which would mean wrapping the contents of the thing in either just a list or something explicit like the Common Lisp progn. Otherwise it'll be really awkward to add some feature to the loop macro if it's invoked with 100s of things in the parameter list (basically every instruction in the loop currently, instead of 2-3 params and the loop body as a single arg.)

Might also want to consider:

  • Labels/jump targets. Many lisp assemblers just use naked keyword symbols in the stream. Another option is a pseudo-instruction e.g. (label :foo) but I'd probably go for :foo myself. (It's easy to distinguish from the list form required for instructions anyway.)
  • How to declare data and sections. Custom mini language? Would be cool to be able to have a (data ...) block anywhere including in code to just have the assembler output stuff e.g. in case we're trying to output instructions we don't have yet or inline something hacky.

@deplinenoise
Copy link
Author

Another reason progn might be needed is to have multiple things slot into "one instruction", when output from a macro. E.g. if we add a macro to compute some value using a two-instruction sequence, what is it going to return? It's going to be spliced in to the middle of a block's linked list of instructions, but it needs to return two! The answer of course is to wrap them in a progn block and return that instead, and the assembler just recurses through those as it works. (Or alternatively you can flatten the tree first, I did that with c-amplify.)

So:

(def-asm-macro myspecial.l (in-reg out-reg)
  (once-only (in-reg out-reg)
    `(progn
      (move.l ,in-reg ,out-reg)
      (eor.l ,out-reg 78))))

When you use it like this:

((move.l ....)
 (myspecial.l (++ input) d0))
 (add.l ...))

The resulting list looks like this:

((move.l ...)
 (progn
   (move.l (++ input) d0)
   (eor.l d0 78))
  (add.l d0 d1))

Finally this brings up the concept of multiple evaluation (e.g hygiene.) - If you swap the parameters to myspecial.l the register will be incremented twice due to the side effect-laden post-increment form. Simplest thing to do would be to just fail to compile in that case, have the macro bail at compile time if it is about to do multiple evaluation of increment/decrement. But sometimes that's desired, like if you're declaring a move64.l macro that does a massive unrolled memcpy or something. The nicest thing to do might be to have the once-only macro for asm introduce a temporary let-like binding that's simply a move.[bwl] (macro-param), temp-reg and then trust that those extra instructions will be dropped by a later optimizer when that's safe to do. Depends on design scope, almost required for a more complete language base, but not needed for a fancy assembler probably.

@emoon
Copy link

emoon commented Jul 31, 2014

Loop: Agreed (another list would just be good enough I guess (this was primary a test to get a fell for the over all structure of a function)

Labels/jumps: I agree that just using :foo here should be good and it makes it stand out a bit more in the code compared to (label :foo)

Yeah a simple lang here should be fine like

(section :data :fast 
  ((dc.l *foo* 0))

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment