4. Your first change: add a builtin¶
This chapter is a guided walkthrough for adding a builtin: a function that looks like a normal call in Incan, but lowers/emits to a specific Rust pattern.
If you haven’t read it yet, start with: Extending the language
Decide: stdlib function vs compiler builtin¶
Use a stdlib function when the behavior can live entirely in runtime support code.
Use a compiler builtin when you need:
- special typing rules,
- special lowering/emission,
- or you want the surface syntax to stay “function-call-like” while generating nontrivial Rust.
End-to-end checklist (compiler builtin)¶
Before you start, sanity-check which layer your change belongs in (to avoid language/tooling drift):
Keep the pipeline aligned (to avoid language/tooling drift):
- Syntax crate (
loaves/kernel/incan_syntax/): lexer → parser → AST → diagnostics - Formatter (
loaves/compiler/incan_format/): prints AST back (idempotent; never emits invalid syntax) - Semantic core (
loaves/kernel/incan_lang/): canonical vocab / shared semantic helpers (avoid duplicating “meaning” in multiple layers) - Compiler (
loaves/compiler/):- typechecker (
incan_frontend/) validates and annotates - lowering (
incan_ir/) turns AST into IR - emission (
incan_emit/) generates correct Rust
- typechecker (
- Runtime/stdlib (
loaves/stdlib/<component>/{src,rust}/): behavior that can live outside the compiler should live here
Rule of thumb: prefer pushing shared meaning “down” into incan_lang/incan_syntax/the stdlib facets, and keep the driver (incan_driver/) and the incan command line (loaves/toolchain/incan-cli/) focused on orchestration and pipeline wiring.
What you will usually touch¶
Builtin changes typically span the compiler crates, so you will usually touch:
- Frontend symbol table (so it typechecks)
loaves/compiler/incan_frontend/src/symbols.rs→ builtin name + signature
- IR builtin enum (so lowering can represent it explicitly)
loaves/compiler/incan_ir/src/expr.rs→BuiltinFnvariant + name mapping
- Lowering (so calls become
BuiltinCall)loaves/compiler/incan_ir/src/lower/expr/
- Emission (so Rust output matches the intended pattern)
loaves/compiler/incan_emit/src/emit/expressions/builtins.rs
- Tests and docs
- add a regression test (parse/typecheck/codegen)
- add/adjust docs if it changes user-visible behavior
A suggested “first builtin” exercise¶
Pick a small builtin where you can clearly verify the generated Rust (and add a regression test):
- a builtin that maps to a single Rust stdlib call
- a builtin that needs a small helper function emitted
Keep it small: your goal is to learn the pipeline and leave the codebase in a better state.
Running your feedback loop¶
After implementing the builtin, validate it through the toolchain:
make pre-commitstill passes (fast local gate: fmt-check + cargo check)make pre-commit-fullpasses before pushing (fmt-check + tests + clippy)make smoke-teststill passes (end-to-end sanity check)- the LSP still parses/diagnoses edited files (no syntax drift)
Optionally, consider adding a tiny example showing the new builtin in use, to lock in the intended behavior. Note that examples are intended for user-visible behavior, not for internal implementation details.
Next¶
Next chapter: 05. Your first syntax change.