Skip to content

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
  • 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:

  1. Frontend symbol table (so it typechecks)
    • loaves/compiler/incan_frontend/src/symbols.rs → builtin name + signature
  2. IR builtin enum (so lowering can represent it explicitly)
    • loaves/compiler/incan_ir/src/expr.rs → BuiltinFn variant + name mapping
  3. Lowering (so calls become BuiltinCall)
    • loaves/compiler/incan_ir/src/lower/expr/
  4. Emission (so Rust output matches the intended pattern)
    • loaves/compiler/incan_emit/src/emit/expressions/builtins.rs
  5. 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-commit still passes (fast local gate: fmt-check + cargo check)
  • make pre-commit-full passes before pushing (fmt-check + tests + clippy)
  • make smoke-test still 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.