$\DeclareMathOperator{\binding}{binding}$

Implementation Notes

The implementation notes document the JavaScript file /system/core.js, which implements the “core” of the EVLambda programming language and the EVL to XML converter (one of the two components of the documentation generator, the other one being the XSLT stylesheet). The implementation notes and the JavaScript file have the same organization (the same sections, in the same order) and are meant to be read side by side.

The JavaScript file implements five interpreter-based evaluators:

Only the trampoline and trampoline++ evaluators allow unbounded iterations through tail-recursive calls. The other evaluators are only useful as stepping stones to understand the trampoline and trampoline++ evaluators. The trampoline++ evaluator is an optimized version of the trampoline evaluator.

The JavaScript file can run inside the IDE (actually inside a web worker started from the IDE) or inside a Node.js runtime environment started from the command line.

Here are quick links to the sections:

  1. Global Variables
  2. Interface (IDE)
  3. Errors
  4. Tokenizer
  5. Reader
  6. EVL to XML Converter
  7. Form Analyzer
  8. Scope-Extent Combinations
  9. Namespaces
  10. Global Environment
  11. Lexical and Dynamic Environments
  12. Pairing Parameters with Arguments
  13. Generic Evaluator
  14. Direct Style Evaluator
  15. Continuation Passing Style Evaluator
  16. Object-Oriented CPS Evaluator
  17. Stack-Based Object-Oriented CPS Evaluator
  18. Trampoline Evaluator
  19. Trampoline++ Evaluator
  20. Primitive Function Definitions
  21. Bounce
  22. Evaluation Request
  23. Outcome
  24. Abrupt Completion
  25. Abrupt Completion of Type error
  26. Abrupt Completion of Type nonlocal-exit
  27. Result
  28. Multiple Values
  29. Primitive Data Type object
  30. Primitive Data Type void
  31. Primitive Data Type boolean
  32. Primitive Data Type number
  33. Primitive Data Type character
  34. Primitive Data Type string
  35. Primitive Data Type symbol
  36. Primitive Data Type keyword
  37. Primitive Data Type variable
  38. Primitive Data Type list
  39. Primitive Data Type empty-list
  40. Primitive Data Type cons
  41. Primitive Data Type vector
  42. Primitive Data Type function
  43. Primitive Data Type primitive-function
  44. Primitive Data Type closure
  45. Miscellaneous Primitive Functions
  46. Primitive Function Definitions (Second Steps)
  47. Variables (Special Operators, etc.)
  48. Interface (Command Line)

Global Variables

The constant isRunningInsideNode is true if the JavaScript file is running inside a Node.js runtime environment and false otherwise.

The variable abortSignalArray contains a shared array used by the IDE to abort the current evaluation without terminating the web worker.

The variable selectedEvaluator contains the name of the selected evaluator.

The constant optimizeMacroCalls controls whether or not the macro expansions should be burned into the code.

Interface (IDE)

This section implements the interface used by the IDE to control the evaluator and the EVL to XML converter.

The evaluator and the EVL to XML converter are running inside a web worker because running them directly inside the main thread would make the GUI unresponsive when they are running.

The main thread and the web worker thread communicate through messages and shared arrays.

Messages

The request messages sent by the main thread to the web worker thread are objects containing the following properties:

The response messages sent by the web worker thread to the main thread are objects containing the following properties:

When the processing of the requested action fails because of an error, the response message contains the following properties:

When the processing of the requested action fails because of the abortion of an evaluation, the response message contains the following properties:

The following sections describe the messages currently implemented. The response messages ERROR and ABORTED are omitted from the descriptions. The property id is omitted from the descriptions.

INITIALIZE

This message is used to request the initialization of the web worker.

Request message:

Response message when the processing of the requested action succeeds:

EVALUATE_FIRST_FORM

This message is used to request the evaluation of the first top-level form contained inside some input string.

Request message:

Response message when the processing of the requested action fails because the input string does not contain any forms:

Response message when the processing of the requested action succeeds:

EVALUATE_ALL_FORMS

This message is used to request the evaluation of the top-level forms contained inside some input string.

Request message:

Response message when the processing of the requested action succeeds:

CONVERT_EVL_TO_XML

This message is used to request the conversion of some input string from EVL to XML.

Request message:

Response message when the processing of the requested action succeeds:

Shared Arrays

abortSignalArray

