# Writing stone: a guide for coding agents
You are going to write programs in stone, a small language that looks like Python but behaves
like Rust: static types, values that are copied instead of shared, and errors reported before the
program runs. Your Python habits will get the syntax mostly right and the semantics sometimes
wrong. This guide covers what you need to write correct stone the first time, and lists what
Python has that stone does not, so you do not reach for it.
Full documentation: . The pages to keep open are
[syntax](https://tarolling.github.io/stone/reference/syntax.html),
[types](https://tarolling.github.io/stone/reference/types.html), and
[builtins](https://tarolling.github.io/stone/reference/builtins.html).
## Your loop
Install with `curl -fsSL https://raw.githubusercontent.com/tarolling/stone/main/install.sh | sh`
(it puts `stone` in `~/.local/bin`). Source files end in `.stn`.
| command | does |
| --- | --- |
| `stone check app.stn` | type checks without running; run it after every edit |
| `stone run app.stn [args...]` | interprets the program; heavy loops can run slower than in Python |
| `stone build app.stn` | compiles a standalone executable to `build/app` next to the file; use it for large inputs and timing |
| `stone fmt app.stn` | rewrites the file in the standard layout (only whitespace changes); run it before finishing |
| `stone` | starts a REPL |
Errors print as `app.stn:3:7: error: message`, with the source line and a caret under the
problem, and exit with status 1. Warnings use the same format with `warning:`. The messages are
written to be acted on: they usually say what to write instead, so read them before guessing.
Runtime errors (an index out of range, `unwrap` of `none`, division by zero) print the same way
under both `run` and `build`. Fix warnings too: each one points at a likely bug, such as a change
that is lost or `[0] * n` meaning something other than in Python. `stone check` also warns about
unused variables, parameters, functions, types, and imports; remove them, or start the name with
`_` to keep one on purpose (`for _ in range(3):`).
## The syntax, in one program
```stone
// comments start with //, and strings use double quotes only
use math
// every parameter has a type; '-> T' if it returns a value
fn classify(n: int) -> str:
if n < 0:
ret "negative"
elif n == 0:
ret "zero"
else:
ret "positive"
// no '->' means no value: calls to it are statements
fn report(label: str, values: list[int]):
total = 0
for v in values:
if v == 0:
cont
total += v
print("{label}: {total} is {classify(total)}")
nums = [3, -1, 0, 4]
report("sum", nums)
for i in range(nums.len()):
print(i, nums[i], math.abs(nums[i]))
done = true
while done:
done = false
print("last:", nums[-1], "reversed:", nums[::-1])
```
The differences from Python here: `fn` instead of `def`, a type on every parameter, `-> T` for a
return value, `ret` instead of `return`, `cont` instead of `continue`, `true`/`false`, `//`
comments, double-quoted strings only, every string writing the values of the `{expression}`s in
it with no `f` prefix (`{{` and `}}` for braces), `xs.len()` instead of `len(xs)`, and `math.abs`
instead of `abs`. Variables are never annotated; their types are inferred. `for _ in range(n):` works for
a loop variable you do not need, and `main` is an ordinary function name.
## Types
| type | example | notes |
| --- | --- | --- |
| `int` | `42` | 64-bit; overflow wraps around |
| `float` | `1.5`, `2e-3` | never mixed with `int`: `float(n)`, `int(x)?` (see conversions) |
| `bool` | `true` | the only type a condition accepts |
| `str` | `"hi\n"` | immutable UTF-8 bytes; `s[i]`, `s[a:b]`, `+` to join |
| `bytes` | `b"\x00\xff"` | immutable raw bytes; `b[i]` is an int, slices, `+`, `"s".bytes()`, `b.text()` (a result), `bytes([1, 2])` (a result) |
| `list[T]` | `[1, 2]`, `[]` | all elements one type |
| `map[K, V]` | `{"a": 1}`, `{}` | keys `int`, `str`, or `bool`; insertion order |
| `tuple[A, B]` | `(1, "a")` | two or more values, always in parentheses |
| `option[T]` | `some(1)`, `none` | stone's replacement for `None` |
| `result[T, E]` | `ok(1)`, `err("bad")` | stone's replacement for exceptions |
| struct | `Point(x=1.0, y=2.0)` | named, typed fields |
| enum | `Shape.Circle(1.0)` | variants that hold values, as in Rust |
| `fn(A) -> B` | `double` | a function named without parentheses; `fn(A)` returns no value |
### Lists, maps, and tuples
```stone
fn divide(a: int, b: int) -> tuple[int, int]:
ret (a / b, a % b)
xs = []
for i in range(5):
xs.append(i * i)
xs.insert(0, 100)
xs.extend([7, 8])
xs.sort()
print(xs, xs[1:3], xs.len())
ages = {"ann": 31, "bob": 27}
ages["cy"] = 45
ages["ann"] += 1
print(ages, "dee" in ages, 3 in xs, "el" in "hello", ages.get("bob").unwrap_or(0))
for name in ages:
print(name, ages[name])
for name, age in ages.items():
print(name, age)
gone = ages.pop("bob")
print(gone, ages.keys(), ages.values())
q, r = divide(17, 5)
pairs = [("a", 1), ("b", 2)]
for name, n in pairs:
print(name, n, q, r)
```
`for k in m` goes through a map's keys in insertion order; `m.keys()`, `m.values()`, and
`m.items()` return new lists. `m[k]` stops the program if `k` is missing; test `k in m` or use
`m.get(k)` when it might be. `x in c` and `x not in c` test a map's keys, a list's elements (by
`==`), or a string's text, as `c.contains(x)` does, and never chain: write `(x in xs) == b`. `m.pop(k)` returns an option and, like a list's `pop`, may be used
inside any expression, as in `m.pop(k).unwrap_or(0)`. Entries change in place like list elements, through any
place: `m[k] += 1`, `m[k].append(x)`, `p.counts[k] = 0`. Only `m[k] = v` adds a missing key; `+=`
and changes inside an entry need the key to exist. A tuple is indexed only by a literal (`t[0]`,
`t[-1]`) and never changes.
### Options
```stone
fn find_index(xs: list[str], target: str) -> option[int]:
for i in range(xs.len()):
if xs[i] == target:
ret some(i)
ret none
names = ["ann", "bob"]
match find_index(names, "bob"):
case some(i):
print("found at", i)
case none:
print("missing")
print(find_index(names, "zed").is_none(), find_index(names, "ann").unwrap())
```
`find`, `pop()`, `get`, and `os.env` return options too: `"abc".find("c")` is `some(2)`, not an
index or -1, so write `s.find(t).unwrap_or(-1)` where Python code compares with -1. `pop()` gives
an option as well, so `xs.pop().unwrap()` takes the last element and `while xs.pop().is_some():`
empties a list; since it changes the list, nothing else in the same statement may use it (see
below).
### Results and ?
```stone
fn parse_pair(line: str) -> result[tuple[int, int], str]:
parts = line.split(",")
if parts.len() != 2:
ret err("expected two numbers in '" + line + "'")
ret ok((int(parts[0])?, int(parts[1])?))
for line in ["3,4", "3", "3,x"]:
match parse_pair(line):
case ok(pair):
print("pair", pair)
case err(message):
print("error:", message)
print(parse_pair("1,2").unwrap(), parse_pair("1").is_err())
```
A function that can fail returns `result[T, E]`: `ok(x)` or `err(e)`. `?` after a result gives
the `ok` value, or returns the `err` from the enclosing function at once; it is only allowed in a
function that returns a `result` with the same error type (`?` on an option needs a function
returning an option, and `r.ok()?` turns a result into one). Results have `is_ok`, `is_err`,
`unwrap`, `unwrap_or`, `unwrap_err`, `ok()`, and `err()`.
### Conversions
```stone
struct Celsius:
degrees: float
struct Fahrenheit:
degrees: float
impl from[Celsius] for Fahrenheit:
fn from(value: Celsius) -> Self:
ret Fahrenheit(degrees=value.degrees * 9.0 / 5.0 + 32.0)
impl try_from[str] for Celsius:
fn try_from(value: str) -> result[Self, str]:
ret ok(Celsius(degrees=float(value)?))
fn parse_all[T: try_from[str]](texts: list[str]) -> result[list[T], str]:
out = []
for text in texts:
out.append(T(text)?)
ret ok(out)
print(Fahrenheit(Celsius(degrees=100.0))) // Fahrenheit(degrees=212.0)
print(Celsius("21.5"), Celsius("warm").is_err()) // ok(Celsius(degrees=21.5)) true
print(float(2), int(2.9), int("x"), str([1])) // 2.0 ok(2) err("invalid literal ...") [1]
print(parse_all(["1", "2"]).unwrap()[0] + 1) // 2
```
A type called with one value, `T(x)`, converts it through the prelude's traits: `from[S]`
(`fn from(value: S) -> Self`) for a conversion that cannot fail, which gives a `T`, or
`try_from[S]` (`fn try_from(value: S) -> result[Self, str]`) for one that can, which gives a
`result[T, str]`. A struct is still made with named values; one positional value converts.
Builtin: `float(n)` of an int and `str(x)` of anything give the value; `int(x)` of a float or a
`str`, `float(s)` of a `str`, and `bytes(xs)` of a `list[int]` give results, so write
`int(s)?` in a function returning `result[_, str]`, or `int(s).unwrap()` where the text is known
good. A type implements `from` or `try_from` once for each source type, never both for one;
`impl from[Celsius] for float:` may go beside `Celsius`. A bound `[T: from[int]]` lets
generic code write `T(n)`.
### Structs and methods
```stone
struct Account:
owner: str
balance: int
history: list[int]
impl Account:
// an associated function: called as Account.open(...)
fn open(owner: str) -> Account:
ret Account(owner=owner, balance=0, history=[])
// a method that changes self: stone sees that and changes the receiver in place
fn deposit(self, amount: int):
self.balance += amount
self.history.append(amount)
fn summary(self) -> str:
ret self.owner + ": " + str(self.balance)
a = Account.open("ann")
a.deposit(50)
a.deposit(25)
print(a.summary(), a.history)
print(a)
```
A struct is made by naming every field once, in any order. There is no inheritance and no
constructor: write an associated function. Methods go in an `impl` block in the struct's file.
`self` is the only untyped parameter.
### Enums and match
```stone
enum Shape:
Circle(float)
Rect(float, float)
Empty
fn area(s: Shape) -> float:
match s:
case Shape.Circle(r):
ret 3.14159 * r * r
case Shape.Rect(w, h):
ret w * h
case Shape.Empty:
ret 0.0
shapes = [Shape.Circle(1.0), Shape.Rect(2.0, 3.0), Shape.Empty]
for s in shapes:
print(s, area(s))
```
A `match` must cover every variant (or end with `case _:`); a missing one is an error naming it.
Variants are always written with their enum, `Shape.Empty`, except `some`/`none`, `ok`/`err`,
and the `ordering` variants `less`/`equal`/`greater`, which are reserved names. An enum can hold itself, which is how you build
trees and linked lists.
### Generics and traits
```stone
trait Named:
fn name(self) -> str
fn greet(self) -> str:
ret "hello, " + self.name()
struct Dog:
nick: str
impl Named for Dog:
fn name(self) -> str:
ret self.nick
struct Version:
major: int
minor: int
impl ord for Version:
fn cmp(self, other: Self) -> ordering:
if self.major != other.major:
ret self.major.cmp(other.major)
ret self.minor.cmp(other.minor)
fn first[T](xs: list[T]) -> T:
ret xs[0]
fn greet_all[T: Named](xs: list[T]):
for x in xs:
print(x.greet())
greet_all([Dog(nick="rex"), Dog(nick="fido")])
vs = [Version(major=2, minor=0), Version(major=1, minor=5)]
vs.sort()
print(first(vs), first(["a", "b"]), vs[0] < vs[1])
```
Inside a generic function a `T` can only be moved, stored, returned, and printed, plus the methods
of its bounds. `[T: eq]` allows `==`, and `[T: ord]` allows `<` and `sort`. Implementing `ord`
(with `cmp`) gives a struct its order for `sort` and `<`; to sort by something else, use
`sort_by_key`. Implementing `display` (with `fn display(self) -> str`) changes how `print`,
`str`, and `"{x}"` write a struct, everywhere it appears.
### Functions as values
```stone
struct City:
name: str
people: int
fn big(c: City) -> bool:
ret c.people > 500
fn apply(f: fn(int) -> int, x: int) -> int:
ret f(x)
cities = [City(name="Oslo", people=700), City(name="Bern", people=130)]
print(cities.filter(big).map(|c| c.name), cities.any(big), cities.all(big))
print(cities.max_by_key(|c| c.people).unwrap().name)
cities.sort_by_key(|c| c.people)
print(cities.map(|c| c.name), apply(|x| x * 2, 4))
```
A top-level function's name without parentheses is a value of type `fn(params) -> ret`, which
can be passed, returned, stored in lists and structs, and called: `f(x)`, `fs[0](x)`, or
`p.run(x)` for a struct field `run` that holds one. A lambda writes one in place: `|x| x * 2`,
`|p| p.name`, `|a: int, b| a + b`, or `|| 1`, its body one expression on the same line, its types
inferred from where it is used (give a parameter a type when nothing else says it). A lambda may
use the variables around it (a closure): `xs.filter(|x| x > limit)` copies `limit` where the lambda
is written, so later changes to `limit` do not reach it, and the lambda cannot change its copy. A
closure cannot capture a socket, a listener, or a value of a type parameter. Builtins (`print`, `str`), methods, and variants
are not values; wrap them in a function. Lists' helpers take functions: `map`, `filter`, `any`,
`all`, `sort_by_key` (in place, stable, the key computed once per element), and `min_by_key` and
`max_by_key` (options: the first least and the last greatest). Functions cannot be compared, and
print as their type. Traits are still how a generic type gets methods.
## Values are copied: the one idea to learn
Lists, maps, and structs are values. Assigning one or passing it to a function behaves as a copy
(it is copy-on-write, so it is cheap). Consequences:
- **A function cannot change its caller's list.** It returns the changed value, and the caller
assigns it: `xs = add_item(xs, 3)`. stone turns this into an in-place update, so it costs
nothing.
- **A function cannot change a global.** Pass it in and return it.
- **A change is a statement of its own**, made to a variable or an element or field of one:
`xs.append(1)`, `xs[i] = v`, `p.x = 1.0`, `grid[r][c] += 1`, `ps[i].deposit(5)`. So
`ys = xs.append(1)` and `f().append(1)` are errors. The exception is `pop` and other methods
that change their receiver and return a value: those may be inside any expression,
`print(xs.pop().unwrap())`, as long as nothing else in that statement uses what they change,
so `print(xs, xs.pop())` is an error; write `x = xs.pop()` first.
- **Methods are the exception that changes in place**: a method that changes `self` changes the
variable it was called on, as `a.deposit(50)` did above.
```stone
fn with_bonus(scores: list[int], bonus: int) -> list[int]:
for i in range(scores.len()):
scores[i] += bonus
ret scores
scores = [10, 20]
backup = scores
scores = with_bonus(scores, 5)
print(scores, backup)
```
This prints `[15, 25] [10, 20]`: `backup` kept its own copy. The Python habit of a function that
mutates its argument and returns nothing gets this warning, and the change is lost:
```text
warning: changes to 'xs' are lost when 'add' returns, since parameters are copies; return it and
assign the result where 'add' is called
```
## Rules the checker enforces
- No implicit conversion: `1 + 2.0` is an error. Convert with `float(n)` or `int(x)?`, and build
strings with values in them: `"n = {n}"`, or `"n = " + str(n)`. `str` and `{x}` take any value
and write it as `print` does.
- Conditions must be `bool`: write `if xs.len() > 0:`, `if s != "":`, `if n != 0:`.
- `==` does not compare lists or maps (it does compare numbers, strings, bools, tuples, structs,
options, and enums). Compare lists with a loop.
- `/` on ints truncates toward zero, and `%` takes the dividend's sign: `-7 / 2 == -3`,
`-7 % 2 == -1`. There is no `//`; that starts a comment.
- `**` takes an `int` exponent, which cannot be negative on an int: use `2.0 ** -1`, and
`math.sqrt` for square roots.
- Arithmetic on lists is element by element, as in numpy: `[1, 2] * 2` is `[2, 4]`,
`[1.0, 2.0] + [3.0, 4.0]` is `[4.0, 6.0]`. So `[0] * n` is not `n` zeros (the checker warns),
and `+` does not join lists; use `extend`.
- `int(x)` on a float truncates toward zero, as in Python (`int(-2.7)` is `ok(-2)`), and is an
`err` for nan or a float too large for an int.
- Every variable is assigned on every path before it is read, and keeps one type: after
`total = 0`, `total = 0.0` is an error (`cannot assign float to 'total', which is int`), so use
a second name.
- A function reads globals but cannot assign or change them, and its parameters and locals may
share a global's name.
- An empty `[]` or `{}` gets its type from later code, read top to bottom, so reading a field
or calling a method on one of its elements before anything has settled its type is an error
(`the type of this value must be known before its field 'n' is read`). Put the `append` first
in the code, or start from a non-empty value.
- Functions, structs, enums, traits, and `impl` blocks are top level only.
- Recursion is limited to 1000 nested calls.
## Not in stone, and what to write instead
| Python | stone |
| --- | --- |
| `[f(x) for x in xs if p(x)]` | `xs.filter(p).map(f)` with named functions, or a `for` loop with `if` and `append` |
| `f"{a} and {b}"`, `"%d" % n`, `.format` | `"{a} and {b}"`: every string takes values, with no `f`; format specs (`{x:.2f}`) are not supported yet |
| `"{}"` in a string that is not an f-string | `"{{}}"`, since braces hold values |
| `__str__`, `__repr__` | `impl display for T:` with `fn display(self) -> str` |
| `a if cond else b` | an `if`/`else` assigning a variable |
| `for k, v in d.items():` | the same; `d.items()` is a list of tuples |
| `d.pop(k)`, `d.pop(k, default)` | `d.pop(k)` returns an option: `.unwrap_or(default)` |
| `del d[k]` | `d.pop(k)` as a statement |
| `set()` | `map[K, bool]` |
| `def f(x, n=1)`, `f(n=2)` | every argument, positionally (only structs take names) |
| `lambda x: x * 2` | `\|x\| x * 2`; a lambda may read variables around it, as copies taken where it is written |
| `map(f, xs)`, `filter(f, xs)`, `any`/`all` with a generator | `xs.map(f)`, `xs.filter(f)`, `xs.any(f)`, `xs.all(f)` |
| `min(xs, key=f)`, `max(xs, key=f)` | `xs.min_by_key(f)`, `xs.max_by_key(f)`, options (the max is the last of equal ones) |
| `try`/`except`, `raise` | return a `result` and pass errors on with `?`, as in `int(s)?` |
| `class` with inheritance | a `struct` plus `impl`, an `enum` for alternatives, a `trait` for shared behavior |
| `None`, `Optional[T]` | `none`, `option[T]` |
| `pass` | restructure so every block has a real statement |
| `del`, `global`, `nonlocal`, `is`, `with`, `yield` | not available |
| `len(x)`, `abs`, `min(a, b)`, `max(a, b)`, `min(xs)` | `x.len()`, `math.abs`, `a.min(b)`, `a.max(b)`, `math.min(xs)` |
| `max(lo, min(x, hi))` | `x.clamp(lo, hi)` |
| `sorted(xs)`, `xs.sort(key=f, reverse=True)` | `ys = xs` then `ys.sort()` or `ys.sort_by_key(f)`, both stable; reverse with `ys[::-1]` |
| `"ab" * 3` | a loop of `+=`; only the string methods below exist |
| `s.lower()`, `s.upper()` | the same, but only ASCII letters change case |
| `print(x, end="")` | build the line as a string, then `print` it once |
| `open(path).read()`, `.readlines()`, `f.write(s)` | `fs.read(path)`, `fs.read_lines(path)`, `fs.write(path, s)`, each a `result` |
| `os.listdir`, `os.path.exists`, `os.remove`, `os.mkdir` | `fs.list_dir` (sorted), `fs.exists`, `fs.remove`, `fs.make_dir` |
| `sys.argv[1:]`, `sys.exit(n)` | `args()`, `os.exit(n)` |
| `subprocess.run([cmd, *args], capture_output=True, text=True)` | `os.run(cmd, args)`, a `result[output, str]` with `.status`, `.stdout`, `.stderr` |
| `socket.create_connection((host, port), timeout)`, `sock.sendall(b)`, `sock.recv(n)` | `net.connect(host, port)` or `net.connect_timeout(host, port, seconds)`, `c.send(text)` or `c.send_bytes(data)`, `c.read_line()`/`c.read()`/`c.read_all()` for text and `c.read_bytes(n)`/`c.read_exact(n)`/`c.read_all_bytes()` for bytes, each a `result`; `c.set_timeout(seconds)` |
| `open(path, "rb").read()`, `open(path, "wb").write(b)` | `fs.read_bytes(path)`, `fs.write_bytes(path, data)`, `fs.append_bytes(path, data)` |
| `socketserver`, `s.bind(...); s.listen(); s.accept()` | `l = net.listen(host, port)?` then `c = l.accept()?`, one connection at a time |
## Builtins
Functions: `print(values...)` (space-separated, newline), `eprint(values...)` (the same, to
stderr), `range(end)` and `range(start, end)`
(only as a `for` iterable), the conversions `int(x)`, `float(x)`, `str(x)`, `bytes(xs)`,
`input()` and `input(prompt)`
(a `result[str, str]`: `ok` of the line without its newline, or `err("end of input")`),
`eof()`, `args()`.
Methods: `len` (str, bytes, list, map); lists: `append`, `pop()` (option) and `pop(i)`, `insert`,
`extend`, `sort`, and taking a function, `sort_by_key`, `map`, `filter`, `any`, `all`,
`min_by_key`, `max_by_key` (options); strings: `strip`, `split()` and `split(sep)`, `splitlines`, `sep.join(xs)`, `find`
and `rfind` (options), `count`, `replace(old, new)` and `replace(old, new, n)`, `lower`, `upper`
(ASCII letters only), `startswith`, `endswith`, `bytes`; bytes: `text` (a result), `contains`; options: `is_some`, `is_none`, `unwrap`, `unwrap_or`; maps:
`contains`, `get` (option), `pop(key)` (option), `keys`, `values`, `items`; any comparable
value: `eq`; any ordered value (int, float, str, bytes, a struct with `impl ord`, a `T: ord`):
`cmp`, `a.min(b)`, `a.max(b)`, `x.clamp(lo, hi)` (stops the program if `lo > hi`).
Builtin modules, imported with `use math` (or `use math.sqrt`):
- `math`: `abs`, `min`, `max` (two or more numbers, or a list), `sqrt`, `floor`
- `random`: `seed`, `random`, `randint(low, high)`, `choice(xs)`
- `time`: `now`, `clock`, `sleep(seconds)`
- `os`: `env(name)` (option), `has_env`, `platform`, `arch`, `hostname`, `cpu_count`, `pid`,
`cwd`, `exit(code)`, `run(cmd, args)`, which runs a program (no shell; a `cmd` without `/` is
looked up in `PATH`, and stdin is empty) and returns a `result[output, str]`: `ok` of the
prelude struct `output` with `status: int` (minus the signal's number if one stopped it),
`stdout: str`, and `stderr: str`, even for a nonzero status, or `err` of a message if the
program cannot start, such as `no such file or directory: 'gti'`
- `fs`: `read(path)`, `read_lines(path)`, `write(path, text)`, `append(path, text)`,
`list_dir(path)` (sorted names), `exists(path)` (bool), `remove(path)`, `make_dir(path)`,
`read_bytes(path)`, `write_bytes(path, data)`, `append_bytes(path, data)`;
all but `exists` return a `result` whose error is a message such as
`no such file or directory: 'data.txt'`
- `net`: `connect(host, port)` (a `result[socket, str]`), `listen(host, port)` (a
`result[listener, str]`; port 0 picks a free one), `resolve(host)` (a `result[list[str], str]`
of addresses), `connect_timeout(host, port, seconds)`. A `socket` has `send(text)`,
`send_bytes(data)`, `read_line()` (`ok(none)` at the end), `read()`, `read_all()`,
`read_bytes(max)`, `read_exact(n)`, `read_all_bytes()`, `set_timeout(seconds)` (then a wait
gives `err("timed out")`), and `close()`; a `listener` has `accept()`, `port()`,
`set_timeout(seconds)`, and `close()`.
Both are types for signatures (`fn serve(l: listener)`), copies share the connection, and each
closes by itself right after the last statement that uses it. No TLS
Reading all of standard input:
```stone
total = 0
while not eof():
line = input().unwrap().strip()
if line == "":
cont
total += int(line).unwrap()
print(total)
```
Files, with errors passed on by `?`:
```stone
use fs
fn save(path: str, values: list[int]) -> result[int, str]:
fs.write(path, "")?
for v in values:
fs.append(path, str(v) + "\n")?
ret ok(values.len())
fn total(path: str) -> result[int, str]:
sum = 0
for line in fs.read_lines(path)?:
sum += int(line)?
ret ok(sum)
print(save("numbers.txt", [1, 2, 3])) // ok(3)
print(total("numbers.txt")) // ok(6)
print(total("missing.txt")) // err("no such file or directory: 'missing.txt'")
fs.remove("numbers.txt").unwrap()
```
Running other programs, where a nonzero status is still `ok`:
```stone
use os
fn lines_of(cmd: str, args: list[str]) -> result[list[str], str]:
out = os.run(cmd, args)?
if out.status != 0:
ret err("{cmd} failed with status {out.status}: {out.stderr.strip()}")
ret ok(out.stdout.strip().split("\n"))
print(lines_of("echo", ["one two"])) // ok(['one two'])
print(lines_of("sh", ["-c", "echo oops >&2; exit 3"])) // err('sh failed with status 3: oops')
print(lines_of("no-such-program", []))
// err("no such file or directory: 'no-such-program'")
```
A server and a client in one program, since a connection to its own port completes before it
is accepted:
```stone
use net
fn echo() -> result[str, str]:
l = net.listen("127.0.0.1", 0)?
c = net.connect("127.0.0.1", l.port())?
s = l.accept()?
c.send("hello\n")?
line = s.read_line()?.unwrap_or("")
s.send("you said {line}\n")?
// s closes after its last use above, so read_all sees the end
ret c.read_all()
print(echo()) // ok('you said hello\n')
```
`gui` draws on canvases (`gui.blank(w, h, color)`, colors from `gui.rgb(r, g, b)`) and shows
them in windows over Wayland or X11. A canvas is a value with `width`, `height`, and `pixels`, and its
methods draw on it in place:
```stone
use gui
c = gui.blank(8, 4, gui.rgb(0, 0, 0))
c.rect(1, 1, 3, 2, gui.rgb(255, 0, 0))
c.line(0, 3, 7, 3, gui.rgb(255, 255, 255))
print(c.get(2, 2)) // some(16711680)
print(c.get(9, 0)) // none
print(c.ppm().len()) // 107, a PPM image for fs.write_bytes
```
A window is `gui.open(title, w, h)?`, then a loop of `es = w.events()` (non-blocking; `w.wait()`
blocks), `w.show(c)`, and `w.close()` on `gui.event.close`. Or implement the trait `gui.app`
(`fn update(self, e: gui.event)` changing `self`, `fn draw(self, c: gui.canvas) -> gui.canvas`,
optional `fn done(self) -> bool`) and call `gui.run(title, w, h, state)`, which returns the last
state. Events are lowercase variants: `key_down(name)`, `key_up(name)`, `text(t)`,
`mouse_move(x, y)`, `mouse_down(button, x, y)`, `mouse_up(button, x, y)`, `scroll(steps)`,
`resize(w, h)`, `redraw`, `close`, `tick(seconds)`, `failed(reason)`. A `case _:` that does
nothing in a function without a value is `ret`.
## Modules
A program is an entry file plus the `.stn` files it uses, named by path from the entry file's
directory: `geometry/shapes.stn` is `use geometry.shapes`, called as `shapes.area(r)`, or
`use geometry.shapes.area` to call `area(r)`. Only `pub fn`, `pub struct`, `pub enum`, and
`pub trait` are visible to other files. A module other than the entry file holds only `use`,
`fn`, `struct`, `enum`, `trait`, and `impl`, never top-level statements. A program of several
files conventionally starts at `main.stn`.
## Style
Indent with four spaces. Write comment lines directly above a function to document it (editors
show them on hover). Name things in `snake_case` and types in `PascalCase`. Keep `use` lines at
the top of the file.
## Where to look next
- Tutorial:
- Every runtime error message:
- Example programs with expected output:
- Python programs next to their stone versions: