agentleFS
sunholo-datasunholo-data/ailang/.agents/skills/builtin-developer/SKILL.md

builtin-developer

Skill38 starsChanged 6 days agoApache-2.0View on GitHub

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%).

The file

We split this skill into the rules it gives an agent

10 rules. About 0 are common, 6 are distinctive, and 4 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈6

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 4

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    Add new builtin functions to AILANG using the modern registry system (M-DX1).

    line 8
  2. Distinctive:

    Write hermetic tests (~1 hour)

    line 16
  3. Distinctive:

    All builtins are registered

    line 57
  4. Very specific:

    Run ailang doctor and list commands.

    line 63
  5. Very specific:

    Use RegisterEffectBuiltin() in init()

    line 123
  6. Distinctive:

    Use Type Builder DSL for type construction

    line 124
  7. Distinctive:

    No manual wiring needed! The registry automatically:

    line 206
  8. Very specific:

    ⚠️ AILANG has no true nullary functions!: All functions that appear to take no arguments must take a unit parameter.

    line 347
  9. Distinctive:

    No feature flag needed (default since v0.3.10)

    line 440
  10. Distinctive:

    Test harness prevents network/FS side effects

    line 442

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

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:

  1. Register builtin with metadata (~30 min)
  2. Write hermetic tests (~1 hour)
  3. Validate and inspect (~30 min)
  4. 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:

.Codex/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:

.Codex/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):

// internal/builtins/string.go
func init() {
    registerMyBuiltin()
}

func registerMyBuiltin() {
    RegisterEffectBuiltin(BuiltinSpec{
        Module:  "std/string",
        Name:    "_str_reverse",
        NumArgs: 1,
        IsPure:  true,        // or false with Effect: "IO"
        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")  // Grant Net capability
ctx.SetHTTPClient(server.Client())  // Use test server

See: resources/testing_patterns.md for more examples

Step 3: Validate and Inspect (~30 min)

Run validation:

# Validate all builtins
ailang doctor builtins

# List all builtins by module
ailang builtins list --by-module

# Check for orphaned builtins (migration safety)
ailang builtins check-migration

Run tests:

# Test specific builtin
go test -v internal/builtins -run TestStrReverse

# Test all builtins
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()

// Primitive types
T.String()
T.Int()
T.Float()
T.Bool()
T.Unit()

// Complex types
T.List(elementType)
T.Record(fields...)
T.Tuple(types...)
T.Con("TypeName")
T.Var("α")

// Function types
T.Func(arg1, arg2, ...).Returns(returnType).Effects("IO", "FS")

// Type applications
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              # Health checks
ailang builtins list                # List all builtins
ailang builtins list --by-module    # Group by module
ailang builtins list --by-effect    # Group by effect
ailang builtins check-migration     # Check for orphaned builtins

7 validation rules:

  1. Type function exists and valid
  2. Implementation function exists
  3. Arity matches NumArgs
  4. Effect consistency (IsPure vs Effect field)
  5. Module name valid
  6. Metadata complete
  7. 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:

TaskBeforeAfterSavings
Files to edit41-75%
Type construction35 LOC10 LOC-71%
Total time7.5h2.5h-67%
Test setup~50 LOC~15 LOC-70%

Resources

Detailed Examples

For type patterns:

  • resources/type_builder_examples.md - All type patterns

For testing patterns:

  • resources/testing_patterns.md - Hermetic test examples

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.

// ✅ CORRECT - Unit-argument model
RegisterEffectBuiltin(BuiltinSpec{
    Module:  "std/sharedmem",
    Name:    "_sharedmem_keys",
    NumArgs: 1,  // Takes unit parameter!
    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) {
        // args[0] is unit, ignored (validates arity)
        keys := ctx.SharedMem.Cache.Keys()
        // ...
    },
})

// ❌ WRONG - True nullary (causes "arity mismatch: 0 vs 1")
RegisterEffectBuiltin(BuiltinSpec{
    Name:    "_sharedmem_keys",
    NumArgs: 0,  // BUG!
})

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:

// ✅ Correct
result, err := impl(ctx, []eval.Value{&eval.UnitValue{}})

// ❌ Wrong - causes arity mismatch
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,  // Uses ctx.GetHTTPClient()
})

Pattern 3: Complex Record Types

See: resources/type_builder_examples.md for full example with nested records.

Progressive Disclosure

This skill loads information progressively:

  1. Always loaded: This SKILL.md file (workflow overview)
  2. Execute as needed: Scripts in scripts/ (validation)
  3. 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

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.