197 lines
4.7 KiB
Forth
197 lines
4.7 KiB
Forth
|
# Muri: a Minimalist Assembler for Nga
|
||
|
|
||
|
This is a small assembler used to build the initial image for
|
||
|
RETRO. The implementation here uses the runtime variant included
|
||
|
in the core RETRO system. See the glossary entries for `i`, `d`,
|
||
|
`r`, `as{`, and `}as` for details on these.
|
||
|
|
||
|
The full assembler has a postfix notation. Syntax is:
|
||
|
|
||
|
<directive> <data>
|
||
|
|
||
|
Directives are a single character. Muri recognizes:
|
||
|
|
||
|
* **i** for instructions
|
||
|
* **d** for numeric data
|
||
|
* **s** for string data
|
||
|
* **:** for creating a label
|
||
|
* **r** for references to labels
|
||
|
|
||
|
Instructions are packed up to four instructions per location.
|
||
|
You can specify them using the first two characters of the
|
||
|
instruction name. For a non operation, use '..' instead of
|
||
|
'no'.
|
||
|
|
||
|
0 nop 5 push 10 ret 15 fetch 20 div 25 zret
|
||
|
1 lit 6 pop 11 eq 16 store 21 and 26 halt
|
||
|
2 dup 7 jump 12 neq 17 add 22 or 27 ienum
|
||
|
3 drop 8 call 13 lt 18 sub 23 xor 28 iquery
|
||
|
4 swap 9 ccall 14 gt 19 mul 24 shift 29 iinvoke
|
||
|
|
||
|
E.g., for a sequence of dup, multiply, no-op, drop:
|
||
|
|
||
|
i dumu..dr
|
||
|
|
||
|
An example of a small program:
|
||
|
|
||
|
i liju....
|
||
|
r main
|
||
|
: square
|
||
|
i dumure..
|
||
|
: main
|
||
|
i lilica..
|
||
|
d 12
|
||
|
r square
|
||
|
i ha......
|
||
|
|
||
|
As mentioned earlier this requires knowledge of Nga architecture.
|
||
|
While you can pack up to four instructions per location, you
|
||
|
should not place anything after an instruction that modifies the
|
||
|
instruction pointer. These are: ju, ca, cc, re, and zr.
|
||
|
|
||
|
~~~
|
||
|
:file:operation #4 io:scan-for io:invoke ;
|
||
|
|
||
|
#0 'file:R const
|
||
|
#1 'file:W const
|
||
|
#2 'file:A const
|
||
|
#3 'file:R+ const
|
||
|
|
||
|
:file:open (sm-h) #0 file:operation ;
|
||
|
:file:close (h-) #1 file:operation ;
|
||
|
:file:read (h-c) #2 file:operation ;
|
||
|
:file:write (ch-) #3 file:operation ;
|
||
|
|
||
|
{{
|
||
|
'FID var
|
||
|
'Size var
|
||
|
'Action var
|
||
|
'Buffer var
|
||
|
:-eof? (-f) @FID file:tell @Size lt? ;
|
||
|
:preserve (q-) &FID [ &Size &call v:preserve ] v:preserve ;
|
||
|
---reveal---
|
||
|
:file:read-line (f-s)
|
||
|
!FID
|
||
|
[ here dup !Buffer buffer:set
|
||
|
[ @FID file:read dup buffer:add
|
||
|
[ ASCII:CR eq? ] [ ASCII:LF eq? ] [ ASCII:NUL eq? ] tri or or ] until
|
||
|
buffer:get drop ] buffer:preserve
|
||
|
@Buffer ;
|
||
|
|
||
|
:file:for-each-line (sq-)
|
||
|
[ !Action
|
||
|
file:open-for-reading !FID !Size
|
||
|
[ @FID file:read-line @Action call -eof? ] while
|
||
|
@FID file:close
|
||
|
] preserve ;
|
||
|
}}
|
||
|
~~~
|
||
|
|
||
|
## Unu
|
||
|
|
||
|
This is documented in *example/retro-unu.forth*, but basically
|
||
|
it provides a combinator that runs a quote for each line in a
|
||
|
file, provided that the lines are in fenced blocks starting and
|
||
|
ending with `~~~`.
|
||
|
|
||
|
The RETRO sources are written in this style, so I include Unu
|
||
|
here to simplify the later workflow.
|
||
|
|
||
|
~~~
|
||
|
{{
|
||
|
'Fenced var
|
||
|
:toggle-fence @Fenced not !Fenced ;
|
||
|
:fenced? (-f) @Fenced ;
|
||
|
:handle-line (s-)
|
||
|
fenced? [ over call ] [ drop ] choose ;
|
||
|
---reveal---
|
||
|
:unu (sq-)
|
||
|
swap [ dup '~~~ s:eq?
|
||
|
[ drop toggle-fence ]
|
||
|
[ handle-line ] choose
|
||
|
] file:for-each-line drop ;
|
||
|
}}
|
||
|
~~~
|
||
|
|
||
|
## Muri
|
||
|
|
||
|
Now for the assembler. I create a couple of data structures: a
|
||
|
buffer for the assembled image and a pointer into this.
|
||
|
|
||
|
~~~
|
||
|
'Image d:create #8192 allot
|
||
|
'AP var
|
||
|
~~~
|
||
|
|
||
|
I then use these to implement `I,`, a word which stores a value
|
||
|
into the image buffer and increment the pointer.
|
||
|
|
||
|
~~~
|
||
|
:I, (n-) &Image @AP + store &AP v:inc ;
|
||
|
~~~
|
||
|
|
||
|
### Pass 1
|
||
|
|
||
|
Muri is a two pass assembler. The first pass handles most of the
|
||
|
work. It processes instrution bundles, data, strings, and creates
|
||
|
labels pointing to specific addresses in the image. References
|
||
|
are compiled as dummy values, to be resolved later.
|
||
|
|
||
|
~~~
|
||
|
#0 !AP
|
||
|
'retro.muri
|
||
|
[ dup s:length n:zero? [ drop #0 ] if 0;
|
||
|
fetch-next &n:inc dip
|
||
|
$i [ i here n:dec fetch I, ] case
|
||
|
$d [ s:to-number I, ] case
|
||
|
$r [ drop #-1 I, ] case
|
||
|
$: [ @AP swap 'muri! s:prepend const ] case
|
||
|
$s [ &I, s:for-each #0 I, ] case
|
||
|
'ERROR s:put nl
|
||
|
] unu
|
||
|
~~~
|
||
|
|
||
|
### Pass 2
|
||
|
|
||
|
The second pass skips over everything except references, which
|
||
|
get resolved and filled in. This allows for forward references.
|
||
|
|
||
|
~~~
|
||
|
#0 !AP
|
||
|
'retro.muri
|
||
|
[ dup s:length n:zero? [ drop #0 ] if 0;
|
||
|
fetch-next &n:inc dip
|
||
|
$i [ drop &AP v:inc ] case
|
||
|
$d [ drop &AP v:inc ] case
|
||
|
$r [ 'muri! s:prepend d:lookup d:xt fetch I, ] case
|
||
|
$: [ drop ] case
|
||
|
$s [ s:length n:inc &AP v:inc-by ] case
|
||
|
'ERROR s:put nl
|
||
|
] unu
|
||
|
~~~
|
||
|
|
||
|
### Save Image
|
||
|
|
||
|
Saving the image is pretty straightforward. For each cell,
|
||
|
convert to bytes and write them to the output file.
|
||
|
|
||
|
~~~
|
||
|
'FID var
|
||
|
|
||
|
:write-byte (n-) @FID file:write ;
|
||
|
:mask (n-) #255 and ;
|
||
|
|
||
|
:write-cell (n-)
|
||
|
dup mask write-byte
|
||
|
#8 shift dup mask write-byte
|
||
|
#8 shift dup mask write-byte
|
||
|
#8 shift mask write-byte ;
|
||
|
|
||
|
:image:save (s-)
|
||
|
file:W file:open !FID
|
||
|
&Image @AP [ fetch-next write-cell ] times drop
|
||
|
@FID file:close ;
|
||
|
|
||
|
'retro.nga image:save
|
||
|
~~~
|