Your first program
Build a small command-line greeter in Python. You'll go from an empty project to code that has been generated, tested, and reviewed, and then you'll change it.
- 1
Describe the target
A target says what your definitions are built into. This one is a single Python script, run with uv, that people use from a terminal. Create
def/targets.lfy:def/targets.lfyuse "elfie/target"; use "cli"; use "platforms"; use "python" as Python; ace const hello: `A greeting on the command line` = targetOf({ output = "out/hello", language = Python.python, runtime = Python.cpython.layer("3.12"), platforms = [posix], ecosystem = Python.uv, frameworks = [Python.argparse], interfaces = [commandLine], layout = Python.uvScript.layer("hello.py"), }); global@targets.add(hello);Each slot takes a layer from a package.
pythonsays how Elfie declarations become Python,uvScriptsays the program is one file,hello.py, andcommandLinesays people run it from a terminal. The last line builds every definition in the project forhello. Compile targets covers each slot. - 2
Describe the program
Now say what the program does. Create
def/main.lfy:def/main.lfyuse "cli"; /// name: who to greet, as a positional argument /// shout: whether to greet in capitals fn greet(name?: string, shout: boolean = false) is command(""): `Greets someone by name` => number { @acceptanceCriteria .add({ behavior = `Prints one line: Hello, then [[name]], then an exclamation mark` }) .add({ situation = `[[name]] is left out`, behavior = `Prints Hello, world!` }) .add({ situation = `[[shout]] is true`, behavior = `The whole line is in capitals` }) .add({ behavior = `Returns 0` }); @test({ input = ["Ada"], expect = 0 }); }There is no function body. A
fnis a contract: its parameters, what it returns, the criteria it must meet, and the tests it must pass. The compiler writes the code.is command("")makesgreetthe program itself rather than a subcommand.- The
///lines describe the parameters.namebecomes a positional argument because its description says so. [[name]]and[[shout]]are references.elfie checkreports a reference that names nothing.- Each
.addis one acceptance criterion.situationsays when it applies.
- 3
Check it
Terminalelfie checkOutputdef/main.lfy: generation warning: hello main: no output has been generated for it 34 files, 0 problems, 1 stale unitsNo problems. The warning says
mainhasn't been built yet, which is expected. The file count includes the packages and the standard library. - 4
See the plan
Terminalelfie compile --dry-runOutputhello main fresh [] batch main [main]One unit,
main, is planned for the targethello, because it is fresh: it has never been built. A unit is one definition file compiled for one target.targets.lfygives no unit, because a target is evaluated at compile time and never generated. - 5
Compile
Terminalelfie compileFor each batch,
elfiewrites a request and runs your compiler with it. The compiler writesout/hello/hello.pyand its tests, then runs the target's commands:uv sync, the tests through pytest, andruffandpyrightfor lint. They must pass with no warnings. Thenelfiechecks that every entity of the unit is marked in the output, the verifier reviews each criterion, and the source maps are recorded underelfie-compile/maps/hello/.The generated code carries markers that tie each part back to its definition:
out/hello/hello.py# @lfy def/main.lfy:greet def greet(name: str | None = None, shout: bool = False) -> int: line = f"Hello, {name or 'world'}!" # @lfy def/main.lfy:greet#<criterion id> if shout: line = line.upper() print(line) return 0Illustrative. The code a compiler writes will differ in its details. - 6
Run it
Terminalcd out/hello uv run hello.py Ada uv run hello.py --shoutOutputHello, Ada! HELLO, WORLD! - 7
Change it
Change the greeting in the first criterion from Hello to Hi, and the second criterion to
Prints Hi, world!. Then check again:Terminalelfie check elfie compileelfie checkreportsmainas stale.elfie compilesends only that unit. Because the criteria kept their places and only their wording changed, the request marks them reworded, the existing markers move to the new criteria, and the verifier reviews those two criteria again. The verdicts for the criteria you didn't touch carry over.
What you used
| Construct | What it did |
|---|---|
ace const | Declared a target, evaluated while compiling. |
targetOf({ ... }) | Gave the record its Target type. |
global@targets.add | Built every definition for the target. |
fn | Declared behavior that the compiler implements. |
is command("") | Applied a trait from the cli package. |
@acceptanceCriteria.add | Added a criterion to the current entity. |
@test | Added a concrete case. |
Familiar concepts
Variables, types, control flow, and the few places Elfie differs from what you know.
Agentic structures
Write better d and fn declarations, and criteria the verifier can check.
Was this guide clear?
Share a suggestion