###### ## ## ## ## ######## ####### ## #### ######
## ## ## ## ### ### ## ## ## ## ## ## ## ##
## #### #### #### ## ## ## ## ## ## ##
###### ## ## ### ## ######## ## ## ## ## ##
## ## ## ## ## ## ## ## ## ## ##
## ## ## ## ## ## ## ## ## ## ## ## ##
###### ## ## ## ######## ####### ######## #### ######
S y m b o l i c b y E x a m p l e
A tour of Symbolic through small, complete, runnable programs — in the
spirit of Rust by Example. Each
section is a program you can paste into a .sym file and run; the ::: comments
show the output it produces.
Compile and run any example with (after bash install.sh && source ~/.symbolic/env):
symc < prog.sym > prog && chmod +x prog && ./prog
Or using the Rust seed directly:
cargo build --release -p symc0
./target/release/symc0 --no-run --target x64-linux -o prog prog.sym && ./prog
New to the language? Read the Tutorial (build-first) or the Language Guide (reference). This page is the quick "show me the code" companion. Every snippet below was compiled and run.
Contents
Hello World · Comments · Printing · Primitives · Registers & mutability · Arithmetic · Comparison · Bitwise & shifts · If / else · Loops & break · Pattern match · Functions · Recursion · Many arguments · Heap memory · Standard input · Hash cells · Content-addressed hashes · Structs · Generics · Closures · Traits & impls · Ternary logic · Balanced-ternary integers
Hello World
((hello, world\n)) > @screen
!!
hello, world
((...)) is a string literal, > flows a value to a destination, @screen is
standard output, and !! halts. Top-level statements are the program — there
is no main.
Comments
::: this whole line is a comment
((hi\n)) > @screen ::: trailing comments work too
!!
Anything from ::: to end of line is ignored by the compiler.
Printing
((text to the screen\n)) > @screen ::: raw bytes
:wrint { [123] } ! ::: an integer, in decimal + newline
:wrch { [(!)] } ! ::: a single byte (here the char '!')
:wrch { [10] } ! ::: 10 is '\n'
text to the screen
123
!
:wrint prints a number as decimal; :wrch writes one raw byte; flowing a
string to @screen writes its bytes verbatim.
Primitives
:wrint { [42] } ! ::: 42 decimal
:wrint { [0xFF] } ! ::: 255 hexadecimal
:wrint { [0b1010] } ! ::: 10 binary
:wrch { [(A)] } ! ::: A a character literal is its byte value (65)
:wrch { [10] } !
42
255
10
A
The default numeric type is a 64-bit integer (i64). (c) is a char literal;
((...)) is a string.
Registers & mutability
42 > ~$x ::: create register $x = 42
$x + 1 > ~$x ::: read $x, add 1, write back
:wrint { [$x] } ! ::: 43
43
$x reads a register; ~$x binds/writes it. Registers are immutable
until you write to them again with ~ — data flow is always explicit and reads
left-to-right.
Arithmetic
:wrint { [3 + 4] } ! ::: 7 add
:wrint { [3 ++ 4] } ! ::: 12 multiply (+ "extended")
:wrint { [3 +++ 4] } ! ::: 81 power (+ "tripled")
:wrint { [10 - 3] } ! ::: 7 subtract
:wrint { [10 -- 3] } ! ::: 3 divide
:wrint { [10 --- 3] } ! ::: 1 modulo
7
12
81
7
3
1
One symbol family, intensified by repetition: + -> ++ -> +++, and its
negated counterpart - -> -- -> ---.
Comparison
5 == 5 > ~$a :wrint { [$a] } ! ::: 1 equal
5 != 3 > ~$b :wrint { [$b] } ! ::: 1 not equal
3 --= 5 > ~$c :wrint { [$c] } ! ::: 1 less-than
5 =++ 3 > ~$d :wrint { [$d] } ! ::: 1 greater-than
5 =++ 9 > ~$e :wrint { [$e] } ! ::: 0
!!
1
1
1
1
0
Comparisons evaluate to 1 (true) or 0 (false), so you can store or print
them. Read them by the grammar: = is the base, + extends it toward
greater, - negates it toward less (--= <, -= <=, =+ >=,
=++ >).
Bitwise & shifts
12 & 10 > ~$a :wrint { [$a] } ! ::: 8 AND
12 &+ 10 > ~$b :wrint { [$b] } ! ::: 14 OR (& extended)
12 -&+ 10 > ~$c :wrint { [$c] } ! ::: 6 XOR
1 -< 4 > ~$d :wrint { [$d] } ! ::: 16 shift left
256 <+ 2 > ~$e :wrint { [$e] } ! ::: 64 shift right
!!
8
14
6
16
64
Prefix -& is bitwise NOT; --< and <++ are rotate-left and rotate-right.
If / else
5 > ~$x
?[$x =++ 3]{ ((big\n)) > @screen } -?{ ((small\n)) > @screen }
!!
big
?[cond]{ ... } runs its body once if the condition is non-zero. -?{ ... } is
the else branch — the - negates the ?. Chain -?{ ?[...]{...} -?{...} }
for else-if ladders.
Loops & break
0 > ~$i
?[1 == 1]{ ::: loop forever...
?[$i =+ 5]{ !!> } ::: ...until i >= 5, then break
:wrint { [$i] } !
$i + 1 > ~$i
}?
:wrint { [999] } !
!!
0
1
2
3
4
999
A trailing ? on the closing brace turns a conditional into a loop (it
re-checks the condition each pass). !!> breaks out of the innermost loop.
Pattern match
2 > ~$n
?? $n {
0 > { ((zero\n)) > @screen }
1 > { ((one\n)) > @screen }
_ > { ((many\n)) > @screen }
}
!!
many
?? value { pattern > { body } ... } matches a value against patterns; _ is
the catch-all, so a match is always exhaustive.
Functions
:add { [i64:$a] & [i64:$b] }
$a + $b >!? ::: >!? returns a value
:add { [20] & [22] } ! > ~$s ::: call with `!`, capture with `>`
:wrint { [$s] } ! ::: 42
!!
42
Declare with :name { [type:$param] & ... } body >!?; call with
:name { [arg] & ... } !. The type annotation (i64:) marks a declaration.
Recursion
:fac { [i64:$n] }
?[$n --= 2]{ 1 >!? } ::: base case: n < 2 -> 1
$n - 1 > ~$m
:fac { [$m] } ! > ~$r
$n ++ $r >!? ::: n * fac(n-1)
:fac { [5] } ! > ~$f
:wrint { [$f] } ! ::: 120
!!
120
A function may call itself; >!? can return early from inside a conditional.
Many arguments
: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
!!
36
There is no fixed limit on arity: the first six arguments travel in registers, the rest on the stack, per the System V ABI.
Heap memory
:alloc { [64] } ! > ~$p ::: allocate 64 bytes; $p is the address
12345 > ~$v
:st64 { [$p] & [$v] } ! ::: store 8 bytes
:ld64 { [$p] } ! > ~$r ::: load them back
:wrint { [$r] } ! ::: 12345
65 > ~$c
:st8 { [$p + 8] & [$c] } ! ::: store one byte at $p+8
:ld8 { [$p + 8] } ! > ~$b
:wrch { [$b] } ! ::: A
:wrch { [10] } !
!!
12345
A
Addresses are ordinary integers, so [$p + 8 + $i] indexes naturally. :ld8/
:st8 move single bytes; :ld64/:st64 move 8-byte words.
Standard input
:rdall { } ! > ~$buf ::: read ALL of stdin into memory
:ld64 { [$buf] } ! > ~$len ::: layout is [length: 8 bytes][raw bytes...]
0 > ~$i
?[$i --= $len]{
:ld8 { [$buf + 8 + $i] } ! > ~$ch
:wrch { [$ch] } !
$i + 1 > ~$i
}?
!!
$ printf 'hello' | ./prog
hello
:rdall returns a pointer to a length-prefixed buffer — the same input path the
self-hosting compiler uses to read source code.
Hash cells
###MAX 1000 ::: ROM constant (baked into the binary)
#count 0 ::: ephemeral mutable cell
###MAX > ~$m
:wrint { [$m] } ! ::: 1000
5 > ~#count ::: write a cell
#count + 1 > ~#count
:wrint { [#count] } ! ::: 6
!!
1000
6
Hash cells are program-global storage. One # is ephemeral (mutable), ## is
persistent, ### is a ROM constant.
Content-addressed hashes
:h { [i64:k] } $k --- 64 >!? ::: a hash function: key mod 64
111 > ~#[:h & 7] ::: store 111 in the slot hash(7)
222 > ~#[:h & 71] ::: 71 mod 64 == 7 -> collides; resolved by probing
#[:h & 7] > ~$a :wrint { [$a] } ! ::: 111
#[:h & 71] > ~$b :wrint { [$b] } ! ::: 222
!!
111
222
#[:fn & key] is a built-in hash map: it computes a slot with :fn(key) and
reads/writes there, probing on collisions so distinct keys never clobber.
Structs
::Pt { i32:[x] & i32:[y] } ::: a type with two fields
::Pt { [100] & [50] } > ~$p ::: instantiate
:wrint { [$p.x] } ! ::: 100 read a field with `.`
75 > ~$p.x ::: fields are mutable
:wrint { [$p.x] } ! ::: 75
:wrint { [$p.y] } ! ::: 50
!!
100
75
50
Types are named ::Name; fields are type:[name], and $p.field accesses them.
Generics
:id $T$ { [$T$ $x] } ::: $T$ is a generic type parameter
$x >!?
:id { [42] } ! > ~$r
:wrint { [$r] } ! ::: 42
!!
42
$T$ (a name between dollar signs) introduces a generic type parameter, so one
function body serves every type.
Closures
10 > ~$base
>{ i32:$x > $x + $base } > ~$f ::: anonymous fn; captures $base by value
$f ! { [5] } > ~$s ::: call it with an argument
:wrint { [$s] } ! ::: 15
!!
15
>{ ... } is a closure. A capture-only closure (>{ $a + $b }) is called with
just $f !; one with a parameter uses type:$p > before the body.
Traits & impls
^.^ attaches methods to a type — inherent methods, or a trait implementation
when a trait type is named after the type. Each method is a label with a braced
body, scoped together inside the block:
::Pt { i32:[x] & i32:[y] }
^.^ ::Pt {
:sum { } $self.x + $self.y >!? }
}
((impl compiled\n)) > @screen
!!
impl compiled
This declares a :sum method on ::Pt (and compiles). A trait impl names the
trait after the type: ^.^ ::Pt ::Draw { ... }. Method invocation and bounds
are covered in the
Language Guide.
Ternary logic
::: Kleene 3-valued logic. codes: 0=False 1=True 2=Unknown
: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
!!
2
1
0
Symbolic has native ternary types. trit is three-valued Kleene logic; trool
adds a fourth "undefined" state that always propagates.
Balanced-ternary integers
:bt { [100] } ! > ~$a ::: encode 100 in balanced ternary
:bt { [23] } ! > ~$b
:btadd { [$a] & [$b] } ! > ~$c ::: add in balanced ternary
:btp { [$c] } ! ::: 123 (decode + print)
:bt { [-5] } ! > ~$d
:btp { [$d] } ! ::: -5
!!
123
-5
Balanced ternary uses digits -1, 0, +1, representing negative numbers without
a separate sign bit — a genuinely different way to count, built into the
language's runtime.
That's the language, by example. For projects (a cipher, a
calculator, Conway's Game of Life) see the Tutorial; for the
full reference, the Language Guide.
In the style of Rust by Example.
Every snippet here was compiled and run with symc (symc0 seed, x64-linux).