A simple interpreted language that mimics the look and feel of assembly
- Run
make
in the root directory and use the executable generated as./bass <filename>.bass
- Try running some examples such as
./bass examples/fact.bass
Hello World is as simple as
println "hello world"
The following example performs some basic arithmetic operations and prints the result.
move r0 #25
move r1 #0x10
add r2 r0 r1
mul r3 r2 #9
println r3
A more complicated program that prints the Fibonacci series.
; Prints first 10 numbers of the Fibonacci Series
move r0 #0
move r1 #0
move r2 #1
loop:
add r0 r0 #1
println r1
add r3 r1 r2
move r1 r2
move r2 r3
cmp r0 #10
jumpl loop ; jump if lesser (only jumps when last cmp result was lesser)
bass
is designed to mimic the look and feel of assembly. Each program consists of a sequence of labels, which act as anchors to jump to, and opcodes along with their respective operands. The basic structure of a program is like:
<LABEL>:
OPCODE <OPERAND_1> <OPERAND_2> ...
...
Each operand to an opcode can be an immediate value (only integers are supported for now), a register, or a memory address. Immediate values are prefixed with a #
, registers with an r
and memory addresses with an @
. All whitespace/indentation is ignored and is optional.
Character literals (delimited by '
) and string literals (delimited by "
) can only be used in print
and println
opcodes. String literals can span multiple lines as well. Character literals only support the escape character \n
for now.
Bare identifiers can only be used in the jump instructions to signify the target label. Also note that labels are followed by a :
. _
is a special label that can be used to mark the entry point. This is useful to start execution from a different location. If no entry point is provided then the execution begins from the start of the file.
; Only prints `bar` since the entry point is specified
println "foo"
_:
println "bar"
Note that _
can only be jumped to if it was explicitly declared.
The special syntax @r[0-7]
can be used as a shortcut for indirect addressing.
This instructs the operation to use the value in the specified register as a memory address. For example, the following program stores, 100 into the memory address, 40 using this syntax, and then prints it
move r0 #40
move @r0 @100
println @40
There are 8 registers, r0
to r7
, which can be used for direct operations. All registers are initialized to 0 at the program start. There are two special registers, the program counter and stack pointer which are inaccessible through bass
for now. Another flag variable stores the result of the last comparison (can be 0, -1 or 1) and is also inaccessible through bass
.
A total of 4MB of addressable memory is available, which is also initialized to 0 at program start. All addresses are simply an index from the start of the memory. When storing integers into memory, make sure to properly align them to 4 bytes (or whatever sizeof(int)
is) to prevent unexpected behaviour. For example, storing elements at @0
, @4
, and @8
simultaneously should be fine, but trying to access or store elements at @5
will instead create a view into the middle of integers in the memory.
A common pattern with any opcode that stores some value is that, the first operand is the location where the result is stored.
For example,
move r0 #16 ; r0 := 16
add r1 r0 #1 ; r1 := r0 + 1
pop @90 ; @90 := [STACK_TOP]
nop
- does nothing
All of the following perform some arithmetic operation and store the result
add
sub
mul
div
mod
Examples
add r0 r0 #1 ; ro := r0 + #1
mul @45 #90 r5 ; @45 := #90 + r5
move
- move value into register or memorystore
- move value into memory using indirect addressingload
- load value from memory using indirect addressing
Examples
move @250 #12 ; @250 := 12
move r0 #12
store r0 #1000 ; @r0 := 1000 (store 1000 at memory address 12 (value of register r0))
print
- print registers, memory, numbers, characters, stringsprintln
- same asprint
but puts a newline at the end
Examples
print r1
print #23
print 'a'
print '\n'
print "hello"
push
- push value into stackpop
- pop value from stack into register or memory
Examples
push #12 ; [STACK_TOP] := 12
push r0 ; [STACK_TOP] := r0
pop @45 ; @45 = [STACK_TOP]
cmp
- sets the comparison flag (0 if equal, 1 if first is greater, -1 if first is less)jump
- unconditional jump to labeljumpz
- jump if comparison flag is 0jumpg
- jump if comparison flag is 1jumpl
- jump if comparison flag is -1call
- jump to a label after pushing the current program counter onto the stackreturn
- pop the stack and jump to the address
These opcodes can also be used with literals or registers to jump to arbitrary opcodes. For example, opcodes are 0-indexed, so jump #0
will start executing the program from the beginning.
Examples
start:
cmp r0 #12
jumpz end ; jump to label `end` if the last comparison resulted in equals
add r0 r0 #1
jump start ; always jump to label `start` at this point
end:
hello:
println "Hello World"
return
_:
call hello
For more examples, check out the examples directory.