bass [options] [-o target] source [source ...]
-d name[=value] will create a define with the given name, and assign to it either an empty value or the value provided.
-c name[=value] will create a constant with the given name, and assign to it either a value of 1 or the value provided.
-strict will abort the assembly process on warnings.
-create will overwrite the target file, if it exists. The default mode is to modify (patch) the target file.
-benchmark will display the time required to assemble the source.
bass is a table-based assembler, which supports multiple architectures, as well as user-defined architectures with appropriate table files provided.
Parsing consists of the following phases:
The tokenize phase will combine all source files, and insert any nested include statements into a single stream of instructions.
The analyze phase will parse blocks, such as macros and functions, and note where they begin and end.
The execute phase will recurse macro invocations, substitute defines and evaluate conditional expressions.
The query phase invokes the execute phase, and computes the values of constants and labels.
The write phase invokes the execute phase, uses the previously computed values for constants and labels, and writes to any opened output file.
Initially, each source file specified on the terminal is loaded in. For each source file, all tabs (\t) and carriage returns (\r) are converted to spaces, and each line is split by line feeds (\n). Next, each line is clipped at the first appearance of a comment marker (//). Then, each line is split by any semicolons (;) not appearing inside of quoted strings. That is to say, semicolons can be used inside of quoted strings. Using semicolons outside of strings splits the line into multiple statements. The semicolon acts as a statement separator, and not as a statement terminator, meaning that a semicolon is not required at the end of each line. Finally, if the statement is an include directive, the source file parser will be invoked recursively to load in nested source files.
Macros, defines, scopes, variables and constants must be in the following format:
[_A-Za-z][_A-Za-z0-9.]*
Valid numbers must be in one of the following formats:
[0-9]+ integer 0b[01]+ binary 0o[0-7]+ octal 0x[0-9a-f]+ hex %[01]+ binary $[0-9a-f]+ hex
Numbers may be prefixed with - or + if desired.
Numbers may also use ' as a digit separator. For example:
123'456'789 //same as 123456789 0b1001'0110 //same as 0b10010110
Strings are surrounded by double-quotes. They support the following escape sequences:
\\ = backslash (\) \s = single-quote (') \d = double-quote (") \b = block separator (;) \n = line feed
Characters are surrounded by single-quotes, and evaluate to integer values which can be used inside of expressions. They support the same escape sequences as strings.
Note that characters are not escaped for block tokenization. That means you must use '\b' instead of ';' to avoid splitting the character into two separate statements.
Execution acts much like a scripting language. Statements are evaluated and a control flow (stack) is maintained.
Defines can be used to substitute values in expressions. The define keyword allows specifying an exact expression to substitute, whereas the evaluate keyword will evaluate the expression to an integer value. The latter is useful for conditional expressions. Defines must be declared before being used, and can be re-declared later on.
define x(1 + 2) print "{x}\n" //prints "1 + 2\n" evaluate x({x} + 3) //x = evaluate(1 + 2 + 3) print "{x}\n" //prints "6\n"
Defines can be invoked with: {defineName}. If a define is not matched, there is no error, the literal {defineName} will be passed along to the assembler verbatim.
Defines are evaluated from right-to-left order, meaning that expressions such as {x{y}} will first expand {y}, and then the result of that expression, {x...}.
It is possible to test if a define has been declared or not by using this special syntax: {defined name}
//create {value} if it does not yet exist if !{defined value} { define value(...) }
{defined name} is substituted with either 1 (if a define by the given name exists) or 0 (if it does not.)
Macros are supported. They can take zero or more arguments, and name overloading with differing arity is possible. Recursion is supported, but requires conditionals in order to break infinite recursion. Macros must be declared before being used, and can be re-declared later on.
By default, macro parameters are simply the names of the values, and are passed in as defines. It is also possible to specify the type of the parameter, which will cause the invocation to pass the value in as the requested type. Supported types are: define, evaluate and variable.
macro seek(offset) { origin {offset} & 0x3fffff base 0xc00000 | {offset} } seek(0xc08000) macro test(define a, evaluate b, variable c, d) { //{d} has no type, so it defaults to "define d" } test(1+2, 1+2, 1+2, 1+2) //{a} = 1+2, {b} = 3, c = 3, {d} = 1+2
Because expanded macros are passed directly to the assembler, a macro with a label name cannot be expanded twice in the same scope, or the label name will be declared twice, resulting in an error. The special token {#} can be used in a label name, where it will be substituted with a numeric value that increments every time a macro is invoked.
Macros can be invoked with the syntax: macroName(parameter, parameter, ...). If a macro is not matched, there is no error, the literal macroName(...) will be passed along to the assembly phase. Note that macros cannot appear inside expressions: the macro invocation must be the entire statement.
Every time a macro is invoked, a new object stack is created, which will supercede all previous stacks. All macro arguments, as well as any objects declared inside of said macro, are appended to the new stack. When the macro completes execution, said stack is destroyed, and said objects are lost. Note that this does not apply to constants, which must always be placed in the global scope to support forward-declarations.
It is however possible to access the global define scope by prefixing object creation with the global keyword, for example:
macro square(value) { global evaluate result({value} * {value}) } square(16) print "{result}\n" //prints 256
bass supports traditional conditional expressions.
define x(16) while {x} > 0 { print "{x}\n" evaluate x({x} - 1) } if {x} > 16 { ... } else if {x} > 8 { ... } else { ... }
Variables and constants can be used in conditional expressions. Just note that variables must be declared before they can be used in expressions. Only constants support forward-declaration.
Any statements that fall through the execute phase are passed into the assembly phase.
Variables and constants hold integer values. Variables must be declared before being used, but can be redefined. Constants can be used before their declaration, but subsequently cannot be redefined. Labels are stored as constants.
variable x(16) lda #x //16 variable x(32) lda #x //32 lda #y //64 constant y(64)
Labels can be created with the syntax: labelName:
loop: dex; bne loop
Labels without names can be created using - and +.
-; beq +; lsr; dex; bne -; + -; bra ++ //A: go to D -; bra + //B: go to C +; bra - //C: go to B +; bra -- //D: go to A
The previous - label can be referenced with -, and the next + label can be referenced with +. The second to last - label can be referenced with --, and the second to next + label can be referenced with ++. Deeper scoping is not supported: you will have to switch to named labels at this point.
Macros, defines, variables and constants can be scoped. This allows reuse of common names like loop and finish inside of parent scopes, without causing declaration collisions. Note that labels are stored as constants, meaning that scoping also applies to labels.
It's also important to understand that for macro scoping, the macro name's scope is determined where the macro is declared, and the actual scope used while executing a macro is determined where the macro is invoked.
variable offset(16) scope information { variable length(32) lda #offset //16 lda #length //32 } lda #offset //16 lda #information.length //32
It is possible to create a new scope that matches the name of the macro when a macro is invoked, which is destroyed when the macro terminates. This is effectively a short-hand syntax for specifying a macro and then a scope inside of the macro.
macro unscoped() { variable x(16) } unscoped(); print x, "\n" macro scope scoped() { variable x(16) } scoped(); print scoped.x, "\n"
The scope keyword is placed after the macro keyword to signify that the scope applies to the macro invocation, rather than to the macro declaration.
It is possible to declare a scope and label at the same time, which is a useful way to mark functions and their boundaries.
scope labelName: { }
It is also possible to create blocks which do not create scopes. These are used strictly for code clarity, and have no functional effect.
labelName: { } - { } + { } { }
Note that this command is parsed in the very first phase, and is only noted here for completeness. It includes another source file in place of this command.
Do not attempt conditional recursion on the same source file, as this will result in an infinite loop which will eventually exhaust all memory.
This command can be used in place of the -o filename [-create] command-line argument, or in addition to it, and can open multiple files sequentially for output (only one output file can be open at a time.) The create parameter, if specified, states to overwrite the target file if it already exists. Otherwise, the file is opened in modification mode.
This command controls whether multi-byte values (eg from dw and dd) are output in little-endian (lsb) or big-endian (msb) format.
This command seeks the output file write cursor to the specified location.
This command creates a signed displacement against the origin value, which is used when computing the pc (program counter) value for labels. This command allows mapping file address space into a virtual memory address space.
This can be used to save and restore internal state. Currently supported values are: origin, base, pc.
This command inserts a binary file into the target file. You can optionally specify a name, offset and length. If you specify a name, it will create a label by the given name, which contains the address where the data begins, and it will also create name.size, which contains the size of the included data. If you specify an offset, it will seek that far into the referenced filename before copying the data. If you want to specify a length, you must specify an offset first, and the length will determine the maximum number of bytes to copy from the referenced filename.
Inserts length number of bytes into the target file. The default fill byte is 0x00, but can be specified via with.
Modifies the mappings for strings passed to db, dw, etc. This can be used to map strings to custom tilemaps that do not follow traditional ASCII values.
char is the first value to modify, value is the value to map said char to, and length can be used for contiguous entries. For instance, if A-Z appear sequentially, give a value of 26 for the length, to avoid having to declare 26 separate assignments. Each step of length increments both the char and value by exactly one, so the characters must be contiguous with both ASCII and your custom map for this to work.
If you wish to restore the table to its default ASCII values, use the following command:
map 0, 0, 256
Inserts binary data directly into the target file. db stores 8-bit values, dw stores 16-bit values, dl stored 24-bit values, dd stored 32-bit values and dq stores 64-bit values.
Prints information to the terminal. Useful for debugging.
Prints a notice to the terminal, but continues assembly.
Prints a warning to the terminal, but continues assembly.
Prints an error to the terminal, and aborts assembly.
bass supports built-in functions. The syntax is equivalent to macros, however they are used in expressions rather than as statements, and they always return a numeric value.
seek(0x8000) //this is a macro statement ... if pc() > 0x8fff { //this is a function used inside an assembler statement ... }
Returns the current origin.
Returns the current base.
Returns the current program counter (origin + base.)
Writes the specified character to the terminal, and returns the value of the printed character. Useful for implementing specialized print functions, such as printing hex values.
Hopefully this has been informative. The best way to learn is through practice, so please do experiment and see what you can come up with!
Thank you for using bass!