LLuce

Hello, Luce

Luce comes as two programs. luce is the compiler: it turns .luc source into something you can run. loom is the terminal that runs it — an interactive shell, and a command line.

Getting the toolchain#

There is no installer and no package. You build it from the repository, with Zig 0.16 and LLVM:

git clone https://github.com/dymokomi/luciaos
cd luciaos
./build.sh

That writes build/luce, build/loom and build/lib/libluce_rt.a. LLVM is needed to build luce, because Luce's one code generator calls libLLVM in process — brew install llvm or apt install llvm-dev is enough, and zig build -Dllvm-config=/path/to/llvm-config points the build at an installation somewhere unusual. loom links no LLVM at all, so a machine that only runs Luce programs needs none.

The smallest program#

A program is a file with a function called main.

main.luc
func main():
    print("hello, loom")
Output
hello, loom

Blocks are introduced by : and marked by indentation, which is four spaces and nothing else — a tab or a three-space indent is a compile error naming the reason. There are no semicolons and no braces.

Compiling and running it#

Put this in hello.luc and hand it to the compiler.

hello.luc
func main():
    var name = "loom"
    if arg_count() > 0:
        name = arg(0)
    print("hello, " + name)
Shell
$ luce build hello.luc
hello.luc -> hello.lc
$ loom run hello.lc
hello, loom
$ loom run hello.lc world
hello, world

That transcript is real: this site's build ran those three commands in a scratch directory and captured what they printed.

luce build writes hello.lc, a portable module. loom run executes it — and the first time it does, it compiles a native artifact hello.lcn beside it and uses that from then on. You never ask for that; it just happens, and the second run starts in a couple of milliseconds.

While you are writing something, one command is shorter:

Shell
$ loom luce hello.luc there
hello, there

loom luce compiles and runs in one step, always as a debug build, so a mistake reports the line it happened on.

The compiler's three commands#

CommandWhat it does
luce build FILE [-o OUT] [--release] [--emit=WHAT]compile and write an artifact
luce check FILEcompile, report problems, write nothing
luce ir FILE [--full]compile and print the readable intermediate form

--emit chooses which artifact build writes — module (the default .lc), object (a relocatable .o), library (a native .lcn), or exe (a standalone binary that needs neither loom nor a runtime beside it). Nothing else differs between them; the same program walks the same pipeline either way.

Builds are debug by default, which means the artifact carries source locations so a runtime failure can say file:line:column. --release strips those and changes nothing else — in particular it does not turn off a single safety check. See the compiler and the terminal.

Errors, at compile time#

Luce is statically typed with inference, and it has no implicit conversions at all. Mistakes are caught before anything runs, and the message tries to name the fix rather than the parser's predicament:

main.luc
func main():
    let n: Int = 1
    let x: Float = 2.5
    print(str(n + x))
luce check — the program is refused
luce: compile failed
main.luc:4:15: operands are Int and Float (conversions are explicit) [luce.sema.type]
        print(str(n + x))
                  ^~~~~

Every diagnostic carries a stable code — luce.lex.*, luce.parse.*, luce.sema.* — and a byte span, so tools can act on them and you can search for them.

Next: what those types are.