Changes
Unreleased
-
A program's subcommands can be split across files:
use ./commands.tmnow adds that file'sfunc main.foo(...)definitions to the program's command-line interface, the same way it adds them to its namespace. A module imported under a name (commands := use ./commands.tm) keeps its subcommands to itself, and defining a subcommand that aused file already defines is an error, since only one of them could be dispatched to. -
_embed_ ./dirembeds a whole directory: every file in it, recursively, as a{Path:[Byte]}table keyed by each file's path relative to the embedded directory (coercible to{Path:Text}or{Path:CString}with the same compile-time checks a file embed gets when coerced toTextorCString). Dotfiles are skipped unless embedded by name, symbolic links are skipped, and a directory with nothing to embed is a compile error. Seedocs/embedding.md. -
Embedded files are build dependencies: editing an embedded file (or adding, removing, or renaming a file in an embedded directory) now recompiles the module embedding it, instead of leaving a stale binary.
-
Embedded paths and local imports must be relative (starting with
./or../). An absolute or~/path in_embed_or auseof a local module is now a parse error, since such a file could never ship in the program's source archive. -
The source archive embedded in every compiled executable is now self-contained and self-describing:
src/holds the program's sources and embedded files (rooted at the deepest directory containing them all, so nothing depends on where the compiler was invoked from),packages/holds every used package's sources by content digest, and a generatedREADME.mdandMakefiledocument and perform the rebuild. Extracted archives rebuild with plainmake, offline, from any directory. Thepackages.linkszip entry is gone, along with the code that read it; source archives written by earlier versions of tomo no longer extract into a buildable tree. -
C sources, headers, assembly files, and static archives that a module
uses by a relative path are included in the source archive too, along with the files they#include "...", so a program using inline C rebuilds from an extracted archive. (Auseof<stdio.h>or an absolute path still names a file the rebuilding machine supplies. Aused static archive is a compiled binary, so an archive containing one only rebuilds on the architecture it was built for.) -
New command:
tomo source <binary>lists what is in a compiled program's embedded source archive, andtomo source <binary> -o <dest>gets it out: a<dest>ending in.zipis written as a zip file of the archive, and anything else is a directory to unpack into. This replacestomo info -x, which is gone:tomo infonow only prints build info. -
New flag:
tomo build --skip-sourceleaves the source archive out of the compiled executable, making it smaller at the cost oftomo sourceno longer being able to unpack it.tomo runandtomo evalnever embed one: their executables are run once and discarded. -
New flag:
tomo build|run|test --package-sources <dir>finds pinned packages by content digest in a local directory of package sources (like an extracted archive'spackages/) instead of downloading them. -
not xdoes bitwise negation on aByte, flipping all eight bits, the way it already did on the fixed-width integer types:not Byte(0x0F)isByte(0xF0). It used to be a compile error. -
tomo evalprints the value of every statement it is given, not a>>doctest of one expression.tomo eval '1+2'prints3where it used to print the expression, its source location, and= 3 : Int. The value is rendered the way>>renders one and colorized only when the output is going to a terminal, the same as whatmain()returns. -
The argument to
tomo evalis ordinary Tomo source rather than a single expression, so it can declare variables and functions anduseother modules, and it can span several lines. Each statement that evaluates to something prints what it evaluated to, and one that evaluates to nothing (a declaration, or a call that returns nothing) prints nothing of its own. -
Top-level statements can be separated by
;as well as by newlines, so a whole program fits on one line:tomo eval 'x := 5; x * 2'prints10. The manpage already describedtomo evalthis way. Statements inside an indented block are still one per line. -
Fixed reading a top-level variable from a function defined above it. A function that read a private (
_-prefixed) top-level variable declared further down the file failed to compile, and one that read a variable assigned at startup skipped the check that reports the use of a not-yet-initialized value, so whether that check ran depended on where the function sat in the file. A variable's accessor is now settled when the variable is bound rather than partway through compiling the file, so it is the same for every reader. -
Fixed using a top-level variable that is initialized at startup with a field access, an index, or a call, such as calling a variable that holds a function. The check that reports a not-yet-initialized value was missing a pair of parentheses, so the field or call bound to the wrong part of it and the program failed to compile.
-
main()and eachmain.<command>subcommand may return a value, which the program prints when it runs.func main(x,y:Int -> Int)returningx + yprints the sum, rendered the way>>renders a value and colorized only when the output is going to a terminal, quotes around text included --say()is still how a program writes text as itself. A command that returns nothing prints nothing, and what a command returns does not affect the exit status. Returning a value used to be a compile error. -
Removed the
~colorizedtext suffix.say("v $n"~colorized)rendered the interpolated values the way>>renders one, but the decision was made at compile time, so the escape codes were emitted even when the output was redirected to a file. It also only reached values that weren't alreadyText-- an interpolatedTextcame out bare, which is the one thing a syntax highlighter would certainly colour -- and nothing stopped$HTML"<p>$n</p>"~colorizedfrom splicing terminal escapes into HTML. The suffix was the only use of a trailing~in the language, and a failed match consumed the~anyway, so an error on"hi"~whateverunderlined the wrong span.tomo evalstill prints its result in colour, now decided at run time byUSE_COLOR. -
tomo evalbuilds its scratch program as a single>>line instead of assembling afunc main()around the parsed statements, so the value is printed by the same code that prints any other>>. Only a single expression is accepted now:tomo eval 'x := 5; x*2'andtomo eval 'use random; random.int(1, 100)'no longer work. -
tomo run,build,test,format,parse, andtranspileno longer take a directory as shorthand for the file inside it:tomo run foomeanttomo run foo/foo.tm. It was undocumented -- each of those commands has always advertisedfile.tm-- and it only ever worked for a bare name, since the directory's own name was read off the end of the argument before the argument was resolved, sotomo run foo/looked forfoo/.tmandtomo run .for..tm. A directory argument is now an error that says so. (tomo package, which does document directory arguments, is unaffected.) -
Path.base_name()andPath.extension()no longer come back empty for a path with a trailing slash. The loop meant to strip the slash tested the byte the pointer was on rather than the one behind it, and the pointer starts on the terminator, so it never stripped anything: the name of/foo/bar/was read as the empty string after the final slash.Path.with_extension()was wrong the same way, turning/foo/bar.txt/into/foo/bar.txt/.cinstead of/foo/bar.c. Path literals andPath.from_text()normalize a trailing slash away, so this was only reachable from a path built in C, such as one taken straight fromargv. -
Path.unique_directory()andPath.write_unique_bytes()no longer require the path to say where the random characters go. A path that doesn't already containXXXXXXgets-XXXXXXappended, so(/tmp/report).unique_directory()yields something like/tmp/report-a1B2c3rather than failing. -
The unused-variable check no longer flags variables that are only written through.
p := &x; p[] = 456reportedpas assigned but never read, even though the write readspto findx. Writes reached through a dereference, and writes to a field or index of a variable declared as a pointer (&xor@x), now count as uses. Writing to a field or index of a plain value still doesn't, since that only mutates the variable itself. -
tomo formatno longer expands inline C that fits on one line into a three-line block. It testedInlineCCode.type, which the compiler fills in and the parser doesn't, so the check was never true while formatting and every inline C expression in a multi-line context grew.tomo format --checkcould not see this, since it compares syntax trees and reparsing dedents a verbatim block, sotest/format/now holds snapshots of the layout the formatter produces, checked bymake test(regenerate withmake regen-format-tests). -
Text.lines()dropped a final line exactly one grapheme long, so"a\nb".lines()returned["a"]and"x".lines()returned[]. Any text whose last line was a single character silently lost it. This also madetomo formatcorrupt inline C code, since the formatter splits verbatim chunks into lines:sizeof(@x)came back assizeof(@(x). -
Struct definitions take a
packed_boolsflag, which stores eachBoolfield in one bit and eachBool?field in two (yes,no, andnoneall fit), so adjacent boolean fields share bytes: a struct of eightBoolfields goes from eight bytes to one, and fiveBool?fields from five to two. Fields of any other type keep their natural size and alignment. Bit-packed fields have no address of their own, so&foo.some_flagon a packed struct is now a compile error explaining that, rather than the C compiler's "address of bit-field requested". Previously everyBoolfield of every struct was bit-packed whether or not it saved anything, which is now what the flag opts into.Enum members take the flag individually (
enum Foo(A{a, b:Bool; packed_bools}, ...)), so packed and unpacked members can sit in the same enum. The bits are packed within the member; the enum's own tag is untouched.Bit-packed fields are read and written through the platform's own bit allocation order, which runs from the least significant bit of a byte on little-endian targets and from the most significant bit on big-endian ones (
s390x), so packed structs behave the same on both.Structs also emit
_Static_asserts checking that the size and alignment the compiler computes for them matches what C lays out, so the two models of struct layout can't drift apart silently.
2026-08-27
-
tomo run --debug prog.tmruns a program under gdb, in Tomo's terms. Breakpoints, stepping, and backtraces already worked on.tmfiles (the generated C carries#linedirectives back to the source); what is new is everything above that. Values print through Tomo's own formatter, syntax coloring and all, including lists, tables, and optionals. A--debugbuild emits the type information for each variable beside it, which is the only way a debugger can see inside a type-erased value. Function and argument names are Tomo's rather than the mangled C symbols, in gdb's own output as well as Tomo's, so a backtrace readshelper (label=..., count=..., items=...). Commands added:tlocals(which inside a lambda lists the variables it closed over as well as its arguments) andtframe.backtrace/bt/whereis replaced by one that prints the stack in Tomo's terms, arguments included (helper(label="widgets", count=7, items=[1, 2, 3])), with values cut short; gdb's own remains asinfo stack. A Tomo variablexis_$xin the generated C, sop(gdb's alias forprint) is replaced by one that understands Tomo names. Without it,print login a program with a variable namedloganswers with libm'slogfunction.printandwatchthemselves are untouched, and anythingpdoes not recognize is passed toprintunchanged. An exactNumwhose value no decimal expresses is shown with a rounded decimal beside it (32768/3 ~= 10922.6666666667,pi ~= 3.1415926536);set tomo-num-digitscontrols the digits. Printed values are cut off at gdb's ownprint elements, since a Tomo value is formatted whole and everything in scope is printed at every stop.frame,up, anddownreport the frame they select the way a stop is reported, with the source around the line and the variables in scope.A new
breakpoint()builtin stops the program where it is called; it compiles to nothing at all without--debug, so leaving one in the source costs a release build nothing. Runtime errors stop in the debugger too, with the failing frame and its variables intact, as do fatal signals;Ctrl-Cstops the program without killing it. A program that runs to completion leaves the debugger with its own exit status.--debugalso works onbuild,transpile, andtest. It compiles at-O0by default (an explicit-Ostill wins) and keeps its DWARF uncompressed, so an external debugger can read it. Seedocs/debugging.md. -
Fixed three source-mapping bugs the debugger work turned up, each of which made generated code claim to be somewhere it wasn't:
- A
matchstatement compiled to aMATCH(...)macro whose argument was the wholeswitchbody. A preprocessor directive inside a macro argument is undefined, so the#linedirectives in the clause bodies were dropped and every line of every clause was attributed to thematchline itself. The macro is now written out, and each clause maps to its own lines. - Compiler-synthesized C fragments dropped into the middle of an expression
carried a
#lineof their own, which moved the line counter backwards mid-statement and left everything after it, to the end of the function, attributed to whatever line the fragment came from. - The generated command-line wrapper claimed to be lines of the
.tmfile, counting on from its first line; it is now attributed to the generated C file it actually is.
- A
-
Fixed a list or table comprehension being rejected in argument position:
f([i for i in 3])failed with "the function takes these args:([Int]), but it's being called with:([Int])", while binding it to a variable first worked. Checking a literal against a parameter type compared each element against the item type, but a comprehension is not an element: it stands for the items it produces, and its own type is the whole list type. It is now looked through, in its loop's scope, so what is checked is what it yields. The table case was skipped outright rather than checked, so a table comprehension's entries are now checked too. -
Breaking:
sleep(),Text.distance(), andList.sample()now useNumrather thanFloat64, so the default numeric type is what the standard library reaches for.sleep()takes aNum(sleep(1/100)is exact now),Text.distance()returns one, andList.sample()takes[Num]weights and afunc(->Num)random source. Code passing an explicitly-Float64value to these needs a conversion; decimal literals likesleep(1.5)are unaffected, since they were alreadyNum.Text.distance()still computes in floating point internally and converts at the end, and its costs are sums of 1, 0.5 and 0.25, all exact in binary floating point, so the result is lossless. -
Programs can now define git-style CLI subcommands by declaring functions named
main.foo(...)(nested arbitrarily deep, e.g.main.submodule.init). Each subcommand gets the same automatic argument parsing,--helptext, and manpage documentation thatmain()gets, with doc comments above each function used as its one-line summary in the generated command listings. Running the program with no arguments (or an unknown command) prints a usage listing of the available commands, unless the program definesmain(). A command can both do something itself and have sub-subcommands (likegit stash/git stash pop): dispatch descends into the longest matching command path. A program with subcommands can also define a plainmain(), which runs when the first argument doesn't name a subcommand (./myprogrunsmain(),./myprog foorunsmain.foo()). Subcommand functions are also ordinary functions: they can be called directly (main.add(...)), used as function values, and accessed from other files (mod.main.add(...)). -
Fixed three bugs in command-line parsing of flags whose value is embedded in the argument itself (
--flag=value):--flag=valuesilently discarded every argument after it, somyprog --name=x --loudparsednameand then ignored--loudentirely. Only the flag's own token is consumed now.--flag=a,b,cand-f=a,b,con a list- or table-valued flag crashed (segfault or hang): the comma-separated values were appended to the list being iterated instead of the output list, with a mismatched item size. The space-separated forms were unaffected.- As a result of the above, list and table flags given in the comma form parsed as empty.
-
New
Text.nearest(candidates, max_distance=0.6)method: the closest ofcandidatesto a text, ornonewhen nothing is close enough.max_distanceis an edit distance per grapheme of the shorter text, so short words have to match more exactly than long ones, since a flat distance can't telltrasnpil(a typo fortranspile) fromcat(not a typo forfmt), since both are 2 edits away. This is the "Did you mean ...?" logic that compiler errors already used, now available to Tomo code and shared with CLI parsing. -
tomo fmtis nowtomo format.fmtremains as an alias, so nothing that already invokes it has to change. CLI commands can declare one alias each; an alias is accepted wherever the command name is, is shown under the command's own--help, and can be suggested for a typo, but it doesn't take up a line of its own in the list of commands. -
An unrecognized subcommand now suggests the closest match, in both generated programs and
tomoitself, alongside the full list of available commands. -
Fixed three bugs in
Text.distance(). They only ever ran in compiler error paths just before exit, so the damage went unnoticed until CLI parsing started using it:- A heap buffer overflow: it allocated its distance matrix at
sizeof(uint32_t)per entry but storeddoubles in it, writing twice past the end of the allocation and corrupting neighbouring heap objects. - The matrix was indexed with a row stride of
b.lengthwhen each row holdsb.length + 1columns, so every row overlapped the next one's first cell and the results were wrong:"flaw".distance("lawn")returned 3 instead of 2, and"abcd".distance("bcda")returned 3 instead of 2. - The transposition check compared
b[i-2]instead ofb[j-2], so transpositions were detected at the wrong offset whenever the two texts were different lengths.
- A heap buffer overflow: it allocated its distance matrix at
-
Breaking:
Text.distance()no longer takes alanguageargument, and its half-cost discount for two graphemes that differ only in case is now ASCII-only.Bool.parse()likewise now does its own ASCII comparison, so it acceptstrue,TRUE, andTruebut no longer scrambled casings liketRuE. Both previously went through libunistring's case folding, which linked its case-mapping tables into every compiled program, sinceText.nearest()andBool's metamethods keep them reachable. A non-ASCII case mismatch now scores as a full substitution instead of half, which nudges a suggestion's ranking rather than breaking it. -
Command-line help, usage, and error messages now respect
NO_COLORandCOLOR=0, and drop their color when output isn't a terminal. The escapes were previously unconditional, so piping--helpinto a file or pager (or settingNO_COLOR) still got them. This coverstomoand compiled programs alike, andprint_err()no longer hardcodes its red highlighting either. The CLI's escapes now all come from one palette (tomo_cli_style()), which returns empty strings when color is off, the same approachreport_style()already used fortomo test/tomo formatoutput. -
The
tomocompiler now runs the sametomo_init()startup that every compiled Tomo program runs, instead of an inlined partial copy of it. Along with removing the duplicated color and hash-key setup, this means the compiler finally gets four things the copy was missing: GMP allocating through the GC (its bigint allocations were never freed),setlocale(), fatal-signal handlers that turn a compiler crash into a stack trace, andatexitcleanup. -
Fixed an argument-parsing error message that colorized its output unconditionally (the "Unsupported type" path for an argument type the parser doesn't handle).
-
The automatic
--help/--versionshort aliases (-h,-v) are no longer claimed when the command or a global flag already declares that letter, so a program whosemain()takesverbose|vstill gets-vfor its own flag. They're popped before the command's own arguments are parsed, so without this they would shadow it. -
tomo --versionnow works; previously onlytomo versiondid. -
A mistyped
tomocommand (tomo bulid foo.tm) now reports the bad command name and lists the available commands. Previously the whole command line was shimmed totomo run, so it failed with a confusing "path not found". More generally, when a command has subcommands and the first word resembles one of them, that's now mentioned in any argument-parsing error for that command. The resemblance alone never blocks anything, since it's only reported once parsing has failed on its own, so an argument that happens to look like a command name still works (tomo testsruns a directory calledtests, one edit away from thetestcommand). -
Command-line dispatch and help rendering are now one implementation, shared by the
tomocompiler and the programs it compiles. Previously the two had only argument parsing in common:tomodispatched at runtime from acli_spec_t, while generated programs got a separate tree of dispatch functions and help text baked in by the compiler, and the two had already drifted apart in what their help looked like. Commands now nest (cli_command_thas children), a program'scli_spec_thas a root command whose own arguments run when the first word isn't a subcommand, and the compiler emits a staticcli_spec_tinstead of code. Two behaviours that genuinely differ between the two are now explicit flags oncli_spec_t:strict_positionals(whether only args marked positional can be filled positionally) andpassthrough_after_double_dash(whether--separates relayed arguments, as intomo run prog.tm -- args..., or just marks the end of flags). As a result, generated programs'--helpnow uses the same layout astomo's, withArguments:/Flags:/Commands:sections. -
Negative numbers can now be passed as command-line arguments. A dashed token is read as a value rather than a flag when the argument it fills is numeric, so
--count -1,--count=-1,-c-1, and a bare positional-1all work, as do lists and tables of numbers (--nums -1 2 -3). Every one of these was previouslyNot a valid flag: -1, and a negative number could only be given after a bare--. The rule follows the argument's type rather than the token, so aTextargument given-1is still a usage error (\-1or--passes it), and a numeric argument given-xstill is too. What counts as a number is whatever the argument's own type parses, so-0x10,-0o644,-1e5, and-inf(forFloat64/Float32) all work; a number out of range for its argument reports that instead of being rejected as a flag. A negative number is never read as a cluster of short flags, so a flag whose letter appears inside one (-eand-1e5,-xand-0x1f) no longer claims it. -
Optional lists, tables, sets, and booleans work as command-line arguments. Passing a value for one (
--nums 1 2,--defs a:1,--tags a b,--force yes) failed withUnsupported type, even though the generated help advertised the argument; only--flag noneworked. An optional boolean is now the toggle its help text already claimed it was:--force,--no-force, and--force=yes|no, with--force=nonefor the third value. Where previously the bare flag reportedNo value providedand--force nonesilently set it tonorather thannone. An optional list or table now also splits--nums=1,2on commas and stops at a flag (--nums --othergives an empty list) exactly like a non-optional one; the two disagreed on both. -
Added
make test-cli, a C-level test suite for the argument parser that the compiler and the programs it compiles share (src/stdlib/cli.c). It covers the different ways to pass flags, the supported argument types, positional filling,--, the generated usage and help text, command dispatch, and the parse errors; it runs as part ofmake test.
2026-08-21
tomo run,tomo eval, andtomo testnow compile at-O0instead of-O1. Clang's-O1runs nearly the whole optimization pipeline, so it cost as much to compile as-O3while these commands throw the executable away after a single run. Generated code leans on precompiled library routines, so the runtime penalty is small (measured at ~2% on an integer-heavy loop). As before,-O0builds are instrumented with UBSan in trap mode, so undefined behaviour, including in hand-writtenC_code, now aborts rather than misbehaving silently on these paths. Pass-O1explicitly to get the old behaviour.- Compiling a Tomo file now preloads a precompiled
tomo.h, cached per toolchain configuration under the XDG cache directory and rebuilt whenever the installed headers change. This cuts the fixed per-file cost of parsing the ~40 standard library headers out of every compile. SetTOMO_NO_PCH=1to turn it off. Together with the-O0change,tomo testover the compiler's own test suite went from 2.0s to 1.1s. - Fixed a
GC_MALLOCmacro redefinition betweentomo/util.hand the bundledgc.hthat made compiling against the installed headers fail under-Werror. It was usually masked by the Zig toolchain's compilation cache returning an object built before the clash appeared. - Breaking: serialization and deserialization now have dedicated syntax,
and the implicit conversions to and from
[Byte]are gone. Useserialize(value)instead ofbytes : [Byte] = value, anddeserialize:T(bytes)instead ofvalue : T = bytes. Because the result type depends on the type written after the colon,deserialize:T(...)is a construct rather than a function call: the type has to be typed out literally there.deserialize:T(...)returns aT?instead of aborting the program on bad input. It gives backnonewhen the bytes aren't a well-formed encoding ofT: truncated data, a nonsensical length or enum tag, or leftover trailing bytes. Since the encoding doesn't record which type it came from, a well-formed encoding of some other type can still deserialize successfully into a nonsensical value, but corrupt or hostile input can no longer read out of bounds or trigger an enormous allocation.- Tomo has no nested optionals, so
deserialize:T?(...)is still just aT?, wherenonemeans either "this didn't decode" or "this decoded anone". Serialize a list if you need to tell those apart. - Serializing a type that has no byte representation (anything containing a function, closure, or type object) is now a compile-time error instead of a runtime failure.
serializeanddeserializeare soft keywords:serializeis only this construct when it's directly followed by(, anddeserializeonly when it's directly followed by:, so both remain usable as ordinary identifiers elsewhere. A function or closure namedserializewould be unreachable throughserialize(...), so that's a compile error rather than a silent shadowing;deserializeis unaffected, sincedeserialize:T(...)can't be confused with a call.
- Fixed a buffer overflow in
CStringdeserialization, which wrote its terminating NUL one byte past the end of its allocation. - Fixed
Int16serialization and deserialization, which always failed because they comparedfwrite()/fread()'s return value (an item count) against a byte count. - Deserializing a
Boolnow rejects any byte other than 0 or 1. A2would previously decode into the in-memory representation of anoneBool?. - Deserializing a
Textnow rejects byte sequences that aren't valid UTF-8 (or that contain a NUL). Such aTextwas previously constructed in an invalid state, and using it later aborted the program. - Fixed a compiler segfault when reading a field through an optional value
(e.g.
p.next.namewherenextis a@Foo?). Generating the "did you mean?" suggestion dereferenced a null namespace, and the compiler died with no diagnostic at all. Suggestions now look through the optional, and the error explains that the value needs a!rather than claiming the field doesn't exist.
2026-08-20
- Removed the special restrictions on struct/enum fields (and function
arguments) whose names start with an underscore. Previously, such fields
couldn't be read from outside the type definition (
x._foo) and couldn't be passed positionally to a constructor or function call, only by keyword. None of that applies anymore; a leading underscore is just a regular, unrestricted identifier character.
2026-08-19
- Breaking: structs and enum variants are now built with curly braces
instead of parentheses, so building a record is syntactically distinct from
calling a function. Struct definitions take a braced field list (
struct Foo{x,y:Int}), enum variants take braced fields while the variant list keeps its parens (enum Baz(A{foo:Foo}, B)), and construction and pattern matching both use braces (Foo{1,2},Baz.A{f},when b is A{foo}). Parentheses on a type name now mean only "call a constructor function", so conversions likeInt("5")andText(5)are unchanged, whileFoo(1,2)on a struct or variant is a compile error pointing at the brace form. A variant with no fields is still written bare (Baz.B,is B). Struct and enum values also convert to text using braces now (Foo{x=1, y=2}), so printed output round-trips as source.
2026-08-18
list.pairs()andtable.entries()used directly in for-position (a loop, comprehension, reducer, or lockstep clause) now compile to inline index loops instead of allocating an iterator closure and calling it indirectly once per iteration. This is a semantics-preserving optimization: behavior, including snapshot semantics, is identical to iterating the closure, but a pairs-heavy loop measured ~6x faster. Storing the iterator in a variable first (p := xs.pairs(); for a, b in p) still uses the closure, as it must.- New
table.entries()andlist.pairs()iterator methods, and direct table iteration is removed.t.entries()yields each key/value pair (for k, v in t.entries()) andxs.pairs()yields each unordered pair of distinct elements once (i < j, e.g.(+: a.dist(b) for a, b in points.pairs()) or 0.0). Both are multi-value out-parameter iterators, so they compose withatcounters,_discards, lockstep iteration, comprehensions, and reducers, and both have snapshot semantics (mutating the container after making the iterator doesn't affect what it yields).for k, v in tis now a compile error pointing at.entries()(or.keys/.values), so loop variables always bind exactly what the iterable yields, and there's no more special case where a table yields a different number of values than everything else. Sets still iterate their elements directly (for x in a_set), which stays unambiguous at one value per iteration. - New lockstep iteration:
for x, y in xs, ysiterates over several iterables at once, advancing all of them together and ending as soon as any one runs out. Each iterable's yielded values bind to its slice of the loop variables in order, and the total arity must match exactly (a mismatch is a compile error naming each iterable's yield count). Any iterable kind can participate: lists, counts, ranges, text, tables (key + value), and iterator functions (including multi-value out-parameter iterators, e.g.for k1, v1, k2, v2 in entries1, entries2)._discards a value,atbinds anInt64iteration counter, andskip/stop/else, comprehensions, and reducers all work as usual (e.g. the dot product(+: x*y for x, y in xs, ys) or 0). Iterables are evaluated once each, left to right, before the loop begins. In container literals the comma binds to the comprehension's iterables, so[x for x in xs, 99]is now an arity error; put plain items before the comprehension instead. - Fixed: the code formatter dropped the closing parenthesis of reduction
expressions (
(+: x for x in xs)formatted as(+: x for x in xs). - Fixed: a reducer inside a lambda didn't capture the variables in its
iterable expression (
func(-> Int) return (+: x*2 for x in xs) or 0failed to compile with an undeclared-identifier error in the generated C). - New in-place list iteration:
for &x in xs(andfor &x at i in xs) yields a live&reference to each element of a mutable list, so element updates are direct in-place stores with no per-element bounds checks or copy-on-write guards. The list stays usable inside the body (reads and indexed writes see live data); resizing the list or making a copy of it while the loop runs is a clean runtime failure, checked every iteration and once after the loop, so a violation in the final iteration is still caught. The reference is a non-escaping&pointer, so it can't outlive its iteration. - New
atloop clause:for x at i in xsbindsias a nativeInt64iteration counter (1, 2, 3, ...) alongside the yielded valuex. This replaces the old leading-index form (for i, x in xsis now a compile error suggestingat): loop variables always bind exactly what the iterable yields, so a loop's meaning never depends on the type of the iterable. Works uniformly across lists, text, integer counts, ranges (for x at step in a.to(b)),onward(), and iterator functions, where the last four previously had no index support at all, and in comprehensions and reducers (e.g.[i*x for x at i in xs]). Table iteration keeps itsfor k, vkey/value meaning, withatavailable there too. - New multi-value iterator protocol: a function whose arguments are all
non-escaping
&out-parameters and whose return type isBoolyields one value per argument on each call (write the values through the out-parameters and returnyes, or returnnoto finish). Loops bind exactly as many variables as the iterator yields (for a, b in pairs_fn; a mismatch is a compile error), the loop variables themselves are the storage the iterator writes into, the yielded types may differ, and comprehensions/reducers work the same way (e.g.(+: a.dist(b) for a, b in pairs_fn) or 0.0). The existing one-valuefunc(->T?)iterator protocol is unchanged. for x in nnow works whennis a native int type (Int64,Int32, ...), compiling to a native counting loop.- Fixed:
stopinside a table or text iteration loop generated invalid C (agototo a label that was never emitted) and failed to compile. - Fixed: a lambda that only wrote through a
&argument (a[] = ...) was wrongly rejected by the enclosing function's unused-variable check. - Debug builds (
-O 0) now run with UBSan enabled in trap mode: undefined behavior, including in user-writtenC_code, crashes with an ILLEGAL INSTRUCTION report instead of silently misbehaving. (Previously-O 0failed to link outright:zig ccenables UBSan at -O0 but the -nostdlib link has no UBSan runtime; trap mode needs none.) To make generated code UBSan-clean, lambdas now receive their closure userdata as avoid *and cast internally (so closure calls no longer go through a mismatched function-pointer type), function-to-closure promotion goes through a per-function-type shim, and the tagged small-int fast paths shift throughuint64_twith explicit bounds on shift amounts instead of relying on formally-undefined shifts. Optimized builds are unchanged (no instrumentation). - Fixed: text iteration with an iteration counter (now
for c at i in some_text) generated invalid C and failed to compile. The counter is 1-based, matching text cluster indexing. - Optional
Numchecks (x!,orfallbacks) now compile to an inline NaN test instead of an out-of-line libc call, which also unblocks register allocation around unwraps in hot loops (~30% faster on the n-body benchmark's inner loop).
2026-08-16
- Reworked install/uninstall around programs instead of packages. The
tomo installcommand is gone; install a program withtomo build --install, which compiles it and copies the executable (and its generated manpage) into the prefix'sbin/andman/man1/. Installing package directories intolib/is no longer supported (tomo packagestill builds package archives).tomo build --installtakes--prefix <dir>to install somewhere other than this Tomo installation's prefix, asks before overwriting existing files (skip with--yes/-y), and warns if the targetbin/isn't on your$PATH. tomo uninstall-selfis gone;tomo uninstallwith no arguments now does the whole-installation removal (with-y/--yes). With arguments,tomo uninstall <name-or-path>removes an installed program: it deletes the binary only if it is a Tomo program (recognized by its embedded build info) and deletes the matchingman/man1/<name>.1only if it is a Tomo-generated manpage (now marked with a.\" Generated by Tomocomment). A missing binary or manpage is a warning, not an error.make uninstalldelegates totomo uninstall --yes.- Tomo no longer shells out to
sudo/doasto write into a prefix it doesn't own. Commands that write to the prefix now fail with a message asking you to re-run with the permissions you need (for example undersudo).
2026-08-13
-
Reworked logging verbosity. The global
--verbose/-vand--quiet/-qflags are gone; each command now has its own--verbose/-vflag (and commands that print progress by default (build,package,install) also have--quiet/-q). Logs are grouped into categories ([build],[skip],[cmd]) tracked by a bitmask;--verboseenables all of them. Run-style commands (tomo file.tm,run,eval) are silent by default, sotomo run -v file.tmis now how you see the compiler's progress (previouslytomo -v file.tm). -
Added a
tomo eval '<expr>'command that evaluates a Tomo expression and prints its result, with syntax coloring when standard output is a terminal. The argument may be several statements separated by newlines or;, so compound inputs liketomo eval 'use random; random.int(1, 100)'work; the value of the final statement is what gets printed.
2026-08-10
-
The install layout is now fully versioned: everything under the prefix lives inside a
tomo@<version>directory per subdirectory, so multiple Tomo versions can coexist cleanly. Tomo's stdlib headers moved frominclude/tomo@VER/*.htoinclude/tomo@VER/tomo/*.h(with the umbrellatomo.hatinclude/tomo@VER/tomo.h), the vendored headers (gc.h,gmp.h, libunistring's) moved from the sharedinclude/intoinclude/tomo@VER/, andlib/libtomo@VER.ais nowlib/tomo@VER/libtomo.a. Compiled programs get-I PREFIX/include/tomo@VERand generated code now says#include <tomo.h>instead of#include <tomo@VER/tomo.h>. Tomo's man pages now live inman/tomo@VER/man{1,3}/with symlinks at the man-visible paths (likebin/tomo→bin/tomo@VER).make uninstallnow also removes the bundled toolchain inlibexec/tomo@VERand the man pages (both previously leaked), without touching other versions' pages or those oftomo installed programs. -
The CLI has been restructured from mode flags into git-style subcommands:
tomo run,tomo build(replacing--compile-exe/-e; takes a single file, with-oto name the output;--compile-obj/-cis gone),tomo transpile,tomo parse,tomo fmt(with-ireplacing--format-inplace),tomo package(with-oto name the archive),tomo install/tomo uninstall,tomo vendor(with-ereplacing--vendor-editable), andtomo info(with-xreplacing--extract-source). Baretomo file.tm [-- args]still runs a file, and baretomostill opens the scratch runner. Global flags like--verboseand-Oare valid anywhere on the command line; command-specific flags only after their command. Usage and help text (both per-command and top-level) are autogenerated from the argument specs. The old mode flags (and the unimplemented--changelog) have been removed,--version/-Vis nowtomo version,--prefixis gone, and program arguments are now only passed after--(the--argsflag is gone). -
The bundled Zig toolchain's global cache (its libc/compiler-rt builds, which can reach several GB) now lives inside Tomo's own cache directory (
~/.cache/tomo/zig) instead of zig's default~/.cache/zig, so it never mingles with a user-run zig's cache andtomo uninstall-selfcan clear it. An explicit$ZIG_GLOBAL_CACHE_DIRin the environment is still respected, and the variable is stripped from the environment of programstomo runexecutes (unless it was explicitly set), so a user-run zig inside a Tomo program keeps using its own cache. -
The bundled Zig toolchain is now shared between coresident Tomo versions: the real copy lives in
libexec/zig@<zig version>and each Tomo version'slibexec/tomo@VER/zigis a symlink into it, so upgrading Tomo without a zig pin change adds ~10MB instead of ~400MB.tomo uninstall-selfremoves toolchain stores that no remaining installation references. -
New
tomo uninstall-selfcommand: uninstalling is now the compiler's job instead of amake uninstallfile list (the Makefile target now just delegates to it). It removes this version's files from the prefix along with its per-user state and cross-compilation target packs; if other Tomo versions are coresident in the prefix, thetomoand man page symlinks are repointed to the newest remaining one (a smooth downgrade), and they are only removed when no other version remains. If notomois left anywhere on$PATHafterwards, the download cache (~/.cache/tomo) is cleared too. The scratch/stdin runner state now also respects$XDG_STATE_HOME. -
New
tomo vendor -u <package>(--unvendor) flag: the inverse of vendoring. It restores the package's./packages.inientry to its first non-vendored fallback source (or the compiler's default pin), re-installs the package into the project's store (re-pinning the digest if editable vendoring dropped it), and deletes the vendored copy. -
tomo transpilenow prints the generated C to stdout, formatted withclang-formatand syntax-highlighted withbatwhen those are installed (--rawdisables both). The--show-codegen/-Cflag has been removed. -
New
--target <platform>flag: cross-compile executables, objects, and packages for any supported platform (e.g.tomo --target aarch64-macos -e foo.tm). The target platform's libraries are installed on demand (with confirmation, or unprompted with--install-target) into the XDG data directory by downloading that platform's distribution archive. Cross-compiled artifacts live in.build/<platform>/and executables are suffixed with the platform name, so they never collide with native builds. -
The Tomo compiler is now built as a fully static executable using
zig ccwith musl libc. The vendored dependencies (Boehm GC, GMP, libunistring) are also built statically with the same toolchain. Building now requiresziginstead of the system C compiler and the system GC/GMP/unistring dev packages. -
A pinned Zig toolchain is now bundled into the Tomo installation (under
libexec/tomo@VERSION/zig/), and the installedtomouses it to compile programs. Tomo installations are therefore self-contained: no system C compiler is needed to build or run Tomo programs. Programs compiled by tomo are themselves fully static musl binaries. -
makebuilds for the current platform;make distbuilds a distribution archive (.tar.xz, extractable directly into an install prefix) for each platform in the matrix: Linux (x86_64, aarch64, riscv64, powerpc64le, s390x), macOS (x86_64, aarch64), and the BSDs (FreeBSD/NetBSD/OpenBSD, x86_64 + aarch64). 32-bit targets are excluded (Tomo requires 64-bit), as is loongarch64 (unsupported by the vendored Boehm GC). Linux targets are fully static musl; macOS and the BSDs link libc dynamically but bundle the vendored libraries statically. Cross-compiling needs only the host Zig, with no target SDK/sysroot. Zig archives are downloaded from ziglang.org and verified against pinned SHA-256 checksums (vendor/zig-checksums.mk);make -C vendor download-all-zigmirrors every platform's Zig archive. -
Runtime stack traces are symbolized in-process using a vendored libbacktrace and collected with the compiler's unwinder on every platform, so they work in fully static binaries (including inlined frames) and need no external tools.
-
Removed the OpenSSL (
-lcrypto) dependency: package digests now use a small built-in SHA-256 implementation.
2026-07-01
- Smoothed over some rough edges between CString and Text, making it easier to convert to and fro. CStrings are now a lot more like Text in string interpolations, debug visualization, etc.
Text(path)now works as well.
2026-06-30
- Tweaked parsing for path literals to avoid problems with delimiters.
- Optional booleans can no longer be automatically promoted to booleans (to avoid confusion).
2026-06-29
- Support piping programs into
tomolike this:echo 'func main() say("HI")' | tomo - Brought back top-level executable code (outside of
main()function). - Improved parser error messages for common mistakes like extra colons.
2026-06-28
- Added support for iterating over grapheme clusters in text
- Deprecated
--run/-rflag
2026-05-08
- Various linker flag fixes.
- Fixed some C string bugs.
- Improved code autoformatter.
- Removed usage of
stringsandawkin favor of inline code. - Improved build info.
2026-04-27
- Add build metadata and version information, which can be retrieved with
tomo -b. - Change interface for
List.binary_search()to be more flexible.
2026-03-16
- Added
Path.copy_to(src, dest)
2026-03-14
- Added
Path.each_child()andPath.walk()for iterating over files without the need to allocate a list of every iterated file. - Added
Text.matches_glob(glob)andPath.matches_glob(glob) - Overhaul to package system (previously: modules)
2026-02-08
- Path syntax no longer requires parentheses, any value starting with
.,/, or~is treated as a path. - Changed
Pathimplementation to use C-style strings instead of anenumwith array components. - Added
Text.distance(a,b)for calculating text distances. List.random()now returns an optional value, which isnonewhen the list is empty, instead of failing.- Improved error messages for misspelled variables and field/method names.
2025-12-31
- Added support for
123.foo()parsing the same as(123).foo() - Changed
is_between()to be bidirectional so5.is_between(10, 1) == yes
2025-12-23.2
- Fixes for OpenBSD and Mac.
2025-12-23
- Improved C preprocessing performance by eliminating expensive macro calls.
2025-12-22
- Use static linking instead of dynamic linking for the Tomo standard library as well as for user libraries. This produces binaries that do not depend on having Tomo and the library installed at runtime.
- Added
Path.writer()andPath.byte_writer()for multiple successive writes
2025-12-21.6
- Add smarter default behavior if run without any args (REPL-like script runner)
2025-12-21.5
- Various fixes for versioning and builds.
2025-12-21.4
- Version bump and deprecated
--changelogflag
2025-12-21.3
- Version bump
2025-12-21.2
- Update build process
2025-12-21
- You can now discard empty struct values.
- For an enum
Foo(A,B,C), the syntaxf!now desugars tof.A!using the first tag defined in the enum. - Error messages are more helpful for
foo.Whatever!enum field accessing. - Simplified logic for enums so there is less difference between enums that have tags with member fields and those without.
- Rename
Empty()toPresent()for set-like tables. - Paths are now an
enum Path(AbsolutePath(components:[Text]), RelativePath(components:[Text]), HomePath(components:[Text])) - Added
enum Result(Success, Failure(message:Text))type for indicating success or failure. - Some path methods now use
Resultreturn types instead of failing:Path.append()Path.append_bytes()Path.create_directory()Path.remove()Path.set_owner()Path.write()Path.write_bytes()
Path.parent()returnsnoneif path is(/)(file root)- Added check for unused variables.
2025-11-30
API changes
- Added
baseparameter to variousInt.parse()methods to allow explicitly setting the numeric base from 1-36.
Bugfixes
- Fixed various issues around parsing integers.
v2025-11-29.2
Bugfixes
- Fix for undefined behavior on enums and structs with padding.
2025-11-29
Syntax changes
- Syntax for tables has changed to use colons (
{k: v}) instead of equals ({k=v}). - Syntax for text literals and inline C code has been simplified.
- Added metadata format instead of
_HELP/_USAGE:HELP: "Help text" USAGE: "Usage text" MANPAGE_SYNOPSYS: "Synopsys..." MANPAGE_DESCRIPTION: (./description.txt) - Deprecated:
externkeyword for declaring external symbols from C.- Use
C_codeinstead.
- Use
- Deprecated: postfix
?to make values optional.- Explicitly optional values can be declared as
my_var : T? = value.
- Explicitly optional values can be declared as
- Deprecated:
>> ... = ...form of doctests. They are now called "debug logs" and you can specify multiple values:>> a, b, c - Deprecated:
extendblocks - Deprecated:
deserializeoperation and.serialized()method call- Instead, convert to and from
[Byte]
- Instead, convert to and from
Versioning and library changes
- Tomo versioning now uses dates instead of semantic versioning.
- Tomo libraries are now installed to
$TOMO_PATH/lib/tomo@TOMO_VERSION/library@LIBRARY_VERSIONinstead of$TOMO_PATH/share/tomo_TOMO_VERSION/installed/module_LIBRARY_VERSION - Core libraries are no longer shipped with the compiler, they have moved to separate repositories.
- Library installation has been cleaned up a bit.
Type Changes
- List indexing now gives an optional value.
- Added support for inline anonymous enums
- Accessing a field on an enum now gives an optional value instead of a boolean.
- Deprecated: Sets are no longer a separate type with separate methods.
- Instead of sets, use tables with a value type of
{KeyType:Empty}. - As a shorthand, you can use
{a,b,c}instead of{a:Empty(), b:Empty(), c:Empty()}and the type annotation{K}as shorthand for{K:Empty}.
- Instead of sets, use tables with a value type of
- Added
Emptyfor a built-in empty struct type andEMPTYfor an instance of the empty struct. - Struct fields that start with underscores can be accessed again and function arguments that start with underscore can be passed (but only as keyword arguments).
API changes
- Added
Path.lines(). - Added
Text.find(text, target, start=1). - Added
at_cleanup()to register cleanup functions. - Added
recursiveargument toPath.create_directory()to create parent directories if needed. setenv()now takes an optional parameter for value, which allows for unsetting environment values.- Tables now have
and,or,xor, and-(minus) metamethods. - Added
table.with(other),table.without(other),table.intersection(other), andtable.difference(other). - Changed
list.unique()to return a table withEmpty()values for each unique list item. - Added a
--formatflag to thetomobinary that autoformats your code (currently unstable, do not rely on it just yet). - Standardized text methods for Unicode encodings:
Text.from_utf8()/Text.utf8()Text.from_utf16()/Text.utf16()Text.from_utf32()/Text.utf32()
Bug fixes
Int.parse()had a memory bug.- Breaking out of a
for line in file.by_line()!loop would leak file handle resources, which could lead to exhausting the number of open file handles. When that happens, the standard library now forces a GC collection to clean up resources, which can result in file handles being freed up. &references failed to propagate when accessing fields likefoo.baz.method()whenfoois a&Fooandbaz.method()takes a&Baz.- Optional paths no longer fail to compile when you check them for
none. - Text replacement no longer infinitely loops when given an empty text to replace.
- Short CLI flag aliases now no longer use the first letter of the argument.
- Stack memory was not correctly detected in some cases, leading to potential memory errors.
Other changes
- Added automatic manpage generation.
- Major improvements to robustness of CLI argument parsing.
v0.3
- Added a versioning system based on
CHANGES.mdfiles andmodules.iniconfiguration for module aliases. - When attempting to run a program with a module that is not installed, Tomo can prompt the user to automatically install it.
- Programs can use
--versionas a CLI flag to print a Tomo program's version number and exit. - Significant improvements to type inference to allow more expressions to be
compiled into known types in a less verbose manner. For example:
enum NumberOrText(Number(n:Num), SomeText(text:Text)) func needs_number_or_text(n:NumberOrText) >> n func main() needs_number_or_text(123) needs_number_or_text(123.5) needs_number_or_text("Hello") - Added
tomo --prefixto print the Tomo install prefix. - Sets now support infix operations for
and,or,xor, and-. - Added new
jsonmodule for JSON parsing and encoding. - Added
Path.sibling(). - Added
Path.has_extension(). - Added
Table.with_fallback(). - Added
Int*.get_bit()andByte.get_bit(). - Added
Byte.parse()to parse bytes from text. - Added optional
remainderparameter toparse()methods, which (if non-none) receives the remaining text after the match. Ifnone, the match will fail unless it consumes the whole text. - Added optional
remainderparameter toText.starts_with()andText.ends_with()to allow you to get the rest of the text without two function calls. - Improved space efficiency of Text that contains non-ASCII codepoints.
- Doctests now use equality checking instead of converting to text.
- Fixed the following bugs:
- Negative integers weren't converting to text properly.
- Mutation of a collection during iteration was violating value semantics.
extendstatements weren't properly checking that the type name was valid.- Lazy recompilation wasn't happening when
use ./foo.cwas used for local C/assembly files or their#includes. - Memory offsets for enums with different member alignments were miscalculated.
- Optional types with trailing padding were not correctly being detected as
none - Tomo identifiers that happened to coincide with C keywords were not allowed.
- Compatibility issues caused compilation failure on some platforms.
v0.2
- Improved compatibility on different platforms.
- Switched to use a per-file unique ID suffix instead of renaming symbols after
compilation with
objcopy. - Installation process now sets user permissions as needed, which fixes an issue where a user not in the sudoers file couldn't install even to a local directory.
- Fixed some bugs with Table and Text hashing.
- Various other bugfixes and internal optimizations.
v0.1
First version to get a version number.