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 (
crates/incan_syntax/): lexer → parser → AST → diagnostics - Formatter (
src/format/): prints AST back (idempotent; never emits invalid syntax) - Semantic core (
crates/incan_core/): canonical vocab / shared semantic helpers (avoid duplicating “meaning” in multiple layers) - Compiler (
src/frontend/,src/backend/):- typechecker validates and annotates
- lowering turns AST into IR
- emission generates correct Rust
- Runtime/stdlib (
crates/incan_stdlib/,stdlib/): behavior that can live outside the compiler should live here
Rule of thumb: prefer pushing shared meaning “down” into incan_core/incan_syntax/incan_stdlib, and keep the incan
(root) crate focused on orchestration and pipeline wiring.
What you will usually touch¶
Builtin changes typically land in the compiler (incan crate), so you will usually touch:
- Frontend symbol table (so it typechecks)
src/frontend/symbols.rs→ builtin name + signature
- IR builtin enum (so lowering can represent it explicitly)
src/backend/ir/expr.rs→BuiltinFnvariant + name mapping
- Lowering (so calls become
BuiltinCall)src/backend/ir/lower/expr.rs
- Emission (so Rust output matches the intended pattern)
src/backend/ir/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 (fmt + clippy + udeps + tests + release build)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.