RRunes
        Learn Symbolic by Building
        ==========================
        seven programs, from "hello" to Conway's Game of Life

This is a hands-on tutorial. Instead of describing the language, we write it - seven complete programs, each teaching new ideas by building something that runs. Type them in, run them, break them, fix them. By the end you'll have written loops, functions, recursion, stdin I/O, a real parser, and a full cellular-automaton simulation.

Companion docs: the Language Guide is the systematic reference (read it alongside this), and spec.md is the terse symbol table. Here we just build.

Every program in this tutorial lives ready-to-run in examples/tutorial/. You can always check your work against those files.

Contents


Setup

Preferred — install the full toolchain:

bash install.sh
source ~/.symbolic/env

This puts symc on your PATH. The self-hosting symc reads source from stdin and writes a binary to stdout:

symc < myprog.sym > myprog && chmod +x myprog && ./myprog

Alternative — build the Rust seed only:

cargo build --release -p symc0

Make a helper so you can compile-and-run quickly. Save this as run.sh:

#!/usr/bin/env sh
# usage: ./run.sh myprog.sym   (optionally pipe input)
./target/release/symc0 --no-run --target x64-linux -o /tmp/a "$1" && /tmp/a
chmod +x run.sh

./run.sh foo.sym compiles foo.sym to a native ELF and runs it. Use --target arm64-linux for AArch64, --target riscv64-linux for RISC-V, etc. (See Language Guide §13 for all thirteen targets.)

A Symbolic program is just a list of statements. The smallest one:

((hi\n)) > @screen
!!

((...)) is text, > sends it somewhere, @screen is the terminal, !! stops the program. Run it and you should see hi. That's the entire ceremony - there's no main, no imports. Let's build something.


Project 1 - Tip calculator

Goal: compute the total of an $80 bill with a 15% tip, then split it 4 ways. You'll learn registers (variables) and the arithmetic symbols.

Step 1 - store a value

A register is created by flowing a value into ~$name:

80 > ~$bill

Read it back as $bill (no ~). Let's print it with the built-in :wrint ("write integer"):

80 > ~$bill
:wrint { [$bill] } !     ::: prints 80

Step 2 - do the math

Arithmetic is a family of symbols. The two you need here:

+    add          ++   multiply
-    subtract     --   divide

15% of the bill is bill * 15 / 100:

$bill ++ 15 > ~$tip      ::: 80 * 15 = 1200
$tip -- 100 > ~$tip      ::: 1200 / 100 = 12

Notice the pattern: read, compute, write back into a register.

Step 3 - the whole program

::: examples/tutorial/01_tip.sym
80 > ~$bill
$bill ++ 15 > ~$tip      ::: 80 * 15
$tip -- 100 > ~$tip      ::: / 100  -> 12
$bill + $tip > ~$total   ::: 92
$total -- 4 > ~$each     ::: 23
:wrint { [$tip] } !       ::: 12
:wrint { [$total] } !     ::: 92
:wrint { [$each] } !      ::: 23
!!
./run.sh examples/tutorial/01_tip.sym
::: 12
::: 92
::: 23

Your turn.

  1. Change the tip to 20%. (Hint: ++ 20.)
  2. Print the tip amount as well as the total.
  3. Symbolic integers are whole numbers - -- truncates. What does 83 -- 4 give, and why? (Try it with :wrint { [83 -- 4] } !.)

Project 2 - FizzBuzz

Goal: print 1 to 15, but "Fizz" for multiples of 3, "Buzz" for multiples of 5, and "FizzBuzz" for multiples of both. You'll learn loops, conditionals, and the modulo operator.

Step 1 - count to 15 with a loop

A loop is a conditional whose closing brace has a trailing ?. It repeats while the condition is true. -= means "less-or-equal":

1 > ~$i
?[$i -= 15]{          ::: while i <= 15
    :wrint { [$i] } !
    $i + 1 > ~$i       ::: don't forget to advance, or it loops forever!
}?

Step 2 - test divisibility with modulo

--- is modulo (remainder). i is a multiple of 3 exactly when i --- 3 == 0:

$i --- 3 > ~$m3       ::: remainder of i / 3
?[$m3 == 0]{ ((Fizz\n)) > @screen }

Step 3 - choose between cases with -?

-? is else (the - negates the ?). Chain them for else-if. Check the most specific case (FizzBuzz) first:

