SSymbolic
   ######  ##    ## ##     ## ########   #######  ##       ####  ######
  ##    ##  ##  ##  ###   ### ##     ## ##     ## ##        ##  ##    ##
  ##         ####   #### #### ##     ## ##     ## ##        ##  ##
   ######     ##    ## ### ## ########  ##     ## ##        ##  ##
        ##    ##    ##     ## ##     ## ##     ## ##        ##  ##
  ##    ##    ##    ##     ## ##     ## ##     ## ##        ##  ##    ##
   ######     ##    ##     ## ########   #######  ######## ####  ######

        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).