Symbolic for Dummies
The complete, leave-nothing-out, hold-your-hand guide to the Symbolic language. If you can copy and paste, you can do every single thing in this document. There is no step you have to "already know."
How to use this page:
- Read top to bottom the first time.
- After that, jump to the section you need.
- Every code block is a real program or fragment. Lines starting with
:::are comments — the computer ignores them; we use them to show what prints.
The one rule to remember: code lives in files ending
.sym. That's it. Not.rs, not.py. Symbolic programs are.symfiles.
Table of contents
- Install it (every platform)
- Run your first program
- The shape of a program
- The 4 grammar rules (the master key)
- Printing things
- Values you can write down (literals)
- Registers (variables)
- Every operator
- Bit widths (the comma)
- Making decisions (if / else)
- Repeating things (loops)
- Stopping (halt, break, panic)
- Pattern matching
- Functions
- Built-in functions (the toolbox)
- Memory and segments
- Reading input
- Hash cells (global storage)
- Structs (your own types)
- Enums and matching them
- Generics
- Traits, impls, and methods
- Closures
- Ternary computing (base-3)
- Building for any platform (targets)
- Using
sigil(the project tool) - When something goes wrong
- The complete symbol cheat-sheet
- "How do I…?" recipe list
- Where to go next
1. Install it (every platform)
You need two programs: symc (the compiler) and sigil (the project
tool). Pick the row that matches your computer. Type the commands exactly.
Linux / macOS / FreeBSD — easiest (one line)
curl -fsSL https://dist.sigil-lang.org/get.sh | sh
This downloads symc + sigil and adds them to your PATH automatically. Open
a new terminal afterward so they work. (No curl? Install it, or use the
clone method below.)
Any Unix from a copy of the repo (no internet needed after cloning)
git clone https://github.com/oda-00/symbolic.git
cd symbolic
sh dist/install.sh # copies the prebuilt symc + sigil and sets PATH
Want to rebuild from source instead of using the prebuilt copy? Run
bash install.sh — it builds the compiler with itself and proves it works,
then installs. Either way, open a new terminal when it finishes.
Windows
Symbolic runs on Windows. The one-line curl method is Unix-only, so on Windows
use a clone:
git clone https://github.com/oda-00/symbolic.git
cd symbolic
powershell -ExecutionPolicy Bypass -File dist\install.ps1
This copies symc.exe + sigil.exe and adds them to your user PATH. Open a
new PowerShell window afterward.
Android (Termux)
Same as Linux — the installers detect Termux automatically:
sh dist/install.sh
Did it work?
Open a new terminal and type:
sigil help
If you see a list of commands, you're done. 🎉 If it says "command not found," you didn't open a new terminal, or PATH wasn't set — run the installer again and read its last line.
2. Run your first program
There are two ways to run Symbolic code. Learn both; use whichever you like.
Way A — the easy way, with sigil (recommended)
sigil makes a little project for you and runs it.
sigil new hello # makes a folder called hello/ with a starter program
cd hello
sigil run # builds and runs it
You should see:
hello, rune!
That's a working program. The code is in hello/src/main.sym — open it and
change the text.
Way B — the raw way, with symc
symc reads a program from "standard input" and writes a finished program to
"standard output." On Unix:
echo '((hi!\n)) > @screen
!!' > hi.sym
symc < hi.sym > hi # compile: hi.sym -> hi (a real program)
chmod +x hi # mark it runnable (Unix only)
./hi # run it -> hi!
On Windows it's almost the same, but the output is a .exe and you don't
need chmod:
symc < hi.sym > hi.exe
.\hi.exe
<means "feed this file in.">means "save the output here." This is your shell's plumbing, not Symbolic's.
3. The shape of a program
A Symbolic program is just a list of statements, top to bottom. There is no
main, no imports, no boilerplate.
((hello, world\n)) > @screen ::: do this
!! ::: then stop
(( ... ))is text (a "string").>means flow: send the thing on the left to the place on the right.@screenis the screen.!!stops the program.:::starts a comment — everything after it on that line is ignored.
That is a complete program. Run it and it prints hello, world.
4. The 4 grammar rules (the master key)
Symbolic has no keywords (no if, while, return, etc.). Instead a few
symbols combine by rules. Learn these and you can guess the language.
RULE 1. - negates flips a symbol's meaning (+ add → - subtract)
RULE 2. + extends broadens/loosens a symbol (= equal → =+ greater-equal)
RULE 3. , reduces shrinks the size in bits ($a → $a, is 32-bit)
RULE 4. spacing matters touching = one combined symbol; spaced = two symbols
PLUS: doubling/tripling intensifies (+ add → ++ multiply → +++ power)
Watch one family grow:
+ add
++ multiply ::: doubled = intensified
+++ power ::: tripled = intensified more
- subtract ::: negated
-- divide
--- modulo (remainder)
And the question-mark family:
?[c]{ ... } do once if c is true (an "if")
?[c]{ ... }? do over and over while true (a "loop" — note the trailing ?)
-?{ ... } otherwise (an "else" — the - negates the ?)
?? match a value against cases
You don't have to memorize every symbol — Section 28 is a full cheat-sheet. Just know the rules so the symbols make sense.
The naming rule: names for registers, functions, and types use letters,
digits, and _, and are at most 6 characters long. Keep names short.
5. Printing things
Four ways to put something on the screen. Use the right one for what you have.
((words go here\n)) > @screen ::: 1) print text. \n is a new line.
:wrint { [42] } ! ::: 2) print a NUMBER in decimal, + a newline -> 42
:wrch { [65] } ! ::: 3) print ONE byte. 65 is the letter 'A' -> A
:wrch { [10] } ! ::: 10 is a newline byte
The fourth way prints a chunk of memory (you'll meet it in Section 16):
:wrbuf { [$ptr] & [$len] } ! ::: 4) print $len bytes starting at address $ptr
Quick rules:
- Have text?
(( ... )) > @screen. - Have a number and want to read it (like
42)?:wrint. - Have a single character/byte?
:wrch.
⚠️
$x > @screensends the raw byte, not the digits.99 > @screenprints the characterc(byte 99), not99. To see99, use:wrint.
6. Values you can write down (literals)
42 ::: a decimal number
0xFF ::: hexadecimal -> 255
0b1010 ::: binary -> 10
3.14 ::: a floating-point number
(A) ::: a character -> its byte value, 65
((hi\n)) ::: a string (text). \n = newline, \t = tab, \\ = backslash
The default number is a 64-bit integer. Characters are just their byte
number, so (A) is exactly 65.
7. Registers (variables)
A register is Symbolic's word for a variable. It holds one 64-bit number.
Make one (and read it)
80 > ~$bill ::: create $bill, put 80 in it
:wrint { [$bill] } ! ::: 80
~$name= write (create or change). The~means "I am binding this."$name= read (just look at it; changes nothing).
Change one
To change a register, write to it again. The pattern is read → compute → write back:
0 > ~$n ::: $n = 0
$n + 1 > ~$n ::: read $n (0), add 1, store -> $n = 1
$n + 1 > ~$n ::: $n = 2
:wrint { [$n] } ! ::: 2
That read-compute-write line is the single most common thing you will write.
Copying
5 > ~$a
$a > ~$b ::: $b is now 5 too (numbers are copied)
:wrint { [$b] } ! ::: 5
💡 The
~is also Symbolic's "ownership" mark (like Rust). For plain numbers it just means "write here." It matters more for big values, where flowing a value into a new name moves it. The compiler will warn you if you read a register before you ever wrote it.
8. Every operator
Every operator takes values and produces a value you can flow somewhere
(print it, store it, etc.). Examples use :wrint so you can see the result.
Arithmetic
:wrint { [3 + 4] } ! ::: 7 add
:wrint { [3 ++ 4] } ! ::: 12 multiply
:wrint { [3 +++ 4] } ! ::: 81 power (3 to the 4th)
:wrint { [10 - 3] } ! ::: 7 subtract
:wrint { [10 -- 3] } ! ::: 3 divide (whole-number; throws away remainder)
:wrint { [10 --- 3]} ! ::: 1 modulo (the remainder)
Comparison (these give 1 for true, 0 for false)
Read them by the grammar: = is the base, + pushes toward greater, -
pushes toward less.
:wrint { [5 == 5] } ! ::: 1 equal
:wrint { [5 != 3] } ! ::: 1 not equal
:wrint { [5 =+ 3] } ! ::: 1 greater-or-equal (>=)
:wrint { [5 =++ 3] } ! ::: 1 greater-than (>)
:wrint { [5 -= 3] } ! ::: 0 less-or-equal (<=)
:wrint { [5 --= 3] } ! ::: 0 less-than (<)
Because they give 1/0, you can store the answer:
7 =++ 4 > ~$big ::: $big = 1
:wrint { [$big] } ! ::: 1
Bitwise and shifts
:wrint { [12 & 10] } ! ::: 8 AND
:wrint { [12 &+ 10] } ! ::: 14 OR (& extended by +)
:wrint { [12 -&+ 10] } ! ::: 6 XOR (- negates, &, extended)
:wrint { [-& 0] } ! ::: -1 NOT (a prefix; flips all bits)
:wrint { [1 -< 4] } ! ::: 16 shift left
:wrint { [256 <+ 2] } ! ::: 64 shift right
:wrint { [1 --< 1] } ! ::: rotate left
:wrint { [1 <++ 1] } ! ::: rotate right
Precedence (what binds tightest)
From loosest to tightest:
comparisons == != --= -= =+ =++
bitwise & &+ -&+
shifts / rotates -< <+ --< <++
add / subtract + -
multiply/div/mod ++ -- ---
power +++ (right to left)
So 2 + 3 ++ 4 means 2 + (3 ++ 4) = 2 + 12 = 14.
🧠 Brain-dead-simple safety tip: if you're not sure how an expression groups, don't guess — split it. Put each step into its own register on its own line. It always works and always reads clearly:
3 ++ 4 > ~$t ::: 12 2 + $t > ~$r ::: 14
9. Bit widths (the comma)
Numbers are 64-bit by default. A touching comma shrinks the size (Rule 3). You can ignore this until you do low-level work.
$a ::: 64-bit (normal)
$a, ::: 32-bit
$a,, ::: 16-bit
$a,,, ::: 8-bit
>, ::: a 32-bit flow
(A comma with spaces around it is a different thing — a separator. Spacing matters, Rule 4.)
10. Making decisions (if / else)
if
?[ condition ]{ body } runs the body once if the condition is true (non-zero).
5 > ~$x
?[$x =++ 3]{ ::: if x > 3
((big\n)) > @screen
}
::: big
else
-?{ body } is the "otherwise" branch (the - negates the ?).
?[$x =++ 10]{ ((big\n)) > @screen }
-?{ ((small\n)) > @screen }
::: small
else-if (a ladder)
Nest them. Check the most specific case first. The closing braces stack up at the end:
?[$n == 0]{ ((zero\n)) > @screen } -?{
?[$n == 1]{ ((one\n)) > @screen } -?{
((other\n)) > @screen }}
Three opens, so two }} at the end close the chain.
11. Repeating things (loops)
Add a trailing ? to the closing brace and the if becomes a loop: it
re-checks the condition every pass and repeats while it's true.
0 > ~$i
?[$i --= 5]{ ::: while i < 5
:wrint { [$i] } !
$i + 1 > ~$i ::: ⚠️ advance the counter, or it loops forever!
}?
::: 0 1 2 3 4
🧠 The #1 beginner bug is forgetting
$i + 1 > ~$i. If your program hangs, you forgot to move the counter. Press Ctrl-C to stop it.
A "loop forever then break" is also common (see next section for !!>):
0 > ~$i
?[1 == 1]{ ::: 1 == 1 is always true -> forever
?[$i =+ 5]{ !!> } ::: until i >= 5, then break out
:wrint { [$i] } !
$i + 1 > ~$i
}?
::: 0 1 2 3 4
12. Stopping (halt, break, panic)
!! stop the whole program, success (like exit 0)
!!! panic: stop immediately because something is wrong (abort)
!!> break out of the loop you're inside right now
!!> ::: jump to just after the innermost }?
Use !! at the very end of most programs (top-level). Use !!> to leave a loop
early. Use !!! only when continuing would be a bug.
13. Pattern matching
?? checks a value against a list of cases. Each case is pattern > { body }.
_ means "anything else," so you always cover every possibility.
3 > ~$n
?? $n {
0 > { ((zero\n)) > @screen }
1 > { ((one\n)) > @screen }
_ > { ((other\n)) > @screen } ::: $n is 3 -> prints "other"
}
This is cleaner than a long if/else-if ladder when you're comparing one value to many fixed options.
14. Functions
A function is a named, reusable block that can take inputs and give back a value.
Declare one
:add { [i64:$a] & [i64:$b] } ::: name :add, two inputs $a and $b
$a + $b >!? ::: >!? gives the value back to the caller
:addis the name.{ [i64:$a] & [i64:$b] }is the input list. Each input is[type:$name], separated by&.i64means "64-bit integer" — the normal number type.>!?returns the value.
Why write
i64:? It tells the compiler "this is a new input named$a," as opposed to a value you're passing in. Inputs always have the type written.
Call one
:name { [value] & [value] } ! — the ! means "do it now." Capture the answer
with > ~$dest:
:add { [20] & [22] } ! > ~$sum
:wrint { [$sum] } ! ::: 42
A function with no inputs is called :name { } !.
As many inputs as you want
:sum8 { [i64:$a]&[i64:$b]&[i64:$c]&[i64:$d]&[i64:$e]&[i64:$f]&[i64:$g]&[i64:$h] }
$a + $b + $c + $d + $e + $f + $g + $h >!?
:sum8 { [1]&[2]&[3]&[4]&[5]&[6]&[7]&[8] } ! > ~$r
:wrint { [$r] } ! ::: 36
Return early
>!? can appear anywhere, even inside an if:
:max { [i64:$a] & [i64:$b] }
?[$a =+ $b]{ $a >!? } ::: if a >= b, hand back a and stop
$b >!?
:max { [3] & [9] } ! > ~$m
:wrint { [$m] } ! ::: 9
A function that calls itself (recursion)
:fac { [i64:$n] }
?[$n --= 2]{ 1 >!? } ::: when n < 2, the answer is 1
$n - 1 > ~$m
:fac { [$m] } ! > ~$r ::: call myself with n-1
$n ++ $r >!? ::: n * fac(n-1)
:fac { [5] } ! > ~$f
:wrint { [$f] } ! ::: 120
🧠 Program shape: define all your functions first, then write the top-level statements that use them, then
!!.
15. Built-in functions (the toolbox)
These are always available — no import needed. They're how your program talks to
the world. Call them like any function (with !).
| Call it like this | What it does |
|---|---|
:wrint { [n] } ! |
print number n in decimal, plus a newline |
:wrch { [b] } ! |
print one byte b (e.g. 10 = newline, 65 = A) |
:wrbuf { [ptr] & [len] } ! |
print len bytes starting at address ptr |
:rdall { } ! > ~$p |
read all of standard input into memory; $p points at [length (8 bytes)][the bytes…] |
:alloc { [n] } ! > ~$p |
grab n bytes of memory; $p is the address |
:ld8 { [addr] } ! > ~$v |
load 1 byte from addr |
:ld64 { [addr] } ! > ~$v |
load 8 bytes (one number) from addr |
:st8 { [addr] & [v] } ! |
store 1 byte v at addr |
:st64 { [addr] & [v] } ! |
store 8 bytes v at addr |
(There are more for ternary math — see Section 24 — and raw system calls via
:sysc for advanced file/OS work.)
16. Memory and segments
Segments
Big named areas of the computer, all written @name:
| Segment | What it is |
|---|---|
@screen |
the display (standard output) |
@inp |
the keyboard (standard input) |
@mem |
general memory (@mem[address]) |
@stck |
the stack |
@sec |
secure memory |
@net @rng @time @sys |
network, randomness, the clock, system vectors |
You've already used one: ((hi\n)) > @screen.
The heap: grab memory, put things in it, read them back
:alloc { [64] } ! > ~$p ::: get 64 bytes; $p is where they are
123456789 > ~$v
:st64 { [$p] & [$v] } ! ::: write 8 bytes at $p
:ld64 { [$p] } ! > ~$r ::: read them back
:wrint { [$r] } ! ::: 123456789
65 > ~$c
:st8 { [$p + 8] & [$c] } ! ::: write the byte 65 ('A') at $p+8
:ld8 { [$p + 8] } ! > ~$b
:wrch { [$b] } ! ::: A
:wrch { [10] } ! ::: newline
Addresses are just numbers, so to walk through memory you add an index:
[$p + 8 + $i].
17. Reading input
:rdall reads everything typed/piped in. The memory it returns starts with an
8-byte length, then the raw bytes. This program echoes its input back:
:rdall { } ! > ~$buf
:ld64 { [$buf] } ! > ~$len ::: first 8 bytes = how many bytes followed
0 > ~$i
?[$i --= $len]{ ::: for each byte...
:ld8 { [$buf + 8 + $i] } ! > ~$ch
:wrch { [$ch] } ! ::: ...print it
$i + 1 > ~$i
}?
!!
Run it and feed it text:
printf 'round trip' | ./echo
::: round trip
18. Hash cells (global storage)
A hash cell is a named box that any part of your program can read or write — a global. Three kinds, by how long they live:
| Written | Lives | Use it for |
|---|---|---|
#name |
this run only (changeable) | counters, global tables |
##name |
survives across runs (where supported) | saved state |
###name |
baked into the program, never changes | constants |
Declare with a starting value, then read/write like a register:
###MAX 1000 ::: a constant
#count 0 ::: a changeable global
###MAX > ~$m
:wrint { [$m] } ! ::: 1000
5 > ~#count ::: write the cell
#count + 1 > ~#count ::: change it
:wrint { [#count] } !::: 6
Content-addressed cells (a built-in hash map)
#[:fn & key] runs your hash function :fn on key to pick a slot, then
reads/writes there. Collisions are handled for you.
:h { [i64:$k] } $k --- 64 >!? ::: a hash function: key mod 64
111 > ~#[:h & 7] ::: store 111 at slot hash(7)
222 > ~#[:h & 71] ::: 71 mod 64 = 7 -> collides; handled automatically
#[:h & 7] > ~$a :wrint { [$a] } ! ::: 111
#[:h & 71] > ~$b :wrint { [$b] } ! ::: 222
19. Structs (your own types)
A struct bundles several fields into one type. Name a type with ::Name.
::Pt { i32:[x] & i32:[y] } ::: a type "Pt" with two fields x and y
Make one, read fields with ., write a field with > ~$p.field:
::Pt { [100] & [50] } > ~$p ::: create a Pt; x=100, y=50
:wrint { [$p.x] } ! ::: 100
75 > ~$p.x ::: fields can change
:wrint { [$p.x] } ! ::: 75
:wrint { [$p.y] } ! ::: 50
(i32 is a 32-bit integer; f64 is a decimal number. Use i64 when unsure.)
20. Enums and matching them
An enum is a type that is one of several named variants. Declare variants
with .Name:
::Shape { .Circle { i64:[r] } & .Square { i64:[s] } }
You match a value's variant the same way as Section 13, with ??. For plain
numbers, matching looks like this:
3 > ~$n
?? $n {
0 > { :wrint { [100] } ! }
1 > { :wrint { [101] } ! }
_ > { :wrint { [999] } ! } ::: $n is 3 -> 999
}
_ is the catch-all, so every case is covered.
21. Generics
A generic lets one function work for any type. Write the type placeholder as
a name between dollar signs: $T$.
:id $T$ { [$T$ $x] } ::: $T$ is "some type"; $x is of that type
$x >!?
:id { [42] } ! > ~$r
:wrint { [$r] } ! ::: 42
One function body serves every type you call it with.
22. Traits, impls, and methods
^.^ attaches methods (functions that belong to a type) to a struct. Inside,
$self is the value the method was called on.
::Pt { i32:[x] & i32:[y] }
^.^ ::Pt {
:sum { } $self.x + $self.y >!? } ::: a method named :sum
}
^.^ ::Pt { ... }— methods that belong to::Pt.^.^ ::Pt ::Draw { ... }— methods that fulfill a trait::Draw(name the trait after the type).
Methods are resolved at compile time (no runtime lookup), so they're free — they compile to exactly the code you'd write by hand. This is what makes iterators (a struct holding its position, advanced by methods) cost nothing extra in a loop.
23. Closures
A closure is a little anonymous function written >{ ... }. It can capture
registers from around it (by value — they're frozen at creation).
Capture-only (no inputs), call it with just !:
3 > ~$a
4 > ~$b
>{ $a + $b } > ~$add ::: captures $a and $b
$add ! > ~$r ::: call it
:wrint { [$r] } ! ::: 7
With an input, write type:$name > before the body, and pass an argument list
when calling:
10 > ~$base
>{ i32:$x > $x + $base } > ~$f ::: input $x, captures $base
$f ! { [5] } > ~$s
:wrint { [$s] } ! ::: 15
24. Ternary computing (base-3)
Symbolic has rare built-in base-3 types, for logic with a third state and for balanced-ternary math. You can skip this unless you need it.
Three-valued logic (trit): False / True / Unknown
Codes: 0=False, 1=True, 2=Unknown. Built-ins follow Kleene logic (Unknown
spreads sensibly):
:tand { [1] & [2] } ! > ~$a :wrint { [$a] } ! ::: 2 True AND Unknown = Unknown
:tor { [0] & [1] } ! > ~$b :wrint { [$b] } ! ::: 1 False OR True = True
:tnot { [1] } ! > ~$c :wrint { [$c] } ! ::: 0 NOT True = False
:teq compares two trits. A trool adds a fourth state, Undefined, that
always spreads (models hardware "don't care").
Balanced-ternary integers (digits -1, 0, +1)
:bt { [100] } ! > ~$a ::: encode 100 in balanced ternary
:bt { [23] } ! > ~$b
:btadd { [$a] & [$b] } ! > ~$c ::: add them
:btp { [$c] } ! ::: 123 (decode + print)
:bt { [-5] } ! > ~$d
:btp { [$d] } ! ::: -5
Balanced ternary stores negative numbers without a separate sign bit.
25. Building for any platform (targets)
One symc builds for 13 platforms. Pick one with --target (default is
x64-linux). The output is a finished binary — no extra tools needed.
symc --target x64-linux < prog.sym > prog # Linux x86-64
symc --target arm64-linux < prog.sym > prog # Linux ARM
symc --target x64-windows < prog.sym > prog.exe # Windows
symc --target wasm32 < prog.sym > prog.wasm # WebAssembly (runs in a browser/WASI)
All the targets:
--target |
You get |
|---|---|
x64-linux |
Linux x86-64 program |
arm64-linux |
Linux ARM64 program |
riscv64-linux |
Linux RISC-V program |
loongarch64 |
Linux LoongArch program |
x64-macos / arm64-macos |
macOS program (Intel / Apple Silicon) |
ios-arm64 |
iOS program |
android-arm64 |
Android program |
x64-windows |
Windows .exe |
x64-uefi |
UEFI application |
x64-freebsd |
FreeBSD program |
wasm32 |
WebAssembly (WASI) |
spirv |
SPIR-V GPU shader |
To run a binary built for another CPU on your machine, use an emulator, e.g.
qemu-aarch64 ./prog for ARM, or wasmtime run prog.wasm for wasm.
With sigil, you don't pass --target on the command line — you set it once in
your project's Sigil.toml (next section).
26. Using sigil (the project tool)
sigil is like cargo (Rust) or npm (JavaScript). A Symbolic project is
called a rune. Here is every command:
| Command | What it does |
|---|---|
sigil new <name> |
make a new project folder <name>/ |
sigil build |
compile this project to target/out |
sigil run |
build, then run it |
sigil test |
build and run it; passes if it exits cleanly |
sigil bench |
build and run it as a benchmark (also times the compile) |
sigil clean |
delete the target/ folder |
sigil add <name> |
add a dependency to Sigil.toml |
sigil install |
download the dependencies listed in Sigil.toml |
sigil update |
re-download the latest of each dependency |
sigil publish |
scan, then publish this rune to the registry |
sigil scan |
compile to wasm and run it sandboxed in a browser to check it's safe |
sigil help |
print the command list |
A typical session
sigil new game # make it
cd game
sigil run # build + run
# ...edit src/main.sym...
sigil run # run again
sigil test # check it still works
The project file: Sigil.toml
Every project has one. It's plain text:
[package]
name = "game"
version = "0.1.0"
description = "my game"
authors = ["you"]
entry = "src/main.sym" # the file to compile
[dependencies] # other runes you depend on
[build]
target = "x64-linux" # change this to build for another platform!
To build for Windows instead, change one line: target = "x64-windows". To
build for the web: target = "wasm32".
Dependencies
sigil add liba # writes liba into [dependencies]
sigil install # fetches it into runes/
sigil finds the compiler through an environment variable SIGIL_CC (set for
you by the installer). The registry it downloads from is SIGIL_REGISTRY.
27. When something goes wrong
The compiler prints an error with a line number. The two you'll hit most:
error: ... name '...' exceeds maximum length of 6 characters
→ A register/function/type name is too long. Names are max 6 characters. Shorten it.
warning: register 'x' may be used before initialization
→ You read $x before you ever wrote ... > ~$x. Write a value into every
register before you read it.
Other common situations:
| Symptom | Cause | Fix |
|---|---|---|
| Program hangs forever | a loop's counter isn't advancing | add $i + 1 > ~$i inside the loop; press Ctrl-C to stop |
| Prints a weird letter instead of a number | you flowed a number to @screen |
use :wrint { [n] } ! instead |
command not found: sigil |
PATH not set / old terminal | open a new terminal; re-run the installer |
permission denied: ./prog (Unix) |
forgot to mark it runnable | chmod +x prog |
| Wrong/garbage math result | precedence surprise | split the expression into one step per line (Section 8 tip) |
28. The complete symbol cheat-sheet
NAMES (the sigil tells you what kind of thing it is)
$x register (variable) ~ write / ownership
:f function / label ::T a type
#h hash cell (this run) ##h hash cell (saved)
###h hash cell (constant) @s memory segment
$T$ generic type ::: comment
LITERALS
42 decimal 0xFF hex 0b1010 binary 3.14 float
(A) char ((text\n)) string \n \t \\ escapes
ARITHMETIC COMPARISON BITWISE / SHIFT
+ add == equal & and
++ multiply != not equal &+ or
+++ power =+ >= -&+ xor
- subtract =++ > -& not (prefix)
-- divide -= <= -< shift left
--- modulo --= < <+ shift right
--< rotate left
<++ rotate right
FLOW & CONTROL
> flow value into destination >!? return from function
?[c]{} if (run once) ?[c]{}? loop while c
-?{} else ?? pattern match
! call a function !! halt (exit ok)
!!> break out of a loop !!! panic (abort)
GROUPING
[ ] arguments / parameters / grouping { } blocks
& separator (and bitwise-and) . field access
, width reduction (touching)
ADVANCED
#[:fn & key] content-addressed hash slot
^.^ method / trait impl block >{ ... } closure
::T { f & f } struct/enum type $self the method's own value
29. "How do I…?" recipe list
| I want to… | Do this |
|---|---|
| Print text | (( text\n )) > @screen |
| Print a number | :wrint { [n] } ! |
| Print one character | :wrch { [byte] } ! |
| Make a variable | value > ~$name |
| Change a variable | $name + 1 > ~$name |
| Add / multiply / divide | a + b / a ++ b / a -- b |
| Remainder | a --- b |
| Compare | a == b, a =++ b (>), a --= b (<) |
| Do something if true | ?[cond]{ ... } |
| Otherwise | ... -?{ ... } |
| Loop while true | ?[cond]{ ... }? |
| Stop a loop early | !!> |
| End the program | !! |
| Match one value to many cases | ?? $x { 0 > {..} _ > {..} } |
| Write a function | :name { [i64:$a] } $a + 1 >!? |
| Call a function | :name { [arg] } ! > ~$result |
| Get memory | :alloc { [bytes] } ! > ~$p |
| Store / load a number | :st64 { [$p] & [v] } ! / :ld64 { [$p] } ! |
| Read all input | :rdall { } ! > ~$buf (then :ld64 { [$buf] } is the length) |
| Make a global | #name 0 then x > ~#name |
| Make a constant | ###NAME value |
| Define a type | ::Pt { i32:[x] & i32:[y] } |
| Use a field | $p.x (read), v > ~$p.x (write) |
| Start a project | sigil new app && cd app && sigil run |
| Build for Windows | set target = "x64-windows" in Sigil.toml |
| Build for the web | set target = "wasm32" in Sigil.toml |
30. Where to go next
You now know how to do everything in the language. To go deeper:
- TUTORIAL.md — build seven real programs, from a tip calculator to Conway's Game of Life.
- BY_EXAMPLE.md — every feature as a tiny runnable snippet.
- LANGUAGE_GUIDE.md — the precise reference.
- ../examples/ — real programs you can run and edit. The
examples/features/folder has one minimal program per feature. - SELFHOSTING.md — how Symbolic compiles itself.
Open examples/, change a number, run it, see what happens.
That is the whole job. You can do this.