Arete is an experimental programming language. It aspires to the following design principles:
-
safety: no undefined behavior. All safety errors are either caught at compile time or at runtime. For many kinds of errors, the programmer can control when they want the checking (that is, Arete will provide gradual typing).
-
separate compilation: libraries (including generic ones) can be separately compiled and linked with applications. This will ensure scalable and efficient compilation of applications and libraries with large software dependencies.
-
efficient runtime: runtime overheads will in general be low and under control of the programmer. Memory management is not via garbage collection, but is semi-automatic and under programmer control. Specialization/monomorphization of generics will be available as a compiler optimization.
-
high power-to-weight ratio: the language will have a relatively low number of features, but those features will be powerful and work well in combination.
The Arete language is being designed using an incremental prototype-first approach. The best way to evaluate a language design is to write and run lots of programs in the new language. The Arete abstract machine (the prototype) is a low-cost way to precisely express the language design and to run programs.
Arete currently includes the following features
-
parallelism (via futures)
-
controlled mutability for data-race freedom, memory safety, etc. via dynamic tracking of fractional permissions.
-
semi-automatic memory management via fractional permissions
-
gradual typing (but not yet sound gradual typing, which is planned)
-
modules
and I plan to add:
-
static checking of fractional permissions,
-
generics with contraints and lexically scoped implementationns,
-
more features for asynchronous and parallel programming.
The design of Arete is currently captured in a prototype implementation written in Python 3.10 that includes:
-
grammar and parser (Arete.lark and parser.py) using the Lark parser generater.
-
constant evaluation (const_eval.py)
-
type checking (type_check.py)
-
interpreting (an abstract machine) (machine.py)
The design of Arete is based on discussions with Dave Abrahams and Dimitri Racordon about their Val language and it is influenced by the design of the Carbon language. The organization of the abstract machine is based on Carbon Explorer.
There are lots of small example programs in the tests directory, including double-linked lists, binary trees, and parallel merge sort.
Here we look at a couple examples that demonstrate how mutation is controlled via fractional permissions in Arete.
Mutable variables are declared with var
and immutable ones are
declared with let
. For example, the following program runs without
error and returns 0
.
fun main() {
var x = 42;
let y = 40;
x = x - y;
return x - 2;
}
Both var
and let
variables are by-reference (in C++ lingo); they
are aliases for the value produced by their initializing expression
(like all variables in Java). In the following, we initialize y
with x
and declare y
to also be a mutable variable. We then write
0
to y
and try to return x + y
.
fun main() {
var x = 42;
var y = x;
y = 0;
return x + y;
}
This program halts with an error when we try to read x
in x + y
,
saying that the pointer (associated with x
) does not have read
permission. In Arete, each pointer has a fraction to control whether
it is allowed to read or write. A pointer with positive fraction may
read and a pointer with a fraction of 1
may write. The pointer
associated with variable x
starts out with a permission of 1
, but
when we initialize y
with x
, all of its permission is transfered
to the new pointer for y
. So the write to y
executes successfully,
but the later read of x
is an error because by that time, x
has
0
permission.
Getting back to the first example, we discuss memory allocation and deallocation.
fun main() {
var x = 42;
let y = 40;
x = x - y;
return x - 2;
}
When x
is initialized with 42
, memory for 42
is allocated and
the resulting pointer is associated with x
(with permission
1
). Likewise for when y
is initialized with 40
. The variables
x
and y
go out of scope at the end of the function, after the
return
. When they go out of scope, the associated pointers are
killed, and because those pointers have permission 1
, the memory
at their address is deallocated.
In this way, memory deallocation is often handled automatically in Arete. However, when dealing with data structures that contain cycles, some manual effort is needed. The following example creates a tuple that contains a pointer to itself.
fun main() {
var x = ⟨0, 1⟩;
x[1] = &x;
return x[0];
}
The x
goes to 0 permission on the assignment x[1] = &x
, so when
x
goes out of scope, the tuple does not get deallocated.
So this program halts with an error.
To run an Arete program, run machine.py on python3.10
with the file name of the Arete program:
python3.10 ./machine.py <filename>
The result of the program is in the exit code. So the following shell command will display it:
echo $?
To debug an Arete program, add the debug
flag:
python3.10 ./machine.py <filename> debug
The interpreter will process all the definitions and then pause as it is
about to call your main
function. You can then enter one of the
following single-character debugger commands:
-
f
evaluate until the current AST node is finished. -
n
evaluate to the next subexpression, but don't dive into function calls. -
s
step to the next subexpression, diving into function calls. -
d
dive into the next function call. -
e
print the current environment (the in-scope variables). Each line displays the name of the variable, the contents of the variable, and the address (a pointer) of the variable. -
m
print the machine's memory. Each line displays the address (an integer) followed by the value stored at that address. -
g
output a graphviz dot file (namedlogs/env_mem_nnn.dot
) that represents the environment and memory as a graph. You can then use graphviz to generate a PDF. -
v
toggle verbose printing -
q
quit
Breakpoints can be inserted by editing the Arete program to add a call
to the breakpoint()
primitive function. To express a conditional
breakpoint, place the call to breakpoint()
inside an if
.
This is the current specification of the Arete language.
Table of Contents
-
-
- Array Creation
- Index into an Array
-
- Address Of
- Delete
- Dereference a Pointer
- Null Pointer Literal
- Transfer Permission
- Write (Assignment)
-
- Index into a Tuple
- Tuple Creation
The Arete language has three main syntactic categories: expressions, statements, and definitions. At runtime, an expression produces a value (a piece of data). Most statements perform side effects; some statements associate a value with an identifier. Most definitions associate a value with an identifier. A program is a sequence of zero or more definitions:
<definition_list> ::= | <definition> <definition_list>
This specification is organized according to major language features, such as functions, threads, arrays, modules, etc. In the section for each major language feature, we define its grammar rules, type checking rules, and runtime behavior.
In this document we give simplified grammar rules for the language,
that for example, leave out the encoding of precedence. For the exact
grammar rules, see Arete.lark. The parser for Arete
produces an abstract syntax tree (AST) where each node is represented
by a Python object. The class definitions for these objects are
organized according to language feature, for example, the Function
AST class for representing function definitions is defined in the file
functions.py
, along with other AST classes related
to functions, such as Call
and Lambda
.
We describe the runtime behavior of an Arete program in terms of a
machine. The precise definition of the machine is in
machine.py, and it dispatches to the step
methods
defined in each AST class. So the precise specification of the runtime
behavior of each language feature is given by the step
method of the
corresponding Python class.
Program execution begins by invoking the declare
method on every
definition. For most definitions, this allocates an empty cell in
memory and associates the name of the definition with its
address. (See the declare
method of the Decl
base class in
ast_base.py
.) Exceptions to this behavior are
discussed with the description of the particular kind of
definition. The primary purpose of this first phase is to enable
recursive definitions.
The machine then proceeds to interpret each definition by invoking its
step
method.
Once all the definitions have been interpreted, the machine calls the
function named main
with no arguments. Once the execution of main
is finished, the program exits with the return value of main
,
provided the machine's memory is empty. If the memory is non-empty,
then the program halts with an error.
The machine has
-
memory
that maps an address (integer) to a value, -
collection of
threads
including thecurrent_thread
and themain_thread
. -
return_value
which eventually holds the value returned by themain
function.
Each thread has
stack
of frames (the procedure call stack), with one frame for each function call.return_value
which eventually holds the value produced by the thread.parent
the thread that spawned this thread (optional).num_children
the number of threads spawned by this thread that are still running.
Each frame has a stack named todo
of node runners.
A node runner is responsible for executing one node in the abstract syntax tree of the program. Think of each node runner as a little state machine. (It's named "node runner" because I enjoyed playing the Lode Runner video game in the 1980's.) Each node runner has
-
ast
(the AST node) -
state
an integer to indicate how far along the node runner is in executing this AST node. -
results
a list of results for the subexpressions of this AST node. Each result consists of a value and a Boolean flag that says whether the value was newly created by the expression (it is a temporary) or not. -
context
specifies whether the expression should be evaluated for its value (aka. rvalue) viaValueCtx
or its address (aka. lvalue) viaAddressCtx
. The context also specifies whether toduplicate
the result value (the default is yes, duplicate the pointer). -
return_value
is to propagate the value from areturn
statement. -
return_mode
(the stringsvalue
oraddress
) is whether the enclosing function expects to return a value or address. -
env
an environment, that is, a dictionary that maps all the in-scope variables to their addresses.
We define the following verbs, i.e., operations that the machine can
perform. (These correspond to methods in the Machine
class.)
The current frame is the frame at the top of the stack
of the
current thread.
The current runner is the node runner at the top of the todo
stack of the current frame.
The current environment is the environment (env
) of the current
node runner.
Inputs: AST node, environment, context (defaults to value at 50%), return mode (optional).
-
Create a node runner with the given inputs. If the return mode is not given, use the return mode of the current runner.
-
Push the node runner onto the
todo
stack of the current frame. -
Return the node runner.
Inputs: result
-
Kill all the temporary values in the
results
of the current runner. -
Pop the current runner from the
todo
stack of the current frame. -
If the
todo
stack of the current frame is not empty, push theresult
onto theresults
of the current node runner. -
Otherwise, if the
stack
of the current thread is not empty, pop the current frame from thestack
and then set thereturn_value
of the current runner to the value of theresult
. -
Otherwise, set the
return_value
of the current thread to the value of theresult
.
-
Let
val
be thereturn_value
of the current runner. -
Kill all the temporary values in the
results
of the current runner. -
Pop the current runner from the
todo
stack of the current frame. -
If the
todo
stack of the current frame is not empty, set thereturn_value
of the current runner toval
. -
Otherwise, if the
stack
of the current thread is not empty, pop the current frame from thestack
and then set thereturn_value
of the current runner toval
. -
Otherwise, set the
return_value
of the current thread to theval
.
-
Kill all the temporary values in the
results
of the current runner. -
Pop the current runner from the
todo
stack of the current frame.
Inputs: parameter
, result
, environment
-
If the value of the
result
is not a pointer or pointer offset, halt with an error. -
If the
result
is a temporary, update theenvironment
to associate the parameter's identifier with the value of theresult
. -
If the
parameter
is alet
do the following:a. If the
address
of the pointer isNone
or its permission is0
, halt with an error.b. If the
result
is not a temporary, update theenvironment
to associate the parameter's identifier with a duplicate of the value of the result, taking 50% of its permission. -
Otherwise, if the
parameter
is avar
orinout
, do the following:a. If the
address
of the pointer isNone
or its permission is not1
, halt with an error.b. If the
result
is not a temporary, update theenvironment
to associate the parameter's identifier with a duplicate of the value of the result, taking 100% of its permission.c. If the
parameter
is avar
, set theno_give_backs
flag to true on the pointer that just added to theenvironment
. -
Otherwise, if the
parameter
is aref
andresult
is not a temporary, update theenvironment
to associate the parameter's identifier with a duplicate of the value of the result, taking 100% of its permission.
Inputs: parameter, argument value, environment
-
Let
ptr
be the value associated with the parameter's identifier in the given environment. -
If the
parameter
isinout
, do the following:a. If the permission of
ptr
is not1
, halt with an error.nb. If the argument value is not live, halt with an error.
c. Transfer all of the permission from
ptr
to the argument value. -
Kill the
ptr
.
Inputs: pointer
-
If the pointer has
0
permission, halt with an error. -
Obtain the value
val
at the pointer'saddress
in memory. Process theval
and the pointer'spath
recursively as follows.-
If the
path
is an empty list, then returnval
. -
If the
path
is non-empty, check thatval
is a tuple and halt with an error if not. Recusively process thei
th element ofval
(wherei
ispath[0]
) withpath[1:]
.
-
Inputs: pointer, value
-
If the pointer does not have
1
permission, halt with an error. -
Let
old_val
be the value obtained by reading from memory with the given pointer (see the above Read operation). -
Let
val_copy
be a duplicate of the input value. -
Update the location in memory for the pointer's address with a new whole value obtained by splicing
val_copy
into the place whereold_val
was in the old whole value at the pointer'saddress
. To be precise, recursively process the value at the pointer'saddress
(call itval
) and the pointer'spath
as follows to produce a new value as follows.-
If the
path
is an empty list, thenval_copy
is the new value. -
If the
path
is non-empty, check thatval
is a tuple and halt with an error if not. Recursively process thei
th element ofval
(wherei
ispath[0]
) withpath[1:]
to obtainnew_val
. The new value is then constructed by creating a tuple whose elements are obtained by concatenatingval.elts[:i]
, thenew_val
, andval.elts[i+1:]
.
-
Inputs: address
-
Kill the value that is in memory at the given address.
-
Delete the entry for this address in the memory.
<statement> ::= assert <expression>;
The type of expression
must be consistent with bool
.
-
Schedule the
expression
in the current environment with value context and duplication. -
If the result is
false
, halt with an error. -
Otherwise, finish this statement.
<statement> ::= { <statement_list> }
Perform type checking on each statement in the block.
-
Schedule the body in the current environment.
-
Finish this statement.
<statement> ::= <expression>;
Perform type checking on the expression
.
-
Schedule the
expression
in the current environment with value context and duplication. -
Finish this statement.
<statement> ::= if (<expression>) <block>
<statement> ::= if (<expression>) <block> else <block>
The one-armed if
is parsed into a two-armed if
whose
else branch is a Pass statement.
The type of expression
must be consistent with bool
.
Perform type checking on the branches.
To interpret a two-armed if
:
-
Schedule the condition
expression
in the current environment with value context and duplication. -
If the result is true, schedule the then branch.
-
Otherwise, if the result is false, schedule the else branch.
-
Finish this statement.
- Finish this statement.
<expression> ::= <prim-op> ( <expression_list> )
See the type_check_prim
function in
primitive_operations.py
.
-
Schedule each argument
expression
. -
Compute the result value according to the function
eval_prim
in primitive_operations.py with the argument results and theprim-op
. -
If the current runner's context is address context, allocate the result value in memory and let
result
be the new pointer. (A temporary). Otherwise, we're in value context and letresult
be the result value. (Also a temporary.)
<type> ::= rec <identifier> in <type>
The identifier
may occur inside the type
expression and represents
the whole type
. For example, the following type could be used to
represent a singly-linked list of integers. Each node is a tuple whose
first element is an integer and whose second element is a pointer to
another node.
rec X ⟨ int, X* ⟩
(Recursive types are unweildy to deal with directly, so the plan is to add syntactic sugar for them.)
<type> ::= <identifier>
An occurence of a type variable may refer to a recursive type. (See the entry for Recursive Type.)
<statement> ::= while (<expression>) <block>
The type of expression
must be consistent with bool
.
-
Schedule the condition
expression
in the current environment with value context and duplication. -
If the result of the condition is true, schedule this
while
statement again and then schedule theblock
. (We schedule theblock
after thiswhile
statement because the machine treats thetodo
list as a stack, that is, in last-in-first-out order.) -
Finish this statement.
There are several auxiliary grammar rules related to variable and parameter definitions.
<parameter> ::= <binding_kind> <identifier> [: <type>]
The parameter
category is used for function parameters and variable
definitions (e.g. the let
statement). If no type annotation is
present, the parameter is given the unknown type ?
.
<binding_kind> ::= | let | var | inout | ref
If no binding kind is specified, it defaults to let
.
<initializer> ::= <expression> | <expression> of <expression>
An initializer specifies what percentage of the permission is taken
from the result value of the given expression. For example, the
following initializes variable y
to be an alias of variable x
,
taking 50% of its permissions.
let y = 1/2 of x;
If no percentage is specified and the context of the current node
runner is an AddressCtx
, then use that context's
percentage. Otherwise use 50%.
<definition> ::= const <identifier> [: <type>] = <expression>;
UNDER CONSTRUCTION
The occurences of identifier
in the program are replaced by the
result of evaluating expression
. (See
const_eval.py.)
- Finish this declaration.
<definition> ::= type <identifier> = <type>;
The result of simplifying type
is associated with identifier
in
the type environment.
- Finish this declaration.
<definition> ::= let <identifier> [: <type>] = <expression>;
UNDER CONSTRUCTION
-
Schedule the
expression
in a value context with duplication. -
Write the resulting value to memory using the pointer associated with
identifier
in the current environment. -
Finish this definition.
<statement> ::= <parameter> = <expression>; <statement_list>
UNDER CONSTRUCTION
-
Schedule the
expression
in the current environment with address context and duplication. -
Copy the current environment and name it
body_env
. -
Bind the result from
expression
to the parameter in thebody_env
. -
Schedule the following
statement_list
in the environmentbody_env
. -
Once the
statement_list
is finished, deallocate the parameter with the result from theexpression
and thebody_env
. -
Finish this statement.
<expression> ::= <identifier>
Check that the identifier is in the static environment to obtain its
static info
and report a static error if it is not.
If the context is a let
binding, then check that the info
state is
readable and report a static error if it is not. Update the info
state to be a proper fraction and add this identifier to the current
set of borrowed variables.
If the context is an inout
binding, then check that the info
state
is a full fraction and report a static error if it is not. Update the
info
state to be an empty fraction and add this identifier to the
current set of borrowed variables.
If the context is var
binding, then check that the info
state is a
full fraction and report a static error if it is not. Update the
info
state to be dead.
If the context is a ref
binding, UNDER CONSTRUCTION
If the context is the left-hand side of an assignment statement, check
that the info
state is a full fraction and report a static error if
it is not.
If the context is the right-hand side of an assignment statement,
check that the info
state is readable and report a static error if
it is not.
The type of this identifier is the type
field of info
.
The translation is the translation
field of info
unless it is
None
, in which case the translation is this identifier.
-
If the identifier is not in the current environment, halt with an error.
-
If the current runner's context is a value context, read from memory using the identifier's pointer (from the current environment). If the current runner's context requests duplication, let
result
be a duplicate of the value in memory, taking the percentage specified by the identifier's pointer. (A temporary.) Otherwise, letresult
be the value in memory. (Not a temporary.) -
Otherwise the current runner's context is an address context. Return the identifier's pointer. (Not a temporary).
-
Instruct the machine to finish this expression with the
result
.
<type> ::= [ <type> ]
<expression> ::= [ <expression> of <expression> ]
The following program creates an array a
of length 10
that is
initialized with 0
. It then writes the integers 0
through 9
in
the array and finishes by accessing the last element.
fun main() {
let n = 10;
var a = [n of 0];
var i = 0;
while (i < len(a)) {
a[i] = i;
i = i + 1;
}
let last = a[9];
return last - 9;
}
UNDER CONSTRUCTION
-
Schedule the size
expression
in value context with duplication. The result must be a integer, call itn
. -
Schedule the initial
expression
in value context with duplication. -
Duplicate the initial value
n
times, taking 50% permission each time. Create a tuple value whose elements are these duplicates. -
If the current runner's context is value context, let
result
be the tuple value. Otherwise, we're in address context, so we allocate the tuple value in memory and letresult
be the new pointer. In either case, the result is a temporary. -
Finish this expression with
result
.
<expression> ::= <expression> [ <expression> ]
UNDER CONSTRUCTION
-
Schedule the first
expression
in the current environemnt in address context with the duplication specified by the current runner. Letptr
be the result. -
Schedule the second
expression
in a value context with duplication. Letindex
be the result, which must be an integer. -
If the current node runner's context is a value context with duplication, let
arr
be the array obtained by reading from memory atptr
. Ifptr
is a temporary, then letresult
be a duplicate of the element atindex
ofarr
(which should also be considered a temporary), taking a percentage of the element according to the permission ofptr. If
ptris not a temporary, let
resultbe the element at
indexof
arr`. -
If the current node runner's context is an address context, create a pointer offset whose offset is
index
and whose underlying pointer is either theptr
(if it was not a temporary) or a duplicate ofptr
(if it was a temporary). Letresult
be the new pointer offeset. We categorize it as temporary if theptr
was temporary. -
Finish this expression with
result
.
The values true
and false
have the type bool
.
<type> ::= bool
<expression> ::= false
<expression> ::= true
A function value (aka. closure) includes information from the originating function (name, parameters, body, etc.) and the environment from its point of definition.
A function type includes a list of parameter types and the return type.
<type> ::= ( <type_list> ) -> <type>
A comma-separated list of types is a type_list
.
<type_list> ::= <type> | <type> , <type_list>
<definition> ::= fun <identifier> (<parameter_list>) [-> <type>] [<return_mode>] <block>
UNDER CONSTRUCTION
-
Create a function expression AST node and schedule it in the current environment with value context and duplication.
-
Write the resulting value to memory at the address associated with the function's name in the current environment.
-
Finish this definition.
<expression> ::= fun ( <parameter_list> ) <return_mode> <block>
UNDER CONSTRUCTION
The free variables of a function are those variables that occur
inside block
without an enclosing variable binding in the block
or
by this function's parameters.
- Create an environment named
clos_env
with a duplicate (50%) for each free variable of the function. - Create a closure value with the
clos_env
and the other information from this function (parameter list, return mode, and body). - If the current node runner's context is a value context,
let
results
be the closure. - If the current node runner's context is an address context,
allocate the closure in memory and let
result
be its pointer. - Finish this expression with
result
.
(There are plans to provide a way to capture free variables that are mutable or to capture a variable's value.)
<expression> ( <initializer_list> )
UNDER CONSTRUCTION
-
Schedule the
expression
in the current environment with value context and duplicaton. The result must be a closure. -
Schedule each
initializer
(left to right) in the current environment with address context and duplication. -
Copy the closure's environment into
body_env
. Bind each parameter to the corresponding result from its initializer. -
Create a new frame and push it onto the
stack
of the current thread. -
Schedule the body of the closure with the environment
body_env
and the closure's return mode. -
Upon completion of the body, deallocate each parameter. If the current node runner's
return_value
isNone
, set it to the void value.a. If the current node runner's context is a value context, and its return mode is
value
, letresult
be the runner'sreturn_value
. If the runner's return mode isaddress
, then the runner'sreturn_value
is a pointer; letresult
be the value read from memory at that pointer. Kill the pointer.b. If the current node runner's context is a address context, and its return mode is
value
, allocate the runner'sreturn_value
in memory and letresult
be the new address. If the return mode isaddress
, letresult
be the runner'sreturn_value
. -
Finish this expression with
result
.
<statement> ::= return <expression>;
UNDER CONSTRUCTION
-
Schedule the
expression
in the current environment with duplication using value or address context as specified by thereturn_mode
of the current node runner. -
Set the
return_value
of the current node runner to a duplicate (at 100%) of the expression's result. -
Finish this statement.
UNDER CONSTRUCTION
<type> ::= ?
This type enables gradual typing because an expression of unkown type
?
may produce any type of runtime value.
<type> ::= <identifier>
An occurence of a type variable may refer to a type parameter of a generic entity.
A module has a name, an environment of exported members, and an environment of all its members. Modules are compile-time entities, they are not (runtime) values.
<definition> ::= from <expression> import <identifier_list>;
UNDER CONSTRUCTION
The declare
method of an import allocates an empty cell in memory
for each name being imported, and associates the name with the cell's
address in the current environment.
-
Schedule the
expression
in the current environment with address context and duplication. -
Read from memory at the resulting pointer, which must produce a module. For each of the names in the
identifier_list
of the import, check whether it is in the module's exports and halt with an error if it is not. Otherwise, read from memory using the pointer associated with the name in the export environment of the module. Then duplicate the value, taking the percentage of the pointer's permission. Write the duplicated value to memory, using the pointer associated with the name in the current environment. -
Finish this definition.
<expression> ::= <expression> . <identifier>
UNDER CONSTRUCTION
-
Schedule
expression
in the current environment with address context with duplication. -
Read from the resulting pointer, which must produce a module value. If
identifier
is not a name exported by the module, halt with an error. Otherwise, letptr
be the associated pointer for theidentifier
in the module's exports. -
If the current node runner's context is a value context, let
result
be a duplicate of the value atptr
in memory, taking a percentage according to the permission ofptr
. Categorize this result as a temporary. -
If the current node runner's context is an address context, let
result
beptr
. (Not a temporary.) -
Finish this expression with
result
.
<definition> ::= module <identifier> exports <identifier_list> { <definition_list> }
UNDER CONSTRUCTION
-
Let
body_env
be a new empty environment. -
Invoke the
declare
method on each definition in the module, with thebody_env
. -
Schedule each definition in the module with the
body_env
. -
For each identifier in the list of exports, check that there is an entry in
body_env
. -
Create a module value. It's
exports
environment maps each name in the module's export list to the pointer for that name inbody_env
. Themembers
environment of the module isbody_env
. -
Write the module value to the address associated with its name in the current environment.
-
Finish this definition.
The numbers currently include integers and rationals.
<type> ::= int
<type> ::= rational
<expression> ::= <integer>
UNDER CONSTRUCTION
-
If the current node runner's context is a value context, let
result
be theinteger
. -
If the current node runner's context is an address context, allocate the
integer
in memory and letresult
be the allocated pointer. -
Finish this expression with
result
.
UNDER CONSTRUCTION
A pointer value includes the following fields:
address
(an integer)path
(list of integers)permission
(fraction)lender
(another pointer, optional)kill_when_zero
(a Boolean)no_give_backs
(a Boolean)
If the value at address
in memory is a tuple, then the path
specifies which part of the tuple this pointer is referring to.
The path is a list and not just a single integer because tuples
can be nested. So, for example, the path [3,2]
refers to
the location of 12
in the tuple ⟨0,1,2,⟨10,11,12,13⟩,4,5⟩
.
A pointer whose permission
is 0
or greater can be copied.
A pointer whose permission
is greater than 0
can be used for
reading.
A pointer whose permission
is 1
can be used for writing.
If the pointer is a copy of another pointer, then its lender
is that
other pointer.
If the kill_when_zero
flag is set to true, then the pointer is
killed when its permission
reaches zero. This used for let
variables.
The if no_give_backs
flag is set to true, then when the pointer is
killed, it does not give its permissions back to its ender (as it
normally would). This is used for var
variables.
A null pointer is a pointer whose address
is None
.
We define the following operations on pointers.
We define a live pointer to be a pointer whose address
field
contains an integer (and not None
). If a pointer is not live,
it's dead.
We define the living lender of a pointer P
to be the first live
pointer in the chain of pointers obtain by following the lender
fields starting with pointer P
.
-
If the pointer has a living lender, then transfer all of this pointer's permission to the living lender. (Unless the
no_give_backs
flag is set to true.) -
If the pointer does not have a living lender
a. if its permission is
1
, delete the memory at itsaddress
.b. if its permission is greater than
0
but less than1
, halt with an error. -
Set the
address
of this pointer toNone
.
Inputs: percentage
-
If the pointer is null, return a null pointer.
-
If the pointer is alive, create a new pointer with the same address and path and transfer the given percentage from this pointer to the new pointer. Return the new pointer.
If the pointer has a living lender, transfer all of the lender's
permission to this pointer. Return true
or false
corresponding to
whether this pointer's permission is equal to 1
.
We sometimes need to delay the duplication of a pointer when using it to index into a tuple, which we accomplish with another pointer-like value called a pointer offset, which has the following fields.
ptr
(a Pointer)offset
(an integer)
A pointer offset acts like its underlying ptr
but with the given
offset
appended to the end of its path
.
<expression> ::= & <expression>
UNDER CONSTRUCTION
-
Schedule
expression
in the current environment with address context with duplication. -
If the current runner's context is value context, do the following. If the result of
expression
was a temporary, duplicate it and setresult
to the duplicate as a new temporary. If the result ofexpression
was not temporary, setresult
to the result ofexpression
. -
Otherwise, if the current runner's context is address context, allocate the result's value in memory and set
result
to the new pointer (it's a temporary). -
Finish this expression with
result
.
<statement> ::= delete <expression>;
UNDER CONSTRUCTION
-
Schedule the
expression
in the current environment with value context and duplication. The result must be a pointer. -
Deallocate the memory associated with the pointer, set its
address
toNone
and its permission to0
. -
Finish this statement.
<expression> ::= * <expression>
UNDER CONSTRUCTION
-
Schedule the
expression
in value context and with the current runner's duplication. The result must be a pointer -
If we're in value context, read from memory at the pointer and duplicate it, taking the percentage according to the permission of the pointer. Let
result
be the duplicate. (It's a temporary.) -
Otherwise, we're in address context. If the pointer is a temporary, let
result
be a duplicate of it. Otherwise letresult
be the pointer. -
Finish this expression with
result
.
<expression> ::= null
This is parsed as a call to a primitive function named null
.
<statement> ::= <expression> <- <expression> of <expression>
Let amount
be the permission of the source pointer multiplied by the
given percentage. Decrease the permission of the source pointer by
amount
and increase the permission of this pointer by amount
. If
the permission of the source pointer becomes 0
and its
kill_when_zero
flag is true, then kill it.
<statement> ::= <expression> = <initializer>;
UNDER CONSTRUCTION
-
Schedule the
initializer
in the current environment with value context and duplication. -
Schedule the
expression
in the current environment with address context and duplication. The result must be a pointer. -
Write the result of the initializer to the pointer.
-
Finish this statement.
(The order of evaluation here does not follow our usual left-to-right policy. Using left-to-right for assignment triggers errors in many of our test cases. I have not yet investigated why this is.)
UNDER CONSTRUCTION
-
Schedule the target
expression
in the current environment with value context and without duplication. The result value must be a pointer. -
Schedule the percentage
expression
in the current environment with value context and duplication. The result value must be a rational number. -
Schedule the source
expression
in the current environment with value context and without duplication. The result value must be a pointer. -
Transfer the given percent from the source pointer to the target pointer.
-
Finish this statement.
A future value has an associated thread.
<expression> ::= spawn <expression>
Evaluate the expression
in a new thread, concurrent with the current
thread. Immediately returns a future associated with the new
thread. This future can later be passed to wait
(see below the
description for await
).
UNDER CONSTRUCTION
Inputs: an expression and environment
-
Create a node runner with the given expression and environment, with a value context and the same return mode as the current runner.
-
Create a new frame whose
todo
list just includes the new runner. -
Increment the
num_children
of this thread. -
Create a new thread whose
stack
just includes the new frame and whoseparent
is the current thread. -
Append the new thread to the
threads
of the machine. -
Return a new future associated with the new thread.
<expression> ::= wait <expression>
UNDER CONSTRUCTION
The expression
evaluates to a future, then the current thread
blocks until the future's thread is finished. The result of this
wait
is the result of the future's thread.
A tuple is a value that contains a sequence of values called its elements. The elements of a tuple can be accessed with zero-based indexing.
The following tuple contains six values, and the element at index 3 is another tuple.
⟨3,true,2,⟨10,false,12,13⟩,4,5⟩
<type> ::= ⟨ <type_list> ⟩
TODO: discuss the operations on tuples.
<expression> ::= ⟨ <initializer_list> ⟩
UNDER CONSTRUCTION
-
Schedule each
initializer
(left-to-right) in the current environment with value context with duplication. -
Create a tuple value whose elements are duplicates (at 100%) of the results of the initializers.
-
If the current node runner's context is a value context, let
result
be the tuple. (A temporary.) -
If the current node runner's context is an address context, allocate the tuple in memory and let
result
be a pointer to that memory. (A temporary.) -
Finish this expression with
result
.
<expression> ::= <expression> [ <expression> ]
UNDER CONSTRUCTION
-
Schedule the first
expression
in the current environemnt in address context with the duplication specified by the current runner. Letptr
be the result. -
Schedule the second
expression
in a value context with duplication. Letindex
be the result, which must be an integer. -
If the current node runner's context is a value context with duplication, let
tup
be the tuple obtained by reading from memory atptr
. Ifptr
is a temporary, then letresult
be a duplicate of the element atindex
oftup
(which should also be considered a temporary), taking a percentage of the element according to the permission ofptr. If
ptris not a temporary, let
resultbe the element at
indexof
tup`. -
If the current node runner's context is an address context, create a pointer offset whose offset is
index
and whose underlying pointer is either theptr
(if it was not a temporary) or a duplicate ofptr
(if it was a temporary). Letresult
be the new pointer offeset. We categorize it as temporary if theptr
was temporary. -
Finish this expression with
result
.