Running programs
There are two ways to start a Luce program under loom, and they differ in exactly one thing: whether a compiler has to be involved.
| Form | What happens |
|---|---|
loom run FILE.lc [ARGS] | One dlopen, one
symbol lookup, one call |
loom luce FILE.luc [ARGS] | Compile if there is not already a current artifact, then the above |
loom FILE.lc [ARGS] | Shorthand for
run |
loom FILE.luc [ARGS] | Shorthand for
luce |
Arguments after the path are the program's, never loom's. They arrive
as main's args: list(string) parameter — handed to the
program rather than asked for by it, which is why the host gate does not cover
them and why the list is owned by main's scope and freed when it
returns.
A .lc is machine code
luce build writes a tagged shared library and calls it
.lc. It is not bytecode, not serialized IR, and not a script.
Nothing about running it is a translation step:
$ time loom run hello.lc
hello, loom
loom run hello.lc 0.00s user 0.00s system 0.002 totalTwo milliseconds, on the machine this was measured on, almost all of it the
operating system's process setup. loom deliberately links no LLVM: libLLVM is
164 MB, the dynamic loader binds all of it before main, and that
cost every single invocation — including the ones that compile nothing at all.
So the compiler links it and the environment does not.
Every artifact carries a tag
An artifact records four things besides the program: the machine it was built for, the host ABI version it was built against, the identity of the code generator that produced it, and a content hash of the program itself. loom reads that tag before it calls anything, and a file that does not match is refused by name:
| loom says | Meaning |
|---|---|
| it is not a compiled Luce artifact | The file is something else |
| its tag is a layout this loader cannot read | The tag format itself moved |
| it was built against a different host ABI | The published
LuceHost table changed under it |
| it was built for a different machine | Another architecture or another operating system |
| it was built by a different code generator | A rebuilt or different compiler produced it |
| the program it was built from has changed | The source moved; the cache entry is stale |
There is no fallback and nothing to fall back to. Refusing by
name is the whole of the answer: a .luc needs a compiler exactly as
a .c does, and an artifact that cannot be trusted is not run
slower, it is not run.
$ loom run bad.lcloom: cannot load bad.lc: it is not a compiled Luce program this machine can run
The platform loader says only "no", never why, so loom makes the one distinction worth making itself: a file that is not there and a file that is not a program are different mistakes, and only the second is about the program.
loom: cannot read nope.lc: no such file
Compiling a .luc, and where the artifact goes
loom luce FILE.luc compiles the file — and the sibling modules it
imports, resolved beside it — and runs the result. The artifact is written
beside the source, as FILE.lc: the same name
luce build FILE.luc would write, so a build can ship one and loom
simply finds it.
$ time loom luce hello.luc hello, loom loom luce hello.luc 0.05s user 0.03s system 0.140 total $ time loom luce hello.luc hello, loom loom luce hello.luc 0.00s user 0.00s system 0.003 total
The first run compiled and linked; the second found the artifact and called
it. A warm run invokes nothing external. A cold one runs
luce once, which runs the linker once.
The cache is keyed on content, not on time
The artifact carries a hash of the program it was built from, so a source file whose bytes changed gets a rebuild whatever the clock says about either file — and a source file that was touched but not changed does not. There is no timestamp comparison anywhere in this, and so no way for a restored backup, a copied tree or a clock skew to hand you the wrong program.
What loom hands the compiler is not the source file. It is the serialized module loom's own front end just produced from that source — the compiler's internal hand-over form — so the artifact is keyed to the exact program that is about to run rather than to whatever a second reading of the same text would have produced. That file is written beside the artifact and removed again, on the failing path as much as the succeeding one: it is a hand-over, never a deliverable, and one left behind in your directory after a failed build is litter you did not make.
When there is nowhere to write beside the program
A read-only directory, or a program with no path at all — the editor loom
carries inside itself — has nothing to sit beside. The artifact then goes to a
directory named luce-artifacts inside TMPDIR, keyed on
the hash because there is no name to key on.
That directory is created rwx------ and is used only if
what is really there is a directory with no group or other permissions at all; a
symbolic link where it should be is refused rather than followed. The reason is
not tidiness. A cached artifact is a file loom dlopens, and its name
is the program's content hash — a name anyone holding the same program can
compute. On a shared /tmp, that file can be planted before loom ever
looks, and the sticky bit does not help: it stops one user replacing
another's file and stops nobody from creating it first. A loom that recompiles
every run is slow; a loom that runs somebody else's code is not loom.
Nothing is ever swept from that directory, deliberately. The
entries are content-keyed, so a stale one is never loaded — it is refused by its
tag and rebuilt over the same name — and a sweep would need an age policy, a
lock, and a way to know that no other loom is about to open the file it is
deleting. Reaping a temp directory is the operating system's job. rm -rf
on it is always safe and costs one recompile.
When a program stops
A run ends in one of five ways, and loom says which in words and in the number a shell reads afterwards. The screen is restored before anything is reported, so a full-screen program that trapped leaves its message on the ordinary terminal rather than inside a frame that is being torn down.
A trap is a bug
A debug artifact — the default — carries source locations, so a trap reports
file:line:column and a call trace, innermost first.
func main():
let numbers = [1, 2, 3]
print(string(numbers[7]))loom: trap: index out of bounds [index_bounds]
at main (boom.luc:3:5)The code in brackets is stable and is the thing to match on. A trace prints at
most twelve innermost frames and counts the rest, so a runaway recursion shows
its shape rather than its length. --release strips the position
tables; the function names still print, and the program behaves identically —
safety is the language, never a build mode.
An uncaught error is news
A trap and a recoverable error that nobody handled are different sentences about a program, and they get different words and different numbers, so a script can tell them apart without parsing standard error. An error prints where it was raised and no stack: the stack is a trap's diagnosis, and an error's news is what the world said.
import std.files
func main() -> !:
let text = try files.read("missing.txt")
print(text)loom: error: cannot read missing.txt [io_failed]
raised in files.read (std/files.luc:26:5)A program may choose its own number
exit(status) is the fourth way a run can end, beside finishing,
trapping and raising: nothing is wrong and nothing failed, the program chose to
stop and chose the number. loom carries it out — on POSIX, as the low eight bits
of its own exit code.
func main():
print("checked and closing")
exit(7)checked and closing
The exit table
One table, because a program's behaviour must not depend on who started it —
and that includes the number a shell reads afterwards. loom run and a
standalone --emit=exe binary answer from the same source.
| Status | Meaning |
|---|---|
0 | The program finished |
1 | It trapped — and also loom's own refusals: no such file, not a program this machine can run, a compile that failed |
3 | It ended on an uncaught error |
70 | Out of memory: the machine ran out, not the program |
71 | The run could not be carried out, or its output could not be delivered |
| anything else | The program called exit with
it |
The last two are not about the program at all, which is why they take
sysexits numbers well clear of anything a program means. 71
covers a case that is easy to lose: output that could not be written did not
happen. A closed or full pipe loses the tail of a program's transcript, and a
runner that returned 0 would be claiming it arrived — so the failure
is said once and counted in the status.
When loom cannot compile
loom carries no code generator, so a cold loom luce needs the
luce binary. It is looked for beside loom's own executable first and
on PATH after — and only on a cold run, because looking for it is
work a warm run must not do. A loom that cannot find one says so, rather than
doing something slower and calling it success:
loom: cannot compile sums.luc: the `luce` compiler is not beside /opt/luce and not on PATH
A compile that failed prints the compiler's own diagnostics, and a program the back end has no lowering for is refused rather than retried somewhere else — the next directory would say the same thing.
What loom reads from the environment
| Variable | Effect |
|---|---|
PATH | Where luce is looked for, after the
directory loom's own binary sits in |
TMPDIR | Where an artifact goes when it cannot go
beside its program; /tmp when unset |
NO_COLOR | Set to any value and the shell prints no escape sequences |
LOOM_EDITOR | A .luc file to use in place
of the built-in editor |
Those are read once, at start, because they are process policy rather than
anything a program or a command can change. Everything else the compiler needs
(LUCE_CC to name a C compiler driver, LUCE_LIB the
directory holding libluce_rt.a) it reads from the environment it
inherits, so loom neither parses nor forwards it.