The shared array contained in the variable abortSignalArray is used by the IDE to abort the current evaluation without terminating the web worker. The shared array contains one boolean flag represented by an $8$-bit unsigned integer. The IDE requests the abortion of the current evaluation by setting the flag to true. The trampoline and trampoline++ evaluators implement the abort-evaluation functionality by regularly checking the value of the flag. The other evaluators do not implement the abort-evaluation functionality.

Errors

This section defines some custom error types.

Tokenizer

For the most part, the tokenizer is implemented as described in the reference manual. The main difference is that the tokens are produced on demand instead of all at once before the parsing begins.

On each invocation, the tokenizer sets the following properties:

Reader

The reader is a recursive descent parser getting its tokens from the tokenizer.

The read-time conditionalization facility is integrated into the reader.

The central function is the function readObject, which returns one of the following:

The function readObject treats XML elements as comments and skips them. However, the objects contained inside XML elements are not completely invisible because a callback can be attached to the tokenizer that will be invoked on each object encountered while skipping XML elements.

Most of the other functions invoke the function readObject and use a switch statement to decide what to do depending on the result of the invocation.

The main function is the function read, which invokes the function readObject and, depending on the result of the invocation, does one of the following:

In order to evaluate the top-level forms contained inside an EVLambda source file, the function read is invoked in a loop. If the function read returns an object, then that object is evaluated and the loop continues. If the function read returns null, then the loop terminates.

When the source file is a plain EVLambda source file, the function read returns each top-level form in turn and all top-level forms get evaluated.

When the source file is a documented EVLambda source file, the first invocation of the function read returns null (because the chapter element is treated as a comment and skipped) and nothing seems to get evaluated. Actually, this is not the case because a callback is attached to the tokenizer that will evaluate each object encountered while skipping the chapter element.

EVL to XML Converter

The job of the EVL to XML converter is to produce an output identical to the input except that (1) the characters less-than, greater-than, and ampersand occurring inside the code have been escaped and (2) some tags have been added to better delimit the code from the surrounding documentation and the comments from the surrounding code.

The EVL to XML converter uses the following context-free grammar to model the input:

$\metavar{input}$ $\Coloneq$ beginning-of-input {$\metavar{top-level-whitespace}$ $\metavar{construct}$}* $\metavar{top-level-whitespace}$ end-of-input
$\metavar{construct}$ $\Coloneq$ $\metavar{xml-construct}$ | $\metavar{evl-construct}$
$\metavar{xml-construct}$ $\Coloneq$ xml-start-tag {$\metavar{xml-whitespace}$ $\metavar{construct}$}* $\metavar{xml-whitespace}$ xml-end-tag | xml-empty-element-tag | xml-comment
$\metavar{evl-construct}$ $\Coloneq$ void | boolean | number | hash-string-construct | string | keyword | variable | quote | quasiquote | unquote | unquote-splicing | hash-plus | hash-minus | dot | {left-parenthesis | hash-left-parenthesis} {$\metavar{evl-whitespace}$ $\metavar{construct}$}* $\metavar{evl-whitespace}$ right-parenthesis

The main differences between the context-free grammar used by the EVL to XML converter and the context-free grammar used by the parser are the following:

The tags are always added before or after a run of whitespace. The converter considers each of the runs of whitespace contained inside the input and, for each of them, decides what to do depending on the following:

The XML tokens are the following tokens: xml-start-tag, xml-end-tag, xml-empty-element-tag, and xml-comment.

The EVL tokens are the end-of-line and end-of-last-line comments (which are treated as a unit) and the following tokens: void, boolean, number, hash-string-construct, string, keyword, variable, quote, quasiquote, unquote, unquote-splicing, hash-plus, hash-minus, dot, left-parenthesis, hash-left-parenthesis, and right-parenthesis.

The end-of-line and end-of-last-line comments are treated as EVL tokens because they are not delimited from the surrounding code by additional tags.

Tags are never added before or after the runs of whitespace occurring in top-level context.

The runs of whitespace occurring in XML context are processed as follows:

$\metavar{xmltok}$$\metavar{ws}$$\metavar{evltok}$ is transformed into
$\metavar{xmltok}$$\metavar{ws}$<toplevelcode><blockcode>$\metavar{evltok}$
$\metavar{evltok}$$\metavar{ws}$$\metavar{evltok}$, where $\metavar{ws}$ contains at least one blank line, is transformed into
$\metavar{evltok}$</blockcode></toplevelcode>$\metavar{ws}$<toplevelcode><blockcode>$\metavar{evltok}$
$\metavar{evltok}$$\metavar{ws}$$\metavar{evltok}$, where $\metavar{ws}$ contains no blank lines, is transformed into
$\metavar{evltok}$$\metavar{ws}$$\metavar{evltok}$
$\metavar{evltok}$$\metavar{ws}$$\metavar{xmltok}$ is transformed into
$\metavar{evltok}$</toplevelcode></blockcode>$\metavar{ws}$$\metavar{xmltok}$
$\metavar{xmltok}$$\metavar{ws}$$\metavar{xmltok}$ is transformed into
$\metavar{xmltok}$$\metavar{ws}$$\metavar{xmltok}$

