Garbage collection - #5
Merged
Merged
Conversation
Sort includes in some sources.
Renamed: - sl_safe_malloc -> mem_alloc - sl_safe_calloc -> mem_calloc - sl_safe_strdup -> mem_strdup - sl_safe_realloc -> mem_realloc
Non-working, but compiles. I don't really like the 'PtrArray' method.
Instead of returning and receiving 'Pool' structures (like in my 'libpool' project), it initializes and operates on a single global expression pool. This should make the interface simpler, specially when integrating it with the garbage collector and the 'expr_new' function.
Tests succeeded with no memory leaks. * Move old body of 'expr_free' to new 'free_expr_members' static function, call it from 'pool_free'. * Add 'BASE_POOL_SZ' macro to 'expr_pool.h'. * Initialize and close pool from 'main'.
Pool allocator
Use a "unmark, mark, collect" approach. Not the most efficient, but it should work for now. Compiles fine, not tested at runtime.
This apparently fixes a big bug, although I don't understand how it's
related. If you run the 'arith.lisp' test, the program will crash trying to
read address 0x20 when accessing 'g_expr_pool->free_node'. If you put a
watchpoint in this member, you will eventually see it's being set to an invalid
address in the 161th call to 'pool_alloc':
Hardware watchpoint 1: g_expr_pool->free_node
Old value = (PoolNode *) 0x55555556d200
New value = (PoolNode *) 0x555500000020
pool_alloc () at src/expr_pool.c:159
159 result->flags &= ~NODE_FREE;
Although people in the #gdb IRC channel told me that, in my architecture, it was
supposedly stopping at the instruction after the one that changed the
value. Either way, I don't understand where this 0x20 value came from, so this
commit might just delay the problem.
Also, this problem only happened when adding the following lines to
'repl_until_eof':
diff --git a/src/main.c b/src/main.c
index 75944b2..1e297ef 100644
--- a/src/main.c
+++ b/src/main.c
@@ -99,6 +99,10 @@ static void repl_until_eof(Env* env, FILE* file, bool print_prompt,
/* Free the evaluated expression */
expr_free(evaluated);
+
+ gc_unmark_all();
+ gc_mark_env(env);
+ gc_collect();
}
}
About pointer ownership.
We can't do the same in 'expr_pool.h' since we use the structure directly (not just pointers) and the compiler needs to know the size and offset of the members.
We don't want to completely remove 'expr_free' and 'expr_list_free', since they are still used in some parts of the program. Perhaps in the future, when we replace this linked list approach with proper cons pairs, we could remove 'expr_list_free'.
The values of the expressions might be pointers to allocated data (e.g. with strings or lambdas), so we have to free that too. Since the expression "owns" a unique pointer to its allocated data (i.e. the another expression won't point to the same allocated string; perhaps to a copy) we should be able to free it safely.
Since these 'env_bind' function will no longer create clones after a future commit. * Add 'g_' prefix to globals declared in 'env.h'. * Make 'g_debug_trace_list' global. We should edit 'debug_is_traced_function' to use it. * Allocate the expressions for these globals from 'env_init_defaults' once.
We should replace it to an assertion after the cons pair changes; hopefully in a near future.
One expression in the second array might reference another expression in the first array, but since it's been freed, it will segfault as soon as we try to even read its flags.
This commit is not just for proving that garbage collection works. In future commits, many calls to 'expr_clone' will be removed, and most expressions will be re-used or modified directly (depending on the context, obviously). I decided to keep these two commits (removing frees and removing clones) separate for readability.
Replacing it will an assertion still fails.
Should have been included in commit 3c5c6a2
This, along with the previous commit (9a17f00), fixes a bug where 'pool_close' tried to free expression members multiple times. The problem was caused in the following case: 1. There was a symbol (for example) in the pool, whose string was freed from the heap when 'pool_close' was called. 2. After freeing that symbol, another expression of type 'EXPR_PARENT' was freed by 'pool_close'. However, this list contained a pointer to the symbol expression whose string we just freed from 'pool_close'. The old 'free_expr_members' tried freeing the expression with 'expr_list_free', which called 'pool_free', which then freed the string again. This is normally (i.e. when garbage-collecting) not a problem because the string of the first symbol would be freed through 'pool_free', which would set 'NODE_FLAG_FREE' in the symbol's 'PoolNode'. Therefore, whenever the list is reached, even if 'expr_list_free' tried freeing each element, 'pool_free' would notice that it's already been freed (since it checks the flag). The problem is that this flag is never set whenever we freed the members of the expression from 'pool_close' (calling 'free_expr_members', not through 'pool_free'), so 'NODE_FLAG_FREE' was never set. There are two possible solutions: 1. Simply mark the expression whose members we are freeing from 'pool_close' with 'NODE_FLAG_FREE'. 2. Don't call 'expr_list_free' from 'free_expr_members' entirely. Although only one of them is necessary, I chose to add both, since it was not a good idea to free lists recursively here anyway; specially since we want to start reusing pointers, and we were not checking if they were in use somewhere else (we are not checking for 'GCMARKED', but even if it was set, maybe we weren't called from the garbage collector). Changes: * Rename 'free_expr_members' to 'free_heap_expr_members'. * Don't free expressions recursively from 'free_heap_expr_members'. * Mark expressions with freed members with 'NODE_FLAG_FREE' from 'pool_close'.
This is not really necessary, but it provides more accurate results in the valgrind summary (since now 'VALGRIND_MEMPOOL_FREE' is being called). Before this commit, valgrind showed that we were missing N frees, where N was the number of non-free nodes when 'pool_close' was called.
The point of using valgrind is avoiding memory leaks and invalid memory accesses in the pool (e.g. overflowing the size of an 'Expr', double-frees, etc.), not controlling when we can access the 'ExprPool' or 'ArrayStart' structures. It's very hard to access these structures by mistake; if we access them, for example from the garbage collector, it's because we need to. However, the 'flags' member of the 'PoolNode' structures, can be easily overwritten by casting a 'Expr*', for example. This commit doesn't fix anything, it just removes unnecessary macro calls. * Remove 'POOL_FOREACH_ARRAYSTART' and 'POOL_FOREACH_ARRAYSTART_END' macros, replace with normal for-loops. * Remove unnecessary calls to 'VALGRIND_MAKE_MEM_DEFINED' and 'VALGRIND_MAKE_MEM_NOACCESS' from 'expr_pool.c'.
Since we store copies in the environment and not the actual pointers in the globals, the garbage collector was freeing the globals, and they were being overwritten with other values. When functions like 'prim_equal' tried to return clones of 'g_tru', it returned an invalid value (like 'car', for example).
8dcc
added a commit
that referenced
this pull request
Jan 13, 2025
Some changes were merged into this 'garbage-collection' branch in #4. Major changes: * Move memory-related functions to 'memory.c' (this should have been done in main). * Add pool allocator for expressions ('Expr' structures). * Add garbage collector that marks expressions from the pool, and frees the non-marked ones. * Replace 'expr_free' with 'pool_free', remove 'expr_list_free'. Minor changes: * Remove most calls to 'expr_free', only call manually from 'parse' (other calls are currently from the garbage collector or the pool itself). * Don't use stack variables from 'env_init_defaults' (and therefore from the 'BIND_PRIM_FLAGS' macro). * Rename 'tru' and 'nil' C globals to 'g_tru' and 'g_nil' (this should have been done in 'main', after merging). * Add 'g_debug_trace_list' global variable to 'env.c'. * Add 'CPPFLAGS' to Makefile (this should have been done in 'main', after merging). * Improve documentation of 'EExprType' enum and 'Expr' structure in comments. * Add many TODO comments. Changes not included in this PR (currently in a 'copy-references' branch): * Use references instead of clones in most places.
8dcc
added a commit
that referenced
this pull request
Feb 16, 2025
Work with references, instead of always returning copies. This change was possible thanks to #5 (which needed #4), #6 and #7. Some changes were merged into this 'copy-references' branch in #7. Major changes: * Remove most calls to 'expr_clone', for working with references instead of copies. * Return or store references in 'car', 'cdr', 'cons', 'nth', 'quote' and 'backquote' primitives. * Add proper environment closures, setting the parent environments whenever a lambda is created, not when it's called. See #7. * Change how GC handles lambdas, add 'is_used' member to 'Env'. Mostly included in #7, but not completely (see 5758c96). * Don't mark globals ('g_nil', 'g_tru' and 'g_debug_trace_list') for garbage collection, since their references are being stored in the environment. Minor changes: * Add 'gc_mark_env' and 'gc_mark_env_and_parents' static functions to 'garbage_collector.c'. * Add an 'Env*' parameter to 'lambdactx_init', remove it from 'lambdactx_eval_body'. * Don't make the 'Expr*' parameter of 'env_bind' and 'env_bind_global' constant. * Add closure test to 'test/lambdas.lisp'. See #7.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Some changes were merged into this
garbage-collectionbranch in #4.Major changes:
memory.c(this should have been done inmain).Exprstructures).expr_freewithpool_free, removeexpr_list_free.Minor changes:
expr_free, only call manually fromparse(other calls are currently from the garbage collector or the pool itself).env_init_defaults(and therefore from theBIND_PRIM_FLAGSmacro).truandnilC globals tog_truandg_nil(this should have been done inmain, after merging).g_debug_trace_listglobal variable toenv.c.CPPFLAGSto Makefile (this should have been done inmain, after merging).EExprTypeenum andExprstructure in comments.Changes not included in this PR (see #8):