Runtime Scripting

Kit supports capability-gated runtime scripting through the Script module. A host program can load a Kit script from source or from a precompiled bytecode artifact, inspect its exports, and call named exported functions through the bytecode VM.

Not Raw Eval

The implemented API is a script engine boundary. Scripts are loaded as modules, calls target exported functions, and data crosses the boundary through Script.Value.

Public API

Script wraps the artifact path and its export table. ScriptAuth is derived from FileReadAuth, so script loading is explicit in function signatures. The public wrapper exposes loading, export discovery, calls, and JSON conversion helpers.

import Auth.File.{file-auth, file-read-auth}
import Auth.Script.{script-auth}
import Script

auth = script-auth (file-read-auth (file-auth env.root))

Script.load-source auth "plugin.kit" source
Script.load-file auth "mods/plugin.kit"
Script.load-bytecode auth "mods/plugin.kitbc"
Script.exports script
Script.has-function? script "on-update"
Script.call auth script "on-update" args
Script.keyword :ready
Script.to-json value
Script.from-json json

Function names may be fully qualified or simple. Calling "on-update" resolves EnemyScript.on-update when no other export has the same simple name; pass the fully qualified name to avoid collisions.

Command Line

The kit script command builds, inspects, and calls script artifacts. kit script build emits a .kitbc bytecode artifact; without -o, the default output appends bc to the source path.

kit script build mods/enemy.kit -o mods/enemy.kitbc
kit script exports mods/enemy.kitbc
kit script call mods/enemy.kitbc on-update '[0.016,{"hp":100,"x":4.0,"y":9.0}]'

kit script call prints the returned value as JSON. Missing exports, arity mismatches, invalid artifacts, and runtime failures are reported as ScriptError.* messages. The JSON argument array is optional and defaults to [].

Host Programs

A host usually creates a Script handle, checks the export it needs, and calls it with Script.Value arguments. The handle stores the artifact path and export table.

module GameHost

import Auth.File.{file-auth, file-read-auth}
import Auth.Script.{script-auth}
import Script

run-update = fn(auth, script) =>
  args = [
    Script.Float 0.016,
    Script.Record [
      ("hp", Script.Int 100),
      ("x", Script.Float 4.0),
      ("y", Script.Float 9.0),
    ],
  ]

  match Script.call auth script "on-update" args
    | Ok value -> value
    | Err e ->
        println "script failed: ${Error.message e}"
        Script.Unit

main = fn(env: Env) =>
  auth = script-auth (file-read-auth (file-auth env.root))
  match Script.load-bytecode auth "mods/enemy.kitbc"
    | Ok script ->
        if Script.has-function? script "on-update" then
          run-update auth script
        else
          Script.Unit
    | Err e ->
        println "could not load script: ${Error.message e}"
        Script.Unit

The script itself is ordinary Kit. Exported functions become callable entry points.

module EnemyScript

export on-update = fn(dt: Float, state) =>
  {
    hp: state.hp,
    x: state.x + (20.0 * dt),
    y: state.y,
  }

Source Loading

Script.load-source and Script.load-file compile Kit source at runtime, cache the resulting script artifact by source hash, and return the same Script handle shape as load-bytecode. Use source loading for tools, development workflows, and user-editable scripts.

source = String.join "\n" [
  "module TestPlugin",
  "",
  "export double = fn(n: Int) => n * 2",
]

match Script.load-source auth "test-plugin.kit" source
  | Ok script -> Script.call auth script "double" [Script.Int 21]
  | Err e -> Err e

For production-style plugin loading, prefer kit script build plus Script.load-bytecode. It keeps the host path smaller and validates the artifact before the VM runs it.

Value Boundary

Script calls accept and return Script.Value. The boundary intentionally uses simple values that can round-trip through JSON.

Script value JSON shape Use
Unit null No meaningful value
Bool, Int, Float, String Native JSON values Primitive data
Keyword {"$keyword":"name"} Keyword tags
Handle {"$handle":42} Opaque host object ids
List JSON array Ordered values
Record JSON object Named fields

Closures, files, sockets, channels, refs, and arbitrary host objects do not cross this boundary. Use Handle when a script needs to refer to a host-side object, and use Script.to-json or Script.from-json when integrating with JSON-based host data.

Errors

Script operations return Result values with structured ScriptError cases.

type ScriptError =
  | ParseError {message: NonEmptyString, file: String, line: NonNegativeInt, column: NonNegativeInt}
  | TypeError {message: NonEmptyString, file: String, line: NonNegativeInt, column: NonNegativeInt}
  | LoadError {message: NonEmptyString}
  | FunctionMissing {name: NonEmptyString}
  | ArityMismatch {name: NonEmptyString, expected: NonNegativeInt, actual: NonNegativeInt}
  | RuntimeError {message: NonEmptyString}
  | PermissionDenied {capability: NonEmptyString}

Bad source becomes ParseError or TypeError with file, line, and column. Missing exports become FunctionMissing. Wrong argument counts become ArityMismatch. VM failures, including recursion and resource limit failures, become RuntimeError.

Capabilities

Application code should use Auth.Script.script-auth. The current authority is backed by FileReadAuth because source files and bytecode artifacts are read from the file system.

import Auth.File.{file-auth, file-read-auth}
import Auth.Script.{script-auth}
import Script

main = fn(env: Env) =>
  file = file-auth env.root
  read = file-read-auth file
  scripts = script-auth read
  Script.load-bytecode scripts "mods/enemy.kitbc"

ScriptRaw.exports-json, ScriptRaw.call-json, and ScriptRaw.load-source-json are implementation details used by Script. They enforce file-read capability checks directly.

Runtime Limits

Script calls run in the bytecode VM with default safety limits:

  • max_frames: 128
  • max_instructions: 10,000,000
  • wall_time_ns: 5 seconds
  • max_allocated_bytes: 64 MiB

These limits catch recursive or runaway scripts and report them as runtime errors. The public Kit wrapper uses the defaults; lower-level Zig integration can provide custom VM setup when embedding scripts directly.

Current Shape

Runtime scripting currently includes source loading, bytecode artifact loading, export discovery, named function calls, structured errors, JSON-backed value conversion, opaque handles, CLI build/call/export helpers, artifact metadata validation, and bytecode VM limits.

It does not expose a general eval builtin, a public host callback registry, direct host object references, or separate script-file/source/host authority types.