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 with gstc build and tested with gstc test.