This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Wile is a Scheme interpreter/compiler in Go with hygienic macros. It compiles Scheme to bytecode and executes it on a stack-based virtual machine, implementing R7RS-style syntax-rules macros with a "sets of scopes" hygiene model (Flatt 2016).
Wile is a Scheme scripting layer that feels native to Go, not a Scheme that happens to be written in Go.
- Full R7RS compliance is the baseline. Compliance is the floor, not the ceiling.
- Embedding is the product. Pure Go (no CGo),
go getdependency, idiomatic API for Go developers.
These are exact patterns. Do not improvise or substitute alternatives.
| Wrong | Correct | Note |
|---|---|---|
| Creating plans in random locations | Creating plans in plans/ |
Plans live at repo root |
if x := f(); x != nil { |
x := f() then if x != nil { |
No compound if-assignments |
func foo() int { return x } |
Multi-line function body | NEVER write single-line function definitions |
panic(werr.ErrFoo) |
panic(werr.WrapForeignErrorf(werr.ErrFoo, "site: what failed")) |
NEVER panic with raw errors — always wrap with location context |
replace:
if <conditional> {
mc.SetValue(values.TrueValue)
} else {
mc.SetValue(values.FalseValue)
}
with:
mc.SetValue( BoolToBoolean(<conditional>) )
ALWAYS create plan files in plans/.
NEVER commit changes without asking first. The user structures commits themselves.
NEVER commit directly to master. All changes must go through feature branches and pull requests.
NEVER write single-line function definitions. This applies to ALL function forms:
named functions, methods, closures (inline, deferred, goroutine, or assigned), and
function arguments. Every function body MUST start on the line after the opening brace
and the closing brace MUST be on its own line. No exceptions.
NEVER write code that exclusively accepts *values.Pair for read-only operations. Use values.Tuple interface instead. Only use *values.Pair when mutation (SetCar, SetCdr) or type-specific predicates (pair?) are required.
When working from TODO.md or a phased plan, read and update TODO.md after completing each phase. Mark items done as you go so progress is visible and no work gets repeated across sessions.
The build is not clean until make lint && make covercheck both pass. Run both after any code changes and fix all failures before claiming the task complete.
Finish codebase reading and exploration before the session ends. If a plan is too large to complete in one session, break it into smaller chunks that can each be completed independently. Partial exploration with no code changes is wasted work.
- Language: R7RS Scheme with hygienic macros (sets of scopes per Flatt 2016), first-class continuations, numeric tower
- Execution: Bytecode VM with explicit PC loop (
MachineContext.Run()inmachine_context.go), separate eval stack — NOT tree-walking interpreter - Continuations: Explicit
MachineContinuationlinked list — NOT Go call stack; enablescall/cc, dynamic-wind, delimited continuations - Pipeline:
Tokenizer → Parser → Expression → Expander → Compiler → VM(string → tokens → SyntaxValue → *Expression → bytecode → execution) - Packages (public):
wile/(Engine, embedding API),values/(Scheme types),werr/(error infrastructure),registry/(primitives/extensions),security/(authorization),extensions/(public extensions) - Packages (internal):
machine/(VM/compiler/expander),environment/(bindings/scopes),internal/{tokenizer,parser,syntax,match,repl,bootstrap,validate,schemeutil,forms,extensions} - Values: Go heap objects managed by Go GC — pure Go, no CGo, no custom allocator
- Error handling: Sentinel + wrap pattern —
werr.NewStaticErrorfor sentinels,werr.WrapForeignErrorffor context; neverfmt.Errorf - Hygiene: Identifiers carry scope sets, resolution checks
bindingScopes ⊆ useScopes; free template identifiers skip intro scope - Concurrency: Engine not thread-safe (one per goroutine); SRFI-18 threads within Engine safe (VM manages coordination)
- Security: Two-layer sandboxing — extension-level (opt-in via
WithExtension()) and fine-grained authorization (security.Authorizerinterface withsecurity.Check()gating) - Source loading: All include/load/import goes through
FileResolverinterface (machine/file_resolver.go). Four implementations:OSFileResolver(OS filesystem),FSFileResolver(virtualfs.FSviaWithSourceFS),EmbedFileResolver(bootstrap),ChainFileResolver(searches multiple resolvers in order). MultipleWithSourceFScalls build a chain;WithSourceOS()appends OS as fallback.
string → Tokenizer(internal/tokenizer) → Parser(internal/parser) → SyntaxValue
→ *Expression(expression.go, Engine.Parse())
→ Expander(machine/expander_*.go) → Compiler(machine/compile_*.go) → NativeTemplate
→ VM(machine/machine_context.go, MachineContext.Run()) → values.Value
Single-expression entry: Engine.Parse() → *Expression → Engine.Eval() or Engine.Compile() + Engine.Run()
Multi-expression entry: Engine.EvalMultiple() (string → parse/expand/compile/run loop internally)
*Expression wraps a single SyntaxValue — the "exactly one expression" constraint
is enforced at parse time by Parse, not by Eval/Compile. This prevents silent
partial consumption of multi-expression input. See expression.go.
werr/ → values/ → environment/ → internal/{tokenizer,parser,syntax,schemeutil,validate,match,bootstrap,extensions,forms,repl}
→ machine/ + security/ → registry/ → extensions/ → wile/ (root)
Note: machine/ and security/ are peers — machine/ imports security/ for authorization gate sites, but security/ has no dependency on machine/.
Public API (embedders): wile/, values/, werr/, registry/, security/, extensions/*. Internal: internal/*. Machine: public but rarely used directly.
Two-layer sandboxing for embedded use:
- Extension-level (zero-cost): Extensions opt-in via
WithExtension(). Unprovided extensions don't exist at compile time.SafeExtensions()provides a safe sandbox (no filesystem, eval, system, threads, Go interop). - Fine-grained authorization:
security.Authorizerinterface gates privileged operations at runtime. K8s-style vocabulary: Resource (file,code,env,process) + Action (read,write,delete,load,exit). Set viaWithAuthorizer()engine option. Gate sites: files, system, eval extensions;include; library import.
- Lowercase filenames, no uppercase or underscores in package names
- Avoid generic
utilpackages — put helpers where they're used - Comments explain why, not what — non-obvious logic gets context, obvious code gets none
- Table-driven tests are the norm for multiple scenarios (see
registry/CLAUDE.md) - All new packages require unit tests; significant features need integration tests in
integration/ - Early return from functions. Check preconditions and known failure modes first, return early on error/edge cases, keep the happy path flat and unindented. No nested if/else chains when guard clauses suffice. See
CODING_STYLE.mdfor examples.
git fetch+git rebase, nevergit pull(merge commits block PRs)- Never push to upstream master — always branch + PR
- Squash fixup commits after review, not before
After any Go code changes, run make lint (or at minimum goimports -w on changed files) before considering the task complete. Do not report completion with outstanding formatting or import issues.
| Name | Role | Rationale |
|---|---|---|
p |
Method receiver (always) | Type is in the signature; role is clear from being a receiver. No need to name it after the type. Exception: compiler uses c. |
q |
Primary return value | Assign q as early as possible so the reader can track "this is what gets returned" through the code flow. |
err |
Error return value | Standard Go convention. |
These names save mental space: p is always the receiver, q is always the result being built, err is always the error. No need to invent descriptive names for roles that are already clear from position.
Two-layer convention: sentinel + wrap. Use werr.NewStaticError for sentinels, werr.WrapForeignErrorf at return sites. Never use bare errors.New or fmt.Errorf in production code. Always use errors.Is/errors.As, never ==/!=.
When debugging type switch issues, READ the actual case types carefully. Do not assume.
case Interface:matches all types implementing that interfacecase *ConcreteType:matches only that specific pointer type
When debugging predicates or type-based dispatch, read the existing cases word-for-word before proposing changes.
Use values.Tuple for read-only operations, *values.Pair only for mutation or type predicates.
| Use Case | Type | Why |
|---|---|---|
| Traversal, pattern matching, assoc lookup | values.Tuple |
Generic (works with *Pair, emptyListType) |
| List copying | Input: Tuple, Output: *Pair |
Read generically, write concretely |
list-set!, set-car!, set-cdr! |
*values.Pair |
Needs SetCar/SetCdr |
pair? predicate |
*values.Pair |
Type-specific per R7RS |
make build # Build to ./dist/{os}/{arch}/wile
make test # Run all tests (go test -v ./...)
make lint # Run golangci-lint
go test -v -run TestName ./package/... # Run a single testSee cmd/CLAUDE.md for full build commands, dist/ structure, and REPL usage.
TODO.md— Pending tasks, missing R7RS features, future extensionsCODING_STYLE.md— Comprehensive style guidePRIMITIVES.md— Complete primitives referenceBIBLIOGRAPHY.md— Academic papers, specifications, canonical referencesdocs/dev/R7RS_SEMANTIC_DIFFERENCES.md— Documented R7RS specification deviationsdocs/dev/ENVIRONMENT_SYSTEM.md— Environment system architecturedocs/dev/NUMERIC_TOWER.md— Numeric tower architecturedocs/dev/DEBUG_METHODOLOGY.md— Systematic debug logging methodology and Go gotchasdocs/EXTENSIONS.md— Extension system architecture and authoring guidedocs/EXTENSION_LIBRARIES.md— R7RS library integration for extensionsdocs/design/SOURCE_LOADING.md— FileResolver chain, embedded stdlib, library import resolutiondocs/design/PEEPHOLE_OPTIMIZER.md— Superinstruction formation, 3-pass pipeline, promoted opcodesplans/CLAUDE.md— Active plan files and design documents