view doc/execution.md @ 154:aa6847f62505

doc: More updates about recent changes
author Lewin Bormann <lbo@spheniscida.de>
date Tue, 03 Sep 2019 15:34:46 +0200
parents f30fbc9202c2
children 92c4365ce0b2
line wrap: on
line source

@page Execution Execution

## Execution model.

Names are scoped and visible in the scope they are defined in, after being
defined, and in child scopes.

The execution runs on the tree of `yexpr_t` after some preprocessing, like
resolution of symbolic references (to values, functions, etc.).

## Scoping.

**Example**:

```
-- This is the top level program.

-- SCOPE 0
(defn f1
 -- SCOPE 1
 (a b)
 (+ (* a a) b))

(defn f2
 -- SCOPE 2
 (a b)
 (defn inner-fn
  -- SCOPE 3
  (x)
  (* x x))
 -- scope 2 again
 (+ (inner-fn a) (inner-fn b)))

-- scope 0 again
(let x 33)

(print "The result is" (f1 5 x))
```

## Preprocessing

This section describes how the parsed syntax tree is processed before
evaluation. It consists of several stages, described in `preprocess.h` and
`preprocess.c`.

### Variable/reference resolution

Symbols in the source code ("symbolic references") are translated into
numeric references (using `yvalue_id_t`) to refer to value slots. This has the
potential to greatly enhance performance as referencing values does not invoke a
potentially expensive hash table lookup anymore but instead a cheap array
dereference. (The third type of reference is "stack-ref", a reference relative
to the head of the stack, used in function calls).

The challenge is identifying each scope uniquely so that there are no reference
conflicts, but possibly also trying to recycle references. The current `value`
implementation doesn't support recycling.

**Value allocation algorithm**: For uniquely allocating references using
`yvalue_create()`.

The value allocation runs as third stage after resolving built-ins and
processing function definitions. Only symbolic references, i.e. ones defined by
the parser, are resolved.

The algorithm uses a stack of lists, each list represents the list of
already-allocated values in a scope, and for each new scope, a new list is
pushed onto the stack. Each `let` or `defn` results in pushing a new reference
to the list or redefining an existing one; each reference to a value results in
looking up the value in the top-most list, then the list below, etc. until a
reference could be found (at which point the symbolic reference is translated
into a numeric reference).

A new scope is started for every list.

* Scan program tree
    * Encounter symbolic reference (an ID)? -> Look up in stack of scope-lists by
    walking each list in the stack until one is found. If none is found, this is
    an error.
    * Encounter new list: Push new scope onto stack.
    * Leave scope: Pop scope list from stack.

(see `preprocess.c`)

TODO: Garbage collection, slot recycling (free-slot-list), reference counting?

### Built-in functions and constructs

Built-in functions or constructs such as `let` and `for` are represented by a
normal `yexpr_t` value of type `YEXPR_BUILTIN` with the `builtin` field in the
inner union being set. Refer to `expr.h` for the complete list of built-ins.

Built-ins are resolved in the first preprocessor step, before symbols are
resolved.

Note that `defn` is a built-in, but is not expected to be present in the
processed program tree. All function definitions should be stored immutably and
be referenced from the program as IDs. They are not defined during execution
phase. See the section below on how `ypreprocess_defn()` handles `defn`
expressions.

## Function calls

`ypreprocess_refs()` extracts function definitions, stores them as values in the
value table, and replaces them with `undef` expressions.

The source code contains references to the arguments by name; the `defn`
preprocessing resolves all references to function arguments to "stack-refs",
meaning `yref_t` values of type `YREF_STACK`. Those references contain a number
referencing the function argument relative to the top of the stack (0 is the
top-most stack value, 1 the one below, etc.).

`yeval()` detects when an expression should be a function call. It is evaluated
as such if the first element of a list is a reference to a function or a
reference to a value containing a reference to a function:

```
(defn f (a) (+ a a))
(let g f)
(g 3)
-- 6
(f 3)
-- 6
```

For function calls, `yeval_call_func()` evaluates each argument and pushes it
onto the call stack (which is part of the `yeval_state_t` struct given to
evaluation functions). When the function body is evaluated from there, the
stack-refs will be resolved by `yeval` to the respective stack values. The
values on the stack are freed after the function body has been evaluated.

Built-ins are a different story; they are called with the whole built-in
expression on the stack as only value.

## Evaluation

-> see `yeval()` in `eval.h`.

Evaluation happens recursively. Each evaluation results in a new `yexpr_t`
value.

### In-place evaluation

Often, the result of an evaluation is not needed afterwards, because all that
was wanted are the side effects. In this case, it is not sensible to allocate
memory for the resulting expression.

The `in_place` option can be set in `yeval()` to avoid allocating memory to
store the result in. The evaluated expression remains untouched.

It may ONLY be set if

* `yeval()` evaluates expressions within a function that are not the last
expression (and thus don't need to be returned to the caller).
* `yeval()` is called with `in_place` already set.

## REPL

You can use the REPL binary `src/repl` to test out ylisp for yourself.