wile

package
v1.20.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 26, 2026 License: Apache-2.0 Imports: 30 Imported by: 0

Documentation

Overview

Package wile provides the public API for embedding the Wile Scheme interpreter.

Basic usage:

engine, err := wile.NewEngine(ctx)
if err != nil {
    log.Fatal(err)
}
result, err := engine.Eval(ctx, engine.MustParse(ctx, "(+ 1 2 3)"))
fmt.Println(result.SchemeString()) // 6

Profile-based configuration:

engine, err := wile.NewEngine(ctx, wile.WithProfile(wile.Small))

engine, err := wile.NewEngine(ctx,
    wile.WithProfile(wile.Console),
    wile.WithEnv("APP_MODE", "production"),
)

Sandboxed eval/load (wile-goast pattern):

engine, err := wile.NewEngine(ctx, wile.WithProfile(wile.ConsoleWithLoad))

Ad-hoc extension selection (bypasses profiles):

engine, err := wile.NewEngine(ctx,
    wile.WithExtension(io.Extension),
    wile.WithExtension(system.Extension),
)

Strict namespace (core-only top level; layer R7RS libraries on top):

engine, err := wile.NewEngine(ctx,
    wile.WithProfile(wile.Small), wile.WithStrictNamespace(),
    wile.WithSourceFS(stdlib.FS), wile.WithLibraryPaths(),
)
// (display 1) errors until imported; (import (scheme r5rs)) layers it on.

One step further — nothing pre-bound at all, so every dependency is declared in source. Only the core special forms remain, because they are phase handlers rather than bindings:

engine, err := wile.NewEngine(ctx,
    wile.WithProfile(wile.Small), wile.WithoutAmbientBindings(),
    wile.WithSourceFS(stdlib.FS), wile.WithLibraryPaths(),
)
// (car '(1 2)) errors too; (import (scheme base)) restores it.
// Budget ~9.4 ms per import — see WithoutAmbientBindings.

Custom primitives:

engine, _ := wile.NewEngine(ctx)
engine.RegisterPrimitive(wile.PrimitiveSpec{
    Name:       "my-func",
    ParamCount: 1,
    Impl:       myFuncImpl,
})

Index

Examples

Constants

View Source
const (
	PhaseTemplate = environment.PhaseTemplate
	PhaseRuntime  = environment.PhaseRuntime
	PhaseExpand   = environment.PhaseExpand
	PhaseCompile  = environment.PhaseCompile
)

Phase constants for LibraryImportEvent.Phase and other phase-keyed APIs. Re-exported from environment for embedder convenience.

View Source
const DefaultMaxCallDepth int = 10000

DefaultMaxCallDepth is the default call depth limit for new engines. At ~500 bytes per frame, 10000 frames ≈ 5MB. Use WithMaxCallDepth(0) to opt out of the limit explicitly; WithMaxCallDepth(n) with n < 0 is clamped to 0 (also unlimited).

Variables

View Source
var EmptyList = wrapValue(values.EmptyList)

EmptyList is the empty list.

View Source
var ErrEngineClosed = werr.NewStaticError("engine is closed")

ErrEngineClosed is returned when Close is called on an already-closed engine.

View Source
var False = wrapValue(values.FalseValue)

False is the #f value.

View Source
var StdLibFS fs.FS = stdlib.LibFS

StdLibFS provides the standard Scheme libraries shipped with wile (e.g., (wile algebra), (wile control), etc.) as an embedded filesystem. Library paths resolve as "lib/...". Consumers add it to the engine with WithSourceFS(StdLibFS) paired with WithLibraryPaths("lib"): the tree keeps its "lib/" prefix, which is not on the default search path, so WithSourceFS alone resolves nothing. (stdlib.FS is the prefix-stripped variant and needs no extra search path.)

The bytes live in the pkg/stdlib package's embed (//go:embed lib). Because go:embed forbids "..", this package re-exports stdlib.LibFS rather than embedding the tree a second time; the two share one copy and one shape.

View Source
var True = wrapValue(values.TrueValue)

True is the #t value.

View Source
var Void = wrapValue(values.Void)

Void is the void value.

Functions

func IsBoolean

func IsBoolean(v Value) bool

IsBoolean returns true if v is a boolean.

func IsIncompleteInput

func IsIncompleteInput(err error) bool

IsIncompleteInput reports whether a parse error indicates the input is a valid prefix of an expression that needs more input to complete. This is useful for REPL implementations that accumulate multi-line input.

Detection is structural (errors.Is), not string matching:

  • wrapped io.EOF — a truncated token at end of stream;
  • io.ErrUnexpectedEOF — the parser ran out of input inside a form (wrapMidParseEOF; covers unclosed lists/vectors/block comments);
  • werr.ErrIncompleteInput — the tokenizer hit EOF inside an unterminated string or extended symbol;
  • parser.ErrUnknownTokenType — a partial token from premature EOF.

Returns false for nil and bare io.EOF (a clean end of stream).

func IsList

func IsList(v Value) bool

IsList returns true if v is a proper list (including the empty list).

func IsNull

func IsNull(v Value) bool

IsNull returns true if v is the empty list.

func IsNumber

func IsNumber(v Value) bool

IsNumber returns true if v is a number.

func IsPair

func IsPair(v Value) bool

