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.
cpsdqs e6c8125cfe
Interactive mandelbrot: use quadtree table for nicer previews
3 years ago
_stash cleanup. remove conditional jumps, replaced by condition embedded in the enum 3 years ago
crsn Fixed conditional parsing bug, npos and nneg options wrong way round 3 years ago
crsn_arith fixes, more trig, demo with floats 3 years ago
crsn_buf basic float arith, wip float trig, abs 3 years ago
crsn_screen add key checking example, key and mouse constants 3 years ago
crsn_stdio fixes, graphic acceleration commands, upscaling pixels 3 years ago
examples Interactive mandelbrot: use quadtree table for nicer previews 3 years ago
launcher implement a more readable bit mask syntax, add (ldXX Wr Rd Rd) with separate dest and both sources 3 years ago
launcher_nox make it work with musl (the _nox target does not link the screen module that wouldn't build) 3 years ago
out add build scripts for native and musl 3 years ago
.gitignore add build scripts for native and musl 3 years ago
Cargo.lock fixes, graphic acceleration commands, upscaling pixels 3 years ago
Cargo.toml fixes, graphic acceleration commands, upscaling pixels 3 years ago add key checking example, key and mouse constants 3 years ago add build scripts for native and musl 3 years ago add build scripts for native and musl 3 years ago Simplify {clz,clo,se}XX parsing and exec, add simple unit test example scripts 3 years ago
crsn.example.json5 add example config file, add default config file to gitignore 3 years ago Simplify {clz,clo,se}XX parsing and exec, add simple unit test example scripts 3 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).


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?

Maybe this is not for you


Yes! You can use crsn as a scripting language!

The first line from a source file is skipped if it starts with #!


Yup, go ahead. You can also develop your own private crsn extensions, they work like plugins.


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.


  • 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


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.


A program has this format:

    ...<instructions and routines>...


    (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))))


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. ( :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.

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.

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.


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

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

; 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 whole program.
; Value must be known at compile time.

; Mark a jump target.
; Numbered labels

; 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)

; 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...)

; Exit the current routine with return values
(ret Rd...)

; Generate a run-time fault with a debugger message
(fault message)
(fault "message text")

; Deny jumps, skips and run across this address, producing a run-time fault with a message.
(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)

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
; 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 Wr Rd Rd)
(and RW Rd)

; OR A|B
(or Wr Rd Rd)
(or RW Rd)

(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)

; IntToFloat
(itf Wr Rd)
; FloatToInt (round)
(fti Wr Rd)
; FloatToInt (ceil)
(ftic Wr Rd)
; FloatToInt (floor)
(ftif Wr Rd)

; FloatTest - nan->invalid, infinities->overflow, positive, negative, zero
(ftst Rd)
; FloatCompare
(fcmp Rd Rd)
; FloatRangeTest
(fcmpr Rd Rd'min Rd'max)

; FloatRng
(frng Wr Rd'min Rd'max)

; --- Basic float arith ---

; FloatAdd
(fadd Wr Rd Rd)
; FloatSub
(fsub Wr Rd Rd)
; FloatMul
(fmul Wr Rd Rd)
; FloatPow
(fpow Wr Rd Rd'pow)
; FloatRoot
(froot Wr Rd Rd'root)
; FloatHyp
(fhyp Wr Rd Rd)
; FloatDiv
(fdiv Wr Wr'rem Rd'a Rd'div)
; FloatMod
(fmod Wr Rd'a Rd'div)
; FloatAbs
(fabs Wr Rd)
; FloatSgn
(fsgn Wr Rd)

; --- Basic trig ---

; FloatSin
(fsin Wr Rd)
; FloatAsin
(fasin Wr Rd)
; FloatCos
(fcos Wr Rd)
; FloatAcos
(facos Wr Rd)
; FloatTan
(ftan Wr Rd)
; FloatAtan
(fatan Wr Rd)
; FloatAtan2
(fatan2 Wr Rd'y Rd'x)
; FloatCot
(fcot Wr Rd)
; FloatAcot
(facot Wr Rd)

; --- Hyperbolic trig ---

; FloatHypSin
(fsinh Wr Rd)
; FloatHypAsin
(fasinh Wr Rd)
; FloatHypCos
(fcosh Wr Rd)
; FloatHypAcos
(facosh Wr Rd)
; FloatHypTan
(ftanh Wr Rd)
; FloatHypAtan
(fatanh Wr Rd)
; FloatHypCot
(fcoth Wr Rd)
; FloatHypAcot
(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, TAU, 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)

Whole buffer manipulation:

; 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:

; 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.

; 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

; Blit (render the pixel buffer).
; This function also updates key and mouse states and handles the window close button
; Blit if needed (when the auto-blit function is enabled)
(sc-blit 0)

; Update key and mouse state, handle the window close button

; 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:

  • KEY_0, KEY_1, KEY_2, KEY_3, KEY_4, KEY_5, KEY_6, KEY_7, KEY_8, KEY_9 (0-9)
  • 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_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)

; 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:

(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.