# Builtin System Design ## Overview Type-safe builtin system for exposing Go functions to TypeScript/JavaScript with automatic type conversion and defaults support. ## Core Design ### Single Argument Pattern All builtins accept a **single struct argument** on the Go side, but are called as **multi-argument functions** on the JavaScript side. ```go type FetchArgs struct { URL string `json:"url"` Options *FetchOptions `json:"options"` } func Fetch(args FetchArgs) (*FetchResult, error) { // Implementation } // JavaScript calls: // fetch("https://example.com") // fetch("https://example.com", { method: "POST" }) ``` **Mapping Rules:** - JavaScript arguments map **positionally** to struct fields in order - Field names in JavaScript come from `json:""` struct tag, or field name if tag is omitted - Missing JavaScript arguments only allowed for **pointer fields** (non-pointer fields must always be provided) ### Default Values Argument structs **must** implement `Defaults()` receiver method to provide default values: ```go type FetchOptions struct { Method string `json:"method"` Headers map[string]string `json:"headers"` } func (f *FetchOptions) Defaults() *FetchOptions { if f.Method == "" { f.Method = "GET" } return f } // If called with just URL, Options will be nil initially, // then we create default: &FetchOptions{Method: "GET"} ``` **Defaults flow:** 1. Create zero-value struct 2. Fill in provided JavaScript arguments positionally 3. Call `Defaults()` on the struct to fill in remaining defaults ### Empty Arguments Zero-argument builtins must still use a struct (can be empty): ```go // Helper type for no-argument builtins type EmptyArgs struct {} func Ping(args EmptyArgs) bool { return true } // JavaScript: ping() ``` ## Registration API ```go package builtin // RegisterBuiltin registers a Go function as a builtin // The function must accept a single struct argument T and return (R, error) or R func RegisterBuiltin[T any, R any](name string, fn func(T) (R, error)) ``` **Usage:** ```go func init() { builtin.RegisterBuiltin("fetch", Fetch) builtin.RegisterBuiltin("add", Add) } ``` ## Type Conversion ### JavaScript → Go Automatic conversion from goja values to Go struct fields: | JavaScript | Go Type | |------------|---------| | string | string | | number | int, int8-64, uint, uint8-64, float32, float64 | | boolean | bool | | object/map | map[string]any | | null/undefined | nil (for pointer fields only) | **Struct Field Conversion:** - Primitives: converted by type - Maps: `map[string]any` → Go map with string keys - Nested structs: Recursive conversion to nested struct ### Go → JavaScript (Return Types) Automatic conversion from Go return values to JavaScript: | Go Type | JavaScript | |---------|------------| | string, number, bool | primitive | | map[string]any | object | | map[string]string | object | | []any | array | | struct | object with fields from json tags | **Struct Return Types:** - Auto-converted to JavaScript objects - Field names from `json:""` struct tags - Nested structs become nested objects - No custom converters needed **Example:** ```go type FetchResult struct { OK bool `json:"ok"` Status int `json:"status"` Body string `json:"body"` } // Returns: { ok: true, status: 200, body: "..." } ``` ## Error Handling Go errors are automatically converted to JavaScript exceptions: ```go func Fetch(args FetchArgs) (*FetchResult, error) { if someError { return nil, fmt.Errorf("fetch failed") } return result, nil } // JavaScript: fetch calls that error will throw ``` ## TypeScript Declaration Generation TypeScript definitions are auto-generated from Go function signatures: ```go // Go: func Fetch(args FetchArgs) (*FetchResult, error) // Generated TypeScript: declare function fetch(url: string, options?: FetchOptions): FetchResult | never; ``` **Type mapping:** - `string` → `string` - `int/int8-64/uint/uint8-64/float32/float64` → `number` - `bool` → `boolean` - `*T` → `T | null` (or `T` if optional) - `map[string]any` → `Record` - `struct` → Interface with fields **Optional parameters:** - Pointer fields become optional parameters with `T | null` - Non-pointer fields are required ## Implementation Notes ### Reflection-Based Wrapping ```go func createWrapper[T any, R any](fn func(T) (R, error)) func(*goja.Runtime, goja.FunctionCall) goja.Value { return func(vm *goja.Runtime, call goja.FunctionCall) goja.Value { // 1. Create zero-value struct var args T argsValue := reflect.ValueOf(&args).Elem() // 2. Map JavaScript args to struct fields positionally for i := 0; i < min(len(call.Arguments), argsValue.NumField()); i++ { jsArg := call.Arguments[i] field := argsValue.Field(i) // Skip nil/undefined if field is pointer if goja.IsUndefined(jsArg) || goja.IsNull(jsArg) { if field.Kind() == reflect.Ptr { continue } } // Convert and assign converted, err := convertJSValueToGo(vm, jsArg, field.Type()) if err != nil { panic(err) } field.Set(reflect.ValueOf(converted)) } // 3. Call Defaults() if defined if defaults, ok := args.(interface{ Defaults() T }); ok { args = defaults.Defaults() } // 4. Call the Go function result, err := fn(args) if err != nil { panic(err) } // 5. Convert return value to JavaScript return convertGoValueToJS(vm, reflect.ValueOf(result)) } } ``` ### Field Name Extraction ```go func getFieldName(field reflect.StructField) string { jsonTag := field.Tag.Get("json") if jsonTag != "" && jsonTag != "-" { name, _, _ := strings.Cut(jsonTag, ",") return name } return field.Name } ``` ## Migration Examples ### Old → New **Old (current system):** ```go func add(a, b int) int { return a + b } // Register: builtin.RegisterBuiltin("add", add) // JS: add(5, 10) ``` **New system:** ```go type AddArgs struct { A int `json:"a"` B int `json:"b"` } func add(args AddArgs) int { return args.A + args.B } // Register: builtin.RegisterBuiltin("add", add) // JS: add(5, 10) // positional // JS: add(a=5, b=10) // named (if supported) ``` **Old fetch:** ```go func Fetch(url string, options map[string]any) (*FetchResult, error) // Requires custom converter for result ``` **New fetch:** ```go type FetchArgs struct { URL string `json:"url"` Options *FetchOptions `json:"options"` } type FetchOptions struct { Method string `json:"method"` Headers map[string]string `json:"headers"` } func (o *FetchOptions) Defaults() *FetchOptions { if o.Method == "" { o.Method = "GET" } return o } type FetchResult struct { OK bool `json:"ok"` Status int `json:"status"` Body string `json:"body"` } func Fetch(args FetchArgs) (*FetchResult, error) { // Implementation - no custom converter needed } // JS: fetch("https://example.com") // JS: fetch("https://example.com", { method: "POST" }) ``` ## Testing All existing tests must pass after migration: - `internal/runtime/runtime_test.go` - `internal/runtime/standard/fetch_test.go` - `test_data/*.ts` test files Verify: - TypeScript definitions are correct - Positional argument mapping works - Defaults are applied correctly - Named JSON tags work - Error handling propagates - Return value conversion works