2.5 Evaluators🔗ℹ

A evaluator is both a starting point for parsing and a starting point for running compiled code. An evaluator also has a module registry that maps module names to module declarations (see Modules and Module-Level Variables). An evaluator’s module registry is shared by all phase levels, and it applies both to parsing and to running compiled code. A function like Evaluate.make_rhombus() creates a fresh evaluator with an empty set of top-level definitions and a module registry that has only rhombus and its dependencies.

As a starting point for parsing, an evaluator provides scopes (one per phase level, plus one that spans all phase levels). Operations such as Evaluator.import create initial bindings using the evaluator’s scopes, and the further parsing and evaluation in the evaluator can create additional bindings. Evaluation of a form with an evaluator always adds the evaluator’s phase-specific scopes to the form and to the result of expanding a top-level form; as a consequence, every binding identifier has at least one scope. Every evaluator uses the same scope as the one added to all phase levels, while the scopes specific to a phase level are always distinct.

As a starting point for evaluating compiled code, each evaluator encapsulates a distinct set of top-level variables at various phases, as well as a potentially distinct set of module instances in each phase. That is, even though module declarations are shared for all phase levels, module instances are distinct for each phase. Each evaluator has a base phase, which corresponds to the phase used by reflective operations such as eval and Evaluator.instantiate. In particular, using eval on a import form instantiates a module in the evaluator’s base phase.

After an evaluator is created, module instances from existing evaluators can be attached to the new evaluator. In terms of the evaluation model, top-level variables from different evaluators essentially correspond to definitions with different prefixes, but attaching a module uses the same prefix for the module’s definitions in evaluators where it is attached. The first step in evaluating any compiled expression is to link its top-level variable and module-level variable references to specific variables in the evaluator.

At all times during evaluation, some evaluator is designated as the current evaluator. The current evaluator has no particular relationship, however, with the evaluator that was used to expand the code that is executing, or with the evaluator that was used to link the compiled form of the currently evaluating code. In particular, changing the current evaluator during evaluation does not change the variables to which executing expressions refer. The current evaluator only determines the behavior of reflective operations to expand code and to start evaluating expanded/compiled code.

> def x = #'orig

> // The following `block` expression is compiled in the original

  // evaluator, so direct references to `x` see `#'orig`

  block:

    def n = Evaluator.make_rhombus() // make new evaluator

    parameterize { Evaluator.current: n }:

      eval('def x = #'new'.strip_scopes()) // in the new evaluator

      println(x)         // prints `#'orig`

      println(eval('x'.strip_scopes())) // prints `#'new`

orig

new

If an identifier is bound to a macro or to an import in an evaluator’s top level, then defining the identifier as a variable shadows the macro or import in future uses of the top level. Similarly, if an identifier is bound to a top-level variable, then binding the identifier to a macro or an import shadows the variable; the variable’s value remains unchanged, however, and may be accessible through previously evaluated expressions.

> def x = 5

> fun f(): x

> x

5

> f()

5

> macro 'x': '10'

> x

10

> f()

5

> def x = 7

> x

7

> f()

7

> module m ~lang rhombus:

    export def x = 8

> import self!m.x

> x

8

> f()

7

Like an evaluator’s top level, each module form has an associated scope to span all phase levels of the module’s content, plus a scope at each phase level. The latter is added to every form, original or appearing through partial macro expansion, within the module’s immediate body. Those same scopes are propagated to a namespace created by Evaluator.from_module for the module. Meanwhile, parsing of a module form begins by removing the all scopes that correspond to the enclosing top-level or (in the case of submodules) module form.