did some cleaning of unused stuff and added docs

This commit is contained in:
cozis
2022-01-22 03:18:34 +01:00
parent 4dfc213c71
commit e6e3efe0e2
6 changed files with 84 additions and 229 deletions
+4 -10
View File
@@ -2,20 +2,14 @@
## Introduction ## Introduction
This language was written as a personal study of how interpreters and compilers work. For this reason, the language is very basic. One of the main inspirations was the CPython's source code since it's extremely readable and has a very simple and clean architecture. This language was written as a personal study of how interpreters and compilers work. For this reason, the language is very basic. One of the main inspirations was CPython's source code.
## Implementation overview ## Implementation overview
The interpreter works by compiling the provided source to a bytecode The architecture is pretty much the same as CPython.
format and executing it. The bytecode is very high level since it does things like: The source code is compiled to a bytecode format and executed. The bytecode is very high level since it does things like explicitly referring to variables by name, which means the compiler does very little static analisys and errors are moved from the compile to run time.
Memory is managed by a garbage collector that moves and compacts allocations.
- explicitly referring to variables by name.
- treating values as atomic things: from the perspective of the
bytecode, a list and an integer occupy the same space on the
stack, which is 1.
- referring to instructions by their index.
All values (objects) are allocated on a garbage-collected heap. All variables are simply references to these objects. The garbage collection algorithm is a copy-and-compact. It behaves as a bump-pointer allocator until there is space left, and when space runs out, it creates a new heap, copies all of the alive object into it, calls the destructors of the dead objects and frees the old one. All values (objects) are allocated on a garbage-collected heap. All variables are simply references to these objects. The garbage collection algorithm is a copy-and-compact. It behaves as a bump-pointer allocator until there is space left, and when space runs out, it creates a new heap, copies all of the alive object into it, calls the destructors of the dead objects and frees the old one.
-87
View File
@@ -1,87 +0,0 @@
fun newBytetree(leaf_size)
{
if leaf_size == none:
leaf_size = 64;
return { root: none, size: 0, leaf_size: leaf_size };
}
fun makeBufferIntoTree(buffer)
{
nleafs = 1.0 * count(data) / tree.leaf_size;
assert(type(nleafs) == type(0.0));
print('nleafs = ', nleafs, '\n');
leafs = [];
# Make a big buffer and then split it
# into `nleafs` slices, then store the
# slices into the `leafs` array.
leafs_buffer = newBuffer(nleafs * tree.leaf_size);
while count(leafs) < nleafs:
leafs[count(leafs)] =
sliceBuffer(leafs_buffer, i * tree.leaf_size, tree.leaf_size);
nodes = leafs;
while count(nodes) > 1:
{
temp = [];
npairs = count(nodes) / 2;
while count(temp) < npairs:
temp[count(temp)] = {
left : nodes[count(temp) * 2],
right: nodes[count(temp) * 2 + 1]
};
if npairs * 2 < count(nodes):
# There's an extra node we
# need to handle.
temp[count(temp)-1] = {
left: temp[count(temp)-1],
right: nodes[count(nodes)-1]
};
nodes = temp;
}
assert(count(nodes) == 1);
return nodes[0];
}
fun insertIntoBytetree(tree, data, index)
{
if index == none:
index = tree.size;
subtree = makeBufferIntoTree(data);
if tree.root == none:
{
tree.root = subtree;
return;
}
# Now find where to store this
# subtree.
current = tree.root;
rel_index = index;
while type(current) == type({}):
{
if rel_index < current.left_size:
current = current.left;
else
{
rel_index = rel_index - current.left_size;
current = current.right;
}
}
}
+12
View File
@@ -0,0 +1,12 @@
fun isalpha(c)
return c <
fun parse(req)
{
i = 0;
while i < count(req) && isalpha(req[i]):
i = i + 1;
}
-131
View File
@@ -1,131 +0,0 @@
fun _count(elm)
{
if type(elm) == type({}):
if type(elm._count) == type(_count):
return elm._count(elm);
else if type(elm._count) == type(1):
return elm._count;
return count(elm);
}
fun newTree()
{
return {root: none, _count: 0};
}
fun locate(tree, index, path)
{
# This function walks the tree to find the
# leaf that is referred to by the specified
# index.
# The traversed nodes are stored in the path
# argument. The last one is the leaf.
# The return value is the index relative to
# the start of the leaf.
assert(type(index) == type(1));
path = []; # List of traversed nodes (without the leaf).
rindex = index; # index relative to the found leaf.
cursor = tree; # Tree cursor. At the end it will refer to the leaf.
path[count(path)] = cursor;
while not cursor.is_leaf:
{
if rindex < _count(cursor.left):
cursor = cursor.left;
else
{
rindex = rindex - _count(cursor.left);
cursor = cursor.right;
}
path[count(path)] = cursor;
}
return rindex;
}
fun insert(tree, data, index)
{
if index == none:
# Insert at the end.
index = _count(tree);
path = [];
rindex = locate(tree, index, path);
assert(count(path) > 1);
leaf = path[count(path)-1];
assert(type(leaf) == type(newBuffer(0)));
if rindex == 0:
# Want to store the data before
# the leaf buffer.
{}
else if rindex < leaf.used:
# Want to store the data in the
# middle of the leaf.
{
fun min(a, b)
{
if a < b:
return a;
return b;
}
fun copy(dst, src, num)
{
i = 0;
while i < num:
{
dst[i] = src[i];
i = i + 1;
}
}
# Insert in the current leaf what's
# possible.
unused = count(leaf.buffer) - leaf.used;
copied = min(unused, count(data));
copy(leaf.buffer, data, copied);
leaf.used = leaf.used + copied;
if copied == count(data):
{
# We're done. The unused portion of
# the leaf was enough to store the
# data.
# Update the path.
i = count(path)-2;
while i > 0:
{
if path[i] == path[i-1].left:
path[i-1]._count = path[i-1]._count + copied;
i = i - 1;
}
return none;
}
# There's more data that needs to be
# stored, but now we don't need to
# insert in the middle of the leaf.
data = sliceBuffer(data, copying, count(data));
}
if rindex == leaf.used:
# Want to append data to the
# end of the contents.
{}
}
print( count(newTree()), '\n');
print(_count(newTree()), '\n');
+67
View File
@@ -1,3 +1,70 @@
/* WHAT IS THIS FILE?
** This is the implementation of the "Heap", an
** object that provides the rest of the program
** with memory and manages it by claiming it back
** implicitly when it's not in use anymore. To
** determine which memory is used or not, the
** heap system must be aware of the object graph.
** This is the reason why the Heap is tightly
** coupled to the object model.
**
** HOW DOES IT WORK?
** The collection algorithm is move-and-compact.
** The allocator is a bump-pointer allocator.
** When the base pool of memory is filled up,
** further allocations are forwarded to the
** stdlib's malloc, but are kept track of by
** putting them in a linked list. When the parent
** system decides to free up some memory, a new
** heap is allocated and the live objects are
** moved to it, then the old heap is freed. The
** references between live objects are updated
** when moving them.
** Some objects implement destructors that must
** be called when a new heap is allocated and
** they're not moved to it. An auxiliary list
** of allocated objects with destructors is stored
** alongside the heap. When the live objects
** are moved and the ones to be destroyed are
** left in the old one, the list of objects with
** destructors is iterated over and the objects
** in it that weren't moved are destroied and
** removed from the list. This approach becomes
** linearly slower with the number of allocated
** objects with destructors, but it's assumed
** that not many of them implement them.
**
** HOW ARE POINTERS UPDATED?
** Basically, when an object is moved from the
** old to the new heap, the location of the object
** in the old heap is overwritten with a placeholder
** object that holds the new location. Then all
** of it's references are iterated over and if
** they refer to placeholders they're updated
** with the new location of the object. If the
** references don't refer to placeholder objects,
** then the referred objects are moved too. This
** is a recursive process that, when applied to
** the root object of the program, moves all reachable
** objects to the new heap and updates the pointers.
** The complexity of this algorithm is proportional
** to the number of live objects.
**
** WHAT IS A BUMP-POINTER ALLOCATOR?
** A bump-pointer allocator is a minimal memory
** management system. A contiguous pool of memory
** is allocated. On a higher level, allocations
** are stacked one after another until the pool is
** all used up. This is done by having a pointer
** that points to the first free buffer of the pool.
** Initially, it points to the first byte of the pool.
** When N bytes are requested, the value of the
** pointer is given to the caller and then it's
** incremented by the allocated amount. When the
** pool has less free memory than what is requested,
** the allocation fails.
**
*/
#include <stdint.h> #include <stdint.h>
#include <assert.h> #include <assert.h>
#include <string.h> #include <string.h>