changeset 136:b930e16125fb

doc: Update documentation about function calls
author Lewin Bormann <lbo@spheniscida.de>
date Sun, 01 Sep 2019 19:35:40 +0200
parents a26a197afe24
children 08d010255027
files doc/execution.md
diffstat 1 files changed, 30 insertions(+), 33 deletions(-) [+]
line wrap: on
line diff
--- a/doc/execution.md	Sun Sep 01 19:26:44 2019 +0200
+++ b/doc/execution.md	Sun Sep 01 19:35:40 2019 +0200
@@ -40,14 +40,17 @@
 ## Preprocessing
 
 This section describes how the parsed syntax tree is processed before
-evaluation.
+evaluation. It consists of several stages, described in `preprocess.h` and
+`preprocess.c`.
 
 ### Variable/reference resolution
 
-Symbols in the source code should be 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.
+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`
@@ -56,6 +59,10 @@
 **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
@@ -76,10 +83,6 @@
     into the list of the parent scope.
     * Leave scope: Pop scope list from stack.
 
-Function definitions (`defn`) are then removed from the syntax tree
-(respectively replace by no-ops) as they are not used anymore. `let` definitions
-are kept, as they are executed every time they are encountered.
-
 TODO: Garbage collection, slot recycling (free-slot-list), reference counting?
 
 ### Built-in functions and constructs
@@ -94,36 +97,30 @@
 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.
+phase. See the section below on how `ypreprocess_defn()` handles `defn`
+expressions.
 
 ## Function calls
 
-During preprocessing, function definitions are found, references in them are
-resolved for the local or parent scope, and the resulting s-expr is stored as
-function expression in the value table. The parent scope is given a reference to
-the function value, which (as `yfunc_t`) includes a description of the
-function's arguments. Subsequent calls are resolved as usual to a numeric ID.
-Calls shouldn't be checked, as assignments can result in a different function
-being stored at runtime when the actual call happens. The `defn` is removed from
-the syntax tree.
+The `ypreprocess_defn()` stage only looks at `defn` expressions. For every
+`defn` it encounters, it creates a new function reference (in the global values
+table, see `value.h`) describing the function with its name, arguments, and
+body. The body is stored as vector of expressions that can be evaluated.
 
-A function is a stored `yexpr_t` tree value. Essentially the same as a normal
-value, but with a few descriptions attached, like the number of arguments it
-expects and the numeric references that it expects arguments in. Before calling,
-expressions are copied into those references.
-
-Arguments are passed by value, so expressions are cloned into the arguments'
-slots. The arguments are described in the `yfunc_t` struct.
+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.).
 
-The return value is the value of the last expression.
-
-All this logic happens within `yeval()`; it binds arguments when calling
-functions and handles the return value.
+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-in functions are called with arguments on the call stack. Those arguments
-are shallow copies from the expressions, so built-ins should not modify them
-except where it's guaranteed not to have side effects (scalar values, for
-example).
+Built-ins are a different story; they are called with the whole built-in
+expression on the stack as only value.
 
 ## Evaluation