On this page:
3.1 Resolving functions
3.2 Calling functions
3.3 Figuring out what to run next
3.4 Uplevel

3 Core operations

Now we’ve got a context, what can we do with it?

3.1 Resolving functions

Looking up a symbol to find its definition needs to recursively walk up the parent contexts, looking in the definitions table until it finds an entry.

(: context-resolve (Context Symbol . -> . (Option Function)))
(define (context-resolve ctx name)
  (or (hash-ref (context-definitions ctx) name #f)
      (and (context-parent ctx)
        (context-resolve (context-parent ctx) name))))

3.2 Calling functions

A function is either a Builtin (a regular function that modifies the context and stack) or Code (a list representing the function body). Regular functions can just be called, but Code requires a new context (and the current context becomes its parent). This uses some extra functions to deal with errors.

(: interp-eval (Context Stack Function
                        . -> . (Values Context Stack)))
(define (interp-eval ctx stack f)
    [(Builtin? f) (f ctx stack)]
    [(list? f) (values (make-context #:body f #:parent ctx) stack)]))
; Resolve the symbol, set up an error handler blaming it,
; and eval its definition
(: interp-call (Context Stack Symbol
                        . -> . (Values Context Stack)))
(define (interp-call ctx stack sym)
  (let ([v (context-resolve ctx sym)])
    (if v
      (interp-try-eval ctx stack sym v)
      (interp-handle-error ctx stack 'undefined (list sym)))))

3.3 Figuring out what to run next

There’s an easy way to do this, and a less easy way. The easy way is to simply read code from the program. This is what quote will use, since it usually maps directly to source code.

(: context-next-code (Context . -> . (Values Context (Maybe Any))))
(define (context-next-code ctx)
  (if (null? (context-body ctx))
    (values ctx (void))
    (let ([code (car (context-body ctx))]
          [ctx (struct-copy context ctx [body (cdr (context-body ctx))])])
      (values ctx code))))

The less easy way is used by the interpreter itself, which needs to take account of uplevels and parent contexts. It needs to “return” to contexts in reverse order of entry, which means it deals with uplevel children first (if there are any), followed by the current context (if there’s any code left, using context-next-code), then finally it tries to return to the parent context.

(: context-next (Context . -> . (Values Context (Maybe Any))))
(define (context-next ctx)
    ; Find the innermost first child
    [(not (null? (context-children ctx)))
     (let* ([parent (struct-copy context ctx
                                 [children (cdr (context-children ctx))])]
            [child (struct-copy context (car (context-children ctx))
                                [parent parent])])
       (context-next child))]
    ; Current context next code
    [(not (null? (context-body ctx)))
     (context-next-code ctx)]
    ; Use the parent, discarding the current context as it's now useless
    [(context-parent ctx)
     (context-next (context-parent ctx))]
    ; There's nothing left. The program is finished.
    [else (values ctx (void))]))

3.4 Uplevel

Finally, uplevel. All this does is move up to the parent context and push the current one on its list of children, like a reverse function call. context-next takes care of the rest.

(: context-uplevel (Context . -> . (Option Context)))
(define (context-uplevel ctx)
  (let ([parent (context-parent ctx)])
    (and parent
         ; Unset parent because it'll be stale
         (let ([child (struct-copy context ctx [parent #f])])
             context parent
             [children (cons child (context-children parent))])))))