The runs of whitespace occurring in EVL context are processed as follows:

$\metavar{evltok}$$\metavar{ws}$$\metavar{xmltok}$ is transformed into
$\metavar{evltok}$</blockcode><indentation style="…"><blockcomment>$\metavar{ws}$$\metavar{xmltok}$
$\metavar{xmltok}$$\metavar{ws}$$\metavar{xmltok}$ is transformed into
$\metavar{xmltok}$$\metavar{ws}$$\metavar{xmltok}$
$\metavar{xmltok}$$\metavar{ws}$$\metavar{evltok}$ is transformed into
$\metavar{xmltok}$</blockcomment></indentation><blockcode>$\metavar{ws}$$\metavar{evltok}$
$\metavar{evltok}$$\metavar{ws}$$\metavar{evltok}$ is transformed into
$\metavar{evltok}$$\metavar{ws}$$\metavar{evltok}$

Form Analyzer

The analysis of a form does not entail the analysis of the direct subforms of the form or, if the form is a lambda abstraction, the analysis of the body of the form. In all evaluators except the trampoline++ evaluator, the direct subforms and the bodies are analyzed if and when they are evaluated. In the trampoline++ evaluator, a top-level form is fully analyzed during the preprocessing phase.

The first part of the analysis is done by the evaluators themselves by checking whether the form is the empty list, a cons, a variable, or something else and, if the form is a cons, by checking whether the car of the cons is a special operator or something else.

Scope-Extent Combinations

The scope-extent combinations are identified using the members of the following enumeration:

Namespaces

The namespaces are identified using the members of the following enumeration:

Global Environment

Because the global environment is unique, any given variable can have, at any given time, at most one global binding in the value namespace and one global binding in the function namespace. An obvious way to implement the global environment is thus to store the global bindings for a variable directly on the JavaScript object representing the variable.

Variables are represented by instances of the class EVLVariable. The property value of the instance representing a variable contains the value of the binding for the variable in the value namespace of the global environment if that binding exists and null otherwise. The property function of the instance representing a variable contains the value of the binding for the variable in the function namespace of the global environment if that binding exists and null otherwise.

Lexical and Dynamic Environments

The user manual specifies the lexical and dynamic environments as follows:

The reference manual introduces two new namespaces: the block namespace and the exit-point namespace.

In actuality, a lexical or dynamic environment is represented not by the result of the sequence of extensions leading to the environment but by a recording of the sequence of extensions leading to the environment. A single extension is recorded into a data structure called a frame and a sequence of extensions is recorded into a chain of frames.

Let $\env$ be a lexical or dynamic environment and$$[(\ns_1,[\var_{1,1},\ldots,\var_{1,m_1}],[\obj_{1,1},\ldots,\obj_{1,m_1}]),\ldots,(\ns_n,[\var_{n,1},\ldots,\var_{n,m_n}],[\obj_{n,1},\ldots,\obj_{n,m_n}])]$$be the sequence of extensions leading to the environment. The extensions are applied from left to right and the first extension is applied to an empty environment. The $i$-th extension$$(\ns_i,[\var_{i,1},\ldots,\var_{i,m_i}],[\obj_{i,1},\ldots,\obj_{i,m_i}])$$extends the previous environment by binding, in the namespace $\ns_i$, the variable $\var_{i,j}$ to the object $\obj_{i,j}$ (for all $i$ from $1$ to $n$ and all $j$ from $1$ to $m_i$). We will denote by $\binding(i,j)$ the binding between $\var_{i,j}$ and $\obj_{i,j}$ created by the $i$-th extension.

The binding for the variable $\var$ in the namespace $\ns$ of the environment $\env$ can easily be located using the following lookup procedure:

Initialize $i$ to $n$. Loop: If $i=0$, then the variable $\var$ in unbound in the namespace $\ns$ of the environment $\env$. Otherwise, if $\ns_i=\ns$ and there exists $j\in\{1,\ldots,m_i\}$ such that $\var_{i,j}=\var$ (if such $j$ exists, it is necessarily unique because the variables $\var_{i,1},\ldots,\var_{i,m_i}$ are distinct), then $\binding(i,j)$ is the binding for the variable $\var$ in the namespace $\ns$ of the environment $\env$. Otherwise, decrement $i$ by $1$ and loop.