::: examples/tutorial/02_fizzbuzz.sym
1 > ~$i
?[$i -= 20]{
    $i --- 15 > ~$m15
    $i --- 3 > ~$m3
    $i --- 5 > ~$m5
    ?[$m15 == 0]{ ((FizzBuzz\n)) > @screen } -?{
    ?[$m3 == 0]{ ((Fizz\n)) > @screen } -?{
    ?[$m5 == 0]{ ((Buzz\n)) > @screen } -?{
        :wrint { [$i] } !
    }}}
    $i + 1 > ~$i
}?
!!
./run.sh examples/tutorial/02_fizzbuzz.sym
::: 1 2 Fizz 4 Buzz Fizz 7 8 Fizz Buzz 11 Fizz 13 14 FizzBuzz 16 17 Fizz 19 Buzz

The three nested ?...-?{ ... } form an if / else-if / else-if / else ladder. The closing }}} shuts all three.

Your turn.

  1. Make it count to 30.
  2. Add a rule: print "Bazz" for multiples of 7.
  3. What happens if you remove $i + 1 > ~$i? (Press Ctrl-C to stop it - then you've felt your first infinite loop.)

Project 3 - Counting input

Goal: read everything from standard input and report how many bytes and lines it contains. You'll learn how a program reads stdin and walks through memory.

Step 1 - slurp stdin

:rdall reads all of stdin into memory and returns a pointer. The data is laid out as an 8-byte length followed by the raw bytes:

:rdall { } ! > ~$buf
:ld64 { [$buf] } ! > ~$len     ::: first 8 bytes = the length

:ld64 loads 8 bytes from an address; :ld8 loads a single byte.

Step 2 - walk every byte

The bytes start at $buf + 8. Index them with + $i:

0 > ~$i
?[$i --= $len]{                 ::: while i < len
    :ld8 { [$buf + 8 + $i] } ! > ~$c
    ::: ...inspect $c here...
    $i + 1 > ~$i
}?

Step 3 - count newlines

A newline is byte 10. Keep a counter and bump it when you see one:

::: examples/tutorial/03_count.sym
:rdall { } ! > ~$buf
:ld64 { [$buf] } ! > ~$len
0 > ~$i
0 > ~$lines
0 > ~$words
0 > ~$inwd    ::: 1 while inside a word
?[$i --= $len]{
    :ld8 { [$buf + 8 + $i] } ! > ~$c
    ?[$c == 10]{ $lines + 1 > ~$lines }
    $c == 32 > ~$sp
    $c == 10 > ~$nl
    $sp &+ $nl > ~$ws    ::: ws=1 if space or newline
    ?[$ws == 0]{
        ?[$inwd == 0]{ $words + 1 > ~$words }
        1 > ~$inwd
    } -?{
        0 > ~$inwd
    }
    $i + 1 > ~$i
}?
:wrint { [$len] } !      ::: total bytes
:wrint { [$lines] } !    ::: number of lines
:wrint { [$words] } !    ::: number of words
!!
printf 'ab\ncd\nef\n' | ./run.sh examples/tutorial/03_count.sym
::: 9
::: 3
::: 3

Your turn.

  1. Also count spaces (byte 32) separately.
  2. Print only the bytes that are digits (bytes 48–57).
  3. Extend to handle tab (byte 9) as whitespace too.

Project 4 - A secret-message cipher

Goal: implement ROT13 - rotate each letter 13 places through the alphabet (a->n, n->a). Applying it twice gives back the original. You'll write your first function and do byte arithmetic.

Step 1 - a function to rotate one letter

A function is :name { [type:$param] } body >!?. The >!? returns a value. To rotate a letter we subtract its base (a=97 or A=65), add 13, wrap with modulo 26, and add the base back:

:rot { [i64:$c] & [i64:$lo] }
    $c - $lo > ~$d
    $d + 13 > ~$d
    $d --- 26 > ~$d
    $lo + $d >!?

Two parameters, separated by &: the character $c and its alphabet base $lo.

Step 2 - detect letters

A byte is a lowercase letter when it's >= 97 and <= 122. Comparisons return 1/0, and & is bitwise-and, so we can combine them:

$c =+ 97 > ~$a   $c -= 122 > ~$b   $a & $b > ~$lc   ::: $lc is 1 if lowercase
$c =+ 65 > ~$a   $c -= 90  > ~$b   $a & $b > ~$uc   ::: $uc is 1 if uppercase

(=+ is "greater-or-equal", -= is "less-or-equal".)

Step 3 - put it together

Read stdin (as in Project 3), rotate letters, pass everything else through with :wrch (write one byte):

::: examples/tutorial/04_rot13.sym
:rot { [i64:$c] & [i64:$lo] }
    $c - $lo > ~$d
    $d + 13 > ~$d
    $d --- 26 > ~$d
    $lo + $d >!?
:rdall { } ! > ~$buf
:ld64 { [$buf] } ! > ~$len
0 > ~$i
?[$i --= $len]{
    :ld8 { [$buf + 8 + $i] } ! > ~$c
    $c =+ 97 > ~$a  $c -= 122 > ~$b  $a & $b > ~$lc
    $c =+ 65 > ~$a  $c -= 90  > ~$b  $a & $b > ~$uc
    ?[$lc == 1]{ :rot { [$c] & [97] } ! > ~$c } -?{
    ?[$uc == 1]{ :rot { [$c] & [65] } ! > ~$c } }
    :wrch { [$c] } !
    $i + 1 > ~$i
}?
!!
printf 'Hello, World!' | ./run.sh examples/tutorial/04_rot13.sym
::: Uryyb, Jbeyq!

Pipe the output back in and you get Hello, World! again - ROT13 is its own inverse.

Your turn.

  1. Make a :shift { [i64:$c] & [i64:$lo] & [i64:$k] } that rotates by any amount $k (a Caesar cipher). ROT13 is then :shift { ... & 13 }.
  2. Leave digits untouched but rotate them within 0–9 too.

Project 5 - Fibonacci

Goal: compute Fibonacci numbers - once with a loop, once with recursion - and see the trade-off. You'll get comfortable with functions that return values.

The iterative version

Keep two running values and slide them forward n times:

::: examples/tutorial/05_fib.sym
:fib { [i64:$n] }
    0 > ~$a
    1 > ~$b
    0 > ~$i
    ?[$i --= $n]{
        $a + $b > ~$t
        $b > ~$a
        $t > ~$b
        $i + 1 > ~$i
    }?
    $a >!?
0 > ~$k
?[$k -= 10]{
    :fib { [$k] } ! > ~$r
    :wrint { [$r] } !
    $k + 1 > ~$k
}?
!!
./run.sh examples/tutorial/05_fib.sym
::: 0 1 1 2 3 5 8 13 21 34 55

The recursive version

A function may call itself. >!? can return early from inside a conditional:

:fib { [i64:$n] }
    ?[$n --= 2]{ $n >!? }       ::: fib(0)=0, fib(1)=1
    $n - 1 > ~$a
    $n - 2 > ~$b
    :fib { [$a] } ! > ~$x
    :fib { [$b] } ! > ~$y
    $x + $y >!?
:fib { [10] } ! > ~$r
:wrint { [$r] } !     ::: 55

Both print 55. The recursive one is shorter and mirrors the math, but it recomputes sub-results, so it's far slower for large n. Try fib(30) with each and time them with ./stage1/bench.sh for inspiration.

Your turn.

  1. Add a :fact { [i64:$n] } that computes n! (use ++ to multiply).
  2. Print fib(0) through fib(15) in a loop.

Project 6 - A calculator

Goal: a working reverse-Polish (RPN) calculator. It reads an expression like 3 4 + 5 ++ from stdin and prints the result (35). This pulls together a heap-backed stack, a small parser, functions, and the operator family

  • the same shape as a real interpreter.

RPN means operators come after their operands: 3 4 + is "3 plus 4". You evaluate it with a stack - push numbers, and when you see an operator pop two numbers and push the result.

Step 1 - a stack in memory

We need a push/pop stack. Store it on the heap (:alloc) and track the depth in a hash cell. (#name declares a program-global cell - see Language Guide §9.)

#stk 0       ::: pointer to the value stack
#sp 0        ::: how many values are on it

:push { [i64:$v] }
    #stk > ~$s
    #sp > ~$n
    :st64 { [$s + $n ++ 8] & [$v] } !   ::: stack[sp] = v
    $n + 1 > ~#sp
    0 >!?
:pop { }
    #sp - 1 > ~#sp
    #stk > ~$s
    #sp > ~$n
    :ld64 { [$s + $n ++ 8] } ! >!?       ::: return stack[--sp]

$n ++ 8 is n * 8 - each value is 8 bytes, so slot n is at byte n*8.

Step 2 - read a number

When the current byte is a digit (48–57), keep reading digits and build the number with the classic num = num*10 + digit loop:

0 > ~$num
?[$dig == 1]{
    :ld8 { [$src + $i] } ! > ~$c
    $c =+ 48 > ~$ge  $c -= 57 > ~$le  $ge & $le > ~$dig
    ?[$dig == 1]{
        $num ++ 10 > ~$num
        $num + $c - 48 > ~$num
        $i + 1 > ~$i
    } -?{ !!> }            ::: stop at the first non-digit (!!> breaks the loop)
}?
:push { [$num] } !

Step 3 - apply an operator

For +, peek the next byte: if it's also +, it's ++ (multiply). The same trick distinguishes - (subtract) from -- (divide). Pop two, combine, push:

?[$c == 43]{                       ::: '+'
    :ld8 { [$src + $i + 1] } ! > ~$c2
    :pop { } ! > ~$y
    :pop { } ! > ~$x
    ?[$c2 == 43]{ $x ++ $y > ~$r  $i + 1 > ~$i } -?{ $x + $y > ~$r }
    :push { [$r] } !
    $i + 1 > ~$i
} -?{
?[$c == 45]{                       ::: '-'
    :ld8 { [$src + $i + 1] } ! > ~$c2
    :pop { } ! > ~$y
    :pop { } ! > ~$x
    ?[$c2 == 45]{ $x -- $y > ~$r  $i + 1 > ~$i } -?{ $x - $y > ~$r }
    :push { [$r] } !
    $i + 1 > ~$i
}}

Step 4 - the whole calculator

::: examples/tutorial/06_rpn.sym
#stk 0
#sp 0

:push { [i64:$v] }
    #stk > ~$s
    #sp > ~$n
    :st64 { [$s + $n ++ 8] & [$v] } !
    $n + 1 > ~#sp
    0 >!?
:pop { }
    #sp - 1 > ~#sp
    #stk > ~$s
    #sp > ~$n
    :ld64 { [$s + $n ++ 8] } ! >!?

:rdall { } ! > ~$buf
:ld64 { [$buf] } ! > ~$len
$buf + 8 > ~$src
:alloc { [8192] } ! > ~#stk
0 > ~#sp
0 > ~$i
?[$i --= $len]{
    :ld8 { [$src + $i] } ! > ~$c
    $c =+ 48 > ~$ge  $c -= 57 > ~$le  $ge & $le > ~$dig
    ?[$dig == 1]{
        0 > ~$num
        ?[$dig == 1]{
            :ld8 { [$src + $i] } ! > ~$c
            $c =+ 48 > ~$ge  $c -= 57 > ~$le  $ge & $le > ~$dig
            ?[$dig == 1]{
                $num ++ 10 > ~$num
                $num + $c - 48 > ~$num
                $i + 1 > ~$i
            } -?{ !!> }
        }?
        :push { [$num] } !
    } -?{
        ?[$c == 43]{
            :ld8 { [$src + $i + 1] } ! > ~$c2
            :pop { } ! > ~$y
            :pop { } ! > ~$x
            ?[$c2 == 43]{ $x ++ $y > ~$r  $i + 1 > ~$i } -?{ $x + $y > ~$r }
            :push { [$r] } !
            $i + 1 > ~$i
        } -?{
        ?[$c == 45]{                ::: '-' or '--'
            :ld8 { [$src + $i + 1] } ! > ~$c2
            :pop { } ! > ~$y
            :pop { } ! > ~$x
            ?[$c2 == 45]{ $x -- $y > ~$r  $i + 1 > ~$i } -?{ $x - $y > ~$r }
            :push { [$r] } !
            $i + 1 > ~$i
        } -?{
            $i + 1 > ~$i            ::: skip spaces and anything else
        }}
    }
}?
:pop { } ! > ~$res
:wrint { [$res] } !
!!
printf '3 4 +'        | ./run.sh examples/tutorial/06_rpn.sym   ::: 7
printf '3 4 + 5 ++'   | ./run.sh examples/tutorial/06_rpn.sym   ::: 35
printf '10 2 - 3 ++'  | ./run.sh examples/tutorial/06_rpn.sym   ::: 24
printf '10 2 --'      | ./run.sh examples/tutorial/06_rpn.sym   ::: 5

You just wrote a program that reads text, parses numbers and operators, and evaluates them with a stack - the heart of every interpreter and compiler. (The language's own self-hosting compiler, sigil/runes/symc/src/main.sym, is the same idea, scaled up: read text, parse, emit. See SELFHOSTING.md.)

Your turn.

  1. Add -- (divide) and --- (modulo).
  2. Add a n token that negates the top of the stack.
  3. Print the whole stack at the end, not just the top, by looping 0..#sp.

Capstone - Conway's Game of Life

Goal: real software - a working cellular-automaton simulator. Conway's Game of Life is a famous "zero-player game": you seed a grid of cells, and they live or die by simple rules, producing astonishingly complex behaviour. We'll animate a glider drifting across the screen.

This is the kind of program people actually write - a 2-D world held in memory, stepped through time, drawn each frame. It uses everything from this tutorial: hash cells for global state, heap buffers, nested loops, functions, and conditionals.

The rules

The board is a grid of cells, each alive (#) or dead (.). Every generation, each cell looks at its 8 neighbours:

   a live cell with 2 or 3 live neighbours  ->  stays alive
   a live cell with any other count         ->  dies
   a dead cell with exactly 3 live neighbours ->  becomes alive

We make the grid a torus - the edges wrap around - so a glider can fly forever.

Step 1 - the board in memory

A 12 × 12 grid is 144 cells. We keep two buffers: the current generation and the next one (you can't update in place - a cell's new state depends on its neighbours' old states). Hash cells hold the pointers and dimensions:

#W 12
#H 12
#cur 0       ::: pointer to the current grid
#next 0      ::: pointer to the grid we're building

A cell (x, y) lives at offset y * W + x. Reading one (++ is multiply):

:cell { [i64:$b] & [i64:$x] & [i64:$y] }
    $y ++ #W > ~$idx
    $idx + $x > ~$idx
    :ld8 { [$b + $idx] } ! >!?

Step 2 - wrap-around coordinates

To make the board a torus, a coordinate of -1 becomes W-1 and W becomes 0. Adding m before the modulo handles the negative case:

:wrap { [i64:$v] & [i64:$m] }
    $v + $m > ~$v
    $v --- $m >!?

Step 3 - count live neighbours

Loop dx and dy over -1, 0, 1, skip the centre (0,0), wrap each coordinate, and sum the live cells:

:nbrs { [i64:$b] & [i64:$x] & [i64:$y] }
    0 > ~$n
    0 - 1 > ~$dy
    ?[$dy -= 1]{
        0 - 1 > ~$dx
        ?[$dx -= 1]{
            $dx == 0 > ~$z1
            $dy == 0 > ~$z2
            $z1 & $z2 > ~$ctr          ::: 1 only at the centre
            ?[$ctr == 0]{
                $x + $dx > ~$nx  :wrap { [$nx] & [#W] } ! > ~$nx
                $y + $dy > ~$ny  :wrap { [$ny] & [#H] } ! > ~$ny
                :cell { [$b] & [$nx] & [$ny] } ! > ~$v
                $n + $v > ~$n
            }
            $dx + 1 > ~$dx
        }?
        $dy + 1 > ~$dy
    }?
    $n >!?

Step 4 - one generation

For every cell, apply the rules into #next, then swap the two buffers:

:step { }
    0 > ~$y
    ?[$y --= #H]{
        0 > ~$x
        ?[$x --= #W]{
            :cell { [#cur] & [$x] & [$y] } ! > ~$alive
            :nbrs { [#cur] & [$x] & [$y] } ! > ~$n
            0 > ~$nv
            ?[$alive == 1]{
                $n == 2 > ~$a  $n == 3 > ~$b  $a &+ $b > ~$nv   ::: 2 or 3 -> live
            } -?{
                $n == 3 > ~$nv                                   ::: born on 3
            }
            $y ++ #W > ~$idx  $idx + $x > ~$idx
            :st8 { [#next + $idx] & [$nv] } !
            $x + 1 > ~$x
        }?
        $y + 1 > ~$y
    }?
    #cur > ~$t  #next > ~#cur  $t > ~#next       ::: swap cur <-> next
    0 >!?

Step 5 - draw, seed, and run

A :show that prints # / . row by row, a :set to light up a cell, a glider seed, and a loop of generations. The full program:

::: examples/tutorial/07_life.sym  - Conway's Game of Life (a drifting glider)
#W 12
#H 12
#cur 0
#next 0

:wrap { [i64:$v] & [i64:$m] }
    $v + $m > ~$v
    $v --- $m >!?
:cell { [i64:$b] & [i64:$x] & [i64:$y] }
    $y ++ #W > ~$idx
    $idx + $x > ~$idx
    :ld8 { [$b + $idx] } ! >!?
:set { [i64:$x] & [i64:$y] }
    $y ++ #W > ~$idx
    $idx + $x > ~$idx
    :st8 { [#cur + $idx] & [1] } !
    0 >!?
:nbrs { [i64:$b] & [i64:$x] & [i64:$y] }
    0 > ~$n
    0 - 1 > ~$dy
    ?[$dy -= 1]{
        0 - 1 > ~$dx
        ?[$dx -= 1]{
            $dx == 0 > ~$z1
            $dy == 0 > ~$z2
            $z1 & $z2 > ~$ctr
            ?[$ctr == 0]{
                $x + $dx > ~$nx  :wrap { [$nx] & [#W] } ! > ~$nx
                $y + $dy > ~$ny  :wrap { [$ny] & [#H] } ! > ~$ny
                :cell { [$b] & [$nx] & [$ny] } ! > ~$v
                $n + $v > ~$n
            }
            $dx + 1 > ~$dx
        }?
        $dy + 1 > ~$dy
    }?
    $n >!?
:step { }
    0 > ~$y
    ?[$y --= #H]{
        0 > ~$x
        ?[$x --= #W]{
            :cell { [#cur] & [$x] & [$y] } ! > ~$alive
            :nbrs { [#cur] & [$x] & [$y] } ! > ~$n
            0 > ~$nv
            ?[$alive == 1]{
                $n == 2 > ~$a  $n == 3 > ~$b  $a &+ $b > ~$nv
            } -?{
                $n == 3 > ~$nv
            }
            $y ++ #W > ~$idx  $idx + $x > ~$idx
            :st8 { [#next + $idx] & [$nv] } !
            $x + 1 > ~$x
        }?
        $y + 1 > ~$y
    }?
    #cur > ~$t  #next > ~#cur  $t > ~#next
    0 >!?
:show { }
    0 > ~$y
    ?[$y --= #H]{
        0 > ~$x
        ?[$x --= #W]{
            :cell { [#cur] & [$x] & [$y] } ! > ~$v
            ?[$v == 1]{ :wrch { [35] } ! } -?{ :wrch { [46] } ! }
            $x + 1 > ~$x
        }?
        :wrch { [10] } !
        $y + 1 > ~$y
    }?
    :wrch { [10] } !
    0 >!?

:alloc { [144] } ! > ~#cur
:alloc { [144] } ! > ~#next
::: seed a glider
:set { [1] & [0] } !
:set { [2] & [1] } !
:set { [0] & [2] } !
:set { [1] & [2] } !
:set { [2] & [2] } !
0 > ~$g
?[$g --= 8]{
    :show { } !
    :step { } !
    $g + 1 > ~$g
}?
!!
./run.sh examples/tutorial/07_life.sym

Watch the five-cell glider march diagonally across the grid, generation by generation:

   gen 0          gen 1          gen 2          gen 4
   .#..           ....           ....           ....
   ..#.    -->    #.#.    -->    ..#.    -->    ..#.
   ###.           .##.           #.#.           ...#
   ....           .#..           .##.           .###

You just wrote a real simulation - a world in memory, advanced through time and drawn each frame. The same skeleton (a grid + rules + a render loop) is how you'd write a screensaver, a falling-sand toy, or the core of a roguelike.

Your turn.

  1. Run more generations - change ?[$g --= 5] to ?[$g --= 20].
  2. Make the grid bigger (e.g. #W 30, #H 16) - remember to resize the :alloc from 144 to W*H.
  3. Seed a different pattern: a blinker (three cells in a row) oscillates; a block (a 2×2 square) stays put. Try them.
  4. Clear the screen between frames (((\x1b[2J\x1b[H)) > @screen before each :show) for a flip-book animation in your terminal.

Reading compiler errors

When something is wrong, symc0 tells you with a line number. The two you'll see most:

error: ... name '...' exceeds maximum length of 6 characters

Register, label, and type names are at most 6 characters. Shorten the name.

warning: register 'x' may be used before initialization

You read $x before any ... > ~$x wrote it. Make sure every register is assigned before it's used.

If a program compiles but loops forever, you almost certainly forgot to advance a counter ($i + 1 > ~$i) or a cursor inside a loop - the single most common bug.


Where to go next

You can now write real Symbolic programs. To go deeper:

  • Language Guide - the full reference: every operator, hashes, structs/enums, generics ($T$), traits (^.^), closures (>{ }), ternary types, and the build targets.
  • examples/features/ - one minimal program per language feature, each with its expected output in run.sh.
  • SELFHOSTING.md - read how the language compiles itself, then open sigil/runes/symc/src/main.sym. After this tutorial, it's just a (big) program you already know how to read.
        Build something. The compiler is small enough to understand,
        and now, so is the language.