- Targets WASM
- Custom allocators
zig
- Comptime
zig
- Quote / Unquote macros for Hygenic Procedural Macros
lisp
- Sum types
ML
- Pattern Matching
ML
- Coroutines
Lua
- Yield Semantics
- NO GC / Runtime *GC Optional
- Perceus Reference Counting
Koka
- Type Inference
ML
- Errors as Value
- WASI(X), POSIX, and Web API support
- Effects
Koka
?? - Immutable, Total / Pure, and Stack allocated by default
Koka
Core features include
- first-class functions
- a higher-rank impredicative polymorphic type and effect system
- algebraic data types
- effect handlers.
Why WASM ? Because it is fast, secure, and universal. AND I'm lazy. It is as simple bytecode to target that has TONS of other tools for package management, optimization and native compilation and JIT.
Like Zig
being able to define custom allocators is pretty useful. Silicon believes in
Reasonable Defaults
but custom allocators can be used.
Creating a new dictionary data-structure with string keys and int values (types) and a WASM allocator.
std::dict::new str,int, wasm_alloc
Comptime is a great way to do compile time abstractions and macros. Silicon also has more LISP like macros that have the FULL language available.
@comp
is the keyword for comptime. OR just use #
@let x = @comp 1 + 5
@let x = #( 1 + 5 )
This is LISP style macros for modifying the language itself. This is how Silicon does progressive bootstrapping of new features.
This is a HUGE win for compiler devs because we don't have to maintain two code bases or use some other weird bootstrapping workaround like Zig.
This is WHY Silicon is already bootstrapped so early.
Literally uses single quotes ''
myFunction 'x + 1'
@type bool
true
false
@
@match x
true |> "You are authorized"
false |> "Authorization Denied"
@
Match can is like switch on steroids. It can be nestend and handle multiple parameters. Match
is an expression, it produces a value. We can fallthrough and accumulate values as we go with @acc
.
So for fizzbuzz if x
is divisible by 5
we still check the next case because if both are true we accumulate (concat)
the values.
Match also has to be exhaustive, meaning ALL possible options must be accounted for.
with @fn
instead of @let
and lamda function
@fn fizzbuzz x
@match x % 5, x % 3
_, 0 |> "fizz" @acc
// if both 3 and 5 then we'll have accumulated "fizz" ++ "buzz"
0,_ |> "buzz"
// for ALL other values, just send `x`
_, _ |> x
@
@
Regular Fizzbuzz
// if x is evently divisible by 5 then it'll be 0. `_` is wildcard for any other value.
@let fizzbuzz x => @match x % 5, x % 3
_, 0 |> "fizz" @acc
// if both 3 and 5 then we'll have accumulated "fizz" ++ "buzz"
0,_ |> "buzz"
// for ALL other values, just send `x`
_, _ |> x
@
// series from 1 to 20
// pipe to fizzbuzz
// pipe that to print
1..20 -> fizzbuzz -> print
I'm not sure about |>
or ->
for pipe
co::new @fn add a,b => a + b
Implicit returns doesn't mean implicit values.
@fn aNumber n
@if n
@then 10
@else
@
// Error: function 'aNumber' branch '@else' doesn't return a value
If a function is just one expression then the Silicon
standard is to use @let
and lambda.
@let aNumber n =>
@if n
@then 10
@else MAYBE::NONE
This would error out because @else
doesn't return a value.
// bool -> int?
@fn aNumber n
@if n
@then 10
@else MAYBE::NONE
@
This returns None
which would make the return type Maybe(int)
Silicon tries to make concurrency safe and as easy as possible. Calling a function as a coroutine should be as similar as calling it as a normal function.
GO
requires the two things
- The function to be coded to use a Channel and Yield values
- The function to accept a channel parameter
This makes Go
functions actually limited in this sense when they become goroutines
. Still MUCH better than async
and await
.
/// int,int -> ints
@fn x_to_y x,y
@map x..y, $i
i
@
@
With lambda syntax
/// int,int -> ints
@let x_to_y x,y => @map x..y, $i, i
ints
is alias for []int
map
automatically inserts coroutine::yield
into the last branch of the body
@fn x_to_y x,y
@map x..y, $i
coroutine::yield i
@
@
This will still return an array of integers, ints
but one at a time. Coroutines have an accumulator that accumulates intermediate results. So our x_to_y
function will still return
and array of integers, even though it yield
s only individual integers.
So we can do coroutine::dispatchAll
which is much like Javascript's Promise.All
which dispatches / runs a list of coroutines and waits until they're done. _
placeholder for default thread count based on current machine.
@let _15k, _10k =
coroutine::dispatchAll _, [OneToN 15_000, OneToN 10_000]
One may look at a bunch of Silicon source code and notice there are no types, none visible anyways. Type annotations are optional unless they cannot be inferred.
Throwing errors is about as bad as throwing babies. Errors as values are much better. Unlike go
though, Silicon has enums and >>=
bind for errors, @try
that keeps the code clean.
Silicon has a very simple effect system with three types: total
, pure
and impure
. All functions have this as the parent return type. These types dictate how safe a function is to run and if error handling etc need to be used.
There are 4 atomic effect types, you'll never see them outside exception or handlers: ex
,div
,rand
,io
.
ex = exception could be thrown div = divergent, may not terminate rand = not deterministic io = outside state and manipulation
total
meaning they have no exceptions, they do use outside state, they terminate and they are deterministic. Basically void
or unit
for effects as it isn't an effect.
/// int,int -> total(int)
@fn add a,b
a + b
@
pure
has become well known. Pure functions are deterministic, don't use outside state but may throw exceptions or never terminate.
/// bool -> pure(bools)
@fn pure a
@while a
a
@
@
j
// may have a DivideByZero exception
/// int,int -> pure(int)
@fn div num,denom
num / denom
@
/// nz_int is a non-zero integer
/// int, int -> total(int)
@fn div num, denom:nonzero
num / denom
@
/// int,int -> total(maybe(int))
@fn div num, denom
@if denom @not 0
@then num / denom
@else MAYBE::NONE
@
I kept the name simple. A function may never terminate, throw exceptions, read or write outside state and be non-deterministic.
/// str -> impure(str)
readFile fileName
I may have subtypes later but this systew will work well enough. One Subtype that may be useful would be pureIO
or pure IO. Not truly pure but at least have referential transparency
Silicon has first-class support for common runtime environment APIs, making adoption, migration and interopability easy.
@let button = web::document.getElementById("myBtn")
Or NodeJS readFile
node::fs.readFile("Demo.txt", Encoding::UTF8, @fn data => node::console::log(data))