Guide for building metacircular evaluators in Scheme-like languages. This skill applies when implementing interpreters that can interpret themselves, handling tasks involving eval/apply loops, environment management, closure implementation, and multi-level interpretation. Use for any metacircular evaluator, Scheme interpreter, or self-interpreting language implementation task.
This skill provides guidance for building metacircular evaluators—interpreters written in the same language they interpret, capable of interpreting themselves. These tasks require careful handling of evaluation levels, environment structures, and the distinction between host-level and interpreted-level data.
Before writing any code:
Examine the host interpreter implementation - Read the existing interpreter (e.g., interp.py) to understand what primitives are provided, how values are represented, and what the evaluation model looks like.
Study the test files - Understand what constructs need to be supported by examining test cases. Identify which tests involve single-level vs. multi-level interpretation.
Establish reliable test execution - Verify the testing mechanism works correctly before deep debugging. Shell command quirks (like echo -e vs printf behavior) can cause false positives in debugging.
Create a mental model of interpretation levels:
interp.py)eval.scm running in Python)eval.scm interpreting eval.scm)Implement environment operations with clear semantics:
;; Environment structure: list of frames, each frame is list of (name . value) pairs
(define (make-env) '(()))
(define (extend-env params args env)
;; Create new frame with parameter bindings, prepend to env
(cons (make-frame params args) env))
(define (lookup var env)
;; Search frames from innermost to outermost
...)
(define (env-define! var val env)
;; Add binding to current (first) frame
...)
Critical: Test environment operations in isolation before using them in the evaluator. Create unit tests for:
Closures must capture their defining environment:
(define (make-closure params body env)
(list 'closure params body env))
(define (closure? obj)
(and (pair? obj) (eq? (car obj) 'closure)))
(define (closure-params c) (cadr c))
(define (closure-body c) (caddr c))
(define (closure-env c) (cadddr c))
Key insight for metacircular interpretation: At level 2, closures are data structures that the level-1 interpreter must correctly recognize and apply. Ensure predicates like closure? work on data that has passed through multiple interpretation layers.
(define (eval exp env)
(cond
((self-evaluating? exp) exp)
((variable? exp) (lookup exp env))
((quoted? exp) (cadr exp))
((definition? exp) (eval-definition exp env))
((if? exp) (eval-if exp env))
((lambda? exp) (make-closure (lambda-params exp) (lambda-body exp) env))
((let? exp) (eval-let exp env))
((begin? exp) (eval-sequence (begin-actions exp) env))
((application? exp) (apply-proc (eval (car exp) env)
(eval-args (cdr exp) env)
env))
(else (error "Unknown expression type"))))
(define (apply-proc proc args env)
(cond
((primitive? proc) (apply-primitive proc args))
((closure? proc)
(eval-sequence (closure-body proc)
(extend-env (closure-params proc)
args
(closure-env proc))))
(else (error "Unknown procedure type"))))
let ExpressionsTwo approaches:
Direct implementation:
(define (eval-let exp env)
(let ((vars (let-vars exp))
(vals (map (lambda (e) (eval e env)) (let-vals exp)))
(body (let-body exp)))
(eval-sequence body (extend-env vars vals env))))
Transform to lambda (fallback approach):
(define (let->lambda exp)
(cons (list 'lambda (let-vars exp) (let-body exp))
(let-vals exp)))
Note: Transforming let to lambda can be a workaround but may mask underlying issues. Prefer direct implementation and investigate if problems persist.
When something works at level 1 but fails at level 2:
Identify the failing component - Is it environment lookup, closure application, or expression evaluation?
Create minimal reproduction - Write the smallest program that demonstrates the failure:
;; Test at level 1
(eval '(let ((x 1)) x) (make-env))
;; Test at level 2 - have eval.scm interpret a simple let
Add debug output - Insert print statements at key points:
Compare execution traces - Run the same expression at level 1 and level 2, comparing the debug output to find where behavior diverges.
Predicate confusion: At level 2, pair? on the host might return #t for a closure structure, but the interpreted pair? might behave differently if closures are represented as lists.
Environment corruption: When environments pass through multiple interpretation layers, ensure the structure is preserved. A common bug is the environment becoming "flattened" or losing nested structure.
Primitive vs. closure distinction: Ensure primitive? and closure? predicates are mutually exclusive and correctly identify procedures at all interpretation levels.
Before testing full programs, verify:
;; Test environment operations
(define test-env (make-env))
(env-define! 'x 1 test-env)
(assert (= (lookup 'x test-env) 1))
;; Test shadowing
(define nested-env (extend-env '(x) '(2) test-env))
(assert (= (lookup 'x nested-env) 2))
;; Test closure creation and access
(define test-closure (make-closure '(a) '((+ a 1)) test-env))
(assert (closure? test-closure))
(assert (equal? (closure-params test-closure) '(a)))
eval.scm interpreting simple expressionseval.scm interpreting itselfCreate separate test files for different concerns:
test-env.scm - Environment operationstest-closure.scm - Closure creation and applicationtest-eval-basic.scm - Basic evaluationtest-metacircular.scm - Self-interpretationWhen running tests via shell commands:
echo -e behavior varies between shells; prefer printf for consistent escape handlingAvoid these until root cause is understood:
Before implementing fixes, clearly answer:
When using debug/trace output:
Before considering the implementation complete: