Learning StingType

This tutorial goes from a first program to libraries, generics, function values, threads, calling C, methods, interfaces and closures. 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

A function that can fail in more than one way can return a Result: the value, or an error saying what went wrong. try hands an error straight back to the caller, and choose looks at both cases:

fn digit(ch: u8): Result<i32, str>
  if ch < 48 || ch > 57 then return err("not a digit") end
  return ok((ch - 48) as i32)
endfn

fn parse_small(s: str): Result<i32, str>
  if str_len(s) == 0 then return err("empty") end
  var n = 0
  for i = 0, str_len(s) as i32 - 1 do
    n = n * 10 + try digit(s[i])      // a bad digit: return its error now
  end
  return ok(n)
endfn

for text in ["42", "", "4x2"] do
  choose parse_small(text)
    is ok(n) then print_i32(n)
    is err(e) then print_str(e)
  end
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.

14. Methods and interfaces

A function named Type.name, whose first parameter is self, is a method: c.area() calls Circle.area(&c). An interface names methods, and a pointer to anything that has them fits it:

interface Shape
  fn area(self): f64
  fn name(self): str
endinterface

struct Circle
  r: f64
endstruct
fn Circle.area(self): f64
  return 3.14159 * self.r * self.r
endfn
fn Circle.name(self): str
  return "circle"
endfn

struct Rect
  w: f64
  h: f64
endstruct
fn Rect.area(self): f64
  return self.w * self.h
endfn
fn Rect.name(self): str
  return "rectangle"
endfn
fn Rect.double(self)
  self.w = self.w * 2.0                // self points at the Rect: this changes it
endfn

var c = Circle(r: 1.0)
var r = Rect(w: 2.0, h: 3.0)
r.double()
var shapes: [2]Shape
shapes[0] = &c
shapes[1] = &r
for s in shapes do
  print_str(s.name())
  print_f64(s.area())
end

15. Closures

A fn written where a value goes is a closure: a function made on the spot, holding copies of the variables it uses:

include "standard.sth" as std

fn make_counter(): closure(): i32
  var count = 0
  return fn(): i32
    count += 1                         // its own copy: kept between calls
    return count
  endfn
endfn

var next = make_counter()
next()
next()
print_i32(next())                      // 3
free_closure(next)                     // the copies' memory, given back

var scores: [4]i32 = [70, 95, 82, 61]
std.sort_by(scores[0 until 4], fn(a: i32, b: i32): bool
  return a > b                         // highest first
endfn)
print_i32(scores[0])

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.