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
- Project 1 - Tip calculator (values, arithmetic, printing)
- Project 2 - FizzBuzz (loops, conditionals, else-if)
- Project 3 - Counting input (stdin, memory, bytes)
- Project 4 - A secret-message cipher (functions, byte math)
- Project 5 - Fibonacci (functions two ways)
- Project 6 - A calculator (a heap stack + a parser)
- Capstone - Conway's Game of Life (real software: a simulation)
- Reading compiler errors
- Where to go next
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.
- Change the tip to 20%. (Hint:
++ 20.)- Print the tip amount as well as the total.
- Symbolic integers are whole numbers -
--truncates. What does83 -- 4give, 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.
- Make it count to 30.
- Add a rule: print "Bazz" for multiples of 7.
- 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.
- Also count spaces (byte 32) separately.
- Print only the bytes that are digits (bytes 48–57).
- 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.
- Make a
:shift { [i64:$c] & [i64:$lo] & [i64:$k] }that rotates by any amount$k(a Caesar cipher). ROT13 is then:shift { ... & 13 }.- 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.
- Add a
:fact { [i64:$n] }that computesn!(use++to multiply).fib(0)throughfib(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.
- Add
--(divide) and---(modulo).- Add a
ntoken that negates the top of the stack.- 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.
- Run more generations - change
?[$g --= 5]to?[$g --= 20].- Make the grid bigger (e.g.
#W 30,#H 16) - remember to resize the:allocfrom144toW*H.- Seed a different pattern: a blinker (three cells in a row) oscillates; a block (a 2×2 square) stays put. Try them.
- Clear the screen between frames (
((\x1b[2J\x1b[H)) > @screenbefore 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 inrun.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.