By selecting the greatest $i$ satisfying the conditions, the lookup procedure correctly handles the possibility of shadowing of a binding by another binding.

Lexical and dynamic environments are represented by instances of the classes Frame and NullDefiniteEnv, which are the two concrete subclasses of the abstract class DefiniteEnv.

For the sake of this document, lexical and dynamic environments are called definite because their scope or their extent is definite. The bindings of a lexical environment have lexical, and thus definite, scope. The bindings of a dynamic environment have dynamic, and thus definite, extent. By contrast, the bindings of the global environment have indefinite scope and indefinite extent.

Instances of Frame, which represent single extensions, have four properties:

The two arrays variables and values must have the same length $m\ge0$. The array variables contains the variables $\var_1,\ldots,\var_m$ and the array values contains the objects $\obj_1,\ldots,\obj_m$. The property next is used to create chains of instances of Frame. Each chain is terminated by an instance of NullDefiniteEnv, which represents an empty environment.

Pairing Parameters with Arguments

The parameters are paired with the arguments by the following functions:

FunctionFunction callRest parameter
pairPrimFunParametersNoApplyplain function call
multiple-value-call-form
N/A
pairPrimFunParametersApplyapply
multiple-value-apply-form
N/A
pairClosureParametersNoApplyNoRestplain function call
multiple-value-call-form
no
pairClosureParametersNoApplyRestplain function call
multiple-value-call-form
yes
pairClosureParametersApplyNoRestapply
multiple-value-apply-form
no
pairClosureParametersApplyRestapply
multiple-value-apply-form
yes

The function pairClosureParametersApplyRest reuses conses from the last element of the spreadable sequence of objects when possible. To illustrate, let us consider the following function calls:

