Documentation
¶
Index ¶
- Constants
- func GoError(v *LVal) error
- func GoFloat64(v *LVal) (float64, bool)
- func GoInt(v *LVal) (int, bool)
- func GoMap(v *LVal) (map[interface{}]interface{}, bool)
- func GoSlice(v *LVal) ([]interface{}, bool)
- func GoString(v *LVal) (string, bool)
- func GoValue(v *LVal) interface{}
- func IsInternalPanic(v *LVal) bool
- func JoinDocStrings(parts []string) string
- func Not(v *LVal) bool
- func RegisterDefaultBuiltin(name string, formals *LVal, fn LBuiltin)
- func RegisterDefaultMacro(name string, formals *LVal, fn LBuiltin)
- func RegisterDefaultSpecialOp(name string, formals *LVal, fn LBuiltin)
- func SealedASTFingerprint(roots []*LVal) uint64
- func SymbolName(v *LVal) (string, bool)
- func True(v *LVal) bool
- func VerifySealedASTs() error
- type CachedSource
- type CallFrame
- type CallStack
- func (s *CallStack) CheckHeight() error
- func (s *CallStack) CheckTailCall() error
- func (s *CallStack) CheckTailIterations() error
- func (s *CallStack) Copy() *CallStack
- func (s *CallStack) DebugPrint(w io.Writer) (int, error)
- func (s *CallStack) Pop() CallFrame
- func (s *CallStack) PushFID(src *token.Location, fid string, pkg string, name string) error
- func (s *CallStack) TerminalFID(fid string) int
- func (s *CallStack) Top() *CallFrame
- type Config
- func WithContext(ctx context.Context) Config
- func WithDebugger(d Debugger) Config
- func WithLibrary(l SourceLibrary) Config
- func WithLoader(fn Loader) Config
- func WithMaxAlloc(n int) Config
- func WithMaxEvalNesting(n int) Config
- func WithMaxMacroExpansionDepth(n int) Config
- func WithMaxSleep(d time.Duration) Config
- func WithMaxSteps(n int64) Config
- func WithMaxTailIterations(n int) Config
- func WithMaximumLogicalStackHeight(n int) Config
- func WithMaximumPhysicalStackHeight(n int) Config
- func WithReader(r Reader) Config
- func WithStderr(w io.Writer) Config
- type DebugAction
- type Debugger
- type ErrorVal
- type FSLibrary
- type ForkOption
- type FunctionInfo
- type LBuiltin
- type LBuiltinDef
- type LEnv
- func (env *LEnv) AddBuiltins(external bool, funs ...LBuiltinDef)
- func (env *LEnv) AddMacros(external bool, macs ...LBuiltinDef)
- func (env *LEnv) AddSpecialOps(external bool, ops ...LBuiltinDef)
- func (env *LEnv) Bindings() iter.Seq2[string, *LVal]
- func (env *LEnv) Context() context.Context
- func (env *LEnv) Copy() *LEnv
- func (env *LEnv) DefinePackage(name *LVal) *LVal
- func (env *LEnv) Error(msg ...interface{}) *LVal
- func (env *LEnv) ErrorAssociate(lerr *LVal) *LVal
- func (env *LEnv) ErrorCondition(condition string, v ...interface{}) *LVal
- func (env *LEnv) ErrorConditionf(condition string, format string, v ...interface{}) *LVal
- func (env *LEnv) Errorf(format string, v ...interface{}) *LVal
- func (env *LEnv) Eval(v *LVal) *LValdeprecated
- func (env *LEnv) EvalContext(ctx context.Context, v *LVal) *LVal
- func (env *LEnv) EvalSExpr(s *LVal) *LVal
- func (env *LEnv) Fork(opts ...ForkOption) (*LEnv, error)
- func (env *LEnv) FunCall(fun, args *LVal) *LValdeprecated
- func (env *LEnv) FunCallContext(ctx context.Context, fun, args *LVal) *LVal
- func (env *LEnv) GenSym() *LVal
- func (env *LEnv) Get(k *LVal) *LVal
- func (env *LEnv) GetFun(fun *LVal) *LVal
- func (env *LEnv) GetFunGlobal(fun *LVal) *LVal
- func (env *LEnv) GetFunName(f *LVal) string
- func (env *LEnv) GetGlobal(k *LVal) *LVal
- func (env *LEnv) InPackage(name *LVal) *LVal
- func (env *LEnv) Lambda(formals *LVal, body []*LVal) *LVal
- func (env *LEnv) Load(name string, r io.Reader) *LValdeprecated
- func (env *LEnv) LoadContext(ctx context.Context, name string, r io.Reader) *LVal
- func (env *LEnv) LoadFile(loc string) *LValdeprecated
- func (env *LEnv) LoadFileContext(ctx context.Context, loc string) *LVal
- func (env *LEnv) LoadLocation(name string, loc string, r io.Reader) *LValdeprecated
- func (env *LEnv) LoadLocationContext(ctx context.Context, name, loc string, r io.Reader) *LVal
- func (env *LEnv) LoadProgram(p Program) *LValdeprecated
- func (env *LEnv) LoadProgramContext(ctx context.Context, p Program) *LVal
- func (env *LEnv) LoadString(name, exprs string) *LValdeprecated
- func (env *LEnv) LoadStringContext(ctx context.Context, name, exprs string) *LVal
- func (env *LEnv) MacroCall(fun, args *LVal) *LVal
- func (env *LEnv) New(typ *LVal, args *LVal) *LVal
- func (env *LEnv) NumBindings() int
- func (env *LEnv) Parent() *LEnv
- func (env *LEnv) ParseProgram(name, loc string, r io.Reader) (Program, error)
- func (env *LEnv) Put(k, v *LVal) *LVal
- func (env *LEnv) PutGlobal(k, v *LVal) *LVal
- func (env *LEnv) SetPackageDoc(doc string)
- func (env *LEnv) SetSymbolDoc(name, doc string)
- func (env *LEnv) Source() *token.Location
- func (env *LEnv) SpecialOpCall(fun, args *LVal) *LVal
- func (env *LEnv) TaggedValue(typ *LVal, val *LVal) *LVal
- func (env *LEnv) Terminal(expr *LVal) *LVal
- func (env *LEnv) Update(k, v *LVal) *LVal
- func (env *LEnv) UsePackage(name *LVal) *LVal
- type LFunType
- type LType
- type LVal
- func Array(dims *LVal, cells []*LVal) *LVal
- func Bool(b bool) *LVal
- func Bytes(b []byte) *LVal
- func Error(err error) *LVal
- func ErrorCondition(condition string, err error) *LVal
- func ErrorConditionf(condition string, format string, v ...interface{}) *LVal
- func Errorf(format string, v ...interface{}) *LVal
- func Float(x float64) *LVal
- func Formals(argSymbols ...string) *LVal
- func Fun(fid string, formals *LVal, fn LBuiltin) *LValdeprecated
- func FunInPackage(pkg, fid string, formals *LVal, fn LBuiltin) *LVal
- func FunRef(symbol, fun *LVal) *LVal
- func GetType(v *LVal) *LVal
- func InitializeTypedef(env *LEnv) *LVal
- func InitializeUserEnv(env *LEnv, config ...Config) *LVal
- func Int(x int) *LVal
- func Macro(fid string, formals *LVal, fn LBuiltin) *LValdeprecated
- func MacroInPackage(pkg, fid string, formals *LVal, fn LBuiltin) *LVal
- func MakeVector(n int) *LVal
- func Native(v interface{}) *LVal
- func Nil() *LVal
- func QExpr(cells []*LVal) *LVal
- func QSymbol(s string) *LVal
- func Quote(v *LVal) *LVal
- func SExpr(cells []*LVal) *LVal
- func SortedMap() *LVal
- func SortedMapFromData(data *MapData) *LVal
- func SpecialOp(fid string, formals *LVal, fn LBuiltin) *LValdeprecated
- func SpecialOpInPackage(pkg, fid string, formals *LVal, fn LBuiltin) *LVal
- func Splice(v *LVal) *LVal
- func SplitSymbol(sym *LVal) *LVal
- func String(str string) *LVal
- func Symbol(s string) *LVal
- func Value(v interface{}) *LVal
- func Vector(cells []*LVal) *LVal
- func (v *LVal) ArrayDims() *LVal
- func (v *LVal) ArrayIndex(index ...*LVal) *LVal
- func (v *LVal) Builtin() LBuiltin
- func (v *LVal) Bytes() []byte
- func (v *LVal) CallStack() *CallStack
- func (v *LVal) Copy() *LVal
- func (v *LVal) Docstring() string
- func (v *LVal) Equal(other *LVal) *LVal
- func (v *LVal) EqualNum(other *LVal) *LVal
- func (v *LVal) FID() string
- func (v *LVal) InheritSeal(src *LVal)
- func (v *LVal) IsMacro() bool
- func (v *LVal) IsNil() bool
- func (v *LVal) IsNumeric() bool
- func (v *LVal) IsQuoted() bool
- func (v *LVal) IsSealed() bool
- func (v *LVal) IsSpecialFun() bool
- func (v *LVal) IsSpecialOp() bool
- func (v *LVal) KeyArg(i int) *LVal
- func (v *LVal) Len() int
- func (v *LVal) MacroExpansion() (MacroExpansionMeta, bool)
- func (v *LVal) Map() *MapData
- func (v *LVal) MapEntries() *LVal
- func (v *LVal) MapGet(k interface{}) *LVal
- func (v *LVal) MapKeys() *LVal
- func (v *LVal) MapSet(k interface{}, val *LVal) *LVal
- func (v *LVal) Package() string
- func (v *LVal) ReqArg(env *LEnv, i int) *LVal
- func (v *LVal) SealAST()
- func (v *LVal) SetCallStack(stack *CallStack)
- func (v *LVal) SetSource(loc *token.Location)
- func (v *LVal) Source() (token.Location, bool)
- func (v *LVal) String() string
- func (v *LVal) UserData() *LVal
- type LoadCache
- type Loader
- type LocationReader
- type LogicalStackOverflowError
- type MacroExpansionMeta
- type Map
- type MapData
- type NativeCloner
- type Package
- func (pkg *Package) Export(names ...string)
- func (pkg *Package) Exports(sym ...string)
- func (pkg *Package) Externals() []string
- func (pkg *Package) Get(k *LVal) *LVal
- func (pkg *Package) GetFunName(fid string) string
- func (pkg *Package) NumExternals() int
- func (pkg *Package) Put(k, v *LVal) *LVal
- func (pkg *Package) Symbol(name string) (*LVal, bool)
- func (pkg *Package) SymbolDoc(name string) string
- func (pkg *Package) SymbolNames() []string
- func (pkg *Package) Update(k, v *LVal) *LVal
- type PackageRegistry
- type ParamInfo
- type ParamKind
- type PhysicalStackOverflowError
- type Profiler
- type Program
- type Reader
- type ReaderIdentity
- type RelativeFileSystemLibrary
- type Runtime
- func (r *Runtime) CheckAlloc(n int) string
- func (r *Runtime) CurrentCondition() *LVal
- func (r *Runtime) EvalNesting() int
- func (r *Runtime) GenEnvID() uint
- func (r *Runtime) GenSym() string
- func (r *Runtime) MaxAllocBytes() int
- func (r *Runtime) MaxEvalNestingDepth() int
- func (r *Runtime) MaxMacroExpansions() int
- func (r *Runtime) MaxSleepCeiling() time.Duration
- func (r *Runtime) PopCondition() *LVal
- func (r *Runtime) PushCondition(err *LVal)
- func (r *Runtime) ResetSteps()
- func (r *Runtime) Steps() int64
- func (r *Runtime) TotalSteps() int64
- type SingletonSnapshot
- type SourceContext
- type SourceLibrary
- type TailIterationLimitError
Constants ¶
const ( CondParseError = "parse-error" CondScanError = "scan-error" CondUnmatchedSyntax = "unmatched-syntax" CondMismatchedSyntax = "mismatched-syntax" CondInvalidSymbol = "invalid-symbol" CondInvalidOctalLiteral = "invalid-octal-literal" CondInvalidHexLiteral = "invalid-hex-literal" CondInvalidFloat = "invalid-float" CondInvalidString = "invalid-string" CondOverflow = "integer-overflow-error" )
Parse error condition names. These are stable API for programmatic error classification in LSP and tooling integrations.
const ( CondContextCancelled = "context-cancelled" CondStepLimitExceeded = "step-limit-exceeded" // CondEvalNestingExceeded reports that the evaluator recursed into // itself more deeply than Runtime.MaxEvalNesting allows. It is the // recoverable substitute for a Go stack overflow, which is a // runtime.throw that neither recover() nor handler-bind can intercept. CondEvalNestingExceeded = "eval-nesting-exceeded" // CondSleepLimitExceeded reports that a requested sleep was longer than // the caller is allowed to sleep for, and was refused WITHOUT sleeping. // It is distinct from context-cancelled: nothing was cancelled and no // time passed, the request was rejected on entry. See // DefaultMaxSleep and Runtime.MaxSleep. CondSleepLimitExceeded = "sleep-limit-exceeded" )
Evaluation limit condition names.
const ( DefaultMaxLogicalStackHeight = 0 DefaultMaxPhysicalStackHeight = 25000 DefaultMaxTailIterations = 1000000 DefaultMaxEvalNesting = 100000 )
Default evaluation depth limits.
DefaultMaxPhysicalStackHeight — the memory guard, keep it on
This bounds the number of frames physically present on the call stack. It protects against unbounded *non-tail* recursion exhausting the Go goroutine stack, which aborts the whole process with an unrecoverable "stack overflow" that no handler-bind can catch. Measured on linux/amd64 with the limit disabled, ELPS non-tail recursion survives 200,000 levels (~678MB of Go stack) and crashes the runtime at 400,000. 25000 sits 8-16x below that threshold, and the margin is load-bearing: the crash point moves with Go's stack settings, per-frame size, and architecture. Do not raise it casually and do not default it off.
Frames elided by tail-call optimization do not count toward it, so a tail-recursive loop runs at constant physical height no matter how many iterations it performs.
It bounds FRAMES, not evaluation depth. A frame is pushed when a function is invoked, and LEnv.evalSExprCells deliberately evaluates a call's arguments BEFORE pushing anything (pushing there would corrupt the error messages and stack dumps produced while an argument is being evaluated). Nested arguments therefore recurse through the Go evaluator at physical height zero, which is exactly the shape this limit exists to stop and exactly the shape it cannot see. DefaultMaxEvalNesting covers that path; see issue #316.
DefaultMaxEvalNesting — the same memory guard, on the evaluator's own recursion
This bounds how deeply LEnv.eval may recurse into itself, which is the true measure of Go stack consumed by the evaluator. Every nested evaluation passes through eval — a call's arguments, a special operator's subforms, a builtin re-entering via env.Eval — so a single counter incremented on entry and decremented on exit bounds all of them, and unlike a stack frame it costs no allocation and does not appear in a stack trace.
Measured on linux/amd64 against the 1GB default goroutine stack: argument-nesting depth 700,000 completes, 800,000 aborts the process with "fatal error: stack overflow" (~1.4KB of Go stack per level). 100,000 sits 7-8x below that, matching the margin DefaultMaxPhysicalStackHeight keeps.
THAT MARGIN IS POINTER-SIZE DEPENDENT. Go caps a goroutine stack at 1GB on 64-bit but 250MB on 32-bit (runtime/proc.go: maxstacksize). At ~1.4KB per level the default costs ~140MB, so the same 100,000 is a 7-8x margin on amd64 and roughly 1.8x on a 32-bit build -- and an embedder whose own call stack is already deep when it enters Eval eats into what is left. Nothing here is exercised on 32-bit: CI builds ubuntu-latest and windows-latest, both amd64. A 32-bit embedder should lower this with WithMaxEvalNesting rather than trust the default.
It also sits comfortably above ordinary recursion. Measured with (defun sum (n) (if (<= n 0) 0 (+ n (sum (- n 1))))), the physical limit binds first for any MaxEvalNesting of 38,000 or more: that recursion costs ~1.5 eval levels per physical frame, so it reaches physical height 25,001 at nesting ~38,000. Only code whose body nests expressions more than ~4 deep per recursion level can reach 100,000 nesting before 25,000 frames -- such code costs 4 eval levels per frame, ~2.6x what ordinary recursion costs, so it really is consuming that much more Go stack per frame.
The two limits bound different quantities and neither implies the other. Deeply nested arguments raise nesting at constant height; a long chain of tail-position calls raises neither.
Before this limit existed the only thing bounding evaluator recursion was rdparser.DefaultMaxParseDepth (10,000) — incidentally, because nesting had to be parsed before it could be evaluated. That bound was never sound: a recursive macro generates nesting at expansion time from an integer argument, so ~120 bytes of source reached the fatal overflow with every documented limit at its default. Parse depth is no longer load-bearing for stack safety.
DefaultMaxTailIterations — the runaway-loop backstop
This bounds how many turns a single tail-recursive loop may take. Its unit is loop turns, so it is a knob an operator can reason about.
It is a backstop against a loop that never terminates, NOT a time bound. One million turns of a trivial O(1) body costs ~4s of interpreter overhead, but turns say nothing about the work done per turn: a body that conses a list, concatenates a string, or calls any O(n) builtin can run for minutes — or effectively forever — inside the same turn budget, and a step limit does not help either because an O(n) builtin is one step. To bound wall-clock time, pass a context with a deadline (WithContext, or the *Context methods on LEnv). That is the only limit here that measures time.
DefaultMaxLogicalStackHeight — off by default
Logical height accumulates every frame elided by tail-call optimization, so it is a running total of *elided frames*, not a nesting depth and not an iteration count. One turn of a tail loop adds the length of the elided terminal chain — 2 for a trivial body, more when the body nests terminal forms more deeply — so the same numeric limit permits a different number of iterations depending on the shape of the loop. That makes it unusable as a default runaway-loop bound: it fails data-dependently with a message that reads like runaway recursion when the program is a correct constant- space loop. MaxTailIterations does that job in honest units instead.
Logical height remains valuable as a stack-trace diagnostic, and callers who specifically want to bound it can opt in via WithMaximumLogicalStackHeight.
Bounding total work
None of these limits catch a non-recursive infinite loop (a host-provided `while`, for example) because such a loop neither grows the stack nor performs tail calls. WithMaxSteps bounds the number of evaluation steps, which does catch that shape.
None of them — including WithMaxSteps — bound elapsed time, because a single step may perform an arbitrary amount of work inside a builtin. A context deadline is the only real time bound; see WithContext and the *Context methods on LEnv.
Bounding total memory
Runtime.MaxAlloc is PER-OPERATION, not cumulative: each builtin checks its own output size against it, so a program that allocates a thousand buffers of MaxAlloc-1 bytes passes every check. Nothing here tracks total heap. The incidental bound is whatever limit stops the loop doing the allocating — MaxTailIterations for a tail loop, MaxSteps for a stepped one — multiplied by MaxAlloc, which is not a memory budget in any useful sense. A host that must bound total memory has to do it outside the interpreter (a cgroup, a container limit, or Go's GOMEMLIMIT plus a watchdog); see Runtime.MaxAlloc and CheckAlloc.
const AnonArgSymbolPrefix = "%"
AnonArgSymbolPrefix is used to indicate unnamed arguments in the anonymous function shorthand “(expr ...)”.
const CondInternalPanic = "internal-panic"
const CondMissingArgument = "missing-argument"
CondInternalPanic is the condition type of an error produced by recovering a Go panic that escaped a builtin, special operator, or any other host code called during evaluation.
It is deliberately NOT an ordinary error condition. A panic means host Go code hit a bug — a nil dereference, an out-of-range index, a failed invariant — and left its data structures in an unknown state. Treating that as a routine, catchable lisp error lets `ignore-errors` and a catch-all `handler-bind` swallow it silently, so a genuine host defect looks exactly like `(error 'my-condition "...")` and the program keeps running on top of it.
Accordingly, `ignore-errors` and the catch-all `condition` handler specifier do not intercept this condition; it propagates to the caller. A handler that genuinely wants to intercept host panics must name the condition explicitly:
(handler-bind ((internal-panic (lambda (c &rest args) ...)))
(risky))
CondMissingArgument reports that a builtin was invoked with fewer argument cells than it reads. The evaluator supplies one cell per declared formal, so this cannot arise from lisp; it means an embedder bound the builtin to formals declaring fewer arguments than the Go function requires. Raised as an ordinary condition so a caller can handler-bind it, rather than panicking in the embedder's process.
const CondModifyLiteral = "modify-literal-error"
CondModifyLiteral reports an attempt to modify a sealed program literal in place. Quoted data, macro arguments and defun bodies are part of the program text, shared by every environment evaluating the same parse (lisp/seal.go), so the in-place mutators that could write them — stable-sort on a sealed list, and the (slice 'vector ...) / (append 'vector ...) forms that would wrap or write a sealed list's backing array — raise this condition instead. It is an ordinary catchable condition: handler-bind can name it and ignore-errors swallows it. The remedy the message names is `(copy x)`, which returns a fresh, fully mutable deep copy.
The empty list is the deliberate carve-out: builtins such as cdr, rest and keys return the shared (sealed) empty-list value, so erroring on it would make `(stable-sort < (rest xs))` fail only when xs happens to have fewer than two elements — a data-dependent error on correct runtime code. An empty list has no storage to write or alias, so the guarded sites accept it and return fresh storage.
const DefaultLangPackage = "lisp"
DefaultLangPackage is the name of default language package
const DefaultMaxAlloc = 10 * 1024 * 1024 // 10 million (bytes or elements)
DefaultMaxAlloc is the per-operation allocation size cap (in bytes for strings, in elements for sequences) enforced by builtins like concat, append, map, zip, reverse, make-sequence, and JSON load. Each operation checks its own output size independently — this is not a cumulative memory budget. It prevents a single malicious or accidental call from exhausting memory. Applications can override this via Runtime.MaxAlloc.
const DefaultMaxMacroExpansionDepth = 1000
DefaultMaxMacroExpansionDepth is the maximum number of successive macro expansions allowed before Eval returns an error. This prevents infinite macro expansion from exhausting memory or running forever.
const DefaultMaxSleep = time.Hour
DefaultMaxSleep bounds a single (time:sleep d) that does not pass an explicit :max, and is the last of the execution limits because it bounds WALL CLOCK rather than work.
Every other limit here counts something the interpreter does — steps, frames, iterations, bytes, nesting. A sleeping goroutine does none of them, which is why time:sleep was unbounded by all of them at once and "9223372036854775807ns" blocked for ~292 years (issue #314). Bounding it needs a wall-clock number, and there is no work-based limit that implies one.
One hour is chosen to be far above any plausible legitimate sleep in an embedded interpreter — a backoff, a poll interval, a test delay are all orders of magnitude below it — while still refusing the 292-year shape immediately rather than after 292 years. A caller who genuinely wants longer says so with :max, which is the point: the length becomes explicit at the call site instead of being an accident of arithmetic.
:max raises the per-call cap but CANNOT exceed Runtime.MaxSleep when the embedder has set one. That split is deliberate. Program source and the embedder are different trust domains: downstream (luthersystems/substrate) the program is customer-supplied phylum running as chaincode, so a cap that program source could raise would be decorative. DefaultMaxSleep is a guard against accidents, which the program may relax; Runtime.MaxSleep is a containment bound, which only the host may relax.
const DefaultUserPackage = "user"
DefaultUserPackage is the name of the entry point package for interpreting user code.
const ElpsVersion = "1.7"
const FalseSymbol = "false"
FalseSymbol is the language's defacto false boolean value, though nil is also considered false by functions and operators expecting a boolean.
const KeyArgSymbol = "&key"
KeyArgSymbol is the symbol used to indicate keyword arguments to a function. Keyword arguments are declared following optional arguments. Keyword arguments may be supplied in any order and must specify the symbol they wish should be bound to by preceding the argument value with a keyward symbol.
const MaxExprFormals = 1024
MaxExprFormals bounds the positional-placeholder index accepted by the `expr` special operator (the #^ reader macro).
The index is read out of the source text and used directly as the length of the generated formals slice, so `#^(%N)` asks opExpr to construct N fresh symbols -- each an *LVal plus a fmt.Sprintf'd name -- from an input whose size is log10(N). Measured on linux/amd64 that is ~136 bytes per formal, so the unbounded form reached `fatal error: out of memory` (a runtime.throw, which recover() cannot intercept and no handler-bind can contain) from 14 bytes of source.
This is deliberately a dedicated bound and NOT Runtime.MaxAlloc. MaxAlloc's unit at every other call site is bytes of a buffer or elements of a slice the caller already holds; here one unit is a whole constructed value, so the two are ~130x apart in cost and no single number serves both. The mismatch is visible in both directions: MaxAlloc's 10,000,000 default still permits ~1.3GB here, while a MaxAlloc of 8 -- reasonable for a byte buffer -- would reject `#^(list %9)`, an ordinary nine-argument lambda.
1024 is chosen against real programs rather than against memory: C's standard guarantees only 127 parameters (C99 5.2.4.1) and the JVM caps a method at 255, so a bound 4-8x above the most permissive mainstream limit cannot plausibly reject a program a human wrote, while capping this site at ~136KB. It is a constant rather than a Runtime knob because it bounds a syntactic construct -- how many parameters a lambda literal may declare -- which is not something an embedder's workload varies.
const MetaArgPrefix = "&"
MetadaArgPrefix is a disallowed prefix for formal argument symbols. Any symbol beginning with MetaArgPrefix in a formal argument list will be treated with special meaning an unrecognized symbols will cause a runtime error to occur.
const OptArgSymbol = "&optional"
OptArgSymbol is the symbol used to indicate optional arguments to a function. Optional arguments are bound to given arguments if there are arguments left over following the binding of required arguments, otherwise they are bound to nil.
const TrueSymbol = "true"
TrueSymbol is the language's defacto true boolean value, though anything other than nil and 'false are considered true by functions and operators expecting a boolean.
const VarArgSymbol = "&rest"
VarArgSymbol is the symbol that indicates a variadic function argument in a function's list of formal arguments. Functions may have at most one variadic argument. Variadic arguments must be defined following optional arguments and are bound after all optional arguments are bound.
Currently, it is an error to declare a function which has both variadic and keyword arguments. While this may change it will always be discouraged due to the difficulty handling such mixtures of argument types. It would be better to define separate functions, one with keyward args and the other with variadic args.
Variables ¶
This section is empty.
Functions ¶
func GoFloat64 ¶
GoFloat64 converts the numeric value that v represents to a float64 and returns it with the value true. If v does not represent a number GoFloat64 returns a false second argument
func GoInt ¶
GoInt converts the numeric value that v represents to and int and returns it with the value true. If v does not represent a number GoInt returns a false second argument
func GoMap ¶
GoMap converts an LSortMap to its Go equivalent and returns it with a true second argument. If v does not represent a map GoMap returns a false second argument. Application's using custom Map implementations which allow arbitrary keys may not be able to construct a native Go map, in which case GoMap returns (nil, true).
func GoSlice ¶
GoSlice returns the string that v represents and the value true. If v does not represent a string GoSlice returns a false second argument
func GoString ¶
GoString returns the string that v represents and the value true. If v does not represent a string GoString returns a false second argument
func GoValue ¶
func GoValue(v *LVal) interface{}
GoValue converts v to its natural representation in Go. Quotes are ignored and all lists are turned into slices. Symbols are converted to strings. The value Nil() is converted to nil. Functions are returned as is.
NOTE: These semantics may change. It's unclear what the exact need is in corner cases.
func IsInternalPanic ¶ added in v1.49.0
IsInternalPanic reports whether v is an error produced by recovering a Go panic that escaped host code during evaluation.
This is the check `ignore-errors` and `handler-bind` use, and embedders should use it too, rather than comparing the condition name against CondInternalPanic. The condition name alone is forgeable: lisp code can write (error 'internal-panic "...") and, if the name were the only test, would produce an error that no catch-all handler could contain.
The marker is the Go stack snapshot the recover handler attaches to the error's CallStack copy. Nothing reachable from lisp can populate it — the live Runtime stack's GoStack is always nil, so an error raised by the `error` builtin always copies a nil GoStack. A forged 'internal-panic is therefore treated as an ordinary condition and stays containable.
func JoinDocStrings ¶ added in v1.16.12
JoinDocStrings joins multiple doc string parts into a single string. Non-empty strings are joined with spaces. Empty strings produce blank lines, acting as paragraph separators.
func RegisterDefaultBuiltin ¶
RegisterDefaultBuiltin adds the given function to the list returned by DefaultBuiltins.
func RegisterDefaultMacro ¶
RegisterDefaultMacro adds the given function to the list returned by DefaultMacros.
func RegisterDefaultSpecialOp ¶
RegisterDefaultSpecialOp adds the given function to the list returned by DefaultSpecialOps.
func SealedASTFingerprint ¶ added in v1.52.0
SealedASTFingerprint computes a structural digest of the sealed nodes reachable from roots: types, scalar payloads, cell structure, and source-location contents, in traversal order. Any in-place change to a sealed node changes the digest; see the file comment for the exact coverage (and non-coverage) contract.
The digest is content-based — no pointer identity — so it is deterministic across processes: fingerprint a tree at parse time, store the value, and compare at any later point to prove the sealed bytes never changed. Fingerprinting never mutates the tree and allocates nothing.
func SymbolName ¶
SymbolName returns the name of the symbol that v represents and the value true. If v does not represent a symbol SymbolName returns a false second argument
func True ¶
True interprets v as a boolean and returns the result.
NOTE: I don't like this name, really. But I can't think of a better one.
func VerifySealedASTs ¶ added in v1.52.0
func VerifySealedASTs() error
VerifySealedASTs re-verifies every sealed parse recorded in this process against its parse-time fingerprint. Production builds (no elpscheck tag) record nothing, so this always returns nil at zero cost; under `go test -tags elpscheck` it performs the real check. See lisp/seal_check_elpscheck.go.
Types ¶
type CachedSource ¶ added in v1.54.0
type CachedSource struct {
// contains filtered or unexported fields
}
CachedSource is an opaque, immutable, sealed parse result. Only elps mints one — an embedder holds and returns handles but cannot construct one, open one, or reach the AST nodes inside it.
A *CachedSource is safe to share: its expressions are sealed throughout (see newCachedSource), so it may be held by a process-wide cache and served to any number of Runtimes, concurrently. Its own fields are written once at construction and never again.
The zero value is not usable; obtain one only from a LoadCache's Load.
func (*CachedSource) Fingerprint ¶ added in v1.54.0
func (s *CachedSource) Fingerprint() uint64
Fingerprint returns the canonical structural digest of the sealed parse (lisp/sealfp.go), taken at admission. It is a value, not a reference: exposing it lets a host log or deduplicate entries without any way to reach the nodes it summarises.
func (*CachedSource) Key ¶ added in v1.54.0
func (s *CachedSource) Key() string
Key returns the cache key elps derived for this entry. It is the key the entry was Stored under, and a cache that hands back an entry under a different key is treated as a miss (see (*LEnv).readCached).
func (*CachedSource) Len ¶ added in v1.54.0
func (s *CachedSource) Len() int
Len returns the number of top-level expressions in the cached parse.
func (*CachedSource) Location ¶ added in v1.54.0
func (s *CachedSource) Location() string
Location returns the physical location the source was parsed under, or the empty string when the parse had no location (the Reader, rather than LocationReader, path).
func (*CachedSource) Name ¶ added in v1.54.0
func (s *CachedSource) Name() string
Name returns the stream name the source was parsed under.
func (*CachedSource) String ¶ added in v1.54.0
func (s *CachedSource) String() string
String returns a short debugging description. Like Program.String it deliberately does not render the cached expressions.
type CallFrame ¶
type CallFrame struct {
Source *token.Location
FID string
Package string
Name string
// HeightLogical is the frame's virtual depth: its physical depth plus
// every frame elided beneath it by tail-call optimization. It is a
// diagnostic quantity — a tail loop runs in constant physical height
// but its HeightLogical grows without bound.
HeightLogical int
Terminal bool
TROBlock bool // Stop tail-recursion optimization from collapsing this frame
// TailIterations counts tail-call iterations performed at this frame.
// Each turn of a tail loop increments it by exactly one, regardless of
// how many frames that turn elided.
//
// DELIBERATELY int32, AND DELIBERATELY PLACED AFTER THE BOOLS. A
// CallFrame is allocated per call, so its size is on the hot path: as
// an `int` declared above Terminal/TROBlock this field grew the struct
// from 72 to 80 bytes (+11.1%), which the benchmark gate caught as
// +8.44% B/op on EnvFunCallRecursion with allocs/op unchanged — same
// allocation count, larger allocations. Two bools leave 6 bytes of
// tail padding that an 8-byte int cannot use but a 4-byte int32 can, so
// this placement is free: the struct stays 72 bytes.
//
// int32 caps at ~2.1e9 iterations against a default budget of 1e6. If
// MaxTailIterations is ever raised near that ceiling, widen this field
// and accept the 8 bytes rather than letting the counter wrap.
TailIterations int32
}
CallFrame is one frame in the CallStack
func (*CallFrame) QualifiedFunName ¶
QualifiedFunName returns the qualified name for the function on the top of the stack. If ignore is non-empty QualifiedFunName returns unqualified names for functions in the given packages.
type CallStack ¶
type CallStack struct {
Frames []CallFrame
GoStack []byte
// MaxHeightLogical bounds CallFrame.HeightLogical, which accumulates
// every frame elided by tail-call optimization. Its unit is *elided
// frames*, not loop turns: one iteration of a tail loop adds the length
// of the elided terminal chain (2 for a trivial (defun f (n) (if ...
// (f ...))), more for a body with deeper terminal nesting). Because
// the ratio depends on the shape of the loop body, this is a poor knob
// for bounding iteration count — use MaxTailIterations for that. Zero
// disables the check (the default).
MaxHeightLogical int
// MaxHeightPhysical bounds the number of frames physically on the
// stack. This is the memory guard: exceeding the Go goroutine stack
// aborts the process with an unrecoverable "stack overflow", so this
// limit must stay comfortably below that threshold. Frames elided by
// tail-call optimization do not count toward it. Zero disables the
// check.
MaxHeightPhysical int
// MaxTailIterations bounds CallFrame.TailIterations — the number of
// tail-call iterations performed at a single stack frame. Unlike
// MaxHeightLogical its unit is loop turns, independent of the shape of
// the loop body, so an operator can reason about it directly. It is a
// runaway-loop backstop, not a business limit. Zero disables the
// check.
MaxTailIterations int
}
CallStack is a function call stack.
For errors produced by env.eval's recover() of a Go panic, GoStack carries the runtime.Stack output captured at the panic site so callers (via ErrorVal.WriteTrace or direct access) can render the Go-level origin alongside the ELPS frames. It is nil for non-panic errors.
Field order is layout-sensitive: the two slice headers lead so the GC scan extent stops at 32 bytes instead of 48. Keep scalars trailing.
func (*CallStack) CheckHeight ¶
func (*CallStack) CheckTailCall ¶ added in v1.49.0
CheckTailCall runs the limit checks that apply after a tail-call optimization elides frames: the (opt-in) logical height bound and the tail-iteration bound. Physical height is unchanged by a tail call, so it is deliberately not rechecked here.
func (*CallStack) CheckTailIterations ¶ added in v1.49.0
CheckTailIterations returns an error if the top frame has performed more tail-call iterations than MaxTailIterations allows.
func (*CallStack) Copy ¶
Copy creates a copy of the current stack so that it can be attach to a runtime error.
func (*CallStack) DebugPrint ¶
DebugPrint prints s
func (*CallStack) Pop ¶
Pop removes the top CallFrame from the stack and returns it. If the stack is empty Pop returns nil.
func (*CallStack) TerminalFID ¶
TerminalFID determines if a chain of terminal stack frames that ends with fid (i.e. fid is a candidate for tail-recursion optimization) and returns the number of frames in the shortest such chain. If no such chain of terminal frames can be found then 0 is returned.
If a stack frame with TROBlock is found then the search for a terminal chain is prematurely terminated as a failure.
NOTE: If tail-recursion optimization is working then the chain of calls found by TerminalFID is unique.
type Config ¶
Config is a function that configures a root environment or its runtime.
func WithContext ¶ added in v1.25.0
WithContext returns a Config that sets the initial context.Context for the root environment. The context is checked at each evaluation step; if it is cancelled or its deadline expires, evaluation returns a CondContextCancelled error. For per-call context control, use the *Context methods on LEnv instead.
func WithDebugger ¶ added in v1.21.0
WithDebugger returns a Config that attaches a debugger to the runtime. When a debugger is attached, tail recursion optimization is disabled to provide predictable stepping behavior and stack traces.
func WithLibrary ¶ added in v1.16.3
func WithLibrary(l SourceLibrary) Config
WithLibrary returns a Config that makes environments use l as a source library.
func WithLoader ¶
WithLoader returns a Config that executes fn and ensures that the environment's working package is reset following execution of fn. Despite fn having the same signature as a Config WithLoader allows a Loader to function more like the LEnv methods LoadFile, LoadString, etc.
func WithMaxAlloc ¶ added in v1.20.0
WithMaxAlloc returns a Config that sets the per-operation allocation size cap (in bytes for strings, in elements for sequences). This limits the output size of any single builtin call, not cumulative memory usage.
func WithMaxEvalNesting ¶ added in v1.49.0
WithMaxEvalNesting returns a Config that bounds how deeply the evaluator may recurse into itself while evaluating a single expression.
This is a distinct quantity from stack height and is not implied by it. A call's arguments are evaluated before the call's frame is pushed, so ((lambda (x) x) ((lambda (x) x) ... )) recurses through the Go evaluator while the physical stack height stays at zero — the exact shape WithMaximumPhysicalStackHeight exists to stop and the one shape it cannot see (issue #316). Nesting is bounded here instead.
A value of 0 selects DefaultMaxEvalNesting. A negative value disables the check, which re-exposes the host process to an unrecoverable "fatal error: stack overflow"; do that only when some other bound on expression depth is guaranteed.
func WithMaxMacroExpansionDepth ¶ added in v1.20.0
WithMaxMacroExpansionDepth returns a Config that limits the number of successive macro expansions during evaluation. This prevents infinite macro expansion from exhausting memory.
func WithMaxSleep ¶ added in v1.49.0
WithMaxSleep returns a Config that sets a HARD CEILING on how long a single (time:sleep d) may block, in the host's hands rather than the program's.
This is the containment bound, and it is not the same knob as DefaultMaxSleep. A sleep with no explicit :max is capped at DefaultMaxSleep, which program source may raise per call with (time:sleep d :max m). That is a guard against accidents. The ceiling set here is what :max may not exceed, so a program cannot opt itself out of it — which matters when the program is untrusted, as customer-supplied phylum source is downstream in luthersystems/substrate.
Zero or negative means no ceiling: :max may name any duration. That is the default, because the interpreter cannot know what wall-clock budget the host is willing to spend.
Note what this does NOT do: it bounds one sleep call, not their sum. A loop of N sleeps each just under the ceiling still blocks for N times the ceiling. Bounding total elapsed time is what a context deadline is for (see WithContext) -- sleep observes that too, and refuses immediately rather than blocking to the deadline.
func WithMaxSteps ¶ added in v1.25.0
WithMaxSteps returns a Config that sets the maximum number of evaluation steps before evaluation returns a CondStepLimitExceeded error. A step is counted for each Eval entry, each TRO iteration, each macro re-expansion, and each turn of a dotimes loop. A value of 0 means unlimited (the default).
The dotimes turn is counted because an empty-bodied loop evaluates nothing: (dotimes (i 2147483647)) consumed no budget and could not be interrupted at all. It costs exactly one extra step per turn, so a dotimes-heavy program now uses more budget than it did -- proportionally most for a small body (a constant body goes from 1 step per turn to 2; a three-form body from 12 to 13). A budget pinned tightly against a previously measured figure may need raising. opDoTimes carries the full measurement table.
The budget is per top-level evaluation: the counter is reset each time an exported entry point (Eval, EvalContext, EvalSExpr, FunCall, FunCallContext, SpecialOpCall, MacroCall, or any Load*) is entered from outside an evaluation. Nested evaluation — a builtin calling back into Eval, the tail-call loops, forms evaluated by load — shares the enclosing budget and does not refill it. Without the reset the limit would be a lifetime quota that permanently kills a long-lived Runtime once it was reached.
Runtime.Steps reports the current evaluation's usage; Runtime.TotalSteps reports the lifetime total.
A step budget is the only limit here that bounds an infinite loop which neither recurses nor tail-calls; the stack limits cannot see such a loop.
func WithMaxTailIterations ¶ added in v1.49.0
WithMaxTailIterations returns a Config that bounds the number of tail-call iterations a single stack frame may perform. Tail calls run in constant stack space, so neither stack-height limit can bound a runaway tail loop; this is the limit that does. Its unit is loop turns, independent of how many frames each turn elides.
A value of 0 disables the check. The default is DefaultMaxTailIterations, chosen as a runaway-loop backstop rather than a business limit.
func WithMaximumLogicalStackHeight ¶
WithMaximumLogicalStackHeight returns a Config that will prevent an execution environment from allowing the logical stack height to exceed n. The logical height of the stack is the stack's physical height plus the number of stack frames which have been elided due to tail recursive call optimizations.
This limit is disabled by default (see DefaultMaxLogicalStackHeight). Its unit is elided frames, not loop turns: one turn of a tail loop adds the length of the elided terminal chain, which varies with the shape of the loop body. To bound how many turns a tail loop may take, use WithMaxTailIterations instead; to bound total evaluation work, use WithMaxSteps.
func WithMaximumPhysicalStackHeight ¶
WithMaximumPhysicalStackHeight returns a Config that will prevent an execution environment from allowing the physical stack height to exceed n. The physical stack height is the literal number of frames in the call stack and does not account for stack frames elided due to tail recursive call optimizations.
func WithReader ¶
WithReader returns a Config that makes environments use r to parse source streams. There is no default Reader for an environment.
func WithStderr ¶
WithStderr returns a Config that makes environments write debugging output to w instead of the default, os.Stderr.
type DebugAction ¶ added in v1.21.0
type DebugAction int
DebugAction represents the action the interpreter should take after the debugger resumes execution from a paused state.
const ( // DebugContinue resumes execution until the next breakpoint. DebugContinue DebugAction = iota // DebugStepInto pauses on the next OnEval call regardless of depth. DebugStepInto // DebugStepOver pauses on the next OnEval call at the same or // lesser stack depth (does not descend into function calls). DebugStepOver // DebugStepOut pauses on the next OnEval call at a lesser stack // depth (waits until the current function returns). DebugStepOut )
type Debugger ¶ added in v1.21.0
type Debugger interface {
// IsEnabled returns true when the debugger is actively debugging.
// A dormant debugger (attached but not yet activated) returns false,
// allowing the interpreter to skip all other hook calls.
IsEnabled() bool
// OnEval is called before evaluating any expression with a real source
// location (v has a non-nil source). Synthetic expressions from macro expansion
// are skipped.
// Returns true if the debugger wants execution to pause (breakpoint hit
// or step complete).
OnEval(env *LEnv, expr *LVal) bool
// WaitIfPaused blocks until the debugger allows execution to continue.
// Called when OnEval returns true. The eval goroutine blocks here while
// the DAP server goroutine processes user commands.
// Returns the action to take after resuming.
WaitIfPaused(env *LEnv, expr *LVal) DebugAction
// OnFunEntry is called when a function is entered, after formal
// parameters have been bound in the function's lexical environment.
// env is the caller's environment; fenv is the function's environment
// with parameter bindings in scope. For builtins (which have no lexical
// env), this hook is not called.
OnFunEntry(env *LEnv, fun *LVal, fenv *LEnv)
// OnFunReturn is called after a function returns.
// fun is the function value, result is the return value.
OnFunReturn(env *LEnv, fun, result *LVal)
// AfterFunCall is called in Eval after EvalSExpr returns, giving the
// debugger a chance to pause at the call-site expression when the
// stack depth has decreased (step-out from tail position). Returns
// true if execution should pause.
AfterFunCall(env *LEnv) bool
// OnError is called when an error condition is created (in
// ErrorCondition and ErrorConditionf). Returns true if the debugger
// wants execution to pause (exception breakpoint). When true, the
// interpreter calls WaitIfPaused with the error value.
OnError(env *LEnv, lerr *LVal) bool
}
Debugger is called by the interpreter at key execution points to support breakpoints, stepping, and variable inspection. When Runtime.Debugger is nil, no hook calls are made and there is zero overhead on the hot path.
Hook calls use a two-check gate pattern:
if d := env.Runtime.Debugger; d != nil && d.IsEnabled() { ... }
The nil check is free (branch-predicted not-taken). IsEnabled allows a debugger to remain attached but dormant, further reducing overhead when not actively debugging.
type ErrorVal ¶
type ErrorVal LVal
ErrorVal implements the error interface so that errors can be first class lisp objects. The error message is stored in the Str field while contextual information (e.g. call stack) can be stored in the Cells slice.
func (*ErrorVal) Condition ¶ added in v1.17.0
Condition returns the error condition name (e.g., "parse-error", "unmatched-syntax"). This is the programmatic error classification stored in the LVal.Str field for LError values.
func (*ErrorVal) Error ¶
Error implements the error interface. When the error condition is not “error” it wil be printed preceding the error message. Otherwise, the name of the function that generated the error will be printed preceding the error, if the function can be determined.
Defensive: a nil receiver returns the nilErrorMessage sentinel rather than dereferencing. This matters because diagnostic code that renders errors may be invoked from a deferred recover handler where the LVal pointer can be stale or zeroed.
func (*ErrorVal) ErrorMessage ¶
ErrorMessage returns the underlying message in the error.
Defensive: a downstream consumer reported a SIGSEGV inside the type switch over Cells[0].Native when the interface header was corrupted (stale itab → invalid pointer deref during the type assertion). The deferred recover ensures the diagnostic pipeline always produces a renderable string even when the underlying LVal is malformed; well-formed errors are unaffected. The recovered panic is logged so the operator sees that something is corrupting the error's Cells[0].Native — silently swallowing would hide a real bug.
func (*ErrorVal) FunName ¶
FunName returns the qualified name of function on the top of the call stack when the error occurred.
type FSLibrary ¶ added in v1.16.14
FSLibrary implements SourceLibrary using an fs.FS, providing natural confinement via the fs.FS contract (which rejects ".." path components and absolute paths). Use os.DirFS(dir) to create an fs.FS rooted at a directory.
func (*FSLibrary) LoadSource ¶ added in v1.16.14
LoadSource reads loc from the embedded fs.FS. The fs.FS contract inherently prevents path traversal — paths must be unrooted slash- separated sequences without ".." elements.
type ForkOption ¶ added in v1.52.0
type ForkOption func(*forkConfig)
ForkOption configures a single Fork call.
func ForkWithContext ¶ added in v1.52.0
func ForkWithContext(ctx context.Context) ForkOption
ForkWithContext rebinds the fork's evaluation context: the returned environment starts with ctx as its context.Context, in place of the template's (which does not travel — a fork never inherits the template's context). Use this at pool-checkout time to bind a request-scoped context to the fork serving that request. For per-call control the *Context methods on LEnv work on a fork exactly as on any environment.
func ForkWithNativeReplacer ¶ added in v1.52.0
func ForkWithNativeReplacer(fn func(payload interface{}) (interface{}, bool)) ForkOption
ForkWithNativeReplacer installs a per-fork substitution hook for native payloads. For every native payload the fork walker encounters, fn is consulted first: returning (replacement, true) stores replacement in the forked value; returning (_, false) falls through to the NativeCloner protocol and then to the default share-by-reference policy.
This is the escape hatch for payload types the embedder cannot modify to implement NativeCloner (third-party handles), and for rebinding fork-specific instances (a per-fork storage handle, a per-test accumulator). fn may be called more than once for payloads reachable through multiple values; it must be pure with respect to the template (never mutate the payload it is given).
func ForkWithStderr ¶ added in v1.52.0
func ForkWithStderr(w io.Writer) ForkOption
ForkWithStderr sets the forked Runtime's Stderr. Without this option the fork shares the template's Stderr writer. A fork-served test runner wants this: each test's environment must write diagnostics to that test's logger, not to the template's.
type FunctionInfo ¶ added in v1.17.0
type FunctionInfo struct {
Source *token.Location
Name string // empty for lambda
Kind string // "defun", "defmacro", or "lambda"
DocString string
Params []ParamInfo
}
FunctionInfo holds metadata extracted from a defun, defmacro, or lambda s-expression.
func InspectFunction ¶ added in v1.17.0
func InspectFunction(node *LVal) *FunctionInfo
InspectFunction extracts metadata from a defun, defmacro, or lambda s-expression. Returns nil if node is not a recognized form.
type LBuiltinDef ¶
LBuiltinDef is a built-in function
func DefaultBuiltins ¶
func DefaultBuiltins() []LBuiltinDef
DefaultBuiltins returns the default set of LBuiltinDefs added to LEnv objects when LEnv.AddBuiltins is called without arguments.
func DefaultMacros ¶
func DefaultMacros() []LBuiltinDef
DefaultMacros returns the default set of LBuiltinDef added to LEnv objects when LEnv.AddMacros is called without arguments.
func DefaultSpecialOps ¶
func DefaultSpecialOps() []LBuiltinDef
DefaultSpecialOps returns the default set of LBuiltinDef added to LEnv objects when LEnv.AddSpecialOps is called without arguments.
type LEnv ¶
LEnv is a lisp environment.
The binding state (scope, funName), the lexical chain (parent) and the evaluator's current location (loc) are unexported — the issue #382 close applied one layer up from LVal. `env.scope[sym] = v` used to let any holder of an *LEnv rebind a symbol in a live environment — including a closure's captured environment, shared by every function value that closed over it — without going through Put, invisible to the runtime seal and to elpsvet; `env.loc = loc` aliased a caller's mutable location into every error and stack frame the evaluator stamped afterwards (the #362 aliasing class). Reads are mediated by Bindings, NumBindings, Parent and Source; the writes are kernel-only.
Field order is layout-sensitive: the pointer-bearing fields lead so the GC scan extent stops at 56 bytes instead of 64. Keep scalars (ID) trailing.
func NewEnvRuntime ¶
NewEnvRuntime initializes a new LEnv, like NewEnv, but it explicitly specifies the runtime to use. NewEnvRuntime is only suitable for creating root LEnv object, so it does not take a parent argument. When rt is nil StandardRuntime() called to create a new Runtime for the returned LEnv. It is an error to use the same runtime object in multiple calls to NewEnvRuntime if the two envs are not in the same tree and doing so will have unspecified results.
func (*LEnv) AddBuiltins ¶
func (env *LEnv) AddBuiltins(external bool, funs ...LBuiltinDef)
AddBuiltins binds the given funs to their names in env. When called with no arguments AddBuiltins adds the DefaultBuiltins to env.
A function's sealed formal argument list is shared with env by reference; an unsealed one is copied so environments never share mutable formals. See registrationFormals and issues #363, #379, #514.
func (*LEnv) AddMacros ¶
func (env *LEnv) AddMacros(external bool, macs ...LBuiltinDef)
AddMacros binds the given macros to their names in env. When called with no arguments AddMacros adds the DefaultMacros to env.
A macro's sealed formal argument list is shared with env by reference; an unsealed one is copied so environments never share mutable formals. See registrationFormals and issues #363, #379, #514.
func (*LEnv) AddSpecialOps ¶
func (env *LEnv) AddSpecialOps(external bool, ops ...LBuiltinDef)
AddSpecialOps binds the given special operators to their names in env. When called with no arguments AddSpecialOps adds the DefaultSpecialOps to env.
An operator's sealed formal argument list is shared with env by reference; an unsealed one is copied so environments never share mutable formals. See registrationFormals and issues #363, #379, #514.
func (*LEnv) Bindings ¶ added in v1.52.0
Bindings iterates the symbol bindings in env's immediate scope; parent scopes are not included (walk Parent for those). Iteration order is unspecified, like Go map order.
Bindings is the read half of the scope map, which went unexported in issue #382: enumerating an environment is a legitimate need (the debugger's variable panes), while the write that came free with an exported map — rebinding a symbol in an environment the writer does not own — is not. Use Put or PutGlobal to bind.
Bindings is nil-receiver safe: a nil LEnv yields nothing.
func (*LEnv) Context ¶ added in v1.25.0
Context returns the context.Context currently associated with this environment. If no context has been set, context.Background() is returned.
func (*LEnv) Copy ¶
Copy returns a new LEnv with a copy of env.scope but a shared parent and stack (not quite a deep copy).
func (*LEnv) DefinePackage ¶
func (*LEnv) Error ¶
Error returns an LError value with an error message given by rendering msg.
Error may be called either with an error or with any number of *LVal values. It is invalid to pass an error argument with any other values and doing so will result in a runtime panic.
Unlike the exported function, the Error method returns LVal with a copy env.Runtime.Stack.
func (*LEnv) ErrorAssociate ¶
ErrorAssociate associates the LError value lerr with env's current call stack and source location. ErrorAssociate returns an LError if lerr is not an error value (indicating a bug in the caller), or nil on success.
func (*LEnv) ErrorCondition ¶
ErrorCondition returns an LError the given condition type and an error message computed by rendering msg.
ErrorCondition may be called either with an error or with any number of *LVal values. It is invalid to pass ErrorCondition an error argument with any other values and doing so will result in a runtime panic.
Unlike the exported function, the ErrorCondition method returns an LVal with a copy env.Runtime.Stack.
func (*LEnv) ErrorConditionf ¶
ErrorConditionf returns an LError value with the given condition type and a a formatted error message rendered using fmt.Sprintf.
Unlike the exported function, the ErrorConditionf method returns an LVal with a copy env.Runtime.Stack.
func (*LEnv) Errorf ¶
Errorf returns an LError value with a formatted error message.
Unlike the exported function, the Errorf method returns an LVal with a copy env.Runtime.Stack.
func (*LEnv) EvalContext ¶ added in v1.25.0
EvalContext evaluates v with the given context. If ctx is cancelled or its deadline expires during evaluation, a CondContextCancelled error is returned.
func (*LEnv) Fork ¶ added in v1.52.0
func (env *LEnv) Fork(opts ...ForkOption) (*LEnv, error)
Fork clones a quiescent, fully loaded template environment, returning an independent environment on a fresh Runtime. Sealed structure (program AST, formals, quoted literals — the large majority of a loaded environment) is shared with the template; mutable data is hermetically copied; closures are remapped so they capture fork state. The entire package registry reachable from env's Runtime is forked along with the environment tree, and the returned environment is the fork's counterpart of env itself (embedders fork the root environment they were handed at load time).
The template must be QUIESCENT: fully loaded, with no evaluation in flight (empty call stack, no active eval entry, no pending condition handlers). Fork returns an error otherwise. There is deliberately no way to bypass the check — forking a mid-evaluation environment would capture torn state.
Fork never mutates the template. The template remains fully usable, and concurrent Fork calls against the same quiescent template are safe; but Fork must not race with evaluation on the template (quiescence is asserted, not synchronized).
The forked Runtime starts with a fresh call stack and empty condition stack, inherits the template's limit configuration (MaxAlloc, stack bounds, step budget configuration, ...), and continues the template's environment-ID and gensym counters so identifiers minted after the fork (lambda FIDs, gensyms) can never collide with identifiers the fork inherited. Profiler and Debugger do not travel: a fork starts with neither, and the embedder attaches its own if wanted. The Reader, SourceLibrary and LoadCache are shared (a reader cache is deliberately process-wide; the source library is read-only at runtime; and a load cache's entries are immutable, sealed, and explicitly safe to serve to any number of Runtimes -- see lisp/loadcache.go), as is Stderr unless ForkWithStderr overrides it. Step accounting (Runtime.TotalSteps) starts at zero.
LoadCache travels for the same reason Reader does, and the reason is the topology: "preheat a template, fork per environment" is elps's own recommended shape, and it is the exact shape the load cache exists to serve. A fork that dropped the cache would reparse every file the template had already parsed -- silently, since nothing fails -- which is the cost this hook was added to remove. The per-Runtime re-entrancy guard (loadCacheActive) is deliberately NOT copied: it is state about a load in progress, and the template is quiescent.
func (*LEnv) FunCallContext ¶ added in v1.25.0
FunCallContext invokes regular function fun with args under the given context. If ctx is cancelled or its deadline expires during the call, a CondContextCancelled error is returned.
func (*LEnv) GetFun ¶
GetFun returns a function referenced by the given LVal. If fun is already an LFun, then fun is returned. If fun is a symbol then GetFun looks for a function bound to the symbol. If fun does not reference a symbol then an error is returned.
GetFun is a suitable for backing an implementation of functional programing constructs, like funcall, map, reduce, etc.
func (*LEnv) GetFunGlobal ¶ added in v1.2.0
GetFunGlobal is like GetFun but only accesses the global package environment.
func (*LEnv) GetFunName ¶
GetFunName returns the function name (if any) known to be bound to the given function. If the function's FID is bound in its package then the global name of the function is returned. When the function is bound within a local scope then the local name used to reference the function (if any) is returned.
Safety: the error path in this function is cosmetic-only and does not mask data corruption. Every caller (MacroCall, SpecialOpCall, funCall, call, profiler) has already verified that f.Type == LFun before reaching here, so pkgFunName should never fail. The fallback to f.Str only affects the human-readable name shown in error messages and stack traces — it cannot influence evaluation, binding, or control flow. We log at BUG level so the issue is visible in diagnostics without changing the return type to an error that every caller would have to handle for an unreachable code path.
func (*LEnv) GetGlobal ¶
GetGlobal takes LSymbol k and returns the value it is bound to in the current package.
func (*LEnv) Load
deprecated
Load reads LVals from r and evaluates them as if in a progn. The value returned by the last evaluated LVal will be retured. After evaluating expressions the current package is restored to the current package at the time Load was called, in case loaded source made calls to "in-package". If env.Runtime.Reader has not been set then an error will be returned by Load.
Deprecated: Use LoadContext for cancellation and timeout support.
func (*LEnv) LoadContext ¶ added in v1.25.0
LoadContext reads LVals from r and evaluates them with the given context.
func (*LEnv) LoadFile
deprecated
LoadFile attempts to use env.Runtime.Library to read a lisp source file and evaluate expressions it contains. Any error encountered will prevent execution of loaded source and be returned. After evaluating expressions the current package is restored to the current package at the time Load was called, in case loaded source made calls to "in-package". If env.Runtime.Reader has not been set then an error will be returned by Load.
Deprecated: Use LoadFileContext for cancellation and timeout support.
func (*LEnv) LoadFileContext ¶ added in v1.25.0
LoadFileContext loads and evaluates a source file with the given context.
func (*LEnv) LoadLocation
deprecated
LoadLocation attempts to use env.Runtime.Library to read a lisp source file, specifying its name and location explicity, and evaluate the expressions it contains. Because the name and location of the stream are specfied explicitly LoadLocation does not depend explicity on an env.Runtime.Library implementation. Any error encountered will prevent execution of loaded source and be returned. After evaluating expressions the current package is restored to the current package at the time Load was called, in case loaded source made calls to "in-package". If env.Runtime.Reader has not been set then an error will be returned by Load.
Deprecated: Use LoadLocationContext for cancellation and timeout support.
func (*LEnv) LoadLocationContext ¶ added in v1.25.0
LoadLocationContext loads and evaluates a source stream at a given location with the given context.
func (*LEnv) LoadProgram
deprecated
added in
v1.52.0
LoadProgram evaluates the program's expressions as if in a progn, exactly as Load evaluates the expressions returned by env.Runtime.Reader. The value of the last expression is returned. After evaluation the current package is restored, in case the program made calls to "in-package".
Deprecated: Use LoadProgramContext for cancellation and timeout support.
func (*LEnv) LoadProgramContext ¶ added in v1.52.0
LoadProgramContext evaluates the program's expressions with the given context. See LoadProgram.
func (*LEnv) LoadString
deprecated
func (*LEnv) LoadStringContext ¶ added in v1.25.0
LoadStringContext loads and evaluates a string with the given context.
func (*LEnv) New ¶ added in v1.14.0
New takes a typedef along with a list of constructor arguments and returns an LTaggedValue containing the result of invoking the typedef's constructor with the given arguments. A typedef is an LTaggedVal itself that wraps a list holding the defined type name along with a constructor.
New requires that the system have typedef tagged-values. That is enabled by calling InitializeUserEnv when initializing the top-level environment. InitializeTypedef, which InitializeUserEnv calls, is a step inside it and not a substitute for it: it panics on an environment InitializeUserEnv has not already established. See its doc comment, and issue #433.
func (*LEnv) NumBindings ¶ added in v1.52.0
NumBindings returns the number of symbols bound in env's immediate scope.
NumBindings is nil-receiver safe.
func (*LEnv) Parent ¶
Parent returns env's lexically enclosing environment, or nil when env is a root environment. The chain is read-only through this accessor: the field went unexported in issue #382 because re-parenting a live environment silently re-scopes every closure that captured it.
Parent is nil-receiver safe.
func (*LEnv) ParseProgram ¶ added in v1.52.0
ParseProgram parses the contents of r using env.Runtime.Reader and seals the result as a Program, without evaluating anything. Like LoadLocation, it uses ReadLocation when the runtime's reader supports locations and falls back to Read (with loc as the stream name) otherwise. An error is returned if env.Runtime.Reader has not been set.
func (*LEnv) Put ¶
Put takes an LSymbol k and binds it to v in env. If k is already bound to a value the binding is updated so that k is bound to v.
func (*LEnv) SetPackageDoc ¶ added in v1.16.12
SetPackageDoc sets the documentation string for the current package.
func (*LEnv) SetSymbolDoc ¶ added in v1.16.12
SetSymbolDoc sets the documentation string for a symbol in the current package.
func (*LEnv) Source ¶ added in v1.52.0
Source returns a copy of the location the evaluator is currently stamping onto values produced in env, or nil when it has none. The location is copied because the evaluator rebinds and mutates its own (issue #382, closing the #362 aliasing class at the environment level): a caller holding the pointer would watch it change under evaluation.
Source is nil-receiver safe.
func (*LEnv) SpecialOpCall ¶
SpecialOpCall invokes special operator fun with the argument list args.
func (*LEnv) TaggedValue ¶ added in v1.14.0
TaggedValue is a low-level function to create a tagged-value and should be used with great care and testing. The first argument must be a symbol and is used as the type of the returned tagged-value. The second argument is the value being tagged.
The type of a tagged-value should be a qualified symbol (e.g. 'lisp:mytype). Unqualified type names can clash with primitive type symbols (e.g. 'string) which can lead to program failures.
func (*LEnv) Update ¶
Update updates the binding of k to v within the scope of env. Update can update either lexical or global bindings. If k is not bound by env, an enclosing LEnv, or the current package an error condition is signaled.
func (*LEnv) UsePackage ¶
type LFunType ¶
type LFunType uint8
LFunType denotes special functions, either macros or special operators.
LFunType constants. LFunNone indicates a normal function.
type LType ¶
type LType uint
LType is the type of an LVal
const ( // LInvalid (0) is not a valid lisp type. LInvalid LType = iota // LInt values store an int in the LVal.Int field. LInt // LFloat values store a float64 in the LVal.Float field. LFloat // LError values use the LVal.Cells slice to store the following items: // [0] a symbol representing the error "condition" (class name) // [1:] error data (of any type) // // In addition, LError values store a copy of the function call stack at // the time of their creation in the LVal.Native field. // // TODO: Make the stack a first class type (or some composite type) so // that it could be inspected during a condition handler. LError // LSymbol values store a string representation of the symbol in the // LVal.Str field. LSymbol LQSymbol // TODO: Remove this... I can't believe it actually has usages // LSExpr values are "list" values in lisp and store their values in // LVal.Cells. LSExpr // LFun values use the following fields in an LVal: // LVal.Str The local name used to reference the function (if any) // LVal.Native A funData object // // In addition to these fields, a function defined in lisp (with defun, // lambda, defmacro, etc) uses the LVal.Cells field to store the following // items: // [0] a list describing the function's arguments // [1:] body expressions of the function (potentially no expressions) // // NOTE: Native go functions (LBuiltin) don't have a lexical environment // by default. If a native function needs a lexical environment in order // to evaluate further expressions it is expected to create one. See the // implementation of the builtin “let”. // // NOTE: Cells[1] in an LFun may contain a string literal which contains a // docstring. To match common-lisp semantics and maintain backwards // compatibility a function with a body consisting of only a string literal // returns the string constant and is considered to have no documentation. // A builtin function may also include a docstring in Cells[1]. LFun // LQuote values are special values only used to represents two or more // levels of quoting (e.g. ”3 or ”””'()). The quoted value is stored // in LVals.Cells[0]. The first level of quoting takes places by setting // the LVal.quoted field on a value with a normal value in LVal.Type. // LQuote values must always have a true LVal.quoted field. LQuote // LString values store a string in the LVal.Str field. LString // LBytes values store a *[]byte in the LVal.Native field. LVal.Native, // and the contained pointer, must must never be nil (the slice being // pointed to may be nil though). LBytes // LSortMap value uses the LVal.Map field to store a map. // // TODO: Use a tree-based map (that is potentially stored in Cells). A // tree based map would be capable of supporting integer keys. LSortMap // LArray values use the LVal.Cells slice to store the following items: // [0] a list containing dimension cardinalities in index 0 // [1] a list containing row-major ordered array values LArray // LNative values store a Go value in the LVal.Native field and can be used // by builtin functions to store values of any type. LNative // LTaggedVal is a user-defined type that uses the following fields in an // LVal: // LVal.Str The user-defined type name // LVal.Cells[0] The user-data for the typed-value LTaggedVal // Mark LVals are used to trasmit information down the stack through return // values. Because the LEnv does not evaluate expressions using a stack // based virtual machine these Mark values, which often wrap other LVal // data in their Cells, are passed back from functions. Typically the // environment is solely responsible for managing mark values and // applications should never see them during calls to builtin functions. LMarkTerminal // LEnv marks the frame as terminal and evaluates tho contained expr LMarkTailRec // LEnv resumes a call a set number of frames down the stack. LMarkMacExpand // LEnv will evaluate the returned LVal a subsequent time. // LTypeMax is not a real type but represents a value numerically greater // than all valid LType values. It also can be used to determine the // number of valid LType values. LTypeMax )
Possible LValType values
type LVal ¶
type LVal struct {
// Native is generic storage for data which cannot be represented as an
// LVal (and thus can't be stored in Cells).
Native interface{}
// Str used by LSymbol and LString values
Str string
// Cells used by many values as a storage space for lisp objects.
//
// TODO: Consider making Cells' type []LVal instead of []*LVal to reduce
// the burden on the allocator/gc.
Cells []*LVal
// Type is the native type for a value in lisp.
Type LType
// Fields used for numeric types.
Int int
Float float64
// FunType used to further classify LFun values.
FunType LFunType
// contains filtered or unexported fields
}
LVal is a lisp value
Field order is chosen so that every pointer-bearing word sits in the leading 64 bytes: the GC only scans up to the last pointer word, so grouping the pointers first and letting Str/Cells contribute their pointer word last leaves their len/cap tails (and all the scalars) outside the scan range. This cuts the GC scan extent from 112 bytes to 64 without changing the struct's overall size. LVal is allocated for every value in the interpreter, so keep the pointers first when adding fields — `govet`'s fieldalignment check (see .golangci.yml) enforces this.
func Array ¶
Array returns an LVal representing an array reference. The dims argument is be a list of integers sizes for each dimension of the array. If non-empty, cells provides the backing storage for the array. The dims argument may be nil, in which case a vector (one dimensional array) is returned. If dims is non-nil then cells must either be nil or have one element for every array element, in row-major order.
When no backing storage is supplied every element is initialized to Nil, the same value MakeVector gives an unset element. It used to be left as the zero value of the slice, a Go nil *LVal, which is not a value the rest of the interpreter can hold: reading one dereferences it. In-tree that was latent -- every caller here fills the cells it asked for before the array escapes -- but Array is exported and its documentation says cells may be nil, so an embedder following it built a value that panicked the host the first time lisp touched it:
env.PutGlobal(Symbol("a"), Array(QExpr([]*LVal{Int(3)}), nil))
(aref a 0) ; internal-panic: nil pointer dereference
A panic is the wrong answer twice over: the read is not an error at all (an unset element is nil, which is what the array now holds), and an internal-panic is deliberately not catchable by handler-bind, so the host had no way to contain it. See issue #367.
func Bool ¶
Bool returns an LVal with truthiness identical to b.
The returned value is a shared singleton — callers MUST NOT mutate it.
func Error ¶
Error returns an LError representing err. Errors store their message in Cells and their condition type in Str. The error condition type must be a valid lisp symbol.
Errors generated during expression evaluation typically have a non-nil Stack field. The Env.Error() method is typically the preferred method for creating error LVal objects because it initializes Stack with an appropriate value.
func ErrorCondition ¶
ErrorCondition returns an LError representing err and having the given condition type. Errors store their message/data in Cells and their condition type in Str. The condition type must be a valid lisp symbol.
Errors generated during expression evaluation typically have a non-nil Stack field. The Env.Error() method is typically the preferred method for creating error LVal objects because it initializes Stack with an appropriate value.
func ErrorConditionf ¶
ErrorConditionf returns an LError with a formatted error message. Errors store their message in Cells and their condition type in Str. The condition type must be a valid symbol.
Errors generated during expression evaluation typically have a non-nil Stack field. The Env.ErrorConditionf() method is typically the preferred method for creating error LVal objects because it initializes Stack with an appropriate value.
func Errorf ¶
Errorf returns an LError with a formatted error message. Errors store their message in Cells and their condition type in Str. The condition type must be a valid symbol.
Errors generated during expression evaluation typically have a non-nil Stack field. The Env.Errorf() method is typically the preferred method for creating error LVal objects because it initializes Stack with an appropriate value.
func Formals ¶
Formals returns an LVal reprsenting a function's formal argument list containing symbols with the given names.
func Fun
deprecated
Fun returns an LVal representing a function. Package is left empty; callers MUST set the funData Package before the value is invoked, or GetFunName will log "BUG: ..." at every call site that observes a package-less LFun.
Deprecated: use FunInPackage, which sets Package atomically. See issue #271.
func FunInPackage ¶ added in v1.48.0
FunInPackage returns an LFun bound to the named package. Prefer this over Fun for code embedding ELPS: Fun leaves Package empty, and a package-less LFun reaching funCall / MacroCall / SpecialOpCall produces "BUG: GetFunName" log spam (issue #271).
func InitializeTypedef ¶ added in v1.14.0
InitializeTypedef injects the meta-typedef object so `new` and `deftype` can be used to create user-defined types. The name of the injected typedef object is not exported and it should not be handled without abstraction in general because user error can break the type system.
InitializeTypedef is a step inside InitializeUserEnv, not an alternative to it. It requires an environment that InitializeUserEnv has already established: it reads env.Runtime.Package.Name, and it writes the typedef into the registry package named by env.Runtime.Registry.Lang. StandardRuntime leaves Runtime.Package nil and Registry.Lang "" (and Packages[""] is a nil *Package), so on a bare environment -- lisp.InitializeTypedef(lisp.NewEnv(nil)) -- this panics on the first of those two before doing anything.
The panics stay: under lisp/ a panic means a bug in the interpreter, or in code that had no business calling this (issue #361). Embedders establish a top-level environment with InitializeUserEnv, which calls this itself; there is no initialization order in which an embedder calls it directly. See issue #433.
See LEnv.TaggedValue for more information about creating tagged-values.
func InitializeUserEnv ¶
InitializeUserEnv creates the default user environment.
func Macro
deprecated
Macro returns an LVal representing a macro. Package is left empty; callers MUST set the funData Package before the value is invoked, or GetFunName will log "BUG: ..." at every call site that observes a package-less LFun.
Deprecated: use MacroInPackage, which sets Package atomically. See issue #271.
func MacroInPackage ¶ added in v1.48.0
MacroInPackage returns a macro LFun bound to the named package. Prefer this over Macro for the same reasons FunInPackage is preferred over Fun. See issue #271.
func MakeVector ¶ added in v1.14.0
MakeVector returns a vector with n cells initialized to Nil.
func Nil ¶
func Nil() *LVal
Nil returns an LVal representing nil, an empty list, an absent value.
The returned value is a shared singleton — callers MUST NOT mutate it. If you need a mutable empty list (e.g., to append children), use SExpr(nil) directly.
func QExpr ¶
QExpr returns an LVal representing an Q-expression, a quoted expression, a list. Provided cells are used as backing storage for the returned list and are not copied.
func SExpr ¶
SExpr returns an LVal representing an S-expression, a symbolic expression. Provided cells are used as backing storage for the returned expression and are not copied.
func SortedMapFromData ¶ added in v1.14.0
SortedMapFromData returns sorted-map with the given backing implementation. Applications calling this function must make ensure the Map implementation provided satisfies the semantics of Map methods.
func SpecialOp
deprecated
SpecialOp returns an LVal representing a special operator. Special operators are function which receive unevaluated results, like macros. However values returned by special operations do not require further evaluation, unlike macros.
Package is left empty; callers MUST set the funData Package before the value is invoked, or GetFunName will log "BUG: ..." at every call site that observes a package-less LFun.
Deprecated: use SpecialOpInPackage, which sets Package atomically. See issue #271.
func SpecialOpInPackage ¶ added in v1.48.0
SpecialOpInPackage returns a special-operator LFun bound to the named package. Prefer this over SpecialOp for the same reasons FunInPackage is preferred over Fun. See issue #271.
func Splice ¶
Splice is used in the implementation of quasiquote to insert a list into an outer slist.
func SplitSymbol ¶ added in v1.2.0
func Value ¶
func Value(v interface{}) *LVal
Value conveniently converts v to an LVal. Types which can be represented directly in lisp will be converted to the appropriate LVal. All other types will be turned into a Native LVal. Value is the inverse of the GoValue function.
func Vector ¶ added in v1.14.0
Vector returns an LVal representing a vector, a 1-dimensional array. Provided cells are used as backing storage for the returned vector and are not copied.
func (*LVal) ArrayDims ¶
ArrayDims returns the dimensions of an array. ArrayDims returns an error if v.Type is not LArray.
func (*LVal) ArrayIndex ¶
ArrayIndex returns the value at the given index in an array.
func (*LVal) Builtin ¶
Builtin returns the native implementation of a builtin function value, or nil for user-defined functions and function values carrying no function data. It panics on non-function values.
func (*LVal) Bytes ¶
Bytes returns the []byte stored in v. Bytes panics if v.Type is not LBytes.
NOT LISP-REACHABLE (#367), and the guard belongs to the CALLER: every in-tree caller tests v.Type == LBytes (or reaches this from inside a `case LBytes`) before calling. `(append 'bytes 0)` was the counterexample -- the type SPECIFIER was validated and the sequence was not -- and it is now a seed in the eval fuzz corpus, replayed on every run.
Deliberately not softened to "return nil for a non-LBytes": nil is a perfectly good empty byte string, so a caller that skipped its type check would get a silent wrong answer instead of a loud one. That is the trade KeyArg's doc comment rejects. A caller that cannot vouch for the type must test it and raise its own error; TestBuiltinRegistryNeverPanics (lisp/lisplib) is what finds the caller that forgot.
func (*LVal) CallStack ¶
CallStack returns the call stack attached to the error v. CallStack panics if v.Type is not LError.
NOT LISP-REACHABLE (#367): the panic guards a Go type assertion, not a program's data. Every in-tree caller -- builtinLoad*/builtinIsKey, env.Error*, macroDefun/macroDefmacro, opLambda, libjson's attachStack, and the two diagnostic renderers -- tests v.Type == LError first, so lisp source has no way to route a non-error here. Reaching it means embedder Go code called the accessor on a value it had not classified, and that is the #351/#355 shape rather than this one. A caller that cannot classify the value should test v.Type == LError itself: the type IS the check, so an accessor that answered nil would be reporting "no stack recorded" for a value that can never have one.
func (*LVal) Copy ¶
Copy creates a deep copy of the receiver.
Copy has within-runtime semantics — an LArray's backing storage is shared with the receiver, so it is not a tool for transferring values between Runtimes; the in-kernel detach (lisp/detach.go, unexported until a real consumer appears) covers that.
The copy owns its positions. Every node Copy reaches gets its own *token.Location, so a write through the copy cannot move what the original reports, and the reverse. That holds for the Locations reachable THROUGH meta's comment tokens as well as for source on the node. Values Go constructed carry no location at all (source is nil and the accessor synthesizes one by value), so there is nothing left to share -- the process-wide "<native code>" singleton this used to except is deleted, see nativeLocation.
The copy also owns its per-node metadata: meta and macroExpansion. The one thing deliberately still shared is macroExpansionInfo's embedded *macroExpansionContext, which describes the macro CALL rather than the node -- see macroExpansionInfo.Copy.
func (*LVal) Docstring ¶ added in v1.16.6
Docstring returns the docstring of the function reference v. If v is not a function Docstring returns the empty string. For user-defined functions, consecutive leading string expressions in the body are concatenated to form the docstring (the body must contain at least one non-string expression after the doc strings). Empty strings produce paragraph breaks.
func (*LVal) Equal ¶
Equal returns a non-nil value if v and other are logically equal, under the rules used by the "equal?" function.
Cyclic operands terminate. Comparison is co-inductive: when the walk reaches a pair of values it is already comparing further up the current path, it takes that pair to be equal and moves on. That is the greatest fixed point -- equality of the two values' infinite unfoldings, the same answer R7RS requires of equal? on circular structure -- and it is an answer rather than a guess: false is only ever returned for a difference actually found at a finite depth, so no equality is claimed that a longer walk could refute. See lisp/cycle.go and issue #390.
func (*LVal) FID ¶
FID returns the function value's unique identifier, or "" for a function value carrying no function data. It panics on non-function values.
func (*LVal) InheritSeal ¶ added in v1.52.0
InheritSeal marks v sealed when src is. It is for a v that was just minted over storage BORROWED from src — a fresh LVal header wrapping a (sub)slice of src's backing array — where the seal has to travel with the storage, not with the header that happens to point at it.
The kernel already does this by hand at every such site, and the comment it repeats is the whole rule: "a two-index slice keeps the original backing array (and its spare capacity), so a sealed input's constraint travels with the intermediate value" (builtinSlice; builtinCdr and builtinRest say the same). Those sites live in package lisp and assign the unexported field directly. This method is that same assignment, exported, for the value-restructuring libraries the seal design expects to migrate into this module — libelpspath being the one named in the commentary above, and the one that shipped without it (issue #392).
It is deliberately NOT a general "seal this value" setter:
- Nothing happens unless src is already sealed. A caller cannot invent a constraint, only propagate one that exists.
- Only the node types SealAST itself marks are eligible. In particular an array is never marked, because the kernel's mutating vector builtins (append!, assoc!) do not consult the flag at all — a "sealed" vector would be a lie that reads as protection. Code wrapping sealed backing in a vector must copy instead, exactly as builtinSlice does for (slice 'vector …).
- Singletons are skipped: they are born sealed, and writing even a flag to a value shared process-wide would race.
Sealing is monotone, so this is idempotent and safe to call on a value that is already sealed.
func (*LVal) IsMacro ¶
IsMacro returns true if v is a macro function. IsMacro doesn't actually check v.Type, only v.FunType.
func (*LVal) IsNil ¶
IsNil returns true if v represents a nil value.
Only the empty list is nil. Written as an expression rather than a switch: there is exactly one interesting type, so a switch would have to name the other seventeen LTypes to say nothing about them.
func (*LVal) IsNumeric ¶
IsNumeric returns true if v has a primitive numeric type (int, float64).
See IsNil for why this is an expression and not a switch.
func (*LVal) IsQuoted ¶ added in v1.52.0
IsQuoted reports whether v carries a single level of quoting — the flag behind the LQuote wrapper and the ['(...)/[...]] display forms. The underlying field is unexported (issue #382): quoting is established at construction time (Quote, Splice, QExpr, QSymbol, the parser) and removed only by the evaluator's own unquote step, so external packages get a read but never a write — an in-place external write to the flag on a shared value was exactly the #333/#334 singleton corruption.
IsQuoted is nil-receiver safe: a nil LVal reports false.
func (*LVal) IsSealed ¶ added in v1.52.0
IsSealed reports whether v is a sealed program-literal node (or a header sharing storage with one). Go code holding a value that reports true must not modify the value in place — modifying it could corrupt the program for every environment sharing the parse — and should either return an error ("cannot modify a program literal") or work on a Copy (whose fresh storage IsSealed reports false).
func (*LVal) IsSpecialFun ¶
IsSpecialFun returns true if v is a special function. IsSpecialFun doesn't actually check v.Type, only v.FunType.
func (*LVal) IsSpecialOp ¶
IsSpecialOp returns true if v is a special operator. IsMacro doesn't actually check v.Type, only v.FunType.
func (*LVal) KeyArg ¶ added in v1.50.0
KeyArg returns the i'th cell of a builtin's argument list, or Nil if the list is shorter than that.
The evaluator always passes one cell per declared formal, so for a builtin reached from lisp this is just Cells[i]. It differs for the builtins this package exports for embedding: an embedder binds the Go function to formals of its own, and one that declares fewer formals than the builtin reads -- easily done for an &key argument, which is invisible at the call site -- would otherwise index past the end of Cells and panic on every call. The Go signature is identical either way, so nothing catches it at compile time.
An absent cell is reported as Nil, which is what the evaluator itself passes for an unsupplied &optional or &key argument. Builtins must therefore read those cells through KeyArg rather than indexing Cells directly.
Use it ONLY for optional and &key cells, never for a required argument. Reporting an absent cell as Nil conflates "the caller supplied nothing" with "the caller supplied nil", and that is only safe where nil already means "not supplied". For a required argument whose valid domain includes Nil it is actively harmful: json:dump-string would answer "null" for a binding that supplied no argument at all, turning a panic into a silent wrong answer. Required arguments use ReqArg instead.
func (*LVal) MacroExpansion ¶ added in v1.23.0
func (v *LVal) MacroExpansion() (MacroExpansionMeta, bool)
MacroExpansion returns a snapshot of v's macro-expansion debug metadata and reports whether v carries any. Metadata exists only on nodes stamped during macro expansion while a debugger is attached (Runtime.Debugger != nil); in production runs every value reports false. The snapshot is a copy: mutating it does not touch v.
MacroExpansion is nil-receiver safe: a nil LVal reports false.
func (*LVal) Map ¶
Map returns the map data stored in v. Map panics if v.Type is not LSortMap.
NOT LISP-REACHABLE (#367), on the same terms as Bytes above: every in-tree caller -- the sorted-map builtins, Copy's LSortMap case, equal, libschema and libelpspath's path walkers -- tests LSortMap first. Returning nil for a non-map would only move the crash to the caller's next method call on the nil *MapData, so the type test stays the caller's job.
func (*LVal) MapEntries ¶ added in v1.14.0
MapEntries returns a list of key-value pairs in the map. MapEntries
func (*LVal) MapGet ¶
MapGet returns the value corresponding to k in v or an LError if k is not present in v. MapGet panics if v.Type is not LSortMap.
func (*LVal) MapKeys ¶
MapKeys returns a list of keys in the map. MapKeys panics if v.Type is not LSortMap. The type of each map key is retained from the first type a value was set for that key. For example, if the MapSet(Symbol("a"), Int(1)) is called before MapSet(String("a"), Int(2)) then MapKey() will contain the symbol and not the string.
func (*LVal) MapSet ¶
MapSet sets k to val in v. MapSet returns an error if v.Type is not LSortMap. String and symbol keys are coerced to avoid programming errors causing symbol and string keys with equal string values from existing in the same map.
func (*LVal) Package ¶
Package returns the name of the package a function value was defined in, or "" for a function value carrying no function data. It panics on non-function values.
func (*LVal) ReqArg ¶ added in v1.50.0
ReqArg returns the i'th cell of a builtin's argument list, or an error if the list is shorter than that.
This is the required-argument counterpart to KeyArg. Indexing Cells directly panics when an embedder binds the builtin to formals declaring fewer arguments than it reads, and a panic in an embedder's process is a far worse outcome than an error value -- the evaluator can only report it as an opaque internal-panic, and a host embedding elps has no way to recover context from it. Reporting the absent cell as Nil is not an option either, for the reason given on KeyArg. So: an error, naming the mismatch.
func (*LVal) SealAST ¶ added in v1.52.0
func (v *LVal) SealAST()
SealAST marks v and every node reachable through its Cells as sealed program-literal nodes. The parser calls it on each completed top-level expression; embedders that build expression trees by hand and share them across environments may call it for the same protection.
Only parser-producible node types are marked: s-expressions, quote forms, symbols, strings and numbers. Any other type (functions, arrays, maps, bytes, errors, natives) was necessarily constructed at runtime; SealAST stops without descending rather than freeze storage the evaluator legitimately mutates. Singletons (Nil/true/false) are born sealed (lisp/singleton.go, issue #376), so the already-sealed check stops the walk before the write; the explicit isSingleton guard stays as a belt-and-braces statement of intent — writing even a flag to a shared singleton would race.
Sealing is idempotent, and an already-sealed node terminates the walk — a sealed node's descendants are always sealed, so revisiting them is pointless and the check doubles as cycle protection.
func (*LVal) SetCallStack ¶
SetCallStack attaches a copy of stack to the error v. SetCallStack panics if v.Type is not LError.
NOT LISP-REACHABLE (#367): same argument as CallStack above -- every in-tree caller guards on v.Type == LError.
func (*LVal) SetSource ¶ added in v1.52.0
SetSource sets v's originating location in source code. A nil loc clears the location. The LVal stores the provided pointer, so a producer (e.g. a parser) may retain loc and continue to fix up its fields after the call — but once an LVal escapes to consumers the location must be treated as frozen, because the reference may be shared by many LVals (issue #362).
A sealed value (a parsed program node — see lisp/seal.go) keeps its parse-time location forever: SetSource on a sealed value is a no-op, because the node may be shared by every environment evaluating the same parse and restamping it would be a cross-environment write. The parser itself always stamps locations before sealing, so no in-repo caller is affected.
func (*LVal) Source ¶
Source returns a copy of v's originating location in source code. The boolean result reports whether v has a location at all — a false return means v carries no location (and the returned zero Location is meaningless), which is distinct from a real location whose fields happen to be zero.
The returned Location is a value copy: mutating it never affects v or any other LVal. The stored reference may be shared by many LVals, which is why no pointer accessor exists (issue #362).
Source is nil-receiver safe: a nil LVal reports no recorded location.
When v has no recorded location the boolean is false and the returned value is the synthetic "<native code>" location (File "<native code>", Pos -1) — the same location that values constructed by Go code have always reported — so the result is printable either way.
func (*LVal) String ¶
String renders v as lisp source.
A value that contains itself renders the marker "#<cycle>" at the point the walk reaches it a second time, so the result is finite and the walk cannot overflow the goroutine stack and kill the process. Rendering is otherwise unchanged: an acyclic value renders in full, at any nesting depth, exactly as it always did. See lisp/cycle.go and issue #390.
type LoadCache ¶ added in v1.54.0
type LoadCache interface {
// Load returns the cached parse stored under key.
Load(key string) (*CachedSource, bool)
// Store records src under key.
Store(key string, src *CachedSource)
}
LoadCache is an embedder-supplied cache of parsed source files. Install one on Runtime.LoadCache and every Load* entry point — which is to say `load-file`, and the Go-side Load/LoadString/LoadLocation family — will consult it before parsing.
The interface is deliberately policy-only. Implementations decide what to keep, how much, and for how long; they never see, construct or copy a *LVal. A minimal implementation is a map guarded by a mutex; a real one is usually size- or age-bounded.
Contract:
- Load returns the entry previously Stored under key, or ok == false. Returning an entry Stored under a DIFFERENT key is treated as a miss rather than trusted, so an implementation that mixes up its own keys degrades to "no cache" instead of running the wrong program.
- Store may drop the entry, keep it, or evict something else; elps makes no assumption that a Stored entry is later Loadable.
- Both methods must be safe for concurrent use when the cache is shared by Runtimes on more than one goroutine. The entries themselves are immutable, so no locking is needed around the values — only around the implementation's own bookkeeping.
- Load and Store must NOT re-enter the load path (they must not call Load*/LoadFile, directly or through a warmer). A re-entrant load is defended against — the guard treats it as a miss and parses without the cache (see (*LEnv).readCached) — but relying on that means giving up caching for the re-entrant load; do the warming outside the hook.
Reader custody on the fast path ¶
When a Reader hands back output already sealed throughout (the standard parser's path), admission stores the Reader's OWN nodes — it does not copy them. This is deliberate: it is what makes a cache miss zero-copy, and the standard and format-preserving parsers do not retain what they return. The contract that keeps it safe is therefore on the Reader: a Reader whose output feeds a LoadCache MUST NOT retain and later mutate the nodes it returned. This is the same residual the seal design already carries for all embedder Go code (seal.go: the flag cannot stop a direct v.Cells[0] = x), and its enforcement is the same — checked builds (-tags elpscheck) record each cached tree's fingerprint and re-verify it after every load, so a Reader that rewrites a stored tree is caught at the load that observed the change; production builds do not check and would be silently corrupted. (Copying on the fast path would close it unconditionally but would add a deep copy to every miss even though no compliant Reader needs it — the cost this hook exists to remove — so the contract is stated instead.)
Behaviour changes a cache can introduce ¶
- Installing a cache can change lisp SEMANTICS for a non-sealing Reader. Admission's copy path runs SealAST, so a guarded mutation site ((stable-sort < …), (append 'vector …), (slice 'vector …)) that succeeded cache-less against a Reader that did not seal begins raising modify-literal-error once a cache is installed. This is a migration hazard, not a doc nit — code that mutated program literals in place stops doing so. (The standard parser already seals, so its callers see no change; format-preserving and hand-written Readers are the ones affected.) A wrapping Reader that synthesizes even ONE node forces the whole file down the copy-and-seal path, so the zero-copy hit is conditional on the Reader sealing its entire output.
- Installing a cache drains the stream with io.ReadAll before parsing, so a streaming Reader that delivers a full program then a non-EOF error succeeds cache-less but fails with a cache (see (*LEnv).readCached).
A nil Runtime.LoadCache disables the hook entirely: the load path is then byte-identical to what it was before this hook existed, with no hashing, no buffering and no extra allocation (TestLoadCacheNilPathUnchanged).
Note the breadth: the hook sits at the read funnel, so it sees LoadString and Load as well as LoadFile — and, because `load-string`/`load-bytes` are builtins, GUEST lisp source can mint entries too (a phylum that load-strings N distinct programs adds N entries). A host that evaluates many distinct one-off strings through one unbounded cache will accumulate an entry per distinct string — retention is the implementation's job, and an implementation with no bound has no bound. (The embedder-side caches this hook replaces intercept Reader.Read and have exactly the same reach.)
type Loader ¶
func LoaderMust ¶
LoaderMust returns its first argument when err is nil. If err is nil LoaderMust panics.
NOT LISP-REACHABLE (#367): this is the Go `Must` idiom, for an embedder building a Loader from a source it controls (a //go:embed constant, a literal) at start-up. Nothing in the interpreter calls it, so no evaluated program can reach it; an embedder wrapping a source a program supplies should handle the error instead.
func TextLoader ¶
TextLoader parses a text stream using r and returns a Loader which evaluates the stream's expressions when called. The reader will be invoked only once.
TextLoader returns an error if r produces any reference type (bytes, map, array, native, etc), a nil node, or output that is not finite: a cycle, or nesting past loaderWalkMaxDepth. It does NOT reject node SHARING, imposes no node budget, and — unlike the Program constructors and the cache — tolerates a Native payload on a sealable type (see newTextLoaderWalk). A Reader that interns symbols or subexpressions, one that returns a single very large expression, and one that annotates nodes through Native all load exactly as they always have. Those rules exist for trees that are ALIASED between environments; every TextLoader load gets expr.Copy(), so none of them is a TextLoader concern.
type LocationReader ¶
type LocationReader interface {
// ReadLocation the contents of r, associated with physical location loc,
// and return the sequence of LVals that it contains. The returned LVals
// should be executed as if inside a progn.
ReadLocation(name string, loc string, r io.Reader) ([]*LVal, error)
}
LocationReader is like Reader but assigns physical locations to the tokens from r.
type LogicalStackOverflowError ¶
type LogicalStackOverflowError struct {
Height int
}
func (*LogicalStackOverflowError) Error ¶
func (e *LogicalStackOverflowError) Error() string
type MacroExpansionMeta ¶ added in v1.52.0
type MacroExpansionMeta struct {
// CallSite is a copy of the location where the macro was invoked.
CallSite *token.Location
// DefSite is a copy of the macro definition location (nil for builtins).
DefSite *token.Location
// Name is the qualified macro name (e.g. "lisp:defun").
Name string
// Args holds the unevaluated call-site arguments. The slice is a copy
// but the nodes are the shared originals — read-only by contract (they
// are typically sealed parse-tree nodes).
Args []*LVal
// ID is unique per stamped node, monotonically increasing within a
// runtime.
ID int64
}
MacroExpansionMeta is a read-only snapshot of the debug metadata attached to values produced by macro expansion while a debugger is attached. It is returned by (*LVal).MacroExpansion; the metadata itself lives in unexported storage (issue #382) because the historical corruption in #370 was a write of expansion metadata onto shared parser nodes — reads get a copy, and the in-kernel stamp is the only writer.
type Map ¶ added in v1.14.0
type Map interface {
Len() int
// Get returns the value associated with the given key and a bool signaling
// if the key was found in the map. The first value returned by Get may be
// an LError type if the implementation does not support the type of key
// given.
Get(key *LVal) (*LVal, bool)
// Set associates key with val in the map. Set may return an LError value
// if the
Set(key *LVal, val *LVal) *LVal
// Del removes any association it has with key. Del may return an LError
// value if key was not a supported type or if the map does not support
// dissociation.
Del(key *LVal) *LVal
// Keys returns a (sorted) list of keys with associated values in the map.
Keys() *LVal
// Entries copies its entries into the first Len() elements of buf.
// Entries are represented as lists with two elements. Entries returns the
// number of elements written (i.e. Len) or an error if any was encountered.
Entries(buf []*LVal) *LVal
}
type MapData ¶ added in v1.14.0
type MapData struct {
// contains filtered or unexported fields
}
MapData is a concrete type to store in an interface as to avoid expensive runtime interface type checking. Construct it with NewMapData; the backing Map cannot be replaced after construction (issue #382).
func NewMapData ¶ added in v1.52.0
NewMapData returns a MapData backed by m. Together with SortedMapFromData it is the extension point for embedders that back a sorted-map with a custom Map implementation.
type NativeCloner ¶ added in v1.52.0
type NativeCloner interface {
CloneNative() interface{}
}
NativeCloner is the opt-in fork-time duplication protocol for native payloads, shared with the broader native-contract design of issue #383. The kernel cannot copy an LVal's Native payload — it is an opaque interface{} — so Fork shares payloads by reference by default. A payload type whose identity or state must NOT be shared between a template and its forks (an accumulator, a stateful handle) implements NativeCloner; Fork then stores the value returned by CloneNative in the forked LVal instead of the shared reference.
CloneNative must return a payload that is independent of the receiver: mutations on either side must be invisible to the other. It must not retain references into the template's Runtime or LEnv tree — the clone crosses a runtime boundary.
type Package ¶
Package is a named set of bound symbols. A package is interpreted code and belongs to the LEnv that creates it.
func NewPackage ¶
NewPackage initializes and returns a package with the given name.
func (*Package) Export ¶ added in v1.52.0
Export appends names to the package's export list verbatim, preserving existing order and without deduplicating (matching historical append semantics on the package's export list). Use Exports for the deduplicating, sorting variant.
func (*Package) Exports ¶
Exports declares symbols exported by the package. The symbols are not required to be bound at the time Exports is called.
func (*Package) Externals ¶
Externals returns the package's exported symbol names in declaration order. Externals allocates and returns a copy on every call so callers cannot modify the package's export list.
func (*Package) Get ¶
Get takes an LSymbol k and returns the LVal it is bound to in pkg.
Get is a pure read. It used to record FunNames[fid] = k.Str on every successful function lookup, so that the name the caller used won over the name the binding was created with. That made a read method write a map that is shared by pointer across goroutines — embedders hand the same *Package to concurrent requests — with no synchronisation. Under -race it is a data race; without -race the Go runtime kills the process outright with "fatal error: concurrent map read and map write", which is a runtime throw that neither recover() nor handler-bind can intercept. See issue #397.
FunNames is maintained on the write path instead: put records the name for every LFun that enters Symbols, so the map is already populated by the time anything reads it. The one behaviour that goes away is the "last lookup wins" preference when a single function value is bound under several names in the same package: GetFunName now reports the name most recently *bound* rather than the name most recently *looked up*. That is cosmetic — it affects the function name rendered in stack traces and error messages only.
func (*Package) GetFunName ¶
GetFunName returns the function name (if any) known to be bound to the given FID.
func (*Package) NumExternals ¶ added in v1.52.0
NumExternals returns the number of exported symbol names without copying the export list.
func (*Package) Put ¶
Put takes an LSymbol k and binds it to v in pkg.
Put stores v as given: it takes no admission walk (see AddPackage and lisp/package_admit.go). It is the write path every `set` reaches through LEnv.PutGlobal, where the value being bound belongs to the environment that is already evaluating it and LEnv.Put/PutGlobal have already taken the checked-mode ownership sighting — so a per-binding walk here would tax the interpreter's hot path to guard a transfer that is not happening. A Go caller that reaches around the environment to Put into a package another Runtime is serving is doing the cross-Runtime sharing AddPackage's snapshot exists to prevent, and owns the consequences.
func (*Package) Symbol ¶ added in v1.52.0
Symbol returns the value bound to name in pkg and reports whether name is bound. Unlike Get, Symbol performs a raw table lookup: it does not resolve the true/false constants, does not record function names, and returns (nil, false) instead of an error LVal when name is unbound.
func (*Package) SymbolDoc ¶ added in v1.52.0
SymbolDoc returns the documentation string bound to name in pkg, or the empty string when name has no documentation.
func (*Package) SymbolNames ¶ added in v1.52.0
SymbolNames returns the names of all symbols bound in pkg in sorted order. SymbolNames allocates a new slice on every call.
type PackageRegistry ¶
type PackageRegistry struct {
Lang string // A default package used by all other packages
// contains filtered or unexported fields
}
PackageRegistry contains a set of packages.
func NewRegistry ¶
func NewRegistry() *PackageRegistry
NewRegistry initializes and returns a new PackageRegistry.
func (*PackageRegistry) AddPackage ¶ added in v1.52.0
func (r *PackageRegistry) AddPackage(p *Package) bool
AddPackage registers a private snapshot of p under p.Name if no package with that name exists already. AddPackage returns true when p was added and false when a package named p.Name was already registered (in which case the registry is unchanged).
AddPackage is an ADMISSION point, not a store (issue #524). A registry is the interpreter state of a Runtime, so a package built outside it — by an embedder, or by another Runtime whose registry is being merged into this one — arrives full of values the caller still holds pointers to. What gets registered is therefore a snapshot of p rather than p itself, and each bound value is admitted according to what the seal can promise about it (lisp/package_admit.go states the rule per value class):
- a value that is sealed throughout, and a singleton, are shared by reference — the sanctioned cross-environment share;
- a code-like tree that is NOT sealed (a runtime-built list, symbol, string or number: fresh mutable storage the caller still aliases) is copied privately and sealed, so neither side can write what the other evaluates;
- everything else — functions, natives, sorted-maps, arrays, byte strings, and trees holding one — is shared by reference, because no seal covers those classes. For them AddPackage transfers custody: the caller must stop mutating them, and evaluating a shared closure under two Runtimes remains the caller's problem (checked builds report it).
Two consequences worth stating for callers. The registry does not hold p, so binding into p afterwards does not change the registered package — bind through the environment (or add a finished package). And the snapshot reads p's maps on the calling goroutine, so, like every other read of a *Package, it requires that no other goroutine is writing p (issue #397).
func (*PackageRegistry) DefinePackage ¶
func (r *PackageRegistry) DefinePackage(name string) *Package
func (*PackageRegistry) Package ¶ added in v1.52.0
func (r *PackageRegistry) Package(name string) *Package
Package returns the package registered under name, or nil if no such package exists.
func (*PackageRegistry) PackageNames ¶ added in v1.52.0
func (r *PackageRegistry) PackageNames() []string
PackageNames returns the names of all registered packages in sorted order. PackageNames allocates a new slice on every call.
type ParamInfo ¶ added in v1.17.0
ParamInfo describes a single parameter in a function signature.
func ParseFormals ¶ added in v1.17.0
ParseFormals extracts parameter info from a formals LVal. The formals list uses &optional, &rest, and &key markers to separate parameter kinds.
type PhysicalStackOverflowError ¶
type PhysicalStackOverflowError struct {
Height int
}
func (*PhysicalStackOverflowError) Error ¶
func (e *PhysicalStackOverflowError) Error() string
type Profiler ¶ added in v1.9.0
type Profiler interface {
// Start the process, and returns a function to stop.
Start(function *LVal) func()
}
Interface for a profiler
type Program ¶ added in v1.52.0
type Program struct {
// contains filtered or unexported fields
}
Program is an opaque sequence of parsed top-level expressions — the sealed form of a parser's []*LVal output. Its purpose is boundary control: code outside this module can hold, cache, and evaluate a Program but can never reach the raw AST nodes inside it, so an embedder's parse cache cannot leak *LVal pointers between environments by construction. The class of bug is eliminated at compile time (there is no accessor to misuse) and at zero runtime cost (Program is a slice header behind a struct; sealing copies nothing).
Producers live where the parse happens so raw-slice custody never leaves this package: ReadProgram, ReadLocationProgram, and (*LEnv).ParseProgram. The consumer is (*LEnv).LoadProgram / LoadProgramContext, which evaluates the sealed expressions exactly as (*LEnv).Load evaluates a Reader's output. There is no exported exit: the in-kernel detach machinery (lisp/detach.go) can produce hermetic deep copies of the sealed expressions, but it stays unexported until a real embedder consumer materializes. A reflection test (program_seal_test.go) guards the surface: no exported method may expose *LVal.
Scope of the guarantee: Program seals the parse/cache boundary in both directions. Outward, it stops raw AST nodes from ESCAPING to embedders (the compile-time seal above). Inward, every constructor establishes the hermetic seal (lisp/seal.go) on the expressions it admits — reader output that is not already sealed throughout is privately copied and sealed, and output the seal cannot cover is rejected (see newProgram, issue #394) — so the sharing a parse cache does is always the sanctioned kind: sealed nodes are frozen storage under the seal's write protection, and evaluating one Program from many environments cannot corrupt it for the others. Concurrency is unchanged by any of this: a Runtime serves one goroutine, so concurrent evaluation still means one environment per goroutine, all of them free to share the sealed Program.
The zero Program is valid, empty, and evaluates to nil.
func ReadLocationProgram ¶ added in v1.52.0
ReadLocationProgram is ReadProgram for a LocationReader, assigning physical location loc to the parsed tokens.
func ReadProgram ¶ added in v1.52.0
ReadProgram parses the contents of r using reader and seals the result as a Program. The parsed expression slice never leaves this package: it goes from the reader's return value through newProgram's seal admission into the sealed Program. Reader output that is not already hermetically sealed (a format-preserving parser, a caller-written Reader) is privately copied and sealed; output the seal cannot protect — reference types, function values — is rejected with an error. See newProgram.
type Reader ¶
type Reader interface {
// Read the contents of r and return the sequence of LVals that it
// contains. The returned LVals should be executed as if inside a progn.
Read(name string, r io.Reader) ([]*LVal, error)
}
Reader abstracts a parser implementation so that it may be implemented in a separate package as an optional/swappable component.
type ReaderIdentity ¶ added in v1.54.0
type ReaderIdentity interface {
// ReaderIdentity returns a stable token that differs between readers whose
// parse output differs. Two readers returning the same token are treated
// as interchangeable producers by the cache.
//
// The EMPTY string is not a valid token: it states nothing, and two
// readers returning it would be declared interchangeable. A reader that
// returns it disables the cache for its own loads (they parse every time)
// rather than risking a wrong-program serve.
ReaderIdentity() string
}
ReaderIdentity is an optional interface a Runtime.Reader may implement to tell the load cache which parses it produces.
The cache key binds every entry to the reader that produced it, not just to the source bytes (see loadCacheKey): two readers that parse the same bytes into DIFFERENT trees must never serve each other's entries. By default that binding is the reader's fully-qualified Go type, which is right for elps's own readers — the standard parser and the format-preserving parser are distinct types — and stable across instances, so many Runtimes each holding their own reader of the same type still share cache entries (the motivating warm-cache topology).
A reader whose parse output depends on configuration NOT reflected in its Go type — the same struct in two modes — is indistinguishable by type alone. Such a reader implements ReaderIdentity to return a token that differs whenever its parse would; the cache folds that token into the key instead of the type. Implementing it is optional: the default is safe without any embedder cooperation, and this is only for readers that multiplex parse behaviours behind one Go type.
type RelativeFileSystemLibrary ¶
type RelativeFileSystemLibrary struct {
// RootDir, when non-empty, confines all file access to this directory
// tree. Any attempt to load a file outside RootDir (including via ..
// path components) will return an error. When empty, no confinement
// is applied and the existing behavior is preserved.
RootDir string
}
RelativeFileSystemLibrary implements SourceLibrary and reads lisp source files from the filesystem, relative to the source context location.
In order to read filepaths relative to a source file's location the application's implementation of Runtime.Reader must implement LocationReader.
func (*RelativeFileSystemLibrary) LoadSource ¶
func (lib *RelativeFileSystemLibrary) LoadSource(ctx SourceContext, loc string) (string, string, []byte, error)
LoadSource attempts to open loc as a filepath.
type Runtime ¶
type Runtime struct {
Registry *PackageRegistry
Package *Package
Stderr io.Writer
Stack *CallStack
Reader Reader
Library SourceLibrary
Profiler Profiler
Debugger Debugger // nil = disabled (zero overhead on hot path)
LoadCache LoadCache // nil = disabled (the load path is then byte-identical to having no hook); see lisp/loadcache.go
MaxAlloc int // Per-operation allocation size cap (0 = use default). Not cumulative.
MaxMacroExpansionDepth int // Maximum macro expansion iterations (0 = use default).
MaxEvalNesting int // Evaluator recursion depth cap (0 = use default, negative = disabled).
MaxSleep time.Duration // Hard ceiling on a single time:sleep (0 or negative = none). See MaxSleepCeiling.
// contains filtered or unexported fields
}
Runtime is an object underlying a family of tree of LEnv values. It is responsible for holding shared environment state, generating identifiers, and writing debugging output to a stream (typically os.Stderr).
Step Limits: Runtime supports optional instruction counting via MaxSteps. Context cancellation is handled per-evaluation via LEnv.evalCtx, which is set by the *Context methods on LEnv (e.g. EvalContext) or the WithContext Config option. When neither context nor step limits are configured, limit checks are two nil/zero comparisons with negligible overhead.
Concurrency: Runtime and its associated LEnv tree are NOT safe for concurrent use from multiple goroutines. All calls to Eval, Load, and any other methods that read or mutate Runtime or LEnv state must be serialized by the caller. To evaluate ELPS code concurrently, create a separate Runtime (and LEnv tree) per goroutine.
The only thread-safe operations are GenEnvID and GenSym, which use atomic counters internally. All other fields — including Registry, Package, Stack, conditionStack, and the LEnv Scope maps — are unprotected.
Field order is layout-sensitive: pointer-bearing fields lead so the GC scan extent stops at 112 bytes instead of 144. Add scalars below conditionStack.
func StandardRuntime ¶
func StandardRuntime() *Runtime
StandardRuntime returns a new Runtime with an empty package registry and Stderr set to os.Stderr.
func (*Runtime) CheckAlloc ¶ added in v1.20.0
CheckAlloc returns a non-empty error message if n exceeds the per-operation allocation size cap. This is a point-in-time check for a single operation, not a cumulative memory tracker. Callers should use this before allocating buffers or sequences whose size is determined by user input.
func (*Runtime) CurrentCondition ¶ added in v1.16.13
CurrentCondition returns the condition currently being handled, or nil.
func (*Runtime) EvalNesting ¶ added in v1.49.0
EvalNesting reports how deeply LEnv.eval is currently recursed into itself. It is zero outside of an evaluation. See DefaultMaxEvalNesting for what the quantity measures and why it is bounded separately from stack height.
func (*Runtime) MaxAllocBytes ¶ added in v1.16.14
MaxAllocBytes returns the effective per-operation allocation size cap. Each builtin that allocates a buffer or sequence checks its output size against this limit independently — it is NOT a cumulative memory tracker. If MaxAlloc is zero, DefaultMaxAlloc is returned.
func (*Runtime) MaxEvalNestingDepth ¶ added in v1.49.0
MaxEvalNestingDepth returns the effective evaluator recursion-depth cap. Zero means "use DefaultMaxEvalNesting"; a negative value disables the check and is reported as zero.
func (*Runtime) MaxMacroExpansions ¶ added in v1.20.0
MaxMacroExpansions returns the effective maximum macro expansion depth. If MaxMacroExpansionDepth is zero, DefaultMaxMacroExpansionDepth is returned.
func (*Runtime) MaxSleepCeiling ¶ added in v1.49.0
MaxSleepCeiling returns the hard upper bound on a single sleep, or 0 when the embedder has set none. A negative Runtime.MaxSleep disables the bound explicitly and is also reported as 0.
This is the ceiling on what (time:sleep d :max m) may request, NOT the cap applied when :max is absent — that is DefaultMaxSleep. See its doc comment for why the two are separate.
func (*Runtime) PopCondition ¶ added in v1.16.13
PopCondition removes and returns the top condition from the stack.
func (*Runtime) PushCondition ¶ added in v1.16.13
PushCondition pushes an error onto the condition stack, making it available to rethrow within a handler-bind handler.
func (*Runtime) ResetSteps ¶ added in v1.25.0
func (r *Runtime) ResetSteps()
ResetSteps resets the current evaluation's step counter to zero. It does not affect TotalSteps.
func (*Runtime) Steps ¶ added in v1.25.0
Steps returns the number of steps consumed by the current top-level evaluation — or, if no evaluation is in progress, by the most recent one. Four things increment the counter by one: each call to Eval, each tail-recursion iteration, each macro re-expansion, and each turn of a dotimes loop. The last of those exists because an empty-bodied dotimes evaluates nothing and would otherwise consume no budget at all -- see opDoTimes, which also records the measured per-turn cost.
The counter is reset when a new top-level evaluation begins (see WithMaxSteps), so it is not a lifetime total. Use TotalSteps for that.
func (*Runtime) TotalSteps ¶ added in v1.49.0
TotalSteps returns the number of steps consumed over the lifetime of the Runtime, across every top-level evaluation. Unlike Steps it is not reset when a new evaluation begins.
type SingletonSnapshot ¶ added in v1.48.0
type SingletonSnapshot struct {
// contains filtered or unexported fields
}
SingletonSnapshot captures the current bit pattern of the three singleton LVals and of the shared native source Location. It is used by test infrastructure to detect inadvertent mutations to shared singletons — see TestMain in lisp_ and the per-test guard in elpstest.Runner.
func TakeSingletonSnapshot ¶ added in v1.48.0
func TakeSingletonSnapshot() SingletonSnapshot
TakeSingletonSnapshot captures the current state of the three singleton LVals so callers can later verify they were not mutated.
func (SingletonSnapshot) Verify ¶ added in v1.48.0
func (s SingletonSnapshot) Verify() string
Verify compares the current singleton state to the snapshot. If any singleton has drifted it returns the name of the offender; otherwise it returns the empty string.
Verify detects drift in VALUES. A write that stores the value a field already holds leaves no drift and Verify reports nothing, even though such a write still races with every concurrent reader — that was issue #333. Do not read an empty return as "no code mutated a singleton". See the "What this does NOT catch" section on checkSingleton in singleton_check_elpscheck.go for what covers that case.
type SourceContext ¶
type SourceContext interface {
// Name is the name of the current source stream being evaluated which
// caused the SourceLibrary LoadLocation operation. Name is like Location
// but is not necessarily tied to a physical location, or otherwise may be
// ambiguous.
//
// NOTE: Name may not be generated by the Runtime SourceLibrary due to
// functions being sourced externally (in particular, the core language and
// application standard library). As such, the Name of a SourceContext is
// meant only for informal use to assist humans and should not be relied
// upon by a SourceLibrary.
Name() string
// Location is the current source location (e.g. file path) being evaluated
// which caused the SourceLibrary LoadSource operation. This may be used
// in determining the location of relative target source locations. If
// executing code is not sourced from a lisp file then Location will return
// an empty string -- this includes LoadSource operations triggered from
// native Go functions and raw strings/[]bytes containing lisp code.
// SourceLibraries should interpret an empty Location string as the process
// working directory.
Location() string
}
SourceContext provides an execution context allowing SourceLibraries flexibility in determining how to interpret a path.
NOTE: SourceContext may be expanded with new methods yielding externally defined implementations incompatible.
func NewSourceContext ¶ added in v1.22.0
func NewSourceContext(name, loc string) SourceContext
NewSourceContext creates a SourceContext with the given name and location. This is useful for callers that need to construct a SourceContext for SourceLibrary.LoadSource calls outside of normal evaluation flow (e.g., DAP source request handlers).
type SourceLibrary ¶
type SourceLibrary interface {
// LoadSource returns the data contained in the source file specified by a
// location string obtained through a user call. For example, the call
// `(load-file "foo.txt")` would pass loc "foo.txt" to LoadSource).
// LoadSource also receives a SourceContext object which may be used to
// determine the physical path to the target location (e.g. what file is
// loading "foo.txt"?).
//
// LoadSource returns four values: a name and true-location unambiguously
// identifying the file, the file data, and any error that occurred while
// retrieving data. An interpreter must use trueloc as an identifier for
// the requested source file anywhere the SourceContext ctx is unavailable.
LoadSource(ctx SourceContext, loc string) (name, trueloc string, data []byte, err error)
}
SourceLibrary is responsible for loading source code from a given path. It is up to the SourceLibrary implementation how a source location should be interpreted. Depending on the application a SourceLibrary implementation may require a Runtime with Reader that implements LocationReader -- though the LocationReader implementation should not need to depend on the LocationReader implementation beyond that.
type TailIterationLimitError ¶ added in v1.49.0
type TailIterationLimitError struct {
Iterations int
}
TailIterationLimitError reports that a single tail-recursive loop performed more iterations than the runtime allows. It is a runaway-loop backstop, not a stack-depth problem: tail calls run in constant stack space.
func (*TailIterationLimitError) Error ¶ added in v1.49.0
func (e *TailIterationLimitError) Error() string
Source Files
¶
- builtins.go
- conditions.go
- config.go
- copy.go
- cycle.go
- debugger.go
- defformals.go
- detach.go
- embed.go
- env.go
- error.go
- fmtmeta.go
- fork.go
- funraw.go
- inspect.go
- lang.go
- library.go
- lisp.go
- loadcache.go
- loader.go
- macro.go
- maps.go
- op.go
- ownership_check_default.go
- package.go
- package_admit.go
- profiler.go
- program.go
- runtime.go
- seal.go
- seal_check_default.go
- sealfp.go
- singleton.go
- singleton_check_default.go
- stack.go
Directories
¶
| Path | Synopsis |
|---|---|
|
Package lisplib is used to conveniently load the standard library for the elps environment
|
Package lisplib is used to conveniently load the standard library for the elps environment |
|
libelpspath
elpspath — positional-arg path operations
|
elpspath — positional-arg path operations |
|
x
|
|
|
debugger
Package debugger implements the ELPS debugger engine (Layer 1).
|
Package debugger implements the ELPS debugger engine (Layer 1). |
|
debugger/dapserver
Package dapserver implements a DAP (Debug Adapter Protocol) server for the ELPS debugger engine.
|
Package dapserver implements a DAP (Debug Adapter Protocol) server for the ELPS debugger engine. |
|
debugger/debugrepl
Package debugrepl provides an interactive CLI debug REPL built on top of the extensible repl.RunEnv function and the debugger engine.
|
Package debugrepl provides an interactive CLI debug REPL built on top of the extensible repl.RunEnv function and the debugger engine. |