Learning StingType
This tutorial goes from a first program to libraries, generics, function
values, threads and calling C. Every program here is complete: save it
as a .st file, then build and run it:
gstc hello.st -o hello
./hello
(On Windows: gstc hello.st -o hello, then hello.exe.) SPEC.md is the
full reference; this is the guided path through it.
1. Hello, and the shape of a program
print_str("hello, world")
A program is its top-level lines, run in order. There is no main
needed (you may write fn main(); it runs after the top level).
Blocks are closed with words, never braces: if ... then ... end,
while ... do ... end, fn ... endfn, struct ... endstruct.
Comments start with //.
2. Variables and numbers
var count: i32 = 10
var price = 2.5 // the type comes from the value: f64
const LIMIT = 100 // a constant, worked out while compiling
count += 5
print_i32(count)
print_f64(price * 2.0)
print_bool(count < LIMIT)
var big: i64 = count as i64 * 1000000000
print_i64(big)
The number types are i8 i16 i32 i64, u8 u16 u32 u64, f32 f64,
plus usize/isize (pointer-sized). Nothing converts by itself:
count as i64 says so. Mixing types in one operator is an error that
tells you which cast to write.
Arithmetic is checked. If a result doesn't fit its type, the program stops and says where:
var x: u8 = 200
var y: u8 = x + 100
runtime error: integer overflow: the result of '+' doesn't fit in u8 (at line 2, column 15)
When you want wrap-around (hashes, random numbers), say so with
wrap_add, wrap_sub and wrap_mul.
3. Deciding and repeating
var n: i32 = 7
if n % 2 == 0 then
print_str("even")
elseif n > 5 then
print_str("odd and big")
else
print_str("odd")
end
var i: i32 = 0
while i < 3 do
print_i32(i)
i += 1
end
for k = 1, 5 do // 1 through 5
if k == 3 then continue end
print_i32(k)
end
repeat
n -= 3
until n < 0
print_i32(n)
choose picks one case of many:
var ch: u8 = 'q'
choose ch
is 'a', 'e', 'i', 'o', 'u' then
print_str("vowel")
is 'b' to 'd', 'f' to 'h', 'j' to 'n', 'p' to 't', 'v' to 'z' then
print_str("consonant")
is '0' to '9' then
print_str("digit")
else
print_str("something else")
end
The first case that matches is the only one that runs. The compiler
checks the cases for you: a value listed twice, or ranges that overlap
(write 'a' to 'z' in the second case above and it says that 'a' is
already taken), is an error, and choosing on an enum must cover every
variant or have an else.
4. Functions
fn area(w: i32, h: i32): i32
return w * h
endfn
fn greet(name: str)
print_str(name)
endfn
print_i32(area(3, 4))
greet("Ada")
Arguments are passed by value: a function gets its own copy of every argument, structs and arrays included. To let a function change something of yours, pass a pointer (section 6).
5. Text, arrays and slices
var word: str = "stingtype"
print_i64(str_len(word) as i64)
print_i32(word[0] as i32) // 115: the byte 's'
var scores: [5]i32 = [90, 72, 85, 60, 99]
var best: i32 = 0
for s in scores do
if s > best then best = s end
end
print_i32(best)
var middle: []i32 = scores[1 until 4] // a slice: elements 1, 2, 3
print_i64(middle.len as i64)
print_i32(middle[0])
A str carries its length; it isn't zero-terminated. An array [N]T
holds exactly N values in place; a slice []T is a view of some of
them. Every index is checked: scores[7] stops the program with the
index and the length.
6. Structs, enums and pointers
struct Point
x: i32
y: i32
endstruct
enum Mood
Calm
Busy
endenum
fn move_right(p: *Point, by: i32)
p.x += by // through the pointer
endfn
var a = Point(x: 1, y: 2)
move_right(&a, 10)
print_i32(a.x)
var m: Mood = Mood.Busy
choose m
is Mood.Calm then
print_str("calm")
is Mood.Busy then
print_str("busy")
end
&a is a pointer to a; p.x reads a field through a pointer as
well as from a value.
7. Libraries
include "standard.sth" as std
include "collections.sth"
var names: List<str>
list_add(&names, "Grace")
list_add(&names, "Ada")
list_add(&names, "Linus")
std.sort_by(list_items(&names), std.str_less)
for name in list_items(&names) do
call std.writeln(name)
end
call list_free(&names)
include "file.sth" as name puts a library's functions under a name
(std.sort); without as, they're plain names. The libraries come
with gstc: standard, collections (List, Map, IntMap), text, random,
hash, encoding, json, regex, mathlib, deflate, files and more -- see
README.md, "The libraries".
Memory is yours: what you create with list_add, alloc or a
function returning new str, you free (list_free, dealloc,
str_free). Freeing twice stops the program with a message.
8. Mistakes as values
Some failures aren't bugs -- bad input, a missing file. Those come back
as false, and you decide:
include "standard.sth" as std
var port: u16 = 0
if !std.parse_int("8080", &port) then
print_str("not a port number")
end
print_i32(port as i32)
if !std.parse_int("99999", &port) then
print_str("99999 doesn't fit in a u16")
end
9. Generics
include "standard.sth" as std
fn biggest<T>(items: []T): T
var best: T = items[0]
for x in items do
if x > best then best = x end
end
return best
endfn
fn zero<T>(): T
return 0 as T
endfn
var ints: [3]i32 = [4, 9, 2]
var floats: [2]f64 = [0.5, 2.5]
print_i32(biggest(ints[0 until 3])) // T = i32, from the argument
print_f64(biggest(floats[0 until 2]))
var z: i64 = zero() // T = i64, from where it goes
print_i64(z + zero<i64>()) // or written out
A generic function is copied out for each set of types it's used with, so it runs as fast as one written for that type.
10. Functions as values
include "standard.sth" as std
struct Person
name: str
age: i32
endstruct
fn by_age(a: Person, b: Person): bool
return a.age < b.age
endfn
fn twice(f: fn(i32): i32, x: i32): i32
return f(f(x))
endfn
fn add_three(x: i32): i32
return x + 3
endfn
var people: [3]Person
people[0] = Person(name: "Linus", age: 55)
people[1] = Person(name: "Ada", age: 36)
people[2] = Person(name: "Grace", age: 85)
std.sort_by(people[0 until 3], by_age)
for p in people do
call std.writeln(p.name)
end
print_i32(twice(add_three, 10))
A function value is a real machine address, so the same value can be handed to a C library as a callback (README.md, "Functions as values").
11. Threads
fn work(arg: *u8): i64
var total: i64 = 0
for i = 1, 1000000 do
total += i as i64
end
return total
endfn
var a = thread_start(work, 0 as *u8)
var b = thread_start(work, 0 as *u8)
print_i64(thread_wait(a) + thread_wait(b))
Each thread has its own stack and its own chain of calls for error
reports; atomic_add and friends share counters safely (SPEC.md,
section 9).
12. Calling C
extern "libc.so.6" fn abs(x: i32): i32
print_i32(abs(-42))
On Windows the library is a DLL (extern "msvcrt.dll" ...). A C
function takes numbers, bools, pointers and callbacks; pass a struct as
a pointer (&s) and a str's bytes with str_ptr(s).
13. One program, many machines
The same source builds for every target gstc knows:
gstc game.st -o game # this machine
gstc game.st -o game --target=windows64 # game.exe
gstc game.st -o game --target=linux-arm64 # Raspberry Pi
gstc game.st -o game --target=android # game.apk
target_windows(), target_linux(), target_android() and friends are
constants, so if target_windows() then ... end builds only the branch
for the target at hand.
Where next
- README.md: the tour of everything gstc can do (graphics, shaders, assets, freestanding programs, Android apps).
- SPEC.md: the language, precisely.
- lib/*.sth: the libraries, each with its own guide at the top.
gstc new myproject: a project folder with a test, built withgstc buildand tested withgstc test.