(apply (_vlambda a (list a)) $x=\code{1}$ $y=\code{2}$ $z=\code{'(3 4)}$)
a is paired with (cons $x$ (cons $y$ $z$))$\,=\,$(1 2 3 4) ($2$ new conses and $2$ reused conses)
(apply (_vlambda (a . b) (list a b)) $x=\code{1}$ $y=\code{2}$ $z=\code{'(3 4)}$)
a is paired with $x$$\,=\,$1
b is paired with (cons $y$ $z$)$\,=\,$(2 3 4) ($1$ new cons and $2$ reused conses)
(apply (_vlambda (a b . c) (list a b c)) $x=\code{1}$ $y=\code{2}$ $z=\code{'(3 4)}$)
a is paired with $x$$\,=\,$1
b is paired with $y$$\,=\,$2
c is paired with $z$$\,=\,$(3 4) ($0$ new conses and $2$ reused conses)
(apply (_vlambda (a b c . d) (list a b c d)) $x=\code{1}$ $y=\code{2}$ $z=\code{'(3 4)}$)
a is paired with $x$$\,=\,$1
b is paired with $y$$\,=\,$2
c is paired with (car $z$)$\,=\,$3
d is paired with (cdr $z$)$\,=\,$(4) ($0$ new conses and $1$ reused cons)
(apply (_vlambda (a b c d . e) (list a b c d e)) $x=\code{1}$ $y=\code{2}$ $z=\code{'(3 4)}$)
a is paired with $x$$\,=\,$1
b is paired with $y$$\,=\,$2
c is paired with (car $z$)$\,=\,$3
d is paired with (car (cdr $z$))$\,=\,$4
e is paired with (cdr (cdr $z$))$\,=\,$() ($0$ new conses and $0$ reused conses)
(apply (_vlambda (a b c d e . f) (list a b c d e f)) $x=\code{1}$ $y=\code{2}$ $z=\code{'(3 4)}$)
EvaluatorError: too-few-arguments: Too few arguments.

Generic Evaluator

The generic evaluator simply dispatches the form to evaluate to the selected evaluator.

Direct Style Evaluator

Continuation Passing Style Evaluator

Object-Oriented CPS Evaluator

Stack-Based Object-Oriented CPS Evaluator

Trampoline Evaluator

Trampoline++ Evaluator

Primitive Function Definitions

Defining a primitive function is a two-step process. First, the function primitiveFunction is used to add to the Map contained in the variable primitiveFunctions an entry mapping the name of the primitive function to a record containing the minimum number of arguments, the maximum number of arguments, and the JavaScript function implementing the primitive function. Then, a binding between the name of the primitive function and an instance of the class EVLPrimitiveFunction is added to the function namespace of the global environment.

The first step occurs where it makes the most sense. For the primitive functions related to a primitive date type, the first step occurs inside the section defining the class representing the primitive data type.

The second step is postponed to the section Primitive Function Definitions (Second Steps) because it cannot happen before the class EVLPrimitiveFunction has been defined.

The actual number of arguments is checked against the minimum and maximum numbers of arguments by the code implementing the invocation of primitive functions.

The types of the arguments are checked by the JavaScript functions implementing the primitive functions.

Bounce

A bounce is what is sent back to the trampoline. A bounce is either an evaluation request or an outcome.

Evaluation Request

An evaluation request specifies a form to evaluate and the lexical environment with respect to which the form is to be evaluated. The dynamic environment with respect to which the form is to be evaluated is always the current dynamic environment. An evaluation request is represented by an instance of the class EvalReq.

Outcome

An outcome is either an abrupt completion or the result of a normal completion.

Abrupt Completion

An abrupt completion is either an abrupt completion of type error or an abrupt completion of type nonlocal-exit.

Abrupt Completion of Type error

An abrupt completion of type error is represented by an instance of the class EVLError.

Abrupt Completion of Type nonlocal-exit

An abrupt completion of type nonlocal-exit is represented by an instance of the class NonlocalExit.

Result

The result of a normal completion is represented by an instance of the class EVLObjects (when the result consists of zero or more than one object) or by an instance of a concrete subclass of the abstract class EVLObject (when the result consists of exactly one object).

Multiple Values

A result consisting of zero or more than one object is represented by an instance of the class EVLOjects.

The method primaryValue returns #v if the result consists of zero objects and the first object otherwise.

The method allValues returns an array containing the objects composing the result.

Primitive Data Type object

The abstract class EVLObject represents the primitive data type object.

An instance of a concrete subclass of the abstract class EVLObject can represent one of two things: an object or a result consisting of a single object.

The method primaryValue returns the single object.

The method allValues returns an array containing the single object.

The method eql implements the default behavior of the equality predicate eql? (that is, pointer equality). The method is overridden in the classes EVLNumber, EVLCharacter, and EVLString.

The default implementation of the method toString is overridden in some subclasses to return the printable representation of this.

Primitive Data Type void

An object of type void is represented by a instance of the class EVLVoid.

Primitive Data Type boolean

An object of type boolean is represented by a instance of the class EVLBoolean.

Primitive Data Type number

An object of type number is represented by a instance of the class EVLNumber.

Primitive Data Type character

An object of type character is represented by a instance of the class EVLCharacter.

Primitive Data Type string

An object of type string is represented by a instance of the class EVLString.

Primitive Data Type symbol

The abstract class EVLSymbol represents the primitive data type symbol.

Primitive Data Type keyword

An object of type keyword is represented by a instance of the class EVLKeyword.

Primitive Data Type variable

An object of type variable is represented by a instance of the class EVLVariable.

Primitive Data Type list

The abstract class EVLList represents the primitive data type list.

Primitive Data Type empty-list

An object of type empty-list is represented by a instance of the class EVLEmptyList.

Primitive Data Type cons

An object of type cons is represented by a instance of the class EVLCons.

Primitive Data Type vector

An object of type vector is represented by a instance of the class EVLVector.

Primitive Data Type function

The abstract class EVLFunction represents the primitive data type function.

Primitive Data Type primitive-function

An object of type primitive-function is represented by a instance of the class EVLPrimitiveFunction.

Primitive Data Type closure

An object of type closure is represented by a instance of the class EVLClosure.

Miscellaneous Primitive Functions

This section implements the primitive functions values, error, and now.

Primitive Function Definitions (Second Steps)

The second steps of the primitive function definitions all occur in this section.

Variables (Special Operators, etc.)

This section creates and interns the objects of type variable naming the special operators, the boolean operators (which can occur in feature expressions), and the macro mlet (which is handled directly by the trampoline++ evaluator).

Interface (Command Line)

This section implements the command-line interface used to control the evaluator and the EVL to XML converter.

The syntax of the command line to control the evaluator is as follows:

node core.js { --directstyle | --cps | --oocps | --sboocps | --trampoline | --trampoliepp }? { -l $\metavar{file}$ | -e $\metavar{form}$ }*

The options and arguments have the following meanings:

The syntax of the command line to control the EVL to XML converter is as follows:

node core.js --convert $\metavar{file}$

The options and arguments have the following meanings: