Croissant Runtime
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.

1066 lines
32 KiB

4 years ago
Croissant (or *crsn* for short) is an extensible runtime emulating a weird microcomputer (or not so micro, that depends on what extensions you install).
4 years ago
## FAQ
### What is this for?
### 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?
4 years ago
4 years ago
Maybe this is not for you
4 years ago
### 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.
4 years ago
# Architecture
4 years ago
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.
4 years ago
## 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).
4 years ago
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.
4 years ago
8-, 16-, 32-bit and floating point arithmetic is not currently implemented, but will be added later. Probably. Maybe.
4 years ago
4 years ago
## Status flags
4 years ago
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
4 years ago
4 years ago
### Status tests (conditions)
4 years ago
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
4 years ago
# 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.
4 years ago
## Program
4 years ago
A program has this format:
4 years ago
...<instructions and routines>...
4 years ago
4 years ago
(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
4 years ago
Instructions are written like this:
4 years ago
(<keyword> <args>... <conditional branches>...)
4 years ago
4 years ago
### Conditional instructions
All instructions can be made conditional by appending `.<cond>` to the keyword, i.e. `( :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
4 years ago
### 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`
4 years ago
- 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.
4 years ago
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.
4 years ago
### 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
4 years ago
### Conditional branches
4 years ago
Conditonal branches are written like this:
4 years ago
(<cond>? <instructions>...)
4 years ago
4 years ago
- 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.
4 years ago
## Routines
4 years ago
4 years ago
A routine is defined as:
4 years ago
4 years ago
(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)))
4 years ago
(sub r0 arg0 1)
(call fac r0)
(mul r0 arg0 res0)
(ret r0)
4 years ago
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.
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.
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)
; ...
4 years ago
(join @r5)
; res0-res15 now contain return values
4 years ago
# Instruction Set
Crsn instruction set is composed of extensions.
4 years ago
Extensions can define new instructions as well as new syntax, so long as it's composed of valid S-expressions.
4 years ago
## 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.
4 years ago
- 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.
4 years ago
4 years ago
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.
4 years ago
### Loop syntactic sugar
Infinite loops are a very common construct, so there is special syntax added for them.
These are converted by the assembler to an anonymous label and a jump to it.
If you want to have a convenient jump target, give the loop a name. This lets you easily "break" and "continue"
by jumping to the labels.
(loop :label
(j :label)
4 years ago
## Built-in Instructions
...and pseudo-instructions
4 years ago
; Do nothing
; Stop execution
; 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.
4 years ago
; Mark a jump target.
4 years ago
; Numbered labels
4 years ago
; 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)
4 years ago
; 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)
4 years ago
; Mark a routine entry point (call target).
(routine PROC)
(routine PROC/A)
4 years ago
; Call a routine with arguments.
; The arguments are passed as argX. Return values are stored in resX registers.
(call PROC Rd...)
4 years ago
4 years ago
; 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...)
4 years ago
; Yield control from a coroutine (use when waiting for a mutex to give control early to
; other coroutines that might be holding it)
; 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)
4 years ago
; Wait for a coroutine to complete, read its return values and delete it.
(join @Obj)
; Begin a critical section (no context switch allowed)
; End a critical section
; Shortcut to define a critical section
; Generate a run-time fault with a debugger message
(fault message)
(fault "message text")
4 years ago
; Deny jumps, skips and run across this address, producing a run-time fault with a message.
(barrier message)
4 years ago
(barrier "message text")
4 years ago
; 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.
4 years ago
(barrier-open LABEL)
(barrier-close LABEL)
; Set coroutine scheduler timeslice (in microseconds). Set to zero to disable preemption.
(rt-opt RT_TIMESLICE Rd'usec)
4 years ago
## 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
4 years ago
; 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)
4 years ago
; 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
4 years ago
; Add A+B
(add Wr Rd Rd)
(add RW Rd)
4 years ago
; Subtract A-B
(sub Wr Rd Rd)
(sub RW Rd)
4 years ago
; Multiply A*B
(mul Wr Rd Rd)
(mul RW Rd)
4 years ago
; Divide A/B
(div Wr Rd Rd'divider)
(div RW Rd'divider)
4 years ago
; 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)
4 years ago
; 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)
4 years ago
4 years ago
; 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)
4 years ago
(and Wr Rd Rd)
(and RW Rd)
4 years ago
; OR A|B
(or Wr Rd Rd)
(or RW Rd)
4 years ago
(xor Wr Rd Rd)
(xor RW Rd)
4 years ago
; CPL ~A (negate all bits)
(cpl DST A)
(cpl DST)
; Rotate right (wrap around)
(ror Wr Rd Rd)
(ror RW Rd)
4 years ago
; Rotate left (wrap around)
(rol Wr Rd'value Rd'count)
(rol RW Rd'count)
4 years ago
; Logical shift right (fill with zeros)
(lsr Wr Rd Rd'count)
(lsr RW Rd'count)
4 years ago
; Logical shift left (fill with zeros)
(lsl Wr Rd Rd'count)
(lsl RW Rd'count)
4 years ago
; Arithmetic shift right (copy sign bit)
(asr Wr Rd Rd'count)
(asr RW Rd'count)
4 years ago
; Arithmetic shift left (this is identical to `lsl`, added for completeness)
(asl Wr Rd Rd'count)
(asl RW Rd'count)
4 years ago
; Delete an object by its handle. Objects are used by some extensions.
(del @Rd)
4 years ago
### 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
4 years ago
(itf Wr Rd)
; Convert float to int (round)
4 years ago
(fti Wr Rd)
; Convert float to int (ceil)
4 years ago
(ftic Wr Rd)
; Convert float to int (floor)
4 years ago
(ftif Wr Rd)
; Test properties of a float
; Set flags:
; NaN -> invalid
; Infinities -> overflow
; Positive, negative, zero
4 years ago
(ftst Rd)
; Float compare
4 years ago
(fcmp Rd Rd)
; Float range test
4 years ago
(fcmpr Rd Rd'min Rd'max)
; FloatRng. Unlike rng, frng is exclusive in the higher bound
4 years ago
(frng Wr Rd'min Rd'max)
; --- Basic float arith ---
; Float add
4 years ago
(fadd Wr Rd Rd)
; Float subtract
4 years ago
(fsub Wr Rd Rd)
; Float multiply
4 years ago
(fmul Wr Rd Rd)
; Float power
4 years ago
(fpow Wr Rd Rd'pow)
; Float root
4 years ago
(froot Wr Rd Rd'root)
; Float hyp - sqrt(a*a + b*b)
4 years ago
(fhyp Wr Rd Rd)
; Float divide
4 years ago
(fdiv Wr Wr'rem Rd'a Rd'div)
; Float modulo
4 years ago
(fmod Wr Rd'a Rd'div)
; Float abs value
4 years ago
(fabs Wr Rd)
; Float signum (returns -1 or 1)
4 years ago
(fsgn Wr Rd)
; --- Basic trig ---
; Float sine
4 years ago
(fsin Wr Rd)
; Float arcsine
4 years ago
(fasin Wr Rd)
; Float cosine
4 years ago
(fcos Wr Rd)
; Float arccosine
4 years ago
(facos Wr Rd)
; Float tangent
4 years ago
(ftan Wr Rd)
; Float arctangent
4 years ago
(fatan Wr Rd)
; Float 2-argumenmt arctangent
4 years ago
(fatan2 Wr Rd'y Rd'x)
; Float cotangent
4 years ago
(fcot Wr Rd)
; Float arccotangent
4 years ago
(facot Wr Rd)
; --- Hyperbolic trig ---
; Float hyperbolic sine
4 years ago
(fsinh Wr Rd)
; Float hyperbolic arcsine
4 years ago
(fasinh Wr Rd)
; Float hyperbolic cosine
4 years ago
(fcosh Wr Rd)
; Float hyperbolic arccosine
4 years ago
(facosh Wr Rd)
; Float hyperbolic tangent
4 years ago
(ftanh Wr Rd)
; Float hyperbolic arctangent
4 years ago
(fatanh Wr Rd)
; Float hyperbolic cotangent
4 years ago
(fcoth Wr Rd)
; Float hyperbolic arccotangent
4 years ago
(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:
; 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).
; Set buffer IO mode
; Mode is one of:
; - BFIO_QUEUE (1)
; - BFIO_STACK (3)
(bfio @Obj MODE)
Primitive buffer ops (position is always 0-based)
; 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:
; Resize the buffer. Removes trailing elements or inserts zero to match the new size.
(bfrsz @Obj Rd:len)
4 years ago
; Reverse a buffer
(bfrev @Obj)
4 years ago