The Baron guide
Baron is a 6502 cross-assembler for the BBC Micro. You feed it
assembly source, and it hands you back binaries, with optional
.inf sidecars, or a ready-to-boot DFS disc image. It is the
successor to BeebAsm, rebuilt from the ground up: a proper multi-pass
assembler with a rich expression language, lexical scoping, macros, pure
functions - and a register allocator for your zero page, which we are
rather proud of.
This guide walks through the language a feature at a time, teaching by example. The dry lists of every switch, keyword and operator live in the reference.
Contents
- Coming from BeebAsm
- A first program
- The shape of a line
- Labels and scopes
- Sections
- Includes
- Inline BASIC
- Expressions
- Lists and ranges
- Subscripts
- Broadcasting
- Strings and character codes
- Zero-page allocation
- IF and FOR
- Predefined symbols
- Macros
- Functions
- Section transforms
- Logging
- The symbol dump
- Saving your work
Coming from BeebAsm
If you know BeebAsm, you already know most of Baron - the mnemonics,
the .label syntax, EQUB, &
hex, { } scopes and colon-separated statements all carry
straight over. The differences are where Baron comes into its own:
ORG,SAVE,GUARDandCLEARare gone. Their jobs are done by sections: a named block carrying attributes (org,load,exec,filename,guard). Giving a section a filename is the request to save it, and aguardaddress is checked when the section closes.- As many passes as it takes. BeebAsm makes two
passes; Baron reassembles until the program converges. Forward
references work nearly everywhere - in a
FORcount, anINCLUDEfilename, a section attribute. - Errors accumulate. One mistake no longer stops the
show; you get the lot in one run, GCC-style
(
file:line:col: error: ...), so your editor can jump to them. - The expression language got richer. Values include
lists and ranges, and operators broadcast across them element-wise - a
whole sine table in one
EQUB. See Expressions. - Zero-page allocation. Declare zero-page variables by name and let Baron pack them, with a liveness analysis proving the packing safe. See Zero-page allocation.
- User-defined
FUNCTIONs, and macro overloading - severalMACROsignatures on one name, selected by shape. - Local labels.
.@binds an anonymous label;@-and@+reference the nearest one either side. PRINThas ten channels, redirectable to files from the command line - build logs, symbol dumps, whatever you like.- An inline BASIC tokeniser. Just incorporate BASIC programs into your source files, mixed with assembly as you wish.
- One file, one assembly. Each source file on the command line assembles in a fresh world; symbols never leak between them.
A few BeebAsm conveniences are spelled differently here:
MAPCHAR is a one-line user function (the recipe is in Strings and character codes),
and COPYBLOCK is now better done with a nested, rephased
section (see Sections).
A first program
Let us print something. Save this as hello.6502:
oswrch = &FFEE ; the OS character-out routine
SECTION Main, org = &2000, filename = "HELLO"
.start
LDX #0
.loop
LDA message,X
BEQ done
JSR oswrch
INX
BNE loop
.done
RTS
.message
EQUS "Hello, owl fans!", 13, 0
ENDSECTION
Then assemble it:
$ baron -p . hello.6502
Success is silent, and a file HELLO appears in the
current directory: naming the file in the section is what asked for it,
and -p says where the raw binaries should land
(. being here). Without -p or -o
Baron assembles and reports as usual but writes nothing, and says as
much.
More usefully, we can go straight to a disc image:
First create boot.6502:
SECTION Boot, filename = "!BOOT", exec = &FFFFFFFF
EQUS "*RUN HELLO", 13
ENDSECTION
Then assemble it:
$ baron -o hello.ssd --title HELLO --opt 3 boot.6502 hello.6502
That builds a DFS .ssd with the two files on it, titled
HELLO, set to *EXEC !BOOT. Load it into your
favourite emulator, hit Shift-Break, and there we are.
While we are here, try baron -v -p . hello.6502 - the
listing shows every byte against its source line, with each label's
address alongside it:
2000 .start
2000 A2 00 LDX #0
2002 .loop
2002 BD 0E 20 LDA message,X
2005 F0 06 BEQ done
...
A grep for a label answers with its address; and an assignment lists
as what it evaluated to, with the source expression bracketed after it
when they differ (PLAY_R7 = 28 [34 - 4 - 2]), so computed
constants show their values too. For a machine-readable version of all
this, see the symbol dump below.
-v keeps long things short: an emission past eight bytes
(EQUS, INCBIN, SKIP...) truncates
its dump with ..., and a list value in an assignment cuts
off after eight elements. -vv is the uncut edition - every
emitted byte, eight per line with the address running down the margin,
and list values rendered whole:
000A 41 42 43 44 45 46 47 48 incbin "blob.bin"
0012 49 4A 4B 4C 4D 4E 4F 50
001A 51 52
The shape of a line
Here are the basics:
- Comments run from
;or\to the end of the line. :separates statements, soLDA #0 : STA &70is two statements on one line.- Keywords are case-insensitive:
lda,LDAandLdaare all the same instruction. - Numbers are decimal (
42,1.5), hex (&FFor$FF) or binary (%1010). A character literal is a number too:'A'is 65, the character's code, soCMP #'Z'+1just works. - Strings are double-quoted; write
""inside one for a literal quote. name = exprbinds a case-sensitive symbol. Symbols are immutable - one name, one value - which is what lets Baron re-run passes freely. (Loops that want a changing variable useFOR.) To define a symbol with the same name as a reserved word, write an@prefix, C#-style:@next = 5bindsnext.
Labels and scopes
.name defines a label at the current address. Braces
open a scope, and a label immediately before the brace names it:
.reset
{
.loop ; this loop is private to reset...
DEX
BNE loop
RTS
}
.wipe
{
LDA #0
.nonzero
{
.loop ; ...so this one does not collide with it
STA &3000,X
BNE loop
}
}
From outside, a named scope's symbols are reachable by their dotted
path: JSR wipe.nonzero. This is good for when you want to
provide additional entry points to a subroutine, or access a scoped
symbol. Anonymous { } scopes work too, and are genuinely
private - there is no path to name them by.
Braces also count as statement separators, so a compact scope fits on one line with no extra colons:
LDX #8 {.loop STA &70,X : DEX : BNE loop } RTS
(One thing braces are not is an IF body:
IF flag { ... } parses, but the braces open a real scope -
its labels stay private, unlike a plain IF branch - and the
ENDIF is still required.)
For tiny hops there are local labels: .@ binds an
anonymous label, and @- / @+ refer to the
nearest one behind / ahead:
LDA &FE4F : BPL @+ ; skip forward to the nearest .@
INC speed+1
.@
Locals never leak out of their scope, so use them freely.
Sections
A section is a named block of output: where the code assembles
(org), and how it is saved (filename,
load, exec). Attributes are
name = expr pairs on the SECTION line:
SECTION Code, org = &1100, filename = "CODE", exec = start
; ... the program ...
ENDSECTION
orgsets the assembly address. Within a section the address only ever moves forwards (SKIP,SKIPTOandALIGNpad with zeroes; there is no rewinding).guardsets an address the section must not reach -guard = &3000means the last byte may land at&2FFFand no further. Overrunning it reports how many bytes too far you went, without stopping the assembly: you get every overrun (and everything else) in one run.- Every section has a built-in guard at the top of memory: the last
byte may land at
&FFFF. Going past it is reported the same way, at the statement that went over. filenameasks for the section to be saved - no filename, no file.loaddefaults toorg, andexecdefaults toload.- Addresses are the BBC's full 32-bit host addresses:
&FFFFxxxxruns on the I/O processor,&0000xxxxon the second processor.org,load,execandguardall take the whole value -loadandexeccarry it through to the catalogue, while the assembly address and the guard check use the 6502's low 16 bits. - Sections nest, and nesting means containment: a child's
bytes are copied into its parent, right where the child is written, and
the parent's address moves past them at
ENDSECTION. A child with noorgof its own simply continues the enclosing address; a child with an explicitorgis rephased - its labels resolve at the runtime address it names while its bytes still land in the parent (see the relocation workflow below). - Attributes are never inherited: they stay on the
SECTIONline they are written on. A nested section that wantscmos = TRUEor aguardsays so itself. cmos = TRUEenables the 65C02 instruction set for the section:PHX,STZ,BRA,LDA (zp)and friends. Everywhere else is plain NMOS 6502, and a CMOS instruction there says so:CMOS-only instruction (needs cmos=TRUE on the section).- Section names don't clash with symbol/label names, and each names one section. Writing a name again reopens that section, where that is possible.
- Two sections may sit at the same address - sideways banks, swap-in overlays - without complaint; each keeps its own instruction pointer.
Code outside any section lands in a nameless default section starting at address 0. That is fine for scratch and experiments, but it can never be saved - anything you want out of the assembler needs a named section with a filename.
A section written at the top level stands alone: it takes no room in
the default section, whose address only follows its own bytes. So
.here below is 0, not 2:
SECTION a, org = &2000
EQUB 1, 2
ENDSECTION
.here
Relocation workflow is achieved with nested sections. Nest the code
at its runtime address inside the loadable section, and it is
carried inside the file while its labels point where it will run; a
label after the ENDSECTION measures its size, and the
copy-down stub follows in place:
SECTION Loader, org = &1200, filename = "GAME", exec = entry
.payload
SECTION Code, org = &400 ; assembled for &400, carried inside GAME
.start
; ... the program ...
ENDSECTION
.entry
; ... copy `payload` down to &400 and jump to it ...
LDX #HI(entry-payload+&FF)
LDY #0
.loop
LDA payload,X : STA &400,X
INY : BNE loop
INC loop+2 : INC loop+5
DEX : BNE loop
JMP start
ENDSECTION
Labels around the child live in the loader's address space
(payload is where the bytes sit in the file's memory image,
entry - payload their length), while labels inside it live
in the child's (start = &400) - which is
exactly what the stub needs on each side of the copy.
Reopening sections
Writing SECTION name again for a section that has
already closed writes another piece of it. The piece carries on exactly
where the last one stopped, and its bytes join the end of the section's
- wherever the piece happens to be written. They belong to the section
being reopened, not to whatever encloses the reopening.
That lets each routine keep its data right beside it, while the data still comes out as one file:
SECTION tables, org = &5000, filename = "TABLES"
ENDSECTION
SECTION code, org = &1900, filename = "CODE"
.draw
{
SECTION tables
.sintab EQUB 0, 25, 49, 71, 90, 106, 117, 125
ENDSECTION
LDA sintab,X
RTS
}
.plot
{
SECTION tables
.masks EQUB &80, &40, &20, &10, &08, &04, &02, &01
ENDSECTION
LDA masks,X
RTS
}
ENDSECTION
draw.sintab lands at &5000 and
plot.masks at &5008. TABLES
holds all sixteen bytes and CODE only the code.
The rules:
- A reopening takes no attributes, not even an
org: the section was described once, on its firstSECTIONline, and every later piece keeps thatguard,cmosand so on. Write that first line before any reopening. If you want code at a different address, that is what a nested section with its ownorgis for. - The
guardis checked as each piece closes, so an overrun is reported at the piece that crossed it. - A section can't be reopened from inside itself - not even from inside one of its own children.
- A section that took room in its parent can't be reopened: its bytes are already in the parent, and the parent has moved on past them, so a second piece would have nowhere to go. That rules out most nested sections. Top-level sections can always be reopened, and so can the nested sections that take no room (see the virtual sections below).
- Since any top-level name can be reopened, a bare
SECTION Codewritten twice simply joins up with the first. A secondSECTION Code, org = &1900, ...is refused, because it has attributes.
Virtual sections
virtual = TRUE makes a section that hands out addresses
but keeps no bytes. Everything in it - SKIP,
EQUB, even instructions - moves its address on and lands
nowhere.
That makes it a pool of workspace, and reopening lets each routine claim its variables right where it uses them:
SECTION workspace, org = &400, guard = &800, virtual = TRUE
ENDSECTION
SECTION Code, org = &1900, filename = "CODE"
.draw
{
SECTION workspace ; carries on from the last claim
.xpos SKIP 1
.buffer SKIP 32
ENDSECTION
LDX xpos
LDA buffer,X
RTS
}
.sound
{
SECTION workspace
.freq EQUW 0 ; EQUW 0 says "two bytes" as well as SKIP 2 does
ENDSECTION
LDA freq
RTS
}
ENDSECTION
draw.xpos lands at &400,
draw.buffer at &401 and
sound.freq at &421, while
CODE holds only the code. A section opens no scope, so each
label belongs to the routine that claims it, reachable from outside by
its dotted path like any other scoped name.
The rules:
- A virtual section has nothing to save, so a
filenameon it is an error. - Inside a real section, a virtual one takes no room at all - so it
needs an
orgof its own. Carrying on from the parent's address would put its labels on top of the parent's next bytes, so that is an error. With its ownorgit can be declared right inside the code that uses it, and still reopened. - Code in a virtual section never runs, so the zero-page allocator ignores it.
The same idea gives plain zero-page bytes when you want them never
shared (org = &70, guard = &90), and with
org = 0 it lays out the field offsets of a structure.
Memory maps
Inside a virtual section, the sections without an org of
their own are laid end to end. A real section in there keeps its own
bytes, and can be saved; the virtual parent just steps past them. So
separately saved files can follow one another in memory without chaining
each org to the end of the last:
SECTION memmap, org = &1900, virtual = TRUE
SECTION code, filename = "CODE"
; ... the program ... ; at &1900
ENDSECTION
SECTION data, filename = "DATA"
; ... the tables ... ; straight after the code
ENDSECTION
.free ; the first unused byte
ENDSECTION
A virtual section inside the map without an org reserves
its room in the same way - a group of buffers, say. One with its own
org is placed elsewhere and takes no room in the map, and
that is what lets it be reopened later. The same goes for a real section
with its own org.
The code in code and data is real, so the
zero-page allocator analyses it like any other.
Includes
INCLUDE "file.6502" splices another source file in
textually (paths resolve relative to the including file), and
INCBIN "file.dat" splices a binary file's bytes into the
output.
Inline BASIC
BASIC ... ENDBASIC drops a BBC BASIC
program straight into the output, tokenised byte-for-byte as BASIC 4
itself would store it - handy for a loader that sets the mode and runs
the main binary:
SECTION loader, org=&1900, filename="Loader"
BASIC
10REM My loader
20MODE 7
30*RUN MAIN
ENDBASIC
ENDSECTION
Each line starts with a decimal line number (0 to 32767) and runs to
the end of the physical line. The whole line belongs to BASIC:
: separates BASIC statements there and ; is a
print separator, not a Baron comment. The tokeniser is the ROM's own
algorithm, quirks included - keywords must be uppercase
(print is a variable name, exactly as it would be on the
Beeb), abbreviations like P. expand in the ROM's order,
line numbers after GOTO and friends get the compact
three-byte encoding, and REM, DATA, strings
and * commands pass through untouched.
ENDBASIC finishes the program with its
&0D &FF terminator, so the emitted bytes, loaded at
PAGE, are a complete program ready to RUN.
Blank lines and Baron comment lines may sit between BASIC lines;
anything else inside the block is an error. Lines are emitted exactly in
the order written - Baron does not sort them, so keep the numbers
ascending if you want GOTO and LIST to behave
- and a line whose tokenised record would pass 255 bytes is refused.
Splicing bytes into a line
Some BASIC lines hold bytes nobody can type: teletext colour codes in
a REM, or a VDU 21 hidden inside a string. A
{ at the very end of a BASIC line drops into Baron, and a
} picks the same line back up:
TT_GREEN = &82
TT_YELLOW = &83
BASIC
1430REM"{
EQUB &7F, TT_GREEN
}Hello,{
EQUB TT_YELLOW
}Soni
ENDBASIC
That is still one BASIC line:
0D 05 96 13 F4 22 7F 82 "Hello," 83 "Soni". The spliced
bytes go in exactly as emitted, never tokenised, and the line's length
byte counts them along with everything else. The 255-byte limit is for
the whole line, splices included.
The tokeniser picks up where it left off, as if the splice weren't
there: inside a string we stay in the string, and after
REM, DATA or a * command the rest
of the line stays raw. A splice does always end a token, though, so
PRI{ ... }NT is two variable names, never
PRINT.
Splice rules
- The
{must be the last thing on the line, blanks aside. Everything between the line number and the{is the line's text, spaces included - so an editor trimming trailing whitespace can't eat the spaces before a splice. - Anything Baron can assemble can go between the
braces. A
}ends a statement just as a newline does, so a short splice can share a line with its bytes:EQUB TT_YELLOW }Reeve{. - The text after the
}runs to the end of that line. It can end in another{, or be empty, which ends the BASIC line right there. - A splice is not a scope, so a label inside it belongs to the rest of the program.
That last rule makes machine code in a REM easy:
BASIC
10REM{
.beep
LDA #7 : JMP &FFEE
}
20CALL PAGE+5
ENDBASIC
The routine starts five bytes into the program - after the line's
four-byte header and the REM token - and the assembly can
use beep wherever it needs the address.
A splice must be closed. If a numbered line, ENDBASIC or
the end of the file comes before the }, that's an error,
with a note pointing back at the { that opened the splice.
That catches a REM which really does end in a brace; to
write one of those, splice it in: end the line with {, then
EQUS "{", then }.
Expressions
Anywhere Baron wants a value, you get the full expression language:
the usual arithmetic (+ - * / ^), integer DIV
and MOD, shifts <<
>>, AND OR EOR
NOT(), comparisons, and a ton of functions
(SIN, SQRT, LO, HI,
...) - the whole list is in the reference.
Comparisons yield booleans (TRUE / FALSE),
and AND, OR, EOR and
NOT() are overloaded on what you hand them: two booleans
get the logical operation (NOT(TRUE) is FALSE,
a < b AND c < d reads exactly as it looks), two
numbers the bitwise 32-bit one (12 AND 10 is 8), and a
mixed pair is refused rather than guessed at. A boolean quietly coerces
to 1 / 0 anywhere a number is wanted - (x > 5) * 10,
EQUB flag.
Note, this is different to BeebAsm, which represented TRUE as
-1, not 1. The command-line switch --beebasm-true
can be used for BeebAsm compatibility, to set TRUE to -1.
LDA #LO(screenbase) ; low byte - or the 6502-style LDA #<screenbase
LDA #HI(312*64-2) ; expressions anywhere an operand goes
* (or its BBC spelling P%) reads the
current assembly address:
.spin JMP * ; jump-to-self
Where it gets interesting is lists and ranges, which get the next three sections to themselves - they are the most powerful part of the language, and the part worth slowing down for.
Lists and ranges
A list is values in braces - any values, nested however you like:
{1, 2, 3}
{"one", 2, {3, 4}} ; mixed types and nesting are fine
Lists nest, so a list of equal-length lists behaves as a 2D table -
rows of columns. SHAPE tells you the axis lengths,
outermost first, and RANK how many axes there are:
SHAPE({1, 2, 3}) ; {3}
SHAPE({{1, 2, 3}, {4, 5, 6}}) ; {2, 3} - two rows of three
RANK({{1, 2, 3}, {4, 5, 6}}) ; 2
SHAPE(5) ; {} - a scalar has no axes
SHAPE({}) ; {0} - one axis, nothing on it
SHAPE(0..255) ; {256} - a range is a rank-1 list
SHAPE({{1, 2}, {3, 4, 5}}) ; {2} - ragged rows agree on one axis only
Ragged lists are legal; they just have less shape for the shapely operations to work with.
A range is a compact list of consecutive integers,
written with ..:
1..5 ; {1, 2, 3, 4, 5} - inclusive at both ends
5..1 ; {5, 4, 3, 2, 1} - descending is inferred from the endpoints
0..<5 ; {0, 1, 2, 3, 4} - ..< excludes the end
5..<5 ; {} - an empty range is a legal empty list
Give a range its second element and you have set its step:
0..2..10 ; {0, 2, 4, 6, 8, 10} - "0, then 2, up to 10"
1..3..10 ; {1, 3, 5, 7, 9} - the end is a limit, not a promise
10..8..1 ; {10, 8, 6, 4, 2} - stepping down
0..2..<10 ; {0, 2, 4, 6, 8} - ..< goes on the final separator
Either end may be left off - 2.., ..7,
0..2.. or just .. - giving an
unbounded range. There is nothing to enumerate, so these are
not data; they exist to be subscripts, where
the axis supplies the missing end.
One habit to note: .. binds looser than arithmetic and
comparisons, so 0..n-1 means 0..(n-1) with no
parentheses needed.
Since EQUB flattens whatever it is given into bytes,
lists and ranges make data tables one-liners:
EQUB 1..5 ; 01 02 03 04 05
EQUB REPEATED(16, &FF) ; sixteen &FFs
EQUB RND(REPEATED(8, 256)) ; eight random bytes
EQUW table + (0..<32) * 8 ; a table of 32 pointers, stride 8
(EQUB and EQUS are the same directive -
strings emit their characters, either name takes anything - and
EQUW / EQUD emit 16- and 32-bit little-endian
units.)
Subscripts
x[...] picks elements out of a list, range or string.
There are three kinds of selector, and they do different things:
- an integer picks one element;
- a range slices - it is forgiving, clipped to the ends, and hands back whatever falls inside;
- a list gathers exactly the named elements, in the order you name them (out of range is an error).
L = {10, 20, 30, 40, 50}
L[1] ; 20
L[1..3] ; {20, 30, 40}
L[{0, 4}] ; {10, 50}
L[{1, 1, 1}] ; {20, 20, 20} - gathers may repeat
Unbounded ranges shine here - the axis fills in the missing end:
L[3..] ; {40, 50} - from index 3 to the end
L[..1] ; {10, 20} - from the start up to index 1
L[..] ; the whole thing
L[0..2..] ; {10, 30, 50} - every other element
L[4..0] ; {50, 40, 30, 20, 10} - a descending slice reverses
L[3..10] ; {40, 50} - slices clip; no error
For nested lists, separate a selector per axis with commas, outermost axis first. An integer drops its axis; a range or list keeps it:
M = {{1, 2, 3}, {4, 5, 6}}
M[1] ; {4, 5, 6} - row 1
M[1, 2] ; 6 - row 1, column 2 (M[1][2] means the same)
M[.., 0] ; {1, 4} - every row's column 0
M[.., 1..] ; {{2, 3}, {5, 6}} - all rows, columns 1 onward
Fewer selectors than axes just leaves the trailing axes whole - which
is why M[1] is a row.
A string is a single value (see Strings and character codes), but subscripts are one of the few things that look inside one. A string subscripts like a list of characters, and always gives back a string:
"hello"[1] ; "e"
"hello"[1..3] ; "ell"
"hello"[4..0] ; "olleh"
"hello"[{0,4}] ; "ho"
Broadcasting
Operators (and the element-wise functions) work on lists by mapping across them, NumPy-fashion. The rules, in the order they apply:
- A range becomes its list of elements.
- A single value - a number, or a string - pairs with everything in the other side, however deep.
- When both sides are lists of the same depth, elements pair off - lengths must match, except that a length 1 list repeats itself.
- When one side is shallower, it is held whole and applied to each element of the deeper side.
{1, 2, 3} * 2 ; {2, 4, 6} - rule 2
{1, 2, 3} + {10, 20, 30} ; {11, 22, 33} - rule 3
{1, 2, 3} + {10} ; {11, 12, 13} - rule 3, the 10 repeats
{1, 2} + {1, 2, 3} ; error: shapes do not match
{{1, 2}, {3, 4}} * 10 ; {{10, 20}, {30, 40}} - rule 2, all the way down
Rule 4 is where it gets properly useful. A rank-1 list against a rank-2 one applies to each row:
{{1, 2, 3}, {4, 5, 6}} * {1, 0, 0} ; {{1, 0, 0}, {4, 0, 0}} - masks every row
and a column against a row gives you all the combinations - the outer product:
{{1}, {2}} + {10, 20} ; {{11, 21}, {12, 22}}
The reductions collapse the results back down: with no axis they fold everything to one scalar; given an axis (0 = outermost) they collapse just that one:
SUM({{1, 2, 3}, {4, 5, 6}}) ; 21 - the lot
SUM({{1, 2, 3}, {4, 5, 6}}, 0) ; {5, 7, 9} - down the columns
SUM({{1, 2, 3}, {4, 5, 6}}, 1) ; {6, 15} - along the rows
SUM(M * v, 1) ; matrix x vector, in two functions
And now the party trick from earlier makes complete sense - a range broadcasts through the maths and each result lands as a byte:
ALIGN &100
.sintable
EQUB ABS(SIN((0..255) / 128 * PI)) * 255 + 0.5
A full 256-entry sine table, one line.
What maps, and what doesn't
Broadcasting belongs to the operators and the element-wise
functions: ABS, SIN, LO,
ROUND, CODES and the rest of that table. Each of
them works on one value at a time, so given a list, it works on every
single value inside it, however deep.
Every other function takes its arguments whole. These are the ones that only make sense of a list as a list - its length, its order, its total:
ABS({-1, {-2, 3}}) ; {1, {2, 3}} - element-wise: every value inside
LEN({{1, 2}, {3, 4, 5}}) ; 2 - whole: the outer list has two elements
REVERSE({{1, 2}, {3, 4}}) ; {{3, 4}, {1, 2}} - whole: the rows swap, their contents don't
SUM({{1, 2}, {3, 4}}) ; 10
There's one exception, and it's a useful one: FIND's
second argument maps, so asking for a list of things gives back a list
of positions. That's what makes character
mapping a one-liner.
Functions take their arguments whole
Your own FUNCTIONs take their
arguments whole too - a list arrives as one value. But when everything
in the body maps, so does the function:
FUNCTION sqr(x) = x*x
sqr({1, 2, 3}) ; {1, 4, 9} - because x*x maps
An IF doesn't map. It runs one branch or the other, so
it needs a single value to decide on, and {1, 300} > 255
is {FALSE, TRUE} - an answer per element, not one
answer:
FUNCTION clamp(x)
IF x > 255
r = 255
ELSE
r = x
ENDIF
= r
clamp(300) ; 255
clamp({1, 300}) ; an error - the IF can't decide for a whole list
So a FUNCTION with an IF in it is one for
single values - unless the choice is made with WHERE, the
element-wise IF. For each value in its first argument, it
takes the matching value of the second where that's true, and of the
third where it's false:
FUNCTION clamp(x) = WHERE(x > 255, 255, x)
clamp({1, 300}) ; {1, 255}
WHERE({TRUE, FALSE}, "on", "off") ; {"on", "off"}
Its three arguments broadcast together, just as an operator's two do.
And like an IF, it only looks at what it picks, so
WHERE(x = 0, 0, 1/x) is safe even where x is
0.
Strings and character codes
A string is text in double quotes - write "" for a quote
inside one. Two strings join with +, and compare in
character-code order with <, = and the rest
(so "Z" < "a"). SORT, MIN and
MAX use the same order:
"HEL" + "LO" ; "HELLO"
"ab" < "b" ; TRUE
SORT({"pear", "apple"}) ; {"apple", "pear"}
A string is one value
A string is a single value, just as a number is - not a list of characters. Everything that builds lists, maps over them or measures their shape treats a string as one thing:
{"a", "b"} + "!" ; {"a!", "b!"} - "!" pairs with each string, as a number would
CONCAT("ab", "cd") ; {"ab", "cd"} - two strings make a list of two
FLATTEN({"ab", {"c"}}) ; {"ab", "c"} - a string is already as flat as it gets
RANK("abc") ; 0 - one value, no axes
It's also why + joins strings: adding two pieces of text
can only mean joining them. A list is many values, so + on
lists adds the values inside, and lists join with CONCAT
instead.
SUM is + over a whole list, so it joins a
list of strings. A list can run over several lines, which makes this the
tidy way to build a long string from pieces:
title = SUM({"STARGLOBE v", AS_STRING(version),
" by ", author})
Numbers don't join quietly - "v" + 3 is an error - so
turn them into text first, with AS_STRING (or
~ for hex).
Looking inside a string
A handful of things work along a sequence: subscripts,
LEN, REVERSE, FIND and
REPLACE. Those read a string as its characters, and give
back a string (or a position in one):
"hello"[1..3] ; "ell"
LEN("hello") ; 5
REVERSE("hello") ; "olleh"
FIND("hello", "ll") ; 2
REPLACE("1.2.3", ".", "-") ; "1-2-3"
Hand them a list and they do the same job with its elements. Baron
has no separate character type - a single character is just a
one-character string - so FIND in a string looks for a
piece of text, of any length.
FOR isn't one of them: it walks lists, and a string is a
single value. Walk its codes instead, with
FOR c = CODES(s).
Searching and replacing
FIND(x, v) gives the position where v first
appears in x, and REPLACE(x, old, new) swaps
every appearance of old for new, working left
to right:
REPLACE("aaa", "aa", "b") ; "ba" - appearances never overlap
REPLACE("v1.2", "v", "") ; "1.2" - an empty new deletes
A REPLACE that finds nothing hands x back
unchanged. A FIND that finds nothing is an error - unless
you give it a third argument, which it hands back instead. That's the
way to ask whether something is there at all:
IF FIND(name, ".", -1) >= 0 ; does name contain a dot?
Both work on lists as well. There, REPLACE swaps
runs of elements, and old and new can
each be a list or a single value:
REPLACE(bytes, &0D, {&0D, &0A}) ; every CR becomes CR LF
REPLACE(bytes, {0, 0}, {}) ; every pair of zeros goes
FIND is different on a list: its second argument maps,
so a list there means "find each of these", and you get a position
apiece. FIND({1, 2, 3, 4}, {2, 3}) is {1, 2},
not where the run 2, 3 starts.
Character codes
CODES turns a string into the list of its character
codes, and CHR turns codes back into a string:
CODES("AB") ; {65, 66}
CHR(72) ; "H"
CHR({72, 73}) ; "HI" - every code becomes a character of ONE string
CODES is element-wise, so a list of strings gives one
list of codes per string. CHR takes its argument whole, and
makes one string from every code in it, however nested:
CODES({"AB", "C"}) ; {{65, 66}, {67}}
CHR({{65, 66}, {67}}) ; "ABC"
That bridge, plus the machinery you already have, is the whole story
of character mapping. A CODES result is a list, so it
broadcasts - and a list of indices is a gather subscript, so
indexing a table with one is the character map,
NumPy-fashion. FIND(x, v) supplies the indices: it returns
where v first occurs in x, and - the one
exception to whole arguments - a list of vs gives a list of
positions.
So BeebAsm's MAPCHAR is a one-liner. Map each character
to its position in a glyph set:
glyphs = " ABCDEFGHIJKLMNOPQRSTUVWXYZ!?"
FUNCTION mapchar(s) = FIND(CODES(glyphs), CODES(s))
EQUS mapchar("HELLO WORLD!") ; each byte is its character's index into glyphs
Or keep explicit {old, new} tables and look bytes up
through both:
from = CODES("ABC")
to = {&80, &90, &A0}
FUNCTION remap(s) = to[FIND(from, CODES(s))]
Or, when the mapping is just an offset, skip the function entirely - a character literal is a number, so it broadcasts across the codes:
EQUS CODES("HELLO WORLD") - ' ' ; font starts at space = 0
A character missing from the set fails the assemble with
Not found: '81' naming the stray code - which is exactly
what you want from a font with holes in it. If you'd rather have a
stand-in, give FIND a third argument:
FIND(CODES(glyphs), CODES(s), 0) sends anything missing to
glyph 0.
Two type predicates round the toolkit out: IS_STRING(x)
and IS_NUMBER(x) answer TRUE or
FALSE for the value as a whole (a list is neither;
test list-ness with RANK(x) > 0) - handy for a
FUNCTION that accepts either a string or a list of
codes.
Numbers as text
AS_STRING writes a number as decimal text, just as
PRINT would show it, and AS_NUMBER reads one
back - the conversions to go with IS_STRING and
IS_NUMBER:
AS_STRING(1234) ; "1234"
AS_STRING(1.5) ; "1.5"
AS_NUMBER("42") ; 42
AS_NUMBER("&1F40") ; 8000
That's handy wherever a number has to end up as characters, like a version number inside a BASIC line:
version = 3
BASIC
10PRINT "Version {
EQUS AS_STRING(version)
}"
ENDBASIC
AS_NUMBER takes exactly one number, written the way
you'd write it in source: - and + are fine,
and so are &, $ and %.
Anything else in the string, a space included, is an error naming the
string, so AS_NUMBER("12x") fails with
Not a number: '12x' rather than quietly giving 12. For hex
text, there's ~ (see Logging), and
AS_NUMBER("&" + ~n) gets you n back.
Zero-page allocation
The headline feature. Hand Baron a pool of zero-page bytes and declare variables by name; it runs a liveness analysis over your actual code and packs them like a compiler allocating registers - provably safely, or it refuses:
ZA_POOL &70..&8F ; the pool
.multiply
{
ZA_AUTO1 a2, b2 ; one-byte variables
ZA_AUTO2 ptr ; a two-byte pair - a pointer
STA a2
...
}
Variables whose lifetimes never overlap share bytes automatically;
anything held live across a JSR is kept clear of everything
the callee touches. It is entirely opt-in - no ZA_POOL, no
allocator - and it has a whole guide to itself: Zero page allocation.
IF and FOR
IF / ELIF / ELSE /
ENDIF assembles conditionally. The condition is any
expression - a boolean (TRUE, FALSE, a
comparison), or a number read as nonzero-is-true - and a dead branch
emits nothing at all. Nor does it compute anything: its lines are read
only to find where the branch ends, so a FUNCTION call, a
builtin like LZ_PARSE or an RND in it costs
nothing and draws nothing. The same goes for the branches of an
IF inside a FUNCTION.
debugrasters = FALSE
IF debugrasters
LDA #&00 + PAL_red : STA &FE21
ENDIF
FOR ... NEXT runs its body once per element of a range
or list, with the loop variable bound to each element in turn:
FOR n = 0..7
LDA #n : STA &FE21 ; unrolled eight times
NEXT
FOR addr = {&FE44, &FE45, &FE4B}
STA addr
NEXT
Each iteration gets its own scope, so labels inside the body do not collide from one iteration to the next. And because these are statements, not preprocessing, they nest freely with everything else - macros, includes, other loops.
Predefined symbols
The -D switch defines a symbol from the command line,
before any source is read - the build script's way of steering the
assembly:
baron -D DEBUG=TRUE -D screenwidth=64 -D version="1.0" game.6502
Each -D takes one name=expression argument
with no spaces in it, and binds the name in the top-level scope of every
file on the line. The right-hand side is a full Baron expression -
numbers, strings, built-in constants and functions, even references to
symbols the source itself defines later (they settle over the passes
like any forward reference). The one thing it cannot do is call a
FUNCTION from the source: functions do not exist yet when
the definitions bind.
A predefinition is an ordinary symbol, and symbols bind once: if the
source also assigns DEBUG = 0, that is a duplicate-symbol
error, with the note pointing back at the -D. To give the
source a default for when the switch is absent, test with
DEFINED() and bind a different name:
IF DEFINED(debug)
dbg = debug ; the command line spoke
ELSE
dbg = FALSE ; the default
ENDIF
(Guarding an assignment to the same name -
IF DEFINED(debug) == FALSE : debug = 0 : ENDIF - cannot
settle: binding the symbol flips its own condition on the next pass.
Baron reports it as
Assembly did not settle: 'debug' kept changing, pointing at
the binding - alongside an undefined-symbol error at any use the flip
strands.)
Macros
A macro is a named statement sequence, expanded wherever its name appears:
MACRO ST16 addr, val
LDA #LO(val) : STA addr
LDA #HI(val) : STA addr+1
ENDMACRO
ST16 &70, screenbase
Arguments are values, evaluated at the call site; the body expands in its own scope, so its labels are private. Define a macro before its first use - a call earlier in the file will not recognise the name.
One name can carry several overloads, distinguished by shape. Punctuation in a signature is quoted and matched verbatim at the call, which lets a macro mimic addressing-mode syntax:
MACRO ADD n : CLC : ADC n : ENDMACRO
MACRO ADD "#" n : CLC : ADC #n : ENDMACRO
ADD &70 ; picks the first
ADD #3 ; the '#' picks the second
Recursion works, with an IF for the base case:
MACRO FILL n
IF n > 0 : EQUB n : FILL n-1 : ENDIF
ENDMACRO
Functions
Where a macro emits statements, a FUNCTION computes a
value - usable in any expression. The one-liner form:
FUNCTION sqr(x) = x*x
EQUB sqr(0..15) ; maps over the range, because x*x does
A function takes each argument whole, so a list arrives as one value; it maps over the list only when everything in its body does (see Functions take their arguments whole).
Longer functions take local assignments, IFs and
FOR loops, with a top-level = as the
return:
FUNCTION ball_ypos(n)
x = (n - numdots/2 + 0.5) / (numdots/2)
y = (x - SIN(x*PI) * 0.1) * radius
= 128 + y
Functions are pure - they compute, they never emit - and lexically
scoped: a body sees its parameters and the globals of where it was
defined, never the caller's locals. Overload by parameter
count, and recurse away (IF for the base case, as with
macros). Definition before use, as with macros.
Recursion can go 128 calls deep; deeper than that is an error (usually a missing base case). To walk a long list, use a loop.
Functions are powerful enough to be able to implement something like the quicksort algorithm, taking a list and returning it sorted!
Locals and loops
A local takes a new value each time you assign it, parameters included, so a body can count and accumulate. That's the only change a body can make: assigning the name of a global makes a local that hides it, and the locals are gone when the call returns, so the function stays pure. A local can be read only once something has assigned it.
FOR ... NEXT works in a body just as it does in a
program, running its statements once for each element of a list or
range. With assignment, that's how to build a list a piece at a
time:
FUNCTION commands(names) ; a length-prefixed command table
out = {}
FOR s = names
out = CONCAT(out, LEN(s), CODES(s))
NEXT
= out
EQUB commands({"LOAD", "SAVE", "RUN"})
The loop variable is an ordinary local, which keeps its last value
after the NEXT, and anything else you assign carries from
one step to the next:
FUNCTION offsets(lengths) ; where each record starts
out = {}
at = 0
FOR n = lengths
out = CONCAT(out, at)
at = at + n
NEXT
= out
EQUW records + offsets({5, 3, 8}) ; records, records+5, records+8
A loop is also the way to apply something that takes its argument
whole to each element of a list - here LEN of each string,
which LEN on the list won't give you:
FUNCTION lengths(strings)
out = {}
FOR s = strings
out = CONCAT(out, LEN(s))
NEXT
= out
lengths({"LOAD", "SAVE", "RUN"}) ; {4, 4, 3}
A few things to know:
- A loop always walks its whole list - there's no
WHILEorBREAK- so put anIFinside it to skip the steps you don't want. - The
=return goes at the top level of the body, never inside a loop or anIF. FORandNEXTare keywords in a body, so they can't name locals (next_oneis fine).- A loop has no depth limit, and each step's working memory is freed
as the next begins. But a list grown with
CONCATis copied at every step, so the time to build one grows with the square of its length: a few thousand elements take a fraction of a second, twenty thousand a second or two.
Errors in a body
An error inside a body is reported on the body line that caused it, followed by a note at the call it came from. A bad argument is the caller's mistake, so that one is reported at the call.
To refuse bad input, put an ASSERT in the body. When its
condition is false, the call's result is an error carrying your message
- the values joined, as ERROR joins them - reported at the
ASSERT:
FUNCTION checked(w)
ASSERT w >= 0, "bad width: ", w
= w
The message is optional: a bare ASSERT w < 100 just
reports "Assertion failed". It is only evaluated when the assertion
fails.
When the refusal is one outcome among several,
ERROR(...) is also a function: it returns an error
value carrying your message, which surfaces as a proper diagnostic once
the result is used:
FUNCTION checked(w)
IF w < 0
r = ERROR("bad width: ", w)
ELSE
r = w
ENDIF
= r
Section transforms
A section can have its bytes rewritten as it closes - compressed,
scrambled, checksummed - by a FUNCTION of your own. Name it
in the section's transform attribute:
FUNCTION scramble(data) = data EOR &55
SECTION Loader, org = &1900, filename = "LOADER"
... ; the unscrambler
.payload
SECTION Code, org = &400, transform = "scramble"
.start
... ; the real code, assembled for &400
ENDSECTION
.payload_end
ENDSECTION
The FUNCTION is given the section's bytes as a list of
numbers, and returns the bytes to keep: a list of numbers 0-255, or a
string. Those are what the section holds and what its parent receives -
here, the loader holds the scrambled bytes between .payload
and .payload_end. The section's own labels stay where the
original code will run once it is unscrambled, so JMP start
still means &400.
The transform runs once everything else has settled - every label
resolved, zero-page variables allocated, INCBIN files
loaded - so it sees the bytes exactly as they will be. If it changes
their size, whatever comes after the section moves, so Baron goes round
again until the sizes hold still. Given the same bytes again, a
transform is not run again. (A transform whose output size depends on
where it lands may never settle, which is reported.)
-v lists a transformed section as it was assembled,
followed by a note of what the transform made of it:
ENDSECTION
; transform "scramble": 312 -> 312 bytes
Bit fields
A compressor writes fields of so many bits. In Baron, a field of
n bits holding v is the single number
2^n + v: the top set bit marks the width, and is not part
of the value. Two builtins work with them:
BITLEN(x)is how many bitsxneeds (BITLEN(5)is 3), so a field's width isBITLEN(f) - 1.BITPACK(fields)concatenates a list of fields into bytes, most significant bit first, padding the last byte with zeroes.
Encodings are then ordinary FUNCTIONs, and like any
arithmetic they work on whole lists at once:
FUNCTION bits(v, n) = 2^n + v ; v in n bits
FUNCTION gamma(n) = 2^(2 * BITLEN(n) - 1) + n ; an Elias gamma code, n >= 1
BITPACK({bits(5, 3), gamma(5)}) ; 101, then 00101: {165}
Compression
LZ_PARSE(data, literal_run, literal, match_run, length, offset)
finds the cheapest way to write data as runs that alternate
between literal bytes and matches (copies of earlier bytes), literals
first, each run's count written before its contents. It knows nothing of
how any field is written. Instead, five tables give the cost in bits of
every value a field could hold, by value, with a negative entry meaning
"impossible". The tables come straight from the encodings:
BITLEN(gamma(0..n)) - 1 is the width of the gamma code for
every count up to n, and gamma(0) is no field
at all, so a count of 0 comes out impossible on its own.
It returns {bits, runs}: the smallest size there is
under those costs, and the runs that achieve it -
{0, bytes} for a literal run,
{1, {offsets, lengths}} for a match run - for your
FUNCTIONs to encode and BITPACK.
Some formats price an offset by the length of its match - a short
match can only reach so far back, say. Then offset can be a
list of tables, one for each length from 1, the last serving every
longer match: {off1, off2, off3} prices 1-byte, 2-byte, and
longer matches differently.
What it assumes
How a format marks its runs decides what the search can take for granted:
- If every match carries its own flag, so a match run costs the same whatever its count, every match tries every offset. Offsets may then cost anything: a fitted table that makes offset 320 cheaper than offset 200 is fine.
- If a match run's count is written, the count must never get cheaper as it grows, and each match takes the nearest offset that reaches its length - so an offset must never get cheaper as offsets grow, either.
Repeat matches
Some formats also have repeat matches, which copy from the
same offset as the match before: the first match after a literal run may
say "same offset again" in a few bits instead of writing the offset out.
That pays because data so often agrees with an earlier stretch except
for the odd byte - a match, a literal, then the match carries on. A
seventh argument, repeat, gives a repeat's cost by its
length:
LZ_PARSE(data, literal_run, literal, match_run, length, offset, repeat)
A repeat comes back with an offset of 0. The search is still exact, though it has more to keep track of: which offset is worth having in hand depends on the bytes still to come, so it tries every offset for the last match of each run.
Some formats allow a repeat only after certain literal runs, and make
every match there pay to say whether it is one. Exomizer, for instance,
allows one only straight after a single literal, where every match
carries a flag bit. Then repeat is a pair,
{lengths, after}: after gives, by the count of
the literal run before, what any match there pays first, with a negative
entry where no repeat can follow:
LZ_PARSE(data, literal_run, literal, match_run, length, offset, {repeat, {-1, 1}}) ; Exomizer's rule
(The first literal run never has an offset in hand, so it pays nothing.) The search assumes that neither a literal run's count, with what a match after it pays, nor a repeat's length ever gets cheaper as it grows.
Examples
examples/compress puts it all together, four times:
lz.6502is a complete compressor in a few dozen lines. It tries every offset split and keeps the smallest.exolite.6502is a format inspired by Exomizer, though not Exomizer's own. It fits a table of bit widths to the counts, lengths and offsets a parse used, parses again with those tables, and repeats while that saves anything. The fitting is written with lists too.zx02.6502writes dmsc's ZX02 format, repeat matches included. Its literals and offsets sit in the stream as whole bytes between the bytes of bits, and theFUNCTIONs lay them out by sorting on position.exomizer.6502writes Exomizer's own format (asexomizer raw -T5 -M256would), fitting its 52 bucket widths the wayexolite.6502does, with an offset table each for 1-byte, 2-byte and longer matches, and offset reuse switched on or off by the loader. Exomizer's own 6502 decruncher unpacks it.
Each has a loader whose decompressor unpacks the same title screen straight into screen memory, and all four loaders go on one disc. ZX02's decompressor is the smallest and quickest to unpack; Exomizer packs the screen smallest.
Logging
PRINT writes text during assembly - values concatenated
with no separator, one newline per statement:
PRINT "code ends at ", *, " (", * - start, " bytes)"
Strings print raw and numbers in decimal; spacing is yours to supply.
A PRINT speaks exactly once per assembly, on the final
pass, with everything resolved.
For the BBC's own dialect, ~ turns a number into a hex
string - two digits, four or eight, whichever is the narrowest that
holds it, and the & is yours to write:
PRINT "code ends at &", ~*, " (", * - start, " bytes)" ; code ends at &1F40 (2112 bytes)
Like < and >, it swallows the whole
expression after it, so ~start+1 is the hex of
start+1. For decimal text, there's AS_STRING
(see Numbers as text).
There are ten channels, #0 to #9, selected
by a leading #n,:
PRINT #1, "symbol dump: start = ", start
Channel 0 is the default and goes to stdout (interleaved with the
-v listing, if you asked for one). Channels 1-9 go nowhere
unless the command line redirects them: -log1 symbols.txt
collects everything printed to #1 into that file.
-log0 captures channel 0 too, if you want stdout kept
clear.
ERROR is PRINT's grumpy sibling: it reports
its message as a genuine assembly error and fails the build (though
assembly carries on, so you still get the full report):
IF * > &3000 : ERROR "code overran the screen by ", * - &3000, " bytes" : ENDIF
ASSERT says the same thing the other way round, in one
line - this is the IF above:
ASSERT * <= &3000, "code overran the screen by ", * - &3000, " bytes"
The message is only evaluated when the assertion fails, and it is
optional: without one you get "Assertion failed". ASSERT
works inside a FUNCTION body
too.
The name ERROR doubles as a function in
expression position - ERROR("bad width: ", w) returns an
error value that reports once it is used, which is how a FUNCTION body refuses bad input.
The symbol dump
--symbols <file> writes everything Baron resolved
- every symbol, where it was defined, and the section it belongs to - to
one JSON file once the whole run has assembled:
$ baron --symbols syms.json main.6502 tools.6502
Take this main.6502, which includes a
zp.6502 holding the single line
ptr = &70:
INCLUDE "zp.6502"
width = 32
SECTION game, org=&3000, filename="GAME"
.start LDA #width
SECTION handler, org=&0400
.irq RTI
ENDSECTION
.done RTS
ENDSECTION
Its dump looks like this:
{
"format": 2,
"assemblies": [
{
"sources": ["main.6502", "zp.6502"],
"sections": [
{
"parent": null, "size": 0,
"assignments": {
"ptr": {"value": 112, "source": 1, "line": 1},
"width": {"value": 32, "source": 0, "line": 3}
}
},
{
"name": "game", "parent": 0, "size": 4,
"attributes": {
"org": 12288,
"filename": "GAME"
},
"labels": {
"done": {"value": 12291, "source": 0, "line": 10},
"start": {"value": 12288, "source": 0, "line": 6}
}
},
{
"name": "handler", "parent": 1, "size": 1,
"attributes": {
"org": 1024
},
"labels": {
"irq": {"value": 1024, "source": 0, "line": 8}
}
}
]
}
]
}Assemblies
assemblies has one entry per source file on the command
line, in order. Each file assembles in its own universe, so their
symbols are never merged.
Indices are local to their entry: "source" indexes that
entry's sources, and "parent" indexes its
sections. Two files each have their own source 0 and their
own section 0.
Sources
sources lists every file the assembly read, each once,
however many times it was included. sources[0] is the file
named on the command line. A predefined
symbol gets a source of its own, named after its switch -
-D DEBUG=1.
Sections
sections lists every section in the order it opened.
Entry 0 is the file's default section, which holds everything outside a
SECTION: it has no name and no parent, and its
size counts only the bytes written outside every section -
none, above.
A nested section's bytes are part of its parent's, so a parent's
size includes its children - game above is 4
bytes, handler's RTI among them. A top-level
section's parent is 0, the default it was written in,
though none of its bytes land there. A reopened section appears once,
with its parent taken from where it was first written and a
size covering every piece.
attributes holds the attributes exactly as the
SECTION line wrote them, in order. A section with no
attributes leaves it out.
Since nothing is inherited, an org appears only on a
section that set one. That tells you about addresses: a section with an
org starts addresses of its own, while one without carries
on from its parent's. To find which address space a label lives in, walk
up parent to the nearest section with an org.
Here, irq and start are in different spaces
even though both sit inside game's bytes.
Symbols
Each symbol sits in the innermost section open where it was defined, keyed by its full dotted path. Within a section, symbols are grouped by kind:
| Group | Holds |
|---|---|
labels |
.name and .@ labels - always
addresses |
assignments |
name = expr |
defines |
-D predefines |
za_autos |
ZA_AUTO variables - each one's allocated address |
loop_vars |
FOR variables, one per iteration |
params |
macro parameters |
A group with nothing in it is left out. For a label, its section is the one whose bytes it addresses. Any other kind just lands in whichever section its line sits in.
Each symbol gives its value, plus the
source and line of the statement that defined
it. For a label inside a macro, that is the line in the macro's body,
while its section is wherever that expansion was assembled.
Values keep their types: numbers as numbers, booleans as
true/false, strings as strings, and lists and
ranges as arrays.
Grepping and diffing
Symbols are sorted within their group, one per line, so
grep '"start"' syms.json answers with its address and line,
and two builds' dumps diff cleanly.
Baron's internals are included too: symbols inside anonymous
{ } scopes, per-iteration FOR frames, macro
calls, and local labels appear under unspellable @ keys
("@0:3:0.i" is iteration 0's i). Filter out
keys containing @ if only the names you wrote matter. A
FUNCTION call's parameters and locals never appear: they
only exist while the call runs.
Like every other output, the file is only written when every file
assembled, and --check skips the write.
Saving your work
Naming a file is the request to save: every section with a
filename attribute is a candidate, and nothing else is.
Where those candidates actually go is the command line's call, and there
are two destinations:
- Raw binaries on the host:
-p buildwrites each named section as a file of that name in thebuilddirectory (which must already exist;-p .for the current one). Add--infto write a.infsidecar beside each ($.CODE 001100 001100 001F40style), which most emulators and transfer tools read. - A disc image:
-o game.ssdgathers the same sections onto a DFS single-sided disc image.--titlesets the disc title,--optthe*OPT4boot option (3execs a!BOOTyou have supplied as just another named section),--cyclethe catalogue cycle number. The image is truncated after the last used sector (emulators read the missing remainder as zeroes);--padwrites it out to a full 200K disc instead, for the tools that expect one.
Neither is implied, and they are not exclusive -
baron -p build -o game.ssd ... writes both. A run given
neither still assembles, reports, lists and prints; it simply has
nowhere to put the bytes, and warns you in case that was not what you
meant. (--check is the way to say it deliberately: assemble
and validate everything, write nothing, no grumbling.)
A multi-file run (baron -p . main.6502 loader.6502)
assembles each file independently and pools all their saved sections
into the output. If anything failed to assemble, nothing at all
is written - no half-built discs.
Examples
Take a look in the examples/ directory for some Baron
source code samples, including the obligatory spinning star globe (now
with even more dots!), and a title screen compressed two ways by section transforms as it assembles.
That is the tour. The full switch, keyword and operator tables await in the reference.