Guide for implementing metacircular evaluators—interpreters that can interpret themselves. This skill should be used when building self-interpreting Scheme-like evaluators, debugging multi-level interpretation issues, or implementing language features like environments, closures, and special forms. Focuses on incremental development, continuous metacircular testing, and systematic debugging of nested interpretation failures.
التثبيت
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
Guide for implementing metacircular evaluators—interpreters that can interpret themselves. This skill should be used when building self-interpreting Scheme-like evaluators, debugging multi-level interpretation issues, or implementing language features like environments, closures, and special forms. Focuses on incremental development, continuous metacircular testing, and systematic debugging of nested interpretation failures.
Metacircular Evaluator Implementation
Overview
This skill provides systematic guidance for implementing metacircular evaluators—interpreters written in the language they interpret. The critical challenge is not just building a working interpreter, but building one that can interpret itself (the metacircular property).
Critical Success Factors
The Metacircular Property is Non-Negotiable
A metacircular evaluator must pass this test:
# Level 1: Direct interpretationecho'test-program.scm' | interpreter eval.scm
# Level 2: Self-interpretation (MUST produce same output)echo -e 'eval.scm\ntest-program.scm' | interpreter eval.scm
Test the metacircular property continuously throughout development, not just at the end. Every feature must work at both levels before adding the next.
Build Incrementally, Test at Both Levels
Wrong approach: Implement all features, then test metacircular property.
Correct approach: For each feature:
Implement the feature
Test at level 1 (direct interpretation)
Test at level 2 (self-interpretation) with minimal test case
Only proceed to next feature after both levels pass
Implementation Strategy
Phase 1: Minimal Self-Interpreting Core
Start with the smallest possible evaluator that can interpret itself:
For each addition, create a minimal test and verify at both levels.
Environment Implementation
Representation Pattern
Use a structure that can be manipulated consistently at all interpretation levels:
;; Environment as list of frames
;; Frame = (vars . vals) where vars and vals are parallel lists
;; env = (frame1 frame2 ... global-frame)
(define (extend-env vars vals base-env)
(cons (cons vars vals) base-env))
(define (lookup var env)
(if (null? env)
(error "Undefined variable:" var)
(lookup-in-frame var (car env)
(lambda () (lookup var (cdr env))))))
Critical Invariants
After any environment operation, verify:
Variable list and value list have same length
Frame structure is (vars . vals) not ((vars vals))
Lookup traverses correctly through frame chain
Common Environment Bugs
Bug: Dotted pairs vs. proper lists
;; Wrong: creates single frame, not extendable
(cons vars vals) ; If vars=(x y), vals=(1 2), result is ((x y) . (1 2))
;; Verify structure manually before proceeding
Bug: Environment extension corrupts at level 2
At level 2, environments are interpreted data structures. The same extend-env code manipulates different representations at different levels. Test environment operations in isolation at level 2:
All test programs produce same output at level 1 and level 2
Full self-interpretation:
eval.scm can interpret eval.scm interpreting test programs
Double metacircular (eval.scm → eval.scm → test.scm) works
Anti-Patterns to Avoid
Don't implement all features before testing metacircular property
Every untested feature may introduce bugs that cascade. Test at both levels after each feature.
Don't use trial-and-error debugging
When level 2 fails, systematically:
Find minimal failing test
Trace data structure transformations
Verify representation invariants
Don't randomly modify code hoping it works.
Don't assume primitives work identically at all levels
(eq? 'x 'x) may behave differently when symbols are interpreted data at level 2. Test primitive operations on interpreted data structures explicitly.
Don't write extensive code before incremental testing
The initial evaluator should be as small as possible while still being self-interpreting. Add complexity only after the core works metacircularly.
I/O Handling
Input stream management
When eval.scm interprets itself:
stdin: "eval.scm\ntest.scm\ninput1\n"
^-- outer eval reads this
^-- inner eval reads this
^-- interpreted program reads this
Each read consumes from the same sequential stream across all levels.
Testing I/O in isolation
# Test read works at level 1echo"42" | python3 interp.py test-read.scm
# Test read works at level 2printf'test-read.scm\n42\n' | python3 interp.py eval.scm
# Verify input bytes are correctprintf'line1\nline2\n' | cat -A
Recovery Strategies
When completely stuck
Simplify: Remove features until something works at level 2
Verify host: Confirm the host interpreter works as expected
Test components: Verify environment operations separately
Minimal rewrite: Sometimes starting the core evaluator fresh with lessons learned is faster than debugging
When level 2 seems impossible
The core insight: if level 2 fails, the evaluator is making assumptions about data representation that break when the evaluator interprets itself.
Identify what assumption is failing
Make the assumption explicit (verify it with test code)
Fix the assumption or change the representation
The evaluator and the interpreted program must handle data structures identically.