IsPair returns true if v is a non-empty pair (cons cell). EmptyList is not a *Pair (it's a separate type), so the type assertion handles the distinction without an explicit IsEmptyList check.

func IsProcedure

func IsProcedure(v Value) bool

IsProcedure returns true if v is a callable procedure (lambda, foreign closure, case-lambda, parameter, captured continuation, or composable continuation).

func IsString

func IsString(v Value) bool

IsString returns true if v is a string.

func IsSymbol

func IsSymbol(v Value) bool

IsSymbol returns true if v is a symbol.

func NewNamespace

func NewNamespace(ctx context.Context, opts ...EngineOption) (*environment.Namespace, error)

NewNamespace creates a fully initialized namespace with a registry, base environment bindings, syntax compilers, expanders, and bootstrap macros. The namespace is then given to NewEngineWithNamespace.

Options are shared with NewEngine: WithExtension, WithRegistry, WithoutCore, WithAuthorizer all work. Engine-specific options (WithMaxCallDepth, WithLibraryPaths, etc.) are accepted but ignored.

Example:

ns, err := wile.NewNamespace(ctx,
    wile.WithExtension(math.Extension),
    wile.WithAuthorizer(security.ReadOnly()),
)
eng, err := wile.NewEngineWithNamespace(ctx, ns)

func ToGoBool

func ToGoBool(v Value) (bool, bool)

ToGoBool extracts a Go bool from a Scheme boolean value. Returns (false, false) if v is not a boolean.

func ToGoFloat

func ToGoFloat(v Value) (float64, bool)

ToGoFloat extracts a float64 from an inexact real value. Returns (0, false) if v is not a Float.

func ToGoInt

func ToGoInt(v Value) (int64, bool)

ToGoInt extracts an int64 from an exact integer value. Returns (0, false) if v is not an exact integer or does not fit in int64.

func ToGoString

func ToGoString(v Value) (string, bool)

ToGoString extracts the Go string from a Scheme string value. Returns ("", false) if v is not a string.

Types

type BootstrapProcedureRewriter added in v1.18.0

type BootstrapProcedureRewriter interface {
	// RewriteBootstrapProcedures receives the engine's bootstrap procedure sources
	// (in load order) and returns the set to bind instead. Return the input unchanged
	// to opt out. The result should preserve load order — later sources may reference
	// definitions from earlier ones.
	RewriteBootstrapProcedures(sources []string) []string
}

BootstrapProcedureRewriter is an optional capability a Dialect may implement to substitute bootstrap procedure sources before they load into the sealed base. It is how a dialect crosses the last ceiling: a bootstrap procedure that depends on a primitive the dialect wants gone (the eager vector-map / string-map are built with vector-set! / string-set!). Removing the primitive alone breaks NewEngine — the bootstrap definition no longer compiles; a global reference resolves by slot at call time so it cannot be unbound afterward either. Rewriting the source is the clean fix.

When a dialect passed to WithDialect also implements BootstrapProcedureRewriter, the engine calls RewriteBootstrapProcedures with the current procedure sources and binds the returned set instead (on a copy — the full registry that backs library environments is untouched). A no-mutation dialect swaps the mutating vector-map/string-map fragment for a mutation-free one, so the mutation primitives can be removed entirely rather than retained. See NoMutation.

type CallContext

type CallContext = machine.CallContext

CallContext is the extension-facing subset of MachineContext. This is a re-export of machine.CallContext for convenience.

type CompilationError

type CompilationError struct {
	Message   string
	Source    string // formatted source location ("file:line:col"), empty if unavailable
	Cause     error
	Condition Value // the condition Scheme would see; nil when there is no cause
	// contains filtered or unexported fields
}

CompilationError wraps errors from parsing, expanding, or compiling Scheme code.

Source

Source provides the source location ("file:line:col") where the error occurred, when available. Compilation errors from the core compiler include source locations; parse errors and some edge cases may have an empty Source.

Condition

Condition holds the same condition object a Scheme guard would have caught had the failure crossed a primitive frame — the compile-time counterpart to RuntimeError.Condition, built by the one converter both funnels use (machine.ConditionFromError). It is nil only when there was no cause to convert. Reading it is how a host recovers the message, irritants, kind and location as values instead of parsing them back out of Error().

func (*CompilationError) Error

func (p *CompilationError) Error() string

func (*CompilationError) Unwrap

func (p *CompilationError) Unwrap() error

type CompiledCode

type CompiledCode struct {
	// contains filtered or unexported fields
}

CompiledCode represents compiled Scheme code ready for execution.

CompiledCode captures the environment from the Engine that compiled it and always executes using that captured environment, regardless of which Engine is used to run it. Using a different Engine instance affects only that Engine's own bookkeeping (for example, evaluation counters), not the environment bindings or syntax interning.

CompiledCode can be run multiple times. It is not safe for concurrent execution (the underlying Engine is not goroutine-safe).

Stability contract:

  • In-memory and process-local. CompiledCode holds live pointers into the compiling Engine's machine template and environment. It is not serializable and cannot be persisted to disk, sent over the wire, or reloaded in a different process. There is no on-disk bytecode format.
  • No cross-version format stability. The internal bytecode and template representation are implementation details that change between releases (often within a minor version). Do not depend on their shape.
  • Trusted-input only. A CompiledCode is the output of this process's own compiler. There is no facility to construct one from untrusted bytes, so there is no untrusted-bytecode attack surface to validate.

To "cache" compilation, keep the CompiledCode value alive within the running program; to share work across processes, share the Scheme source.

func (*CompiledCode) String

func (p *CompiledCode) String() string

String returns a string representation of the compiled code.

type Dialect added in v1.18.0

type Dialect interface {
	// Name identifies the dialect for diagnostics (e.g. "r6rs").
	Name() string

	// InstallForms customizes the per-engine forms registry. It runs once, at
	// engine origin, on a fresh clone of the R7RS default.
	InstallForms(fr *forms.FormRegistry) error
}

Dialect customizes an engine's special-form surface. At engine construction (via WithDialect) the engine forks a copy of the R7RS-default forms registry and hands it to InstallForms, which may add, remove, or rename special forms for that engine only. This is how a non-R7RS standard (a strict R5RS, R6RS, or a bespoke embedding dialect) plugs in like an extension — see the dialect roadmap in plans/ARCHITECTURE.local.md.

The registry passed to InstallForms is a per-engine clone, copy-on-write over the shared default: mutating it (fr.Remove, fr.RegisterValidator, fr.RegisterCompiler, fr.Register) affects only the engine being built, never the default or any other engine. Since SP1 (per-engine codegen dispatch) a dialect can also install a form's compiler via fr.RegisterCompiler, so it may introduce forms with bespoke codegen — not only remove or rename existing ones.

InstallForms should return a non-nil error (wrapped with a werr sentinel) to abort engine construction; the engine surfaces it wrapped in werr.ErrEngineInit.

Boundary: InstallForms takes *forms.FormRegistry, which lives in the internal package pkg/internal/forms. Only code within this module (in-tree dialects such as the future R7RS/R6RS packages) can implement Dialect; an external embedder in another module cannot. A public embedder-facing dialect API is deferred to a later phase.

var DefaultDialect Dialect = r7rsDialect{}

DefaultDialect is the R7RS baseline dialect, applied by every engine that does not supply its own via WithDialect. R7RS is thus a dialect like any other — the default one — not a hardcoded special case: engine construction always forks the R7RS-default forms registry and applies a dialect to it, uniformly.

Its InstallForms is a no-op: the forked registry already carries the R7RS Tier-1 special forms (they are registered onto the package-default registry at init time, and the fork clones them). DefaultDialect names that baseline and gives derived dialects a base to start from — e.g. a mutation-free dialect that starts as DefaultDialect and removes set!.

var NoMutation Dialect = noMutationDialect{}

NoMutation is a dialect that derives from the R7RS baseline and removes the in-place mutation surface entirely: the set! special form (via InstallForms), ALL the mutation primitives (set-car!, vector-set!, string-set!, …, via the optional PrimitiveRemover capability), and — via BootstrapProcedureRewriter — the bootstrap's own use of them. The eager vector-map / string-map are built with vector-set! / string-set!; NoMutation swaps that bootstrap fragment for a mutation-free one (core.ImmutableVectorStringMapSource) so those two primitives can be removed with the rest, leaving a top level in which nothing can be destructively updated and vector-map / string-map still work (built functionally). The default engine keeps the faster mutating fragment — only a no-mutation engine pays the functional cost.

Which primitives count as destructive is declared on the spec (registry.PrimitiveSpec.Mutates), not read off a "!" suffix. The suffix is a naming convention and it was wrong in both directions: record-modifier destructively sets a record field and has no bang, while thread-start! and mutex-lock! carry one and act on the world rather than on a value a program holds.

One consequence is user-visible and worth stating before it is met: define-record-type expands to (record-modifier type 'field-tag) unconditionally, so on a NoMutation engine a record type that DECLARES a modifier fails at DEFINITION time with an unbound-identifier error, before any instance exists. Not "you cannot mutate it" — you cannot define it. A record type that declares no modifier is unaffected.

Boundary: removal is at the visible top level only. The full registry still backs library environments, so a program can reach a mutator again via (import (scheme base)). NoMutation is therefore a *language-surface* statement, not a hard capability guarantee: it narrows what the flat top level offers, it does not enforce immutability. For a runtime capability sandbox use security.Authorizer, which composes orthogonally with any dialect.

type Engine

type Engine struct {
	// contains filtered or unexported fields
}

Engine is the main entry point for embedding Wile.

An Engine is NOT safe for concurrent use from multiple goroutines. Most methods that parse, compile, or evaluate code mutate the environment. Each goroutine should use its own Engine, or synchronize externally.

Within an Engine, the VM coordinates its own runtime structures and SRFI-18 thread scheduling. It does NOT make concurrent mutation of shared Scheme objects atomic: vector-set!, set-car!/set-cdr!, record and port writes, and set! on a captured variable are plain stores. Programs that share mutable state across SRFI-18 threads must synchronize it themselves, with SRFI-18 mutexes or the atomic primitives.

func NewEngine

func NewEngine(ctx context.Context, opts ...EngineOption) (*Engine, error)

NewEngine creates a new Wile engine, building its own namespace. By default, only core primitives are included. Use WithExtension to add optional extensions.

NewEngine accepts every option, namespace-consumed and engine-only alike, because it performs the bootstrap that consumes the first group. To build an engine over a namespace you already have, use NewEngineWithNamespace, whose variadic is narrowed to EngineOnlyOption — a namespace option handed to it is a compile error rather than a silent drop.

Initialization Order Invariant

NewEngine performs 6 initialization steps that MUST execute in this order. Each step depends on prior steps; reordering causes silent failures or panics.

  1. Config — build engineConfig from options
  2. Registry — buildRegistry(cfg): register core + extension primitives
  3. Namespace — NewNamespace() + SetRegistry, then the config writes: SetAuthorizer, SetEnvMap, SetImmutableTopLevel, SetContractEnforcement. The last two must precede step 4, which reads them back via applyOptionsFromNamespace.
  4. Bootstrap — applyBaseEnvironment: bind primitives, syntax compilers, expanders, bootstrap macros (uses EmbedFileResolver, NOT the runtime file resolver)
  5. File resolver — env.SetFileResolver: runtime include/load resolver. Must come AFTER bootstrap (step 4) so bootstrap uses its own EmbedFileResolver, not the runtime resolver.
  6. Library system — setupLibrarySystem: search paths, extension libraries, library env factory. Requires file resolver (step 5) and bootstrap macros (step 4) for define-library parsing.

Steps 2-4 are exactly what NewNamespace performs, so NewEngineWithNamespace skips them and trusts that the caller bootstrapped correctly. Steps 5 and 6 run on both paths — 5 only if the supplied namespace has no file resolver of its own, 6 only if WithLibraryPaths was passed, which it can be to either constructor.

Example
package main

import (
	"context"
	"fmt"
	"log"

	"github.com/aalpar/wile/pkg/wile"
)

func main() {
	engine, err := wile.NewEngine(context.Background())
	if err != nil {
		log.Fatal(err)
	}

	ctx := context.Background()
	result, err := engine.Eval(ctx, engine.MustParse(ctx, "(+ 1 2 3)"))
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println(result.SchemeString())
}
Output:
6
Example (WithExtension)
package main

import (
	"context"
	"fmt"
	"log"

	"github.com/aalpar/wile/pkg/extensions/io"
	"github.com/aalpar/wile/pkg/wile"
)

func main() {
	_, err := wile.NewEngine(context.Background(),
		wile.WithExtension(io.Extension),
	)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println("engine created with I/O extension")
}
Output:
engine created with I/O extension

func NewEngineWithNamespace added in v1.20.0

func NewEngineWithNamespace(ctx context.Context, ns *environment.Namespace, opts ...EngineOnlyOption) (*Engine, error)

NewEngineWithNamespace creates an engine over a namespace built by NewNamespace, instead of building one. This enables sharing a namespace across engines, or pre-configuring one with specific capabilities.

The namespace is a PARAMETER, not an option, and the variadic is narrowed to EngineOnlyOption. Together those make "you passed a namespace option to the engine constructor" a compile error: an option only bootstrapNamespace can consume (WithProfile, WithSandbox, WithAuthorizer, WithDialect, …) does not implement EngineOnlyOption, so it cannot be written here at all. Pass those to NewNamespace, which consumes every one of them, and give its result here.

Example:

ns, err := wile.NewNamespace(ctx,
    wile.WithExtension(math.Extension),
    wile.WithAuthorizer(security.ReadOnly()),
)
eng, err := wile.NewEngineWithNamespace(ctx, ns, wile.WithLibraryPaths("."))

A nil namespace is an error, not a request to bootstrap one: this parameter names the namespace the engine is built on, and there is no engine without it. Use NewEngine when you want one built for you.

func (*Engine) AvailableLibraries

func (p *Engine) AvailableLibraries(ctx context.Context) ([]LibraryName, error)

AvailableLibraries returns all importable library names by combining filesystem discovery with registry-known libraries (synthetic extensions). Returns a sorted, deduplicated list. If the library system is not enabled (no WithLibraryPaths call), returns an empty list.

func (*Engine) BoundNames

func (p *Engine) BoundNames() []string

BoundNames returns a sorted, deduplicated list of every binding name visible in the engine across all phases (runtime, expand, compile) and the sealed base. It includes macro and special-form keywords, not only runtime value bindings, so it is broader than the (environment-bound-names) primitive — it is the set a REPL wants for tab completion. Returns nil if the engine has no namespace.

This is the stable, typed alternative to walking Environment().Namespace() phase frames directly.

func (*Engine) Call

func (p *Engine) Call(ctx context.Context, proc Value, args ...Value) (Value, error)

Call invokes a Scheme procedure with arguments. Supports lambdas, foreign closures, case-lambdas, and parameters. Composable continuations cannot be called from Go (they require the VM winding stack) and return an error.

Example
package main

import (
	"context"
	"fmt"
	"log"

	"github.com/aalpar/wile/pkg/wile"
)

func main() {
	engine, err := wile.NewEngine(context.Background())
	if err != nil {
		log.Fatal(err)
	}

	// Define a Scheme function.
	ctx := context.Background()
	_, err = engine.EvalMultiple(ctx, `
		(define (square x) (* x x))
	`)
	if err != nil {
		log.Fatal(err)
	}

	// Retrieve and call it from Go.
	proc, ok := engine.Get("square")
	if !ok {
		log.Fatal("square not found")
	}

	result, err := engine.Call(ctx, proc, wile.NewInteger(12))
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println(result.SchemeString())
}
Output:
144

func (*Engine) CheckProgram added in v1.20.0

func (p *Engine) CheckProgram(ctx context.Context, code string, source string) error

CheckProgram parses, expands, and compiles code as a single top-level program without executing it, and reports the first error with source location — the go build equivalent for a Scheme program. It returns nil when the program compiles clean. source labels the code in diagnostics; pass "" if none.

It mirrors Engine.EvalProgram exactly, including the whole-program (begin form ...) splice that makes top-level defines mutually visible, and omits only the final execution step. What that buys is every diagnostic the compiler already produces — unbound names, malformed special forms, macro-expansion failures, call-site arity against a statically known callee — reported for code paths a test run would have to reach to discover, such as the body of a procedure that is never called.

Two things it is NOT:

Not side-effect-free. (import ...) executes the imported library's body during compilation of the importing form (see machine/compilation/library_loader.go, compileAndExecuteLibrary), so checking a program that imports a side-effecting library runs those effects. Only the checked program's own top level is guaranteed not to run.

Not read-only with respect to the engine. Compiling a top-level define registers its binding — that registration is what makes forward references resolve — so a checked program leaves its top-level names defined in this engine's namespace. Consequences for a caller checking more than one program on one engine: later programs see earlier programs' definitions, and checking the same program twice reports a redefinition error against the first pass under the default immutable top level. Use a fresh engine per program when either matters.

func (*Engine) Close

func (p *Engine) Close() error

Close releases the resources this engine holds, from two sources: extensions implementing registry.Closeable have their Close method called, and per-engine hooks registered via PrimitiveRegistry.AddCloser are run against this engine's runtime frame. The shipped threads and process extensions use the latter — closing an engine terminates the SRFI-18 threads it started and kills the children it spawned, and only those: the hooks find their trackers on this engine's Namespace, so an engine sharing a registry with another (WithRegistry, where every engine binds the first engine's primitives) still reaps its own. Errors from individual closers are collected and returned via errors.Join. Calling Close on an already-closed engine returns ErrEngineClosed.

An engine built with NewEngineWithNamespace has NO closers: that path reuses a pre-built namespace and never runs buildRegistry, which is where both sources are collected. That is pre-existing — it was already true of registry.Closeable — and unchanged here.

func (*Engine) Compile

func (p *Engine) Compile(ctx context.Context, expr *Expression) (*CompiledCode, error)

Compile compiles a parsed expression without executing. The result can be executed later with Engine.Run.

Example
package main

import (
	"context"
	"fmt"
	"log"

	"github.com/aalpar/wile/pkg/wile"
)

func main() {
	engine, err := wile.NewEngine(context.Background())
	if err != nil {
		log.Fatal(err)
	}

	// Define a variable, then compile an expression that uses it.
	//
	// The variable is created by Define, NOT by evaluating "(define x 0)". Under
	// the default immutable top level a Scheme define is rebind-stable, and
	// Define refuses to rebind one — the loop below would fail on its first
	// iteration. A name the host owns end to end stays rebindable. (An engine
	// built WithMutableTopLevel accepts either.)
	ctx := context.Background()
	err = engine.Define("x", wile.NewInteger(0))
	if err != nil {
		log.Fatal(err)
	}

	compiled, err := engine.Compile(context.Background(), engine.MustParse(context.Background(), "(* x x)"))
	if err != nil {
		log.Fatal(err)
	}

	// Run the same compiled code with different values of x.
	for _, n := range []int64{3, 5, 7} {
		err = engine.Define("x", wile.NewInteger(n))
		if err != nil {
			log.Fatal(err)
		}

		result, err := engine.Run(ctx, compiled)
		if err != nil {
			log.Fatal(err)
		}

		fmt.Println(result.SchemeString())
	}
}
Output:
9
25
49

func (*Engine) ContextWithLoadPath added in v1.20.0

func (p *Engine) ContextWithLoadPath(ctx context.Context, filePath string) (context.Context, error)

ContextWithLoadPath returns a copy of ctx that names filePath as the file currently being loaded, so a directory-relative (include …) or (load …) in code evaluated under it resolves against filePath's directory, and (current-load-path) reports it.

Returns an error if filePath is empty.

Example:

loadCtx, err := engine.ContextWithLoadPath(ctx, "/app/scripts/main.scm")
if err != nil {
    return err
}
// (load "helper.scm") resolves relative to /app/scripts/
_, err = engine.EvalMultiple(loadCtx, `(load "helper.scm")`)

The returned context carries its OWN stack, seeded from ctx's if there is one, so the caller cannot leak a push into the context it was handed.

func (*Engine) CurrentLoadPath

func (p *Engine) CurrentLoadPath(ctx context.Context) string

CurrentLoadPath returns the path of the file ctx names as currently loading, or "" if ctx is not inside a load.

func (*Engine) Define

func (p *Engine) Define(name string, value Value) error

Define binds a value to a name in the top-level environment.

It REFUSES (werr.ErrImmutableBinding) to rebind a name that Scheme code already defined at the top level, because under the default immutable top level such a binding is proven rebind-stable and the compiler acts on that proof irreversibly: it pins the value into self-tail-call sites and it refuses a program at compile time on an arity mismatch against a stable callee. There is no run at which to re-check the second one, so Define cannot be permitted and merely deoptimized — it would turn a program that runs correctly into a compile error.

What still works, and is the intended shape: a name the host owns end to end. A name never defined from Scheme carries no proof, so Define creates it and may rebind it as often as it likes. A name the embedder also IMPORTED is also rebindable — a define shadows an import rather than assigning through it.

Use WithMutableTopLevel to opt the whole engine out; nothing is stamped stable there, at the cost of the frame-reclaim win on user recursion.

Example
package main

import (
	"context"
	"fmt"
	"log"

	"github.com/aalpar/wile/pkg/wile"
)

func main() {
	engine, err := wile.NewEngine(context.Background())
	if err != nil {
		log.Fatal(err)
	}

	err = engine.Define("width", wile.NewInteger(800))
	if err != nil {
		log.Fatal(err)
	}

	err = engine.Define("height", wile.NewInteger(600))
	if err != nil {
		log.Fatal(err)
	}

	ctx := context.Background()
	result, err := engine.Eval(ctx, engine.MustParse(ctx, "(* width height)"))
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println(result.SchemeString())
}
Output:
480000

func (*Engine) DisassembleValue

func (p *Engine) DisassembleValue(v Value) (string, error)

DisassembleValue returns the formatted disassembly of a callable value. For compiled closures, shows bytecode instructions. For case-lambda, shows each clause separately. For foreign closures, shows name, arity, and documentation. Returns an error for anything other than a compiled closure, a case-lambda, or a foreign closure, parameters and continuations included.

func (*Engine) EffectiveRegistry added in v1.19.0

func (p *Engine) EffectiveRegistry() *registry.PrimitiveRegistry

EffectiveRegistry returns a clone of the registry the engine's visible top level was bound from: Engine.Registry narrowed by WithStrictNamespace and by a dialect's PrimitiveRemover. Registry answers "what was registered"; this answers "what can this engine call". They differ only when something narrowed the surface, and are otherwise the same content.

Procedures only. A dialect shapes two axes, and special forms live in the forms registry rather than here: wile.NoMutation removes both the set! form and the set-car! procedure, and only Engine.Forms shows the former's absence.

The full registry still backs library environments and imports, so a narrowed primitive may remain reachable through (import …) — this reports the flat top level, not total reachability.

func (*Engine) Environment

func (p *Engine) Environment() *environment.EnvironmentFrame

Environment returns the underlying environment frame.

This is an advanced escape hatch: it exposes the internal environment.EnvironmentFrame type for white-box embedders that need direct access to phase frames, the namespace, or the sealed base. That type is internal and may change between minor versions, so it is not part of the stable API surface. Prefer the typed Engine methods (Get, Define, LoadedLibraries, …) where they suffice.

func (*Engine) Eval

func (p *Engine) Eval(ctx context.Context, expr *Expression) (Value, error)

Eval compiles and executes a parsed expression, returning the result. Use Engine.Parse to obtain an Expression from source code. For evaluating multi-expression strings, use Engine.EvalMultiple.

func (*Engine) EvalIn

func (p *Engine) EvalIn(ctx context.Context, expr *Expression, ns *environment.Namespace) (Value, error)

EvalIn compiles and executes a parsed expression in the given namespace, rather than the engine's own namespace.

Security checks during execution are decided by the intersection of the engine's authorizer and the target namespace's, most-restrictive-wins, so a target cannot widen the engine's policy. If the target has no authorizer of its own, the engine's is propagated to it before evaluation.

That policy follows the EXECUTING namespace, not the one a primitive was registered in. The distinction is invisible until they differ, which is here and nowhere else in production: a primitive reached from this namespace runs on an apply frame belonging to the namespace that registered it — the engine root, for every extension library — and reading policy off that frame is what used to leave the target's authorizer unconsulted.

func (*Engine) EvalMultiple

func (p *Engine) EvalMultiple(ctx context.Context, code string) (Value, error)

EvalMultiple evaluates multiple expressions, returning the last result.

Example
package main

import (
	"context"
	"fmt"
	"log"

	"github.com/aalpar/wile/pkg/wile"
)

func main() {
	engine, err := wile.NewEngine(context.Background())
	if err != nil {
		log.Fatal(err)
	}

	ctx := context.Background()
	result, err := engine.EvalMultiple(ctx, `
		(define x 10)
		(define y 20)
		(+ x y)
	`)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println(result.SchemeString())
}
Output:
30

func (*Engine) EvalMultipleWithSource

func (p *Engine) EvalMultipleWithSource(ctx context.Context, code string, source string) (Value, error)

EvalMultipleWithSource evaluates multiple expressions, returning the last result. The source parameter identifies where the code came from (e.g. a filename) and appears in error messages and stack traces.

Each top-level form is compiled and run independently, so a forward reference between two separate defines — (define (f) (g)) before (define (g) ...) — fails to compile. Use Engine.EvalProgram for whole-program/file semantics where all top-level defines are mutually visible.

func (*Engine) EvalProgram

func (p *Engine) EvalProgram(ctx context.Context, code string, source string) (Value, error)

EvalProgram evaluates code as a single compilation unit: it parses every top-level form, splices them into one (begin form ...), and compiles that as a unit so all top-level defines are mutually visible — a (define (f) (g)) may precede (define (g) ...). This is the forward-reference behavior of loading a file, and the recommended entry point for evaluating a whole program or script. source labels the code in diagnostics; pass "" if none.

It contrasts with Engine.EvalMultiple, which compiles and runs each top-level form independently. The (begin ...) wrapper is built structurally rather than by concatenating source text, so every form keeps its own source location.

func (*Engine) FormLabel

func (p *Engine) FormLabel(v Value) string

FormLabel returns a human-readable type label for a value: "primitive" for foreign (Go-implemented) closures, "procedure" for compiled Scheme closures, "" for everything else, including parameters, continuations, typed nils, and non-callable values.

func (*Engine) Forms added in v1.19.0

func (p *Engine) Forms() []string

Forms returns the sorted names of the special forms this engine's dialect installed. Together with Engine.EffectiveRegistry this is the engine's actual surface: syntax here, procedures there.

Names rather than the specs themselves: a FormSpec carries validator and codegen hooks that are internal by construction and are not an embedder contract.

func (*Engine) Get

func (p *Engine) Get(name string) (Value, bool)

Get retrieves a value by name from the environment.

Resolution is scoped to the ambient (empty) scope set, symmetric with Engine.Define, which creates under that same set. A wildcard read would return the name's first slot regardless of hygiene, so once any macro introduced a same-named top-level binder the host could not round-trip its own value: Define would write one slot and Get would read another.

A name bound ONLY under a non-empty scope set is correctly reported as absent. Such a binding is macro-introduced and is unreachable from any source-written reference too, so there is no name the host could legitimately use to ask for it.

The same reasoning governs every reflective read of a bare symbol, so environment-ref/environment-bound? and namespace-ref/namespace-bound? resolve under the ambient set as well. Wildcard survives only where a NAME, not a variable, is the unit: registerSchemeDocstrings below and searchEnvironmentBindings (registry/search.go) attach and retrieve docstrings, and one name owns one doc entry however many hygiene-distinct bindings share it, so the ambiguity does not arise. REPL completion still walks the unfiltered key map and can therefore offer a name that resolves to nothing; that is a known defect, filed in TODO.md, not an endorsement of wildcard.

func (*Engine) LastCounters

func (p *Engine) LastCounters() machine.VMCounters

LastCounters returns the VM performance counters from the most recent Run or Eval call. Sub-context counters are not aggregated.

func (*Engine) LoadedLibraries

func (p *Engine) LoadedLibraries() ([]*LibraryInfo, error)

LoadedLibraries returns metadata for all currently loaded libraries, sorted by name. Returns (nil, nil) if no library registry is configured.

func (*Engine) LookupLibrary

func (p *Engine) LookupLibrary(parts ...string) (*LibraryInfo, error)

LookupLibrary returns info for a loaded library identified by its name parts (e.g., "scheme", "base"). Returns (nil, nil) if no library registry is configured. Returns a non-nil error if the registry has an unexpected type.

func (*Engine) MustParse

func (p *Engine) MustParse(ctx context.Context, code string) *Expression

MustParse is like Parse but panics on error.

func (*Engine) MustParseWithSource

func (p *Engine) MustParseWithSource(ctx context.Context, code string, source string) *Expression

MustParseWithSource is like ParseWithSource but panics on error.

func (*Engine) Namespace

func (p *Engine) Namespace() *environment.Namespace

Namespace returns the Namespace for advanced use. This provides access to per-instance syntax interning and phase management.

Like Environment, this is an advanced escape hatch exposing an internal type (environment.Namespace) that may change between minor versions; it is not part of the stable API surface.

func (*Engine) Parse

func (p *Engine) Parse(ctx context.Context, code string) (*Expression, error)

Parse parses a single Scheme expression from code.

Parse returns a CompilationError if the input is empty, malformed, or contains more than one expression.

func (*Engine) ParseWithSource

func (p *Engine) ParseWithSource(ctx context.Context, code string, source string) (*Expression, error)

ParseWithSource parses a single Scheme expression from code. The source parameter identifies where the code came from (e.g. a filename) and appears in error messages.

func (*Engine) ReadExpression

func (p *Engine) ReadExpression(ctx context.Context, r io.Reader) (*Expression, error)

ReadExpression reads a single complete expression from r.

Unlike Engine.Parse, ReadExpression does not require the reader to contain exactly one expression — it reads the first complete expression and stops. Trailing input in the reader is ignored (the reader position advances past the consumed expression).

Use IsIncompleteInput to check whether a returned error indicates the input is a valid prefix of an expression that needs more input to complete. This is the intended pattern for REPL implementations:

expr, err := eng.ReadExpression(ctx, r)
if err != nil {
    if wile.IsIncompleteInput(err) {
        // prompt for more input
    }
    // real parse error
}

func (*Engine) ReadExpressions

func (p *Engine) ReadExpressions(ctx context.Context, r io.Reader) ([]*Expression, error)

ReadExpressions parses every complete expression available in r, in order, reusing a single parser so the tokenizer's one-rune lookahead is preserved across forms (a fresh parser per form would drop the inter-form delimiter).

On clean end-of-input it returns the parsed expressions and a nil error. If the input ends partway through an expression, it returns the complete expressions parsed so far together with an error satisfying IsIncompleteInput — the REPL uses that signal to keep accumulating lines. A genuine syntax error is returned wrapped, with the expressions parsed before it.

Unlike ReadExpression (exactly one form) this is the multi-form read the REPL needs to evaluate every expression on a pasted or piped line rather than silently dropping all but the first.

func (*Engine) RegisterFunc

func (p *Engine) RegisterFunc(name string, fn any) error

RegisterFunc registers a Go function as a Scheme primitive using natural Go signatures.

Reflection-based FFI bridging: pre-computes argument and return converters at registration time using Go's reflect package. Each call uses the cached converters to translate between Scheme values and Go types, avoiding per-call reflection overhead. See BIBLIOGRAPHY.md "Reflection-Based FFI Bridging".

Supported Types

Parameter types: int64, int, float64, complex128, string, bool, []byte, []T (typed slices), map[K]V, structs (exported fields), func(...) (callbacks), Value, and context.Context (first param only).

Return types: int64, int, float64, string, bool, []byte, []T, map[K]V, structs, Value, error (last return only), and void.

Variadic Functions

Variadic Go functions are supported. The variadic parameter receives all excess arguments from Scheme, converted element-by-element.

Context Forwarding

If the first parameter is context.Context, the VM's context is forwarded automatically and does not count toward the Scheme parameter count.

Callbacks

Callback parameters (func types) receive a Go closure that invokes a Scheme procedure through a VM sub-context. The Scheme argument must be a lambda, a case-lambda, or a parameter object; Go-implemented primitives (foreign closures, including functions registered through RegisterFunc or RegisterPrimitive) and continuations are rejected at call time with werr.ErrNotAProcedure, even though Scheme considers them procedures.

Callbacks must be called synchronously during the registered function's execution. Storing a callback for later invocation or calling it from another goroutine is unsafe — the closure captures VM state that is not goroutine-safe.

Returns an error wrapping werr.ErrFFIRegistration if fn is not a function or uses unsupported types.

Example
package main

import (
	"context"
	"fmt"
	"log"

	"github.com/aalpar/wile/pkg/wile"
)

func main() {
	engine, err := wile.NewEngine(context.Background())
	if err != nil {
		log.Fatal(err)
	}

	// Register a Go function with a natural signature — no MachineContext needed.
	err = engine.RegisterFunc("double", func(n int64) int64 {
		return n * 2
	})
	if err != nil {
		log.Fatal(err)
	}

	ctx := context.Background()
	result, err := engine.Eval(ctx, engine.MustParse(ctx, "(map double '(1 2 3 4 5))"))
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println(result.SchemeString())
}
Output:
(2 4 6 8 10)

func (*Engine) RegisterFuncs

func (p *Engine) RegisterFuncs(funcs map[string]any) error

RegisterFuncs registers multiple Go functions as Scheme primitives. Each key in the map is the Scheme name; each value must be a Go function with a signature supported by [RegisterFunc].

Registration stops on the first error. The error message includes the binding name that failed first. When multiple functions are invalid, the particular binding that fails first is non-deterministic because Go map iteration order is unspecified. Functions registered before the failure remain registered.

func (*Engine) RegisterPrimitive

func (p *Engine) RegisterPrimitive(spec PrimitiveSpec) error

RegisterPrimitive adds a Go function as a Scheme primitive.

The spec is validated first (registry.PrimitiveSpec.Validate) and an invalid spec is returned as an error, never bound. This is the same contract registry.PrimitiveRegistry.AddPrimitives enforces by panicking: an embedder assembling a spec dynamically gets a value it can handle, rather than a binding whose first call takes down the engine. The IsVariadic + ParamCount:0 shape is the one that matters — its panic fires during frame setup, so it wedges every subsequent evaluation on this engine, not just the offending call.

It shares Engine.Define's refusal: a name Scheme code already defined at the top level is rebind-stable under the default immutable top level and cannot be replaced. Register before running Scheme that defines the same name.

Example
package main

import (
	"context"
	"fmt"
	"log"

	"github.com/aalpar/wile/pkg/values"
	"github.com/aalpar/wile/pkg/wile"
)

func main() {
	engine, err := wile.NewEngine(context.Background())
	if err != nil {
		log.Fatal(err)
	}

	// Register a Go function that doubles an integer.
	err = engine.RegisterPrimitive(wile.PrimitiveSpec{
		Name:       "double",
		ParamCount: 1,
		Impl: func(mc wile.CallContext) error {
			n := mc.Arg(0).(*values.Integer).Value
			mc.SetValue(values.NewInteger(n * 2))
			return nil
		},
	})
	if err != nil {
		log.Fatal(err)
	}

	ctx := context.Background()
	result, err := engine.Eval(ctx, engine.MustParse(ctx, "(double 21)"))
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println(result.SchemeString())
}
Output:
42

func (*Engine) Registry

func (p *Engine) Registry() *registry.PrimitiveRegistry

Registry returns a clone of the engine's registry. The returned registry can be filtered with Without, WithoutCategory, or WithoutBindings and passed to NewEngine via WithRegistry to create a restricted engine.

This is the pre-dialect base: everything registered, which is what makes it the right input for building a derived engine. It is NOT what this engine can call — WithStrictNamespace and a dialect's PrimitiveRemover both narrow the visible top level below it, and this clone still lists what they removed. For the surface this engine actually has, use Engine.EffectiveRegistry and Engine.Forms.

func (*Engine) Run

func (p *Engine) Run(ctx context.Context, cc *CompiledCode) (Value, error)

Run executes previously compiled code.

func (*Engine) SetDebugger

func (p *Engine) SetDebugger(d *debug.Debugger)

SetDebugger attaches a debugger to the engine. Subsequent Engine.Run calls will execute with the debugger active, enabling breakpoints and stepping. Pass nil to detach the debugger.

func (*Engine) UnloadedLibraries

func (p *Engine) UnloadedLibraries(ctx context.Context) []*LibraryInfo

UnloadedLibraries returns metadata for libraries discoverable via the file resolver but not yet imported. Returns nil if no resolver is available. Thread-safe via lazy initialization with retry on failure.

type EngineOnlyOption added in v1.20.0

type EngineOnlyOption interface {
	EngineOption
	// contains filtered or unexported methods
}

EngineOnlyOption is the subset of EngineOption that NewEngine reads AFTER the namespace exists, so it applies equally to a pre-built one. It is the element type of NewEngineWithNamespace's variadic: a namespace-consumed option does not implement it, so passing one there is a compile error rather than a silent drop.

Embedding EngineOption keeps every engine-only option passable to NewEngine and to a []EngineOption literal.

func WithCoverage

func WithCoverage(c *coverage.Collector) EngineOnlyOption

WithCoverage enables Scheme-side line coverage collection. After each compilation, the engine registers the resulting top-level template and every sub-template reachable via its literals pool with the given collector. Per-s-expression execution is then aggregated into the collector's Entries.

Zero hot-path cost when not set (nil check in VM dispatch).

func WithImportObserver

func WithImportObserver(obs func(LibraryImportEvent)) EngineOnlyOption

WithImportObserver sets a callback that is invoked each time a library is imported. The observer is read-only — it cannot influence the import. Requires WithLibraryPaths to be effective (no libraries loaded without it).

func WithInlineThreshold

func WithInlineThreshold(n int) EngineOnlyOption

WithInlineThreshold sets the maximum body length (in top-level expressions) for procedure inlining. Procedures with bodies longer than this threshold are not inlined. A value of 0 disables inlining entirely. When not called, the engine uses compilation.DefaultInlineThreshold (5).

func WithLibraryPaths

func WithLibraryPaths(paths ...string) EngineOnlyOption

WithLibraryPaths enables the R7RS library system (define-library / import) and configures directories to search for .sld library files.

Without this option, (import ...) raises a configuration error.

Paths are searched in order: user-supplied paths first, then the default ("."). The embedded standard library is served by the FileResolver chain (e.g. WithSourceFS(stdlib.FS)), not a search path. An empty call WithLibraryPaths() enables library support with the default only.

Example:

eng, err := wile.NewEngine(ctx,
    wile.WithLibraryPaths("/app/libs", "./vendor"),
)
// search order: /app/libs, ./vendor, .

func WithLossyConversionsAllowed

func WithLossyConversionsAllowed() EngineOnlyOption

WithLossyConversionsAllowed permits FFI converters to silently truncate when converting Scheme numerics to fixed-precision Go types (float64, complex128). When set, *BigFloat with magnitude exceeding float64 range converts to ±math.Inf(0) without error; *Rational with non-representable denominators rounds via (*big.Rat).Float64 with the loss bit discarded; *BigComplex imaginary/real components each may truncate independently.

Default (option not set): the FFI converter returns werr.ErrLossyConversion (wrapped, with direction info) when any precision loss would occur. This is the "fail loud" discipline — opt-in is required to suppress.

The option is per-engine; the flag is captured into each FFI closure at RegisterFunc time, so calling WithLossyConversionsAllowed after some functions have already registered does NOT change their behavior.

func WithMaxCallDepth

func WithMaxCallDepth(n int) EngineOnlyOption

WithMaxCallDepth sets the maximum recursion depth for the VM. When the continuation stack exceeds this depth, ErrCallDepthExceeded is returned. A value of 0 means unlimited (no depth check). Negative values are clamped to 0 and therefore also mean unlimited (matches WithInlineThreshold). When not called, the engine uses DefaultMaxCallDepth (10000).

func WithMaxExpandDepth

func WithMaxExpandDepth(n int) EngineOnlyOption

WithMaxExpandDepth sets the maximum structural recursion depth the macro expander will accept. The parser already bounds nesting in textual input (see WithMaxParseDepth); this bounds programmatically-constructed deep syntax — macro output, datum->syntax, and quasiquote — which reaches the expander without passing through the parser. When expansion nests deeper, ErrExpandDepthExceeded is returned instead of crashing with a fatal Go stack overflow. A value of 0 means unlimited (negative values are clamped to 0). When not called, the expander uses DefaultMaxExpandDepth (50000).

Scope: this bound applies to expansion of top-level program text run through the engine. Expansion triggered from within running Scheme — (eval ...), (load ...), (compile ...), (expand ...) — always uses DefaultMaxExpandDepth regardless of this option, because the primitive layer has no channel to the engine's configured value. Those paths are still protected from the fatal stack overflow (by the default); they are simply not retunable per-engine.

func WithMaxParseDepth

func WithMaxParseDepth(n int) EngineOnlyOption

WithMaxParseDepth sets the maximum structural nesting depth the parser will accept. When input nests deeper, ErrParseDepthExceeded is returned instead of crashing with a fatal Go stack overflow. A value of 0 means unlimited (negative values are clamped to 0). When not called, the parser uses DefaultMaxParseDepth (10000).

Scope: this bound is threaded onto the engine's own parse entry points — Parse, ParseWithSource, ReadExpression, ReadExpressions, EvalMultiple, EvalProgram, and file/-e execution. Parsing triggered from within running Scheme — (read ...), (read-syntax ...), (eval ...), (compile ...) — uses DefaultMaxParseDepth regardless of this option, because the primitive layer has no channel to the engine's configured value (mirrors WithMaxExpandDepth). Those paths are still protected from the fatal stack overflow by the default; they are simply not retunable per-engine.

func WithMaxStackSize

func WithMaxStackSize(n uint64) EngineOnlyOption

WithMaxStackSize sets the maximum eval stack size for the VM. When the eval stack exceeds this size, ErrStackOverflow is returned. This is opt-in: a value of 0 means unlimited (no stack size check). There is no default — when not called, the stack is unlimited.

func WithSourceFS

func WithSourceFS(fsys fs.FS) EngineOnlyOption

WithSourceFS adds a virtual filesystem layer to the source file resolver chain. Multiple calls add layers searched in call order. When no resolver options are used, the engine defaults to the OS filesystem. Once any resolver option is used (WithSourceFS or WithSourceOS), only the explicitly configured resolvers are active.

Bootstrap macros are unaffected — they always load from the embedded bootstrap filesystem.

WithSourceFS panics if fsys is nil: a nil filesystem is a programming error, not a runtime condition an embedder can recover from.

Example:

//go:embed scheme
var schemeFS embed.FS

eng, err := wile.NewEngine(ctx,
    wile.WithSourceFS(schemeFS),  // searched first
    wile.WithSourceOS(),          // OS filesystem searched last
)

func WithSourceOS

func WithSourceOS() EngineOnlyOption

WithSourceOS adds the OS filesystem to the source file resolver chain. This is typically called last so that virtual filesystems are searched first. When no resolver options are used, the engine defaults to the OS filesystem; WithSourceOS is only needed when building an explicit chain with WithSourceFS.

Example:

eng, err := wile.NewEngine(ctx,
    wile.WithSourceFS(embedFS),  // virtual FS first
    wile.WithSourceOS(),         // OS fallback last
)

type EngineOption

type EngineOption interface {
	// contains filtered or unexported methods
}

EngineOption configures an Engine. The method is unexported, so the set of implementations is closed to this package — as it already effectively was, since engineConfig is unexported.

func WithAuthorizer

func WithAuthorizer(auth security.Authorizer) EngineOption

WithAuthorizer sets the Authorizer for the engine. The authorizer is recorded on the engine's namespace at construction and consulted by gate sites via MachineContext.Authorizer(), gating runtime primitives and compile-time code loading.

An explicit WithAuthorizer takes precedence over any profile's built-in authorizer regardless of option order: WithAuthorizer(a) and WithProfile(p) resolve to a (then intersected with any WithSandbox layer) no matter which is written first. Passing nil is meaningful — it opens the engine, overriding a profile authorizer (symmetric with WithEnvMap(nil)).

Without this option, all operations are allowed (open by default) unless a profile or sandbox supplies an authorizer. The authorizer is immutable after engine construction.

Example:

eng, err := wile.NewEngine(ctx,
    wile.WithAuthorizer(security.ReadOnly()),
)

func WithContractEnforcement

func WithContractEnforcement() EngineOption

WithContractEnforcement enables runtime type validation for primitives that declare ParamTypes contracts. When enabled, each contracted primitive validates its arguments against declared types before calling the implementation and returns a typed error on mismatch.

Namespace-scoped: the flag is recorded on the namespace at construction and TRAVELS WITH IT, so two engines over one namespace share it, and it cannot be passed to NewEngineWithNamespace — pass it to NewNamespace instead. This is what makes the three binding sites agree. Enforcement is a decoration baked into the *ForeignClosure values that the namespace's frames hold, and those frames are filled from three places (the base environment, library environments, and post-construction Engine.RegisterPrimitive); reading the engine's config instead left the first of the three unenforced whenever the namespace was pre-built.

Disabled by default. Intended as a correctness-verification aid for extension authors — production extensions should perform their own argument checks (e.g., via helpers.RequireArg) rather than depend on this option, since enabling it adds a per-call validator invocation.

func WithDialect added in v1.18.0

func WithDialect(d Dialect) EngineOption

WithDialect installs a Dialect on the engine, customizing its special-form surface at construction time. Last-wins if supplied more than once. A nil dialect (the default) applies DefaultDialect, leaving the R7RS-default forms in place.

Namespace-scoped: the forms registry the dialect writes to belongs to the namespace, so this option is namespace-consumed and cannot be passed to NewEngineWithNamespace — pass it to NewNamespace instead. If the dialect's InstallForms returns an error, whichever constructor performs the bootstrap (NewNamespace, or NewEngine when it builds the namespace itself) fails with that error wrapped in werr.ErrEngineInit.

func WithEnv

func WithEnv(key, value string) EngineOption

WithEnv adds a single virtual environment variable. When any virtual env var is set, the envvars extension reads from the virtual map instead of os.Getenv.

func WithEnvMap

func WithEnvMap(m map[string]string) EngineOption

WithEnvMap sets the complete virtual environment variable map. Replaces any previously set virtual env vars.

Passing nil clears the virtual env map so envvars primitives fall back to os.Getenv (still gated by the authorizer). This is symmetric with WithAuthorizer(nil): the zero value means "no restriction", not "empty sandbox". To explicitly sandbox with no visible env, pass an empty map.

Note: when combined with WithProfile(Console) or WithProfile(ConsoleWithLoad), option order matters. WithProfile fills in an empty map only if envMap is currently nil; a later WithEnvMap(nil) re-nils it and opens the sandbox.

func WithExtension

func WithExtension(ext registry.Extension) EngineOption

WithExtension adds an extension to the engine.

func WithExtensions

func WithExtensions(exts ...registry.Extension) EngineOption

WithExtensions adds multiple extensions to the engine.

func WithImmutableTopLevel

func WithImmutableTopLevel() EngineOption

WithImmutableTopLevel selects top-level-define immutability in the user program. A top-level define that is defined-once and never set! within its compilation unit is marked rebind-stable (BindingMeta.Stable), and a subsequent set! of such a binding is rejected with ErrImmutableBinding. This is now the DEFAULT (newEngineConfig); the option remains as an explicit, redundant selector for source compatibility.

This is a documented deviation from strict R7RS §4.1.6/§5.3 (which permit top-level set!/redefinition); it unlocks the frame-reclamation optimizer's top-level payoff (sibling escape-gated plan) — the "compile for speed" contract used by sealed-module Schemes. Use WithMutableTopLevel() to opt out.

Enforcement is scoped to the engine's own user runtime global (the layered-environment sealed-base carve): a re-define of a sealed primitive/stdlib name is a child-frame shadow rather than a rejected rebind, and user-loaded LIBRARIES stay mutable — a library's cross-form (define x)/(set! x) is permitted. See docs/reference/r7rs-differences.md.

func WithMutableTopLevel

func WithMutableTopLevel() EngineOption

WithMutableTopLevel selects strict R7RS mutable/redefinable top-level bindings, the inverse of WithImmutableTopLevel. It exists as an explicit opt-out so callers can request mutable semantics independent of the engine default (which Phase 4 of the layered-environment work flips to immutable). Opting out forfeits the frame-reclaim GC win for user recursion.

func WithProfile

func WithProfile(p Profile) EngineOption

WithProfile configures the engine with the named profile's extensions and authorization constraints. WithProfile is additive: it appends the profile's extensions to any already configured via WithExtension/WithExtensions, and records the profile's authorizer only if one is defined. An explicit WithAuthorizer always takes precedence over the profile's authorizer regardless of option order (see resolveAuthorizer), so profile and authorizer options compose commutatively.

Across multiple WithProfile calls the last profile that defines an authorizer wins; a later profile with no authorizer (e.g. Tiny, Small, KitchenSink) does not clear an earlier one. This fails safe — a prior restriction is retained rather than silently dropped.

Per-profile envMap behavior (all five current profiles):

  • Tiny: envMap untouched; profile registers no extensions, so envvars primitives are absent.
  • Console: allocates empty envMap when unset; sandboxes to the virtual map (no os.Getenv fallthrough).
  • ConsoleWithLoad: same as Console (empty envMap when unset).
  • Small: envMap untouched. envvars primitives are registered and fall through to os.Getenv when envMap is nil, gated by the authorizer (if any).
  • KitchenSink: same fallthrough behavior as Small.

When WithEnv/WithEnvMap is combined with WithProfile(Console*), the caller-supplied contents are preserved: WithProfile only allocates an empty map when none is set. Option order matters — a later WithEnvMap(nil) re-nils the map and opens the sandbox.

func WithRegistry

func WithRegistry(r *registry.PrimitiveRegistry) EngineOption

WithRegistry uses a custom registry instead of the default. When set, core primitives are NOT automatically added.

func WithSandbox

func WithSandbox(opts ...SandboxOption) EngineOption

WithSandbox layers a restrictive authorizer on top of any profile. File reads and stats are allowed; file writes and deletes are denied. Environment variable reads are prefix-filtered (default "WILE_"). Code loading and process execution are denied.

The sandbox layer is always intersected (most-restrictive-wins, via security.All) on top of whatever base authorizer the profile and/or WithAuthorizer resolve to. This holds regardless of option order — the composition is performed once at engine construction by resolveAuthorizer, so WithSandbox may appear before or after WithProfile/WithAuthorizer.

Multiple WithSandbox calls accumulate: each layer is intersected with the previously recorded sandbox authorizer, so restrictions only ever tighten (a second call cannot silently widen the first). Intersection is order- independent for the allow/deny decision.

func WithStrictNamespace added in v1.18.0

func WithStrictNamespace() EngineOption

WithStrictNamespace narrows the engine's visible top-level surface to CORE ONLY: the profile's extension primitives are REGISTERED (so libraries can import them) but are NOT pre-bound at the top level. Reach them with (import …), which needs WithLibraryPaths — off by default, and without it every import fails with werr.ErrLibraryConfiguration. The precondition is identical at both levels; it bites harder at level 2, where there is no ambient surface to fall back to (see WithoutAmbientBindings' first cost). Core primitives and core syntax remain visible, so the visible surface matches a Tiny engine's; the full profile registry is still used for library environments, so a layered import such as (import (scheme r5rs)) resolves over the core-only baseline.

Orthogonal to WithProfile/WithSandbox/WithAuthorizer; composes commutatively. The profile (or explicit WithExtension set) remains the security boundary — strict mode never widens what is reachable, it only withholds it from the top level until imported. Off by default.

The visible surface is carved when the namespace is built, so strictness must be set at namespace-creation time. Like WithRegistry/WithExtension/WithoutCore, this option is namespace-consumed and so cannot be passed to NewEngineWithNamespace at all (a pre-built namespace is authoritative for its own top level) — bake strictness in at NewNamespace. It is incompatible with WithRegistry/WithoutCore (which supply a custom or coreless registry): strict mode derives its visible surface from the default core registry, so the combination is rejected at construction.

func WithoutAmbientBindings added in v1.20.0

func WithoutAmbientBindings() EngineOption

WithoutAmbientBindings narrows the engine's visible top-level surface all the way: NOTHING is pre-bound. It is one step past WithStrictNamespace, which still pre-binds the core surface, and it composes with it by max — applying both in either order yields this level.

What survives is not the empty set: exactly 41 names, whatever the profile, pinned name-by-name by TestNoAmbientBindingsBoundSet. Two mechanisms put them there. 38 are PHASE HANDLERS, not bindings: registered by the compiler, living in sealed frames that ordinary value resolution never sees, and never sourced from a registry — so withholding the registry cannot withhold them. The other three (unless, guard, guard-aux) are syntax-rules definitions in core.LateBootstrapMacroSource, which LoadBootstrapCore loads unconditionally rather than through Registry.MacroSources(); that, and not any phase-handler property, is why when and unless land on opposite sides of this floor.

BOUND and USABLE are different partitions, and they cross in both directions: syntax-rules is usable and is NOT one of the 41 (define-syntax recognizes it inline), while guard is one of the 41 and is unusable. The usability partition, by shape here and name-by-name in docs/embedding/api-design.md:

  • Usable: the value forms (lambda, if, quote, define, begin, set!, the let and define-syntax families, cond-expand, case-lambda), the library forms (define-library, library, import), the expand-phase family (syntax, syntax-case, er-macro-transformer, begin-for-syntax, define-for-syntax, eval-when, meta), plus syntax-error and with-continuation-mark. A CONSTANT quasiquote or quasisyntax template belongs here too.
  • Resolves, unusable: a form whose expansion CALLS a primitive resolves and then fails at the call. quasiquote is the one to know — `(1 2) works, but the moment an unquote appears the expansion emits list and dies. quasisyntax with an unsyntax (needs datum->syntax), with-syntax (list), unless (not) and guard (call-with-exit) are the same shape.
  • Auxiliary keywords, bound so the expander recognizes them positionally and an error standalone: unquote, unsyntax, their -splicing forms, export, guard-aux, with-binding-scope.
  • include and include-ci resolve, and want a FileResolver rather than a registry — a different axis from this option.

Everything else the registry supplies — the core primitives and the bootstrap macros cond, case, when, and, or, do, define-record-type, let-values, delay, parameterize — is simply unbound. This option is strict for PROCEDURES AND DERIVED SYNTAX; do not read it as "R7RS-strict" unqualified.

Nothing is withheld permanently: import is a phase handler, and REGISTERED is untouched, so a program reaches any part of the profile it wants by importing it, starting with (import (scheme base)). What this option buys is that the program must SAY SO — no ambient dependency goes undeclared. It is an explicitness property (a CI or portability-audit property), not confinement; the profile remains the security boundary at every level.

Three costs, all real:

  • The import route exists only if WithLibraryPaths was called. It is off by default, so WithProfile(Small) plus this option alone — the shortest spelling of level 2, and the one the ladder table shows — constructs without error and then fails every (import …) with werr.ErrLibraryConfiguration. That is confinement, reached by the most likely invocation rather than by the two exotic ones below, and the configuration is silent even though the failure is loud. Reaching the stdlib further needs a resolver that serves it (WithSourceFS(stdlib.FS)). The pair is deliberately not rejected at construction: Engine.Define and Engine.RegisterPrimitive make "empty top level, no library system, host supplies every binding from Go" a legitimate DSL host.
  • A library environment is engine-sized, so every import is expensive: (import (scheme base)) alone measures ~9.4 ms, against ~3.9 ms to build a whole Small engine. A program on this level pays that at least once, and an eight-library R7RS preamble costs ~59 ms / ~7.9 MB heap.
  • It is usable on Small and KitchenSink only. Tiny cannot import (scheme base) (64 of its exports are unregistered there) and Console/ConsoleWithLoad are denied code:load on the stdlib path. Both failures pre-date this option and reproduce without it, but at this level there is no ambient surface to fall back to. WithSandbox denies code:load for the same reason: combined with this option it leaves a program on the phase-handler floor with no route off it, which IS confinement — a real configuration, but not the one the paragraph above describes.

Carries the same constraints as WithStrictNamespace: set it at namespace-creation time (it cannot be passed to NewEngineWithNamespace), and it is incompatible with WithRegistry/WithoutCore. Off by default.

func WithoutCore

func WithoutCore() EngineOption

WithoutCore creates an engine with an empty registry — no core primitives (arithmetic, pairs, control flow, etc.) are added. Extensions added via WithExtension are still applied.

This is useful for building minimal engines where only specific extensions are needed, or for testing extension isolation.

type Expression

type Expression struct {
	// contains filtered or unexported fields
}

Expression represents a single parsed Scheme expression.

Expression wraps a syntax value produced by the parser, before any macro expansion or compilation. It captures the source name (if any) for use in error messages.

Expression is not safe for concurrent use.

func (*Expression) Source

func (p *Expression) Source() string

Source returns the source name associated with this expression. Returns the empty string if no source was specified at parse time.

func (*Expression) String

func (p *Expression) String() string

String returns a string representation of the expression.

type ForeignFunction

type ForeignFunction = machine.ForeignFunction

ForeignFunction is the signature for primitive implementations. This is a re-export of machine.ForeignFunction for convenience.

type LibraryImportEvent

type LibraryImportEvent = compilation.LibraryImportEvent

LibraryImportEvent records what happened when a library was imported. See compilation.LibraryImportEvent for field documentation.

type LibraryInfo

type LibraryInfo struct {
	Name        string // Scheme representation, e.g. "(scheme base)"
	Description string
	SourceFile  string
	Exports     []string // sorted export names
}

LibraryInfo holds read-only metadata about a Scheme library.

type LibraryName

type LibraryName struct {
	Parts []string
}

LibraryName identifies an R7RS library by its structured name parts. For example, the library (scheme base) has Parts ["scheme", "base"].

LibraryName is the public projection of the engine's internal library identifier: it carries the parts without exposing the machine/compilation type in the API surface. The Parts slice is owned by the caller (a fresh copy per value), so mutating it cannot affect engine state.

func (LibraryName) String

func (p LibraryName) String() string

String returns the Scheme representation of the library name, e.g. "(scheme base)".

type MachineContext

type MachineContext = machine.MachineContext

MachineContext provides access to the VM during primitive execution. This is a re-export of machine.MachineContext for convenience.

type Phase

type Phase = environment.Phase

Phase is a level in one owner's macro tower, counted RELATIVE to that owner's own runtime (0) rather than as an absolute stage of compilation. The named constants below are the levels the top level occupies, not the range: the tower climbs past them whenever a transformer body defines a macro of its own. Re-exported from environment for embedder convenience; see environment.Phase for the full model.

type PrimitiveRemover added in v1.18.0

type PrimitiveRemover interface {
	// RemovedPrimitives returns the names of procedures to omit from the engine's
	// top level. It should return a fresh slice each call (callers may retain it).
	RemovedPrimitives() []string
}

PrimitiveRemover is an optional capability a Dialect may implement to shape the engine's surface beyond the forms layer: it names procedures to omit from the visible top level. The base Dialect interface is forms-only (InstallForms reaches the per-engine forms registry, which the validator and compiler read); the mutation procedures (set-car!, vector-set!, …) live in the separate per-engine *registry.PrimitiveRegistry and are invisible to InstallForms. A dialect that also implements PrimitiveRemover crosses that ceiling.

When a dialect passed to WithDialect also implements PrimitiveRemover, the engine omits the named primitives from the top-level binding set (via registry.Without, on a copy — the full registry is untouched). Referencing a removed name at the top level is then an unbound reference (werr.ErrNoSuchBinding), the same failure shape as a removed special form.

Boundary: removal is at the *visible top level* only. The full registry still backs library environments, so (import (scheme base)) re-exposes a removed procedure. A PrimitiveRemover dialect is a *language-surface* statement (what the flat top level offers), NOT a capability sandbox — for the latter use security.Authorizer, which composes orthogonally. Airtight enforcement across the import surface is the expander-level dialect track. See NoMutation.

type PrimitiveSpec

type PrimitiveSpec = registry.PrimitiveSpec

PrimitiveSpec defines a primitive to be registered. This is a re-export of registry.PrimitiveSpec for convenience.

type Profile

type Profile int

Profile identifies a named environment configuration. Each profile defines which extensions are loaded and what authorization constraints apply.

const (
	// Tiny is a pure computational Scheme -- core primitives only.
	// No I/O, no filesystem, no threads. The lowest common denominator:
	// every other profile is a superset of Tiny.
	Tiny Profile = iota

	// Console adds I/O and sandboxed file access to Tiny.
	// All port primitives work. File operations restricted to /tmp.
	// stdin/stdout/stderr available. Environment variables read from
	// virtual env map only (no os.Getenv fallthrough).
	Console

	// ConsoleWithLoad is Console plus the eval extension, with an
	// authorizer that allows `code:load` under /tmp (in addition to
	// file r/w/d under /tmp). Enables (eval ...) and (load ...) within
	// the same /tmp security envelope. Process execution still denied.
	// Primary consumer: wile-goast and similar embedders that stage
	// Scheme files into /tmp and load them.
	ConsoleWithLoad

	// Small is R7RS-small complete -- all 16 (scheme ...) libraries.
	// Includes file I/O, system interface. No threads, no Go interop.
	Small

	// KitchenSink includes every available extension: threads, Go interop,
	// process execution, namespace manipulation.
	KitchenSink
)

func (Profile) String

func (p Profile) String() string

String returns the kebab-case name of the profile.

type RuntimeError

type RuntimeError struct {
	Message    string
	Cause      error
	Condition  Value  // non-nil when Scheme raise produced the error; nil for VM/primitive errors
	Source     string // formatted source location ("file:line:col"), empty if unavailable
	StackTrace string // formatted VM stack trace, empty if unavailable
	// contains filtered or unexported fields
}

RuntimeError wraps errors from executing Scheme code.

Condition

When the error originated from a Scheme raise or raise-continuable, Condition holds the raised value and RuntimeError.IsSchemeException returns true. When the error originated from Go code (VM errors, primitive failures, type mismatches), Condition is nil.

Source and Stack Trace

Source and StackTrace provide the source location and VM stack trace at the point of the error. Both are empty strings when per-operation source tracking is unavailable.

Cause

Cause may contain internal machine types. Callers should treat it as an opaque error suitable for logging and errors.Is/errors.As matching, not for direct type inspection.

func (*RuntimeError) Error

func (p *RuntimeError) Error() string

func (*RuntimeError) IsSchemeException

func (p *RuntimeError) IsSchemeException() bool

IsSchemeException reports whether this error originated from a Scheme raise or raise-continuable expression. When true, Condition holds the raised value.

func (*RuntimeError) Unwrap

func (p *RuntimeError) Unwrap() error

type SandboxOption

type SandboxOption func(*sandboxConfig)

SandboxOption configures the sandbox modifier.

func SandboxEnvPrefix

func SandboxEnvPrefix(prefix string) SandboxOption

SandboxEnvPrefix sets the environment variable prefix that the sandbox allows reading. Default is "WILE_".

type Value

type Value interface {
	// SchemeString returns the Scheme representation.
	SchemeString() string
	// IsVoid returns true if this is the void value.
	IsVoid() bool
	// Internal returns the underlying values.Value for advanced use.
	// This is exported for use by testing packages and advanced embedding scenarios.
	Internal() values.Value
	// contains filtered or unexported methods
}

Value represents a Scheme value in the public API.

func Car

func Car(v Value) (Value, bool)

Car returns the car of a pair or other Tuple type. Returns (value, true) on success, or (nil, false) if v is not a non-empty Tuple.

func Cdr

func Cdr(v Value) (Value, bool)

Cdr returns the cdr of a pair or other Tuple type. Returns (value, true) on success, or (nil, false) if v is not a non-empty Tuple.

func NewBigFloat

func NewBigFloat(f *big.Float) Value

NewBigFloat creates a big float value from a big.Float.

func NewBigFloatFromFloat64

func NewBigFloatFromFloat64(f float64) Value

NewBigFloatFromFloat64 creates a big float value from a float64.

func NewBigFloatFromString

func NewBigFloatFromString(s string) Value

NewBigFloatFromString creates a big float from a string. Returns nil if the string is not a valid float.

func NewBigInteger

func NewBigInteger(n *big.Int) Value

NewBigInteger creates a big integer value from a big.Int.

func NewBigIntegerFromInt64

func NewBigIntegerFromInt64(n int64) Value

NewBigIntegerFromInt64 creates a big integer value from an int64.

func NewBigIntegerFromString

func NewBigIntegerFromString(s string, base int) Value

NewBigIntegerFromString creates a big integer from a string in the given base. Returns nil if the string is not a valid integer.

func NewBoolean

func NewBoolean(b bool) Value

NewBoolean creates a Scheme boolean.

func NewComplex

func NewComplex(v complex128) Value

NewComplex creates a Scheme complex number from a Go complex128.

func NewComplexFromParts

func NewComplexFromParts(realPart, imagPart float64) Value

NewComplexFromParts creates a Scheme complex number from real and imaginary parts.

func NewFloat

func NewFloat(f float64) Value

NewFloat creates a Scheme inexact real.

func NewInteger

func NewInteger(n int64) Value

NewInteger creates a Scheme integer.

func NewList

func NewList(vals ...Value) Value

NewList creates a Scheme list from values.

func NewRational

func NewRational(num, denom int64) Value

NewRational creates a Scheme exact rational number.

func NewRationalFromBigInt

func NewRationalFromBigInt(num, denom *big.Int) Value

NewRationalFromBigInt creates a Scheme exact rational from big.Int numerator and denominator.

func NewString

func NewString(s string) Value

NewString creates a Scheme string.

func NewSymbol

func NewSymbol(s string) Value

NewSymbol creates a Scheme symbol.

func NewVector

func NewVector(vals ...Value) Value

NewVector creates a Scheme vector from values.

func ToSlice

func ToSlice(ctx context.Context, v Value) ([]Value, bool)

ToSlice converts a proper list to a Go slice. Returns (slice, true) on success, or (nil, false) if v is not a proper list.

func WrapValue

func WrapValue(v values.Value) Value

WrapValue wraps a values.Value as a wile.Value for use with Engine methods that accept Value (FormLabel, DisassembleValue, etc.). Returns nil if v is nil.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL