Native Functions

Register Go functions that can be called from Scriptling scripts using the Native API.

Basic Function

For functions that only use positional arguments:

import (
    "context"
    "github.com/paularlott/scriptling"
    "github.com/paularlott/scriptling/object"
)

func main() {
    p := scriptling.New()

    p.RegisterFunc("double", func(ctx context.Context, kwargs object.Kwargs, args ...object.Object) object.Object {
        if len(args) != 1 {
            return &object.Error{Message: "double requires 1 argument"}
        }

        if intObj, ok := args[0].(*object.Integer); ok {
            return &object.Integer{Value: intObj.Value * 2}
        }

        return &object.Error{Message: "argument must be integer"}
    })

    p.Eval(`
result = double(21)
print(result)  # 42
`)
}

Function with Keyword Arguments

Functions can accept keyword arguments using the kwargs wrapper:

p.RegisterFunc("make_duration", func(ctx context.Context, kwargs object.Kwargs, args ...object.Object) object.Object {
    // Reject positional arguments
    if len(args) > 0 {
        return &object.Error{Message: "make_duration takes no positional arguments"}
    }

    // Use kwargs helper methods with defaults
    hours, err := kwargs.GetFloat("hours", 0.0)
    if err != nil {
        return &object.Error{Message: err.Error()}
    }

    minutes, err := kwargs.GetFloat("minutes", 0.0)
    if err != nil {
        return &object.Error{Message: err.Error()}
    }

    seconds, err := kwargs.GetFloat("seconds", 0.0)
    if err != nil {
        return &object.Error{Message: err.Error()}
    }

    totalSeconds := hours*3600 + minutes*60 + seconds
    return &object.Float{Value: totalSeconds}
})

p.Eval(`
duration = make_duration(hours=2, minutes=30)
print(duration)  # 9000.0
`)

Mixed Positional and Keyword Arguments

p.RegisterFunc("format_greeting", func(ctx context.Context, kwargs object.Kwargs, args ...object.Object) object.Object {
    if len(args) != 1 {
        return &object.Error{Message: "format_greeting requires name argument"}
    }

    name, err := args[0].AsString()
    if err != nil {
        return &object.Error{Message: "name must be string"}
    }

    // Use kwargs helper methods with defaults
    prefix, err := kwargs.GetString("prefix", "Hello")
    if err != nil {
        return &object.Error{Message: err.Error()}
    }

    suffix, err := kwargs.GetString("suffix", "!")
    if err != nil {
        return &object.Error{Message: err.Error()}
    }

    return &object.String{Value: prefix + ", " + name + suffix}
})

p.Eval(`
print(format_greeting("World"))                    # Hello, World!
print(format_greeting("World", prefix="Hi"))       # Hi, World!
print(format_greeting("World", suffix="..."))      # Hello, World...
print(format_greeting("World", prefix="Hey", suffix="?"))  # Hey, World?
`)

Kwargs Helper Methods

The object.Kwargs type provides convenient helper methods:

Method Description
GetString(name, default) (string, Object) Extract string, return default if missing
GetInt(name, default) (int64, Object) Extract int (accepts Integer/Float)
GetFloat(name, default) (float64, Object) Extract float (accepts Integer/Float)
GetBool(name, default) (bool, Object) Extract bool
GetList(name, default) ([]Object, Object) Extract list elements
Has(name) bool Check if key exists
Keys() []string Get all keys
Len() int Get number of kwargs
Get(name) Object Get raw Object value

Must* Variants

For simple cases where you want to use defaults on any error:

Method Description
MustGetString(name, default) string Extract string, ignore errors
MustGetInt(name, default) int64 Extract int, ignore errors
MustGetFloat(name, default) float64 Extract float, ignore errors
MustGetBool(name, default) bool Extract bool, ignore errors
MustGetList(name, default) []Object Extract list, ignore errors

Type-Safe Accessor Methods

All Scriptling objects implement type-safe accessor methods:

// Using type-safe accessors (clean!)
p.RegisterFunc("add_tax", func(ctx context.Context, kwargs object.Kwargs, args ...object.Object) object.Object {
    if len(args) != 2 {
        return &object.Error{Message: "add_tax requires 2 arguments"}
    }

    // AsFloat() automatically handles both Integer and Float
    price, err := args[0].AsFloat()
    if err != nil {
        return &object.Error{Message: "price: " + err.Error()}
    }

    rate, err := args[1].AsFloat()
    if err != nil {
        return &object.Error{Message: "rate: " + err.Error()}
    }

    result := price * (1 + rate)
    return &object.Float{Value: result}
})

Available Accessor Methods

Method Description
AsString() (string, Object) Extract string value
AsInt() (int64, Object) Extract integer (floats truncate)
AsFloat() (float64, Object) Extract float (ints convert automatically)
AsBool() (bool, Object) Extract boolean
AsList() ([]Object, Object) Extract list/tuple elements (returns a copy)
AsDict() (map[string]Object, Object) Extract dict as map (keys are human-readable strings)

Note: The second return value is nil on success, or an *Error object on failure. You can check for errors like if err != nil { ... }.

Coercion Methods

