forked from MightyPork/crsn
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1035 lines
31 KiB
1035 lines
31 KiB
# CROISSANT VIRTUAL MACHINE
|
|
|
|
Croissant (or *crsn* for short) is an extensible runtime emulating a weird microcomputer (or not so micro, that depends on what extensions you install).
|
|
|
|
## FAQ
|
|
|
|
### What is this for?
|
|
|
|
F U N
|
|
|
|
### How is the performance?
|
|
|
|
Silly fast, actually. 60fps animations are perfectly doable if that's your thing.
|
|
It's probably faster than you need for most things, actually.
|
|
|
|
You can slow it down using the `-C` argument, or using sleep instructions.
|
|
|
|
### What if I don't enjoy writing assembly that looks like weird Lisp?
|
|
|
|
Maybe this is not for you
|
|
|
|
### Shebang?
|
|
|
|
Yes! You can use crsn as a scripting language!
|
|
|
|
The first line from a source file is skipped if it starts with `#!`
|
|
|
|
### Contributing
|
|
|
|
Yup, go ahead. You can also develop your own private *crsn* extensions, they work like plugins.
|
|
|
|
# Architecture
|
|
|
|
The runtime is built as a register machine with a stack and status flags.
|
|
|
|
- All mutable state (registers and status), called "execution frame", is local to the running routine or the root of the program.
|
|
- A call pushes the active frame onto a frame stack and a clean frame is created for the callee.
|
|
- The frame stack is not accessible to the running program, it is entirely handled by the runtime.
|
|
- When a call is made, the new frame's argument registers are pre-filled with arguments passed by the caller.
|
|
- Return values are inserted into the callee's frame's result registers before its execution resumes.
|
|
|
|
## Registers
|
|
|
|
- 16 general purpose registers `r0`-`r15`
|
|
- 16 argument registers `arg0`-`arg15`
|
|
- 16 result registers `res0`-`res15`
|
|
- 16 global registers `g0`-`g15`
|
|
|
|
Global registers are accessible everywhere. Other registers are only valid within an execution frame (in a routine, or the initial scope).
|
|
|
|
All registers are 64-bit unsigned integers that can be treated as
|
|
signed, if you want to. Overflow is allowed and reported by status flags.
|
|
|
|
8-, 16-, 32-bit and floating point arithmetic is not currently implemented, but will be added later. Probably. Maybe.
|
|
|
|
## Status flags
|
|
|
|
Arithmetic and other operations set status flags that can be used for conditional jumps.
|
|
|
|
- Equal … Values are equal
|
|
- Lower … A < B
|
|
- Greater … A > B
|
|
- Zero … Value is zero, buffer is empty, etc.
|
|
- Positive … Value is positive
|
|
- Negative … Value is negative
|
|
- Overflow … Arithmetic overflow or underflow, buffer underflow, etc.
|
|
- Invalid … Invalid arguments for an instruction
|
|
- Carry … Arithmetic carry; used by extensions (currently unused, planned for the byte/halfword/word versions of the arith module)
|
|
- Full … full condition; used by extensions
|
|
- Empty … empty condition; used by extensions
|
|
- EOF … end of a stream, file, etc; used by extensions
|
|
|
|
### Status tests (conditions)
|
|
|
|
These keywords (among others) are used in conditional branches to specify flag tests:
|
|
|
|
- `eq` … Equal
|
|
- `ne` … NotEqual
|
|
- `z` … Zero
|
|
- `nz` … NotZero
|
|
- `lt` … Lower
|
|
- `le` … LowerOrEqual
|
|
- `gt` … Greater
|
|
- `ge` … GreaterOrEqual
|
|
- `pos` … Positive
|
|
- `neg` … Negative
|
|
- `npos` … NonPositive
|
|
- `nneg` … NonNegative
|
|
- `c` … Carry
|
|
- `nc` … NotCarry
|
|
- `val`, `valid`, `ok` … Valid
|
|
- `inval`, `nok` … Invalid
|
|
- `ov` … Overflow
|
|
- `nov` … NotOverflow
|
|
- `f`, `full` … Full
|
|
- `nf`, `nfull` … Not full
|
|
- `em`, `empty` … Empty
|
|
- `nem`, `nempty` … Not empty
|
|
- `eof` … EOF
|
|
- `neof` … Not EOF
|
|
- `else` … Always true, may be used in the last branch
|
|
|
|
# Syntax
|
|
|
|
*The syntax is very much subject to change at the moment. The format described here
|
|
is valid at the time this file is added to version control.*
|
|
|
|
Instructions are written using S-expressions, because they are easy to parse
|
|
and everyone loves Lisp.
|
|
|
|
## Program
|
|
|
|
A program has this format:
|
|
|
|
```
|
|
(
|
|
...<instructions and routines>...
|
|
)
|
|
```
|
|
|
|
e.g.
|
|
|
|
```
|
|
(
|
|
(ld r0 100) ; load value into a register
|
|
(:again) ; a label
|
|
(sub r0 1 ; subtract from a register
|
|
(nz? ; conditional branch "not zero?"
|
|
(j :again))) ; jump to the label :again
|
|
)
|
|
```
|
|
|
|
The same program can be written in a compact form:
|
|
|
|
```
|
|
((ld r0 100)(:again)(sub r0 1 (nz? (j :again))))
|
|
```
|
|
|
|
## Instruction
|
|
|
|
Instructions are written like this:
|
|
|
|
```
|
|
(<keyword> <args>... <conditional branches>...)
|
|
```
|
|
|
|
### Conditional instructions
|
|
|
|
All instructions can be made conditional by appending `.<cond>` to the keyword, i.e. `(j.ne :LABEL)` means "jump if not equal".
|
|
These modifiers are mainly used by the assembler when translating conditional branches to executable code.
|
|
|
|
Note that the flags can only be tested immediately after the instruction that produced them, or after instructions that do not
|
|
affect flags (pseudo-instructions like `def` and `sym`, `nop`, `j`, `fj`, `s`, `call` etc). Instructions that can set flags first
|
|
clear all flags to make the result predictable.
|
|
|
|
Status flags can be saved to and restored from a register using the `stf` and `ldf` instructions. This can also be used to set
|
|
or test flags manually, but the binary format may change
|
|
|
|
### Instruction arguments
|
|
|
|
Arguments are always ordered writes-first, reads-last.
|
|
|
|
This document uses the following notation for arguments:
|
|
- `REG` - one of the registers (`regX`, `argX`, `resX`)
|
|
- `SYM` - a symbol defined as a register alias (e.g. `(sym x r0)`)
|
|
- `@REG` / `@SYM` - access an object referenced by a handle. Handle is simply a numeric value stored in a register of some kind.
|
|
- `_` - a special "register" that discards anything written to it.
|
|
The "discard register" is used when you do not need the value and only care about side effects or status flags.
|
|
- `CONST` - name of a constant defined earlier in the program (e.g. `(def SCREEN_WIDTH 640)`)
|
|
- `NUM` - literal values
|
|
- unsigned `123`
|
|
- signed `-123`
|
|
- float `-45.6789`. For now, you must use literals with a period to enter float literals, integers will not be converted to
|
|
float when used in floating point instructions!
|
|
- hex `0xabcd`, `#abcd`
|
|
- binary `0b0101`
|
|
- character `'a'`, `'🐁'`. Supports unicode and C-style escapes. Use `\\` for a literal backslash.
|
|
- `"str"` - a double-quoted string (`"ahoj\n"`). Supports unicode and C-style escapes. Use `\\` for a literal backslash.
|
|
- `:LABEL` - label name
|
|
- `PROC` - routine name
|
|
- `PROC/A` - routine name with arity (number of arguments)
|
|
|
|
The different ways to specify a value can be grouped as "reads" and "writes":
|
|
|
|
- `Rd` - read: `REG`, `SYM`, `@REG`, `@SYM`, `VALUE`, `CONST`
|
|
- `Wr` - writes: `REG`, `SYM`, `@REG`, `@SYM`, `_`
|
|
- `RW` - intersection of the two sets, capable of reading and writing: `REG`, `SYM`, `@REG`, `@SYM`
|
|
|
|
Objects (`@reg`, `@sym`) can be read or written as if they were a register, but only if the referenced object supports it.
|
|
Other objects may produce a runtime fault or set the INVALID flag.
|
|
The object syntax is also used to read values yielded by a generator-like coroutine.
|
|
|
|
In the instruction lists below, I will use the symbols `Rd` for reads, `Wr` for writes, `RW` for read-writes, and `@Obj` for object handles,
|
|
with optional description after an apostrophe, such as: `(add Wr'dst Rd'a Rd'b)`.
|
|
|
|
Some instructions use bit offsets and widths. Width is directly attached to the opcode (e.g. `(ld16 …)`);
|
|
offsets are attached at the respective arguments after a colon: `(ld16 r0:8 r1:32)` -
|
|
load 16 bits, starting at offset 32 of `r1`, into `r0`, starting at offset 8. The rest if the register is not affected.
|
|
|
|
### Compile-time arithmetics
|
|
|
|
Quite often you will want to do some calculations at compile time, for example to create a constant whose value depends on another,
|
|
or to compose a binary value using bit shifts and masking. This can be done in any place that expects an input operand (`Rd`).
|
|
|
|
The syntax for this is as follows:
|
|
|
|
```
|
|
(ld r0 (=add 123 456))
|
|
```
|
|
|
|
The expressions can be nested. The equals sign is not required in the inner levels, and the output operand must be omitted
|
|
(the compiler inserts a placeholder register there during evaluation):
|
|
|
|
```
|
|
(def WIDTH 1024)
|
|
(def HALFW_MAX (=sub (div WIDTH 2) 1))
|
|
```
|
|
|
|
Almost any instruction can be evaluated this way, excluding instructions that perform IO, work with the screen, objects etc.
|
|
Instructions that are not compile-time evaluation safe will produce a compile error.
|
|
|
|
There are several limitations to consider:
|
|
|
|
- Only immediate values (literals) and constant names may be used.
|
|
- Registers, if used, will always read as zero and writes have no effect. (This should also produce an error)
|
|
- Only instructions that take the form `(op Wr ...)` can be used. The `Wr` operand is inserted automatically and must NOT be specified!
|
|
- Conditionals are not allowed inside expressions: branches, conditional suffixes
|
|
|
|
### Conditional branches
|
|
|
|
Conditonal branches are written like this:
|
|
|
|
```
|
|
(<cond>? <instructions>...)
|
|
```
|
|
|
|
- If there is more than one conditional branch chained to an instruction,
|
|
then only one branch is taken - there is no fall-through.
|
|
- The definition order is preserved, i.e. if the `inval` flag is to be checked, it should be done
|
|
before checking e.g. `nz`, which is, incidentally, true by default, because most flags are cleared by instructions that affects flags.
|
|
- `else` can be used as a final choice of branch that will always be taken.
|
|
|
|
## Routines
|
|
|
|
A routine is defined as:
|
|
|
|
```
|
|
(proc <name>/<arity> instructions...)
|
|
```
|
|
|
|
- `name` is a unique routine name
|
|
- `arity` is the number of arguments it takes, e.g. `3`.
|
|
- you can define multiple routines with the same name and different arities, the correct one will be used depending on how it's called
|
|
|
|
Or, with named arguments:
|
|
|
|
```
|
|
(proc <name> <arguments>... instructions...)
|
|
```
|
|
|
|
Arguments are simply aliases for the argument registers that can then be used inside the routine.
|
|
|
|
Here is an example routine to calculate the factorial of `arg0`:
|
|
|
|
```
|
|
(proc fac/1
|
|
(cmp arg0 2 (eq? (ret 2)))
|
|
(sub r0 arg0 1)
|
|
(call fac r0)
|
|
(mul r0 arg0 res0)
|
|
(ret r0)
|
|
)
|
|
```
|
|
|
|
It can also be written like this:
|
|
|
|
```
|
|
(proc fac num
|
|
...
|
|
)
|
|
```
|
|
|
|
...or by specifying both the arity and argument names:
|
|
|
|
```
|
|
(proc fac/1 num
|
|
...
|
|
)
|
|
```
|
|
|
|
## Coroutines
|
|
|
|
Croissant implements something that could be called "pre-emptive coroutines". They do not provide any performance gain,
|
|
but add asynchronicity to the program, and can work as generators!
|
|
|
|
There is no true parallelism, it is difficult to implement safely and efficiently with a global state.
|
|
|
|
### Spawning
|
|
|
|
*Any procedure can be used as a coroutine.*
|
|
|
|
A coroutine is created using the `spawn` instruction, which produces an object handle.
|
|
|
|
```
|
|
(spawn r0 do_stuff 1 2 3)
|
|
```
|
|
|
|
At this point, the program is evenly divided between the original and the coroutine "thread".
|
|
|
|
The spawned coroutine is scheduled to run immediately after being spawned.
|
|
|
|
### Task switching
|
|
|
|
Coroutines take turns to execute the program. The scheduling interval can be configured.
|
|
|
|
Control can be given up using the `yield` instruction; for example, when waiting for a mutex. This happens automatically when
|
|
a `sleep` instruction is invoked.
|
|
|
|
### Race conditions
|
|
|
|
Take care when working with objects, resources and global registers: you can get race conditions
|
|
with coroutines. Use atomic instructions (`cas`, `casXX`, `bfcas`…) to implement synchronization.
|
|
|
|
The `casXX` instruction is very powerful: you can use one bit of a register as a mutex and the rest of it to store some useful data.
|
|
You can also use one register for up to 64 mutexes.
|
|
|
|
Remember to only use global registers (or buffer items) as mutexes: `g0`-`g15`. Each coroutine has its own set of *regular* registers.
|
|
|
|
Another way to avoid race conditions is to use **critical sections**.
|
|
Context switch (switching between active coroutines) is forbidden in a critical section. Try to keep critical sections as short as possible,
|
|
since they can distort sleep times and cause other similar problems.
|
|
|
|
```
|
|
(crit-begin)
|
|
...
|
|
(crit-end)
|
|
```
|
|
|
|
A safer way is to use the "critical block", which expands to the same, but also detects some common bugs at compile time,
|
|
like trying to jump out of the critical section.
|
|
|
|
```
|
|
(crit
|
|
...
|
|
)
|
|
```
|
|
|
|
Critical section nesting is allowed, but probably a bug.
|
|
|
|
**Beware deadlocks!**
|
|
|
|
### Using coroutines as generators
|
|
|
|
A coroutine can "yield a value" by invoking the `yield` instruction with an operand. This can be done any number of times.
|
|
|
|
```
|
|
(yield r0)
|
|
```
|
|
|
|
The coroutine is blocked until the value is consumed by someone. To consume a yielded value, read the coroutine object handle:
|
|
|
|
```
|
|
(spawn r5 foo)
|
|
|
|
(ld r0 @r5) ; read a yielded value
|
|
```
|
|
|
|
**Caution!!!** Due to the way this is implemented, the instruction that tries to use a yielded value might partially execute
|
|
before detecting that it is blocked, and will be retried when the thread is next scheduled to run. This has the consequence
|
|
that if something like a object handle is read by the same instruction, it may be read multiple times while waiting for the
|
|
yielded value. It is recommended to *never combine reads of a yielded value with reads of other object handles*.
|
|
|
|
### Joining a coroutine
|
|
|
|
Use the `join` instruction with a coroutine object handle to wait for its completion.
|
|
|
|
A coroutine completes by calling `ret` at its top level. This naturally means that a coroutine can return values!
|
|
|
|
The returned values are placed in the result registers, just like with the `call` instruction.
|
|
|
|
```
|
|
(spawn r5 foo)
|
|
; ...
|
|
|
|
(join @r5)
|
|
; res0-res15 now contain return values
|
|
```
|
|
|
|
# Instruction Set
|
|
|
|
Crsn instruction set is composed of extensions.
|
|
|
|
Extensions can define new instructions as well as new syntax, so long as it's composed of valid S-expressions.
|
|
|
|
## Labels, jumps and barriers
|
|
|
|
These are defined as part of the built-in instruction set (see below).
|
|
|
|
- Barrier - marks the boundary between routines to prevent overrun. Cannot be jumped across.
|
|
- Local labels - can be jumped to within the same routine, both forward and backward.
|
|
- Far labels - can be jumped to from any place in the code using a far jump (disregarding barriers).
|
|
This is a very cursed functionality that may or may not have some valid use case.
|
|
- Skips - cannot cross a barrier, similar to a jump but without explicitly defining a label.
|
|
All local jumps are turned into skips by the assembler.
|
|
|
|
Skipping across conditional branches may have *surprising results* - conditional branches are expanded
|
|
to a varying number of skips and conditional instructions by the assembler. Only use skips if you really know what you're doing.
|
|
|
|
Jumping to a label is always safer than a manual skip.
|
|
|
|
## Built-in Instructions
|
|
|
|
...and pseudo-instructions
|
|
|
|
```
|
|
; Do nothing
|
|
(nop)
|
|
|
|
; Stop execution
|
|
(halt)
|
|
|
|
; Define a register alias.
|
|
; The alias is only valid in the current routine or in the root of the program.
|
|
; However, if the register is a global register, then the alias is valid everywhere.
|
|
(sym SYM REG)
|
|
|
|
; Define a constant. These are valid in the entire program following the definition point, unless
|
|
; un-defined. Constants are evaluated and assigned at compile time, the program control flow has no
|
|
; effect.
|
|
; Value must be known at compile time.
|
|
(def CONST VALUE)
|
|
|
|
; Mark a jump target.
|
|
(:LABEL)
|
|
; Numbered labels
|
|
(:#NUM)
|
|
|
|
; Mark a far jump target (can be jumped to from another routine).
|
|
; This label is preserved in optimized code.
|
|
(far :LABEL)
|
|
|
|
; Jump to a label
|
|
(j :LABEL)
|
|
|
|
; Jump to a label that can be in another function
|
|
(fj :LABEL)
|
|
|
|
; Skip backward or forward
|
|
(s Rd)
|
|
|
|
; Copy a value
|
|
(ld Wr Rd)
|
|
|
|
; Copy lower XX bits (the rest is untouched).
|
|
; Offsets can be specified to work with arbitrary bit slices
|
|
(ldXX RW:dst_offset Rd:src_offset)
|
|
(ldXX Wr Rd:dst_offset Rd:src_offset)
|
|
|
|
; Copy a value N times. This is useful when used with stream handles or buffers.
|
|
(ldn Wr Rd'src Rd'count)
|
|
|
|
; Write a sequence of values, or all codepoints from a string, into the destination.
|
|
; This is most useful with object handles, such as a buffer or @cout.
|
|
; Functionally, this instruction is equivalent to a sequence of "ld"
|
|
(lds Wr (Rd...)) ; example - (lds @cout (65 66 67))
|
|
(lds Wr "string")
|
|
; Some objects can be used as the source for "lds":
|
|
; - @cin = read all to EOF or the first fault (invalid utf8)
|
|
; - @buffer = read all items in a buffer, first to last, without consuming it
|
|
(lds Wr @Obj)
|
|
|
|
; Exchange two register's values
|
|
(xch RW RW)
|
|
|
|
; Exchange XX bits in two registers
|
|
; Offsets can be specified to work with arbitrary bit slices
|
|
(xchXX RW:offset RW:offset)
|
|
|
|
; Compare and swap; atomic instruction for coroutine synchronization
|
|
; - The "Equal" flag is set on success, use it with conditional branches or conditional execution.
|
|
; - The new value is not read until the comparison passes and it is needed.
|
|
; This behavior may matter for side effects when used with object handles.
|
|
(cas RW Rd'expected Rd'new)
|
|
|
|
; Compare and swap a bit slice; atomic instruction for coroutine synchronization
|
|
; See (cas) above for more info.
|
|
;
|
|
; Offsets can be specified to work with arbitrary bit slices
|
|
(casXX RW:offset Rd:offset'expected Rd:offset'new)
|
|
|
|
; Store status flags to a register
|
|
(stf Wr)
|
|
|
|
; Load status flags from a register
|
|
(ldf Rd)
|
|
|
|
; Mark a routine entry point (call target).
|
|
(routine PROC)
|
|
(routine PROC/A)
|
|
|
|
; Call a routine with arguments.
|
|
; The arguments are passed as argX. Return values are stored in resX registers.
|
|
(call PROC Rd...)
|
|
|
|
; Spawn a coroutine. The handle is stored in the output register.
|
|
(spawn Wr'handle PROC Rd...)
|
|
|
|
; Exit the current routine (or coroutine) with return values
|
|
(ret Rd...)
|
|
|
|
; Yield control from a coroutine (use when waiting for a mutex to give control early to
|
|
; other coroutines that might be holding it)
|
|
(yield)
|
|
|
|
; Yield a value generated by a coroutine. Gives up control and blocks until the value is consumed.
|
|
; Conversely, an instruction that tries to read a yielded value using the object handle is blocked
|
|
; until such value becomes available.
|
|
(yield Rd'value)
|
|
|
|
; Wait for a coroutine to complete, read its return values and delete it.
|
|
(join @Obj)
|
|
|
|
; Begin a critical section (no context switch allowed)
|
|
(crit-begin)
|
|
; End a critical section
|
|
(crit-end)
|
|
|
|
; Shortcut to define a critical section
|
|
(crit
|
|
...ops
|
|
)
|
|
|
|
; Generate a run-time fault with a debugger message
|
|
(fault)
|
|
(fault message)
|
|
(fault "message text")
|
|
|
|
; Deny jumps, skips and run across this address, producing a run-time fault with a message.
|
|
(barrier)
|
|
(barrier message)
|
|
(barrier "message text")
|
|
|
|
; Block barriers are used for routines. They are automatically skipped in execution
|
|
; and the whole pair can be jumped *across*.
|
|
; The label can be a numeric or string label, its sole purpose is tying the two together. They must be unique in the program.
|
|
(barrier-open LABEL)
|
|
(barrier-close LABEL)
|
|
|
|
; Set coroutine scheduler timeslice (in microseconds). Set to zero to disable preemption.
|
|
(rt-opt RT_TIMESLICE Rd'usec)
|
|
```
|
|
|
|
## Arithmetic Module
|
|
|
|
This module makes heavy use of status flags.
|
|
|
|
Many instructions have two forms:
|
|
- 3 args ... explicit source and destination
|
|
- 2 args ... destination is also used as the first argument
|
|
|
|
```lisp
|
|
; Test properties of a value - zero, positive, negative
|
|
(tst SRC)
|
|
|
|
; Compare two values. Sets EQ, LT, GT, and Z, POS and NEG if the values equal
|
|
(cmp Rd Rd)
|
|
|
|
; Check if a value is in a range (inclusive).
|
|
; Sets the EQ, LT and GT flags. Also sets Z, POS and NEG based on the value.
|
|
(rcmp Rd'val Rd'start Rd'end)
|
|
|
|
; Get a random number
|
|
(rng Wr) ; the value will fill all 64 bits of the target
|
|
(rng Wr Rd'max) ; 0 to max, max is inclusive
|
|
(rng Wr Rd'min Rd'max) ; min to max, both are inclusive
|
|
|
|
; Add A+B
|
|
(add Wr Rd Rd)
|
|
(add RW Rd)
|
|
|
|
; Subtract A-B
|
|
(sub Wr Rd Rd)
|
|
(sub RW Rd)
|
|
|
|
; Multiply A*B
|
|
(mul Wr Rd Rd)
|
|
(mul RW Rd)
|
|
|
|
; Divide A/B
|
|
(div Wr Rd Rd'divider)
|
|
(div RW Rd'divider)
|
|
|
|
; Divide and get remainder
|
|
; Both DST and REM are output registers
|
|
(divr Wr'result Wr'remainder Rd Rd'divider)
|
|
(divr RW Wr'remainder Rd'divider)
|
|
|
|
; Get remainder A%B
|
|
; This is equivalent to (divr _ REM A B),
|
|
; except status flags are updated by the remainder value
|
|
(mod Wr Rd Rd'divider)
|
|
(mod RW Rd'divider)
|
|
|
|
; Get abs value
|
|
(abs Wr Rd)
|
|
(abs RW)
|
|
|
|
; Get signum
|
|
(sgn Wr Rd)
|
|
(sgn RW)
|
|
|
|
; Power - e.g. (pow r0 2 8) is 256
|
|
(pow Wr Rd Rd'pow)
|
|
|
|
; Swap the 32-bit halves of a value
|
|
; 0x01234567_89abcdef -> 0x89abcdef_01234567
|
|
(sw32 Wr Rd)
|
|
(sw32 RW)
|
|
|
|
; Swap 16-bit halves of each 32-bit part
|
|
; 0x0123_4567_89ab_cdef -> 0x4567_0123_cdef_89ab
|
|
(sw16 Wr Rd)
|
|
(sw16 RW)
|
|
|
|
; Swap bytes in each 16-bit part
|
|
; 0x01_23_45_67_89_ab_cd_ef -> 0x23_01_67_45_ab_89_ef_cd
|
|
(sw8 Wr Rd)
|
|
(sw8 RW)
|
|
|
|
; Reverse endian (byte order)
|
|
(rev Wr Rd)
|
|
(rev RW)
|
|
|
|
; Reverse bit order
|
|
(rbit Wr Rd)
|
|
(rbit RW)
|
|
|
|
; Count leading zeros
|
|
(clz Wr Rd)
|
|
(clz RW)
|
|
; Count leading zeros in the lower XX bits
|
|
; Offsets can be specified to work with arbitrary bit slices
|
|
(clzXX Wr Rd:src_offs)
|
|
(clzXX RW:src_offs)
|
|
|
|
; Count leading ones
|
|
(clo Wr Rd)
|
|
(clo RW)
|
|
; Count leading ones in the lower XX bits
|
|
; Offsets can be specified to work with arbitrary bit slices
|
|
(cloXX Wr Rd:src_offs)
|
|
(cloXX RW:src_offs)
|
|
|
|
; Sign extend a XX-bit value to 64 bits, XX in range 1..63)
|
|
(seXX Wr Rd)
|
|
(seXX RW)
|
|
|
|
; AND A&B
|
|
(and Wr Rd Rd)
|
|
(and RW Rd)
|
|
|
|
; OR A|B
|
|
(or Wr Rd Rd)
|
|
(or RW Rd)
|
|
|
|
; XOR A&B
|
|
(xor Wr Rd Rd)
|
|
(xor RW Rd)
|
|
|
|
; CPL ~A (negate all bits)
|
|
(cpl DST A)
|
|
(cpl DST)
|
|
|
|
; Rotate right (wrap around)
|
|
(ror Wr Rd Rd)
|
|
(ror RW Rd)
|
|
|
|
; Rotate left (wrap around)
|
|
(rol Wr Rd'value Rd'count)
|
|
(rol RW Rd'count)
|
|
|
|
; Logical shift right (fill with zeros)
|
|
(lsr Wr Rd Rd'count)
|
|
(lsr RW Rd'count)
|
|
|
|
; Logical shift left (fill with zeros)
|
|
(lsl Wr Rd Rd'count)
|
|
(lsl RW Rd'count)
|
|
|
|
; Arithmetic shift right (copy sign bit)
|
|
(asr Wr Rd Rd'count)
|
|
(asr RW Rd'count)
|
|
|
|
; Arithmetic shift left (this is identical to `lsl`, added for completeness)
|
|
(asl Wr Rd Rd'count)
|
|
(asl RW Rd'count)
|
|
|
|
; Delete an object by its handle. Objects are used by some extensions.
|
|
(del @Rd)
|
|
```
|
|
|
|
### Floating Point Arithmetics
|
|
|
|
The arithmetics module has support for floating point values. There are some gotchas though:
|
|
|
|
- Floating point is simply the binary representation of it in an unsigned integer register.
|
|
I thought of adding special float registers for this, but then you can't easily pass floats
|
|
to subroutines, push them on a stack etc. Not worth it.
|
|
- To enter a float literal, always use the notation with a decimal point. It should support minus and scientific notation too.
|
|
- There are special instructions dedicated to working with floats. The regular integer instructions
|
|
will happily work with their binary forms, but that's absolutely not what you want.
|
|
|
|
```
|
|
(itf r0 1.0) ; NO!!! it is already float, what are you doing
|
|
(ld r0 1.0) ; okay
|
|
(itf r0 1) ; also okay
|
|
|
|
(fmul r0 2) ; NO!!!!!!!!!!!!! 2 is not float
|
|
(mul r0 2.0) ; ALSO NO!!!!!!!!!!!!! mul is not a float instruction!
|
|
(fmul r0 2.0) ; good
|
|
```
|
|
|
|
You have to be a bit careful, that's all.
|
|
|
|
Here's an abridged summary of the floating point submodule:
|
|
|
|
(most of these support the shorthand version too - `RW` in place of `Wr Rd`)
|
|
|
|
```
|
|
; Convert int to float
|
|
(itf Wr Rd)
|
|
; Convert float to int (round)
|
|
(fti Wr Rd)
|
|
; Convert float to int (ceil)
|
|
(ftic Wr Rd)
|
|
; Convert float to int (floor)
|
|
(ftif Wr Rd)
|
|
|
|
; Test properties of a float
|
|
; Set flags:
|
|
; NaN -> invalid
|
|
; Infinities -> overflow
|
|
; Positive, negative, zero
|
|
(ftst Rd)
|
|
; Float compare
|
|
(fcmp Rd Rd)
|
|
; Float range test
|
|
(fcmpr Rd Rd'min Rd'max)
|
|
|
|
; FloatRng. Unlike rng, frng is exclusive in the higher bound
|
|
(frng Wr Rd'min Rd'max)
|
|
|
|
; --- Basic float arith ---
|
|
|
|
; Float add
|
|
(fadd Wr Rd Rd)
|
|
; Float subtract
|
|
(fsub Wr Rd Rd)
|
|
; Float multiply
|
|
(fmul Wr Rd Rd)
|
|
; Float power
|
|
(fpow Wr Rd Rd'pow)
|
|
; Float root
|
|
(froot Wr Rd Rd'root)
|
|
; Float hyp - sqrt(a*a + b*b)
|
|
(fhyp Wr Rd Rd)
|
|
; Float divide
|
|
(fdiv Wr Wr'rem Rd'a Rd'div)
|
|
; Float modulo
|
|
(fmod Wr Rd'a Rd'div)
|
|
; Float abs value
|
|
(fabs Wr Rd)
|
|
; Float signum (returns -1 or 1)
|
|
(fsgn Wr Rd)
|
|
|
|
; --- Basic trig ---
|
|
|
|
; Float sine
|
|
(fsin Wr Rd)
|
|
; Float arcsine
|
|
(fasin Wr Rd)
|
|
; Float cosine
|
|
(fcos Wr Rd)
|
|
; Float arccosine
|
|
(facos Wr Rd)
|
|
; Float tangent
|
|
(ftan Wr Rd)
|
|
; Float arctangent
|
|
(fatan Wr Rd)
|
|
; Float 2-argumenmt arctangent
|
|
(fatan2 Wr Rd'y Rd'x)
|
|
; Float cotangent
|
|
(fcot Wr Rd)
|
|
; Float arccotangent
|
|
(facot Wr Rd)
|
|
|
|
; --- Hyperbolic trig ---
|
|
|
|
; Float hyperbolic sine
|
|
(fsinh Wr Rd)
|
|
; Float hyperbolic arcsine
|
|
(fasinh Wr Rd)
|
|
; Float hyperbolic cosine
|
|
(fcosh Wr Rd)
|
|
; Float hyperbolic arccosine
|
|
(facosh Wr Rd)
|
|
; Float hyperbolic tangent
|
|
(ftanh Wr Rd)
|
|
; Float hyperbolic arctangent
|
|
(fatanh Wr Rd)
|
|
; Float hyperbolic cotangent
|
|
(fcoth Wr Rd)
|
|
; Float hyperbolic arccotangent
|
|
(facoth Wr Rd)
|
|
```
|
|
|
|
Wow, thats a lot. I didn't test many of these yet. There may be bugs.
|
|
|
|
There are also some pre-defined constants: `PI`, `PI_2` (½×`PI`), `TAU` (2×`PI`), `E`
|
|
|
|
## Buffers Module
|
|
|
|
This module defines dynamic size integer buffers.
|
|
|
|
A buffer needs to be created using one of the init instructions:
|
|
|
|
```lisp
|
|
; Create an empty buffer and store its handle into a register
|
|
(mkbf Wr)
|
|
|
|
; Create a buffer of a certain size, filled with zeros.
|
|
; COUNT may be a register or an immediate value
|
|
(mkbf Wr Rd:count)
|
|
|
|
; Create a buffer and fill it with characters from a string (unicode code points)
|
|
(mkbf Wr "string")
|
|
|
|
; Create a buffer and fill it with values.
|
|
(mkbf Wr (Rd...))
|
|
```
|
|
|
|
Buffers can be as stacks or queues by reading and writing the handle (e.g. `(ld @buf 123)`).
|
|
The behavior of reads and writes is configurable per stack, and can be changed at any time.
|
|
The default mode is forward queue.
|
|
|
|
This feature may be a bit confusing at first, but it is extremely powerful.
|
|
One consequence of this feature is that `(ld @buf @buf)` will move items from one end to the other
|
|
in one or the other direction (queue mode), or do nothing at all (stack mode).
|
|
|
|
```list
|
|
; Set buffer IO mode
|
|
; Mode is one of:
|
|
; - BFIO_QUEUE (1)
|
|
; - BFIO_RQUEUE (2)
|
|
; - BFIO_STACK (3)
|
|
; - BFIO_RSTACK (4)
|
|
(bfio @Obj MODE)
|
|
```
|
|
|
|
Primitive buffer ops (position is always 0-based)
|
|
|
|
```lisp
|
|
; Get buffer size
|
|
(bfsz Wr @Obj)
|
|
|
|
; Read from a position
|
|
(bfrd Wr @Obj Rd:index)
|
|
|
|
; Write to a position
|
|
(bfwr @Obj Rd:index Rd)
|
|
|
|
; Insert at a position, shifting the rest to the right
|
|
(bfins @Obj Rd:index Rd)
|
|
|
|
; Remove item at a position, shifting the rest to the left to fill the empty space
|
|
(bfrm Wr @Obj Rd:index)
|
|
|
|
; Buffer value compare and swap; atomic instruction for coroutine synchronization.
|
|
; - The "Equal" flag is set on success, use it with conditional branches or conditional execution.
|
|
; - The new value is not read until the comparison passes and it is needed.
|
|
; This behavior may matter for side effects when used with object handles.
|
|
;
|
|
; This instruction is useful when more than one lock is needed and they are stored in a buffer at well known positions.
|
|
; Naturally, the buffer must not be mutated in other ways that would undermine the locking.
|
|
;
|
|
; If an index just outside the buffer is used, the value is read as zero the position is created (if zero was expected).
|
|
(bfcas @Obj Rd:index Rd'expected Rd'new)
|
|
```
|
|
|
|
Whole buffer manipulation:
|
|
|
|
```lisp
|
|
; Resize the buffer. Removes trailing elements or inserts zero to match the new size.
|
|
(bfrsz @Obj Rd:len)
|
|
|
|
; Reverse a buffer
|
|
(bfrev @Obj)
|
|
|
|
; Append a buffer
|
|
(bfapp @Obj @Obj:other)
|
|
|
|
; Prepend a buffer
|
|
(bfprep @Obj @Obj:other)
|
|
```
|
|
|
|
Stack-style buffer ops:
|
|
|
|
```lisp
|
|
; Push (insert at the end)
|
|
(bfpush @Obj Rd)
|
|
|
|
; Pop (remove from the end)
|
|
(bfpop Wr @Obj)
|
|
|
|
; Reverse push (insert to the beginning)
|
|
(bfrpush @Obj Rd)
|
|
|
|
; Reverse pop (remove from the beginning)
|
|
(bfrpop Wr @Obj)
|
|
```
|
|
|
|
To delete a buffer, use the `del` instruction - `(del @Obj)`
|
|
|
|
## Screen module
|
|
|
|
This module uses the minifb rust crate to provide a framebuffer with key and mouse input.
|
|
|
|
Colors use the `0xRRGGBB` or `#RRGGBB` hex format.
|
|
|
|
If input events are required, then make sure to periodically call `(sc-blit)` or `(sc-poll)`.
|
|
This may not be needed if the auto-blit function is enabled and the display is regularly written.
|
|
|
|
The default settings are 60 FPS and auto-blit enabled.
|
|
|
|
NOTE: Logging can significantly reduce crsn run speed.
|
|
Make sure the log level is at not set to "trace" when you need high-speed updates,
|
|
such as animations.
|
|
|
|
```lisp
|
|
; Initialize the screen (opens a window)
|
|
(sc-init WIDTH HEIGHT)
|
|
|
|
; Set pixel color
|
|
(sc-wr Rd'x Rd'y Rd'color) ; legacy name: sc-px
|
|
|
|
; Get pixel color
|
|
(sc-rd Wr'color Rd'x Rd'y)
|
|
|
|
; Set screen option. Constants are pre-defined.
|
|
; - SCREEN_AUTO_BLIT (1) ... auto-blit (blit automatically on pixel write when needed to achieve the target FPS)
|
|
; - SCREEN_FPS (2) ......... frame rate
|
|
; - SCREEN_UPSCALE (3) ..... upscaling factor (big pixels).
|
|
; Scales coordinates for mouse and pixels and increases pixel area
|
|
(sc-opt OPTION VALUE)
|
|
|
|
; Blit (render the pixel buffer).
|
|
; This function also updates key and mouse states and handles the window close button
|
|
(sc-blit)
|
|
; Blit if needed (when the auto-blit function is enabled)
|
|
(sc-blit 0)
|
|
|
|
; Update key and mouse state, handle the window close button
|
|
(sc-poll)
|
|
|
|
; Read mouse position into two registers.
|
|
; Sets the overflow flag if the cursour is out of the window
|
|
(sc-mouse X Y)
|
|
|
|
; Check key status. Keys are 0-127. Reads 1 if the key is pressed, 0 if not.
|
|
; A list of supported keys can be found in the extension source code.
|
|
; Run the example screen_keys.scn to interactively check key codes.
|
|
(sc-key Wr'pressed Rd'num)
|
|
|
|
; Check mouse button state.
|
|
; 0-left, 1-right, 2-middle
|
|
(sc-mbtn Wr'pressed Rd'btn)
|
|
```
|
|
|
|
Available constants provided by the module:
|
|
|
|
- SCREEN_AUTO_BLIT, SCREEN_FPS, SCREEN_UPSCALE,
|
|
- MBTN_LEFT, MBTN_RIGHT, MBTN_MIDDLE (MBTN_MID)
|
|
- KEY_0, KEY_1, KEY_2, KEY_3, KEY_4, KEY_5, KEY_6, KEY_7, KEY_8, KEY_9 (0-9)
|
|
- KEY_A, KEY_B, KEY_C, KEY_D, KEY_E, KEY_F, KEY_G, KEY_H, KEY_I, KEY_J, KEY_K, KEY_L, KEY_M, KEY_N, KEY_O, KEY_P, KEY_Q, KEY_R, KEY_S, KEY_T, KEY_U, KEY_V, KEY_W, KEY_X, KEY_Y, KEY_Z (10-35)
|
|
- KEY_F1, KEY_F2, KEY_F3, KEY_F4, KEY_F5, KEY_F6, KEY_F7, KEY_F8, KEY_F9, KEY_F10, KEY_F11, KEY_F12, KEY_F13, KEY_F14, KEY_F15,
|
|
- KEY_Down, KEY_Left, KEY_Right, KEY_Up, KEY_Apos, KEY_Backtick, KEY_Backslash, KEY_Comma, KEY_Equal, KEY_BracketL, KEY_Minus, KEY_Period, KEY_BracketR, KEY_Semicolon, KEY_Slash, KEY_Backspace, KEY_Delete, KEY_End, KEY_Enter, KEY_Escape, KEY_Home, KEY_Insert, KEY_Menu, KEY_PageDown, KEY_PageUp, KEY_Pause, KEY_Space, KEY_Tab, KEY_NumLock, KEY_CapsLock, KEY_ScrollLock,
|
|
- KEY_KP0, KEY_KP1, KEY_KP2, KEY_KP3, KEY_KP4, KEY_KP5, KEY_KP6, KEY_KP7, KEY_KP8, KEY_KP9,
|
|
- KEY_KPDot, KEY_KPSlash, KEY_KPAsterisk, KEY_KPMinus, KEY_KPPlus, KEY_KPEnter,
|
|
- KEY_ShiftL, KEY_ShiftR, KEY_CtrlL, KEY_CtrlR, KEY_AltL, KEY_AltR, KEY_WinL, KEY_WinR,
|
|
|
|
### Graphic acceleration commands
|
|
|
|
```
|
|
; Fill a rectangle
|
|
(sc-rect Rd'x Rd'y Rd'w Rd'h Rd'color)
|
|
|
|
; Erase the screen (fill with black)
|
|
(sc-erase)
|
|
|
|
; Fill with a custom color
|
|
(sc-erase Rd'color)
|
|
```
|
|
|
|
## Stdio module
|
|
|
|
- This module defines 4 global handles: `@cin`, `@cout`, `@cin_r`, `@cout_r`.
|
|
- You can think of these handles as streams or SFRs (special function registers).
|
|
To use them, simply load data to or from the handles (e.g. `(ld r0 @cin)`).
|
|
- They operate over unicode code points, which are a superset of ASCII.
|
|
- The "_r" variants work with raw bytes. Do not combine them, or you may get problems with multi-byte characters.
|
|
|
|
End of stream is reported by the 'eof' status flag when a stream is read or written.
|
|
|
|
You can use these special handles in almost all instructions:
|
|
|
|
```lisp
|
|
(cmp @cin 'y'
|
|
(eq? (ld @cout '1'))
|
|
(ne? (ld @cout '0')))
|
|
```
|
|
|
|
When you compile a program using such handles, you will get a strange looking assembly:
|
|
|
|
```
|
|
0000 : (ld @0x6372736e00000001 72)
|
|
0001 : (ld @0x6372736e00000001 101)
|
|
0002 : (ld @0x6372736e00000001 108)
|
|
```
|
|
|
|
These are unique constants assigned to the streams at compile time. They are not meant to be used
|
|
directly, but the value can be obtained by simply leaving out the '@' sign: `(ld r0 cin)`.
|
|
That can be useful when these stream handles need to be passed to a function. Obviously this makes
|
|
more sense when there are different kinds of streams available, not just these two default ones.
|
|
|
|
.
|
|
|