Python API¶
The fourier package exposes the compiler as a single function plus the underlying lex / parse / codegen entry points.
compile_source¶
from fourier import compile_source
def compile_source(source: str, name: str | None = None) -> bytes
The full pipeline: lex → parse → codegen → assemble. Returns the raw VM bytecode ready to deploy.
src = '''
contract Counter {
storage value: uint @ 0;
fn init() { value = 42; }
pub fn get() -> uint { return value; }
pub fn inc() { value = value + 1; }
}
'''
bytecode = compile_source(src)
print(len(bytecode), "bytes")
# 198 bytes (current counter; sizes shift slightly as codegen evolves)
Multi-contract sources¶
A source file may declare multiple top-level contract blocks. To pick which one compile_source returns, pass name=:
src = '''
contract Math { pub fn add(a: uint, b: uint) -> uint { return a + b; } }
contract String { pub fn len(s: uint) -> uint { return 0; } }
'''
bc_math = compile_source(src, name="Math")
bc_string = compile_source(src, name="String")
Without name=, a multi-contract source raises ValueError. For all bytecodes at once, use compile_source_all:
from fourier import compile_source_all
result = compile_source_all(src)
# {'Math': b'...', 'String': b'...'}
Raises:
LexErroron illegal tokensParseErroron malformed syntaxCompileErroron type / slot / unknown-identifier errors
See Error reference.
Lower-level entry points¶
For tools that need to inspect intermediate stages:
from fourier import tokenize, Token, TokenKind
from fourier import parse, ParseError
from fourier import compile_contract, CompileError
tokenize(source: str) -> list[Token]¶
Lex only. Returns the full token list plus a final Token(EOF, ...). Each Token has kind: TokenKind, value: object, line: int, col: int. See fourier/lexer.py for the full enum.
tokens = tokenize("contract X { storage y: uint @ 0; }")
for t in tokens:
print(t)
# Tok(CONTRACT @1:1)
# Tok(IDENT, 'X' @1:10)
# Tok(LBRACE @1:12)
# ...
parse(tokens) -> Contract | list[Contract]¶
Recursive-descent parser. Returns a Contract AST node for a single- contract source, or a list[Contract] for multi-contract sources — see fourier/ast_nodes.py for the dataclass shapes. The companion parse_all(tokens) -> list[Contract] always returns a list.
ast = parse(tokens)
print(ast.name) # 'X'
print(ast.storage) # [StorageDecl(name='y', ty=TypeRef(name='uint'), slot=0, ...)]
print(ast.functions) # []
compile_contract(contract: Contract) -> bytes¶
Lower the AST to bytecode. Internally calls vm.asm.assemble.
Combining stages¶
from fourier import tokenize, parse, compile_contract
def trace_compile(source: str) -> bytes:
tokens = tokenize(source)
print(f"tokenized to {len(tokens)} tokens")
ast = parse(tokens)
print(f"parsed contract: {ast.name} ({len(ast.functions)} fns)")
return compile_contract(ast)
This is what compile_source does, minus the prints.
Error handling¶
from fourier import compile_source
from fourier.lexer import LexError
from fourier.parser import ParseError
from fourier.codegen import CompileError
try:
bc = compile_source(src)
except LexError as e:
print(f"lex error at {e.line}:{e.col}: {e}")
except ParseError as e:
print(f"parse error: {e}")
except CompileError as e:
print(f"codegen error: {e}")
All three error classes carry the source position in their message (format: line:col: message).
Threading model¶
No global state. compile_source is pure (modulo int.from_bytes / hashlib calls), safe to call concurrently. Each invocation creates its own _ContractGen and _FnCtx objects.
Caching¶
The compiler does no caching. If you compile the same source twice you get the same bytecode twice; if you need a cache, build one on top of compile_source(src) → bytes. The result is deterministic for a given source string.
Embedding in a build tool¶
Example: a tiny make-style helper that recompiles changed .fou files:
import os, sys, time
from fourier import compile_source
SOURCES = ["counter.fou", "token.fou"]
OUTDIR = "build"
os.makedirs(OUTDIR, exist_ok=True)
for src_path in SOURCES:
out_path = os.path.join(OUTDIR, os.path.basename(src_path) + ".bin")
if (os.path.exists(out_path)
and os.path.getmtime(out_path) >= os.path.getmtime(src_path)):
continue
print(f"compiling {src_path}...")
with open(src_path) as f:
bc = compile_source(f.read())
with open(out_path, "wb") as f:
f.write(bc)
print(f" wrote {len(bc)} bytes → {out_path}")