Back to skills
SkillHub ClubShip Full StackFull Stack

Builtin Developer

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

Packaged view

This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.

Stars
22
Hot score
88
Updated
March 20, 2026
Overall rating
C2.3
Composite score
2.3
Best-practice grade
D52.0

Install command

npx @skill-hub/cli install sunholo-data-ailang-builtin-developer
programmingAILANGbuiltin-functionsdevelopment-workflowgo

Repository

sunholo-data/ailang

Skill path: .claude/skills/builtin-developer

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

Open repository

Best for

Primary workflow: Ship Full Stack.

Technical facets: Full Stack.

Target audience: everyone.

License: Unknown.

Original source

Catalog source: SkillHub Club.

Repository owner: sunholo-data.

This is still a mirrored public skill entry. Review the repository before installing into production workflows.

What it helps with

  • Install Builtin Developer into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
  • Review https://github.com/sunholo-data/ailang before adding Builtin Developer to shared team environments
  • Use Builtin Developer for development workflows

Works across

Claude CodeCodex CLIGemini CLIOpenCode

Favorites: 0.

Sub-skills: 0.

Aggregator: No.

Original source / Raw SKILL.md

---
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:**
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:**
```bash
.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:**
```bash
.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`):

```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](resources/type_builder_examples.md) for type patterns

### Step 2: Write Hermetic Tests (~1 hour)

**Add tests in** `internal/builtins/register_test.go`:

```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:**
```go
ctx := testctx.NewMockEffContext()
ctx.GrantAll("Net")  // Grant Net capability
ctx.SetHTTPClient(server.Client())  // Use test server
```

**See:** [resources/testing_patterns.md](resources/testing_patterns.md) for more examples

### Step 3: Validate and Inspect (~30 min)

**Run validation:**
```bash
# 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:**
```bash
# Test specific builtin
go test -v internal/builtins -run TestStrReverse

# Test all builtins
make test
```

