aboutsummaryrefslogtreecommitdiff
path: root/docs
diff options
context:
space:
mode:
authorBruce Hill <bruce@bruce-hill.com>2025-12-11 13:50:01 -0500
committerBruce Hill <bruce@bruce-hill.com>2025-12-11 13:52:46 -0500
commit7f8f2117799cdfa6b62909a9182b5adade1d0bd2 (patch)
tree1db466db870768e952f50572453660e090e434e0 /docs
parent630f910563b6f27dd34a4a0496a43d32539eadcb (diff)
parent02886fab651d3f64d2c8ded5597e6c075dc69b5f (diff)
Merge branch 'dev' into constructive-reals
Diffstat (limited to 'docs')
-rw-r--r--docs/bytes.md2
-rw-r--r--docs/command-line-parsing.md38
-rw-r--r--docs/enums.md103
-rw-r--r--docs/functions.md22
-rw-r--r--docs/integers.md31
-rw-r--r--docs/iterators.md17
-rw-r--r--docs/langs.md4
-rw-r--r--docs/libraries.md13
-rw-r--r--docs/lists.md46
-rw-r--r--docs/nums.md31
-rw-r--r--docs/operators.md76
-rw-r--r--docs/optionals.md9
-rw-r--r--docs/paths.md6
-rw-r--r--docs/pointers.md18
-rw-r--r--docs/reductions.md56
-rw-r--r--docs/serialization.md12
-rw-r--r--docs/structs.md23
-rw-r--r--docs/tables.md67
-rw-r--r--docs/text.md6
-rw-r--r--docs/versions.md20
20 files changed, 322 insertions, 278 deletions
diff --git a/docs/bytes.md b/docs/bytes.md
index 196d5762..f18071e2 100644
--- a/docs/bytes.md
+++ b/docs/bytes.md
@@ -4,7 +4,7 @@ Byte values have the type `Byte`, which corresponds to an unsigned 8-bit
integer ranging from 0 to 255. It is generally recommended to use `Int8`
instead of `Byte` when performing math operations, however, `Byte`s are used in
API methods for `Text` and `Path` that deal with raw binary data, such as
-`Path.read_bytes()` and `Text.bytes()`. Byte literals can be written using
+`Path.read_bytes()` and `Text.utf8()`. Byte literals can be written using
the `Byte()` constructor: `Byte(5)`.
# API
diff --git a/docs/command-line-parsing.md b/docs/command-line-parsing.md
index 714e6e9c..e1af5419 100644
--- a/docs/command-line-parsing.md
+++ b/docs/command-line-parsing.md
@@ -223,3 +223,41 @@ OPTIONS
--frob | --no-frob
Whether or not to frob your gropnoggles
```
+
+## Metadata
+
+You can specify metadata for a program, which is used for CLI messages like
+`--help`, as well as manpage documentation. Metadata can be specified as either
+a text literal (no interpolation) or as a file path literal.
+
+```
+USAGE: "--foo <n>"
+HELP: "
+ This is some custom help text.
+ You can use these flags:
+
+ --foo <n> The foo parameter
+ --help Show this message
+"
+MANPAGE_DESCRIPTION: (./description.roff)
+```
+
+Supported metadata:
+
+- `EXECUTABLE`: the name of the executable to compile. If not provided, the name
+ will be the name of the Tomo source file without the ".tm" extension.
+
+- `USAGE`: the short form usage shown in CLI parsing errors and help pages. This
+ should be a single line without the name of the program, so `USAGE: "--foo"`
+ would translate to the error message `Usage: myprogram --foo`. If this is not
+ present, it will be generated automatically.
+
+- `HELP`: The help message displayed when the `--help` flag is used or when there
+ is an argument parsing error. This should be a description of the program with
+ a multi-line documentation of commonly used flags.
+
+- `MANPAGE`: the full manpage (overrules the options below).
+
+- `MANPAGE_SYNOPSYS`: the synopsis section of the manpage (inserted literally).
+
+- `MANPAGE_DESCRIPTION`: the description section of the manpage (inserted literally).
diff --git a/docs/enums.md b/docs/enums.md
index 27ebdb86..5d6a2fb9 100644
--- a/docs/enums.md
+++ b/docs/enums.md
@@ -34,10 +34,8 @@ an `else` block to handle all unmatched patterns.
Tags can also be quickly checked using the `.TagName` field:
```tomo
->> a.AnInteger
-= yes
->> a.TwoWords
-= no
+assert a.AnInteger != none
+assert a.TwoWords == none
```
## Reducing Boilerplate
@@ -52,7 +50,7 @@ from a function with an explicit return type:
```tomo
enum ArgumentType(AnInt(x:Int), SomeText(text:Text))
-enum ReturnType(Nothing, AnInt(x:Int))
+enum ReturnType(AnInt(x:Int), Nothing)
func increment(arg:ArgumentType -> ReturnType)
when arg is AnInt(x)
@@ -62,10 +60,8 @@ func increment(arg:ArgumentType -> ReturnType)
...
->> increment(AnInt(5))
-= AnInt(6)
->> increment(SomeText("HI"))
-= Nothiing
+assert increment(AnInt(5)) == AnInt(6)
+assert increment(SomeText("HI")) == Nothiing
```
This lets us have overlapping tag names for different types, but smartly infer
@@ -127,3 +123,92 @@ func pad_text_wrapper(text:Text, width:Int, align:???)
**Note:** Each enum type is distinct, regardless of whether the enum shares the
same values with another enum, so you can't define another enum with the same
values and use that in places where a different anonymous enum is expected.
+
+
+## Result Type
+
+One very common pattern for enums is something which can either succeed or fail
+with an informative message. For example, if you try to delete a file, you will
+either succeed or fail, and if you fail, you might want to know that it was
+because the file doesn't exist or if you don't have permission to delete it.
+For this common pattern, Tomo includes a `Result` enum type in the standard
+library:
+
+```
+enum Result(Success, Failure(reason:Text))
+```
+
+You're free to define your own similar enum type or reuse this one as you see
+fit.
+
+
+## Field Access
+
+In some cases, a full `when` block is overkill when a value is assumed to have
+a certain tag. In those cases, you can access the enum's tag value using field
+access. The resulting value is `none` if the enum value is not the expected tag,
+otherwise it will hold the struct contents of the enum value for the given tag.
+
+```tomo
+func maybe_fail(should_fail:Bool -> Result)
+ if should_fail
+ return Failure("It failed")
+ else
+ return Success
+
+>> maybe_fail(yes).Failure
+# Prints 'Failure("It failed")'
+assert maybe_fail(yes).Failure!.text == "It failed"
+
+>> maybe_fail(no).Failure
+# Prints 'none'
+```
+
+## Enum Assertions
+
+In general, it's best to always handle failure results close to the call site
+where they occurred. However, sometimes, there's simply nothing you can do
+beyond reporting the error to the user and closing the program.
+
+```tomo
+result := (/tmp/log.txt).append(msg)
+when result is Failure(msg)
+ fail(msg)
+is Success
+ pass
+```
+
+For these cases, you can reduce the amount of code using a couple of
+simplifications. Firstly, you can access `.Success` to get the optional empty
+value of the Result enum (or `none` if there was a failure) and use `!` to
+assert that the value is non-`none`.
+
+```tomo
+(/tmp/log.txt).append(msg).Success!
+```
+
+Tomo is smart enough to give you a good error message in this case that will
+look something like:
+
+```
+This was expected to be Success, but it was:
+Failure("Could not write to file: /tmp/log.txt (Permission denied)")
+```
+
+You can further reduce the verbosity of this code by applying the `!` directly
+to the Result enum value:
+
+```tomo
+(/tmp/log.txt).append(msg)!
+```
+
+When the `!` operator is applied to an enum value, the effect is the same as
+applying `.Success!` or whatever the first tag in the enum definition is.
+
+```tomo
+enum Foo(A(member:Int), B)
+
+f := Foo.A(123)
+assert f! == f.A!
+assert f!.member == 123
+```
diff --git a/docs/functions.md b/docs/functions.md
index b325ee9a..11e1ea63 100644
--- a/docs/functions.md
+++ b/docs/functions.md
@@ -27,11 +27,9 @@ Default arguments are used to fill in arguments that were not provided at the
callsite:
```tomo
->> increment(5)
-= 6
+assert increment(5) == 6
->> increment(5, 10)
-= 15
+assert increment(5, 10) == 15
```
**Note:** Default arguments are re-evaluated at the callsite for each function
@@ -50,11 +48,9 @@ any unbound arguments, in order:
func foo(x:Int, y:Text, z:Float64)
return "x=$x y=$y z=$z"
->> foo(x=1, y="hi", z=2.5)
-= "x=1 y=hi z=2.5"
-
->> foo(z=2.5, 1, "hi")
-= "x=1 y=hi z=2.5"
+func main()
+ assert foo(x=1, y="hi", z=2.5) == "x=1 y=hi z=2.5"
+ assert foo(z=2.5, 1, "hi") == "x=1 y=hi z=2.5"
```
As an implementation detail, all function calls are compiled to normal
@@ -157,10 +153,10 @@ func create_adder(n:Int -> func(i:Int -> Int))
n = -1 // This does not affect the adder
return adder
-...
-add10 := create_adder(10)
->> add10(5)
-= 15
+
+func main()
+ add10 := create_adder(10)
+ assert add10(5) == 15
```
Under the hood, all user functions that are passed around in Tomo are passed as
diff --git a/docs/integers.md b/docs/integers.md
index 6be96880..877ab39d 100644
--- a/docs/integers.md
+++ b/docs/integers.md
@@ -37,8 +37,7 @@ The simplest form of integer literal is a string of digits, which is inferred
to have type `Int` (unbounded size).
```tomo
->>> 123456789012345678901234567890
-= 123456789012345678901234567890 : Int
+i := 123456789012345678901234567890
```
Underscores may also be used to visually break up the integer for readability:
@@ -79,12 +78,10 @@ quotient := numerator / denominator
remainder := numerator mod denominator
# Modulus always gives a non-negative result:
->> remainder >= 0
-= yes
+assert remainder >= 0
# The numerator can be reconstructed sensibly:
->> numerator == denominator * quotient + remainder
-= yes
+assert numerator == denominator * quotient + remainder
```
Importantly, these invariants hold for both positive and negative numerators
@@ -95,25 +92,23 @@ negative numbers are involved. Integer division rounds _down_ instead of
rounding _towards zero_, and modulus never gives negative results:
```tomo
->> quotient := -1 / 5
-= -1
+quotient := -1 / 5
+assert quotient == -1
->> remainder := -1 mod 5
-= 4
+remainder := -1 mod 5
+assert remainder == 4
->> -1 == 5 * -1 + 4
-= yes
+assert -1 == 5 * -1 + 4
```
```tomo
->> quotient := 16 / -5
-= -3
+quotient := 16 / -5
+assert quotient == -3
->> remainder := -1 mod 5
-= 1
+remainder := -1 mod 5
+assert remainder == 1
->> 16 == -5 * -3 + 1
-= yes
+assert 16 == -5 * -3 + 1
```
# API
diff --git a/docs/iterators.md b/docs/iterators.md
index 9337d859..d1ff7b4b 100644
--- a/docs/iterators.md
+++ b/docs/iterators.md
@@ -14,15 +14,11 @@ successively gets one line from a file at a time until the file is exhausted:
line three
")
->> iter := (./test.txt).each_line()
->> iter()
-= "line one" : Text?
->> iter()
-= "line two" : Text?
->> iter()
-= "line three" : Text?
->> iter()
-= none : Text?
+iter := (./test.txt).each_line()
+assert iter() == "line one"
+assert iter() == "line two"
+assert iter() == "line three"
+assert iter() == none
for line in (./test.txt).each_line()
pass
@@ -44,7 +40,6 @@ func primes_up_to(limit:Int)
n += 1
return (n - 1)?
->> [p for p in primes_up_to(11)]
-= [2, 3, 5, 7, 11]
+assert [p for p in primes_up_to(11)] == [2, 3, 5, 7, 11]
```
diff --git a/docs/langs.md b/docs/langs.md
index 8f440932..f8784fbc 100644
--- a/docs/langs.md
+++ b/docs/langs.md
@@ -29,8 +29,8 @@ situations where a malicious user might set their username to something like
`<script>alert('pwned')</script>`.
```
->> username := Text.read_line("Choose a username: ")
-= "<script>alert('pwned')</script>"
+username := Text.read_line("Choose a username: ")
+assert username == "<script>alert('pwned')</script>"
page := $HTML"
<html><body>
Hello $username! How are you?
diff --git a/docs/libraries.md b/docs/libraries.md
index 79477070..fc1eb4ea 100644
--- a/docs/libraries.md
+++ b/docs/libraries.md
@@ -151,8 +151,7 @@ that can be used by other Tomo projects. You can build a library by running
If you additionally add the `-I` flag, Tomo will copy the entire directory
(excluding files and directories that begin with `.` such as `.git`) into
-`~/.local/lib/tomo_vX.Y/` (where `X` and `Y` are the major/minor
-version of the compiler).
+`~/.local/lib/tomo@vTOMO_VERSION/LIBRARY_NAME@LIBRARY_VERSION`.
### Using Shared Libraries
@@ -167,13 +166,13 @@ read from the source files during compilation.
When you build and install a library, its version is determined from a
`CHANGES.md` file at the top level of the library directory (see:
[Versions](versions.md)). The library's version number is added to the file
-path where the library is installed, so if the library `foo` has version
+path where the library is installed, so if the library `mylib` has version
`v1.2`, then it will be installed to
-`~/.local/lib/tomo_vX.Y/foo_v1.2/`. When using a library, you must
+`~/.local/lib/tomo@TOMO_VERSION/mylib@v1.2/`. When using a library, you must
explicitly supply either the exact version in the `use` statement like this:
-`use foo_v1.2`, or provide a `modules.ini` file that lists version information
-and other details about modules being used. For each module, you should provide
-a `[modulename]` section with a `version=` field.
+`use mylib@v1.2`, or provide a `modules.ini` file that lists version
+information and other details about modules being used. For each module, you
+should provide a `[modulename]` section with a `version=` field.
```tomo
# File: foo.tm
diff --git a/docs/lists.md b/docs/lists.md
index d12a0b5b..2700fe81 100644
--- a/docs/lists.md
+++ b/docs/lists.md
@@ -30,17 +30,15 @@ Lists can also use comprehensions, where you specify how to dynamically create
all the elements by iteration instead of manually specifying each:
```tomo
->> [i*10 for i in (3).to(8)]
-= [30, 40, 50, 60, 70, 80]
->> [i*10 for i in (3).to(8) if i != 4]
-= [30, 50, 60, 70, 80]
+assert [i*10 for i in (3).to(8)] == [30, 40, 50, 60, 70, 80]
+assert [i*10 for i in (3).to(8) if i != 4] == [30, 50, 60, 70, 80]
```
Comprehensions can be combined with regular items or other comprehensions:
```tomo
->> [-1, i*10 for i in (3).to(8), i for i in 3]
-= [-1, 30, 40, 50, 60, 70, 80, 1, 2, 3]
+nums := [-1, i*10 for i in (3).to(8), i for i in 3]
+assert nums == [-1, 30, 40, 50, 60, 70, 80, 1, 2, 3]
```
## Length
@@ -48,8 +46,7 @@ Comprehensions can be combined with regular items or other comprehensions:
List length can be accessed by the `.length` field:
```tomo
->> [10, 20, 30].length
-= 3
+assert [10, 20, 30].length == 3
```
## Indexing
@@ -61,20 +58,15 @@ last item, `-2` is the second-to-last, and so on.
```tomo
list := [10, 20, 30, 40]
->> list[1]
-= 10?
+assert list[1] == 10?
->> list[2]
-= 20?
+assert list[2] == 20?
->> list[999]
-= none
+assert list[999] == none
->> list[-1]
-= 40?
+assert list[-1] == 40?
->> list[-2]
-= 30?
+assert list[-2] == 30?
```
If a list index of `0` or any value larger than the length of the list is
@@ -103,8 +95,7 @@ has the items from one appended to the other. This should not be confused with
the addition operator `+`, which does not work with lists.
```tomo
->> [1, 2] ++ [3, 4]
-= [1, 2, 3, 4]
+assert [1, 2] ++ [3, 4] == [1, 2, 3, 4]
```
## Implementation Details
@@ -172,24 +163,20 @@ nums[4] = 40
// Constant time operation, but increments the reference count:
tmp := nums
->> tmp
-= [10, 20, 30, 40]
+assert tmp == [10, 20, 30, 40]
// Now, a mutation will trigger a copy-on-write,
// which resets the reference count to zero:
nums[4] = 999
->> nums
-= [10, 20, 30, 999]
+assert nums == [10, 20, 30, 999]
// Because of the copy-on-write, `tmp` is unchanged:
->> tmp
-= [10, 20, 30, 40]
+assert tmp == [10, 20, 30, 40]
// Since the reference count has been reset, we can do more
// mutations without triggering another copy-on-write:
nums[4] = -1
->> nums
-= [10, 20, 30, -1]
+assert nums == [10, 20, 30, -1]
```
List reference counting is _approximate_, but will only ever err on the side
@@ -211,8 +198,7 @@ nums := @[10, 20, 30]
tmp := nums
nums.insert(40)
->> tmp
-= @[10, 20, 30, 40]
+assert tmp == @[10, 20, 30, 40]
```
Having multiple pointers to the same heap-allocated list does not cause the
diff --git a/docs/nums.md b/docs/nums.md
index 911144ef..4cd24c76 100644
--- a/docs/nums.md
+++ b/docs/nums.md
@@ -41,46 +41,39 @@ very liberal use of type coercion and implicit `none` checks when values are
required to be non-none. Here are a few examples:
```tomo
->> x := 0.0
-= 0 : Float64
+zero := 0.0
+assert zero == 0
y := 1.0
# Division might produce none:
->> x / y
-= 0 : Float64?
->> x / x
-= none : Float64?
+assert zero / y == 0
+assert zero / zero == none
# Optional types and none values propagate:
->> x/y + 1 + 2
-= 3 : Float64?
->> x/x + 1 + 2
-= none : Float64?
+assert zero/y + 1 + 2 == 3
+assert zero/zero + 1 + 2 == none
# Optional Nums can be handled explicitly using `or` and `!`:
->> x/x or -123
-= -123 : Float64
+assert zero/zero or -123 == -123
-# This would raise a runtime error if `x` and `y` were zero:
->> (x/y)!
-= 0 : Float64
+# This would raise a runtime error if `zero` and `y` were zero:
+assert (zero/y)! == 0
# Assigning to a non-optional variable will do an implicit check for none and
# raise a runtime error if the value is none, essentially the same as an
# implicit `!`:
-x = x/y
+zero = zero/y
func doop(x:Float64 -> Float64)
# If a function's return type is non-optional and an optional value is
# used in a return statement, an implicit none check will be inserted and
# will error if the value is none:
- return x / 2
+ return zero / 2
# Function arguments are also implicitly checked for none if the given value
# is optional and the function needs a non-optional value:
->> doop(x/y)
-= 0 : Float64
+assert doop(zero/y) == 0
```
Hopefully the end result of this system is one where users can take advantage
diff --git a/docs/operators.md b/docs/operators.md
index fdc2aee5..02559067 100644
--- a/docs/operators.md
+++ b/docs/operators.md
@@ -25,19 +25,16 @@ between the two objects. The `<>` operator exposes this signed comparison
value to the user.
```tomo
->> 0 <> 99
-= -1[32]
->> 5 <> 5
-= 0[32]
->> 99 <> 0
-= 1[32]
+assert 0 <> 99 == -1[32]
+assert 5 <> 5 == 0[32]
+assert 99 <> 0 == 1[32]
```
It's particularly handy for using the list `sort()` method, which takes a
function that returns a signed integer:
```tomo
->> foos.sort(func(a,b:&Foo): a.length <> b.length)
+foos.sort(func(a,b:&Foo): a.length <> b.length)
```
## Reducers
@@ -50,15 +47,12 @@ the need for several polymorphic functions used in other languages like
reducers in action:
```tomo
->> nums := [10, 20, 30]
->> (+: nums)!
-= 60
->> (or: n > 15 for n in nums)!
-= yes
-
->> texts := ["one", "two", "three"]
->> (++: texts)!
-= "onetwothree"
+nums := [10, 20, 30]
+assert (+: nums)! == 60
+assert (or: n > 15 for n in nums)! == yes
+
+texts := ["one", "two", "three"]
+assert (++: texts)! == "onetwothree"
```
The simplest form of a reducer is an infix operator surrounded by parentheses,
@@ -75,8 +69,8 @@ first option is to not account for it, in which case you'll get a runtime error
if you use a reducer on something that has no values:
```tomo
->> nums : [Int] = []
->> (+: nums)!
+nums : [Int] = []
+result := (+: nums)!
Error: this collection was empty!
```
@@ -85,8 +79,7 @@ If you want to handle this case, you can either wrap it in a conditional
statement or you can provide a fallback option with `else` like this:
```tomo
->> (+: nums) or 0
-= 0
+assert (+: nums) or 0 == 0
```
The `else` clause must be a value of the same type that would be returned.
@@ -102,21 +95,17 @@ struct Foo(x,y:Int)
func is_even(f:Foo -> Bool)
return (f.x + f.y) mod 2 == 0
->> foos := [Foo(1, 2), Foo(-10, 20)]
+foos := [Foo(1, 2), Foo(-10, 20)]
->> (+.x: foos)
-= -9
+assert (+.x: foos) == -9
// Shorthand for:
->> (+: f.x for f in foos)
-= -9
+assert (+: f.x for f in foos) == -9
->> (or).is_even() foos
-= yes
+assert (or).is_even() foos == yes
// Shorthand for:
->> (or) f.is_even() for f in foos
+assert ((or) f.is_even() for f in foos) == yes
->> (+.x.abs(): foos)
-= 11
+assert (+.x.abs(): foos) == 11
```
## `_min_` and `_max_`
@@ -126,10 +115,8 @@ Tomo introduces a new pair of operators that may be unfamiliar: `_min_` and
larger or smaller of two elements:
```tomo
->> 3 _max_ 5
-= 5
->> "XYZ" _min_ "ABC"
-= "ABC"
+assert 3 _max_ 5 == 5
+assert "XYZ" _min_ "ABC" == "ABC"
```
Initially, this might seem like a fairly useless operator, but there are two
@@ -144,22 +131,18 @@ Here's some examples:
```tomo
// Get the largest absolute value number:
->> 3 _max_.abs() -15
-= -15
+assert 3 _max_.abs() -15 == -15
struct Person(name:Text, age:Int)
// Get the oldest of two people:
->> Person("Alice", 33) _max_.age Person("Bob", 20)
-= Person(name="Alice", age=33)
+assert Person("Alice", 33) _max_.age Person("Bob", 20) == Person(name="Alice", age=33)
// Get the longest of two lists:
->> [10, 20, 30, 40] _max_.length [99, 1]
-= [10, 20, 30, 40]
+assert [10, 20, 30, 40] _max_.length [99, 1] == [10, 20, 30, 40]
// Get the list with the highest value in the last position:
->> [10, 20, 999] _max_[-1] [99, 1]
-= [10, 20, 999]
+assert [10, 20, 999] _max_[-1] [99, 1] == [10, 20, 999]
```
The keyed comparison can chain together multiple field accesses, list index
@@ -173,12 +156,9 @@ This means that you get get the minimum or maximum element from an iterable
object using them:
```tomo
->> nums := [10, -20, 30, -40]
->> (_max_: nums)
-= 30
-
->> (_max_.abs(): nums)
-= -40
+nums := [10, -20, 30, -40]
+assert (_max_: nums) == 30
+assert (_max_.abs(): nums) == -40
```
## Operator Overloading
diff --git a/docs/optionals.md b/docs/optionals.md
index 6f2e3d5e..a6d6c338 100644
--- a/docs/optionals.md
+++ b/docs/optionals.md
@@ -90,14 +90,11 @@ an `Abort` type like `fail()` or `exit()`:
```tomo
maybe_x : Int? = 5
->> maybe_x or -1
-= 5 : Int
->> maybe_x or fail("No value!")
-= 5 : Int
+assert (maybe_x or -1) == 5
+assert (maybe_x or fail("No value!")) == 5
maybe_x = none
->> maybe_x or -1
-= -1 : Int
+assert (maybe_x or -1) == -1
>> maybe_x or fail("No value!")
# Failure!
diff --git a/docs/paths.md b/docs/paths.md
index 2fa55b13..810cb6df 100644
--- a/docs/paths.md
+++ b/docs/paths.md
@@ -13,10 +13,8 @@ syntax. A path literal begins with either `(/`, `(./`, `(../`, or `(~/` and cont
until a matching closing parenethesis:
```tomo
->> (/tmp)
-= (/tmp)
->> (~/path with/(parens) is/ok/)
-= (~/path with/(parens) is/ok/)
+assert (/tmp) == (/tmp)
+assert (~/path with/(parens) is/ok/) == (~/path with/(parens) is/ok/)
```
### Interpolation
diff --git a/docs/pointers.md b/docs/pointers.md
index 254db07e..fb7668f1 100644
--- a/docs/pointers.md
+++ b/docs/pointers.md
@@ -18,16 +18,14 @@ func no_mutation_possible(nums:[Int])
...
my_nums := [0, 1, 2]
no_mutation_possible(my_nums)
->> my_nums
-= [0, 1, 2]
+assert my_nums == [0, 1, 2]
func do_mutation(nums:@[Int])
nums[1] = 10 // The mutates the value at the given pointer's location
...
my_nums := @[0, 1, 2]
do_mutation(my_nums)
->> my_nums
-= @[10, 1, 2]
+assert my_nums == @[10, 1, 2]
```
## Dereferencing
@@ -37,8 +35,7 @@ memory location using the `[]` postfix operator (with no value inside).
```tomo
nums := @[10, 20]
->> nums[]
-= [10, 20]
+assert nums[] == [10, 20]
```
## Equality and Comparisons
@@ -52,12 +49,10 @@ doing a mutation to the other.
```tomo
x := @[10, 20, 30]
y := @[10, 20, 30]
->> x == y
-= no
+assert x != y
z := x
->> x == z
-= yes
+assert x == z
```
Pointers are ordered by memory address, which is somewhat arbitrary, but
@@ -112,8 +107,7 @@ inside of any datastructures as elements or members.
```tomo
nums := @[10, 20, 30]
->> nums.first(func(x:&Int): x / 2 == 10)
-= 2 : Int?
+assert nums.first(func(x:&Int): x / 2 == 10) == 2
```
Normal `@` pointers can be promoted to immutable view pointers automatically,
diff --git a/docs/reductions.md b/docs/reductions.md
index abd612b0..929e5d00 100644
--- a/docs/reductions.md
+++ b/docs/reductions.md
@@ -7,8 +7,7 @@ infix operator followed by a colon, followed by a collection:
```tomo
nums := [10, 20, 30]
sum := (+: nums)
->> sum
-= 60 : Int?
+assert sum == 60
```
Reductions return an optional value which will be a null value if the thing
@@ -21,15 +20,12 @@ provide a fallback value:
nums : [Int] = []
sum := (+: nums)
->> sum
-= none : Int?
+assert sum == none
->> sum or 0
-= 0
+assert sum or 0 == 0
->> nums = [10, 20]
->> (+: nums)!
-= 30
+nums = [10, 20]
+assert (+: nums)! == 30
```
Reductions can be used as an alternative to generic functions like `sum()`,
@@ -38,20 +34,16 @@ Reductions can be used as an alternative to generic functions like `sum()`,
```tomo
# Sum:
->> (+: [10, 20, 30])!
-= 60
+assert (+: [10, 20, 30])! == 60
# Product:
->> (*: [2, 3, 4])!
-= 24
+assert (*: [2, 3, 4])! == 24
# Any:
->> (or: [no, yes, no])!
-= yes
+assert (or: [no, yes, no])! == yes
# All:
->> (and: [no, yes, no])!
-= no
+assert (and: [no, yes, no])! == no
```
## Minimum and Maximum
@@ -61,12 +53,10 @@ a collection using the `_min_` and `_max_` infix operators.
```tomo
# Get the maximum value:
->> (_max_: [10, 30, 20])!
-= 30
+assert (_max_: [10, 30, 20])! == 30
# Get the minimum value:
->> (_min_: [10, 30, 20])!
-= 10
+assert (_min_: [10, 30, 20])! == 10
```
Reducers also support field and method call suffixes, which makes it very easy
@@ -76,28 +66,22 @@ feature_.
```tomo
# Get the longest text:
->> (_max_.length: ["z", "aaaaa", "mmm"])!
-= "aaaaa"
+assert (_max_.length: ["z", "aaaaa", "mmm"])! == "aaaaa"
# Get the number with the biggest absolute value:
->> (_max_.abs(): [1, -2, 3, -4])!
-= -4
+assert (_max_.abs(): [1, -2, 3, -4])! == -4
```
You can also use suffixes on other operators:
```tomo
texts := ["x", "y", "z"]
->> (==: texts)
-= no
->> (==.length: texts)
-= yes
->> (+.length: texts)
-= 3
+assert (==: texts) == no
+assert (==.length: texts) == yes
+assert (+.length: texts) == 3
nums := [1, 2, -3]
->> (+.abs(): nums)
-= 6
+assert (+.abs(): nums) == 6
```
## Comprehensions
@@ -108,10 +92,8 @@ while filtering out values or while applying a transformation:
```tomo
# Sum the lengths of these texts:
->> (+: t.length for t in ["a", "bc", "def"])!
-= 6
+assert (+: t.length for t in ["a", "bc", "def"])! == 6
# Sum the primes between 1-100:
->> (+: i for i in 100 if i.is_prime())!
-= 1060
+assert (+: i for i in 100 if i.is_prime())! == 1060
```
diff --git a/docs/serialization.md b/docs/serialization.md
index 287cbda7..8c72cb83 100644
--- a/docs/serialization.md
+++ b/docs/serialization.md
@@ -67,12 +67,14 @@ struct Cycle(name:Text, next:@Cycle?=none)
c := @Cycle("A")
c.next = @Cycle("B", next=c)
->> c
+say("$c")
+# @Cycle(name="A", next=@Cycle(name="B", next=@~1))
+bytes : [Byte] = c
+say("$bytes")
+# [0x02, 0x02, 0x41, 0x01, 0x04, 0x02, 0x42, 0x01, 0x02]
+roundtrip : @Cycle = bytes
+say("$roundtrip")
# @Cycle(name="A", next=@Cycle(name="B", next=@~1))
->> bytes : [Byte] = c
-# [0x02, 0x02, 0x41, 0x01, 0x04, 0x02, 0x42, 0x01, 0x02] : [Byte]
->> roundtrip : @Cycle = bytes
-# @Cycle(name="A", next=@Cycle(name="B", next=@~1)) : @Cycle
assert roundtrip.next.next == roundtrip
```
diff --git a/docs/structs.md b/docs/structs.md
index 1dfa49c9..8f280fab 100644
--- a/docs/structs.md
+++ b/docs/structs.md
@@ -6,10 +6,9 @@ types that can be accessed by fields:
```tomo
struct Foo(name:Text, age:Int)
...
->> my_foo := Foo("Bob", age=10)
-= Foo(name="Bob", age=10)
->> my_foo.name
-= "Bob"
+my_foo := Foo("Bob", age=10)
+assert my_foo == Foo(name="Bob", age=10)
+assert my_foo.name == "Bob"
```
Structs are value types and comparisons on them operate on the member values
@@ -49,11 +48,8 @@ struct Password(raw_password_text:Text; secret)
struct User(username:Text, password:Password)
...
user := User("Stanley", Password("Swordfish"))
->> user
-= User(username="Stanley", password=Password(...))
-
->> "$user" == 'User(username="Stanley", password=Password(...))'
-= yes
+assert user == User("Stanley", Password("Swordfish"))
+assert "You are: $user" == 'You are: User(username="Stanley", password=Password(...))'
```
Designing APIs so they take secrecy-protected structs instead of raw data
@@ -62,17 +58,12 @@ your logs! Secrecy-protected values still work the same as any other struct,
they just don't divulge their contents when converting to strings:
```tomo
->> user.password == Password("Swordfish")
-= yes
+assert user.password == Password("Swordfish")
```
You can also access the fields directly, but hopefully this extra amount of
friction reduces the chances of accidentally divulging sensitive content:
```tomo
->> user.password
-= Password(...)
-
->> user.password.raw_password_text
-= "Swordfish"
+assert user.password.raw_password_text == "Swordfish"
```
diff --git a/docs/tables.md b/docs/tables.md
index eaf0083e..12b2eb35 100644
--- a/docs/tables.md
+++ b/docs/tables.md
@@ -40,10 +40,8 @@ optional value:
```tomo
table := {"A": 1, "B": 2}
->> table["A"]
-= 1?
->> table["missing"]
-= none
+assert table["A"] == 1
+assert table["missing"] == none
```
As with all optional values, you can use the `!` postfix operator to assert
@@ -51,11 +49,9 @@ that the value is non-none (and create a runtime error if it is), or you can
use the `or` operator to provide a fallback value in the case that it's none:
```tomo
->> table["A"]!
-= 1
+assert table["A"]! == 1
->> table["missing"] or -1
-= -1
+assert (table["missing"] or -1) == -1
```
### Fallback Tables
@@ -66,18 +62,15 @@ is not found in the table itself:
```tomo
t := {"A": 10}
t2 := {"B": 20; fallback=t}
->> t2["A"]
-= 10?
+assert t2["A"] == 10
```
The fallback is available by the `.fallback` field, which returns an optional
table value:
```tomo
->> t2.fallback
-= {"A": 10}?
->> t.fallback
-= none
+assert t2.fallback == {"A": 10}
+assert t.fallback == none
```
### Default Values
@@ -87,13 +80,10 @@ present in the table or its fallback (if any).
```tomo
counts := &{"foo": 12; default=0}
->> counts["foo"]
-= 12
->> counts["baz"]
-= 0
+assert counts["foo"] == 12
+assert counts["baz"] == 0
counts["baz"] += 1
->> counts["baz"]
-= 1
+assert counts["baz"] == 1
```
When values are accessed from a table with a default value, the return type
@@ -108,8 +98,7 @@ You can assign a new key/value mapping or overwrite an existing one using
t := {"A": 1, "B": 2}
t["B"] = 222
t["C"] = 333
->> t
-= {"A": 1, "B": 222, "C": 333}
+assert t == {"A": 1, "B": 222, "C": 333}
```
## Length
@@ -117,8 +106,7 @@ t["C"] = 333
Table length can be accessed by the `.length` field:
```tomo
->> {"A": 10, "B": 20}.length
-= 2
+assert {"A": 10, "B": 20}.length == 2
```
## Accessing Keys and Values
@@ -128,10 +116,8 @@ constant-time immutable slice of the internal data from the table:
```tomo
t := {"A": 10, "B": 20}
->> t.keys
-= ["A", "B"]
->> t.values
-= [10, 20]
+assert t.keys == ["A", "B"]
+assert t.values == [10, 20]
```
## Iteration
@@ -150,6 +136,31 @@ Table iteration operates over the value of the table when the loop began, so
modifying the table during iteration is safe and will not result in the loop
iterating over any of the new values.
+## Sets
+
+For an interface similar to Python's Sets, Tomo tables can be used with an
+empty struct as its value type. For convenience, if a value or value is
+omitted, Tomo will assign a default value type of `struct Present()` (an empty
+struct). This way, the values stored in the table take up no space, but you
+still have an easy way to represent Set-like data.
+
+```tomo
+nums := {10, 20, 30, 10}
+assert nums.items == [10, 20, 30]
+assert nums[10] == Present()
+assert nums[99] == none
+```
+
+The following set-theoretic operations are available for tables:
+
+- Set union: (AKA `or`) `{10, 20, 30}.with({30, 40})` -> `{10, 20, 30, 40}`
+- Set intersection (AKA `and`) `{10, 20, 30}.intersection({30, 40})` -> `{10,
+ 20, 30, 40}`
+- Set difference (AKA, `xor`, disjunctive union, symmetric difference) `{10,
+ 20, 30}.difference({30, 40})` -> `{10, 20, 40}`
+- Set subtraction (AKA, `-`, asymmetric difference) `{10, 20, 30}.without({30,
+ 40})` -> `{10, 20}`
+
# API
[API documentation](../api/tables.md)
diff --git a/docs/text.md b/docs/text.md
index 2df27811..b41140f7 100644
--- a/docs/text.md
+++ b/docs/text.md
@@ -247,10 +247,8 @@ when you think of "letters" in a string. If you have text with an emoji that has
several joining modifiers attached to it, that text has a length of 1.
```tomo
->> "hello".length
-= 5
->> "👩🏽‍🚀".length
-= 1
+assert "hello".length == 5
+assert "👩🏽‍🚀".length == 1
```
### Iteration
diff --git a/docs/versions.md b/docs/versions.md
index eb617f43..0af7595c 100644
--- a/docs/versions.md
+++ b/docs/versions.md
@@ -33,6 +33,10 @@ Major version change including:
Bugfixes and new features...
```
+The versions here use a `vMAJOR.MINOR` semantic versioning style, but this is
+not required. Versions can be any string literal, for example:
+`edition-2025-11-29` or `1.2.3` or `6.1-EliteOcelot`.
+
When you build the compiler or a library, if this file exists, it will be used
to determine the current version (the top-most level 2 header).
@@ -40,16 +44,16 @@ to determine the current version (the top-most level 2 header).
The version for the Tomo language itself will come into play in a few ways:
-1. The compiler will be installed to `tomo_vX.Y` (where `X` is the major
- version number and `Y` is the minor version number).
+1. The compiler will be installed to `tomo@TOMO_VERSION` (where `TOMO_VERSION`
+ is the verion of the Tomo compiler).
2. A symbolic link will be installed from `tomo` to the largest version of Tomo
that is installed on your machine (e.g. `~/.local/bin/tomo ->
- ~/.local/bin/tomo_v2.12`).
+ ~/.local/bin/tomo@v2025-11-29.1`).
3. Each version of Tomo will build and install its own shared library file
- (e.g. `~/.local/lib/libtomo_v1.2.so`) and headers (e.g.
- `~/.local/include/tomo_v1.2/tomo.h`).
+ (e.g. `~/.local/lib/libtomo@v2025-11-29.1.so`) and headers (e.g.
+ `~/.local/include/tomo@v2025-11-29.1/tomo.h`).
4. Tomo libraries will be installed to a separate subdirectory for each version
- of the compiler (e.g. `~/.local/lib/tomo_v1.2/`).
+ of the compiler (e.g. `~/.local/lib/tomo@v2025-11-29.1/`).
## Tomo Program Versions
@@ -57,7 +61,7 @@ When you write a Tomo program (say, `foo.tm`) and run it, Tomo will
automatically add support for parsing a version number out of an accompanying
`CHANGES.md` file in the same directory. You can use the `--version` flag to
print the version number and exit. For example, if I run `tomo foo.tm --
---version`, it will print `v0.0` if no `CHANGES.md` file exists, otherwise it
+--version`, it will print `v0` if no `CHANGES.md` file exists, otherwise it
will compile the program with the most recent version number from that file and
print it instead. Similarly, if you run `tomo -e foo.tm` to build `foo` as a
standalone executable and then run `./foo --version`, it will print the version
@@ -68,7 +72,7 @@ number and exit without running the program.
Tomo libraries also have version numbers. When you install a library, its
version number will be used to determine its installation location and how it's
used in code. You must either explicitly import the library with its version
-number (e.g. `use foo_v1.2`) or include a `modules.ini` configuration file that
+number (e.g. `use foo@v1.2`) or include a `modules.ini` configuration file that
maps a shorthand alias to a specific version of a library. For example, if the
`modules.ini` file has a `[foo]` section with `version=v1.2`, you can put `use
foo` to use v1.2 of the `foo` library (assuming you have it installed).