Method Type Safety Description
AsString() Strict Returns string only if object is a STRING
AsInt() Strict Returns int64 only if object is INTEGER/FLOAT
AsFloat() Strict Returns float64 only if object is INTEGER/FLOAT
CoerceString() Loose Auto-converts any type to string
CoerceInt() Loose Auto-converts strings/floats to int
CoerceFloat() Loose Auto-converts strings/ints to float

Function with Output Capture

Functions can write to the output capture system:

import (
    "fmt"
    "github.com/paularlott/scriptling/evaluator"
)

p.RegisterFunc("debug_print", func(ctx context.Context, kwargs object.Kwargs, args ...object.Object) object.Object {
    // Get environment from context
    env := evaluator.GetEnvFromContext(ctx)
    writer := env.GetWriter()

    // Print debug information
    fmt.Fprintf(writer, "[DEBUG] Function called with %d arguments\n", len(args))
    for i, arg := range args {
        fmt.Fprintf(writer, "[DEBUG] Arg %d: %s\n", i, arg.Inspect())
    }

    return &object.String{Value: "logged"}
})

Adding Help Text

p.RegisterFunc("calculate", func(ctx context.Context, kwargs object.Kwargs, args ...object.Object) object.Object {
    // Implementation
    return &object.Integer{Value: 42}
}, `calculate(x, y) - Perform calculation

  Parameters:
    x - First number
    y - Second number

  Returns:
    The calculated result

  Examples:
    calculate(10, 5)  # Returns 15
`)

// Users can then access help:
// help("calculate")  # Shows the documentation

If you omit the help text, basic help will be auto-generated:

p.RegisterFunc("my_func", func(...) object.Object {
    // Auto-generates: "my_func(...) - User-defined function"
    return object.NULL
})

Return Types

Integers

return &object.Integer{Value: 42}
return object.NewInteger(42)

Floats

return &object.Float{Value: 3.14}
return object.NewFloat(3.14)

Strings

return &object.String{Value: "hello"}

Booleans

return &object.Boolean{Value: true}
return object.True
return object.False

Lists

return &object.List{Elements: []object.Object{
    &object.Integer{Value: 1},
    &object.String{Value: "two"},
}}

Dictionaries

return object.NewStringDict(map[string]object.Object{
    "name":  &object.String{Value: "Alice"},
    "count": &object.Integer{Value: 42},
})

None/Null

return object.None

Errors

return &object.Error{Message: "something went wrong"}

Context Usage

Cancellation

p.RegisterFunc("long_operation", func(ctx context.Context, kwargs object.Kwargs, args ...object.Object) object.Object {
    for i := 0; i < 1000000; i++ {
        // Check for cancellation
        select {
        case <-ctx.Done():
            return &object.Error{Message: "operation cancelled"}
        default:
            // Continue processing
        }
    }
    return &object.Integer{Value: 1}
})

Timeout Handling

p.RegisterFunc("fetch_with_timeout", func(ctx context.Context, kwargs object.Kwargs, args ...object.Object) object.Object {
    url, _ := args[0].AsString()
    timeoutSec, _ := kwargs.GetInt("timeout", 30)

    // Create child context with timeout
    childCtx, cancel := context.WithTimeout(ctx, time.Duration(timeoutSec)*time.Second)
    defer cancel()

    // Use childCtx for operations
    result := fetchData(childCtx, url)

    return result
})

Best Practices

1. Always Validate Arguments

p.RegisterFunc("divide", func(ctx context.Context, kwargs object.Kwargs, args ...object.Object) object.Object {
    if len(args) != 2 {
        return &object.Error{Message: "divide requires 2 arguments: (a, b)"}
    }

    a, err := args[0].AsFloat()
    if err != nil {
        return &object.Error{Message: "first argument must be a number"}
    }

    b, err := args[1].AsFloat()
    if err != nil {
        return &object.Error{Message: "second argument must be a number"}
    }

    if b == 0 {
        return &object.Error{Message: "division by zero"}
    }

    return &object.Float{Value: a / b}
})

2. Provide Help Text

p.RegisterFunc("process", func(ctx context.Context, kwargs object.Kwargs, args ...object.Object) object.Object {
    // implementation
}, `process(data, options={}) - Process data with options

Parameters:
  data - Input data to process
  options - Optional configuration dictionary
    - format: Output format ("json", "xml")
    - validate: Validate input (default: True)

Returns:
  Processed result as dictionary`)

3. Handle Context

p.RegisterFunc("fetch", func(ctx context.Context, kwargs object.Kwargs, args ...object.Object) object.Object {
    url, _ := args[0].AsString()

    // Create HTTP request with context
    req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
    if err != nil {
        return &object.Error{Message: err.Error()}
    }

    // ... rest of implementation
})

4. Use Kwargs for Optional Parameters

p.RegisterFunc("connect", func(ctx context.Context, kwargs object.Kwargs, args ...object.Object) object.Object {
    host, _ := args[0].AsString()

    // Optional parameters with defaults
    port, _ := kwargs.GetInt("port", 8080)
    timeout, _ := kwargs.GetInt("timeout", 30)
    useTLS, _ := kwargs.GetBool("tls", false)

    // implementation
})

See Also