On this page
Embedding tasks
Pipit evaluates Go source inside your application. You choose the host functions and packages scripts can call. For module setup and a complete first program, start with Getting started.
Each Go block below is a complete program. In the application workspace created by
the tutorial, replace main.go with the example and run go run .. Expected
values are shown beside the print calls.
Expose a host function
Register functions under an import path with NewInterpreterWithSymbols. This
example exposes one function without registering the standard library:
package main
import (
"context"
"fmt"
"log"
"reflect"
"pipit.sh/pipit"
)
func main() {
symbols := pipit.SymbolExports{
"example.com/host": {
"Greet": reflect.ValueOf(func(name string) string {
return "Hello, " + name
}),
},
}
interp := pipit.NewInterpreterWithSymbols(symbols)
value, err := interp.Eval(context.Background(), `
import "example.com/host"
host.Greet("Pipit")
`)
if err != nil {
log.Fatal(err)
}
fmt.Println(value) // Hello, Pipit
}
Host functions execute native Go with access to your process. Interpreter resource limits cannot forcibly interrupt a native function already running. Review the operations and objects you expose; see execution security.
For reusable registries, implement Exports() pipit.SymbolExports and pass the
provider to WithSymbolProvider. Later providers override earlier symbols with
the same package and name. Symbols passed to NewInterpreterWithSymbols override
all providers supplied through options.
For larger packages, the webserver example
shows symbol generation with pipit extract. Generated
tables are committed, so running that example does not require the generator.
Choose imports
NewInterpreter() registers no host packages. Register only the packages your
scripts need. This example registers two bundles:
package main
import (
"context"
"fmt"
"log"
"pipit.sh/pipit"
"pipit.sh/pipit/sdk/stdlib/codec"
"pipit.sh/pipit/sdk/stdlib/core"
)
func main() {
interp := pipit.NewInterpreter(core.WithCore(), codec.WithCodec())
value, err := interp.Eval(context.Background(), `
import "strings"
strings.ToUpper("hello")
`)
if err != nil {
log.Fatal(err)
}
fmt.Println(value) // HELLO
}
See the bundle reference for the available groups.
Compile and reuse a package
This program compiles two source files, saves the package in build/program.pbc,
reloads it, and runs its entrypoint twice. It prints 42 twice:
package main
import (
"context"
"fmt"
"log"
"pipit.sh/pipit"
)
func main() {
if err := run(); err != nil {
log.Fatal(err)
}
}
func run() error {
ctx := context.Background()
sources := map[string]string{
"main.go": "package main; func result() int { return answer() }",
"answer.go": "package main; func answer() int { return 42 }",
}
store, err := pipit.NewDirectoryBytecodeStore("./build")
if err != nil {
return err
}
interp := pipit.NewInterpreter(pipit.WithBytecodeStore(store))
program, err := interp.CompileFileSet(ctx, sources)
if err != nil {
return err
}
if err := pipit.SaveCompiledToFile(ctx, interp, "./build/program.pbc", program); err != nil {
return err
}
loaded, err := pipit.LoadCompiledFromFile(ctx, interp, "./build/program.pbc")
if err != nil {
return err
}
for range 2 {
value, err := interp.ExecuteEntrypoint(ctx, loaded, "result")
if err != nil {
return err
}
fmt.Println(value) // 42
}
return nil
}
The store must be rooted at the file's parent directory. Programs using host packages need compatible registrations when loaded. The format is versioned; see bytecode compatibility.
Retain state between submissions
An interpreter owns mutable execution state, including package globals. Reuse it
for sequential work and call Reset to clear that state. Use a Session when
later snippets need earlier imports and declarations. Separate Eval calls do
not retain those declarations.
This program retains an import and variable across three submissions and prints
PIPIT:
package main
import (
"context"
"fmt"
"log"
"pipit.sh/pipit"
"pipit.sh/pipit/sdk/stdlib"
)
func main() {
if err := run(); err != nil {
log.Fatal(err)
}
}
func run() error {
ctx := context.Background()
interp := pipit.NewInterpreter(stdlib.WithStandardLibrary())
session := interp.NewSession()
for _, source := range []string{
`import "strings"`,
`name := "pipit"`,
`strings.ToUpper(name)`,
} {
value, err := session.Submit(ctx, source)
if err != nil {
return err
}
if value != nil {
fmt.Println(value)
}
}
session.Reset()
return nil
}
Inspect returns the session's known declarations and imports. A failed
submission does not guarantee rollback of mutations to existing runtime values.
Concurrent use
An interpreter or session must not be evaluated concurrently. Configure a base interpreter, then clone it for each worker or request:
package main
import (
"context"
"fmt"
"log"
"pipit.sh/pipit"
"pipit.sh/pipit/sdk/stdlib"
)
func main() {
if err := run(); err != nil {
log.Fatal(err)
}
}
func run() error {
ctx := context.Background()
// Configure base before starting concurrent work.
base := pipit.NewInterpreter(stdlib.WithStandardLibrary())
// Each worker uses its own clone.
worker := base.Clone()
value, err := worker.Eval(ctx, "1 + 1")
if err != nil {
return err
}
fmt.Println(value) // 2
return nil
}
Clones have fresh execution state and share the symbol registry and configuration. Protect any objects shared by registered host functions with the usual Go concurrency controls. Configure registrations before cloning.
Set limits and logging
Pass WithLogger(logger) to receive interpreter diagnostics. Without it, records
go to slog.Default(). This program sets a one-second timeout and a cost budget.
Any diagnostics go to stderr; this short program produces none and prints 2 on
stdout:
package main
import (
"context"
"fmt"
"log"
"log/slog"
"os"
"pipit.sh/pipit"
"time"
)
func main() {
if err := run(); err != nil {
log.Fatal(err)
}
}
func run() error {
ctx := context.Background()
logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
Level: slog.LevelDebug,
}))
interp := pipit.NewInterpreter(
pipit.WithLogger(logger),
pipit.WithMaxExecutionTime(time.Second),
pipit.WithCostBudget(1000),
)
value, err := interp.Eval(ctx, "1 + 1")
if err != nil {
return err
}
fmt.Println(value) // 2
return nil
}
Restrict imports and capabilities
The public restricted constructor requires a symbol provider. In the workspace
from Getting started, save this as main.go and
run go run .. It prints 7:
package main
import (
"context"
"fmt"
"log"
"pipit.sh/pipit"
"pipit.sh/pipit/sdk/stdlib"
)
func main() {
interp, err := pipit.NewRestrictedInterpreter(pipit.RestrictedConfig{
Imports: []string{"math"},
}, stdlib.Providers()...)
if err != nil {
log.Fatal(err)
}
result, err := interp.Eval(context.Background(), `
import "math"
math.Sqrt(49)
`)
if err != nil {
log.Fatal(err)
}
fmt.Println(string(result.Value)) // 7
}
Control native calls with a capability hook
A capability hook lets the host allow or deny native calls. This example registers
two host functions, allows Greet, and rejects Farewell:
package main
import (
"context"
"errors"
"fmt"
"log"
"reflect"
"runtime"
"pipit.sh/pipit"
)
type selectedFunction struct {
pipit.DenyCapabilityHook
name string
}
func (hook selectedFunction) CheckFunctionCall(_ context.Context, _, name string, _ []reflect.Value) error {
if name == hook.name {
return nil
}
return pipit.ErrCapabilityDenied
}
func greet(name string) string { return "Hello, " + name }
func farewell(name string) string { return "Goodbye, " + name }
func main() {
allowed := reflect.ValueOf(greet)
hook := selectedFunction{name: runtime.FuncForPC(allowed.Pointer()).Name()}
interp := pipit.NewInterpreterWithSymbols(pipit.SymbolExports{
"example.com/host": {
"Greet": allowed,
"Farewell": reflect.ValueOf(farewell),
},
}, pipit.WithCapabilityHook(hook))
value, err := interp.Eval(context.Background(), `import "example.com/host"
host.Greet("Pipit")`)
if err != nil {
log.Fatal(err)
}
fmt.Println(value) // Hello, Pipit
_, err = interp.Eval(context.Background(), `import "example.com/host"
host.Farewell("Pipit")`)
fmt.Println(errors.Is(err, pipit.ErrCapabilityDenied)) // true
}
The hook receives the native function's name, which can differ from its script
import name. The example gets that name from the registered Go function.
errors.Is checks whether the second call failed with ErrCapabilityDenied.
DenyCapabilityHook denies the typed file, network, process, and environment
checks. Its CheckFunctionCall method allows calls by default, so this example
overrides that method to select one function. PermissiveCapabilityHook allows
all checks by default. Embed it only when you want to deny specific operations.
Use WithCapabilityHook for ordinary embedding, or set RestrictedConfig.Hook
for restricted execution. Typed checks such as CheckFileOpen can inspect a path
or arguments, but only cover operations the interpreter recognises. Review the
functions you register: work inside a native function does not become a separate
script call. Hooks do not provide process isolation.
Attach a debugger
Run evaluation in a goroutine so the host can respond while the script is paused.
This program stops before value++, reads a local variable, evaluates
value * 2, then steps over the increment and continues:
package main
import (
"context"
"fmt"
"log"
"time"
"pipit.sh/pipit"
)
func main() {
if err := run(); err != nil {
log.Fatal(err)
}
}
func run() error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
debugger := pipit.NewDebugger()
interp := pipit.NewInterpreter(pipit.WithDebugger(debugger), pipit.WithDebugInfo())
program, err := interp.CompileFileSet(ctx, map[string]string{
"script.go": `package main
func answer() int {
value := 21
value++
return value
}`,
})
if err != nil {
return err
}
breakpoint := debugger.SetBreakpoint("script.go", 4)
if !breakpoint.Verified {
return fmt.Errorf("breakpoint: %s", breakpoint.Message)
}
done := make(chan error, 1)
go func() {
_, err := interp.ExecuteEntrypoint(ctx, program, "answer")
done <- err
}()
defer func() { cancel(); <-done }()
event, err := debugger.WaitForPause(ctx)
if err != nil {
return err
}
frames, err := debugger.StackTrace(event.ThreadID)
if err != nil {
return err
}
fmt.Println("paused in", frames[0].Function)
locals, err := debugger.Variables(event.ThreadID, 0, pipit.ScopeLocals)
if err != nil {
return err
}
for _, local := range locals {
if local.Name == "value" {
fmt.Println("value:", local.Value)
}
}
result, err := debugger.Evaluate(ctx, event.ThreadID, 0, "value * 2", pipit.EvalOptions{})
if err != nil {
return err
}
fmt.Println("value * 2:", result.Value)
if err := debugger.StepOver(event.ThreadID); err != nil {
return err
}
event, err = debugger.WaitForPause(ctx)
if err != nil {
return err
}
result, err = debugger.Evaluate(ctx, event.ThreadID, 0, "value", pipit.EvalOptions{})
if err != nil {
return err
}
fmt.Println("after step:", result.Value)
if err := debugger.Continue(); err != nil {
return err
}
for {
event, err := debugger.WaitForEvent(ctx)
if err != nil {
return err
}
if event.Kind == pipit.DebugEventExited {
if event.Err != nil {
return event.Err
}
fmt.Println("finished")
return nil
}
}
}
Expected output:
paused in main.answer
value: 21
value * 2: 42
after step: 22
finished
The context limits the whole example to five seconds. The host waits for the
evaluation goroutine before returning, including when inspection fails.
CompileFileSet gives the source a stable filename for breakpoints, and
WithDebugInfo keeps the information needed to inspect variables.
Most debugger methods return errors. WaitForPause returns ErrDebugExited if the
script ends before it pauses; Eval does not report its end, so use the other run
methods with a debugger. Inspection and stepping require a paused script;
otherwise they return ErrDebugNotPaused. WaitForEvent also reports thread
starts, thread exits, and the final execution result. See the
debugger reference.
See debugging.go for the public API and the DAP guide for editor usage and supported behaviour.
See the option and error reference for configuration details.
Related guides
- Compatibility: language and native interoperability limitations.
- Execution security: restricted embedding and experimental isolation.
- CLI reference: commands for running and inspecting scripts.