diff --git a/Makefile b/Makefile index aff05ae..14dea55 100644 --- a/Makefile +++ b/Makefile @@ -15,7 +15,7 @@ # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. CC = gcc -CFLAGS = -Wall -Wextra -Wno-unused-parameter -O3 -std=c99 -pedantic -D_XOPEN_SOURCE +CFLAGS = -dD -Wall -Wextra -Wno-unused-parameter -O3 -std=c99 -pedantic -D_XOPEN_SOURCE YACC = bison YACC_FLAGS = -y -d -t -v @@ -28,8 +28,9 @@ OBJ_FILES = y.tab.o lex.yy.o src/eval.o src/str.o src/regexp.o src/cli.o \ src/nodes.o src/args.o src/flathead.o src/debug.o src/gc.o src/props.o \ src/runtime/runtime.o src/runtime/lib/Math.o src/runtime/lib/RegExp.o \ src/runtime/lib/Error.o src/runtime/lib/String.o src/runtime/lib/console.o \ -src/runtime/lib/Function.o src/runtime/lib/Object.o src/runtime/lib/Boolean.o \ -src/runtime/lib/Number.o src/runtime/lib/Date.o src/runtime/lib/Array.o +src/runtime/lib/gc.o src/runtime/lib/Function.o src/runtime/lib/Object.o \ +src/runtime/lib/Boolean.o src/runtime/lib/Number.o src/runtime/lib/Date.o \ +src/runtime/lib/Array.o OUT_FILE = bin/flat YACC_FILE = src/grammar.y @@ -76,8 +77,8 @@ all: default debug: CFLAGS += -g -O0 debug: clean default -malloc-debug: CC = gcc-4.7 -malloc-debug: LIBS += -lefence +malloc-debug: CC = gcc-4.9 +malloc-debug: LIBS += -lduma malloc-debug: debug diff --git a/src/debug.c b/src/debug.c index e11b95d..a0ab4ac 100644 --- a/src/debug.c +++ b/src/debug.c @@ -131,7 +131,7 @@ fh_debug(FILE *stream, js_val *val, int indent, bool newline) else if (IS_FUNC(val)) cfprintf(stream, ANSI_BLUE, "[Function]"); else if (IS_DATE(val)) - fh_debug(stream, fh_to_primitive(val, 0), indent, false); + fprintf(stream, "[Date %ld]", (long)val->object.primitive->number.val); else debug_obj(stream, val, indent, false); break; diff --git a/src/eval.c b/src/eval.c index 662a3bc..650028c 100644 --- a/src/eval.c +++ b/src/eval.c @@ -88,6 +88,7 @@ member_exp(js_val *ctx, ast_node *member) int i = member->e1->val, len = parent->string.length; if (i < len && i >= 0) { char *str = malloc(sizeof(char) + 1); + str[1] = '\0'; sprintf(str, "%c", parent->string.ptr[i]); return JSSTR(str); } @@ -466,10 +467,10 @@ switch_stmt(js_val *ctx, ast_node *node) js_val *val, *result, *test = fh_eval(ctx, node->e1); ast_node *current, - *caseblock = node->e2, - *clauses_a = caseblock->e1, - *defaultclause = caseblock->e2, - *clauses_b = caseblock->e3; + *caseblock = node->e2, + *clauses_a = caseblock->e1, + *defaultclause = caseblock->e2, + *clauses_b = caseblock->e3; bool matched = false; @@ -806,6 +807,7 @@ call(js_val *ctx, js_val *this, js_val *func, eval_state *state, js_args *args) state->caller_info = "(anonymous function)"; js_val *func_scope = setup_call_env(ctx, this, func, args); + state->scope = func_scope; return fh_eval(func_scope, func->object.node->e2); } diff --git a/src/flathead.c b/src/flathead.c index ac8e70b..b5e3666 100644 --- a/src/flathead.c +++ b/src/flathead.c @@ -40,6 +40,7 @@ fh_new_val(js_type type) val->type = type; val->signal = S_NONE; val->proto = NULL; + val->marked = false; return val; } @@ -71,6 +72,7 @@ fh_new_string(char *x) js_val *val = fh_new_val(T_STRING); val->string.ptr = malloc((strlen(x) + 1) * sizeof(char)); + val->string.ptr[strlen(x)] = '\0'; strcpy(val->string.ptr, x); fh_set_len(val, strlen(x)); val->proto = fh_try_get_proto("String"); @@ -99,6 +101,11 @@ fh_new_object() val->object.extensible = false; val->object.parent = NULL; val->object.primitive = NULL; + val->object.bound_this = NULL; + val->object.bound_args = NULL; + val->object.scope = NULL; + val->object.instance = NULL; + val->object.node = NULL; val->proto = fh->object_proto; return val; @@ -129,6 +136,7 @@ fh_new_function(struct ast_node *node) fh_set(val, "caller", JSNULL()); val->object.native = false; + val->object.provide_this = false; val->object.generator = false; val->object.node = node; val->object.scope = NULL; @@ -198,6 +206,7 @@ fh_new_error(char *name, const char *tpl, ...) va_end(ap); char *msg = malloc(size + 1); + msg[size] = '\0'; // Now build the error message va_start(ap, tpl); @@ -253,6 +262,7 @@ fh_new_state(int line, int column) state->ctx = NULL; state->this = NULL; + state->scope = NULL; state->parent = NULL; state->construct = false; state->catch = false; @@ -275,6 +285,7 @@ fh_new_global_state() state->global = NULL; state->function_proto = NULL; state->object_proto = NULL; + state->callstack = NULL; state->script_name = "main"; @@ -414,6 +425,7 @@ fh_to_string(js_val *val) fmt = "%g"; int size = snprintf(NULL, 0, fmt, val->number.val) + 1; char *num = malloc(size); + num[size] = '\0'; snprintf(num, size, fmt, val->number.val); return JSSTR(num); } @@ -428,17 +440,23 @@ fh_to_object(js_val *val) { if (IS_UNDEF(val) || IS_NULL(val)) fh_throw(NULL, fh_new_error(E_TYPE, "cannot convert %s to object", fh_typeof(val))); - if (IS_OBJ(val)) + if (IS_OBJ(val)) return val; js_val *obj = JSOBJ(); obj->object.primitive = val; - if (IS_BOOL(val)) + if (IS_BOOL(val)) { fh_set_class(obj, "Boolean"); - if (IS_NUM(val)) + obj->proto = fh_try_get_proto("Boolean"); + } + if (IS_NUM(val)) { fh_set_class(obj, "Number"); - if (IS_STR(val)) + obj->proto = fh_try_get_proto("Number"); + } + if (IS_STR(val)) { fh_set_class(obj, "String"); + obj->proto = fh_try_get_proto("String"); + } return obj; } @@ -558,6 +576,12 @@ fh_has_instance(js_val *func, js_val *val) return JSBOOL(0); } +js_val * +fh_implicit_this_value(js_val *obj) +{ + return obj->object.provide_this ? obj->object.bound_this : JSUNDEF(); +} + js_val * fh_has_property(js_val *obj, char *prop) { @@ -568,6 +592,14 @@ fh_has_property(js_val *obj, char *prop) js_val * fh_try_get_proto(char *type) { + // Try to avoid a lookup. + if (STREQ(type, "Function") && fh->function_proto) + return fh->function_proto; + if (STREQ(type, "Object") && fh->object_proto) + return fh->object_proto; + if (STREQ(type, "Array") && fh->array_proto) + return fh->array_proto; + js_val *global = fh->global; if (global != NULL) { js_val *obj = fh_get(global, type); diff --git a/src/flathead.h b/src/flathead.h index ed9e7da..799a667 100644 --- a/src/flathead.h +++ b/src/flathead.h @@ -167,6 +167,7 @@ typedef struct eval_state { bool catch; struct js_val *ctx; struct js_val *this; + struct js_val *scope; jmp_buf jmp; struct eval_state *parent; } eval_state; @@ -206,6 +207,7 @@ typedef struct js_val * (js_native_function)(struct js_val *, struct js_args *, typedef struct { bool native; bool generator; + bool provide_this; bool extensible; // [[Extensible]] char class[10]; // [[Class]] struct js_val *primitive; // [[PrimitiveValue]] @@ -232,6 +234,7 @@ typedef struct js_val { ctl_signal signal; struct js_val *proto; bool marked; + bool flagged; js_prop *map; } js_val; @@ -269,6 +272,7 @@ js_val * fh_to_object(js_val *); js_val * fh_cast(js_val *, js_type); js_val * fh_has_instance(js_val *, js_val *); +js_val * fh_implicit_this_value(js_val *obj); js_val * fh_has_property(js_val *, char *); char * fh_typeof(js_val *); void fh_set_len(js_val *, unsigned long); diff --git a/src/gc.c b/src/gc.c index aba4283..8c211df 100644 --- a/src/gc.c +++ b/src/gc.c @@ -22,6 +22,22 @@ #include "gc.h" #include "debug.h" +#ifdef FH_GC_PROFILE +#define GC_PRINT(indent, ...) printf("%*s", indent, ""); printf(__VA_ARGS__) +#define GC_DEBUG(indent, val) fh_debug(stdout, val, indent, true) +#else +#define GC_PRINT(indent, ...) +#define GC_DEBUG(indent, val) +#endif + +#ifdef FH_GC_PROFILE_VERBOSE +#define GC_PRINT_VERBOSE(indent, ...) GC_PRINT(indent, ...) +#define GC_DEBUG_VERBOSE(indent, val) GC_DEBUG(indent, val) +#else +#define GC_PRINT_VERBOSE(indent, ...) +#define GC_DEBUG_VERBOSE(indent, val) +#endif + /* GC Overview * * Bi-color, non-incremental, mark & sweep, stop-the-world garbage collection. @@ -66,13 +82,10 @@ static gc_arena * fh_new_arena() { - size_t arena_size = sizeof(js_val) * SLOTS_PER_ARENA; - js_val *slots = malloc(arena_size); gc_arena *arena = malloc(sizeof(gc_arena)); arena->num_slots = SLOTS_PER_ARENA; arena->used_slots = 0; - arena->slots = slots; memset(arena->freelist, 0, sizeof(arena->freelist)); return arena; @@ -96,13 +109,18 @@ fh_get_arena() js_val * fh_malloc(bool first_attempt) { + if (fh->gc_state != GC_STATE_NONE) { + fprintf(stderr, "Error: politely refusing to allocate during garbage collection"); + exit(EXIT_FAILURE); + } + gc_arena *arena = fh_get_arena(); int i; for (i = 0; i < arena->num_slots; i++) { if (!arena->freelist[i]) { arena->freelist[i] = true; arena->used_slots++; - return &(arena->slots[i]); + return &arena->slots[i]; } } if (first_attempt) { @@ -147,11 +165,11 @@ fh_gc_debug() } printf( - " %ld slots | %ld/%ld used slots | %ld/%ld vacant slots | %ld usage (bytes) | %d runs\n", + " %ld slots | %ld/%ld used slots | %ld/%ld vacant slots | %ld usage KB | %d runs\n", total_slots, used_slots, total_slots, total_slots - used_slots, total_slots, - total_slots * sizeof(js_val), + (total_slots * sizeof(js_val)) / 1000, fh->gc_runs ); @@ -159,27 +177,54 @@ fh_gc_debug() } static void -fh_gc_mark(js_val *val) +fh_gc_debug_arena(gc_arena *arena) +{ +#ifdef FH_GC_PROFILE_VERBOSE + for (int i = 0; i < arena->num_slots; i++) { + printf("slot[%4d]: (USED %d) (MARKED %d)\n", i, arena->freelist[i], arena->freelist[i] && arena->slots[i].marked); + } +#endif +} + +static void +fh_gc_mark(js_val *val, int depth) { - if (!val || val->marked) return; + if (val && val->flagged) + puts("Attempting to mark flagged val"); + + if (!val || val->marked) return; val->marked = true; - fh_gc_mark(val->proto); + GC_DEBUG_VERBOSE(depth, val); + + GC_PRINT_VERBOSE(depth, "Marking prototype\n"); + fh_gc_mark(val->proto, depth + 1); if (IS_OBJ(val)) { - fh_gc_mark(val->object.primitive); - fh_gc_mark(val->object.bound_this); - fh_gc_mark(val->object.scope); - fh_gc_mark(val->object.instance); - fh_gc_mark(val->object.parent); + GC_PRINT_VERBOSE(depth, "Marking primitive\n"); + fh_gc_mark(val->object.primitive, depth + 1); + + GC_PRINT_VERBOSE(depth, "Marking bound this\n"); + fh_gc_mark(val->object.bound_this, depth + 1); + + GC_PRINT_VERBOSE(depth, "Marking scope\n"); + fh_gc_mark(val->object.scope, depth + 1); + + GC_PRINT_VERBOSE(depth, "Marking instance\n"); + fh_gc_mark(val->object.instance, depth + 1); + + GC_PRINT_VERBOSE(depth, "Marking parent\n"); + fh_gc_mark(val->object.parent, depth + 1); } if (val->map) { js_prop *prop; OBJ_ITER(val, prop) { - if (prop->ptr && !prop->circular) - fh_gc_mark(prop->ptr); + if (prop->ptr && !prop->circular) { + GC_PRINT_VERBOSE(depth, "Marking %s\n", prop->name); + fh_gc_mark(prop->ptr, depth + 1); + } } } } @@ -191,7 +236,6 @@ fh_gc_free_val(js_val *val) // // Note we're not freeing the values pointed at, only the pointers to them // and the hashtable overhead. - /* if (val->map) { js_prop *prop, *tmp; HASH_ITER(hh, val->map, prop, tmp) { @@ -199,7 +243,6 @@ fh_gc_free_val(js_val *val) if (prop != NULL) free(prop); } } - */ // Free any strings (dynamically alloc-ed outside slots) if (IS_STR(val) && val->string.ptr != NULL) { @@ -212,16 +255,20 @@ fh_gc_free_val(js_val *val) static void fh_gc_sweep(gc_arena *arena) { - int i; js_val val; - for (i = 0; i < SLOTS_PER_ARENA; i++) { + int sweeped_count = 0; + for (int i = 0; i < arena->num_slots; i++) { + if (!arena->freelist[i]) return; val = arena->slots[i]; if (!val.marked) { + if (val.flagged) puts("GC: freeing flagged val"); arena->freelist[i] = false; fh_gc_free_val(&val); arena->used_slots--; + sweeped_count++; + } else { + arena->slots[i].marked = false; } - val.marked = false; } } @@ -230,13 +277,22 @@ fh_gc() { gc_arena *arena = fh_get_arena(); + fh_gc_debug_arena(arena); + // Start fh->gc_state = GC_STATE_STARTING; fh_gc_debug(); // Mark fh->gc_state = GC_STATE_MARK; - fh_gc_mark(fh->global); + fh_gc_mark(fh->global, 0); + if (fh->callstack) { + eval_state *top = fh->callstack; + while (top) { + fh_gc_mark(top->scope, 0); + top = top->parent; + } + } fh_gc_debug(); // Sweep @@ -247,4 +303,6 @@ fh_gc() // Stop fh->gc_state = GC_STATE_NONE; fh_gc_debug(); + + fh_gc_debug_arena(arena); } diff --git a/src/gc.h b/src/gc.h index afc862a..a7c985e 100644 --- a/src/gc.h +++ b/src/gc.h @@ -27,7 +27,7 @@ typedef struct gc_arena { int num_slots; int used_slots; bool freelist[SLOTS_PER_ARENA]; - js_val *slots; + js_val slots[SLOTS_PER_ARENA]; } gc_arena; js_val * fh_malloc(bool); diff --git a/src/nodes.c b/src/nodes.c index ea2d1bb..0230a6a 100644 --- a/src/nodes.c +++ b/src/nodes.c @@ -64,6 +64,7 @@ node_new(enum ast_node_type type, ast_node *e1, ast_node *e2, ast_node *e3, node->sval = NULL; if (s != NULL) { node->sval = malloc((strlen(s) + 1) * sizeof(char)); + node->sval[strlen(s)] = '\0'; strcpy(node->sval, s); } return node; diff --git a/src/props.c b/src/props.c index 7dc183c..faa0aec 100644 --- a/src/props.c +++ b/src/props.c @@ -127,6 +127,7 @@ fh_set_prop(js_val *obj, char *name, js_val *val, js_prop_flags flags) if (new) { prop->name = malloc((strlen(name) + 1) * sizeof(char)); strcpy(prop->name, name); + prop->name[strlen(name)] = '\0'; HASH_ADD_KEYPTR(hh, obj->map, prop->name, strlen(prop->name), prop); } } diff --git a/src/runtime/lib/gc.c b/src/runtime/lib/gc.c new file mode 100644 index 0000000..3723d5e --- /dev/null +++ b/src/runtime/lib/gc.c @@ -0,0 +1,67 @@ +// gc.c +// --------- +// Non-standard utility functions for running and debugging garbage collection +// +// The `gc` object is available when flathead is compiled with GC exposed, which is +// currently the default setting. If access to the garbage collector is not desirable, these utilities can be disabled by compiling with: +// +// make gcexpose=off +// +// As objects are instantiated by the interpreter to call a function, it's +// possible that calling one of these methods may trigger the GC. + +#include "gc.h" + +// gc.run() +// +// Runs garbage collection immediately and returns undefined. +js_val * +gc_run(js_val *instance, js_args *args, eval_state *state) +{ + fh_gc(); + return JSUNDEF(); +} + +// gc.info() +// +// Returns information about the last garbage collection. +js_val * +gc_info(js_val *instance, js_args *args, eval_state *state) +{ + js_val *info = JSOBJ(); + fh_set_prop(info, "arenas", JSNUM(fh->gc_num_arenas), P_DEFAULT); + fh_set_prop(info, "arenaSize", JSNUM(SLOTS_PER_ARENA), P_DEFAULT); + fh_set_prop(info, "runs", JSNUM(fh->gc_runs), P_DEFAULT); + fh_set_prop(info, "lastStart", JSNUM(fh->gc_last_start), P_DEFAULT); + fh_set_prop(info, "time", JSNUM(fh->gc_time), P_DEFAULT); + return info; +} + +// gc.spy(value) +// +// Flags a value for verbose GC logging. Because there's so much noise from +// allocating objects for the standard runtime, it can be difficult to follow +// the lifecycle of an individual value. +js_val * +gc_spy(js_val *instance, js_args *args, eval_state *state) +{ + if (ARGLEN(args) >= 1) { + js_val *val = ARG(args, 0); + val->flagged = true; + } + return JSUNDEF(); +} + +js_val * +bootstrap_gc() +{ + js_val *gc = JSOBJ(); + + DEF(gc, "run", JSNFUNC(gc_run, 0)); + DEF(gc, "info", JSNFUNC(gc_info, 0)); + DEF(gc, "spy", JSNFUNC(gc_spy, 1)); + + fh_attach_prototype(gc, fh->function_proto); + + return gc; +} diff --git a/src/runtime/lib/gc.h b/src/runtime/lib/gc.h new file mode 100644 index 0000000..5080fdd --- /dev/null +++ b/src/runtime/lib/gc.h @@ -0,0 +1,15 @@ +// gc.h +// ---- + +#ifndef JS_GC_H +#define JS_GC_H + +#include "../runtime.h" + +js_val * gc_run(js_val *, js_args *, eval_state *); +js_val * gc_info(js_val *, js_args *, eval_state *); +js_val * gc_spy(js_val *, js_args *, eval_state *); + +js_val * bootstrap_gc(void); + +#endif diff --git a/src/runtime/runtime.c b/src/runtime/runtime.c index 42213cb..0eb74ab 100644 --- a/src/runtime/runtime.c +++ b/src/runtime/runtime.c @@ -22,6 +22,7 @@ #include "runtime.h" #include "lib/console.h" +#include "lib/gc.h" #include "lib/Math.h" #include "lib/Object.h" #include "lib/Function.h" @@ -143,14 +144,6 @@ global_eval(js_val *instance, js_args *args, eval_state *state) return fh_eval_string(code->string.ptr, state->ctx); } -// gc() -js_val * -global_gc(js_val *instance, js_args *args, eval_state *state) -{ - fh_gc(); - return JSUNDEF(); -} - // print() js_val * global_print(js_val *instance, js_args *args, eval_state *state) @@ -233,6 +226,9 @@ fh_bootstrap() DEF(global, "Error", bootstrap_error(global)); DEF(global, "Math", bootstrap_math()); DEF(global, "console", bootstrap_console()); +#ifdef FH_GC_EXPOSE + DEF(global, "gc", bootstrap_gc()); +#endif // The Object constructor and its methods are created before the Function // constructor exists, so we need connect the prototypes manually. @@ -269,9 +265,5 @@ fh_bootstrap() DEF(global, "load", JSNFUNC(global_load, 1)); DEF(global, "print", JSNFUNC(global_print, 1)); -#ifdef FH_GC_EXPOSE - DEF(global, "gc", JSNFUNC(global_gc, 0)); -#endif - return global; } diff --git a/src/str.c b/src/str.c index e8dc455..13a52c4 100644 --- a/src/str.c +++ b/src/str.c @@ -32,6 +32,7 @@ fh_str_concat(char *a, char *b) { size_t size = strlen(a) + strlen(b) + 1; char *new = malloc(size); + new[size] = '\0'; snprintf(new, size, "%s%s", a, b); return new; } @@ -44,6 +45,7 @@ fh_str_slice(char *str, unsigned start, unsigned end) return NULL; size_t size = end - start + 1; char *new = malloc(size); + new[size] = '\0'; snprintf(new, size, "%.*s", end - start, str + start); return new; } diff --git a/test/test_gc.js b/test/test_gc.js index 0cedc19..327710c 100644 --- a/test/test_gc.js +++ b/test/test_gc.js @@ -21,7 +21,7 @@ var z = '99 Luftballons'; // use the method on global, if exposed. if (typeof gc !== 'undefined') { - gc(); + gc.run(); } // ...or trigger it the old-fashioned way with a big loop. else { diff --git a/test/test_math.js b/test/test_math.js index f850e75..8eb68d3 100644 --- a/test/test_math.js +++ b/test/test_math.js @@ -15,16 +15,15 @@ var zero = function() { // ---------------------------------------------------------------------------- test('properties', function() { - // Just sanity checks, acutal precision should be the same as the cmath - // constants used internally. - assertClose(2.718, Math.E, 0.001); - assertClose(0.693, Math.LN2, 0.001); - assertClose(2.303, Math.LN10, 0.001); - assertClose(1.443, Math.LOG2E, 0.001); - assertClose(0.434, Math.LOG10E, 0.001); - assertClose(3.14159, Math.PI, 0.00001); - assertClose(0.707, Math.SQRT1_2, 0.001); - assertClose(1.414, Math.SQRT2, 0.001); + // Just sanity checks, see Math.c + assertClose(2.718, Math.E, 0.001); + assertClose(0.693, Math.LN2, 0.001); + assertClose(2.303, Math.LN10, 0.001); + assertClose(1.443, Math.LOG2E, 0.001); + assertClose(0.434, Math.LOG10E, 0.001); + assertClose(3.14159, Math.PI, 0.00001); + assertClose(0.707, Math.SQRT1_2, 0.001); + assertClose(1.414, Math.SQRT2, 0.001); }); // ----------------------------------------------------------------------------