**Test in REPL:**
```bash
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:**
```go
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](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:**
```bash
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
```

**6 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

## 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:**
- [resources/type_builder_examples.md](resources/type_builder_examples.md) - All type patterns

**For testing patterns:**
- [resources/testing_patterns.md](resources/testing_patterns.md) - Hermetic test examples

**For comprehensive guide:**
- [M-DX1-FINAL-SUMMARY.md](../../../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.

```go
// ✅ 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:**
```ailang
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:**
```go
// ✅ Correct
result, err := impl(ctx, []eval.Value{&eval.UnitValue{}})

// ❌ Wrong - causes arity mismatch
result, err := impl(ctx, []eval.Value{})
```

**Reference:** [M-DX10 design doc](../../../design_docs/implemented/v0_4_6/m-dx10-nullary-function-calls.md)

### Pattern 1: Pure String Function

```go
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

```go
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](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


---

## Referenced Files

> The following files are referenced in this skill and included for context.

### resources/type_builder_examples.md

```markdown
# Type Builder Examples

Complete examples of using the Type Builder DSL for AILANG builtins.

## Basic Types

### Pure String Function

```go
func makeStrLenType() types.Type {
    T := types.NewBuilder()
    return T.Func(T.String()).Returns(T.Int())
}
```

### Multi-Argument Function

```go
func makeStrSliceType() types.Type {
    T := types.NewBuilder()
    return T.Func(
        T.String(),  // string to slice
        T.Int(),     // start index
        T.Int(),     // end index
    ).Returns(T.String())
}
```

## Complex Types with Records

### HTTP Request Function

```go
func makeHTTPRequestType() types.Type {
    T := types.NewBuilder()

    // Define header type: {name: string, value: string}
    headerType := T.Record(
        types.Field("name", T.String()),
        types.Field("value", T.String()),
    )

    // Define response type: {status: int, headers: [Header], body: string}
    responseType := T.Record(
        types.Field("status", T.Int()),
        types.Field("headers", T.List(headerType)),
        types.Field("body", T.String()),
    )

    // Function signature with Result type
    return T.Func(
        T.String(),           // url
        T.String(),           // method
        T.List(headerType),   // headers
        T.String(),           // body
    ).Returns(
        T.App("Result", responseType, T.Con("NetError")),
    ).Effects("Net")
}
```

### JSON Decode Function

```go
func makeJSONDecodeType() types.Type {
    T := types.NewBuilder()

    // Use type variable for polymorphic return
    alpha := T.Var("α")

    // Result[α, JSONError]
    resultType := T.App("Result", alpha, T.Con("JSONError"))

    return T.Func(T.String()).Returns(resultType)
}
```

## List and Tuple Types

### List Operations

```go
func makeListMapType() types.Type {
    T := types.NewBuilder()

    alpha := T.Var("α")
    beta := T.Var("β")

    // (α -> β) -> [α] -> [β]
    return T.Func(
        T.Func(alpha).Returns(beta),  // mapper function
        T.List(alpha),                 // input list
    ).Returns(T.List(beta))
}
```

### Tuple Constructor

```go
func makeTupleType() types.Type {
    T := types.NewBuilder()

    alpha := T.Var("α")
    beta := T.Var("β")

    // α -> β -> (α, β)
    return T.Func(alpha, beta).Returns(
        T.Tuple(alpha, beta),
    )
}
```

## Effect Functions

### File System Read

```go
func makeFSReadFileType() types.Type {
    T := types.NewBuilder()

    // Result[string, FSError]
    resultType := T.App("Result", T.String(), T.Con("FSError"))

    return T.Func(T.String()).Returns(resultType).Effects("FS")
}
```

### Multiple Effects

```go
func makeHTTPWithLoggingType() types.Type {
    T := types.NewBuilder()

    headerType := T.Record(
        types.Field("name", T.String()),
        types.Field("value", T.String()),
    )

    responseType := T.Record(
        types.Field("status", T.Int()),
        types.Field("body", T.String()),
    )

    return T.Func(
        T.String(),
        T.String(),
        T.List(headerType),
    ).Returns(
        T.App("Result", responseType, T.Con("NetError")),
    ).Effects("Net", "IO")  // Multiple effects
}
```

## Advanced Patterns

### Option/Maybe Types

```go
func makeFindType() types.Type {
    T := types.NewBuilder()

    alpha := T.Var("α")

    // (α -> bool) -> [α] -> Option[α]
    return T.Func(
        T.Func(alpha).Returns(T.Bool()),  // predicate
        T.List(alpha),                     // list
    ).Returns(
        T.App("Option", alpha),  // Option[α]
    )
}
```

### Nested Records

```go
func makeNestedRecordType() types.Type {
    T := types.NewBuilder()

    // Inner record: {x: int, y: int}
    pointType := T.Record(
        types.Field("x", T.Int()),
        types.Field("y", T.Int()),
    )

    // Outer record: {topleft: Point, bottomright: Point}
    rectType := T.Record(
        types.Field("topleft", pointType),
        types.Field("bottomright", pointType),
    )

    return T.Func().Returns(rectType)
}
```

### Higher-Order Functions

```go
func makeComposeType() types.Type {
    T := types.NewBuilder()

    alpha := T.Var("α")
    beta := T.Var("β")
    gamma := T.Var("γ")

    // (β -> γ) -> (α -> β) -> α -> γ
    return T.Func(
        T.Func(beta).Returns(gamma),   // f
        T.Func(alpha).Returns(beta),   // g
        alpha,                          // x
    ).Returns(gamma)
}
```

## Type Builder API Quick Reference

### Primitive Types
- `T.String()` → `string`
- `T.Int()` → `int`
- `T.Float()` → `float`
- `T.Bool()` → `bool`
- `T.Unit()` → `()`

### Compound Types
- `T.List(elementType)` → `[T]`
- `T.Tuple(t1, t2, ...)` → `(T1, T2, ...)`
- `T.Record(fields...)` → `{field1: T1, field2: T2, ...}`

### Type Constructors
- `T.Con("TypeName")` → Named type constructor
- `T.Var("α")` → Type variable (polymorphism)
- `T.App("Con", arg1, arg2)` → Type application (e.g., `Result[T, E]`)

### Function Types
- `T.Func(arg1, arg2, ...).Returns(retType)` → `arg1 -> arg2 -> retType`
- `.Effects("IO", "FS")` → Add effect annotation

### Field Construction (for Records)
```go
types.Field("fieldName", fieldType)
```

## Common Mistakes

### ❌ Wrong: Forgetting Returns()

```go
// This doesn't compile!
T.Func(T.String())  // Missing .Returns()
```

### ✅ Correct: Always use Returns()

```go
T.Func(T.String()).Returns(T.Int())
```

### ❌ Wrong: Effects without Returns()

```go
// This doesn't work!
T.Func(T.String()).Effects("IO")
```

### ✅ Correct: Returns() before Effects()

```go
T.Func(T.String()).Returns(T.Unit()).Effects("IO")
```

### ❌ Wrong: Creating Record fields directly

```go
// Don't do this!
T.Record(map[string]types.Type{
    "name": T.String(),
})
```

### ✅ Correct: Use types.Field()

```go
T.Record(
    types.Field("name", T.String()),
    types.Field("age", T.Int()),
)
```

## See Also

- **Testing**: [testing_patterns.md](testing_patterns.md) - How to test these builtins
- **Registry**: `internal/builtins/spec.go` - Registration system
- **Examples**: `internal/builtins/*.go` - Real builtin implementations

```

### resources/testing_patterns.md

```markdown
# Testing Patterns for AILANG Builtins

Complete guide to hermetic testing using the Test Harness (`internal/effects/testctx/`).

## Basic Test Structure

### Pure Function Test

```go
func TestStrReverse(t *testing.T) {
    ctx := testctx.NewMockEffContext()

    tests := []struct {
        input    string
        expected string
    }{
        {"hello", "olleh"},
        {"", ""},
        {"🎉", "🎉"},
        {"abc", "cba"},
    }

    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))
    }
}
```

### Multi-Argument Function Test

```go
func TestStrSlice(t *testing.T) {
    ctx := testctx.NewMockEffContext()

    tests := []struct {
        str      string
        start    int
        end      int
        expected string
    }{
        {"hello", 0, 3, "hel"},
        {"world", 1, 4, "orl"},
        {"test", 0, 4, "test"},
    }

    for _, tt := range tests {
        result, err := strSliceImpl(ctx, []eval.Value{
            testctx.MakeString(tt.str),
            testctx.MakeInt(tt.start),
            testctx.MakeInt(tt.end),
        })
        assert.NoError(t, err)
        assert.Equal(t, tt.expected, testctx.GetString(result))
    }
}
```

## Testing with Records

### Simple Record

```go
func TestRecordBuiltin(t *testing.T) {
    ctx := testctx.NewMockEffContext()

    // Create input record
    input := testctx.MakeRecord(map[string]eval.Value{
        "name": testctx.MakeString("Alice"),
        "age":  testctx.MakeInt(30),
    })

    result, err := myRecordBuiltin(ctx, []eval.Value{input})
    assert.NoError(t, err)

    // Extract and verify output record
    output := testctx.GetRecord(result)
    assert.Equal(t, "Alice", testctx.GetString(output["name"]))
    assert.Equal(t, 31, testctx.GetInt(output["age"]))
}
```

### Nested Records

```go
func TestNestedRecord(t *testing.T) {
    ctx := testctx.NewMockEffContext()

    // Create nested record
    point := testctx.MakeRecord(map[string]eval.Value{
        "x": testctx.MakeInt(10),
        "y": testctx.MakeInt(20),
    })

    rect := testctx.MakeRecord(map[string]eval.Value{
        "topleft":     point,
        "bottomright": testctx.MakeRecord(map[string]eval.Value{
            "x": testctx.MakeInt(100),
            "y": testctx.MakeInt(200),
        }),
    })

    result, err := processRect(ctx, []eval.Value{rect})
    assert.NoError(t, err)

    // Extract nested values
    output := testctx.GetRecord(result)
    topleft := testctx.GetRecord(output["topleft"])
    assert.Equal(t, 10, testctx.GetInt(topleft["x"]))
}
```

## Testing with Lists

### Simple List

```go
func TestListOperation(t *testing.T) {
    ctx := testctx.NewMockEffContext()

    // Create list of integers
    input := testctx.MakeList([]eval.Value{
        testctx.MakeInt(1),
        testctx.MakeInt(2),
        testctx.MakeInt(3),
    })

    result, err := sumList(ctx, []eval.Value{input})
    assert.NoError(t, err)
    assert.Equal(t, 6, testctx.GetInt(result))
}
```

### List of Records

```go
func TestListOfRecords(t *testing.T) {
    ctx := testctx.NewMockEffContext()

    people := testctx.MakeList([]eval.Value{
        testctx.MakeRecord(map[string]eval.Value{
            "name": testctx.MakeString("Alice"),
            "age":  testctx.MakeInt(30),
        }),
        testctx.MakeRecord(map[string]eval.Value{
            "name": testctx.MakeString("Bob"),
            "age":  testctx.MakeInt(25),
        }),
    })

    result, err := getNames(ctx, []eval.Value{people})
    assert.NoError(t, err)

    names := testctx.GetList(result)
    assert.Equal(t, 2, len(names))
    assert.Equal(t, "Alice", testctx.GetString(names[0]))
    assert.Equal(t, "Bob", testctx.GetString(names[1]))
}
```

## Testing Effect Functions

### HTTP Request (Hermetic)

```go
func TestNetHTTPRequest(t *testing.T) {
    // Create test HTTP server
    server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // Verify request
        assert.Equal(t, "GET", r.Method)
        assert.Equal(t, "/api/test", r.URL.Path)

        // Return response
        w.WriteHeader(200)
        w.Write([]byte(`{"status": "ok"}`))
    }))
    defer server.Close()

    // Create mock context with HTTP capability
    ctx := testctx.NewMockEffContext()
    ctx.GrantAll("Net")
    ctx.SetHTTPClient(server.Client())

    // Create request arguments
    url := testctx.MakeString(server.URL + "/api/test")
    method := testctx.MakeString("GET")
    headers := testctx.MakeList([]eval.Value{})
    body := testctx.MakeString("")

    // Execute builtin
    result, err := effects.NetHTTPRequest(ctx,
        url,
        method,
        headers,
        body,
    )

    // Verify result
    assert.NoError(t, err)
    resp := testctx.GetRecord(result)
    assert.Equal(t, 200, testctx.GetInt(resp["status"]))
    assert.Contains(t, testctx.GetString(resp["body"]), "ok")
}
```

### File System Operations (Mock)

```go
func TestFSReadFile(t *testing.T) {
    ctx := testctx.NewMockEffContext()
    ctx.GrantAll("FS")

    // Note: In real tests, you'd mock the filesystem
    // For now, this shows the pattern
    path := testctx.MakeString("/tmp/test.txt")

    result, err := fsReadFile(ctx, []eval.Value{path})

    // Handle Result[string, FSError]
    assert.NoError(t, err)
    resultVariant := testctx.GetVariant(result)
    if resultVariant.Tag == "Ok" {
        content := testctx.GetString(resultVariant.Value)
        assert.NotEmpty(t, content)
    }
}
```

### Testing Capability Denial

```go
func TestCapabilityDenied(t *testing.T) {
    ctx := testctx.NewMockEffContext()
    // Don't grant Net capability

    url := testctx.MakeString("http://example.com")
    method := testctx.MakeString("GET")
    headers := testctx.MakeList([]eval.Value{})
    body := testctx.MakeString("")

    result, err := effects.NetHTTPRequest(ctx, url, method, headers, body)

    // Should fail with capability error
    assert.Error(t, err)
    assert.Contains(t, err.Error(), "capability")
}
```

## Testing Result Types

### Ok Variant

```go
func TestResultOk(t *testing.T) {
    ctx := testctx.NewMockEffContext()

    result, err := myBuiltin(ctx, []eval.Value{
        testctx.MakeString("valid-input"),
    })

    assert.NoError(t, err)

    // Extract Result variant
    variant := testctx.GetVariant(result)
    assert.Equal(t, "Ok", variant.Tag)

    // Get the Ok value
    value := testctx.GetString(variant.Value)
    assert.Equal(t, "expected", value)
}
```

### Err Variant

```go
func TestResultErr(t *testing.T) {
    ctx := testctx.NewMockEffContext()

    result, err := myBuiltin(ctx, []eval.Value{
        testctx.MakeString("invalid-input"),
    })

    assert.NoError(t, err)  // Builtin executed successfully

    // Extract Result variant
    variant := testctx.GetVariant(result)
    assert.Equal(t, "Err", variant.Tag)

    // Get the Err value
    errValue := testctx.GetString(variant.Value)
    assert.Contains(t, errValue, "invalid")
}
```

## Testing Option Types

### Some Variant

```go
func TestOptionSome(t *testing.T) {
    ctx := testctx.NewMockEffContext()

    list := testctx.MakeList([]eval.Value{
        testctx.MakeInt(1),
        testctx.MakeInt(2),
        testctx.MakeInt(3),
    })

    result, err := findInList(ctx, []eval.Value{
        testctx.MakeInt(2),
        list,
    })

    assert.NoError(t, err)

    variant := testctx.GetVariant(result)
    assert.Equal(t, "Some", variant.Tag)
    assert.Equal(t, 2, testctx.GetInt(variant.Value))
}
```

### None Variant

```go
func TestOptionNone(t *testing.T) {
    ctx := testctx.NewMockEffContext()

    list := testctx.MakeList([]eval.Value{
        testctx.MakeInt(1),
        testctx.MakeInt(2),
    })

    result, err := findInList(ctx, []eval.Value{
        testctx.MakeInt(99),  // Not in list
        list,
    })

    assert.NoError(t, err)

    variant := testctx.GetVariant(result)
    assert.Equal(t, "None", variant.Tag)
}
```

## Test Harness API Reference

### Value Constructors

```go
// Primitives
testctx.MakeString(s string) *eval.StringValue
testctx.MakeInt(i int) *eval.IntValue
testctx.MakeFloat(f float64) *eval.FloatValue
testctx.MakeBool(b bool) *eval.BoolValue
testctx.MakeUnit() *eval.UnitValue

// Compound types
testctx.MakeList(items []eval.Value) *eval.ListValue
testctx.MakeRecord(fields map[string]eval.Value) *eval.RecordValue
testctx.MakeTuple(items []eval.Value) *eval.TupleValue

// Variants (ADTs)
testctx.MakeVariant(tag string, value eval.Value) *eval.VariantValue
```

### Value Extractors

```go
// Primitives
testctx.GetString(v eval.Value) string
testctx.GetInt(v eval.Value) int
testctx.GetFloat(v eval.Value) float64
testctx.GetBool(v eval.Value) bool

// Compound types
testctx.GetList(v eval.Value) []eval.Value
testctx.GetRecord(v eval.Value) map[string]eval.Value
testctx.GetTuple(v eval.Value) []eval.Value

// Variants
testctx.GetVariant(v eval.Value) *eval.VariantValue
// Then access variant.Tag and variant.Value
```

### Mock Context Methods

```go
// Create context
ctx := testctx.NewMockEffContext()

// Grant capabilities
ctx.GrantAll(effect string)
ctx.RevokeAll(effect string)

// Mock HTTP
ctx.SetHTTPClient(client *http.Client)

// Mock Filesystem (future)
ctx.SetFS(fs FS)
```

## Common Patterns

### Table-Driven Tests

```go
func TestMathOperations(t *testing.T) {
    ctx := testctx.NewMockEffContext()

    tests := []struct {
        name     string
        a        int
        b        int
        expected int
    }{
        {"positive", 5, 3, 8},
        {"negative", -5, -3, -8},
        {"zero", 0, 0, 0},
        {"mixed", -5, 10, 5},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            result, err := addInts(ctx, []eval.Value{
                testctx.MakeInt(tt.a),
                testctx.MakeInt(tt.b),
            })
            assert.NoError(t, err)
            assert.Equal(t, tt.expected, testctx.GetInt(result))
        })
    }
}
```

### Error Testing

```go
func TestErrorHandling(t *testing.T) {
    ctx := testctx.NewMockEffContext()

    tests := []struct {
        name        string
        input       string
        expectError bool
        errorMsg    string
    }{
        {"valid", "valid", false, ""},
        {"invalid", "", true, "empty string"},
        {"too long", strings.Repeat("a", 1000), true, "too long"},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            result, err := validateString(ctx, []eval.Value{
                testctx.MakeString(tt.input),
            })

            if tt.expectError {
                assert.Error(t, err)
                assert.Contains(t, err.Error(), tt.errorMsg)
            } else {
                assert.NoError(t, err)
                assert.NotNil(t, result)
            }
        })
    }
}
```

### Benchmark Tests

```go
func BenchmarkStrReverse(b *testing.B) {
    ctx := testctx.NewMockEffContext()
    input := testctx.MakeString("hello world")

    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        _, err := strReverseImpl(ctx, []eval.Value{input})
        if err != nil {
            b.Fatal(err)
        }
    }
}
```

## Best Practices

1. **Always use MockEffContext** - Never use real effects in tests
2. **Test edge cases** - Empty strings, zero, negative numbers, etc.
3. **Use table-driven tests** - Makes adding test cases easy
4. **Test error conditions** - Not just happy path
5. **Use subtests** - For better test organization
6. **Hermetic HTTP tests** - Use httptest.NewServer()
7. **Don't test the harness** - Test your builtin logic
8. **Keep tests fast** - No network, no filesystem I/O
9. **Clear assertions** - Use descriptive error messages
10. **One assertion per concept** - Don't overload tests

## Common Mistakes

### ❌ Wrong: Using real HTTP

```go
// Don't do this!
resp, err := http.Get("https://api.example.com")
```

### ✅ Correct: Use httptest

```go
server := httptest.NewServer(handler)
defer server.Close()
ctx.SetHTTPClient(server.Client())
```

### ❌ Wrong: Type assertion without checking

```go
// This can panic!
str := result.(*eval.StringValue).Value
```

### ✅ Correct: Use testctx extractors

```go
str := testctx.GetString(result)
```

### ❌ Wrong: Testing without EffContext

```go
// Don't skip the context!
result := myFunction(args)
```

### ✅ Correct: Always use EffContext

```go
ctx := testctx.NewMockEffContext()
result, err := myFunction(ctx, args)
```

## See Also

- **Type Builder**: [type_builder_examples.md](type_builder_examples.md) - How to define types
- **Test Harness**: `internal/effects/testctx/` - Implementation
- **Examples**: `internal/builtins/*_test.go` - Real test examples

```

### ../../../design_docs/implemented/v0_4_6/m-dx10-nullary-function-calls.md

```markdown
# M-DX10: Complete S-CALL0 and Unit-Argument Model

**Status**: ✅ Implemented
**Target**: v0.4.6
**Completed**: v0.4.6
**Priority**: P0 - High (blocks CLI args feature)
**Estimated**: 2-3 days
**Dependencies**: S-CALL0 (partial implementation exists)

## Implementation Summary

**Implemented in v0.4.6** - All phases complete:

1. **Phase 1: Builtins aligned** - All "zero-arg" builtins updated to `NumArgs: 1` with unit parameter:
   - `_clock_now`: `internal/builtins/clock.go` - Type: `() -> int ! {Clock}`
   - `_env_getArgs`: `internal/builtins/env.go` - Type: `() -> [string] ! {Env}`
   - `_io_readLine`: `internal/builtins/io.go` - Type: `() -> string ! {IO}`
   - All implementations validate unit argument (defense against type system bugs)

2. **Phase 1.5: Entry invocation** - Runtime invokes `main()` with unit argument (`cmd/ailang/run_helpers.go:258`)

3. **Phase 2: S-CALL0 complete** - `f()` → `f(())` desugaring works in all contexts (expression, statement, lambda, match)

4. **Phase 3: Stdlib fixed** - All wrappers call builtins with `()`:
   - `std/clock.ail`: `now() = _clock_now()`
   - `std/env.ail`: `getArgs() = _env_getArgs()`
   - `std/io.ail`: `readLine() = _io_readLine()`

5. **Phase 4: Documentation** - Teaching prompts updated with unit-argument model explanation

**Verification**: All success criteria met. New builtins (e.g., `_sharedmem_keys` in v0.5.11) follow this pattern.

## AI-First Alignment Check

**Score this feature against AILANG's core principles:**

| Principle | Impact | Score | Notes |
|-----------|--------|-------|-------|
| Reduce Syntactic Noise | + | +1 | Enables clean `getArgs()` syntax without special cases |
| Preserve Semantic Clarity | + | +1 | Unifies all "zero-arg" functions under unit-argument model |
| Increase Determinism | + | +1 | Single, consistent desugaring rule: `f()` → `f(())` everywhere |
| Lower Token Cost | + | +1 | No arity-dependent rules for AI to learn; `f()` always works |
| **Net Score** | | **+4** | **Decision: Move forward (complete existing feature)** |

**Decision rule:** Net score > +1 → Move forward | ≤ 0 → Reject or redesign

**Reference:** See [AI-first DX philosophy](../v0_3_15/example-parity-vision-alignment.md#-design-principle-ai-first-dx)

## Problem Statement

**Discovered during M-LANG-CLI-ARGS implementation (v0.4.6):**

"Zero-argument" functions (`_env_getArgs`, `_clock_now`, `_io_readLine`) cannot be called correctly from AILANG code, blocking the CLI args feature and causing AI confusion.

**Current State:**
- `_env_getArgs` returns `<*eval.BuiltinFunction>` (function object) instead of `["arg1", "arg2"]`
- `_clock_now()` causes type error: "function arity mismatch: 0 vs 1"
- `_io_readLine()` has same issue
- Stdlib wrappers don't work: `export func getArgs() -> [string] ! {Env} = _env_getArgs`
- Only workaround: Call builtin implementation directly via `effects.Call()` (Go tests only)

**Impact:**
- **Blocks v0.4.6 CLI args feature** - cannot call `getArgs()` from AILANG code
- Affects existing builtins: `clock.now()`, `io.readLine()` (dormant bugs)
- AI models don't know whether to use `f`, `f()`, or `f ()`
- Inconsistent with AILANG's ML-style function application semantics

**Root Cause:**

AILANG **already chose the unit-argument model** with S-CALL0, but the implementation is incomplete:
1. **S-CALL0 sugar exists**: `f()` desugars to `f(())` in expression contexts
2. **Builtins are misaligned**: Registered as "zero-arg" in Go, expecting empty argument lists
3. **Stdlib wrappers are broken**: Try to call builtins without passing unit
4. **Teaching prompt is silent**: Doesn't explain the unit-argument model

This isn't a "missing feature" - it's an **incomplete feature rollout**.

## Semantic Model: No True Nullaries

**AILANG's Design Decision** (implicit in S-CALL0):

> **There are no semantically distinct nullary functions in AILANG.**
>
> All `func f() -> T` are surface syntax sugar for `func f(_ : ()) -> T`.
> All "zero-arg" calls are just applications to the unit value `()`.

**Why this model?**
- ✅ Consistent with ML tradition (`unit` as a proper type)
- ✅ Simplifies elaboration (no arity-dependent transformations)
- ✅ Preserves function application by juxtaposition (`f x`)
- ✅ Already implemented partially (S-CALL0)
- ✅ No special "nullary call" construct needed in core

**Type system:**
```
Surface:  func getArgs() -> [string] ! {Env}
Core:     () -> [string] ! {Env}          (single unit parameter)
```

**Call sites:**
```ailang
-- Surface syntax (S-CALL0 sugar)
let args = getArgs()

-- Desugars to
let args = getArgs (())

-- Both are valid; sugar is for ergonomics
```

## Goals

**Primary Goal:** Complete the unit-argument model by aligning builtins, stdlib, and documentation with S-CALL0

**Success Metrics:**
- All 3 affected builtins work: `_env_getArgs()`, `_clock_now()`, `_io_readLine()`
- Stdlib wrappers work correctly
- `cli_args` benchmark passes (currently 0%)
- No new language constructs (complete existing S-CALL0)
- AI models can reliably call "zero-arg" functions (measured via eval)
- Teaching prompt explicitly documents unit-argument model

## Solution Design

### Overview

**Complete the S-CALL0 feature by:**
1. Aligning builtin types to expect unit parameter
2. Ensuring S-CALL0 desugaring works everywhere
3. Fixing stdlib wrappers to pass unit
4. Documenting the unit-argument model

**No new semantics**. This is housekeeping.

### Architecture

**Components:**
1. **Builtins**: Update "zero-arg" builtins to have type `() -> T` (single unit param)
2. **S-CALL0**: Ensure `f()` → `f(())` transformation is complete
3. **Stdlib**: Update wrappers to call with `_builtin()`
4. **Teaching Prompt**: Document unit-argument model explicitly

**Invariant to enforce:**

> Every function that appears to take "no arguments" actually takes a single parameter of type `()` in core.

### Implementation Plan

**Phase 1: Align Builtins** (~1 day, 4 hours)

Audit all "zero-arg" builtins and update their Go implementations:

- [ ] **`_clock_now`** (`internal/builtins/clock.go`)
  - Current: Registered as `NumArgs: 0`
  - Fix: Change to `NumArgs: 1`, type `() -> int ! {Clock}`
  - Update `clockNowImpl` to expect 1 unit argument

- [ ] **`_env_getArgs`** (`internal/builtins/env.go`)
  - Current: Registered as `NumArgs: 0`
  - Fix: Change to `NumArgs: 1`, type `() -> [string] ! {Env}`
  - Update `envGetArgs` to expect 1 unit argument

- [ ] **`_io_readLine`** (`internal/builtins/io.go`)
  - Current: Registered as `NumArgs: 0` (assumed)
  - Fix: Change to `NumArgs: 1`, type `() -> string ! {IO}`
  - Update `readLineImpl` to expect 1 unit argument

- [ ] **Update golden snapshots**
  - Regenerate `internal/pipeline/testdata/builtin_types.golden`
  - Verify types show `() -> T` not `T`

**Phase 1.5: Entry Invocation** (~0.5 days, 2 hours)

Update runtime to invoke `main()` with unit argument:

- [ ] **Runtime invocation** (`cmd/ailang/main.go`, `internal/runtime/`)
  - Current: Assumes `main` has arity 0, calls with empty args
  - Fix: Call `main` with unit value: `main(())`
  - Consistent with `() -> () ! {E}` type

- [ ] **Entry detection**
  - Verify entry modules with `main()` still work
  - Test with various effect combinations: `! {IO}`, `! {IO, Env}`, etc.

- [ ] **Regression tests**
  - Entry modules with different effect signatures
  - Ensure no "arity mismatch" errors at runtime

**Phase 2: Complete S-CALL0** (~0.5 days, 2 hours)

Ensure `f()` → `f(())` desugaring works in **all** contexts:

**Where S-CALL0 must work:**
- [ ] **Expression contexts**:
  - `let x = f()` ✅ (already works)
  - `if cond then f() else g()`
  - `f() ++ g()` (binary operators)
  - Right-hand side of bindings

- [ ] **Statement contexts**:
  - Top of block: `{ f(); g(); }` ✅ (already works)
  - Sequence expressions
  - Effect statements

- [ ] **Lambda bodies**:
  - `\x. f()` (important for HOF)
  - Nested lambdas

- [ ] **Match arms** (expression position):
  - `match x { _ => f() }` (this bit us before!)
  - Guard expressions if implemented

- [ ] **Top-level in entry modules**:
  - If top-level `f()` is allowed (currently not, but future-proof)

**Where () must NOT appear:**
- ❌ **Patterns**: `match f() { ... }` (invalid - `f()` is expr, not pattern)
- ❌ **Type syntax**: `() -> T` is a type, not `f()`

- [ ] **Regression tests** (`internal/elaborate/elaborator_test.go`):
  - One test per context listed above
  - Verify AST transformation: `f()` → `App(f, UnitLit)`
  - Verify error messages for invalid positions

**Phase 3: Fix Stdlib Wrappers** (~0.5 days, 2 hours)

Update stdlib to use S-CALL0 sugar:

- [ ] **`std/clock.ail`**:
  ```ailang
  -- Before (broken)
  export func now() -> int ! {Clock} = _clock_now

  -- After (fixed)
  export func now() -> int ! {Clock} = _clock_now()
  ```

- [ ] **`std/env.ail`**:
  ```ailang
  -- Before (broken)
  export func getArgs() -> [string] ! {Env} = _env_getArgs

  -- After (fixed)
  export func getArgs() -> [string] ! {Env} = _env_getArgs()
  ```

- [ ] **`std/io.ail`**:
  ```ailang
  -- Before (broken)
  export func readLine() -> string ! {IO} = _io_readLine

  -- After (fixed)
  export func readLine() -> string ! {IO} = _io_readLine()
  ```

**Phase 4: Documentation & Testing** (~0.5 days, 2 hours)

- [ ] **Teaching prompt updates** (`prompts/v0.4.6.md`):
  - Add "Zero-Argument Functions" section
  - Explain unit-argument model
  - Show `f()` syntax and desugaring
  - Clarify that `()` is unit value, not "empty args"

- [ ] **Integration tests**:
  - Fix `tests/cli_args_test.ail` to use `getArgs()`
  - Test `clock.now()` in examples
  - Test `io.readLine()` if examples exist

- [ ] **Benchmark**:
  - Run `cli_args` benchmark
  - Target: 0% → 80%+ success rate

### Files to Modify/Create

**Modified files:**

**Phase 1: Builtins**
- `internal/builtins/clock.go` - Change `NumArgs: 0` → `1`, add unit validation (~15 LOC)
- `internal/builtins/env.go` - Change `NumArgs: 0` → `1`, add unit validation (~15 LOC)
- `internal/builtins/io.go` - Change `NumArgs: 0` → `1`, add unit validation (~15 LOC)
- `internal/pipeline/testdata/builtin_types.golden` - Update types (~3 LOC)

**Phase 1.5: Entry Invocation**
- `cmd/ailang/main.go` - Update `main()` invocation to pass unit (~10 LOC)
- `internal/runtime/runtime.go` - Ensure entry calls use unit argument (~10 LOC)
- `internal/runtime/runtime_test.go` - Test entry with various effect signatures (~30 LOC)

**Phase 2: S-CALL0**
- `internal/elaborate/elaborator_test.go` - Comprehensive S-CALL0 regression tests (~60 LOC)
- `internal/parser/parser_test.go` - Parser tests for `f()` contexts (~20 LOC)

**Phase 3: Stdlib**
- `std/clock.ail` - Add `()` to call: `_clock_now()` (~1 LOC)
- `std/env.ail` - Add `()` to call: `_env_getArgs()` (~1 LOC)
- `std/io.ail` - Add `()` to call: `_io_readLine()` (~1 LOC)
- `tests/cli_args_test.ail` - Use `getArgs()` instead of direct builtin (~2 LOC)

**Phase 4: Documentation**
- `prompts/v0.4.6.md` - Document unit-argument model with examples (~50 LOC)

**New files:**
- `design_docs/planned/v0_4_6/NULLARY-FUNCTIONS-SPEC.md` - Canonical spec (~100 LOC)

**Total: ~193 LOC (45 impl + 110 tests + 38 docs + stdlib)**

## Examples

### Example 1: CLI Arguments (The Blocker)

**Before (broken):**
```ailang
import std/env (getArgs)

export func main() -> () ! {IO, Env} {
  let args = getArgs;  -- ERROR: Returns function object, not list
  println(show(args))  -- Prints: <*eval.BuiltinFunction>
}
```

**After (fixed):**
```ailang
import std/env (getArgs)

export func main() -> () ! {IO, Env} {
  let args = getArgs();  -- ✓ Desugars to getArgs(()), returns ["arg1", "arg2"]
  println(show(args))    -- Prints: ["arg1", "arg2"]
}
```

### Example 2: Clock Operations

**Before (broken):**
```ailang
import std/clock (now)

export func main() -> () ! {Clock} {
  let ts = now();  -- ERROR: arity mismatch (builtin expects 0, got 1)
  ...
}
```

**After (fixed):**
```ailang
import std/clock (now)

export func main() -> () ! {Clock} {
  let ts = now();  -- ✓ Desugars to now(()), builtin expects unit, all good
  ...
}
```

### Example 3: Builtin Type Signature

**Before (broken):**
```
_env_getArgs : [string] ! {Env}  -- Wrong: looks like a value, not a function
```

**After (fixed):**
```
_env_getArgs : () -> [string] ! {Env}  -- Correct: single unit parameter
```

### Example 4: Higher-Order Functions (Still Work)

```ailang
-- Passing "zero-arg" function as value
let f = now  -- f has type (() -> int ! {Clock})

-- Calling through higher-order function
let callTwice[a](g: () -> a) -> (a, a) ! {Clock} = (g(), g())

let (t1, t2) = callTwice(now)  -- ✓ Both calls desugar to now(())
```

## Semantic Specification: "Nullary Functions" in AILANG

**Canonical rule:**

> AILANG does not have nullary functions at the semantic level.
>
> Surface syntax `func f() -> T` is sugar for `func f(_ : ()) -> T`.
> Surface syntax `f()` is sugar for `f(())` (application to unit value).

**Implications:**

1. **Type system**:
   - "Zero-arg function" types in core are always `() -> T`.
   - The parameter list contains exactly one element: the unit type.

2. **Elaboration**:
   - Parser recognizes `f()` as syntactic sugar.
   - Elaborator transforms to `App(f, UnitLit)`.
   - No arity-dependent logic; this is universal syntax sugar.

3. **Builtins**:
   - Every "zero-arg" builtin must be registered with `NumArgs: 1`.
   - Go implementation must expect one `eval.Value` (unit value).
   - Type builder must include unit in parameter list: `T.Func().Returns(...)`
   - **Validation invariant**: Builtin impl should validate the argument is unit (or safely ignore it).
     - Defense against type system bugs (e.g., annotation threading unsoundness).
     - If non-unit value is passed, panic with clear error: "internal invariant violation: expected unit".

4. **Stdlib**:
   - Wrapper functions use `f()` syntax (sugar).
   - Example: `export func now() -> int ! {Clock} = _clock_now()`

5. **Higher-order functions**:
   - "Zero-arg" functions can be passed as values: `let f = now`.
   - Type: `f : () -> int ! {Clock}`.
   - Calling: `f()` desugars to `f(())`.

6. **AI teaching**:
   - Teach: "Use `f()` to call functions with no parameters."
   - Don't teach: "Zero-arg functions are special."
   - Rationale: Reduces cognitive load; one rule covers all cases.
   - **Power-user note** (for teaching prompt):
     ```ailang
     -- These functions have type () -> T. You can pass them as values:
     let f = getArgs;      -- f : () -> [string] ! {Env}
     let xs = f();         -- calls getArgs(()), returns [string]

     -- Works with higher-order functions:
     let callTwice[a](g: () -> a) -> (a, a) = (g(), g())
     let (t1, t2) = callTwice(now)  -- both calls work
     ```

## Success Criteria

- [ ] `_env_getArgs()` returns `["arg1", "arg2"]` (not function object)
- [ ] `_clock_now()` returns Unix timestamp (no arity mismatch)
- [ ] `_io_readLine()` reads a line from stdin
- [ ] Stdlib wrappers work: `getArgs()`, `now()`, `readLine()`
- [ ] **Runtime invokes `main()` as `main(())` (or equivalent) and no entry module breaks**
- [ ] `cli_args` benchmark: 0% → 80%+ success rate
- [ ] All existing tests still pass (no regressions)
- [ ] Teaching prompt documents unit-argument model with examples
- [ ] Can pass "zero-arg" function as value: `let f = now` (type `() -> int ! {Clock}`)
- [ ] Golden snapshot shows `() -> T` types (not bare `T`)
- [ ] No new core AST nodes or arity-dependent elaboration
- [ ] S-CALL0 works in all listed contexts (expr, stmt, lambda, match)

## Testing Strategy

**Unit tests:**
- Builtins: Each "zero-arg" builtin accepts unit value, returns correct result
- S-CALL0: `f()` desugars to `f(())` in all contexts (expr, stmt, match, lambda)
- Type checker: Infers `() -> T` for "zero-arg" functions
- Higher-order: Can pass `now` as value, call via `g()`

**Integration tests:**
- `tests/cli_args_test.ail` - CLI arguments work end-to-end
- `examples/tests/micro_clock_measure.ail` - Clock operations work
- REPL: `now()` works interactively

**Manual testing:**
```bash
# Test 1: CLI args (the blocker)
ailang run --caps IO,Env --entry main tests/cli_args_test.ail arg1 arg2
# Expected: Prints "argc: 2", "arg: arg1", "arg: arg2"

# Test 2: Clock
ailang run --caps Clock examples/tests/micro_clock_measure.ail
# Expected: No errors, prints timestamps

# Test 3: REPL
ailang repl --caps Clock
> now()
# Expected: Returns int (Unix timestamp)

# Test 4: Stdlib wrappers
ailang repl --caps Env
> import std/env (getArgs)
> getArgs()
# Expected: Returns list of args
```

## Non-Goals

**Not in this feature:**
- ❌ True nullary functions (distinct semantic entity)
- ❌ New call syntax (e.g., `f!` operator)
- ❌ Auto-calling on reference (`f` → auto-call)
- ❌ Arity-dependent elaboration hacks
- ❌ Special "nullary call" core AST node

**Why not?**
- AILANG already chose the unit-argument model (S-CALL0)
- Adding true nullaries would require deep semantic surgery
- Current model is simpler, more consistent, and AI-friendly

## Timeline

**Day 1** (4 hours):
- Phase 1: Align builtins (change NumArgs, update impls)
- Update golden snapshots
- Unit tests for builtin changes

**Day 2** (4 hours):
- Phase 2: S-CALL0 regression tests
- Phase 3: Fix stdlib wrappers
- Integration tests

**Day 3** (2 hours):
- Phase 4: Teaching prompt updates
- Run benchmarks
- Verify no regressions
- Write canonical spec document

**Total: ~10 hours across 3 days**

## Risks & Mitigations

| Risk | Impact | Mitigation |
|------|--------|-----------|
| Breaks existing code using "zero-arg" functions | High | Test all examples; expect breakage (dormant bugs) |
| **Entry runtime still assuming arity-0 `main`** | **High** | **Phase 1.5: Update runtime invocation explicitly** |
| S-CALL0 doesn't work in some contexts | Medium | Comprehensive regression tests; audit elaborator per context list |
| Golden snapshot changes cause test failures | Low | Regenerate with `UPDATE_GOLDEN=1` |
| AI models still confused by `f` vs `f()` | Medium | Clear teaching prompt; emphasize `f()` syntax |
| Type annotation threading interactions | Medium | Re-run concat inference & annotation tests after builtin changes |

## References

- **Triggered by**: M-LANG-CLI-ARGS implementation (v0.4.6)
- **Related features**:
  - S-CALL0 (partial implementation)
  - M-DX1 Builtin System (uses NumArgs)
- **Affected builtins**:
  - `internal/builtins/clock.go` - `_clock_now`
  - `internal/builtins/env.go` - `_env_getArgs`
  - `internal/builtins/io.go` - `_io_readLine`
- **Stdlib wrappers**:
  - `std/clock.ail` - `now()`
  - `std/env.ail` - `getArgs()`
  - `std/io.ail` - `readLine()`
- **Teaching materials**:
  - `prompts/v0.4.6.md` - Needs unit-argument model section
- **Type system**:
  - Hindley-Milner with row polymorphism
  - Unit type `()` as first-class value

## Cross-References to Other M-Docs

**Dependencies & Interactions:**

1. **M-BUG-CONCAT-INFERENCE** (String concatenation type inference)
   - **Impact**: Changing builtin types to `() -> T` adds more explicit function types
   - **Action**: Re-run concat inference tests after M-DX10 implementation
   - **Why**: Typechecker will see more `() -> T` patterns; verify no surprise interactions

2. **Type Annotation Threading** (Expected-type propagation)
   - **Impact**: Users will write explicit types like `() -> [string] ! {Env}` in code
   - **Action**: Ensure annotation threading doesn't silently ignore these types for builtins
   - **Why**: Runtime must not explode when annotations differ from actual types
   - **Critical**: This composes with M-DX10 - both touch typechecker paths

3. **M-LANG-CLI-ARGS** (CLI arguments feature)
   - **Blocks**: This feature is the primary blocker
   - **Unblocks**: After M-DX10, `getArgs()` will be usable from AILANG code
   - **Testing**: Use cli_args benchmark as success metric

**Suggested Implementation Order:**
1. M-DX10 (this doc) - Complete unit-argument model
2. M-LANG-CLI-ARGS - Unblocked by M-DX10
3. Type annotation threading - Builds on stable builtin types
4. M-BUG-CONCAT-INFERENCE - Can now assume `() -> T` is stable

**Note**: M-DX10 and annotation threading compose. Expect to touch both in same release.

## Future Work

**If true nullaries are ever needed** (defer to v0.5.0+):
- Introduce distinct nullary function kind at type level
- Add nullary call construct to core AST
- Define interaction with higher-order functions, partial application
- Update teaching prompt to explain both models

**For now**: The unit-argument model is sufficient, consistent, and AI-friendly.

---

## Appendix: Why the Unit-Argument Model?

**Advantages over true nullaries:**

1. **Simpler elaboration**: No arity-dependent transformations.
2. **Uniform function application**: `f x` covers all cases; `f ()` is just `f` applied to unit.
3. **ML tradition**: Follows OCaml/SML where `()` is the canonical "no meaningful value".
4. **Type system**: Unit is a proper type, not special syntax.
5. **Higher-order functions**: Can pass "zero-arg" functions as values without special cases.
6. **Already implemented**: S-CALL0 exists; this just completes it.

**Disadvantages:**
- Slightly verbose: `f()` instead of `f` (but sugar mitigates this).
- Not obvious from docs: Requires explicit teaching (this doc fixes that).

**Decision rationale**: AILANG prioritizes machine decidability and determinism. The unit-argument model is more compositional and requires less context-dependent reasoning.

---

**Document created**: 2025-11-18
**Last updated**: 2025-11-18 (revised with architectural feedback)
**Replaces**: Previous M-DX10 draft (semantic model changed from true nullaries to unit-argument)
**Discovered by**: M-LANG-CLI-ARGS sprint (Milestone 2)
**Blocks**: CLI arguments feature (v0.4.6)

**Key revisions:**
- Added Phase 1.5: Entry invocation handling (prevents repeat of S-CALL0 failure pattern)
- Expanded S-CALL0 contexts list with explicit inclusions/exclusions
- Added builtin validation invariant (defense against type system bugs)
- Enhanced teaching section with power-user HOF examples
- Added cross-references to type annotation threading and concat inference
- Updated risks to highlight entry runtime assumption

```

### scripts/validate_builtins.sh

```bash
#!/usr/bin/env bash
set -euo pipefail

# Validate AILANG builtins registry

echo "Validating AILANG builtins..."

# Check if ailang command exists
if ! command -v ailang &> /dev/null; then
    echo "✗ ailang command not found"
    echo "  Run: make install"
    exit 1
fi

# Run doctor builtins
echo ""
echo "Running ailang doctor builtins..."
if ailang doctor builtins 2>&1 | tee /tmp/builtin_doctor.log; then
    echo "✓ All builtins are valid"
else
    echo "✗ Builtin validation failed"
    echo "  See /tmp/builtin_doctor.log for details"
    exit 1
fi

# Count builtins
echo ""
echo "Counting registered builtins..."
BUILTIN_COUNT=$(ailang builtins list | grep -c "^\s*_" || true)
echo "✓ Found $BUILTIN_COUNT builtins"

# Check for orphaned builtins
echo ""
echo "Checking for orphaned builtins..."
if ailang builtins check-migration 2>&1 | tee /tmp/builtin_migration.log | grep -q "No orphaned"; then
    echo "✓ No orphaned builtins found"
else
    echo "⚠ Found orphaned builtins - see /tmp/builtin_migration.log"
fi

echo ""
echo "✓ Builtin validation complete"
echo ""
echo "Summary:"
echo "  Total builtins: $BUILTIN_COUNT"
echo "  Status: All valid"
echo ""
echo "Run ./scripts/check_builtin_health.sh for detailed listing"

```

### scripts/check_builtin_health.sh

```bash
#!/usr/bin/env bash
set -euo pipefail

# Check AILANG builtin health and list all builtins

echo "AILANG Builtin Health Check"
echo "============================"
echo ""

# Check if ailang command exists
if ! command -v ailang &> /dev/null; then
    echo "✗ ailang command not found"
    echo "  Run: make install"
    exit 1
fi

# Run doctor
echo "1. Running ailang doctor builtins..."
echo "-----------------------------------"
if ailang doctor builtins; then
    echo ""
    echo "✓ All builtins are valid"
else
    echo ""
    echo "✗ Builtin validation failed"
    exit 1
fi

echo ""
echo "2. Listing builtins by module..."
echo "--------------------------------"
ailang builtins list --by-module

echo ""
echo "3. Builtin statistics..."
echo "------------------------"
TOTAL=$(ailang builtins list | grep -c "^\s*_" || true)
PURE=$(ailang builtins list | grep -c "\[pure\]" || true)
EFFECTS=$((TOTAL - PURE))

echo "Total builtins: $TOTAL"
echo "Pure functions: $PURE"
echo "Effect functions: $EFFECTS"

echo ""
echo "✓ Health check complete"

```

Builtin Developer | SkillHub