| name | builtin-developer |
| description | Guides development of AILANG builtin functions. Use when user wants to add a builtin function, register new builtins, or understand the builtin system. Reduces development time from 7.5h to 2.5h (-67%). |
Builtin Developer
Add new builtin functions to AILANG using the modern registry system (M-DX1).
Quick Start
Development time: ~2.5 hours (down from 7.5h with legacy system)
Most common workflow:
- Register builtin with metadata (~30 min)
- Write hermetic tests (~1 hour)
- Validate and inspect (~30 min)
- Runtime auto-wires (already done!)
When to Use This Skill
Invoke this skill when:
- User wants to add a new builtin function
- User asks about builtin registration process
- User needs to understand the builtin system
- User wants to validate or inspect builtins
- User asks "how do I add a builtin?"
System Overview
Status: 🎉 M-DX1 COMPLETE (Oct 2025) - 52 builtins migrated, fully documented
Key Benefits:
- 67% faster: 2.5h instead of 7.5h
- 1 file instead of 4: Single-point registration
- 71% less code: Type Builder DSL reduces boilerplate
- Automatic wiring: Registry connects to runtime/link
- Hermetic tests: MockEffContext with HTTP/FS mocking
Components:
- Central Registry (
internal/builtins/spec.go) - Single registration point
- Type Builder DSL (
internal/types/builder.go) - Fluent type construction
- Test Harness (
internal/effects/testctx/) - Hermetic testing helpers
- CLI Commands - Validation and inspection tools
Available Scripts
scripts/validate_builtins.sh
Validate all builtins in the registry.
Usage:
.claude/skills/builtin-developer/scripts/validate_builtins.sh
What it checks:
- All builtins are registered
- Proper metadata structure
- Type signatures valid
- Test coverage exists
scripts/check_builtin_health.sh
Run ailang doctor and list commands.
Usage:
.claude/skills/builtin-developer/scripts/check_builtin_health.sh
Workflow
Step 1: Register the Builtin (~30 min)
Create or edit module file (e.g., internal/builtins/string.go):
func init() {
registerMyBuiltin()
}
func registerMyBuiltin() {
RegisterEffectBuiltin(BuiltinSpec{
Module: "std/string",
Name: "_str_reverse",
NumArgs: 1,
IsPure: true,
Type: makeReverseType,
Impl: strReverseImpl,
Metadata: &BuiltinMetadata{
Description: "Reverse a string (Unicode-aware)",
Params: []ParamDoc{
{Name: "s", Description: "String to reverse"},
},
Returns: "Reversed string",
Examples: []Example{
{Code: `_str_reverse("hello")`, Description: "Returns \"olleh\""},
},
Since: "v0.3.15",
Stability: StabilityStable,
Tags: []string{"string", "reverse", "unicode"},
Category: "string",
},
})
}
func makeReverseType() types.Type {
T := types.NewBuilder()
return T.Func(T.String()).Returns(T.String())
}
func strReverseImpl(ctx *effects.EffContext, args []eval.Value) (eval.Value, error) {
str := args[0].(*eval.StringValue).Value
runes := []rune(str)
for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
runes[i], runes[j] = runes[j], runes[i]
}
return &eval.StringValue{Value: string(runes)}, nil
}
Key points:
- Use
RegisterEffectBuiltin() in init()
- Use Type Builder DSL for type construction
- Include complete metadata (description, params, examples, tags)
- Set
IsPure: true for pure functions, or Effect: "IO" for effects
See: resources/type_builder_examples.md for type patterns
Step 2: Write Hermetic Tests (~1 hour)
Add tests in internal/builtins/register_test.go:
func TestStrReverse(t *testing.T) {
ctx := testctx.NewMockEffContext()
tests := []struct {
input string
expected string
}{
{"hello", "olleh"},
{"", ""},
{"🎉", "🎉"},
}
for _, tt := range tests {
result, err := strReverseImpl(ctx, []eval.Value{
testctx.MakeString(tt.input),
})
assert.NoError(t, err)
assert.Equal(t, tt.expected, testctx.GetString(result))
}
}
Test Harness helpers:
testctx.NewMockEffContext() - Create test context
testctx.MakeString(), MakeInt(), MakeRecord() - Create test values
testctx.GetString(), GetInt(), GetRecord() - Extract values
For HTTP/FS effects:
ctx := testctx.NewMockEffContext()
ctx.GrantAll("Net")
ctx.SetHTTPClient(server.Client())
See: resources/testing_patterns.md for more examples
Step 3: Validate and Inspect (~30 min)
Run validation:
ailang doctor builtins
ailang builtins list --by-module
ailang builtins check-migration
Run tests:
go test -v internal/builtins -run TestStrReverse
make test
Test in REPL:
ailang repl
> :type _str_reverse
string -> string
> _str_reverse("hello")
"olleh" : string
Step 4: Runtime Wiring (Automatic!)
No manual wiring needed! The registry automatically:
- ✅ Registers with evaluator (
internal/eval)
- ✅ Adds to type environment (
internal/types)
- ✅ Links to runtime (
internal/runtime)
- ✅ Generates module interface (
internal/link)
Feature flag removed in v0.3.10 - New registry is now the default.
Key Components
Central Registry
Location: internal/builtins/spec.go
Features:
- Single registration point with
RegisterEffectBuiltin()
- Compile-time validation (arity, types, impl, effects)
- Freeze-safe (no registration after init)
- 52 builtins migrated
Type Builder DSL
Location: internal/types/builder.go
Reduces type construction from 35 lines → 10 lines (-71%)
Available methods:
T := types.NewBuilder()
T.String()
T.Int()
T.Float()
T.Bool()
T.Unit()
T.List(elementType)
T.Record(fields...)
T.Tuple(types...)
T.Con("TypeName")
T.Var("α")
T.Func(arg1, arg2, ...).Returns(returnType).Effects("IO", "FS")
T.App("Result", okType, errType)
See: resources/type_builder_examples.md
Test Harness
Location: internal/effects/testctx/
Value constructors:
MakeString(s string) → StringValue
MakeInt(i int) → IntValue
MakeFloat(f float64) → FloatValue
MakeBool(b bool) → BoolValue
MakeList([]Value) → ListValue
MakeRecord(map[string]Value) → RecordValue
Value extractors:
GetString(Value) → string
GetInt(Value) → int
GetFloat(Value) → float64
GetBool(Value) → bool
GetList(Value) → []Value
GetRecord(Value) → map[string]Value
Effect mocking:
ctx.GrantAll(effect) - Grant capabilities
ctx.SetHTTPClient(client) - Mock HTTP
ctx.SetFS(fs) - Mock filesystem
Validation & Inspection
Commands:
ailang doctor builtins
ailang builtins list
ailang builtins list --by-module
ailang builtins list --by-effect
ailang builtins check-migration
7 validation rules:
- Type function exists and valid
- Implementation function exists
- Arity matches NumArgs
- Effect consistency (IsPure vs Effect field)
- Module name valid
- Metadata complete
- Determinism:
IsPure: true builtins MUST NOT use nondeterministic Go patterns (map iteration, goroutines, time-dependent logic). Tests must run with -count=20 to verify. See xml.go lookupPrefix bug — Go map iteration caused findAll to randomly drop elements.
Metrics
Development time:
| Task | Before | After | Savings |
|---|
| Files to edit | 4 | 1 | -75% |
| Type construction | 35 LOC | 10 LOC | -71% |
| Total time | 7.5h | 2.5h | -67% |
| Test setup | ~50 LOC | ~15 LOC | -70% |
Resources
Detailed Examples
For type patterns:
For testing patterns:
For comprehensive guide:
- M-DX1-FINAL-SUMMARY.md - Complete M-DX1 summary
design_docs/planned/easier-ailang-dev.md - Design rationale
CHANGELOG.md (v0.3.10+) - Version history
File Locations
Builtin files by module:
internal/builtins/string.go - String functions (9 builtins)
internal/builtins/math.go - Math functions (37 builtins)
internal/builtins/io.go - IO functions (3 builtins)
internal/builtins/net.go - Network functions (1 builtin)
internal/builtins/show.go - Show typeclass (1 builtin)
internal/builtins/json_decode.go - JSON decoding (1 builtin)
Test files:
internal/builtins/*_test.go - Builtin tests
internal/effects/testctx/*_test.go - Test harness tests
Common Patterns
Pattern 0: "Zero-Arg" Builtins (M-DX10 Unit-Argument Model)
⚠️ AILANG has no true nullary functions! All functions that appear to take no arguments must take a unit parameter.
RegisterEffectBuiltin(BuiltinSpec{
Module: "std/sharedmem",
Name: "_sharedmem_keys",
NumArgs: 1,
Effect: "SharedMem",
Type: func() types.Type {
T := types.NewBuilder()
return T.Func(T.Unit()).Returns(T.List(T.String())).Effects("SharedMem")
},
Impl: func(ctx *effects.EffContext, args []eval.Value) (eval.Value, error) {
keys := ctx.SharedMem.Cache.Keys()
},
})
RegisterEffectBuiltin(BuiltinSpec{
Name: "_sharedmem_keys",
NumArgs: 0,
})
Stdlib wrapper:
export func keys(u: unit) -> list[string] ! {SharedMem} {
_sharedmem_keys(u)
}
-- Or expression body:
export func now() -> int ! {Clock} = _clock_now()
Go tests must pass unit:
result, err := impl(ctx, []eval.Value{&eval.UnitValue{}})
result, err := impl(ctx, []eval.Value{})
Reference: M-DX10 design doc
Pattern 1: Pure String Function
RegisterEffectBuiltin(BuiltinSpec{
Module: "std/string",
Name: "_str_len",
NumArgs: 1,
IsPure: true,
Type: func() types.Type {
T := types.NewBuilder()
return T.Func(T.String()).Returns(T.Int())
},
Impl: func(ctx *effects.EffContext, args []eval.Value) (eval.Value, error) {
s := args[0].(*eval.StringValue).Value
return &eval.IntValue{Value: len([]rune(s))}, nil
},
})
Pattern 2: Effect Function with HTTP
RegisterEffectBuiltin(BuiltinSpec{
Module: "std/net",
Name: "_net_httpRequest",
NumArgs: 4,
Effect: "Net",
Type: makeHTTPRequestType,
Impl: effects.NetHTTPRequest,
})
Pattern 3: Complex Record Types
See: resources/type_builder_examples.md for full example with nested records.
Progressive Disclosure
This skill loads information progressively:
- Always loaded: This SKILL.md file (workflow overview)
- Execute as needed: Scripts in
scripts/ (validation)
- Load on demand: Detailed type examples in
resources/
Notes
- No feature flag needed (default since v0.3.10)
- Registry auto-wires to all subsystems
- Test harness prevents network/FS side effects
- Metadata enables future tooling (docs, LSP)
- Type Builder DSL is type-safe at compile time