-
Notifications
You must be signed in to change notification settings - Fork 0
How To Write L0
I use this document as my canonical quick-start for writing valid L0 modules.
I always keep module sections in this exact order:
vertypesconstsexternglobalsfns
I always include every section, even when empty.
I keep numeric ids deterministic and contiguous by family:
- types:
t0,t1, ... - constants:
k0,k1, ... - globals:
g0,g1, ... - functions:
f0,f1, ... - blocks:
b0,b1, ... - SSA values:
v0,v1, ...
I write one instruction per line.
- value-producing:
vX = OP arg... : tY - non-value:
OP arg... - terminators:
br bK,cbr vX bT bF,ret vX,ret
I avoid expression nesting, implicit casts, and non-canonical spacing.
I use contiguous tN ids and I keep RHS tokens canonical.
- primitive integers:
i1,i8,i16,i32,i64,u8,u16,u32,u64 - pointer:
p0<i8> - struct:
s{tA,tB,...} - fixed array:
aN<tA>whereN > 0 - function type:
fn(tA,...)->tR
I keep every block terminated exactly once. I define values before I use them. I keep arg indices within function arity. I ensure every branch target block exists in the same function.
I keep runnable examples in docs/examples/:
- arithmetic:
01_arithmetic_add_wrap.l0 - compare:
02_compare_icmp_eq.l0 - control flow:
03_control_cbr_select.l0 - multi-block cfg:
15_cfg_branch_const_select.l0,16_cfg_merge_mem_select.l0 - spill/reload stress:
17_spill_stress_kernel.l0 - SysV ABI argument matrix:
18_sysv_abi_sum6_kernel.l0 - memory:
04_memory_roundtrip.l0,05_memory_gep_roundtrip.l0 - calls:
06_call_add_two_function.l0 - intrinsics:
07_intrinsic_malloc.l0,08_intrinsic_free.l0,09_intrinsic_write.l0,10_intrinsic_trace.l0,11_intrinsic_exit.l0 - type forms:
12_types_struct_sig.l0,13_types_array_sig.l0,14_types_fn_sig.l0
I verify them with:
for f in docs/examples/*.l0; do
./bin/l0c verify "$f"
doneI emit side artifacts while building:
./bin/l0c build docs/examples/10_intrinsic_trace.l0 /tmp/trace_example.l0img \
--debug-map /tmp/trace_example.map \
--trace-schema /tmp/trace_example.schemaI inspect those artifacts with:
./bin/l0c mapcat /tmp/trace_example.map
./bin/l0c schemacat /tmp/trace_example.schema
./bin/l0c tracecat /tmp/trace.bin
./bin/l0c tracejoin /tmp/trace.bin /tmp/trace_example.mapI use mapcat to inspect stable instruction ranges, tracecat to decode raw binary records, and tracejoin to map trace ids to instruction ranges.
Before I accept generated L0, I check:
- section order is exact and complete
- ids are canonical by family
- value lines use
vX = ... : tY - every block has exactly one terminator at the end
- no use-before-def for any
vN - branch targets and call targets exist
- declared and used types match
-
./bin/l0c verify <file>returnsok
I keep complete details in:
- language reference:
docs/LANGUAGE.md - full compiler/spec contract:
docs/SPEC.md - implementable token-level contract:
docs/IMPLEMENTABLE_SPEC.md
- How-To-Write-L0
- Language-Reference
- Instruction-Set
- CLI-and-Compiler-Spec
- Implementable-Spec
- Command-Reference
- Examples-Catalog
- LLM-Quick-Reference
- Opcode-Examples
- LLM-Doc-Index