Learning Cryn

This tutorial teaches the whole Cryn language from zero. It is written for people who have never programmed before. If you have programmed before, you can skim, but read the first chapters anyway: Cryn works differently from most languages, and the early mental model matters.

Every example in this tutorial is a complete program. You can type it into a file and run it. How to run programs is explained right after the first example.

1. The idea behind Cryn

Most languages give you symbols: +, {, ;, ->. Cryn gives you none of those. Instead, a Cryn program is a list of short English sentences. Each sentence tells a small machine to do one thing.

The machine has four parts, and everything in this tutorial builds on them:

  1. Cells. A cell is a box that holds one value. A cell has a type, like a number box or a letter box. Cells live in a frame (one per transformation call) or in the world (shared by the whole program).
  2. Focus. The focus is a finger pointing at one cell (or at an element inside a cell). Commands like read and write act on whatever the focus points at.
  3. Working value. This is the one value you are currently holding in your hand. Arithmetic and comparisons work on it. At any point in the program it has exactly one type.
  4. Value stack. This is a pile where you can put values aside for later. You can only reach the top of the pile.

A Cryn program is a sequence of sentences that move values between these four places. There are no variables in the usual sense, no expressions, and no operator precedence to memorize. If you can follow a recipe of plain instructions, you can follow Cryn.

One more comfort: whitespace means nothing. Newlines are just spaces. Every program could be written on a single line if you wanted. Blocks are marked with words (define ... end), not with braces or indentation.

2. First program

Make a file called hello.cryn with this content:

define main
    make string "Hello, world!\n" greeting
    address greeting
    output text
    return
end

Run it from a terminal:

cryn run hello.cryn

You should see:

Hello, world!

What happened, sentence by sentence:

The same program in Python:

print("Hello, world!")

Exercise: make it print your name instead, keeping the newline.

Useful compiler commands (you will use the first two a lot):

cryn run hello.cryn       # compile and run
cryn hello.cryn           # compile to an executable called "hello"
cryn build hello.cryn -o out   # compile to an executable called "out"
cryn check hello.cryn     # type check only, do not build
cryn emit-c hello.cryn -o out.c   # look at the generated C code
cryn emit-ir hello.cryn   # look at the compiler's intermediate code

3. Comments

A comment is text the compiler ignores. Cryn has exactly one comment form. It starts with [# and ends with #]. It can span lines.

[# this is a comment #]

[# this comment
   covers two lines #]

define main
    set 1 [# comments can sit next to code #]
    output
    return
end

Comments cannot nest. The first #] closes the comment.

4. The working value and set

The working value is what you are holding. The set command puts a literal value into it.

define main
    set 42
    output
    return
end

This prints 42.

In Python you would write x = 42 and then print(x). Cryn has no x. The working value is the one thing in hand, and set puts a value into it. The named places come later, as cells (chapter 9).

Literals you can set:

Example literalMeaning
set 42an integer
set -7a negative integer
set 0x1Fan integer in hexadecimal
set 2.5a float
set 1.5e3a float with an exponent, 1500
set 'A'one character, value 65
set '\n'the newline character
set truea boolean, printed as true
set falsea boolean, printed as false
set nothingno value at all

output prints the working value. It never prints a space or a newline by itself. If you want a newline, print the character '\n' or a string that ends with \n.

Character escapes inside '...' and "...": \n newline, \t tab, \r carriage return, \0 zero byte, \\ backslash, \' quote, \" double quote.

set nothing empties the working value. Its type becomes nothing. You cannot output or convert nothing; it is mostly used to clear your hand.

Exercise: print true, then a tab character, then false, with nothing between them.

5. Math

Math commands change the working value. Each one takes the other operand either from a literal written after the command, or from the top of the value stack.

With a literal:

define main
    set 10
    add 5
    output
    return
end

This prints 15. In Python: print(10 + 5).

From the stack (explained fully in chapter 10, but the idea is simple: push puts the working value on a pile, and a math command without a literal takes the top of the pile as the other operand):

define main
    set 10
    push
    set 3
    subtract
    output
    return
end

The subtract pops the 10 and computes working value minus popped value, which is 3 - 10. This prints -7. The Python spelling is print(3 - 10).

The six math commands:

CommandResult
addworking value plus other
subtractworking value minus other
multiplyworking value times other
divideworking value divided by other, fraction cut off toward zero
remainderthe remainder left over after divide
negateflips the sign of the working value, no operand

Examples you can try:

define main
    set 7
    divide 2
    output
    set -7
    divide 2
    output
    set 7
    remainder 2
    output
    set 5
    negate
    output
    return
end

This prints 3, then -3, then 1, then -5. Note two honest details: division cuts off toward zero (-7 / 2 is -3), and the remainder keeps the sign of the left side.

Division by zero stops the program with an error message. More on errors in chapter 25.

Exercise: compute (10 - 4) * (3 + 2) with push and print the result. Mind the order: subtract takes the popped value away from the working value, so the 4 goes on the pile before the 10 lands in hand.

6. Types

Every value has one type, and the compiler checks types before the program runs. The full type list lives in the Language Reference. The ones you meet first:

TypeHolds
int8 int16 int32 int64signed whole numbers of 8 to 64 bits
uint8 uint16 uint32 uint64unsigned whole numbers of 8 to 64 bits
float32 float64decimal numbers, single or double precision
charone byte, 0 to 255, with character intent
booltrue or false
addressa location in memory
nothingno value

A plain literal like set 5 has the special type "integer literal". It turns into a concrete type the moment you combine it with one, for example when you store it into a typed cell. If two plain literals meet, they become int64.

convert changes the type of the working value on purpose:

define main
    set 65
    convert uint8
    output
    return
end

This prints A, because 65 is the code for the letter A and uint8 values print as single bytes.

Integer conversions keep only the low bits, which is called wrapping:

define main
    set 65535
    convert uint16
    add 2
    output
    return
end

65535 plus 2 is 65537, which does not fit in 16 bits. It wraps to 1. This prints 1. Wrapping is defined behavior in Cryn, not an accident.

Float to integer conversion saturates instead of wrapping. It clamps to the nearest value that fits:

define main
    set 300.7
    convert int8
    output
    return
end

This prints 127, the largest int8. A value below the range would print -128.

Python has one integer type and it never overflows. Cryn fixes the width in the type and defines overflow as wrapping, so 127 plus 1 in an int8 cell is -128, not an error.

Some conversion rules worth knowing:

Exercise: predict what set 3.9, convert int64, output prints, then run it. Repeat with -3.9. Then predict set 300.9, convert int8, output.

7. Choices: compare and jump when

Cryn has no if. You compare two values, and then you can jump to a named spot only when the comparison had a certain outcome.

compare compares the working value with either a literal or the top of the value stack. The answer is remembered. Then jump <name> when <condition> jumps to a label only when the remembered answer matches the condition.

The six conditions:

ConditionTrue when the working value was
lesssmaller than the other value
equalequal to the other value
greaterlarger than the other value
differentnot equal to the other value
less-or-equalsmaller or equal
greater-or-equallarger or equal

A label is made with mark <name> and is visible in the whole transformation. jump <name> without when always jumps.

define main
    set 10
    compare 10
    jump same when equal
    set 0
    stop
    mark same
    set 1
    output
    return
end

The compare says "working value 10 equals 10", so the jump is taken. This prints 1. If you change set 10 to set 11, the jump is skipped, the program runs set 0 and stop, prints nothing, and exits with code 0.

The same shape in Python is an if:

if 10 == 10:
    print(1)

Cryn has no if. The compare leaves an answer, the jump reads it, and those are always two separate steps.

Two rules to remember:

  1. A conditional jump must follow a compare on every path that reaches it. The compiler rejects a when jump that could arrive without a comparison.
  2. Comparisons are only on numbers, chars, booleans against booleans, and addresses against addresses.

Exercise: print 1 when 3 is less than 4, and 0 when it is not. Two paths through one mark.

8. Loops

A loop is a label plus a jump back to it. Here is a countdown:

define main
    make int64 i
    set 5
    move i
    write
    mark again
    move i
    read
    compare 0
    jump done when equal
    move i
    read
    output
    move i
    read
    subtract 1
    move i
    write
    set 0
    jump again
    mark done
    return
end

This prints 54321.

In Python:

i = 5
while i != 0:
    print(i, end="")
    i = i - 1

The mark again plays the while and jump again is what loops back.

New commands here: make, move, read, write. They get their full chapter next. For now: make int64 i creates a cell, move i points the focus at it, write stores the working value into it, read loads it back into the working value.

Notice set 0 right before jump again. This is a small habit that keeps loops simple: it makes the working value the same on every arrival at the label. Without it, the first arrival holds a literal 5 and later arrivals hold other things. The compiler would widen the working value to int64, which works here, but there are loops where the types would clash. Getting into the habit of normalizing the working value before jumping back saves you confusing errors. Chapter 28 lists the common ones.

Exercise: change the countdown to print 12345 instead. The order of the compare, the print, and the subtract is the whole puzzle.

9. Cells, focus, read and write

Cells are the boxes of the machine. Every cell has a name and a type and starts at zero.

Making cells:

make int64 score          [# one cell, zeroed #]
make float32 rate
make array 10 of int64 ns [# ten int64 cells in a row #]
make string "hi\n" s      [# a cell holding bytes plus a zero byte #]
make 8 of char buf        [# a heap block of 8 chars, address below #]

Inside a transformation, make ... name creates a frame cell for that call. At the top of the file, make creates a world cell shared by the whole program (chapter 13).

The focus is your finger. You point it at a cell with move:

define main
    make int64 score
    set 5
    move score
    write
    move score
    read
    add 1
    move score
    write
    move score
    read
    output
    return
end

This prints 6. The pattern move score / write stores, move score / read loads.

In Python:

score = 5
score = score + 1
print(score)

A cell is the variable. The focus is which variable you mean, and move is how you point at it.

Focus movement commands:

CommandFocus after
move namepoints at the named cell, and resets the focus history
move 3steps into element 3 of the array in focus
move indexsteps into the element given by the working value
move atsteps to the address held in the working value
backundoes one step of move 3, move index, move at
homeback to the frame root, history cleared

The focus history is how back works. Only the stepping moves push onto the history. move name resets the history, because a named move is a fresh anchor: after move score, back takes you to the frame root, and another back is an error.

A small walk through an array:

define main
    make array 3 of int64 nums
    set 7
    move nums
    move 1
    write
    move nums
    move 1
    read
    output
    home
    return
end

This prints 7. move nums points at the array, move 1 steps into element 1, write stores there.

Rules about read and write:

Exercise: make two cells holding 3 and 8, and print the bigger one using a third cell. Chapter 7 does the deciding.

10. The value stack: push, pop, swap

The value stack is a pile. push puts the working value on top. pop takes the top off and makes it the working value. swap exchanges the working value with the top of the pile.

define main
    set 2
    push
    set 3
    push
    set 4
    add
    multiply
    output
    return
end

Walk through it: push 2, push 3, hold 4. add pops 3 and computes 4 + 3 = 7. multiply pops 2 and computes 7 * 2 = 14. This prints 14.

The expression 2 * (3 + 4) is the same computation. Python evaluates the parentheses first and hides the pile. Cryn makes you lay the operands out in the order the machine eats them.

The pile is the honest replacement for expressions. To compute 2 * (3 + 4) you set up the numbers in the order the machine consumes them.

swap is handy when you want to keep a value but work on another one:

define main
    set 9
    push
    set 4
    swap
    output
    pop
    output
    return
end

swap exchanges 4 (in hand) with 9 (on the pile). Now the working value is 9 and the pile holds 4. This prints 9 then 4.

Exercise: compute (10 - 4) * (3 + 2) and print it. Six commands plus output, and the pile holds the answer's factors along the way.

11. Transformations: define, call, return

A transformation is Cryn's word for a function. You define one with define, you run it with call, and it finishes with return or by reaching end.

define square int64 x
    move x
    read
    push
    move x
    read
    multiply
    return
end

define main
    set 9
    push
    call square
    output
    return
end

This prints 81.

In Python:

def square(x):
    return x * x

print(square(9))

The result type is inferred from the return, like Python infers the function's return. Recursive transformations must announce it up front with result <type>, because the body calls itself before the end is in sight.

How parameters work:

A transformation's result type is inferred from the type of the working value at its return. All returns in one transformation must agree. Recursive transformations must state the type up front with result <type>:

define fact int64 n result int64
    move n
    read
    compare 1
    jump base when less-or-equal
    move n
    read
    push
    move n
    read
    subtract 1
    push
    call fact
    multiply
    return
    mark base
    set 1
    return
end

define main
    set 10
    push
    call fact
    output
    return
end

This prints 3628800, which is 10 factorial.

Mutual recursion works the same way. Both partners need result:

define iseven int64 n result bool
    move n
    read
    compare 0
    jump yes when equal
    move n
    read
    subtract 1
    push
    call isodd
    return
    mark yes
    set true
    return
end

define isodd int64 n result bool
    move n
    read
    compare 0
    jump no when equal
    move n
    read
    subtract 1
    push
    call iseven
    return
    mark no
    set false
    return
end

define main
    set 10
    push
    call iseven
    output
    return
end

This prints true.

Rules worth knowing:

Exercise: write define biggest int64 a int64 b result int64 that returns the larger parameter. A compare and one jump decide it. Chapter 22 shows one shape of the answer.

12. stop: ending the program early

stop ends the whole program. If the working value is a whole number, it becomes the exit status code. Nothing else runs after stop.

define main
    set 3
    stop
end

This program prints nothing and exits with code 3. On most systems only the lowest 8 bits of the code survive, so use 0 to 255. Reaching the end of main normally exits with code 0.

In Python: sys.exit(3).

Exercise: exit with code 7 and print nothing. Check yourself with echo $? in the shell.

13. World cells

A make at the top of the file, outside any define, creates a world cell. World cells exist the whole run and every thread sees them. They are how independent parts of a program share state.

make int64 total

define bump int64 n
    move n
    read
    push
    move total
    read
    add
    move total
    write
    set 0
    return
end

define main
    set 5
    push
    call bump
    set 7
    push
    call bump
    move total
    read
    output
    return
end

This prints 12.

These are module level variables in Python:

total = 0

def bump(n):
    global total
    total = total + n

Every thread sees the same cells. That is the point, and the danger.

World cells can hold values, arrays, and strings:

make int64 count
make array 100 of float64 samples
make string "untitled\n" title

A world cell name must not collide with a transformation name.

Exercise: add a second world cell that counts how many times bump ran, raise it inside bump, and print both cells at the end.

14. Addresses and memory

An address is the location of a cell or block. Cryn exposes addresses as plain values you can hold, pass, and follow.

Three ways to get an address:

CommandWorking value becomes
addressthe address of the focused cell
address namethe address of a named cell (or a transform reference, chapter 15)
make <size> of <type> [name]the address of a fresh heap block

And one way to follow one:

CommandEffect
move atfocus moves to the cell at the working value address

A heap block is memory that lives until you free it:

define main
    make 8 of int64 block
    move block
    read
    move at
    move 3
    set 42
    write
    move block
    read
    move at
    move 3
    read
    output
    move block
    read
    free
    return
end

One detail to slow down for: in make 8 of int64 block, the word block is a cell that receives the block's address. So move block / read puts the address into the working value, and move at follows it, putting the focus on the block itself. Now move 3 steps to element 3 and write stores there. The example stores 42 into element 3, reads it back, prints 42, and frees the block.

The size in make <size> of can also be a cell name holding the count, computed at run time:

make int64 n
...
make n of float64 data

You can do arithmetic on addresses. add 1 on an address moves it forward by one element of its pointee type, not by one byte. Subtracting two addresses of the same type gives the number of elements between them.

define main
    make int64 a
    make int64 b
    set 7
    move a
    write
    set 9
    move b
    write
    address b
    move at
    read
    output
    return
end

This prints 9. The address of b was taken, then followed with move at, then read.

Python has nothing like this. The nearest familiar ground is C: b is the cell, &b is the address, and *(&b) is the move at / read pair. Cryn addresses are values like any other, which is what makes linked structures possible.

Rules that keep memory honest:

Linked structures are cells inside blocks holding addresses:

make 2 of int64 node      [# node 0: value, node 1: address of next #]

The examples folder of the compiler repository has a full linked list and a free-list allocator written this way.

Exercise: allocate make 4 of int64, store 42 into element 3, print it back, and free the block.

15. Transform references

A transformation can itself be held in a cell of type transform. You get one with address <transformation name>, and you run it with call when the working value holds the reference.

define greet
    make string "hi\n" s
    address s
    output text
    set 0
    return
end

define main
    make transform f
    address greet
    move f
    write
    move f
    read
    call
    return
end

This prints hi.

In Python this is a function object: f = greet, then f() runs it. The transform cell is the variable holding the function, and call with no name is the f().

Two honest limits:

This is enough for callbacks: a transformation can receive, via a world cell, a reference to work that should happen later.

Exercise: make two transformations that print different words, store each into the same transform cell in turn, and call both through the cell.

16. Input from the keyboard

input reads one byte from the current input stream, which is the keyboard by default. The working value becomes the byte value, or -1 when the input has ended.

define main
    set 0
    mark next
    input
    compare -1
    jump done when equal
    convert uint8
    output
    jump next
    mark done
    return
end

This program copies its input to its output, like a tiny cat. Try it:

printf 'hello\n' | cryn run copy.cryn

The convert uint8 turns the -1-or-byte number into a byte so output prints it as a character instead of a number.

The Python shape is ch = sys.stdin.buffer.read(1), empty bytes at the end. Cryn uses -1 for the end instead, because the working value is always a number.

Exercise: count the bytes until the end of the input and print the count. Six bytes piped in, 6 out.

17. Files

Cryn gives files to you as first-class handles. A handle is a plain integer that stands for one open file. Many files can be open at the same time, and you choose which one the streams point at.

The five file commands:

CommandWhat it does
open readopens the path in the working value for reading, handle goes into the working value
open writeopens for writing, existing content is discarded
open bothopens for reading and writing, content discarded
open appendopens for writing at the end, existing content kept
usepoints the streams through the handle in the working value
use 0points the streams back at the keyboard and terminal
flushpushes written bytes out of the buffer to the file
seek npositions the handle's streams at byte n from the start
closecloses the handle in the working value

Details:

A full write-then-read program:

define main
    make string "notes.txt" name
    make string "cryn can write files\n" note
    make int64 h

    address name
    open write
    move h
    write

    move h
    read
    use
    home
    address note
    output text
    flush
    home

    move h
    read
    close

    address name
    open read
    move h
    write

    move h
    read
    use
    home
    set 0
    mark next
    input
    compare -1
    jump done when equal
    convert uint8
    output
    jump next
    mark done
    return
end

This prints cryn can write files. It writes the note into notes.txt, closes it, opens it again for reading, routes the input stream through it, and echoes the bytes to the terminal. (The cell is called note and not text because text is part of the language, see output text, and words of the language cannot be names.)

Two files at once is the point of handles. Keep each handle in its own cell, and use the one you want before each read or write. The repository test tests/28_handles.cryn shows exactly this, including append.

seek is what one file open both is for: reading and writing the same file, back and forth. In Python terms: open is open(), seek n is f.seek(n) measured from the start, and use is choosing which open file the next read or write touches. This program writes a row of letters, then rewrites one letter in the middle without rewriting anything else:

define main
    make string "row.txt" name
    make int64 h

    address name
    open both
    move h
    write
    push
    use
    home

    make string "ABCDE" row
    address row
    output text

    seek 3
    set 'X'
    output
    flush

    pop
    close

    address name
    open read
    move h
    write
    push
    use
    home

    set 0
    mark next
    input
    compare -1
    jump done when equal
    convert uint8
    output
    jump next
    mark done

    pop
    close
    return
end

This prints ABCXE. The open both handle lets the program write the row, then seek 3 moves the position to the fourth byte and set 'X' plus output replaces just that byte. close puts the streams back at the terminal, the file is opened again for reading, and the echo loop prints it. (The repository test tests/31_seek.cryn checks each step's result, and keeps reading through the both handle with more seek calls.)

Paths are passed to the operating system unchanged. There is no folder listing, no rename, and no delete command. If you need those, the next chapter has an escape hatch.

Exercise: open one file in append mode three times, writing one line each open, then read the whole file back and count the lines.

18. system: running a shell command

system hands a command to the system shell. The working value must be an address to the command text. The working value becomes the command's exit status, as the host reports it.

define main
    make string "echo shell works" cmd
    address cmd
    system
    output
    return
end

This prints the line shell works (the shell prints it, not Cryn) and then 0 (the status, printed by output). Use it sparingly. It makes programs depend on the operating system.

In Python: subprocess.run(cmd, shell=True).returncode.

Exercise: run the command true, print its status, run false, print that status too. Two numbers, 0 and 1.

19. Threads: spawn and wait

spawn <name> starts a new thread that runs the given transformation. The transformation must take no parameters. wait pauses until every context spawned by this one has finished. The working value becomes 0.

Threads talk through world cells or shared heap addresses.

make int64 winner

define setter
    set 7
    move winner
    write
    return
end

define main
    spawn setter
    spawn setter
    wait
    move winner
    read
    output
    return
end

This prints 7. Both threads wrote the same value, so the answer is certain.

In Python:

import threading

def setter():
    global winner
    winner = 7

ts = [threading.Thread(target=setter) for _ in range(2)]
for t in ts:
    t.start()
for t in ts:
    t.join()

wait is the two join lines.

What happens when threads race on the same cell? Cryn promises exactly this:

So a plain counter increments can lose updates, because read, add 1, write is three steps and threads interleave. When counting exactly matters, use the next chapter.

Exercise: spawn two workers that each add 100 to one int64 cell with plain read, add 1, write, wait, and print the cell. Run the program many times. Sometimes the answer is 200, sometimes less. The next chapter is the fix.

20. exchange: atomics without mystery

exchange swaps the working value with the focused cell in one indivisible step. You hand a value over and receive whatever the cell held, and no other thread can slip a step in between.

define main
    make int64 probe
    set 5
    move probe
    write
    set 9
    move probe
    exchange
    output
    move probe
    read
    output
    return
end

After the exchange the cell holds 9 and the working value holds 5. This prints 5 then 9.

In Python the atomic step is hidden inside threading.Lock. Here it is the whole tool: one cell, exchange, and the ordering promise written in the rules. There is no lock object.

Rules:

That last property is what makes locks possible. A lock is a cell that holds 0 for free and 1 for taken:

make int64 counter
make int64 lock

define worker
    make int64 k
    set 0
    move k
    write
    mark outer
    move k
    read
    compare 100
    jump done when greater-or-equal
    mark spin
    set 1
    move lock
    exchange
    compare 0
    jump spin when different
    move counter
    read
    add 1
    move counter
    write
    set 0
    move lock
    exchange
    set 0
    move k
    read
    add 1
    move k
    write
    set 0
    jump outer
    mark done
    return
end

define main
    spawn worker
    spawn worker
    wait
    move counter
    read
    output
    return
end

Two workers each add 100 under the lock. The answer is always 200, never less. The trick: trying to take the lock is set 1 / move lock / exchange. If the old value came back 0, the lock was free and is now yours. If it came back 1, someone else has it, so spin and try again. Releasing is storing 0 with another exchange.

Exercise: delete the release exchange near the end of worker and predict what happens before you run it.

21. Putting it together: reversing a string

Chapters 1 to 20 introduced the words one at a time. This chapter uses them together on one task: measure a string, then print it backwards. The program is complete and runs as written.

make string "stressed" s
make string "\n" nl

define newline result nothing
  address nl
  output text
  set nothing
  return
end

define main
  make int64 len
  make int64 k

  [# measure the string: len is both counter and index #]
  set 0
  move len
  write
  home

  mark measure
  move len
  read
  move s
  move index
  read
  compare 0
  jump measured when equal
  move len
  read
  add 1
  move len
  write
  home
  jump measure

  mark measured
  set 0
  home

  [# print backwards: k from len-1 down to 0 #]
  move len
  read
  subtract 1
  move k
  write
  home

  mark printLoop
  move k
  read
  compare 0
  jump finish when less
  move k
  read
  move s
  move index
  read
  output
  move k
  read
  subtract 1
  move k
  write
  home
  jump printLoop

  mark finish
  set 0
  call newline
  return
end

This prints desserts, which is stressed backwards.

Look at the measure loop closely, because it has every moving part at once:

mark measure
move len
read
move s
move index
read
compare 0
jump measured when equal

len is both the counter and the index. Each pass puts the current count into the working value, focuses the string cell s, and steps into element number len with move index. read brings that byte into the working value, and compare 0 tests for the zero byte that ends every make string cell. When the loop finds it, len holds the length: 8.

The loop tail counts one up and normalizes the working value before the label:

move len
read
add 1
move len
write
home
jump measure

After write the working value is an int64, and the first arrival at mark measure holds a literal 0 from set 0. The two agree, so the paths join cleanly. Chapter 8 promised this habit would matter. Here it does.

The print loop is the same walk with the index running down instead of up. k starts at len - 1, and each pass prints element k as one raw byte, because read on a string cell gives bytes and output prints bytes as they are. When k drops below 0, the program jumps to finish, prints the newline, and returns.

One more detail worth noticing: move index on a string cell is bounds checked at run time against the cell's real size, 9 here (8 letters plus the zero byte). A walk like this cannot leave the string by accident.

In Python the whole program is print("stressed"[::-1]). Cryn makes you be the slice: find the end yourself, then walk back one byte at a time.

Exercise: print the length after the reversed string, with a space between. The length is already sitting in a cell.

22. Patterns you will reuse

Four shapes come up again and again. Each one below is a complete program you can run.

Swap two cells:

define main
  make int64 a
  make int64 b
  make int64 t
  set 3
  move a
  write
  set 8
  move b
  write
  move a
  read
  move t
  write
  move b
  read
  move a
  write
  move t
  read
  move b
  write
  move a
  read
  output
  move b
  read
  output
  return
end

This prints 83. The cell t holds one value while the other one moves. There is no word for swapping two cells, and swap works on the value stack, so a temp cell is the tool. Python spells it a, b = b, a and hides the temp; C does it exactly this way.

Biggest of two:

define main
  make int64 x
  make int64 y
  make int64 big
  set 4
  move x
  write
  set 9
  move y
  write
  set 0
  home
  move x
  read
  move big
  write
  home
  move x
  read
  push
  move y
  read
  compare
  home
  jump done when less-or-equal
  move y
  read
  move big
  write
  home
  mark done
  move big
  read
  output
  return
end

This prints 9. big starts as x, then the compare puts y in hand and x on the pile, so the condition less-or-equal asks one question: is y <= x? When yes, x already wins and the jump skips the copy. When no, y overwrites big. Note the home between compare and the jump: both paths must arrive at mark done with the focus in the same place, and the focus sits on y at the compare. Sending it home before the branch makes the two arrivals agree.

Sum an array:

define main
  make array 5 of int64 ns
  make int64 i
  make int64 sum
  set 7
  move ns
  move 0
  write
  set 30
  move ns
  move 1
  write
  set 5
  move ns
  move 4
  write
  set 0
  move i
  write
  home

  mark addLoop
  move i
  read
  compare 5
  jump done when greater-or-equal
  move i
  read
  move ns
  move index
  read
  push
  move sum
  read
  add
  move sum
  write
  home
  move i
  read
  add 1
  move i
  write
  home
  jump addLoop

  mark done
  move sum
  read
  output
  return
end

This prints 42 (7 + 30 + 5, and the untouched elements count as 0). The middle of the loop shows a stack trick you will use constantly: the element lands in the working value, gets pushed, then move sum / read puts the running total in hand, and add pops the element back. Every move and read replaces the working value, so anything you still need goes on the pile first.

Pass a block to a transformation:

define getelem address array of int64 blk int64 idx result int64
  move idx
  read
  push
  move blk
  read
  move at
  pop
  move index
  read
  return
end

define main
  make 4 of int64 a
  move a
  read
  move at
  move 1
  set 41
  write
  home
  move a
  read
  push
  set 1
  push
  call getelem
  output
  return
end

This prints 41. A cell made with make 4 of int64 holds the address of a run time sized block, and that is exactly the type address array of int64 names, so blocks pass to transformations as plain values. Inside getelem the same push and pop dance happens: the index goes on the pile, the block is focused with move at, the index comes back, move index steps in, read loads the element.

Strings are the one thing this pattern does not fit. A string cell is a fixed array, and its address carries the fixed size in its type (address array 9 of uint8 for a 9 byte string), which does not match address array of uint8. Converting the address with convert address array of uint8 compiles, but indexing through it aborts at run time, because run time bounds checking works from the base of a live heap block. Walk strings in place with move name and move index, the way chapter 21 does, and pass heap blocks to transformations.

Exercise: write setelem, the twin of getelem above, that stores a value into a block slot instead of loading one. Three parameters, one write.

23. Output details

What output prints for each type:

Working value typePrinted as
signed integersminus sign and digits, like -5
unsigned integersdigits, like 18446744073709551615
float32, float64the shortest digits that read back exactly, like 2.75
booltrue or false
char, uint8one raw byte
address0x followed by the address in hexadecimal
plain integer literalas signed if it fits int64, otherwise as unsigned

output text prints the bytes at the working value address until the first zero byte. Strings made with make string always end with a zero byte.

output writes to the current output stream, which is the terminal unless a file opened for writing has been routed with use. Nothing is ever printed automatically. No spaces, no newlines.

A printing helper you will reuse constantly:

make string "\n" nl
...
address nl
output text

24. Program shape

A complete program is one file. The file contains world cells and transformations in any order:

make string "1.0\n" version

define helper ...
define main ...

Order does not matter. Names are case sensitive. Names use letters, digits, and underscores, and cannot start with a digit. Every word of the language (see the table in the Language Reference) is reserved and cannot be a name. In particular you cannot name a cell set, end, or when.

The whole program compiles in one pass into one C file, then into a normal executable. There is no separate compilation yet, one .cryn file is one program.

25. Errors

Two kinds exist and they look different.

Compile time errors stop the build. They name the file, line, and column:

tests/errors/mixed_sign.cryn:9:5: error: arithmetic requires matching
numeric types (uint64 and int64); use 'convert'

Run cryn check file.cryn to test for these without building. Some examples of things the compiler catches:

Runtime errors stop the compiled program, print one line, and exit with code 1:

cryn: runtime error: division by zero
cryn: runtime error: index 9 out of bounds (size 4)
cryn: runtime error: free of an address that is not a heap block ...
cryn: runtime error: use of a handle that is not open in this context ...

These are considered defined behavior: the program stops at a known point with a clear reason. Deliberate crashes are how Cryn avoids silently corrupt results.

26. Reading what the compiler tells you

Chapter 25 shows what the messages look like. This chapter shows what to do about them.

Three commands do the looking:

CommandWhat it shows
cryn check file.cryntype checks without building anything
cryn emit-ir file.crynthe program as the checker understood it, one step per line
cryn emit-c file.cryn -o out.cthe generated C, for comparing against something familiar

A worked example. This loop counts a cell down to 0 and prints nothing, and it does not compile:

define main
  make int64 i
  set 3
  move i
  write
  home
  set true
  mark again
  move i
  read
  compare 0
  jump done when equal
  set 0
  home
  move i
  read
  subtract 1
  move i
  write
  home
  jump again
  mark done
  set 0
  return
end

The compiler answers:

count.cryn:21:13: error: control flow joins at this label: working value
type differs between paths (bool vs int64); normalize it with 'set'
before entering the loop or branch

Read it in pieces. File, line 21, column 13, which is the jump again. The label where the paths disagree is again. The first arrival holds true, a bool. The jump back at the end of the loop holds the new count, an int64. The fix is one word: set 0 instead of set true before the label.

The same disagreement happens with the focus:

error: control flow joins at this label: focus depth differs between
paths (2 vs 1)

One path arrives at the label still sitting inside an array, the other at the frame root. A home on the deeper path, before the branch, fixes it.

Runtime errors stop the program instead of the build:

cryn: runtime error: index 9 out of bounds (size 3)

This one names the index tried and the real size, so the guilty move index is easy to find. One line, exit code 1, nothing else runs.

When a program compiles, runs, and still does the wrong thing, emit-ir is the next stop. It prints every step with its type:

transformation getelem(blk:address array of int64 idx:int64) result int64
     0 move-local idx  [int64]
     1 load  [int64]
     2 push  [int64]
     3 move-local blk  [address array of int64]
     4 load  [address array of int64]
     5 move-at  [array of int64]
     6 pop  [int64]
     7 move-index  [int64]
     8 load  [int64]
     9 return

Reading it top to bottom usually exposes the step you did not write: a missing push, or a read that replaced a value still needed.

Exercise: take the countdown from chapter 8, change the set 0 before the loop to set true, and read the diagnostic the same way. Then fix it and confirm the program runs.

27. Limits

Fixed limits in this version, all enforced with clear messages:

LimitValue
focus history entries per context1024
labels per transformation4096
cells per transformation4096
value stack depth the compiler proves4096
open files per context256
outstanding spawned contexts per context256
one make <size> of allocation16 TiB of data, checked in bytes
distinct types in one programbounded only by memory

28. Common mistakes and how to fix them

Loop joins with different working value types. The place where a loop rejoins must see the same type of working value from every direction. Fix: set 0 (or a value of your chosen type) right before the label or before jumping back.

Returning with a full pile. push without a matching pop or call leaves values on the value stack, and return refuses. Fix: balance them.

A conditional jump after a label that some path reaches without a compare. The comparison state must be valid on every arrival. Fix: compare again after the label before the when jump.

Storing a float into an integer cell. The checker sends you to convert and means it. Convert first, then write.

back at the frame root. The history is empty there. Fix: home is what you wanted.

output of nothing. set nothing clears your hand, it does not print. Fix: print a value or use output text with a string.

Handling handles loosely. Keep each handle in its own cell the moment open gives it, check open results with compare 0, and never use or close a handle after it was closed. Handles are plain numbers, and a closed handle's number can be handed out again by the next open.

29. Cheat sheet

[# comment ... #]                 comment, may span lines

set <literal>                     working value = literal
add/subtract/multiply/divide/remainder [literal]
negate                            working value = -working value
convert <type>                    change working value type
compare [literal]                 compare working value with other
push / pop / swap                 value stack: put / take / exchange

make <type> [name]                cell (heap cell if no name)
make array <n> of <type> [name]   array cell
make string "..." [name]          string cell
make <size> of <type> [name]      heap block, address into working value
free                              free heap block at working value

move <name> / move <n> / move index / move at
back / home                       focus movement
read / write                      copy between focus and working value
address [name]                    working value = address

jump <label> [when <condition>]   conditions: less equal greater
                                  different less-or-equal greater-or-equal
mark <label>                      define a label
call <name> / call                run transformation (or reference)
return / stop                     end transformation / end program

input                             read one byte, -1 at end
output / output text              print working value / text at address
open read|write|both|append       open path at address, handle out
use [0]                           route streams through handle
flush                             flush output stream
seek [n]                          position handle streams at byte n
close                             close handle in working value
system                            run shell command at address

spawn <name>                      run zero-parameter transformation in a thread
wait                              join all spawned
exchange                          atomic swap of working value and focus

30. Where to go next