From 9fccc47b8bd43ec1ac8a2868b938e2d64c2806aa Mon Sep 17 00:00:00 2001 From: Marco Bambini Date: Sat, 24 Jan 2026 09:22:29 +0100 Subject: [PATCH 01/37] Several leaks and bugs fixed (added new unit test) --- .gitignore | 2 + src/compiler/gravity_codegen.c | 2 + src/compiler/gravity_ircode.c | 2 +- src/compiler/gravity_lexer.c | 15 ++-- src/compiler/gravity_optimizer.c | 24 ++++-- src/compiler/gravity_parser.c | 1 + src/optionals/gravity_opt_env.c | 8 +- src/optionals/gravity_opt_file.c | 30 ++++---- src/optionals/gravity_opt_json.c | 6 +- src/optionals/gravity_opt_math.c | 26 ++++--- src/runtime/gravity_core.c | 6 +- src/runtime/gravity_vm.c | 11 ++- src/shared/gravity_array.h | 13 ++-- src/shared/gravity_macros.h | 1 - src/shared/gravity_memory.c | 2 +- src/shared/gravity_value.c | 74 +++++++++++++++++-- src/shared/gravity_value.h | 31 ++++---- src/utils/gravity_utils.c | 10 +-- test/unittest/class_grow_chain.gravity | 30 ++++++++ test/unittest/closure_nested_scope.gravity | 31 ++++++++ test/unittest/const_fold_div.gravity | 23 ++++++ test/unittest/env_type_error.gravity | 9 +++ test/unittest/file_size_type_error.gravity | 9 +++ test/unittest/file_type_error.gravity | 9 +++ test/unittest/file_write_type_error.gravity | 9 +++ test/unittest/int_overflow_ops.gravity | 25 +++++++ test/unittest/json_parse.gravity | 29 ++++++++ test/unittest/map_list_types.gravity | 30 ++++++++ test/unittest/math/gcf_negative.gravity | 21 ++++++ test/unittest/math/lcm_edge.gravity | 25 +++++++ test/unittest/math/xrt_float.gravity | 24 ++++++ test/unittest/math/xrt_zero_base.gravity | 14 ++++ test/unittest/optimizer_arithmetic.gravity | 31 ++++++++ test/unittest/range_operations.gravity | 33 +++++++++ test/unittest/string_escape_sequences.gravity | 33 +++++++++ .../string_interpolation_complex.gravity | 33 +++++++++ test/unittest/string_repeat_overflow.gravity | 12 +++ test/unittest/string_repeat_valid.gravity | 25 +++++++ test/unittest/string_unicode_escape.gravity | 27 +++++++ 39 files changed, 658 insertions(+), 88 deletions(-) create mode 100644 test/unittest/class_grow_chain.gravity create mode 100644 test/unittest/closure_nested_scope.gravity create mode 100644 test/unittest/const_fold_div.gravity create mode 100644 test/unittest/env_type_error.gravity create mode 100644 test/unittest/file_size_type_error.gravity create mode 100644 test/unittest/file_type_error.gravity create mode 100644 test/unittest/file_write_type_error.gravity create mode 100644 test/unittest/int_overflow_ops.gravity create mode 100644 test/unittest/json_parse.gravity create mode 100644 test/unittest/map_list_types.gravity create mode 100644 test/unittest/math/gcf_negative.gravity create mode 100644 test/unittest/math/lcm_edge.gravity create mode 100644 test/unittest/math/xrt_float.gravity create mode 100644 test/unittest/math/xrt_zero_base.gravity create mode 100644 test/unittest/optimizer_arithmetic.gravity create mode 100644 test/unittest/range_operations.gravity create mode 100644 test/unittest/string_escape_sequences.gravity create mode 100644 test/unittest/string_interpolation_complex.gravity create mode 100644 test/unittest/string_repeat_overflow.gravity create mode 100644 test/unittest/string_repeat_valid.gravity create mode 100644 test/unittest/string_unicode_escape.gravity diff --git a/.gitignore b/.gitignore index de47c871..8c8c306a 100644 --- a/.gitignore +++ b/.gitignore @@ -325,3 +325,5 @@ binding/GravityObjC/GravityObjC.xcodeproj/project.xcworkspace/xcshareddata/IDEWo gravity.xcodeproj/xcuserdata/marco.xcuserdatad/xcschemes/gravity.xcscheme *.xcscheme *.xcscheme +gravity.xcodeproj/xcuserdata/marco.xcuserdatad/xcschemes/gravity.xcscheme +*.d diff --git a/src/compiler/gravity_codegen.c b/src/compiler/gravity_codegen.c index 3e9b48ad..5abf08d4 100644 --- a/src/compiler/gravity_codegen.c +++ b/src/compiler/gravity_codegen.c @@ -77,6 +77,7 @@ static void report_error (gvisitor_t *self, gnode_t *node, const char *format, . // build error message char buffer[1024]; + buffer[0] = 0; va_list arg; if (format) { va_start (arg, format); @@ -2142,6 +2143,7 @@ gravity_function_t *gravity_codegen(gnode_t *node, gravity_delegate_t *delegate, marray_pop(data.context); assert(marray_size(data.context) == 0); marray_destroy(data.context); + marray_destroy(data.superfix); // in case of codegen errors explicity free code and return NULL if (visitor.nerr != 0) {ircode_free(code); f->bytecode = NULL;} diff --git a/src/compiler/gravity_ircode.c b/src/compiler/gravity_ircode.c index ef7e60c8..b16bb432 100644 --- a/src/compiler/gravity_ircode.c +++ b/src/compiler/gravity_ircode.c @@ -293,7 +293,7 @@ void ircode_dump (void *_code) { switch (n) { case 0: { printf("%05d\t%s\n", line, opcode_name(op)); - } + } break; case 1: { printf("%05d\t%s %d\n", line, opcode_name(op), p1); diff --git a/src/compiler/gravity_lexer.c b/src/compiler/gravity_lexer.c index 9d6bf865..9408b19f 100644 --- a/src/compiler/gravity_lexer.c +++ b/src/compiler/gravity_lexer.c @@ -39,7 +39,7 @@ typedef enum { // LEXER macros #define NEXT lexer->buffer[lexer->offset++]; ++lexer->position; INC_COL -#define PEEK_CURRENT ((int)lexer->buffer[lexer->offset]) +#define PEEK_CURRENT ((lexer->offset < lexer->length) ? (int)lexer->buffer[lexer->offset] : 0) #define PEEK_NEXT ((lexer->offset < lexer->length) ? lexer->buffer[lexer->offset+1] : 0) #define PEEK_NEXT2 ((lexer->offset+1 < lexer->length) ? lexer->buffer[lexer->offset+2] : 0) #define INC_LINE ++lexer->lineno; RESET_COL @@ -184,9 +184,9 @@ static inline bool next_utf8(gravity_lexer_t *lexer, int *result) { switch(len) { case 1: break; - case 2: INC_OFFSET; INC_TOKBYTES; break; - case 3: INC_OFFSET; INC_OFFSET; INC_TOKBYTES; INC_TOKBYTES; break; - case 4: INC_OFFSET; INC_OFFSET; INC_OFFSET; INC_TOKBYTES; INC_TOKBYTES; INC_TOKBYTES; INC_POSITION; INC_TOKUTF8LEN; break; + case 2: if (IS_EOF) return false; INC_OFFSET; INC_TOKBYTES; break; + case 3: if (IS_EOF) return false; INC_OFFSET; if (IS_EOF) return false; INC_OFFSET; INC_TOKBYTES; INC_TOKBYTES; break; + case 4: if (IS_EOF) return false; INC_OFFSET; if (IS_EOF) return false; INC_OFFSET; if (IS_EOF) return false; INC_OFFSET; INC_TOKBYTES; INC_TOKBYTES; INC_TOKBYTES; INC_POSITION; INC_TOKUTF8LEN; break; } if (result) *result = c; @@ -352,11 +352,14 @@ static gtoken_t lexer_scan_string(gravity_lexer_t *lexer) { // handle escaped characters if (c2 == '\\') { INC_OFFSET_POSITION; - INC_OFFSET_POSITION; - INC_TOKLEN; INC_TOKLEN; // sanity check + if (IS_EOF) return lexer_error(lexer, "Unexpected EOF inside a string literal"); + + INC_OFFSET_POSITION; + INC_TOKLEN; + if (IS_EOF) return lexer_error(lexer, "Unexpected EOF inside a string literal"); continue; } diff --git a/src/compiler/gravity_optimizer.c b/src/compiler/gravity_optimizer.c index d1b7e147..b8ada3f7 100644 --- a/src/compiler/gravity_optimizer.c +++ b/src/compiler/gravity_optimizer.c @@ -20,8 +20,8 @@ #define IS_NEG(inst) ((inst) && (inst->op == NEG)) #define IS_NUM(inst) ((inst) && (inst->op == LOADI)) #define IS_MATH(inst) ((inst) && (inst->op >= ADD) && (inst->op <= REM)) -#define IS_SKIP(inst) (inst->tag == SKIP_TAG) -#define IS_LABEL(inst) (inst->tag == LABEL_TAG) +#define IS_SKIP(inst) ((inst) && (inst->tag == SKIP_TAG)) +#define IS_LABEL(inst) ((inst) && (inst->tag == LABEL_TAG)) #define IS_NOTNULL(inst) (inst) #define IS_PRAGMA_MOVE_OPT(inst) ((inst) && (inst->tag == PRAGMA_MOVE_OPTIMIZATION)) @@ -325,15 +325,23 @@ static bool optimize_const_instruction (inst_t *inst, inst_t *inst1, inst_t *ins case DIV: // don't optimize in case of division by 0 - if ((int64_t)d2 == 0) return false; - if (type == DOUBLE_TAG) d = d1 / d2; - else n = n1 / n2; + if (type == DOUBLE_TAG) { + if (d2 == 0.0) return false; + d = d1 / d2; + } else { + if (n2 == 0) return false; + n = n1 / n2; + } break; case REM: - if ((int64_t)d2 == 0) return false; - if (type == DOUBLE_TAG) d = (double)((int64_t)d1 % (int64_t)d2); - else n = n1 % n2; + if (type == DOUBLE_TAG) { + if (d2 == 0.0) return false; + d = (double)((int64_t)d1 % (int64_t)d2); + } else { + if (n2 == 0) return false; + n = n1 % n2; + } break; default: diff --git a/src/compiler/gravity_parser.c b/src/compiler/gravity_parser.c index 5c95962d..aa931f91 100644 --- a/src/compiler/gravity_parser.c +++ b/src/compiler/gravity_parser.c @@ -168,6 +168,7 @@ static void report_error (gravity_parser_t *parser, error_type_t error_type, gto // build error message char buffer[1024]; + buffer[0] = 0; va_list arg; if (format) { va_start (arg, format); diff --git a/src/optionals/gravity_opt_env.c b/src/optionals/gravity_opt_env.c index 2a3307ff..9512a2fe 100644 --- a/src/optionals/gravity_opt_env.c +++ b/src/optionals/gravity_opt_env.c @@ -35,9 +35,7 @@ static gravity_list_t *argv = NULL; * */ static bool gravity_env_get(gravity_vm *vm, gravity_value_t *args, uint16_t nargs, uint32_t rindex) { - #pragma unused(nargs) - - if(!VALUE_ISA_STRING(args[1])) { + if(nargs < 2 || !VALUE_ISA_STRING(args[1])) { RETURN_ERROR("Environment variable key must be a string."); } @@ -63,9 +61,7 @@ static bool gravity_env_get(gravity_vm *vm, gravity_value_t *args, uint16_t narg * @retval Weather this function was successful or not. */ static bool gravity_env_set(gravity_vm *vm, gravity_value_t *args, uint16_t nargs, uint32_t rindex) { - #pragma unused(nargs) - - if(!VALUE_ISA_STRING(args[1]) || (!VALUE_ISA_STRING(args[2]) && !VALUE_ISA_NULL(args[2]))) { + if(nargs < 2 || !VALUE_ISA_STRING(args[1]) || (!VALUE_ISA_STRING(args[2]) && !VALUE_ISA_NULL(args[2]))) { RETURN_ERROR("Environment variable key and value must both be strings."); } diff --git a/src/optionals/gravity_opt_file.c b/src/optionals/gravity_opt_file.c index 8550ba68..f7c8508c 100644 --- a/src/optionals/gravity_opt_file.c +++ b/src/optionals/gravity_opt_file.c @@ -56,7 +56,7 @@ static gravity_file_t *gravity_ifile_new (gravity_vm *vm, FILE *f) { static bool internal_file_size (gravity_vm *vm, gravity_value_t *args, uint16_t nargs, uint32_t rindex) { // 1 parameter of type string is required - if (nargs != 2 && !VALUE_ISA_STRING(args[1])) { + if (nargs != 2 || !VALUE_ISA_STRING(args[1])) { RETURN_ERROR("A path parameter of type String is required."); } @@ -67,7 +67,7 @@ static bool internal_file_size (gravity_vm *vm, gravity_value_t *args, uint16_t static bool internal_file_exists (gravity_vm *vm, gravity_value_t *args, uint16_t nargs, uint32_t rindex) { // 1 parameter of type string is required - if (nargs != 2 && !VALUE_ISA_STRING(args[1])) { + if (nargs != 2 || !VALUE_ISA_STRING(args[1])) { RETURN_ERROR("A path parameter of type String is required."); } @@ -78,7 +78,7 @@ static bool internal_file_exists (gravity_vm *vm, gravity_value_t *args, uint16_ static bool internal_file_delete (gravity_vm *vm, gravity_value_t *args, uint16_t nargs, uint32_t rindex) { // 1 parameter of type string is required - if (nargs != 2 && !VALUE_ISA_STRING(args[1])) { + if (nargs != 2 || !VALUE_ISA_STRING(args[1])) { RETURN_ERROR("A path parameter of type String is required."); } @@ -89,7 +89,7 @@ static bool internal_file_delete (gravity_vm *vm, gravity_value_t *args, uint16_ static bool internal_file_read (gravity_vm *vm, gravity_value_t *args, uint16_t nargs, uint32_t rindex) { // 1 parameter of type string is required - if (nargs != 2 && !VALUE_ISA_STRING(args[1])) { + if (nargs != 2 || !VALUE_ISA_STRING(args[1])) { RETURN_ERROR("A path parameter of type String is required."); } @@ -106,7 +106,7 @@ static bool internal_file_read (gravity_vm *vm, gravity_value_t *args, uint16_t static bool internal_file_write (gravity_vm *vm, gravity_value_t *args, uint16_t nargs, uint32_t rindex) { // 2 parameters of type string are required - if (nargs != 3 && !VALUE_ISA_STRING(args[1]) && !VALUE_ISA_STRING(args[2])) { + if (nargs != 3 || !VALUE_ISA_STRING(args[1]) || !VALUE_ISA_STRING(args[2])) { RETURN_ERROR("A path parameter of type String and a String parameter are required."); } @@ -119,7 +119,7 @@ static bool internal_file_write (gravity_vm *vm, gravity_value_t *args, uint16_t static bool internal_file_buildpath (gravity_vm *vm, gravity_value_t *args, uint16_t nargs, uint32_t rindex) { // 2 parameters of type string are required - if (nargs != 3 && !VALUE_ISA_STRING(args[1]) && !VALUE_ISA_STRING(args[2])) { + if (nargs != 3 || !VALUE_ISA_STRING(args[1]) || !VALUE_ISA_STRING(args[2])) { RETURN_ERROR("A file and path parameters of type String are required."); } @@ -137,7 +137,7 @@ static bool internal_file_buildpath (gravity_vm *vm, gravity_value_t *args, uint static bool internal_file_is_directory (gravity_vm *vm, gravity_value_t *args, uint16_t nargs, uint32_t rindex) { // 1 parameter of type string is required - if (nargs != 2 && !VALUE_ISA_STRING(args[1])) { + if (nargs != 2 || !VALUE_ISA_STRING(args[1])) { RETURN_ERROR("A path parameter of type String is required."); } @@ -148,7 +148,7 @@ static bool internal_file_is_directory (gravity_vm *vm, gravity_value_t *args, u static bool internal_file_directory_create (gravity_vm *vm, gravity_value_t *args, uint16_t nargs, uint32_t rindex) { // 1 parameter of type string is required - if (nargs != 2 && !VALUE_ISA_STRING(args[1])) { + if (nargs != 2 || !VALUE_ISA_STRING(args[1])) { RETURN_ERROR("A path parameter of type String is required."); } @@ -218,12 +218,12 @@ static bool internal_file_directory_scan (gravity_vm *vm, gravity_value_t *args, // optional bool 2nd parameter int nindex = 2; bool recursive = true; - if (VALUE_ISA_BOOL(args[2])) { + if (nargs > 2 && VALUE_ISA_BOOL(args[2])) { recursive = VALUE_AS_BOOL(args[2]); nindex = 3; } - - if (!VALUE_ISA_CLOSURE(args[nindex])) { + + if (nargs <= (uint16_t)nindex || !VALUE_ISA_CLOSURE(args[nindex])) { RETURN_ERROR("A closure parameter is required."); } @@ -280,7 +280,7 @@ static bool internal_file_iread (gravity_vm *vm, gravity_value_t *args, uint16_t // var data = file.read(N) // 1 parameter of type int is required - if (nargs < 1 && (!VALUE_ISA_INT(args[1]) && !VALUE_ISA_STRING(args[1]))) { + if (nargs < 2 || (!VALUE_ISA_INT(args[1]) && !VALUE_ISA_STRING(args[1]))) { RETURN_ERROR("A parameter of type Int or String is required."); } @@ -344,8 +344,8 @@ static bool internal_file_iread (gravity_vm *vm, gravity_value_t *args, uint16_t static bool internal_file_iwrite (gravity_vm *vm, gravity_value_t *args, uint16_t nargs, uint32_t rindex) { // var written = file.write(data) - // 1 parameter of type int is required - if (nargs < 1 && !VALUE_ISA_STRING(args[1])) { + // 1 parameter of type string is required + if (nargs < 2 || !VALUE_ISA_STRING(args[1])) { RETURN_ERROR("A parameter of type String is required."); } @@ -360,7 +360,7 @@ static bool internal_file_iseek (gravity_vm *vm, gravity_value_t *args, uint16_t // var result = file.seek(offset, whence) // 2 parameters of type int are required - if (nargs != 3 && !VALUE_ISA_INT(args[1]) && !VALUE_ISA_INT(args[2])) { + if (nargs != 3 || !VALUE_ISA_INT(args[1]) || !VALUE_ISA_INT(args[2])) { RETURN_ERROR("An offset parameter of type Int and a whence parameter of type Int are required."); } diff --git a/src/optionals/gravity_opt_json.c b/src/optionals/gravity_opt_json.c index d256cb82..88461972 100644 --- a/src/optionals/gravity_opt_json.c +++ b/src/optionals/gravity_opt_json.c @@ -133,8 +133,10 @@ static bool JSON_parse (gravity_vm *vm, gravity_value_t *args, uint16_t nargs, u gravity_string_t *string = VALUE_AS_STRING(value); json_value *json = json_parse(string->s, string->len); if (!json) RETURN_VALUE(VALUE_FROM_NULL, rindex); - - RETURN_VALUE(JSON_value(vm, json), rindex); + + gravity_value_t result = JSON_value(vm, json); + json_value_free(json); + RETURN_VALUE(result, rindex); } //static bool JSON_begin_object (gravity_vm *vm, gravity_value_t *args, uint16_t nargs, uint32_t rindex) { diff --git a/src/optionals/gravity_opt_math.c b/src/optionals/gravity_opt_math.c index a38d4f87..99f6de5c 100644 --- a/src/optionals/gravity_opt_math.c +++ b/src/optionals/gravity_opt_math.c @@ -223,6 +223,11 @@ static bool math_xrt (gravity_vm *vm, gravity_value_t *args, uint16_t nargs, uin RETURN_VALUE(VALUE_FROM_INT(0), rindex); } + // check for division by zero in 1.0/base + if ((VALUE_ISA_INT(base) && base.n == 0) || (VALUE_ISA_FLOAT(base) && base.f == 0.0)) { + RETURN_VALUE(VALUE_FROM_UNDEFINED, rindex); + } + if (VALUE_ISA_INT(value) && VALUE_ISA_INT(base)) { gravity_float_t computed_value = (gravity_float_t)pow((gravity_float_t)value.n, 1.0/base.n); RETURN_VALUE(VALUE_FROM_FLOAT(computed_value), rindex); @@ -238,7 +243,7 @@ static bool math_xrt (gravity_vm *vm, gravity_value_t *args, uint16_t nargs, uin RETURN_VALUE(VALUE_FROM_FLOAT(computed_value), rindex); } - if (VALUE_ISA_FLOAT(value) && VALUE_ISA_INT(base)) { + if (VALUE_ISA_FLOAT(value) && VALUE_ISA_FLOAT(base)) { gravity_float_t computed_value = (gravity_float_t)pow((gravity_float_t)value.f, 1.0/base.f); RETURN_VALUE(VALUE_FROM_FLOAT(computed_value), rindex); } @@ -340,16 +345,14 @@ static bool math_floor (gravity_vm *vm, gravity_value_t *args, uint16_t nargs, u } static int gcf(int x, int y) { - if (x == 0) { - return y; - } + if (x < 0) x = -x; + if (y < 0) y = -y; + if (x == 0) return y; + if (y == 0) return x; while (y != 0) { - if (x > y) { - x = x - y; - } - else { - y = y - x; - } + int t = y; + y = x % y; + x = t; } return x; } @@ -372,7 +375,8 @@ static bool math_gcf (gravity_vm *vm, gravity_value_t *args, uint16_t nargs, uin } static int lcm(int x, int y) { - return x*y/gcf(x,y); + if (x == 0 || y == 0) return 0; + return (x / gcf(x, y)) * y; } static bool math_lcm (gravity_vm *vm, gravity_value_t *args, uint16_t nargs, uint32_t rindex) { diff --git a/src/runtime/gravity_core.c b/src/runtime/gravity_core.c index 126ec849..c96a5630 100644 --- a/src/runtime/gravity_core.c +++ b/src/runtime/gravity_core.c @@ -2487,7 +2487,11 @@ static bool string_repeat (gravity_vm *vm, gravity_value_t *args, uint16_t nargs } // figure out the size of the array we need to make to hold the new string - uint32_t new_size = (uint32_t)(main_str->len * times_to_repeat); + uint64_t computed_size = (uint64_t)main_str->len * (uint64_t)times_to_repeat; + if (computed_size > UINT32_MAX) { + RETURN_ERROR("String.repeat() would exceed maximum string size"); + } + uint32_t new_size = (uint32_t)computed_size; char *new_str = mem_alloc(vm, new_size+1); CHECK_MEM_ALLOC(new_str); diff --git a/src/runtime/gravity_vm.c b/src/runtime/gravity_vm.c index fe3d55e5..f6c480a5 100644 --- a/src/runtime/gravity_vm.c +++ b/src/runtime/gravity_vm.c @@ -2053,6 +2053,10 @@ gravity_closure_t *gravity_vm_loadbuffer (gravity_vm *vm, const char *buffer, si void_r objects; marray_init(objects); + void *saved = vm->data; + void_r stack; + marray_init(stack); + // start json parsing json_value *json = json_parse (buffer, len); if (!json) goto abort_load; @@ -2096,12 +2100,9 @@ gravity_closure_t *gravity_vm_loadbuffer (gravity_vm *vm, const char *buffer, si // fix superclass(es) size_t count = marray_size(objects); - if (count) { - void *saved = vm->data; + if (count) { // prepare stack to help resolve nested super classes - void_r stack; - marray_init(stack); vm->data = (void *)&stack; // loop of each processed object @@ -2122,7 +2123,9 @@ gravity_closure_t *gravity_vm_loadbuffer (gravity_vm *vm, const char *buffer, si report_runtime_error(vm, GRAVITY_ERROR_RUNTIME, "%s", "Unable to parse JSON executable file."); abort_super: + marray_destroy(stack); marray_destroy(objects); + vm->data = saved; if (json) json_value_free(json); gravity_gc_setenabled(vm, true); return NULL; diff --git a/src/shared/gravity_array.h b/src/shared/gravity_array.h index 38702642..eb44ad13 100644 --- a/src/shared/gravity_array.h +++ b/src/shared/gravity_array.h @@ -27,13 +27,14 @@ #define marray_inc(v) (++(v).n) #define marray_dec(v) (--(v).n) #define marray_nset(v,N) ((v).n = N) -#define marray_push(type, v, x) {if ((v).n == (v).m) { \ +#define marray_push(type, v, x) do {if ((v).n == (v).m) { \ (v).m = (v).m? (v).m<<1 : MARRAY_DEFAULT_SIZE; \ - (v).p = (type*)realloc((v).p, sizeof(type) * (v).m);} \ - (v).p[(v).n++] = (x);} -#define marray_resize(type, v, n) (v).m += n; (v).p = (type*)realloc((v).p, sizeof(type) * (v).m) -#define marray_resize0(type, v, n) (v).p = (type*)realloc((v).p, sizeof(type) * ((v).m+n)); \ - (v).m ? memset((v).p+(sizeof(type) * n), 0, (sizeof(type) * n)) : memset((v).p, 0, (sizeof(type) * n)); (v).m += n + void *_tmp = realloc((v).p, sizeof(type) * (v).m); \ + if (_tmp) (v).p = (type*)_tmp;} \ + if ((v).p) (v).p[(v).n++] = (x);} while(0) +#define marray_resize(type, v, n) do { (v).m += (n); (v).p = (type*)realloc((v).p, sizeof(type) * (v).m); } while(0) +#define marray_resize0(type, v, n) do { (v).p = (type*)realloc((v).p, sizeof(type) * ((v).m+(n))); \ + (v).m ? memset((v).p+(v).m, 0, (sizeof(type) * (n))) : memset((v).p, 0, (sizeof(type) * (n))); (v).m += (n); } while(0) #define marray_npop(v,k) ((v).n -= k) #define marray_reset(v,k) ((v).n = k) #define marray_reset0(v) marray_reset(v, 0) diff --git a/src/shared/gravity_macros.h b/src/shared/gravity_macros.h index c1fb8737..0c1f4242 100644 --- a/src/shared/gravity_macros.h +++ b/src/shared/gravity_macros.h @@ -87,7 +87,6 @@ #define VALUE_ISA_NULLCLASS(v) (v.isa == gravity_class_null) #define VALUE_ISA_NULL(v) ((v.isa == gravity_class_null) && (v.n == 0)) #define VALUE_ISA_UNDEFINED(v) ((v.isa == gravity_class_null) && (v.n == 1)) -#define VALUE_ISA_CLASS(v) (v.isa == gravity_class_class) #define VALUE_ISA_CALLABLE(v) (VALUE_ISA_FUNCTION(v) || VALUE_ISA_CLASS(v) || VALUE_ISA_FIBER(v)) #define VALUE_ISA_VALID(v) (v.isa != NULL) #define VALUE_ISA_NOTVALID(v) (v.isa == NULL) diff --git a/src/shared/gravity_memory.c b/src/shared/gravity_memory.c index 3406b462..67fd490c 100644 --- a/src/shared/gravity_memory.c +++ b/src/shared/gravity_memory.c @@ -154,7 +154,7 @@ void *memdebug_realloc(gravity_vm *vm, void *ptr, size_t new_size) { } void *new_ptr = realloc(ptr, new_size); - if (!ptr) { + if (!new_ptr) { BUILD_ERROR("Unable to reallocate a block of %zu bytes", new_size); BUILD_STACK(n, stack); memdebug_report(current_error, stack, n, &memdebug.slot[index]); diff --git a/src/shared/gravity_value.c b/src/shared/gravity_value.c index f002e2f2..dcd0fbc6 100644 --- a/src/shared/gravity_value.c +++ b/src/shared/gravity_value.c @@ -121,11 +121,14 @@ gravity_class_t *gravity_class_getsuper (gravity_class_t *c) { } bool gravity_class_grow (gravity_class_t *c, uint32_t n) { - if (c->ivars) mem_free(c->ivars); if (c->nivars + n >= MAX_IVARS) return false; - c->nivars += n; - c->ivars = (gravity_value_t *)mem_alloc(NULL, c->nivars * sizeof(gravity_value_t)); - for (uint32_t i=0; inivars; ++i) c->ivars[i] = VALUE_FROM_NULL; + uint32_t new_nivars = c->nivars + n; + gravity_value_t *new_ivars = (gravity_value_t *)mem_alloc(NULL, new_nivars * sizeof(gravity_value_t)); + if (!new_ivars) return false; + for (uint32_t i=0; iivars) mem_free(c->ivars); + c->ivars = new_ivars; + c->nivars = new_nivars; return true; } @@ -158,6 +161,8 @@ gravity_class_t *gravity_class_new_single (gravity_vm *vm, const char *identifie c->identifier = string_dup(identifier); c->superclass = NULL; c->nivars = nivar; + marray_init(c->inames); + c->htable = gravity_hash_create(0, gravity_value_hash, gravity_value_equals, gravity_hash_keyfree, NULL); if (nivar) { c->ivars = (gravity_value_t *)mem_alloc(NULL, nivar * sizeof(gravity_value_t)); @@ -209,13 +214,45 @@ uint32_t gravity_class_count_ivars (gravity_class_t *c) { } int16_t gravity_class_add_ivar (gravity_class_t *c, const char *identifier) { - #pragma unused(identifier) - // TODO: add identifier in array (for easier debugging) + marray_push(gravity_value_t, c->inames, (identifier) ? VALUE_FROM_CSTRING(NULL, identifier) : VALUE_FROM_NULL); ++c->nivars; return c->nivars-1; // its a C array so index is 0 based } +int16_t gravity_class_ivar_index (gravity_class_t *c, const char *identifier) { + // -1 means NOT FOUND + if (!identifier) return -1; + + size_t n = marray_size(c->inames); + for (size_t i=0; iinames, i); + if (VALUE_ISA_STRING(v)) { + const char *name = VALUE_AS_CSTRING(v); + if (string_cmp(identifier, name) == 0) return (int16_t)i; + } + } + return -1; +} + +static void gravity_class_dump_ivars(gravity_class_t *c) { + size_t n = marray_size(c->inames); + for (size_t i=0; iinames, i); + if (VALUE_ISA_NULL(v)) { + printf("%05zu\tNULL\n", i); + continue; + } + if (VALUE_ISA_STRING(v)) { + printf("%05zu\tSTRING: %s\n", i, VALUE_AS_CSTRING(v)); + continue; + } + // SHOULD NEVER REACH THIS POINT + printf("ivar type error in ivar %zu\n", i); + } +} + void gravity_class_dump (gravity_class_t *c) { + gravity_class_dump_ivars(c); gravity_hash_dump(c->htable); } @@ -243,6 +280,18 @@ void gravity_class_serialize (gravity_class_t *c, json_t *json) { // number of instance (and static) variables json_add_int(json, GRAVITY_JSON_LABELNIVAR, c->nivars); if ((c != meta) && (meta->nivars > 0)) json_add_int(json, GRAVITY_JSON_LABELSIVAR, meta->nivars); + + // ivar names + size_t n = marray_size(c->inames); + if (n > 0) { + json_begin_array(json, GRAVITY_JSON_LABELINAMES); + for (size_t i=0; iinames, i); + if (VALUE_ISA_STRING(v)) json_add_cstring(json, NULL, VALUE_AS_CSTRING(v)); + else json_add_null(json, NULL); + } + json_end_array(json); + } // struct flag if (c->is_struct) json_add_bool(json, GRAVITY_JSON_LABELSTRUCT, true); @@ -315,6 +364,18 @@ gravity_class_t *gravity_class_deserialize (gravity_vm *vm, json_value *json) { gravity_class_grow(meta, (uint32_t)value->u.integer); continue; } + + // inames + if (string_casencmp(key, GRAVITY_JSON_LABELINAMES, strlen(key)) == 0) { + uint32_t m = value->u.array.length; + for (uint32_t j=0; ju.array.values[j]; + gravity_object_t *obj = NULL; + if (r->type == json_string) obj = gravity_object_deserialize(NULL, r); + marray_push(gravity_value_t, c->inames, (obj) ? VALUE_FROM_OBJECT(obj) : VALUE_FROM_NULL); + } + continue; + } // struct if (string_casencmp(key, GRAVITY_JSON_LABELSTRUCT, strlen(key)) == 0) { @@ -373,6 +434,7 @@ static void gravity_class_free_internal (gravity_vm *vm, gravity_class_t *c, boo if (c->identifier) mem_free((void *)c->identifier); if (c->superlook) mem_free((void *)c->superlook); + marray_destroy(c->inames); if (!skip_base) { // base classes have functions not registered inside VM so manually free all of them diff --git a/src/shared/gravity_value.h b/src/shared/gravity_value.h index a2fd578c..900312f9 100644 --- a/src/shared/gravity_value.h +++ b/src/shared/gravity_value.h @@ -66,8 +66,8 @@ extern "C" { #endif -#define GRAVITY_VERSION "0.8.5" // git tag 0.8.5 -#define GRAVITY_VERSION_NUMBER 0x000805 // git push --tags +#define GRAVITY_VERSION "0.9.0" // git tag 0.9.0 +#define GRAVITY_VERSION_NUMBER 0x000900 // git push --tags #define GRAVITY_BUILD_DATE __DATE__ #ifndef GRAVITY_ENABLE_DOUBLE @@ -110,13 +110,13 @@ extern "C" { #define GLOBALS_DEFAULT_SLOT 4096 #define CPOOL_INDEX_MAX 4096 // 2^12 -#define CPOOL_VALUE_SUPER CPOOL_INDEX_MAX+1 -#define CPOOL_VALUE_NULL CPOOL_INDEX_MAX+2 -#define CPOOL_VALUE_UNDEFINED CPOOL_INDEX_MAX+3 -#define CPOOL_VALUE_ARGUMENTS CPOOL_INDEX_MAX+4 -#define CPOOL_VALUE_TRUE CPOOL_INDEX_MAX+5 -#define CPOOL_VALUE_FALSE CPOOL_INDEX_MAX+6 -#define CPOOL_VALUE_FUNC CPOOL_INDEX_MAX+7 +#define CPOOL_VALUE_SUPER (CPOOL_INDEX_MAX+1) +#define CPOOL_VALUE_NULL (CPOOL_INDEX_MAX+2) +#define CPOOL_VALUE_UNDEFINED (CPOOL_INDEX_MAX+3) +#define CPOOL_VALUE_ARGUMENTS (CPOOL_INDEX_MAX+4) +#define CPOOL_VALUE_TRUE (CPOOL_INDEX_MAX+5) +#define CPOOL_VALUE_FALSE (CPOOL_INDEX_MAX+6) +#define CPOOL_VALUE_FUNC (CPOOL_INDEX_MAX+7) #define MAX_INSTRUCTION_OPCODE 64 // 2^6 #define MAX_REGISTERS 256 // 2^8 @@ -133,12 +133,12 @@ extern "C" { #define DEFAULT_MINSTRING_SIZE 32 // minimum string allocation size #define DEFAULT_MINSTACK_SIZE 256 // sizeof(gravity_value_t) * 256 = 16 * 256 => 4 KB #define DEFAULT_MINCFRAME_SIZE 32 // sizeof(gravity_callframe_t) * 48 = 32 * 48 => 1.5 KB -#define DEFAULT_CG_THRESHOLD 5*1024*1024 // 5MB -#define DEFAULT_CG_MINTHRESHOLD 1024*1024 // 1MB +#define DEFAULT_CG_THRESHOLD (5*1024*1024) // 5MB +#define DEFAULT_CG_MINTHRESHOLD (1024*1024) // 1MB #define DEFAULT_CG_RATIO 0.5 // 50% -#define MAXNUM(a,b) ((a) > (b) ? a : b) -#define MINNUM(a,b) ((a) < (b) ? a : b) +#define MAXNUM(a,b) ((a) > (b) ? (a) : (b)) +#define MINNUM(a,b) ((a) < (b) ? (a) : (b)) #define EPSILON 0.000001 #define MIN_LIST_RESIZE 12 // value used when a List is resized @@ -180,7 +180,7 @@ typedef int64_t gravity_int_t; #else typedef int32_t gravity_int_t; #define GRAVITY_INT_MAX 2147483647 -#define GRAVITY_INT_MIN -2147483648 +#define GRAVITY_INT_MIN (-GRAVITY_INT_MAX-1) #endif // Forward references (an object ptr is just its isa pointer) @@ -379,7 +379,7 @@ typedef struct gravity_class_s { const char *superlook; // when a superclass is set to extern a runtime lookup must be performed gravity_hash_t *htable; // hash table uint32_t nivars; // number of instance variables - //gravity_value_r inames; // ivar names + gravity_value_r inames; // ivar names gravity_value_t *ivars; // static variables } gravity_class_s; @@ -464,6 +464,7 @@ GRAVITY_API uint32_t gravity_upvalue_size (gravity_vm *vm, gravity_up // MARK: - CLASS - GRAVITY_API void gravity_class_blacken (gravity_vm *vm, gravity_class_t *c); GRAVITY_API int16_t gravity_class_add_ivar (gravity_class_t *c, const char *identifier); +GRAVITY_API int16_t gravity_class_ivar_index (gravity_class_t *c, const char *identifier); GRAVITY_API void gravity_class_bind (gravity_class_t *c, const char *key, gravity_value_t value); GRAVITY_API uint32_t gravity_class_count_ivars (gravity_class_t *c); GRAVITY_API gravity_class_t *gravity_class_deserialize (gravity_vm *vm, json_value *json); diff --git a/src/utils/gravity_utils.c b/src/utils/gravity_utils.c index a35d197d..87836087 100644 --- a/src/utils/gravity_utils.c +++ b/src/utils/gravity_utils.c @@ -200,20 +200,20 @@ char *file_buildpath (const char *filename, const char *dirpath) { char *file_name_frompath (const char *path) { if (!path || (path[0] == 0)) return NULL; - + // must be sure to have a read-write memory address char *buffer = string_dup(path); - if (!buffer) return false; - + if (!buffer) return NULL; + char *name = NULL; size_t len = strlen(buffer); for (size_t i=len-1; i>0; --i) { if (buffer[i] == PATH_SEPARATOR) { - buffer[i] = 0; name = string_dup(&buffer[i + 1]); break; } } + mem_free(buffer); return name; } @@ -358,7 +358,7 @@ int string_casencmp(const char *s1, const char *s2, size_t n) { } int string_cmp (const char *s1, const char *s2) { - if (!s1) return 1; + if (!s1 || !s2) return (s1 == s2) ? 0 : (s1 ? -1 : 1); return strcmp(s1, s2); } diff --git a/test/unittest/class_grow_chain.gravity b/test/unittest/class_grow_chain.gravity new file mode 100644 index 00000000..3c952c55 --- /dev/null +++ b/test/unittest/class_grow_chain.gravity @@ -0,0 +1,30 @@ +#unittest { + name: "Deep class inheritance chain."; + error: NONE; + result: true; +}; + +class A { + var a = 1; +} + +class B : A { + var b = 2; +} + +class C : B { + var c = 3; +} + +class D : C { + var d = 4; +} + +func main() { + var obj = D(); + var r1 = (obj.a == 1); + var r2 = (obj.b == 2); + var r3 = (obj.c == 3); + var r4 = (obj.d == 4); + return r1 and r2 and r3 and r4; +} diff --git a/test/unittest/closure_nested_scope.gravity b/test/unittest/closure_nested_scope.gravity new file mode 100644 index 00000000..989bf6f8 --- /dev/null +++ b/test/unittest/closure_nested_scope.gravity @@ -0,0 +1,31 @@ +#unittest { + name: "Closure captures nested scope correctly."; + error: NONE; + result: true; +}; + +func main() { + var counter = 0; + + // Create a closure that captures and modifies outer variable + var increment = func() { + counter += 1; + return counter; + }; + + var r1 = (increment() == 1); + var r2 = (increment() == 2); + var r3 = (increment() == 3); + var r4 = (counter == 3); + + // Closure over loop variable + var funcs = []; + for (var i in 0...2) { + var val = i; + funcs.push(func() { return val; }); + } + var r5 = (funcs[0]() == 0); + var r6 = (funcs[2]() == 2); + + return r1 and r2 and r3 and r4 and r5 and r6; +} diff --git a/test/unittest/const_fold_div.gravity b/test/unittest/const_fold_div.gravity new file mode 100644 index 00000000..7745d03b --- /dev/null +++ b/test/unittest/const_fold_div.gravity @@ -0,0 +1,23 @@ +#unittest { + name: "Constant folding with division and modulo."; + error: NONE; + result: true; +}; + +func main() { + // Integer constant folding + var r1 = (100 / 5 == 20); + var r2 = (99 / 10 == 9); + var r3 = (17 % 5 == 2); + var r4 = (100 % 7 == 2); + + // Float constant folding + var r5 = (10.0 / 4.0 == 2.5); + + // Mixed operations + var r6 = (10 + 20 == 30); + var r7 = (50 - 30 == 20); + var r8 = (6 * 7 == 42); + + return r1 and r2 and r3 and r4 and r5 and r6 and r7 and r8; +} diff --git a/test/unittest/env_type_error.gravity b/test/unittest/env_type_error.gravity new file mode 100644 index 00000000..f8e545d6 --- /dev/null +++ b/test/unittest/env_type_error.gravity @@ -0,0 +1,9 @@ +#unittest { + name: "ENV.get rejects non-string key."; + error: RUNTIME; +}; + +func main() { + // Passing a non-string key should error + return ENV.get(12345); +} diff --git a/test/unittest/file_size_type_error.gravity b/test/unittest/file_size_type_error.gravity new file mode 100644 index 00000000..37436a9f --- /dev/null +++ b/test/unittest/file_size_type_error.gravity @@ -0,0 +1,9 @@ +#unittest { + name: "File.size rejects non-string arguments."; + error: RUNTIME; +}; + +func main() { + // Passing a list where a string path is expected should error + return File.size([1, 2, 3]); +} diff --git a/test/unittest/file_type_error.gravity b/test/unittest/file_type_error.gravity new file mode 100644 index 00000000..4a4408c9 --- /dev/null +++ b/test/unittest/file_type_error.gravity @@ -0,0 +1,9 @@ +#unittest { + name: "File functions reject non-string arguments."; + error: RUNTIME; +}; + +func main() { + // Passing an integer where a string path is expected should error + return File.exists(12345); +} diff --git a/test/unittest/file_write_type_error.gravity b/test/unittest/file_write_type_error.gravity new file mode 100644 index 00000000..94c4632d --- /dev/null +++ b/test/unittest/file_write_type_error.gravity @@ -0,0 +1,9 @@ +#unittest { + name: "File.write rejects non-string arguments."; + error: RUNTIME; +}; + +func main() { + // Passing integers where strings are expected should error + return File.write(123, 456); +} diff --git a/test/unittest/int_overflow_ops.gravity b/test/unittest/int_overflow_ops.gravity new file mode 100644 index 00000000..f0ef0087 --- /dev/null +++ b/test/unittest/int_overflow_ops.gravity @@ -0,0 +1,25 @@ +#unittest { + name: "Integer arithmetic operations."; + error: NONE; + result: true; +}; + +func main() { + // Basic operations + var r1 = (2 + 3 == 5); + var r2 = (10 - 7 == 3); + var r3 = (6 * 8 == 48); + var r4 = (20 / 4 == 5); + var r5 = (17 % 5 == 2); + + // Negative number operations + var r6 = (-5 + 3 == -2); + var r7 = (-3 * -4 == 12); + var r8 = (-10 / 2 == -5); + + // Bitwise operations + var r9 = (0xFF & 0x0F == 0x0F); + var r10 = (0xF0 | 0x0F == 0xFF); + + return r1 and r2 and r3 and r4 and r5 and r6 and r7 and r8 and r9 and r10; +} diff --git a/test/unittest/json_parse.gravity b/test/unittest/json_parse.gravity new file mode 100644 index 00000000..d0b32dc1 --- /dev/null +++ b/test/unittest/json_parse.gravity @@ -0,0 +1,29 @@ +#unittest { + name: "JSON parse and access."; + error: NONE; + result: true; +}; + +func main() { + // Parse a simple JSON object + var json_str = '{"name":"gravity","version":1,"active":true}'; + var obj = JSON.parse(json_str); + + var r1 = (obj["name"] == "gravity"); + var r2 = (obj["version"] == 1); + var r3 = (obj["active"] == true); + + // Parse JSON array + var json_arr = '[1, 2, 3, 4, 5]'; + var arr = JSON.parse(json_arr); + var r4 = (arr[0] == 1); + var r5 = (arr[4] == 5); + var r6 = (arr.count == 5); + + // Parse null values + var json_null = '{"key":null}'; + var obj2 = JSON.parse(json_null); + var r7 = (obj2["key"] == null); + + return r1 and r2 and r3 and r4 and r5 and r6 and r7; +} diff --git a/test/unittest/map_list_types.gravity b/test/unittest/map_list_types.gravity new file mode 100644 index 00000000..110a894a --- /dev/null +++ b/test/unittest/map_list_types.gravity @@ -0,0 +1,30 @@ +#unittest { + name: "Map and List type operations."; + error: NONE; + result: true; +}; + +func main() { + // List operations + var list = [10, 20, 30, 40, 50]; + var r1 = (list.count == 5); + var r2 = (list[0] == 10); + var r3 = (list[4] == 50); + + // List push and pop + list.push(60); + var r4 = (list.count == 6); + var popped = list.pop(); + var r5 = (popped == 60); + + // Map operations + var map = ["name": "gravity", "version": 1]; + var r6 = (map["name"] == "gravity"); + var r7 = (map["version"] == 1); + + // Map modification + map["new_key"] = true; + var r8 = (map["new_key"] == true); + + return r1 and r2 and r3 and r4 and r5 and r6 and r7 and r8; +} diff --git a/test/unittest/math/gcf_negative.gravity b/test/unittest/math/gcf_negative.gravity new file mode 100644 index 00000000..fbc8e3fb --- /dev/null +++ b/test/unittest/math/gcf_negative.gravity @@ -0,0 +1,21 @@ +#unittest { + name: "Test Math.gcf() with negative numbers."; + error: NONE; + result: true; +}; + +func main() { + // Negative arguments should work correctly (absolute values) + var r1 = Math.gcf(-12, 8) == 4; + var r2 = Math.gcf(12, -8) == 4; + var r3 = Math.gcf(-12, -8) == 4; + + // Zero cases + var r4 = Math.gcf(0, 5) == 5; + var r5 = Math.gcf(7, 0) == 7; + + // Large coprime numbers + var r6 = Math.gcf(17, 13) == 1; + + return r1 and r2 and r3 and r4 and r5 and r6; +} diff --git a/test/unittest/math/lcm_edge.gravity b/test/unittest/math/lcm_edge.gravity new file mode 100644 index 00000000..3602ea4c --- /dev/null +++ b/test/unittest/math/lcm_edge.gravity @@ -0,0 +1,25 @@ +#unittest { + name: "Test Math.lcm() edge cases."; + error: NONE; + result: true; +}; + +func main() { + // Basic cases + var r1 = Math.lcm(4, 6) == 12; + var r2 = Math.lcm(7, 5) == 35; + + // Same number + var r3 = Math.lcm(8, 8) == 8; + + // One is a multiple of the other + var r4 = Math.lcm(3, 9) == 9; + + // Coprime numbers + var r5 = Math.lcm(7, 11) == 77; + + // Larger numbers that would overflow with naive x*y approach + var r6 = Math.lcm(1000, 1500) == 3000; + + return r1 and r2 and r3 and r4 and r5 and r6; +} diff --git a/test/unittest/math/xrt_float.gravity b/test/unittest/math/xrt_float.gravity new file mode 100644 index 00000000..bb1549a6 --- /dev/null +++ b/test/unittest/math/xrt_float.gravity @@ -0,0 +1,24 @@ +#unittest { + name: "Test Math.xrt() with float arguments."; + error: NONE; + result: true; +}; + +func main() { + // Test int+int (already covered) + var r1 = Math.xrt(2, 9) == 3; + + // Test int+float + var r2 = Math.xrt(2.0, 16); + var r2ok = (r2 > 3.99 && r2 < 4.01); + + // Test float+int + var r3 = Math.xrt(2, 25.0); + var r3ok = (r3 > 4.99 && r3 < 5.01); + + // Test float+float (was previously dead code due to duplicate condition) + var r4 = Math.xrt(2.0, 100.0); + var r4ok = (r4 > 9.99 && r4 < 10.01); + + return r1 and r2ok and r3ok and r4ok; +} diff --git a/test/unittest/math/xrt_zero_base.gravity b/test/unittest/math/xrt_zero_base.gravity new file mode 100644 index 00000000..73972585 --- /dev/null +++ b/test/unittest/math/xrt_zero_base.gravity @@ -0,0 +1,14 @@ +#unittest { + name: "Test Math.xrt() with zero base."; + error: NONE; + result: true; +}; + +func main() { + // xrt with zero base should return undefined (division by zero in 1/base) + var r1 = Math.xrt(0, 16); + var r2 = Math.xrt(0.0, 25.0); + + // undefined comparisons should be false + return (r1 == undefined) and (r2 == undefined); +} diff --git a/test/unittest/optimizer_arithmetic.gravity b/test/unittest/optimizer_arithmetic.gravity new file mode 100644 index 00000000..5b0725d7 --- /dev/null +++ b/test/unittest/optimizer_arithmetic.gravity @@ -0,0 +1,31 @@ +#unittest { + name: "Optimizer constant folding arithmetic."; + error: NONE; + result: true; +}; + +func main() { + // These expressions should be constant-folded by the optimizer + + // Addition + var r1 = (3 + 7 == 10); + + // Subtraction + var r2 = (100 - 42 == 58); + + // Multiplication + var r3 = (13 * 7 == 91); + + // Division (was broken - always returned false due to wrong zero check) + var r4 = (84 / 12 == 7); + var r5 = (100 / 3 == 33); + + // Modulo (also was broken) + var r6 = (100 % 13 == 9); + var r7 = (27 % 4 == 3); + + // Negative results + var r8 = (5 - 10 == -5); + + return r1 and r2 and r3 and r4 and r5 and r6 and r7 and r8; +} diff --git a/test/unittest/range_operations.gravity b/test/unittest/range_operations.gravity new file mode 100644 index 00000000..e677a1f7 --- /dev/null +++ b/test/unittest/range_operations.gravity @@ -0,0 +1,33 @@ +#unittest { + name: "Range creation and iteration."; + error: NONE; + result: true; +}; + +func main() { + // Basic range + var sum = 0; + for (var i in 1...5) { + sum += i; + } + var r1 = (sum == 15); // 1+2+3+4+5 + + // Range count + var r = 1...10; + var r2 = (r.count == 10); + + // Range contains + var r3 = r.contains(5); + var r4 = !r.contains(11); + + // Negative step not needed, just test range bounds + var first = 0; + var count2 = 0; + for (var i in 1...3) { + if (count2 == 0) first = i; + count2 += 1; + } + var r5 = (first == 1 && count2 == 3); + + return r1 and r2 and r3 and r4 and r5; +} diff --git a/test/unittest/string_escape_sequences.gravity b/test/unittest/string_escape_sequences.gravity new file mode 100644 index 00000000..bc3b5728 --- /dev/null +++ b/test/unittest/string_escape_sequences.gravity @@ -0,0 +1,33 @@ +#unittest { + name: "String escape sequences."; + error: NONE; + result: true; +}; + +func main() { + // Basic escapes + var s1 = "hello\tworld"; + var r1 = s1.length == 11; + + // Newline + var s2 = "line1\nline2"; + var r2 = s2.length == 11; + + // Hex escape + var s3 = "\x41\x42\x43"; + var r3 = (s3 == "ABC"); + + // Unicode escape (2-byte UTF-8) + var s4 = "\u00E9"; // e with accent + var r4 = s4.bytes == 2; + + // Multiple escapes in sequence + var s5 = "\t\n\r"; + var r5 = s5.length == 3; + + // Escaped backslash + var s6 = "a\\b"; + var r6 = s6.length == 3; + + return r1 and r2 and r3 and r4 and r5 and r6; +} diff --git a/test/unittest/string_interpolation_complex.gravity b/test/unittest/string_interpolation_complex.gravity new file mode 100644 index 00000000..6f58686a --- /dev/null +++ b/test/unittest/string_interpolation_complex.gravity @@ -0,0 +1,33 @@ +#unittest { + name: "Complex string interpolation."; + error: NONE; + result: true; +}; + +func main() { + var x = 10; + var y = 20; + + // Basic interpolation + var s1 = "sum is \(x + y)"; + var r1 = (s1 == "sum is 30"); + + // Nested parentheses in interpolation + var s2 = "val \((x + y) * 2)"; + var r2 = (s2 == "val 60"); + + // Multiple interpolations + var s3 = "\(x) and \(y)"; + var r3 = (s3 == "10 and 20"); + + // Interpolation with method call + var name = "world"; + var s4 = "hello \(name.upper())"; + var r4 = (s4 == "hello WORLD"); + + // Empty prefix/suffix around interpolation + var s5 = "\(42)"; + var r5 = (s5 == "42"); + + return r1 and r2 and r3 and r4 and r5; +} diff --git a/test/unittest/string_repeat_overflow.gravity b/test/unittest/string_repeat_overflow.gravity new file mode 100644 index 00000000..12b0f1d3 --- /dev/null +++ b/test/unittest/string_repeat_overflow.gravity @@ -0,0 +1,12 @@ +#unittest { + name: "String repeat overflow protection."; + error: RUNTIME; +}; + +func main() { + // A long string repeated many times should trigger overflow protection + // even if each individual value is under the max limit + var s = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + // 26 * 200000000 > UINT32_MAX, should error + return s.repeat(200000000); +} diff --git a/test/unittest/string_repeat_valid.gravity b/test/unittest/string_repeat_valid.gravity new file mode 100644 index 00000000..81f8e592 --- /dev/null +++ b/test/unittest/string_repeat_valid.gravity @@ -0,0 +1,25 @@ +#unittest { + name: "String repeat with valid arguments."; + error: NONE; + result: true; +}; + +func main() { + // Basic repeat + var s1 = "ab".repeat(3); + var r1 = (s1 == "ababab"); + + // Repeat 1 time (identity) + var s2 = "hello".repeat(1); + var r2 = (s2 == "hello"); + + // Single char repeat + var s3 = "x".repeat(5); + var r3 = (s3 == "xxxxx"); + + // Verify length + var s4 = "abc".repeat(4); + var r4 = (s4.length == 12); + + return r1 and r2 and r3 and r4; +} diff --git a/test/unittest/string_unicode_escape.gravity b/test/unittest/string_unicode_escape.gravity new file mode 100644 index 00000000..513445fe --- /dev/null +++ b/test/unittest/string_unicode_escape.gravity @@ -0,0 +1,27 @@ +#unittest { + name: "Unicode escape sequences in strings."; + error: NONE; + result: true; +}; + +func main() { + // ASCII via unicode escape + var s1 = "\u0041"; // 'A' + var r1 = (s1 == "A"); + + // 2-byte UTF-8 (Latin chars with accents) + var s2 = "\u00E9"; // e-acute + var r2 = (s2.bytes == 2); + var r3 = (s2.length == 1); + + // 3-byte UTF-8 (CJK character) + var s3 = "\u4E16"; // Chinese char for 'world' + var r4 = (s3.bytes == 3); + var r5 = (s3.length == 1); + + // Multiple unicode escapes together + var s4 = "\u0048\u0065\u006C\u006C\u006F"; + var r6 = (s4 == "Hello"); + + return r1 and r2 and r3 and r4 and r5 and r6; +} From 633aa73992e7641cbe2bd1233f0e2ab74e6908bf Mon Sep 17 00:00:00 2001 From: Marco Bambini Date: Sat, 24 Jan 2026 09:29:32 +0100 Subject: [PATCH 02/37] Update README.md --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index 781c6d2f..8d608ef3 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,3 @@ -[![Build Status](https://travis-ci.com/marcobambini/gravity.svg?branch=master)](https://travis-ci.com/marcobambini/gravity) -

Gravity Programming Language

From f82bd06179c7269a288ec31b41b94dc4812f05ad Mon Sep 17 00:00:00 2001 From: Marco Bambini Date: Fri, 30 Jan 2026 09:00:52 +0100 Subject: [PATCH 03/37] Update index.html --- docs/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.html b/docs/index.html index 8552e9d4..d961ca67 100644 --- a/docs/index.html +++ b/docs/index.html @@ -29,7 +29,7 @@ hook.beforeEach(function (html) { let url = 'https://github.com/marcobambini/gravity/edit/master/docs/' + vm.route.file let rev = 'rev2' - let version = '0.8.0' + let version = '0.9.0' let edit = 'Edit on GitHub\n' return html From 76b3d7b52f55ed179329e37837f84feffc2e5778 Mon Sep 17 00:00:00 2001 From: iAndyHD3 <54410739+iAndyHD3@users.noreply.github.com> Date: Fri, 30 Jan 2026 21:13:06 +0100 Subject: [PATCH 04/37] fix clang build --- src/compiler/gravity_lexer.c | 24 ++++++++++++------------ src/compiler/gravity_optimizer.c | 6 +++--- src/runtime/gravity_core.c | 12 ++++++------ src/runtime/gravity_vm.c | 12 ++++++------ src/shared/gravity_hash.c | 4 ++-- src/shared/gravity_value.c | 32 ++++++++++++++++---------------- src/utils/gravity_utils.c | 2 +- 7 files changed, 46 insertions(+), 46 deletions(-) diff --git a/src/compiler/gravity_lexer.c b/src/compiler/gravity_lexer.c index 9408b19f..a10ac2e0 100644 --- a/src/compiler/gravity_lexer.c +++ b/src/compiler/gravity_lexer.c @@ -69,11 +69,11 @@ typedef enum { // MARK: - -static inline bool is_whitespace (int c) { +static bool is_whitespace (int c) { return ((c == ' ') || (c == '\t') || (c == '\v') || (c == '\f')); } -static inline bool is_newline (gravity_lexer_t *lexer, int c) { +static bool is_newline (gravity_lexer_t *lexer, int c) { // CR: Carriage Return, U+000D (UTF-8 in hex: 0D) // LF: Line Feed, U+000A (UTF-8 in hex: 0A) // CR+LF: CR (U+000D) followed by LF (U+000A) (UTF-8 in hex: 0D0A) @@ -105,35 +105,35 @@ static inline bool is_newline (gravity_lexer_t *lexer, int c) { return false; } -static inline bool is_comment (int c1, int c2) { +static bool is_comment (int c1, int c2) { return (c1 == '/') && ((c2 == '*') || (c2 == '/')); } -static inline bool is_semicolon (int c) { +static bool is_semicolon (int c) { return (c == ';'); } -static inline bool is_alpha (int c) { +static bool is_alpha (int c) { if (c == '_') return true; return isalpha(c); } -static inline bool is_digit (int c, gravity_number_type ntype) { +static bool is_digit (int c, gravity_number_type ntype) { if (ntype == NUMBER_BIN) return (c == '0' || (c == '1')); if (ntype == NUMBER_OCT) return (c >= '0' && (c <= '7')); if ((ntype == NUMBER_HEX) && ((toupper(c) >= 'A' && toupper(c) <= 'F'))) return true; return isdigit(c); } -static inline bool is_string (int c) { +static bool is_string (int c) { return ((c == '"') || (c == '\'')); } -static inline bool is_special (int c) { +static bool is_special (int c) { return (c == '@'); } -static inline bool is_builtin_operator (int c) { +static bool is_builtin_operator (int c) { // PARENTHESIS // { } [ ] ( ) // PUNCTUATION @@ -149,11 +149,11 @@ static inline bool is_builtin_operator (int c) { (c == '[') || (c == ']') || (c == '(') || (c == ')') ); } -static inline bool is_preprocessor (int c) { +static bool is_preprocessor (int c) { return (c == '#'); } -static inline bool is_identifier (int c) { +static bool is_identifier (int c) { // when called I am already sure first character is alpha so next valid characters are alpha, digit and _ return ((isalpha(c)) || (isdigit(c)) || (c == '_')); } @@ -172,7 +172,7 @@ static gtoken_t lexer_error(gravity_lexer_t *lexer, const char *message) { return TOK_ERROR; } -static inline bool next_utf8(gravity_lexer_t *lexer, int *result) { +static bool next_utf8(gravity_lexer_t *lexer, int *result) { int c = NEXT; INC_TOKLEN; diff --git a/src/compiler/gravity_optimizer.c b/src/compiler/gravity_optimizer.c index b8ada3f7..ba8251d0 100644 --- a/src/compiler/gravity_optimizer.c +++ b/src/compiler/gravity_optimizer.c @@ -222,7 +222,7 @@ static void finalize_function (gravity_function_t *f, bool add_debug) { // MARK: - -inline static bool pop1_instruction (ircode_t *code, uint32_t index, inst_t **inst1) { +static bool pop1_instruction (ircode_t *code, uint32_t index, inst_t **inst1) { *inst1 = NULL; for (int32_t i=index-1; i>=0; --i) { @@ -236,7 +236,7 @@ inline static bool pop1_instruction (ircode_t *code, uint32_t index, inst_t **in return false; } -inline static bool pop2_instructions (ircode_t *code, uint32_t index, inst_t **inst1, inst_t **inst2) { +static bool pop2_instructions (ircode_t *code, uint32_t index, inst_t **inst1, inst_t **inst2) { *inst1 = NULL; *inst2 = NULL; @@ -254,7 +254,7 @@ inline static bool pop2_instructions (ircode_t *code, uint32_t index, inst_t **i return false; } -inline static inst_t *current_instruction (ircode_t *code, uint32_t i) { +static inst_t *current_instruction (ircode_t *code, uint32_t i) { while (1) { inst_t *inst = ircode_get(code, i); if (inst == NULL) return NULL; diff --git a/src/runtime/gravity_core.c b/src/runtime/gravity_core.c index c96a5630..c6dc7315 100644 --- a/src/runtime/gravity_core.c +++ b/src/runtime/gravity_core.c @@ -173,7 +173,7 @@ static bool convert_object_string (gravity_vm *vm, gravity_value_t *args, uint16 RETURN_VALUE(v, rindex); } -static inline gravity_value_t convert_map2string (gravity_vm *vm, gravity_map_t *map) { +static gravity_value_t convert_map2string (gravity_vm *vm, gravity_map_t *map) { // allocate initial memory to a 512 buffer uint32_t len = 512; char *buffer = mem_alloc(NULL, len+1); @@ -249,7 +249,7 @@ static inline gravity_value_t convert_map2string (gravity_vm *vm, gravity_map_t return result; } -static inline gravity_value_t convert_list2string (gravity_vm *vm, gravity_list_t *list) { +static gravity_value_t convert_list2string (gravity_vm *vm, gravity_list_t *list) { // allocate initial memory to a 512 buffer uint32_t len = 512; char *buffer = mem_alloc(NULL, len+1); @@ -297,7 +297,7 @@ static inline gravity_value_t convert_list2string (gravity_vm *vm, gravity_list_ return result; } -inline gravity_value_t convert_value2int (gravity_vm *vm, gravity_value_t v) { +gravity_value_t convert_value2int (gravity_vm *vm, gravity_value_t v) { if (VALUE_ISA_INT(v)) return v; // handle conversion for basic classes @@ -320,7 +320,7 @@ inline gravity_value_t convert_value2int (gravity_vm *vm, gravity_value_t v) { return VALUE_FROM_ERROR(NULL); } -inline gravity_value_t convert_value2float (gravity_vm *vm, gravity_value_t v) { +gravity_value_t convert_value2float (gravity_vm *vm, gravity_value_t v) { if (VALUE_ISA_FLOAT(v)) return v; // handle conversion for basic classes @@ -343,7 +343,7 @@ inline gravity_value_t convert_value2float (gravity_vm *vm, gravity_value_t v) { return VALUE_FROM_ERROR(NULL); } -inline gravity_value_t convert_value2bool (gravity_vm *vm, gravity_value_t v) { +gravity_value_t convert_value2bool (gravity_vm *vm, gravity_value_t v) { if (VALUE_ISA_BOOL(v)) return v; // handle conversion for basic classes @@ -370,7 +370,7 @@ inline gravity_value_t convert_value2bool (gravity_vm *vm, gravity_value_t v) { return VALUE_FROM_ERROR(NULL); } -inline gravity_value_t convert_value2string (gravity_vm *vm, gravity_value_t v) { +gravity_value_t convert_value2string (gravity_vm *vm, gravity_value_t v) { if (VALUE_ISA_STRING(v)) return v; // handle conversion for basic classes diff --git a/src/runtime/gravity_vm.c b/src/runtime/gravity_vm.c index f6c480a5..af8d71c2 100644 --- a/src/runtime/gravity_vm.c +++ b/src/runtime/gravity_vm.c @@ -219,7 +219,7 @@ gravity_value_t gravity_vm_keyindex (gravity_vm *vm, uint32_t index) { return cache[index]; } -static inline gravity_callframe_t *gravity_new_callframe (gravity_vm *vm, gravity_fiber_t *fiber) { +static gravity_callframe_t *gravity_new_callframe (gravity_vm *vm, gravity_fiber_t *fiber) { #pragma unused(vm) // check if there are enough slots in the call frame and optionally create new cframes @@ -243,7 +243,7 @@ static inline gravity_callframe_t *gravity_new_callframe (gravity_vm *vm, gravit return &fiber->frames[fiber->nframes - 1]; } -static inline bool gravity_check_stack (gravity_vm *vm, gravity_fiber_t *fiber, uint32_t stacktopdelta, gravity_value_t **stackstart) { +static bool gravity_check_stack (gravity_vm *vm, gravity_fiber_t *fiber, uint32_t stacktopdelta, gravity_value_t **stackstart) { #pragma unused(vm) if (stacktopdelta == 0) return true; @@ -1562,22 +1562,22 @@ void gravity_vm_free (gravity_vm *vm) { mem_free(vm); } -inline gravity_value_t gravity_vm_lookup (gravity_vm *vm, gravity_value_t key) { +gravity_value_t gravity_vm_lookup (gravity_vm *vm, gravity_value_t key) { gravity_value_t *value = gravity_hash_lookup(vm->context, key); return (value) ? *value : VALUE_NOT_VALID; } -inline gravity_closure_t *gravity_vm_fastlookup (gravity_vm *vm, gravity_class_t *c, int index) { +gravity_closure_t *gravity_vm_fastlookup (gravity_vm *vm, gravity_class_t *c, int index) { #pragma unused(vm) return (gravity_closure_t *)gravity_class_lookup_closure(c, cache[index]); } -inline gravity_value_t gravity_vm_getvalue (gravity_vm *vm, const char *key, uint32_t keylen) { +gravity_value_t gravity_vm_getvalue (gravity_vm *vm, const char *key, uint32_t keylen) { STATICVALUE_FROM_STRING(k, key, keylen); return gravity_vm_lookup(vm, k); } -inline void gravity_vm_setvalue (gravity_vm *vm, const char *key, gravity_value_t value) { +void gravity_vm_setvalue (gravity_vm *vm, const char *key, gravity_value_t value) { gravity_hash_insert(vm->context, VALUE_FROM_CSTRING(vm, key), value); } diff --git a/src/shared/gravity_hash.c b/src/shared/gravity_hash.c index 1aa908c4..425f25cd 100644 --- a/src/shared/gravity_hash.c +++ b/src/shared/gravity_hash.c @@ -100,7 +100,7 @@ struct gravity_hash_t { #define COMPUTE_HASH_NOMODULO(key,hash) register uint32_t hash = murmur3_32(key, len, HASH_SEED_VALUE) #define RECOMPUTE_HASH(tbl,key,hash) hash = murmur3_32(key, len, HASH_SEED_VALUE); hash = hash % tbl->size -static inline uint32_t murmur3_32 (const char *key, uint32_t len, uint32_t seed) { +static uint32_t murmur3_32 (const char *key, uint32_t len, uint32_t seed) { static const uint32_t c1 = 0xcc9e2d51; static const uint32_t c2 = 0x1b873593; static const uint32_t r1 = 15; @@ -203,7 +203,7 @@ bool gravity_hash_isempty (gravity_hash_t *hashtable) { return (hashtable->count == 0); } -static inline int gravity_hash_resize (gravity_hash_t *hashtable) { +static int gravity_hash_resize (gravity_hash_t *hashtable) { uint32_t size = (hashtable->size * 2); gravity_hash_t newtbl = { .size = size, diff --git a/src/shared/gravity_value.c b/src/shared/gravity_value.c index dcd0fbc6..8bcc8e42 100644 --- a/src/shared/gravity_value.c +++ b/src/shared/gravity_value.c @@ -455,7 +455,7 @@ void gravity_class_free (gravity_vm *vm, gravity_class_t *c) { gravity_class_free_internal(vm, c, true); } -inline gravity_object_t *gravity_class_lookup (gravity_class_t *c, gravity_value_t key) { +gravity_object_t *gravity_class_lookup (gravity_class_t *c, gravity_value_t key) { while (c) { gravity_value_t *v = gravity_hash_lookup(c->htable, key); if (v) return (gravity_object_t *)v->p; @@ -472,13 +472,13 @@ gravity_class_t *gravity_class_lookup_class_identifier (gravity_class_t *c, cons return NULL; } -inline gravity_closure_t *gravity_class_lookup_closure (gravity_class_t *c, gravity_value_t key) { +gravity_closure_t *gravity_class_lookup_closure (gravity_class_t *c, gravity_value_t key) { gravity_object_t *obj = gravity_class_lookup(c, key); if (obj && OBJECT_ISA_CLOSURE(obj)) return (gravity_closure_t *)obj; return NULL; } -inline gravity_closure_t *gravity_class_lookup_constructor (gravity_class_t *c, uint32_t nparams) { +gravity_closure_t *gravity_class_lookup_constructor (gravity_class_t *c, uint32_t nparams) { if (c->xdata) { // bridged class so check for special $initN function if (nparams == 0) { @@ -1908,13 +1908,13 @@ uint32_t gravity_value_hash (gravity_value_t value) { return gravity_hash_compute_buffer((const char *)value.p, sizeof(gravity_object_t*)); } -inline gravity_class_t *gravity_value_getclass (gravity_value_t v) { +gravity_class_t *gravity_value_getclass (gravity_value_t v) { if ((v.isa == gravity_class_class) && (v.p->objclass == gravity_class_object)) return (gravity_class_t *)v.p; if ((v.isa == gravity_class_instance) || (v.isa == gravity_class_class)) return (v.p) ? v.p->objclass : NULL; return v.isa; } -inline gravity_class_t *gravity_value_getsuper (gravity_value_t v) { +gravity_class_t *gravity_value_getsuper (gravity_value_t v) { gravity_class_t *c = gravity_value_getclass(v); return (c && c->superclass) ? c->superclass : NULL; } @@ -2421,7 +2421,7 @@ void gravity_range_blacken (gravity_vm *vm, gravity_range_t *range) { // MARK: - -inline gravity_value_t gravity_string_to_value (gravity_vm *vm, const char *s, uint32_t len) { +gravity_value_t gravity_string_to_value (gravity_vm *vm, const char *s, uint32_t len) { gravity_string_t *obj = mem_alloc(NULL, sizeof(gravity_string_t)); if (len == AUTOLENGTH) len = (uint32_t)strlen(s); @@ -2443,7 +2443,7 @@ inline gravity_value_t gravity_string_to_value (gravity_vm *vm, const char *s, u return value; } -inline gravity_string_t *gravity_string_new (gravity_vm *vm, char *s, uint32_t len, uint32_t alloc) { +gravity_string_t *gravity_string_new (gravity_vm *vm, char *s, uint32_t len, uint32_t alloc) { gravity_string_t *obj = mem_alloc(NULL, sizeof(gravity_string_t)); if (len == AUTOLENGTH) len = (uint32_t)strlen(s); @@ -2457,13 +2457,13 @@ inline gravity_string_t *gravity_string_new (gravity_vm *vm, char *s, uint32_t l return obj; } -inline void gravity_string_set (gravity_string_t *obj, char *s, uint32_t len) { +void gravity_string_set (gravity_string_t *obj, char *s, uint32_t len) { obj->s = (char *)s; obj->len = len; obj->hash = gravity_hash_compute_buffer((const char *)s, len); } -inline void gravity_string_free (gravity_vm *vm, gravity_string_t *value) { +void gravity_string_free (gravity_vm *vm, gravity_string_t *value) { #pragma unused(vm) DEBUG_FREE("FREE %s", gravity_object_debug((gravity_object_t *)value, true)); if (value->alloc) mem_free(value->s); @@ -2483,30 +2483,30 @@ void gravity_string_blacken (gravity_vm *vm, gravity_string_t *string) { gravity_vm_memupdate(vm, gravity_string_size(vm, string)); } -inline gravity_value_t gravity_value_from_error(const char* msg) { +gravity_value_t gravity_value_from_error(const char* msg) { return ((gravity_value_t){.isa = NULL, .p = ((gravity_object_t *)msg)}); } -inline gravity_value_t gravity_value_from_object(void *obj) { +gravity_value_t gravity_value_from_object(void *obj) { return ((gravity_value_t){.isa = (((gravity_object_t *)(obj))->isa), .p = (gravity_object_t *)(obj)}); } -inline gravity_value_t gravity_value_from_int(gravity_int_t n) { +gravity_value_t gravity_value_from_int(gravity_int_t n) { return ((gravity_value_t){.isa = gravity_class_int, .n = (n)}); } -inline gravity_value_t gravity_value_from_float(gravity_float_t f) { +gravity_value_t gravity_value_from_float(gravity_float_t f) { return ((gravity_value_t){.isa = gravity_class_float, .f = (f)}); } -inline gravity_value_t gravity_value_from_null(void) { +gravity_value_t gravity_value_from_null(void) { return ((gravity_value_t){.isa = gravity_class_null, .n = 0}); } -inline gravity_value_t gravity_value_from_undefined(void) { +gravity_value_t gravity_value_from_undefined(void) { return ((gravity_value_t){.isa = gravity_class_null, .n = 1}); } -inline gravity_value_t gravity_value_from_bool(bool b) { +gravity_value_t gravity_value_from_bool(bool b) { return ((gravity_value_t){.isa = gravity_class_bool, .n = (b)}); } diff --git a/src/utils/gravity_utils.c b/src/utils/gravity_utils.c index 87836087..9ff34361 100644 --- a/src/utils/gravity_utils.c +++ b/src/utils/gravity_utils.c @@ -506,7 +506,7 @@ char *string_replace(const char *str, const char *from, const char *to, size_t * */ -inline uint32_t utf8_charbytes (const char *s, uint32_t i) { +uint32_t utf8_charbytes (const char *s, uint32_t i) { unsigned char c = (unsigned char)s[i]; // determine bytes needed for character, based on RFC 3629 From bb850e88847d4c07c07468c5b1b38c4875f723af Mon Sep 17 00:00:00 2001 From: Marco Bambini Date: Mon, 23 Feb 2026 13:24:01 +0100 Subject: [PATCH 05/37] =?UTF-8?q?Bump=20version=20to=200.9.5=20=E2=80=94?= =?UTF-8?q?=20extensive=20bug=20fixes,=20memory=20safety=20improvements,?= =?UTF-8?q?=20and=20documentation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Compiler - Lexer: fix off-by-one in PEEK_NEXT/PEEK_NEXT2 boundary checks - Parser: fix memory leaks in parse_file_expression, parse_variable_declaration, and string interpolation handling; fix undersized buffer for escaped strings - Semacheck2: fix wrong constant in upvalue limit error message (MAX_LOCALS → MAX_UPVALUES) - Codegen: fix wrong variable name in for-loop register allocation (temp2 → temp3); replace multiple early-return paths in visit_postfix_expr with goto cleanup to prevent leaking self_list/args arrays; fix register clobbering in LOCATION_CLASS_IVAR_OUTER assignment; remove unnecessary NOP for empty statements; add VISIT_MOVE_OPT macro to consolidate pragma patterns - Optimizer: fix constant folding checking wrong register (inst2->p2 → inst2->p1); fix NEG optimization writing to wrong destination register (inst->p2 → inst->p1); fix OPCODE_SET_FOUR8bit macro missing opcode field - IR code: convert register allocation from bool[256] array to compact bitmask (32 bytes), improving cache efficiency; add bounds checks on empty label stacks; add assertion after inst_new allocation ## Runtime / VM - Fix boolean fast-path comparison incorrectly applied to ordered comparisons (LT, GT, LEQ, GEQ) — now restricted to EQ/NEQ only - Fix crash in gravity_vm_setslot/gravity_vm_getslot when no frames exist - Fix OPCODE_GET_FOUR8bit macro inconsistency with encoder - Fix list_iterator_next out-of-bounds read (add bounds check) - Fix list_storeat duplicate marray_set after resize - Fix range_contains not handling reversed ranges correctly - Fix function_exec default-arg filling using wrong index formula - Fix int_random truncation from gravity_int_t to int - Fix string_count broken partial-match logic (rewrite using string_strnstr) - Fix string_upper/string_lower off-by-one (iterated one byte past end) - Fix string_loadat UTF-8 corruption when reversing multi-byte sequences - Fix string_loop iterating byte-by-byte instead of by UTF-8 character - Fix string_iterator/string_iterator_next missing bounds checks - Fix fiber_abort wrong nargs check (args[0] is self, not the message) - Fix system_input blindly stripping last char instead of checking for newline - Fix convert_map2string/convert_list2string losing original pointer on realloc failure - Fix gravity_class_grow discarding existing ivar values when growing - Add overflow guard to gravity_function_cpool_add (uint16_t limit) ## Optional Modules - Math: fix atan2 → atan2f for float builds; use POW macro consistently in math_xrt and math_round; fix division-by-zero in math_logx when base=1; fix integer overflow in math_random when range endpoint is GRAVITY_INT_MAX; fix pointer arithmetic bugs in math_round string truncation - File: fix memory leak in file_buildpath (result string not freed); fix leak in scan_directory recursive path; fix wrong nargs check in file_open; fix leaked FILE* when instance creation fails; fix crash on negative read size; fix swapped fread/fwrite arguments (size vs count); fix integer overflow in read buffer resize; fix typo "enought" → "enough" - JSON: rewrite string escaping in JSON.stringify to properly escape backslashes, quotes, control characters, and handle large strings - ENV: add Windows compatibility for environ access; simplify key-length loop ## Utilities - Debug: add buffer overflow protection in DUMP_VM macros; add bounds check in opcode_name; fix memory leak in gravity_disassemble - JSON serializer: fix escape buffer too small (len*2 → len*6+1); add control character escaping (\uXXXX); add overflow check - Utils: fix uninitialized fd in file_read; fix swapped PathCombineA arguments on Windows; fix file_name_frompath returning NULL when no separator found; add NULL check in string_dup; fix signed arithmetic and missing digit validation in number_from_bin ## Shared / Core Data Structures - gravity_array.h: fix marray_push updating capacity before confirming realloc succeeded; add bounds check before write; fix marray_resize/marray_resize0 to check realloc result - gravity_hash.c: fix gravity_hash_memsize counting buckets as nodes; improve float hashing precision (%f → %.17g) ## CLI - Fix memory leaks in unittest_scan (full_path not freed on skip/recurse) - Fix inline execution buffer leak ## Documentation - Add comprehensive ARCHITECTURE.md covering the full compilation pipeline, VM internals, value system, garbage collector, instruction set, and embedding API - Add CLAUDE.md with project conventions for Claude Code - Update README.md: add Building, Usage, and Project Structure sections; update line counts to reflect current codebase size; enhance Features list; add links to ARCHITECTURE.md ## Tests - Add 30 new unit tests covering all major bug fixes: bool comparison, string operations (count, upper/lower, UTF-8 iteration/reversal), math functions (round, logx, xrt, random), file I/O, JSON escaping, range operations, list bounds, fiber abort, optimizer constant folding, and more Co-Authored-By: Claude Opus 4.6 --- ARCHITECTURE.md | 1593 +++++++++++++++++ CLAUDE.md | 67 + README.md | 51 +- gravity.xcodeproj/project.pbxproj | 8 +- .../xcschemes/gravity.xcscheme | 10 +- src/cli/gravity.c | 20 +- src/compiler/gravity_codegen.c | 135 +- src/compiler/gravity_ircode.c | 113 +- src/compiler/gravity_lexer.c | 4 +- src/compiler/gravity_optimizer.c | 8 +- src/compiler/gravity_parser.c | 15 +- src/compiler/gravity_semacheck2.c | 2 +- src/optionals/gravity_opt_env.c | 14 +- src/optionals/gravity_opt_file.c | 16 +- src/optionals/gravity_opt_json.c | 58 +- src/optionals/gravity_opt_math.c | 27 +- src/runtime/gravity_core.c | 121 +- src/runtime/gravity_vm.c | 6 +- src/runtime/gravity_vmmacros.h | 2 +- src/shared/gravity_array.h | 16 +- src/shared/gravity_hash.c | 8 +- src/shared/gravity_value.c | 8 +- src/shared/gravity_value.h | 4 +- src/utils/gravity_debug.c | 16 +- src/utils/gravity_json.c | 12 +- src/utils/gravity_utils.c | 14 +- test/unittest/bugfix_bool_compare.gravity | 29 + test/unittest/bugfix_buildpath.gravity | 15 + .../bugfix_class_inherit_ivars.gravity | 27 + test/unittest/bugfix_const_folding.gravity | 32 + .../bugfix_fiber_abort_noargs.gravity | 8 + test/unittest/bugfix_file_open_noargs.gravity | 9 + .../bugfix_file_read_negative.gravity | 11 + test/unittest/bugfix_file_readwrite.gravity | 27 + .../unittest/bugfix_func_default_args.gravity | 27 + test/unittest/bugfix_inline_exec.gravity | 20 + test/unittest/bugfix_int_random_range.gravity | 28 + .../bugfix_json_stringify_escape.gravity | 36 + .../bugfix_json_stringify_long.gravity | 19 + test/unittest/bugfix_list_next_bounds.gravity | 29 + .../bugfix_list_storeat_resize.gravity | 19 + test/unittest/bugfix_math_logx_base1.gravity | 18 + .../unittest/bugfix_math_random_range.gravity | 19 + test/unittest/bugfix_math_round.gravity | 11 + .../bugfix_math_round_precision.gravity | 24 + test/unittest/bugfix_math_xrt.gravity | 28 + test/unittest/bugfix_neg_optimize.gravity | 31 + .../bugfix_range_backward_forin.gravity | 34 + test/unittest/bugfix_range_contains.gravity | 24 + test/unittest/bugfix_reverse_range.gravity | 13 + test/unittest/bugfix_string_count.gravity | 30 + .../bugfix_string_count_partial.gravity | 35 + test/unittest/bugfix_string_iteration.gravity | 27 + test/unittest/bugfix_string_loop_utf8.gravity | 28 + .../bugfix_string_reverse_utf8.gravity | 24 + .../bugfix_string_upper_lower.gravity | 11 + 56 files changed, 2805 insertions(+), 236 deletions(-) create mode 100644 ARCHITECTURE.md create mode 100644 CLAUDE.md create mode 100644 test/unittest/bugfix_bool_compare.gravity create mode 100644 test/unittest/bugfix_buildpath.gravity create mode 100644 test/unittest/bugfix_class_inherit_ivars.gravity create mode 100644 test/unittest/bugfix_const_folding.gravity create mode 100644 test/unittest/bugfix_fiber_abort_noargs.gravity create mode 100644 test/unittest/bugfix_file_open_noargs.gravity create mode 100644 test/unittest/bugfix_file_read_negative.gravity create mode 100644 test/unittest/bugfix_file_readwrite.gravity create mode 100644 test/unittest/bugfix_func_default_args.gravity create mode 100644 test/unittest/bugfix_inline_exec.gravity create mode 100644 test/unittest/bugfix_int_random_range.gravity create mode 100644 test/unittest/bugfix_json_stringify_escape.gravity create mode 100644 test/unittest/bugfix_json_stringify_long.gravity create mode 100644 test/unittest/bugfix_list_next_bounds.gravity create mode 100644 test/unittest/bugfix_list_storeat_resize.gravity create mode 100644 test/unittest/bugfix_math_logx_base1.gravity create mode 100644 test/unittest/bugfix_math_random_range.gravity create mode 100644 test/unittest/bugfix_math_round.gravity create mode 100644 test/unittest/bugfix_math_round_precision.gravity create mode 100644 test/unittest/bugfix_math_xrt.gravity create mode 100644 test/unittest/bugfix_neg_optimize.gravity create mode 100644 test/unittest/bugfix_range_backward_forin.gravity create mode 100644 test/unittest/bugfix_range_contains.gravity create mode 100644 test/unittest/bugfix_reverse_range.gravity create mode 100644 test/unittest/bugfix_string_count.gravity create mode 100644 test/unittest/bugfix_string_count_partial.gravity create mode 100644 test/unittest/bugfix_string_iteration.gravity create mode 100644 test/unittest/bugfix_string_loop_utf8.gravity create mode 100644 test/unittest/bugfix_string_reverse_utf8.gravity create mode 100644 test/unittest/bugfix_string_upper_lower.gravity diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 00000000..d229dae7 --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,1593 @@ +# Gravity Language Architecture + +This document provides a detailed technical description of the Gravity programming language implementation, covering the full compilation pipeline, runtime virtual machine, type system, garbage collector, embedding API, and supporting infrastructure. + +## Table of Contents + +- [1. High-Level Overview](#1-high-level-overview) +- [2. Compilation Pipeline](#2-compilation-pipeline) + - [2.1 Lexer](#21-lexer) + - [2.2 Parser](#22-parser) + - [2.3 Abstract Syntax Tree (AST)](#23-abstract-syntax-tree-ast) + - [2.4 Semantic Analysis — Pass 1](#24-semantic-analysis--pass-1) + - [2.5 Semantic Analysis — Pass 2](#25-semantic-analysis--pass-2) + - [2.6 Code Generation (AST → IR)](#26-code-generation-ast--ir) + - [2.7 IR Representation](#27-ir-representation) + - [2.8 Optimizer & Bytecode Emission](#28-optimizer--bytecode-emission) +- [3. Runtime Virtual Machine](#3-runtime-virtual-machine) + - [3.1 VM Structure](#31-vm-structure) + - [3.2 Instruction Dispatch](#32-instruction-dispatch) + - [3.3 Instruction Set](#33-instruction-set) + - [3.4 Instruction Encoding](#34-instruction-encoding) + - [3.5 Stack and Call Frames](#35-stack-and-call-frames) + - [3.6 Fiber (Coroutine) Model](#36-fiber-coroutine-model) + - [3.7 Execution Flow](#37-execution-flow) + - [3.8 Fast-Path Optimizations](#38-fast-path-optimizations) +- [4. Value System and Type Hierarchy](#4-value-system-and-type-hierarchy) + - [4.1 Value Representation](#41-value-representation) + - [4.2 Object Header and GC Metadata](#42-object-header-and-gc-metadata) + - [4.3 Built-in Types](#43-built-in-types) + - [4.4 Functions and Closures](#44-functions-and-closures) + - [4.5 Upvalues](#45-upvalues) + - [4.6 Classes and Instances](#46-classes-and-instances) +- [5. Garbage Collector](#5-garbage-collector) +- [6. Core Data Structures](#6-core-data-structures) + - [6.1 Hash Table](#61-hash-table) + - [6.2 Dynamic Array](#62-dynamic-array) + - [6.3 Memory Management](#63-memory-management) +- [7. Optional Modules](#7-optional-modules) + - [7.1 Registration Pattern](#71-registration-pattern) + - [7.2 Math Module](#72-math-module) + - [7.3 File Module](#73-file-module) + - [7.4 JSON Module](#74-json-module) + - [7.5 ENV Module](#75-env-module) +- [8. Embedding API](#8-embedding-api) + - [8.1 Compiler API](#81-compiler-api) + - [8.2 VM API](#82-vm-api) + - [8.3 Delegate Pattern](#83-delegate-pattern) + - [8.4 Bridging](#84-bridging) +- [9. Utilities](#9-utilities) +- [10. CLI](#10-cli) +- [11. Build System](#11-build-system) +- [12. Test Infrastructure](#12-test-infrastructure) + +--- + +## 1. High-Level Overview + +Gravity is a dynamically typed, embeddable programming language written in portable C99 with zero external dependencies (only stdlib). It features Swift-like syntax and supports procedural, object-oriented, functional, and prototype-based programming paradigms. + +The implementation follows a classic multi-pass compiler architecture that produces register-based bytecode executed by a stack-based virtual machine with coroutine (fiber) support. + +### Source Layout + +``` +src/ +├── cli/ CLI entry point (gravity.c) +├── compiler/ Lexer, parser, AST, semantic analysis, IR, optimizer, codegen +├── runtime/ Virtual machine (gravity_vm), built-in types (gravity_core) +├── shared/ Value representation, opcodes, hash table, dynamic array, memory/GC +├── optionals/ Optional modules: Math, File, JSON, ENV +└── utils/ Debug disassembler, JSON serialization, file I/O, UTF-8 utilities +``` + +### Full Pipeline + +``` +Source Code + │ + ▼ +┌─────────┐ +│ Lexer │ Character stream → Token stream +└────┬─────┘ + ▼ +┌─────────┐ +│ Parser │ Token stream → Abstract Syntax Tree +└────┬─────┘ + ▼ +┌──────────────┐ +│ Semacheck 1 │ Gather non-local declarations into symbol tables +└──────┬───────┘ + ▼ +┌──────────────┐ +│ Semacheck 2 │ Resolve identifiers, detect upvalues, validate scopes +└──────┬───────┘ + ▼ +┌──────────────┐ +│ Codegen │ AST → IR instructions (virtual registers) +└──────┬───────┘ + ▼ +┌──────────────┐ +│ Optimizer │ Constant folding, dead code elimination, label resolution +└──────┬───────┘ + ▼ +┌──────────────┐ +│ Bytecode │ Packed 32-bit instruction words +└──────┬───────┘ + ▼ +┌──────────────┐ +│ VM │ Register-based execution with computed goto dispatch +└──────────────┘ +``` + +The compiler entry point (`gravity_compiler_run` in `gravity_compiler.c`) orchestrates this pipeline: it creates a mini VM for GC during compilation, runs the parser to produce an AST, applies both semantic passes, generates IR code, optimizes it into final bytecode, and returns a `gravity_closure_t` ready for execution. + +--- + +## 2. Compilation Pipeline + +### 2.1 Lexer + +**Files:** `src/compiler/gravity_lexer.c`, `src/compiler/gravity_lexer.h` + +The lexer is a zero-allocation streaming tokenizer that scans source code character-by-character, producing tokens without copying or modifying the input buffer. Token values are pointers into the original source string. + +#### Lexer State + +```c +struct gravity_lexer_t { + const char *buffer; // source buffer (not owned) + uint32_t offset; // current byte offset + uint32_t position; // current character position (UTF-8 aware) + uint32_t length; // buffer length in bytes + uint32_t lineno; // 1-based line number + uint32_t colno; // 0-based column number + uint32_t fileid; // source file identifier + gtoken_s token; // current token + bool peeking; // in peek mode + gravity_delegate_t *delegate; // error callback +}; +``` + +#### Token Structure + +```c +struct gtoken_s { + gtoken_t type; // token type (enum) + uint32_t lineno; // line number + uint32_t colno; // column at end of token + uint32_t position; // byte offset of first character + uint32_t bytes; // length in bytes + uint32_t length; // length in UTF-8 characters + uint32_t fileid; // source file ID + gbuiltin_t builtin; // builtin identifier (__LINE__, __FILE__, etc.) + const char *value; // pointer into source buffer (NOT null-terminated) +}; +``` + +#### Token Categories (~80 total) + +| Category | Count | Examples | +|----------|-------|---------| +| General | 8 | `EOF`, `ERROR`, `COMMENT`, `STRING`, `NUMBER`, `IDENTIFIER`, `SPECIAL`, `MACRO` | +| Keywords | 36 | `func`, `class`, `var`, `const`, `if`, `else`, `for`, `while`, `return`, `import`, `enum`, `switch`, `true`, `false`, `null`, `undefined`, `super`, `isa`, ... | +| Operators | 36 | `+`, `-`, `*`, `/`, `%`, `&`, `\|`, `^`, `~`, `<<`, `>>`, `<`, `<=`, `==`, `!=`, `===`, `!==`, `~=`, `&&`, `\|\|`, `=`, `+=`, `..<`, `...`, ... | +| Punctuators | 10 | `(`, `)`, `[`, `]`, `{`, `}`, `;`, `:`, `.`, `,` | + +#### Key Features + +- **UTF-8 support:** Tracks both byte offset and character position separately using `utf8_charbytes()`. Handles 1–4 byte sequences. +- **Number literals:** Decimal, hexadecimal (`0x`), binary (`0b`), octal (`0o`), floating-point with scientific notation (`1.25e-2`). Uses state-machine with lookahead. +- **String literals:** Single (`'`) and double (`"`) quoted strings with backslash escapes. Multi-line strings tracked across line boundaries. +- **String interpolation:** Detected as `LITERAL_STRING_INTERPOLATED` for `"text \(expr)"` syntax. +- **Nested block comments:** `/* ... /* ... */ ... */` with a nesting depth counter. +- **Builtin identifiers:** `__LINE__`, `__FILE__`, `__COLUMN__`, `__CLASS__`, `__FUNC__` resolved during lexing. +- **Line separators:** CR, LF, CR+LF, NEL (U+0085), LS (U+2028). + +--- + +### 2.2 Parser + +**Files:** `src/compiler/gravity_parser.c`, `src/compiler/gravity_parser.h` + +The parser uses a **Pratt parser** (top-down operator precedence) to build an Abstract Syntax Tree. It supports a lexer stack for `#include` directives and maintains a declaration scope stack for context tracking. + +#### Parser State + +```c +struct gravity_parser_t { + lexer_r *lexer; // stack of lexers (for includes) + gnode_r *declarations; // declaration scope stack + gnode_r *statements; // statement list being built + gravity_delegate_t *delegate; // error callbacks + uint32_t nerrors; // accumulated error count + uint32_t unique_id; // unique identifier counter + uint32_t depth; // statement nesting depth + uint32_t expr_depth; // expression nesting depth +}; +``` + +#### Precedence Levels + +``` +PREC_LOWEST = 0 +PREC_ASSIGN = 90 = += -= *= /= %= <<= >>= &= |= ^= +PREC_TERNARY = 100 ?: +PREC_LOGICAL_OR = 110 || +PREC_LOGICAL_AND = 120 && +PREC_COMPARISON = 130 < <= > >= == != === !== ~= +PREC_ISA = 132 is +PREC_RANGE = 135 ..< ... +PREC_TERM = 140 + - | ^ +PREC_FACTOR = 150 * / % & +PREC_SHIFT = 160 << >> +PREC_UNARY = 170 + - ! ~ +PREC_CALL = 200 . ( [ +``` + +Each grammar rule carries a prefix handler, infix handler, precedence level, and a right-associativity flag: + +```c +typedef struct { + parse_func prefix; // prefix expression handler (or NULL) + parse_func infix; // infix expression handler (or NULL) + prec_level precedence; // binding power + const char *name; // operator name for diagnostics + bool right; // right-associative +} grammar_rule; +``` + +#### Statement Types + +- Compound statements (blocks) +- Variable/constant declarations (`var`, `const`, with optional type annotations and initialization) +- Function declarations (with parameters, default values) +- Class declarations (with inheritance, access modifiers, struct flag) +- Enum declarations +- Module declarations +- Control flow: `if`/`else`, `switch`/`case`/`default`, `for`, `while`, `repeat` +- Jump statements: `break`, `continue`, `return` +- Expression statements +- Empty statements + +#### Error Recovery + +- **One error per line:** Suppresses cascading errors from the same source line. +- **Token synchronization:** `parse_skip_until()` advances to a recovery point (e.g., next statement boundary). +- **Recursion limits:** `MAX_RECURSION_DEPTH = 1000` for statements, `MAX_EXPRESSION_DEPTH = 512` for expressions. + +--- + +### 2.3 Abstract Syntax Tree (AST) + +**Files:** `src/compiler/gravity_ast.c`, `src/compiler/gravity_ast.h` + +The AST uses a **non-uniform node design** — each node type has its own struct, but all share a common base for dispatch. A visitor pattern (`gvisitor_t`) is used for all tree traversals. + +#### Node Types (21 total) + +**Statements (7):** + +| Node | Purpose | +|------|---------| +| `NODE_LIST_STAT` | Root/global statement list | +| `NODE_COMPOUND_STAT` | Block with local scope and symbol table | +| `NODE_LABEL_STAT` | Switch case/default label | +| `NODE_FLOW_STAT` | `if`/`else`, `switch`, ternary | +| `NODE_JUMP_STAT` | `break`, `continue`, `return` | +| `NODE_LOOP_STAT` | `while`, `repeat`, `for` loops | +| `NODE_EMPTY_STAT` | Empty statement | + +**Declarations (6):** + +| Node | Purpose | +|------|---------| +| `NODE_ENUM_DECL` | Enumeration definition | +| `NODE_FUNCTION_DECL` | Function (with params, defaults, upvalue list) | +| `NODE_VARIABLE_DECL` | Variable/constant declaration group | +| `NODE_CLASS_DECL` | Class (with superclass, protocols, ivar counts) | +| `NODE_MODULE_DECL` | Module definition | +| `NODE_VARIABLE` | Individual variable within a declaration | + +**Expressions (8):** + +| Node | Purpose | +|------|---------| +| `NODE_BINARY_EXPR` | Binary operations | +| `NODE_UNARY_EXPR` | Unary operations | +| `NODE_FILE_EXPR` | `__FILE__` constant | +| `NODE_LIST_EXPR` | Array/map literals | +| `NODE_LITERAL_EXPR` | Numbers, strings, booleans | +| `NODE_IDENTIFIER_EXPR` | Variable references | +| `NODE_KEYWORD_EXPR` | `true`, `false`, `null`, `undefined`, `super` | +| `NODE_POSTFIX_EXPR` | Calls, subscripts, property access (with subtypes) | + +#### Base Node + +```c +typedef struct { + gnode_n tag; // node type discriminant + uint32_t refcount; // reference counting for shared nodes + uint32_t block_length; // byte length (for autocompletion) + gtoken_s token; // source location + bool is_assignment; // assignment target flag + void *decl; // enclosing declaration +} gnode_t; +``` + +#### Location Tracking + +After semantic analysis, each identifier is annotated with a resolved location: + +```c +typedef enum { + LOCATION_LOCAL, // local variable + LOCATION_GLOBAL, // global variable + LOCATION_UPVALUE, // closure upvalue + LOCATION_CLASS_IVAR_SAME, // instance variable (same class) + LOCATION_CLASS_IVAR_OUTER // instance variable (outer class) +} gnode_location_type; + +typedef struct { + gnode_location_type type; + uint16_t index; // symbol index + uint16_t nup; // upvalue or outer index +} gnode_location_t; +``` + +#### Visitor Pattern + +```c +typedef struct gvisitor { + uint32_t nerr; + void *data; // visitor-specific state + void *delegate; // error callback delegate + + // 22 callbacks — one per node type, plus pre/post hooks + void (*visit_pre)(visitor, node); + void (*visit_post)(visitor, node); + void (*visit_list_stmt)(visitor, node); + void (*visit_compound_stmt)(visitor, node); + void (*visit_function_decl)(visitor, node); + // ... one for each AST node type +} gvisitor_t; +``` + +The dispatch function `gvisit()` calls `visit_pre`, then the node-specific callback based on `node->tag`, then `visit_post`. + +--- + +### 2.4 Semantic Analysis — Pass 1 + +**File:** `src/compiler/gravity_semacheck1.c` + +The first semantic pass gathers all **non-local declarations** into symbol tables, enabling forward references. It does not perform full name resolution or type checking. + +#### What It Does + +1. Creates symbol tables for each scope (global, class, module, enum). +2. Inserts function, class, enum, module, and variable declarations. +3. Reports duplicate declaration errors. +4. Assigns instance variable indices for class members. +5. Applies name mangling for static class members (prefixed with `"$"`). + +#### Symbol Table + +```c +struct symboltable_t { + ghash_r *stack; // stack of hash tables (nested scopes) + uint16_t count1; // local variable counter + uint16_t count2; // instance variable counter + uint16_t count3; // static variable counter + symtable_tag tag; // GLOBAL, FUNC, CLASS, MODULE, or ENUM +}; +``` + +This pass enables forward references — a function can call another function declared later in the same scope: + +```swift +func foo() { return bar(); } +func bar() { return 42; } +``` + +--- + +### 2.5 Semantic Analysis — Pass 2 + +**File:** `src/compiler/gravity_semacheck2.c` + +The second semantic pass validates all identifiers within function bodies, resolves variable references, and detects closure upvalues. + +#### What It Does + +1. Validates all identifier references (reports "undefined variable" errors). +2. Resolves each identifier to its declaration and sets the `location` field. +3. Detects upvalue usage and builds upvalue lists for closures. +4. Validates declaration nesting constraints. +5. Checks `break`/`continue` appear only inside loops. +6. Validates module declarations are at global scope. + +#### Identifier Lookup Order + +The lookup traverses the declaration stack from innermost to outermost: + +1. Local scope (current compound statement) +2. Enclosing function scopes +3. Enclosing class scopes (including superclass hierarchy) +4. Module scope +5. Global scope + +#### Declaration Nesting Rules + +What can be declared inside each construct: + +``` + func var enum class module +func │ YES YES NO YES YES +var │ YES NO NO YES YES +enum │ YES NO NO YES YES +class │ YES NO NO YES YES +module │ NO NO NO NO NO +``` + +--- + +### 2.6 Code Generation (AST → IR) + +**File:** `src/compiler/gravity_codegen.c` + +The code generator walks the AST using the visitor pattern and emits IR instructions with virtual registers. It maintains a context stack of functions and classes being compiled. + +```c +struct codegen_t { + gravity_object_r context; // stack of functions/classes + gnode_class_r superfix; // superclass resolution stack + uint32_t lasterror; // last error line + gravity_vm *vm; // mini VM for GC during codegen +}; +``` + +#### Key Responsibilities + +- **Operator mapping:** Converts token operators to opcodes (e.g., `TOK_OP_ADD` → `ADD`). +- **Implicit self:** Inserts self parameter for instance methods. +- **Super calls:** Emits `LOADS` instruction for superclass method lookup. +- **Collection literals:** `LISTNEW`/`MAPNEW` + `SETLIST` instructions. +- **Range literals:** `RANGENEW` with inclusive/exclusive flag. +- **String interpolation:** Converts `"text \(expr)"` into string concatenation operations. +- **Closures:** `CLOSURE` instruction references the function in the constant pool; `CLOSE` releases upvalues when scope exits. + +--- + +### 2.7 IR Representation + +**Files:** `src/compiler/gravity_ircode.c`, `src/compiler/gravity_ircode.h` + +The IR is a flat sequence of instructions with virtual registers, acting as the bridge between the AST and final packed bytecode. + +#### IR Instruction + +```c +typedef struct { + opcode_t op; // operation code + optag_t tag; // metadata tag + int32_t p1, p2, p3; // operand parameters + union { + double d; // embedded float constant (DOUBLE_TAG) + int64_t n; // embedded int constant (INT_TAG) + }; + uint32_t lineno; // source line for debug info +} inst_t; +``` + +#### Instruction Tags + +| Tag | Meaning | +|-----|---------| +| `NO_TAG` | Normal instruction | +| `INT_TAG` | Carries an embedded integer literal | +| `DOUBLE_TAG` | Carries an embedded float literal | +| `LABEL_TAG` | Label marker (resolved to offset by optimizer) | +| `SKIP_TAG` | Dead instruction (removed by optimizer) | +| `RANGE_INCLUDE_TAG` | Inclusive range flag | +| `RANGE_EXCLUDE_TAG` | Exclusive range flag | +| `PRAGMA_MOVE_OPTIMIZATION` | Hint for move elimination | + +#### Register Allocation + +The IR uses a bitmask-based register allocator (256 registers max = 32 bytes of bitmask): + +- **Local registers** `[0 .. nlocals-1]`: Reserved for parameters and local variables. +- **Temp registers** `[nlocals .. 255]`: Allocated/freed for expression evaluation. +- Register 0 is always reserved. + +Key operations: +- `ircode_register_push_temp()` — allocate the next free temp register. +- `ircode_register_pop()` — free the most recently allocated temp register. +- `ircode_register_first_temp_available()` — find first free temp slot. + +#### Label Management + +Three separate label stacks manage control flow: +- `label_true` — target for true branch of conditionals. +- `label_false` — target for false branch. +- `label_check` — target for loop checks and safety guards. + +--- + +### 2.8 Optimizer & Bytecode Emission + +**Files:** `src/compiler/gravity_optimizer.c`, `src/compiler/gravity_optimizer.h` + +The optimizer is the final compilation stage. It converts IR instructions into packed 32-bit bytecodes, resolves labels, and applies peephole optimizations. + +#### Optimizations Performed + +1. **Constant folding:** Arithmetic on constant operands evaluated at compile time. + ``` + LOADI r1, 5 ; LOADI r2, 3 ; ADD r0, r1, r2 → LOADI r0, 8 + ``` + +2. **Dead code elimination:** Unreachable instructions after unconditional jumps/returns are marked `SKIP` and removed. + +3. **Move elimination:** Redundant `MOVE` instructions are detected via `PRAGMA_MOVE_OPTIMIZATION` hints and removed when safe. + +4. **Label resolution:** Symbolic labels are mapped to concrete instruction offsets. + +#### 32-Bit Instruction Encoding + +``` +Standard (3 operands): [ opcode:6 | A:8 | B:8 | C:10 ] +LOADI (immediate): [ opcode:6 | A:8 | sign:1 | N:17 ] +JUMP (offset): [ opcode:6 | N:26 ] +``` + +--- + +## 3. Runtime Virtual Machine + +### 3.1 VM Structure + +**Files:** `src/runtime/gravity_vm.c`, `src/runtime/gravity_vm.h` + +The VM is an opaque struct (`gravity_vm`) with the following key components: + +```c +struct gravity_vm { + // Execution + gravity_fiber_t *fiber; // current fiber (coroutine) + gravity_hash_t *context; // global variable table + gravity_delegate_t *delegate; // runtime delegate + uint32_t pc; // program counter + bool aborted; // runtime error flag + + // Recursion limits + uint32_t maxccalls; // max nested C calls (default: 100) + uint32_t nccalls; // current C call depth + gravity_int_t maxrecursion;// max recursive depth (0 = unlimited) + + // Garbage collector + int32_t gcenabled; // reference-counted enable flag + gravity_object_t *gchead; // linked list of all GC objects + gravity_object_r graylist; // mark phase gray list + gravity_object_r gctemp; // temporary GC-protected objects + gravity_int_t memallocated;// total allocated memory + gravity_int_t gcthreshold; // GC trigger threshold (default: 5MB) + gravity_int_t gcminthreshold; // minimum threshold (default: 1MB) + gravity_float_t gcratio; // threshold growth ratio (default: 0.5) + + // Callbacks + vm_transfer_cb transfer; // object allocation hook + vm_cleanup_cb cleanup; // VM cleanup hook + vm_filter_cb filter; // selective cleanup filter +}; +``` + +An internal operator name cache (`cache[GRAVITY_VTABLE_SIZE]`) holds pre-computed strings for operator method names (`"+"`, `"-"`, `"*"`, etc.) to avoid repeated allocations during dispatch. + +--- + +### 3.2 Instruction Dispatch + +**File:** `src/runtime/gravity_vmmacros.h` + +The VM uses **computed goto** for instruction dispatch (GCC/Clang), falling back to a `switch` statement on MSVC: + +```c +// Computed goto (GCC/Clang): +#define DISPATCH() goto *dispatchTable[OPCODE_GET_OPCODE(*ip)] + +// Switch fallback (MSVC): +#define INTERPRET_LOOP switch (OPCODE_GET_OPCODE(*ip)) +#define CASE_CODE(x) case x: +``` + +Computed goto provides O(1) dispatch with no branch prediction overhead. Each opcode is a label address stored in a static table, and `DISPATCH()` performs an indirect jump. + +Key macros in the dispatch loop: + +| Macro | Purpose | +|-------|---------| +| `OPCODE_GET_OPCODE(inst)` | Extract 6-bit opcode | +| `OPCODE_GET_ONE8bit_ONE18bit(inst, A, N)` | Decode register + immediate | +| `OPCODE_GET_THREE8bit(inst, A, B, C)` | Decode three register operands | +| `LOAD_FRAME()` | Synchronize local variables from fiber state | +| `STORE_FRAME()` | Save local variables back to fiber state | +| `PUSH_FRAME(closure, stackstart, dest, nargs)` | Create a new call frame | +| `FN_COUNTREG(f, nargs)` | Compute register window size: `max(nparams, nargs) + nlocals + ntemps` | + +--- + +### 3.3 Instruction Set + +The VM implements **56 opcodes** (6-bit opcode field supports up to 64): + +#### General (5) + +| Opcode | Description | +|--------|-------------| +| `RET0` | Return null | +| `HALT` | Stop VM execution | +| `NOP` | No operation | +| `RET` | Return value from register | +| `CALL` | Call function/closure | + +#### Load/Store (13) + +| Opcode | Semantics | +|--------|-----------| +| `LOAD` | `R(A) = R(B)[R(C)]` — property access | +| `LOADAT` | `R(A) = R(B)[R(C)]` — subscript access | +| `LOADS` | Super property access | +| `LOADK` | `R(A) = K(Bx)` — load constant from pool | +| `LOADG` | `R(A) = G[K(Bx)]` — load global | +| `LOADI` | `R(A) = N` — load inline integer | +| `LOADU` | `R(A) = U(B)` — load upvalue | +| `MOVE` | `R(A) = R(B)` — register copy | +| `STORE` | `R(B)[R(C)] = R(A)` — property write | +| `STOREAT` | `R(B)[R(C)] = R(A)` — subscript write | +| `STOREG` | `G[K(Bx)] = R(A)` — store global | +| `STOREU` | `U(B) = R(A)` — store upvalue | + +#### Jump (2) + +| Opcode | Semantics | +|--------|-----------| +| `JUMP` | Unconditional jump (26-bit signed offset) | +| `JUMPF` | Jump if false (18-bit signed offset) | + +#### Arithmetic & Logic (19) + +| Opcode | Operation | +|--------|-----------| +| `ADD`, `SUB`, `MUL`, `DIV`, `REM` | Arithmetic | +| `AND`, `OR` | Logical and/or | +| `LT`, `GT`, `LEQ`, `GEQ` | Ordered comparison | +| `EQ`, `NEQ` | Equality | +| `EQQ`, `NEQQ` | Strict equality (identity) | +| `ISA` | Instance-of check | +| `MATCH` | Pattern match (`~=`) | +| `NEG`, `NOT` | Unary negation/logical not | + +#### Bitwise (6) + +| Opcode | Operation | +|--------|-----------| +| `LSHIFT`, `RSHIFT` | Bit shifts | +| `BAND`, `BOR`, `BXOR` | Bitwise and/or/xor | +| `BNOT` | Bitwise complement | + +#### Collections (4) + +| Opcode | Semantics | +|--------|-----------| +| `MAPNEW` | `R(A) = new Map(B)` | +| `LISTNEW` | `R(A) = new List(B)` | +| `RANGENEW` | `R(A) = new Range(B, C, flag)` | +| `SETLIST` | Populate list/map from register range | + +#### Closures (2) + +| Opcode | Semantics | +|--------|-----------| +| `CLOSURE` | Create closure from function constant | +| `CLOSE` | Close open upvalues at register level | + +#### Special (1) + +| Opcode | Semantics | +|--------|-----------| +| `CHECK` | Clone struct value (enforces value semantics) | + +#### Operator Vtable + +Each class defines operator methods via a vtable indexed by `GRAVITY_VTABLE_INDEX`: + +```c +typedef enum { + GRAVITY_ADD_INDEX, // "+" + GRAVITY_SUB_INDEX, // "-" + GRAVITY_MUL_INDEX, // "*" + GRAVITY_DIV_INDEX, // "/" + // ... one for each overloadable operator + GRAVITY_EXEC_INDEX // "()" — call +} GRAVITY_VTABLE_INDEX; +``` + +--- + +### 3.4 Instruction Encoding + +All instructions are 32 bits wide with varying field layouts: + +``` +Standard 3-operand: [ opcode:6 ][ A:8 ][ B:8 ][ C:10 ] +Immediate (LOADI): [ opcode:6 ][ A:8 ][ sign:1 ][ N:17 ] +Jump (JUMP): [ opcode:6 ][ N:26 ] +``` + +Operand extraction uses bit shifts and masks: + +```c +#define OPCODE_GET_OPCODE(v) ((v >> 26) & 0x3F) +#define OPCODE_GET_THREE8bit(v, A, B, C) A = (v >> 18) & 0xFF; \ + B = (v >> 10) & 0xFF; \ + C = v & 0x3FF; +#define OPCODE_GET_ONE8bit_ONE18bit(v, A, N) A = (v >> 18) & 0xFF; \ + N = v & 0x3FFFF; +``` + +--- + +### 3.5 Stack and Call Frames + +#### Call Frame + +```c +typedef struct { + uint32_t *ip; // instruction pointer + uint32_t dest; // destination register for return value + uint16_t nargs; // actual argument count + gravity_list_t *args; // implicit _args array (if needed) + gravity_closure_t *closure; // closure being executed + gravity_value_t *stackstart; // first stack slot of this frame + bool outloop; // set when called from gravity_vm_runclosure +} gravity_callframe_t; +``` + +#### Stack Layout Per Frame + +``` +stackstart[0] = self (implicit first parameter) +stackstart[1..n] = explicit parameters +stackstart[n+1..m] = local variables +stackstart[m+1..p] = temporary values +``` + +#### Sliding Register Window + +When a `CALL` instruction executes, the register window for the callee starts at `r2+1` (where `r2` is the callable register). This sliding window design minimizes value copying between frames: + +``` +Caller: [ ... | self | arg1 | arg2 | ... ] + ↑ + rwin = r2 + 1 → callee's stackstart +Callee: [ self | arg1 | arg2 | locals... | temps... ] +``` + +The stack grows on demand (power-of-2 reallocation). When the stack is reallocated, all frame pointers are adjusted to maintain consistency. The stack never shrinks. + +--- + +### 3.6 Fiber (Coroutine) Model + +**Fibers** are Gravity's concurrency primitive. Each fiber has its own stack and call frame array, enabling cooperative multitasking. + +```c +typedef struct { + gravity_class_t *isa; + gravity_gc_t gc; + + // Stack + gravity_value_t *stack; // value stack buffer + gravity_value_t *stacktop; // current stack pointer + uint32_t stackalloc; // allocated capacity + + // Call frames + gravity_callframe_t *frames; // frame buffer + uint32_t nframes; // frames in use + uint32_t framesalloc; // allocated capacity + + // Closures + gravity_upvalue_t *upvalues; // open upvalue linked list + + // Status + gravity_fiber_status status; // NEVER_EXECUTED, RUNNING, ABORTED, TERMINATED, TRYING + char *error; // error message + bool trying; // inside try block + gravity_fiber_t *caller; // parent fiber + gravity_value_t result; // final result + + // Timing (for yield with timeout) + nanotime_t lasttime; + gravity_float_t timewait; + gravity_float_t elapsedtime; +} gravity_fiber_t; +``` + +Fiber status values: `FIBER_NEVER_EXECUTED`, `FIBER_RUNNING`, `FIBER_ABORTED_WITH_ERROR`, `FIBER_TERMINATED`, `FIBER_TRYING`. + +--- + +### 3.7 Execution Flow + +#### `gravity_vm_exec` — Main Bytecode Loop + +```c +bool gravity_vm_exec(gravity_vm *vm) { + DECLARE_DISPATCH_TABLE; + // Load fiber, frame, function, stackstart, ip, bytecode ... + + while (1) { + INTERPRET_LOOP { + CASE_CODE(ADD): { + // 1. Decode operands + // 2. Check fast path (inline int/float arithmetic) + // 3. Fallback: look up "+" method on r2's class + // 4. Call method, store result + DISPATCH(); + } + CASE_CODE(CALL): { + // 1. Decode: r1=dest, r2=callable, r3=nargs + // 2. Compute register window: rwin = r2 + 1 + // 3. Resolve closure (directly or via "exec" method) + // 4. Push frame, fill defaults for missing args + // 5. Dispatch by type: + // - NATIVE: PUSH_FRAME, continue loop + // - INTERNAL: call C function directly + // - BRIDGED: call delegate->bridge_execute + DISPATCH(); + } + CASE_CODE(RET): { + // 1. Pop frame + // 2. Close open upvalues + // 3. If outloop flag → return to gravity_vm_runclosure + // 4. Else → continue with caller frame + DISPATCH(); + } + // ... 53 more opcodes + } + } +} +``` + +#### `gravity_vm_runclosure` — External Entry Point + +Called from the embedding API or internally to invoke a specific closure: + +1. Validate VM is not aborted. +2. Set up stack window and parameters. +3. Dispatch by function type: + - **Native:** Increment `nccalls`, call `gravity_vm_exec()`, decrement. + - **Internal:** Call C function pointer directly. + - **Bridged:** Call delegate `bridge_execute` callback. +4. Restore frame pointers and adjust stack top. + +--- + +### 3.8 Fast-Path Optimizations + +- **Inline arithmetic:** When both operands are `Int` or `Float`, arithmetic is computed directly without method lookup. +- **Jump fusion:** Compare instructions (e.g., `EQ`, `LT`) peek ahead for a following `JUMPF`. If found, the compare and jump are fused into a single operation. +- **Register window:** The sliding register window avoids copying arguments between caller and callee. +- **Computed goto:** O(1) instruction dispatch with no branch prediction overhead. +- **Pre-allocated frames:** Call frames and stack space are pre-allocated and reused. + +--- + +## 4. Value System and Type Hierarchy + +### 4.1 Value Representation + +**Files:** `src/shared/gravity_value.h`, `src/shared/gravity_value.c` + +Gravity uses a **16-byte tagged union** for all values (not NaN-boxing): + +```c +typedef struct { + gravity_class_t *isa; // 8 bytes: type tag (pointer to class) + union { // 8 bytes: payload + gravity_int_t n; // integer value + gravity_float_t f; // float/double value + gravity_object_t *p; // pointer to heap object + }; +} gravity_value_t; +``` + +The `isa` pointer serves double duty: it identifies the type and provides the method lookup table. Special sentinel values: +- **Null:** `isa = NULL`, `n = 0` +- **Undefined:** `isa = NULL`, `n = 1` + +**Unboxed types** (value stored directly in the union): `Bool`, `Int`, `Float`, `Null`, `Undefined`. + +**Boxed types** (pointer to heap-allocated object): `String`, `List`, `Map`, `Class`, `Instance`, `Closure`, `Function`, `Range`, `Fiber`, `Upvalue`. + +--- + +### 4.2 Object Header and GC Metadata + +All heap-allocated objects share a common header: + +```c +typedef struct gravity_object_s { + gravity_class_t *isa; // class pointer (method dispatch) + gravity_gc_t gc; // GC metadata +} gravity_object_t; + +typedef struct { + bool isdark; // marked during GC + bool visited; // prevents double-counting in size calc + gravity_object_t *next; // intrusive linked list (GC object chain) + gravity_gc_callback free; // destructor callback + gravity_gc_callback size; // size reporting callback + gravity_gc_callback blacken; // mark-children callback +} gravity_gc_t; +``` + +Every heap object is linked into the VM's GC chain via `gc.next`. The three callbacks (`free`, `size`, `blacken`) implement type-specific GC behavior without virtual dispatch overhead. + +--- + +### 4.3 Built-in Types + +The runtime registers these built-in classes (in `gravity_core.c`): + +| Class | Behavior | +|-------|----------| +| `gravity_class_int` | 64-bit integer, arithmetic operators, bitwise ops | +| `gravity_class_float` | IEEE 754 double, arithmetic operators | +| `gravity_class_bool` | Boolean, logical operators | +| `gravity_class_null` | Null singleton | +| `gravity_class_string` | Immutable UTF-8 string, concatenation, methods | +| `gravity_class_object` | Base class (all types inherit from this) | +| `gravity_class_function` | Function prototype | +| `gravity_class_closure` | Closure (function + captured environment) | +| `gravity_class_fiber` | Fiber (coroutine) | +| `gravity_class_class` | Metaclass | +| `gravity_class_instance` | User-defined class instance | +| `gravity_class_list` | Dynamic array | +| `gravity_class_map` | Hash map | +| `gravity_class_range` | Integer range (inclusive or exclusive) | +| `gravity_class_upvalue` | Captured variable reference | + +Each class binds operator methods and instance methods. For example, `gravity_class_int` binds `"+"`, `"-"`, `"*"`, etc. as well as methods like `loop()`, `random()`, and conversion operators. + +--- + +### 4.4 Functions and Closures + +#### Function Prototype + +```c +typedef struct { + gravity_class_t *isa; + gravity_gc_t gc; + + const char *identifier; // function name + uint16_t nparams; // formal parameters (including self) + uint16_t nlocals; // local variables + uint16_t ntemps; // temporary registers + uint16_t nupvalues; // captured variables + gravity_exec_type tag; // execution type + + union { + // EXEC_TYPE_NATIVE (compiled Gravity code): + struct { + gravity_value_r cpool; // constant pool + gravity_value_r pvalue; // default parameter values + gravity_value_r pname; // parameter names + uint32_t ninsts; // instruction count + uint32_t *bytecode; // packed 32-bit instructions + uint32_t *lineno; // line number mapping (debug) + bool useargs; // needs implicit _args array + }; + + // EXEC_TYPE_INTERNAL (C callback): + gravity_c_internal internal; // bool (*)(vm, args, nargs, rindex) + + // EXEC_TYPE_SPECIAL (computed property): + struct { + uint16_t index; // property index + void *special[2]; // [0]=getter, [1]=setter + }; + }; +} gravity_function_t; +``` + +Execution types: +- `EXEC_TYPE_NATIVE` — compiled Gravity bytecode. +- `EXEC_TYPE_INTERNAL` — C function callback with signature `bool (*)(gravity_vm*, gravity_value_t*, uint16_t, uint32_t)`. +- `EXEC_TYPE_BRIDGED` — external bridge, executed via delegate callback. +- `EXEC_TYPE_SPECIAL` — getter/setter computed property. + +#### Closure + +```c +typedef struct { + gravity_class_t *isa; + gravity_gc_t gc; + gravity_vm *vm; // owning VM + gravity_function_t *f; // function prototype (shared) + gravity_object_t *context; // captured self reference + gravity_upvalue_t **upvalue; // captured upvalue array + uint32_t refcount; // bridge reference counting +} gravity_closure_t; +``` + +Multiple closures can share the same function prototype while having different captured environments. + +--- + +### 4.5 Upvalues + +Upvalues implement Lua-style **open/closed** variable capture: + +```c +typedef struct upvalue_s { + gravity_class_t *isa; + gravity_gc_t gc; + gravity_value_t *value; // points to stack slot (open) or self->closed (closed) + gravity_value_t closed; // storage when variable leaves scope + struct upvalue_s *next; // linked list (ordered by stack position) +} gravity_upvalue_t; +``` + +- **Open upvalue:** `value` points to a live stack slot. The fiber maintains a linked list of open upvalues ordered by descending stack address. +- **Closed upvalue:** When the enclosing function returns, the captured value is copied from the stack into `closed`, and `value` is repointed to `&self->closed`. + +The `CLOSE` instruction walks the open upvalue list and closes any upvalues at or above a given register level. + +--- + +### 4.6 Classes and Instances + +#### Class + +```c +typedef struct { + gravity_class_t *isa; // metaclass + gravity_gc_t gc; + + gravity_class_t *objclass; // metaclass reference + const char *identifier; // class name + bool has_outer; // has outer class ivar + bool is_struct; // value semantics (copy on assignment) + bool is_inited; // metaclass initialized + void *xdata; // bridge extension data + + gravity_class_t *superclass; // parent class + const char *superlook; // extern superclass name (lazy binding) + gravity_hash_t *htable; // method/property hash table + + uint32_t nivars; // instance variable count + gravity_value_r inames; // ivar names (debug) + gravity_value_t *ivars; // static (class) variables +} gravity_class_t; +``` + +Method resolution traverses the superclass chain. Methods and computed properties are stored in the class hash table. + +#### Instance + +```c +typedef struct { + gravity_class_t *isa; + gravity_gc_t gc; + gravity_class_t *objclass; // actual class + void *xdata; // bridge extension data + gravity_value_t *ivars; // instance variable array (indexed by position) +} gravity_instance_t; +``` + +Instance variables are stored in a flat array indexed by position (set during semacheck1), providing O(1) access. + +--- + +## 5. Garbage Collector + +**Location:** `src/runtime/gravity_vm.c` + +Gravity uses a **tri-color mark-and-sweep** garbage collector. + +### Mark Phase + +1. Mark temporary protected objects (in `vm->gctemp`). +2. Mark the current fiber as a root. +3. Mark all globals in the context hash table. +4. Process the gray list: for each gray object, call its `blacken` callback to mark all referenced objects. +5. Repeat until the gray list is empty. + +### Sweep Phase + +1. Walk the `vm->gchead` linked list. +2. For each object **not** marked (`!isdark`): call its `free` callback and remove it from the chain. +3. For each marked object: clear the `isdark` flag for the next cycle. + +### GC Triggers + +- **Automatic:** When `memallocated >= gcthreshold` during `gravity_gc_transfer` (object allocation). +- **Manual:** `gravity_gc_start(vm)`. +- **Stress test:** Every allocation (when compiled with `GRAVITY_GC_STRESSTEST`). + +### Dynamic Threshold Adjustment + +After each collection: +``` +new_threshold = memallocated + (memallocated * gcratio / 100) +if (new_threshold < minthreshold) new_threshold = minthreshold +if (new_threshold < original) new_threshold = original +``` + +Default values: `gcthreshold = 5MB`, `gcminthreshold = 1MB`, `gcratio = 0.5 (50%)`. + +### GC-Safe Coding Pattern + +The enable flag is reference-counted, allowing nested disable/enable calls: + +```c +gravity_gc_setenabled(vm, false); // disable GC (increments counter) +// ... allocate objects safely ... +gravity_gc_setenabled(vm, true); // re-enable (decrements counter) +``` + +Temporary objects can be protected from collection: + +```c +gravity_gc_temppush(vm, object); // protect +// ... use object ... +gravity_gc_temppop(vm); // unprotect +``` + +--- + +## 6. Core Data Structures + +### 6.1 Hash Table + +**Files:** `src/shared/gravity_hash.c`, `src/shared/gravity_hash.h` + +A chained hash table used for symbol tables, class method lookup, global variables, and the `Map` type. + +```c +typedef struct hash_node_s { + uint32_t hash; // cached hash value + gravity_value_t key; + gravity_value_t value; + struct hash_node_s *next; // collision chain +} hash_node_t; + +struct gravity_hash_t { + uint32_t size; // bucket count + uint32_t count; // entry count + hash_node_t **nodes; // bucket array + gravity_hash_compute_fn compute_fn; // hash function + gravity_hash_isequal_fn isequal_fn; // equality function + gravity_hash_iterate_fn free_fn; // entry cleanup callback + void *data; // callback context +}; +``` + +| Property | Value | +|----------|-------| +| Hash function | Murmur3-32 (seed 5381) | +| Collision resolution | Chaining (linked list per bucket) | +| Load factor | 0.75 | +| Growth strategy | Double bucket count on resize | +| Initial size | 32 buckets | +| Max entries | 2^30 | + +Hash function variants: `gravity_hash_compute_buffer()` for strings, `gravity_hash_compute_int()` for integers, `gravity_hash_compute_float()` for floats. + +--- + +### 6.2 Dynamic Array + +**File:** `src/shared/gravity_array.h` + +A macro-based generic dynamic array: + +```c +#define marray_t(type) struct { size_t n, m; type *p; } +// count capacity data +``` + +| Macro | Purpose | +|-------|---------| +| `marray_init(v)` | Initialize to zero | +| `marray_push(T, v, x)` | Append (doubles capacity if needed) | +| `marray_pop(v)` | Remove and return last element | +| `marray_get(v, i)` | Access element by index | +| `marray_size(v)` | Current element count | +| `marray_max(v)` | Current capacity | +| `marray_resize(T, v, n)` | Extend capacity to at least `n` | +| `marray_destroy(v)` | Free backing memory | + +Growth strategy: double capacity on each reallocation. + +--- + +### 6.3 Memory Management + +**Files:** `src/shared/gravity_memory.h`, `src/shared/gravity_memory.c` + +Production mode provides thin wrappers around `malloc`/`realloc`/`free` with max block size enforcement (`MAX_MEMORY_BLOCK = 150MB`). + +Debug mode (`GRAVITY_MEMORY_DEBUG`) adds: +- Tracking of every allocation with call stack. +- Detection of double-free and use-after-free. +- Leak reporting on shutdown. + +All allocations go through `mem_alloc()`, which integrates with the VM's `memallocated` counter for GC threshold tracking. + +--- + +## 7. Optional Modules + +### 7.1 Registration Pattern + +**File:** `src/optionals/gravity_optionals.h` + +Each optional module follows the same pattern: + +1. Compile-time guard (`#ifndef GRAVITY_INCLUDE_MATH` / `#define GRAVITY_INCLUDE_MATH`). +2. Macro wrappers that become no-ops when disabled. +3. Singleton class with reference counting. +4. Static methods bound to the metaclass. +5. Registration: `gravity_vm_setvalue(vm, name, class)`. + +```c +// Typical module lifecycle: +static gravity_class_t *gravity_class_math = NULL; +static uint32_t refcount = 0; + +void gravity_math_register(gravity_vm *vm) { + if (!gravity_class_math) create_optional_class(); + ++refcount; + gravity_vm_setvalue(vm, "Math", VALUE_FROM_OBJECT(gravity_class_math)); +} + +void gravity_math_free(void) { + if (--refcount) return; // wait for all VMs to unregister + // destroy class ... +} +``` + +Computed properties (read-only constants) use a getter-only closure: + +```c +gravity_closure_t *closure = computed_property_create(NULL, NEW_FUNCTION(getter), NULL); +gravity_class_bind(meta, "PI", VALUE_FROM_OBJECT(closure)); +``` + +--- + +### 7.2 Math Module + +**File:** `src/optionals/gravity_opt_math.c` — Class name: `"Math"` + +**Methods (23):** + +| Category | Functions | +|----------|-----------| +| Trigonometric | `sin`, `cos`, `tan`, `asin`, `acos`, `atan`, `atan2` | +| Rounding | `ceil`, `floor`, `round` (with optional precision) | +| Logarithmic | `log`, `log10`, `logx` (custom base) | +| Algebraic | `abs`, `sqrt`, `cbrt`, `xrt` (nth root), `pow`, `exp` | +| Combinatorial | `gcf`, `lcm` | +| Interpolation | `lerp` | +| Comparison | `min`, `max` (variadic) | +| Random | `random()`, `random(max)`, `random(min, max)` | + +**Constants (8):** `PI`, `E`, `LN2`, `LN10`, `LOG2E`, `LOG10E`, `SQRT2`, `SQRT1_2`. + +Random number generator: LFSR258 (64-bit) or LFSR113 (32-bit), seeded with `nanotime()` on first call. + +--- + +### 7.3 File Module + +**File:** `src/optionals/gravity_opt_file.c` — Class name: `"File"` + +Uses a custom `gravity_file_t` struct wrapping a `FILE*` pointer, with GC integration for automatic cleanup. + +**Class (static) methods:** `size`, `exists`, `delete`, `read`, `write`, `buildpath`, `is_directory`, `directory_create`, `directory_scan`. + +**Instance methods:** `open` (factory), `read`, `write`, `seek`, `eof`, `error`, `flush`, `close`. + +The `directory_scan` method accepts a closure callback invoked for each entry with `(filename, fullpath, isdir)`. + +--- + +### 7.4 JSON Module + +**File:** `src/optionals/gravity_opt_json.c` — Class name: `"JSON"` + +Two static methods: +- `stringify(value)` — Serialize any Gravity value to a JSON string. Handles nested structures, escapes special characters, uses heap allocation for strings >4KB. +- `parse(jsonString)` — Deserialize a JSON string into nested Gravity lists and maps. Returns `null` for invalid JSON. + +--- + +### 7.5 ENV Module + +**File:** `src/optionals/gravity_opt_env.c` — Class name: `"ENV"` + +**Methods:** `get(key)`, `set(key, value)`, `keys()`. + +**Properties:** `argc` (read-only), `argv` (read-only list). + +Supports map-access syntax: `ENV["PATH"]` via overloaded load/store-at handlers. Cross-platform: uses `_putenv_s` on Windows, `setenv` on Unix. + +--- + +## 8. Embedding API + +### 8.1 Compiler API + +**File:** `src/compiler/gravity_compiler.h` + +```c +gravity_compiler_t *gravity_compiler_create(gravity_delegate_t *delegate); +gravity_closure_t *gravity_compiler_run(compiler, source, len, fileid, is_static, add_debug); +gnode_t *gravity_compiler_ast(compiler); +void gravity_compiler_transfer(compiler, vm); // move objects to VM's GC +void gravity_compiler_free(compiler); +``` + +Serialization for ahead-of-time compilation: +```c +json_t *gravity_compiler_serialize(compiler, closure); +bool gravity_compiler_serialize_infile(compiler, closure, path); +``` + +--- + +### 8.2 VM API + +**File:** `src/runtime/gravity_vm.h` + +```c +// Lifecycle +gravity_vm *gravity_vm_new(gravity_delegate_t *delegate); +gravity_vm *gravity_vm_newmini(void); // lightweight (no optionals) +void gravity_vm_free(vm); +void gravity_vm_reset(vm); + +// Execution +bool gravity_vm_runmain(vm, closure); +bool gravity_vm_runclosure(vm, closure, sender, params, nparams); +gravity_value_t gravity_vm_result(vm); + +// Globals +void gravity_vm_setvalue(vm, key, value); +gravity_value_t gravity_vm_getvalue(vm, key, keylen); +gravity_value_t gravity_vm_lookup(vm, key); + +// Memory & GC +void gravity_vm_transfer(vm, object); +void gravity_gc_start(vm); +void gravity_gc_setenabled(vm, enabled); +void gravity_gc_setvalues(vm, threshold, minthreshold, ratio); + +// Bytecode loading +gravity_closure_t *gravity_vm_loadfile(vm, path); +gravity_closure_t *gravity_vm_loadbuffer(vm, buffer, len); + +// Optional modules +void gravity_opt_register(vm); +void gravity_opt_free(void); +``` + +--- + +### 8.3 Delegate Pattern + +**File:** `src/shared/gravity_delegate.h` + +The delegate is a struct of function pointers used for all communication between the compiler/VM and the host application: + +```c +typedef struct { + // Error handling + gravity_error_callback error_callback; // syntax, semantic, runtime errors + + // Compiler hooks + gravity_loadfile_callback loadfile_callback; // resolve import paths + gravity_filename_callback filename_callback; // map fileid → filename + gravity_precode_callback precode_callback; // inject code at parse time + gravity_parser_callback parser_callback; // syntax highlighting hook + gravity_type_callback type_callback; // bind type annotations + + // Logging + gravity_log_callback log_callback; + gravity_log_clear log_clear; + + // Bridge (C interop) + gravity_bridge_initinstance bridge_initinstance; + gravity_bridge_execute bridge_execute; + gravity_bridge_blacken bridge_blacken; + gravity_bridge_equals bridge_equals; + gravity_bridge_clone bridge_clone; + gravity_bridge_size bridge_size; + gravity_bridge_free bridge_free; + gravity_bridge_getvalue bridge_getvalue; + gravity_bridge_setvalue bridge_setvalue; + + // Testing + gravity_unittest_callback unittest_callback; +} gravity_delegate_t; +``` + +Error types: `GRAVITY_ERROR_SYNTAX`, `GRAVITY_ERROR_SEMANTIC`, `GRAVITY_ERROR_RUNTIME`, `GRAVITY_ERROR_IO`, `GRAVITY_WARNING`. + +--- + +### 8.4 Bridging + +Gravity supports binding external (C, Objective-C, Swift) objects through the bridge delegate callbacks: + +- `EXEC_TYPE_BRIDGED` functions are dispatched via `delegate->bridge_execute`. +- Instance creation goes through `delegate->bridge_initinstance`. +- Property access uses `bridge_getvalue` / `bridge_setvalue`. +- Objects store host-side data in the `xdata` pointer present on classes, instances, and functions. + +#### Typical Embedding Usage + +```c +// 1. Create compiler +gravity_delegate_t delegate = {.error_callback = report_error}; +gravity_compiler_t *compiler = gravity_compiler_create(&delegate); + +// 2. Compile +gravity_closure_t *closure = gravity_compiler_run( + compiler, source, strlen(source), 0, true, true); + +// 3. Create VM and transfer ownership +gravity_vm *vm = gravity_vm_new(&delegate); +gravity_compiler_transfer(compiler, vm); +gravity_compiler_free(compiler); + +// 4. Execute +if (gravity_vm_runmain(vm, closure)) { + gravity_value_t result = gravity_vm_result(vm); + // ... use result ... +} + +// 5. Cleanup +gravity_vm_free(vm); +gravity_core_free(); +``` + +--- + +## 9. Utilities + +### Debug / Disassembler + +**Files:** `src/utils/gravity_debug.c`, `src/utils/gravity_debug.h` + +- `opcode_name(opcode_t)` — maps opcode enum to mnemonic string. +- `opcode_constname(int)` — maps constant pool indices to names (`SUPER`, `NULL`, `UNDEFINED`, `TRUE`, `FALSE`, etc.). +- `gravity_disassemble()` — full bytecode disassembler; outputs human-readable assembly with line numbers and decoded operands. + +### JSON Serialization + +**Files:** `src/utils/gravity_json.c`, `src/utils/gravity_json.h` + +Two components: +- **Serializer:** `json_t` object with hierarchical `json_add_*()`, `json_begin/end_array()`, `json_begin/end_object()` functions. Used by the compiler to serialize bytecode to JSON. +- **Parser:** Third-party JSON parser (`json_parse()`) that produces a `json_value` tree. Used by `gravity_vm_loadfile` to deserialize compiled bytecode. + +### File I/O and Platform Utilities + +**Files:** `src/utils/gravity_utils.c`, `src/utils/gravity_utils.h` + +- High-resolution timer: `nanotime()` (platform-specific: `mach_absolute_time` on macOS, `clock_gettime` on Linux, `QueryPerformanceCounter` on Windows). +- File operations: `file_read`, `file_write`, `file_exists`, `file_delete`, `file_size`, `file_buildpath`. +- Directory operations: `directory_create`, `directory_init`, `directory_read`, `is_directory`. +- String utilities: `string_dup`, `string_replace`, `string_reverse`. +- UTF-8: `utf8_charbytes`, `utf8_encode`, `utf8_len`, `utf8_nbytes`, `utf8_reverse`. +- Number parsing: `number_from_bin`, `number_from_hex`, `number_from_oct`. + +--- + +## 10. CLI + +**File:** `src/cli/gravity.c` + +### Operation Modes + +| Flag | Mode | Description | +|------|------|-------------| +| *(filename)* | `OP_COMPILE_RUN` | Compile and execute in one pass | +| `-c file` | `OP_COMPILE` | Compile to bytecode file (default: `gravity.json`) | +| `-x file` | `OP_RUN` | Execute precompiled JSON bytecode | +| `-i 'code'` | `OP_INLINE_RUN` | Compile and execute inline string (wrapped in `func main() { ... }`) | +| `-t folder` | `OP_UNITTEST` | Run unit tests recursively | +| `-o file` | — | Specify output filename | +| `-q` | — | Quiet mode (suppress result and timing) | + +The CLI sets up a `gravity_delegate_t` with `error_callback` and `loadfile_callback` (for `import` resolution), then drives the compiler and VM through the standard embedding API. + +--- + +## 11. Build System + +**File:** `Makefile` + +### Targets + +| Target | Output | +|--------|--------| +| `make` | `gravity` CLI executable | +| `make mode=debug` | Debug build (`-g -O0 -DDEBUG`) | +| `make lib` | Shared library (`libgravity.dylib` / `.so` / `.dll`) | +| `make example` | C embedding API example | +| `make clean` | Remove all build artifacts | + +### Compiler Flags + +``` +-std=gnu99 -fgnu89-inline -fPIC -DBUILD_GRAVITY_API +-O2 (release) +-g -O0 -DDEBUG (debug) +``` + +### Platform Detection + +- **macOS:** `libgravity.dylib` +- **Linux/BSD:** `libgravity.so`, links `-lm` +- **Windows:** `gravity.dll`, links `Shlwapi` + +### Dependencies + +- C99-compatible compiler +- Standard C library (including `math.h`) +- Platform headers (`dirent.h`, `sys/time.h`, or Windows equivalents) +- No external library dependencies + +--- + +## 12. Test Infrastructure + +### Test Format + +Unit tests are individual `.gravity` files in `test/unittest/`. Each test declares expected results in a metadata block: + +```swift +#unittest { + name: "Test description"; + result: expected_value; +}; + +func main() { + // test logic + return actual_value; +} +``` + +The test runner compiles and executes each file, then compares the return value of `main()` against the declared `result`. + +### Test Metadata Fields + +| Field | Purpose | +|-------|---------| +| `name` | Human-readable test description | +| `result` | Expected return value (compared with `==`) | +| `expected_error` | Expected error type (for negative tests) | +| `expected_row` | Expected error line number | +| `expected_col` | Expected error column number | + +### Running Tests + +```bash +./gravity -t test/unittest/ # run all tests +./test/unittest/run_all.sh # run with timeouts (used by CI) +./gravity test/unittest/test_file.gravity # run a single test +``` + +### Test Organization + +Tests are organized by category in subdirectories: compiler phases, language features, built-in types, optional modules, edge cases, and bug regressions. The runner recursively scans the target directory, skips any `/disabled/` subdirectories, and applies fuzzy comparison for tests under `/fuzzy/`. + +CI runs: `make && test/unittest/run_all.sh` diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..ac6258ea --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,67 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +Gravity is a dynamically typed, embeddable programming language written in portable C99 with no external dependencies (except stdlib). It features Swift-like syntax and supports procedural, OOP, functional, and prototype-based programming paradigms. Originally developed for the Creo project for cross-platform iOS/Android scripting. + +## Build Commands + +```bash +make # Build the gravity CLI executable +make mode=debug # Debug build (-g -O0 -DDEBUG) +make lib # Build shared library (libgravity.dylib/so/dll) +make example # Build the C API example +make clean # Clean all build artifacts +``` + +Compiler flags: `-std=gnu99 -fgnu89-inline -fPIC -DBUILD_GRAVITY_API` + +## Testing + +```bash +./gravity -t test/unittest # Run all unit tests +./test/unittest/run_all.sh # Run all tests via shell script (with timeouts) +./gravity test/unittest/somefile.gravity # Run a single test file +./gravity -c test.gravity # Compile only (produces gravity.json) +./gravity -x gravity.json # Execute compiled bytecode +./gravity -i 'print("hello")' # Inline execution +``` + +CI runs: `make && test/unittest/run_all.sh` + +## Architecture + +The codebase follows a multi-pass compiler pipeline feeding into a bytecode VM: + +**Source → Lexer → Parser → AST → Semantic Check (2 passes) → IR → Optimizer → Bytecode → VM** + +### Source Layout + +- **`src/compiler/`** — Multi-pass compiler: lexer, parser, AST, two semantic analysis passes (`semacheck1`, `semacheck2`), IR code generation, optimizer, and final codegen +- **`src/runtime/`** — Stack-based virtual machine (`gravity_vm`), built-in classes/functions (`gravity_core`), VM execution macros +- **`src/shared/`** — Value representation and type system (`gravity_value`), hash table, dynamic array, memory management/GC, opcode definitions +- **`src/utils/`** — Debug utilities, JSON serialization, file I/O helpers +- **`src/optionals/`** — Optional modules (math, file, json, env) registered via `gravity_opt_register()` +- **`src/cli/gravity.c`** — CLI entry point + +### Key Design Patterns + +- The compiler uses an AST visitor pattern (`gravity_visitor`) for tree traversal +- The VM is stack-based with a mark-and-sweep garbage collector +- Optional modules are self-contained and registered at runtime +- The embedding API uses a delegate pattern (`gravity_delegate_t`) for callbacks (errors, logging, etc.) + +### Embedding API + +Core API in `src/runtime/` and example usage in `examples/example.c`: +- `gravity_compiler_create/run` — compile source to closures +- `gravity_vm_new/runmain/runclosure` — create VM and execute code +- `gravity_vm_loadfile/loadbuffer` — load from file or memory + +## Code Style + +- Private functions are `static` and don't use the `gravity_` prefix +- Public API functions use the `gravity_` prefix +- Unit tests are individual `.gravity` source files in `test/unittest/`, organized by compiler phase and feature area diff --git a/README.md b/README.md index 8d608ef3..1a1f0c4b 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ **Gravity** supports procedural programming, object-oriented programming, functional programming, and data-driven programming. Thanks to special built-in methods, it can also be used as a prototype-based programming language. -**Gravity** has been developed from scratch for the Creo project in order to offer an easy way to write portable code for the iOS and Android platforms. It is written in portable C code that can be compiled on any platform using a C99 compiler. The VM code is about 4K lines long, the multipass compiler code is about 7K lines and the shared code is about 3K lines long. The compiler and virtual machine combined add less than 200KB to the executable on a 64-bit system. +**Gravity** has been developed from scratch for the Creo project in order to offer an easy way to write portable code for the iOS and Android platforms. It is written in portable C code that can be compiled on any platform using a C99 compiler. The VM code is about 6.5K lines long, the multipass compiler code is about 10K lines and the shared code is about 4.7K lines long. The compiler and virtual machine combined add less than 200KB to the executable on a 64-bit system. ## What Gravity code looks like @@ -52,7 +52,7 @@ func main() { ``` ## Features -* multipass compiler +* multipass compiler with optimizer * dynamic typing * classes and inheritance * higher-order functions and classes @@ -60,18 +60,59 @@ func main() { * coroutines (via fibers) * nested classes * closures -* garbage collection +* garbage collection (mark-and-sweep) * operator overriding -* powerful embedding API +* string interpolation +* enums, modules, and structs (value types) +* switch/case and ranges +* optional modules (Math, File, JSON, ENV) +* powerful embedding API with bridging support * built-in unit tests * built-in JSON serializer/deserializer * **optional semicolons** +## Building + +```bash +make # Build the gravity CLI executable +make mode=debug # Debug build with symbols +make lib # Build shared library (libgravity.dylib/so/dll) +make example # Build the C embedding API example +make clean # Clean all build artifacts +``` + +Requires a C99 compiler. No external dependencies. + +## Usage + +```bash +./gravity file.gravity # Compile and execute a source file +./gravity -c file.gravity # Compile to bytecode (outputs gravity.json) +./gravity -o out.json -c file.gravity # Compile to a specific output file +./gravity -x gravity.json # Execute precompiled bytecode +./gravity -i 'return 2 + 3' # Execute inline code +./gravity -t test/unittest # Run unit tests +``` + +## Project Structure + +``` +src/ +├── cli/ Command-line interface +├── compiler/ Lexer, parser, AST, semantic analysis, IR, optimizer, codegen +├── runtime/ Stack-based VM, built-in types and core methods +├── shared/ Value representation, opcodes, hash table, array, memory/GC +├── optionals/ Optional modules: Math, File, JSON, ENV +└── utils/ Debug disassembler, JSON serialization, file I/O, UTF-8 +``` + +For a comprehensive technical deep-dive into the implementation, see [ARCHITECTURE.md](ARCHITECTURE.md). + ## Special thanks Gravity was supported by a couple of open-source projects. The inspiration for closures comes from the elegant Lua programming language; specifically from the document Closures in Lua. For fibers, upvalues handling and some parts of the garbage collector, my gratitude goes to Bob Nystrom and his excellent Wren programming language. A very special thanks should also go to my friend **Andrea Donetti** who helped me debugging and testing various aspects of the language. ## Documentation -The Getting Started page is a guide for downloading and compiling the language. There is also a more extensive language documentation. Official [wiki](https://github.com/marcobambini/gravity/wiki) is used to collect related projects and tools. +The Getting Started page is a guide for downloading and compiling the language. There is also a more extensive language documentation. Official [wiki](https://github.com/marcobambini/gravity/wiki) is used to collect related projects and tools. For implementation internals, see the [Architecture Document](ARCHITECTURE.md). ## Where Gravity is used * Gravity is the core language built into Creo (https://creolabs.com) diff --git a/gravity.xcodeproj/project.pbxproj b/gravity.xcodeproj/project.pbxproj index b8fb9a9d..68e4a510 100644 --- a/gravity.xcodeproj/project.pbxproj +++ b/gravity.xcodeproj/project.pbxproj @@ -273,7 +273,7 @@ A9506CE81E69AAEB009A0045 /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1230; + LastUpgradeCheck = 1410; ORGANIZATIONNAME = Creolabs; TargetAttributes = { A9506CEF1E69AAEB009A0045 = { @@ -369,6 +369,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = "-"; COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; @@ -426,6 +427,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = "-"; COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; @@ -449,6 +451,7 @@ CLANG_WARN_ASSIGN_ENUM = YES; CLANG_WARN_IMPLICIT_SIGN_CONVERSION = NO; CLANG_WARN_SUSPICIOUS_IMPLICIT_CONVERSION = NO; + DEAD_CODE_STRIPPING = YES; GCC_TREAT_IMPLICIT_FUNCTION_DECLARATIONS_AS_ERRORS = YES; GCC_TREAT_INCOMPATIBLE_POINTER_TYPE_WARNINGS_AS_ERRORS = YES; GCC_WARN_ABOUT_MISSING_FIELD_INITIALIZERS = YES; @@ -460,6 +463,7 @@ GCC_WARN_SIGN_COMPARE = NO; GCC_WARN_UNUSED_LABEL = YES; GCC_WARN_UNUSED_PARAMETER = YES; + MACOSX_DEPLOYMENT_TARGET = "$(RECOMMENDED_MACOSX_DEPLOYMENT_TARGET)"; PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Debug; @@ -470,6 +474,7 @@ CLANG_WARN_ASSIGN_ENUM = YES; CLANG_WARN_IMPLICIT_SIGN_CONVERSION = NO; CLANG_WARN_SUSPICIOUS_IMPLICIT_CONVERSION = NO; + DEAD_CODE_STRIPPING = YES; GCC_TREAT_IMPLICIT_FUNCTION_DECLARATIONS_AS_ERRORS = YES; GCC_TREAT_INCOMPATIBLE_POINTER_TYPE_WARNINGS_AS_ERRORS = YES; GCC_WARN_ABOUT_MISSING_FIELD_INITIALIZERS = YES; @@ -481,6 +486,7 @@ GCC_WARN_SIGN_COMPARE = NO; GCC_WARN_UNUSED_LABEL = YES; GCC_WARN_UNUSED_PARAMETER = YES; + MACOSX_DEPLOYMENT_TARGET = "$(RECOMMENDED_MACOSX_DEPLOYMENT_TARGET)"; PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Release; diff --git a/gravity.xcodeproj/xcuserdata/marco.xcuserdatad/xcschemes/gravity.xcscheme b/gravity.xcodeproj/xcuserdata/marco.xcuserdatad/xcschemes/gravity.xcscheme index d2a16a9d..1da4b693 100644 --- a/gravity.xcodeproj/xcuserdata/marco.xcuserdatad/xcschemes/gravity.xcscheme +++ b/gravity.xcodeproj/xcuserdata/marco.xcuserdatad/xcschemes/gravity.xcscheme @@ -1,6 +1,6 @@ - - - - + + - - is_fuzzy = (strstr(full_path, "/fuzzy/") != NULL); // load source code @@ -234,7 +235,7 @@ static void unittest_scan (const char *folder_path, unittest_data *data) { } } gravity_vm_free(vm); - + // case for empty files or simple declarations test if (!data->processed) { ++data->nsuccess; @@ -425,6 +426,8 @@ int main (int argc, const char* argv[]) { // pass argc and argv to the ENV class gravity_env_register_args(vm, argc, argv); + char *inline_buffer = NULL; + // check if input file is source code that needs to be compiled if ((type == OP_COMPILE) || (type == OP_COMPILE_RUN) || (type == OP_INLINE_RUN)) { @@ -447,10 +450,10 @@ int main (int argc, const char* argv[]) { // create closure to execute inline code if (type == OP_INLINE_RUN) { - char *buffer = mem_alloc(NULL, size+1024); - assert(buffer); - size = snprintf(buffer, size+1024, "func main() {%s};", input_file); - source_code = buffer; + inline_buffer = mem_alloc(NULL, size+1024); + assert(inline_buffer); + size = snprintf(inline_buffer, size+1024, "func main() {%s};", input_file); + source_code = inline_buffer; } // create compiler @@ -494,6 +497,7 @@ int main (int argc, const char* argv[]) { } cleanup: + if (inline_buffer) mem_free(inline_buffer); if (compiler) gravity_compiler_free(compiler); if (vm) gravity_vm_free(vm); gravity_core_free(); diff --git a/src/compiler/gravity_codegen.c b/src/compiler/gravity_codegen.c index 5abf08d4..14835352 100644 --- a/src/compiler/gravity_codegen.c +++ b/src/compiler/gravity_codegen.c @@ -60,6 +60,12 @@ typedef struct codegen_t codegen_t; #define CODEGEN_ASSERT_REGISTERS(_n1,_n2,_v) #endif +#define VISIT_MOVE_OPT(e) do { \ + ircode_pragma(code, PRAGMA_MOVE_OPTIMIZATION, 1, LINE_NUMBER(node)); \ + visit(e); \ + ircode_pragma(code, PRAGMA_MOVE_OPTIMIZATION, 0, LINE_NUMBER(node)); \ +} while(0) + // MARK: - static void report_error (gvisitor_t *self, gnode_t *node, const char *format, ...) { codegen_t *current = (codegen_t *)self->data; @@ -516,6 +522,12 @@ static void visit_flow_ternary_stmt (gvisitor_t *self, gnode_flow_stmt_t *node) DEBUG_CODEGEN("visit_flow_ternary_stmt"); DECLARE_CODE(); + // Both branches must produce their result in the same register slot. + // This works because: after popping the condition register, the allocator + // is at state S. The true branch visits+pops (returning to S), then the + // false branch visits from the same state S — deterministic allocation + // guarantees both branches push their result into the same register. + // At runtime only one branch executes, but both target the same slot. uint32_t reg; uint32_t label_false = ircode_newlabel(code); uint32_t label_final = ircode_newlabel(code); @@ -740,8 +752,8 @@ static void visit_loop_for_stmt (gvisitor_t *self, gnode_loop_stmt_t *node) { ircode_add(code, MOVE, temp1, iterate_fn, 0, LINE_NUMBER(node)); temp2 = ircode_register_push_temp(code); // ++TEMP => 6 ircode_add(code, MOVE, temp2, $expr, 0, LINE_NUMBER(node)); - temp2 = ircode_register_push_temp(code); // ++TEMP => 7 - ircode_add(code, MOVE, temp2, $value, 0, LINE_NUMBER(node)); + temp3 = ircode_register_push_temp(code); // ++TEMP => 7 + ircode_add(code, MOVE, temp3, $value, 0, LINE_NUMBER(node)); ircode_add(code, CALL, $value, temp1, 2, LINE_NUMBER(node)); temp = ircode_register_pop(code); // --TEMP => 6 DEBUG_ASSERT(temp != REGISTER_ERROR, "Unexpected register error."); @@ -821,9 +833,6 @@ static void visit_jump_stmt (gvisitor_t *self, gnode_jump_stmt_t *node) { static void visit_empty_stmt (gvisitor_t *self, gnode_empty_stmt_t *node) { #pragma unused(self, node) DEBUG_CODEGEN("visit_empty_stmt"); - - DECLARE_CODE(); - ircode_add(code, NOP, 0, 0, 0, LINE_NUMBER(node)); } // MARK: - Declarations - @@ -1440,6 +1449,12 @@ static void visit_postfix_expr (gvisitor_t *self, gnode_postfix_expr_t *node) { DECLARE_CODE(); CODEGEN_COUNT_REGISTERS(n1); + + // resources managed by cleanup label + uint32_r self_list; marray_init(self_list); + uint32_r args; marray_init(args); + uint32_t dest_register = 0; + ircode_push_context(code); // disable MOVE optimization @@ -1454,17 +1469,18 @@ static void visit_postfix_expr (gvisitor_t *self, gnode_postfix_expr_t *node) { uint32_t target_register = ircode_register_pop_context_protect(code, true); if (target_register == REGISTER_ERROR) { report_error(self, (gnode_t *)node->id, "Invalid postfix expression."); - return; + goto cleanup; } // register where to store final result - uint32_t dest_register = target_register; + dest_register = target_register; // mandatory self register (initialized to 0 in case of implicit self or explicit super) - uint32_r self_list; marray_init(self_list); - uint32_t first_self_register = compute_self_register(self, code, node->id, target_register, node->list); - if (first_self_register == UINT32_MAX) return; - marray_push(uint32_t, self_list, first_self_register); + { + uint32_t first_self_register = compute_self_register(self, code, node->id, target_register, node->list); + if (first_self_register == UINT32_MAX) goto cleanup; + marray_push(uint32_t, self_list, first_self_register); + } // process each subnode and set is_assignment flag bool is_assignment = node->base.is_assignment; @@ -1506,7 +1522,7 @@ static void visit_postfix_expr (gvisitor_t *self, gnode_postfix_expr_t *node) { uint32_t treg = ircode_register_pop_context_protect(code, true); if (treg == REGISTER_ERROR) { report_error(self, (gnode_t *)subnode, "Unexpected register error."); - return; + goto cleanup; } // always add SELF parameter (must be temp+1) @@ -1516,39 +1532,37 @@ static void visit_postfix_expr (gvisitor_t *self, gnode_postfix_expr_t *node) { treg = ircode_register_pop_context_protect(code, true); if (treg == REGISTER_ERROR) { report_error(self, (gnode_t *)subnode, "Unexpected register error."); - return; + goto cleanup; } // process each parameter (each must be temp+2 ... temp+n) - marray_decl_init(uint32_r, args); + marray_init(args); size_t n = gnode_array_size(subnode->args); for (size_t j=0; jargs, j); - ircode_pragma(code, PRAGMA_MOVE_OPTIMIZATION, 1, LINE_NUMBER(node)); - visit(arg); - ircode_pragma(code, PRAGMA_MOVE_OPTIMIZATION, 0, LINE_NUMBER(node)); + VISIT_MOVE_OPT(arg); uint32_t nreg = ircode_register_pop_context_protect(code, true); if (nreg == REGISTER_ERROR) { report_error(self, (gnode_t *)arg, "Invalid argument expression at index %d.", j+1); - return; + goto cleanup; } // make sure args are in consecutive register locations (from temp_target_register + 1 to temp_target_register + n) if (nreg != temp_target_register + j + 2) { uint32_t temp = ircode_register_push_temp(code); - if (temp == 0) return; // temp value == 0 means codegen error (error will be automatically reported later in visit_function_decl + if (ircode_iserror(code)) goto cleanup; ircode_add(code, MOVE, temp, nreg, 0, LINE_NUMBER(node)); ircode_register_clear(code, nreg); nreg = ircode_register_pop_context_protect(code, true); if (nreg == REGISTER_ERROR) { report_error(self, (gnode_t *)arg, "Invalid argument expression"); - return; + goto cleanup; } } if (nreg != temp_target_register + j + 2) { report_error(self, (gnode_t *)arg, "Invalid register computation in call expression."); - return; + goto cleanup; } // a checkpoint should be added after each nreg computation in order to support STRUCT @@ -1580,14 +1594,14 @@ static void visit_postfix_expr (gvisitor_t *self, gnode_postfix_expr_t *node) { uint32_t last_register = ircode_register_last(code); if (last_register == REGISTER_ERROR) { report_error(self, (gnode_t *)subnode, "Invalid call expression."); - return; + goto cleanup; } if (dest_is_temp && last_register == dest_register) dest_is_temp = false; } if (dest_is_temp) ircode_register_push(code, dest_register); if (!ircode_register_protect_outside_context(code, dest_register)) { report_error(self, (gnode_t *)subnode, "Invalid register access."); - return; + goto cleanup; } } @@ -1599,27 +1613,31 @@ static void visit_postfix_expr (gvisitor_t *self, gnode_postfix_expr_t *node) { gnode_identifier_expr_t *expr = (gnode_identifier_expr_t *)subnode->expr; uint32_t index = gravity_function_cpool_add(GET_VM(), context_function, VALUE_FROM_CSTRING(NULL, expr->value)); uint32_t index_register = ircode_register_push_temp(code); + if (ircode_iserror(code)) { + report_error(self, (gnode_t *)expr, "Register allocation failed."); + goto cleanup; + } ircode_add(code, LOADK, index_register, index, 0, LINE_NUMBER(expr)); uint32_t temp = ircode_register_pop(code); if (temp == REGISTER_ERROR) { report_error(self, (gnode_t *)expr, "Invalid access expression."); - return; + goto cleanup; } // generate LOAD/STORE instruction dest_register = (is_real_assigment) ? ircode_register_pop(code) : ircode_register_push_temp(code); if (dest_register == REGISTER_ERROR) { report_error(self, (gnode_t *)expr, "Invalid access expression."); - return; + goto cleanup; } if (is_super) { gravity_class_t *class = context_get_class(self); if (!class) { report_error(self, (gnode_t *)node, "Unable to use super keyword in a non class context."); - return; + goto cleanup; } - + // check if class has a superclass not yet processed const char *identifier = lookup_superclass_identifier (self, class); if (!identifier) { @@ -1627,21 +1645,25 @@ static void visit_postfix_expr (gvisitor_t *self, gnode_postfix_expr_t *node) { class = class->superclass; identifier = (class) ? class->identifier : GRAVITY_CLASS_OBJECT_NAME; } - + uint32_t cpool_index = gravity_function_cpool_add(GET_VM(), context_function, VALUE_FROM_CSTRING(NULL, identifier)); ircode_add_constant(code, cpool_index, LINE_NUMBER(node)); uint32_t temp_reg = ircode_register_pop(code); + if (temp_reg == REGISTER_ERROR) { + report_error(self, (gnode_t *)node, "Invalid super access expression."); + goto cleanup; + } ircode_add(code, LOADS, dest_register, temp_reg, index_register, LINE_NUMBER(node)); } else { ircode_add(code, (is_real_assigment) ? STORE : LOAD, dest_register, target_register, index_register, LINE_NUMBER(node)); } - + if (!is_real_assigment) { if (i+1expr); - ircode_pragma(code, PRAGMA_MOVE_OPTIMIZATION, 0, LINE_NUMBER(node)); + VISIT_MOVE_OPT(subnode->expr); uint32_t index = ircode_register_pop(code); if (index == REGISTER_ERROR) { report_error(self, (gnode_t *)subnode->expr, "Invalid subscript expression."); - return; + goto cleanup; } // generate LOADAT/STOREAT instruction dest_register = (is_real_assigment) ? ircode_register_pop(code) : ircode_register_push_temp(code); if (dest_register == REGISTER_ERROR) { report_error(self, (gnode_t *)subnode->expr, "Invalid subscript expression."); - return; + goto cleanup; } ircode_add(code, (is_real_assigment) ? STOREAT : LOADAT, dest_register, target_register, index, LINE_NUMBER(node)); if ((!is_real_assigment) && (i+1expr, "Unexpected register error."); - return; + goto cleanup; } marray_push(uint32_t, self_list, rtemp); } @@ -1696,21 +1716,30 @@ static void visit_postfix_expr (gvisitor_t *self, gnode_postfix_expr_t *node) { ircode_pop_context(code); // temp fix for not optimal register allocation algorithm generated code - uint32_t temp_register = ircode_register_first_temp_available(code); - if (temp_register < dest_register) { - // free dest register - ircode_register_pop(code); - // allocate a new register (that I am now sure does not have holes) - temp_register = ircode_register_push_temp(code); - ircode_add(code, MOVE, temp_register, dest_register, 0, LINE_NUMBER(node)); - ircode_register_clear(code, dest_register); + { + uint32_t temp_register = ircode_register_first_temp_available(code); + if (temp_register < dest_register) { + // free dest register + ircode_register_pop(code); + // allocate a new register (that I am now sure does not have holes) + temp_register = ircode_register_push_temp(code); + ircode_add(code, MOVE, temp_register, dest_register, 0, LINE_NUMBER(node)); + ircode_register_clear(code, dest_register); + } } - + CODEGEN_COUNT_REGISTERS(n2); CODEGEN_ASSERT_REGISTERS(n1, n2, (is_assignment) ? -1 : 1); // re-enable MOVE optimization ircode_pragma(code, PRAGMA_MOVE_OPTIMIZATION, 1, LINE_NUMBER(node)); + return; + +cleanup: + marray_destroy(args); + marray_destroy(self_list); + ircode_pop_context(code); + ircode_pragma(code, PRAGMA_MOVE_OPTIMIZATION, 1, LINE_NUMBER(node)); } static void visit_file_expr (gvisitor_t *self, gnode_file_expr_t *node) { @@ -1898,15 +1927,13 @@ static void visit_identifier_expr (gvisitor_t *self, gnode_identifier_expr_t *no uint32_t target = 0; if (type == LOCATION_CLASS_IVAR_OUTER) { + // keep the outer ref register alive until after STORE/LOAD to prevent + // index_register allocation from clobbering it (push_temp reuses freed slots) dest = ircode_register_push_temp(code); for (uint16_t i=0; ilist2, j); - ircode_pragma(code, PRAGMA_MOVE_OPTIMIZATION, 1, LINE_NUMBER(node)); - visit(e); - ircode_pragma(code, PRAGMA_MOVE_OPTIMIZATION, 0, LINE_NUMBER(node)); + VISIT_MOVE_OPT(e); nreg = ircode_register_pop_context_protect(code, true); if ((nreg == REGISTER_ERROR) || ((nreg <= dest) && (ircode_register_istemp(code, nreg)))) { report_error(self, (gnode_t *)e, "Invalid map expression."); diff --git a/src/compiler/gravity_ircode.c b/src/compiler/gravity_ircode.c index b16bb432..2aa9621e 100644 --- a/src/compiler/gravity_ircode.c +++ b/src/compiler/gravity_ircode.c @@ -11,8 +11,14 @@ #include "gravity_debug.h" #include -typedef marray_t(inst_t *) code_r; -typedef marray_t(bool *) context_r; +// Register bitmask helpers (256 registers packed into 32 bytes) +#define REG_BITMASK_SIZE (MAX_REGISTERS / 8) +#define REG_SET(a, i) ((a)[(i) >> 3] |= (unsigned char)(1u << ((i) & 7))) +#define REG_CLR(a, i) ((a)[(i) >> 3] &= (unsigned char)~(1u << ((i) & 7))) +#define REG_TEST(a, i) (((a)[(i) >> 3] & (1u << ((i) & 7))) != 0) + +typedef marray_t(inst_t *) code_r; +typedef marray_t(unsigned char *) context_r; struct ircode_t { code_r *list; // array of ircode instructions @@ -27,8 +33,8 @@ struct ircode_t { uint16_t nlocals; // number of local registers (params + local variables) bool error; // error flag set when no more registers are availables - bool state[MAX_REGISTERS]; // registers mask - bool skipclear[MAX_REGISTERS]; // registers protection for temps used in for loop + unsigned char state[REG_BITMASK_SIZE]; // registers allocation bitmask + unsigned char skipclear[REG_BITMASK_SIZE]; // temp protection bitmask (for loop vars) uint32_r registers; // registers stack context_r context; // context array }; @@ -54,12 +60,14 @@ ircode_t *ircode_create (uint16_t nlocals) { marray_init(code->registers); marray_init(code->context); - // init state array (register 0 is reserved) - bzero(code->state, MAX_REGISTERS * sizeof(bool)); - code->state[0] = true; - for (uint32_t i=0; istate[i] = true; + // init register bitmasks + memset(code->state, 0, REG_BITMASK_SIZE); + memset(code->skipclear, 0, REG_BITMASK_SIZE); + // mark register 0 and all local registers as allocated + for (uint32_t i = 0; i < nlocals; ++i) { + REG_SET(code->state, i); } + if (nlocals == 0) REG_SET(code->state, 0); // register 0 is always reserved return code; } @@ -128,6 +136,7 @@ static inst_t *inst_new (opcode_t op, uint32_t p1, uint32_t p2, uint32_t p3, opt #endif inst_t *inst = (inst_t *)mem_alloc(NULL, sizeof(inst_t)); + assert(inst); inst->op = op; inst->tag = tag; inst->p1 = p1; @@ -138,7 +147,6 @@ static inst_t *inst_new (opcode_t op, uint32_t p1, uint32_t p2, uint32_t p3, opt if (tag == DOUBLE_TAG) inst->d = d; else if (tag == INT_TAG) inst->n = n; - assert(inst); return inst; } @@ -357,18 +365,21 @@ void ircode_unsetlabel_check (ircode_t *code) { uint32_t ircode_getlabel_true (ircode_t *code) { size_t n = marray_size(code->label_true); + if (n == 0) return 0; uint32_t v = marray_get(code->label_true, n-1); return v; } uint32_t ircode_getlabel_false (ircode_t *code) { size_t n = marray_size(code->label_false); + if (n == 0) return 0; uint32_t v = marray_get(code->label_false, n-1); return v; } uint32_t ircode_getlabel_check (ircode_t *code) { size_t n = marray_size(code->label_check); + if (n == 0) return 0; uint32_t v = marray_get(code->label_check, n-1); return v; } @@ -437,25 +448,17 @@ void ircode_add_check (ircode_t *code) { // MARK: - Context based functions - -#if 0 -static void dump_context(bool *context) { - for (uint32_t i=0; icontext, context); + unsigned char *context = mem_alloc(NULL, REG_BITMASK_SIZE); + memset(context, 0, REG_BITMASK_SIZE); + marray_push(unsigned char *, code->context, context); } void ircode_pop_context (ircode_t *code) { - bool *context = marray_pop(code->context); - // apply context mask - for (uint32_t i=0; istate[i] = false; + unsigned char *context = marray_pop(code->context); + // clear bits in state that are set in context + for (uint32_t b = 0; b < REG_BITMASK_SIZE; ++b) { + code->state[b] &= ~context[b]; } mem_free(context); } @@ -464,12 +467,12 @@ uint32_t ircode_register_pop_context_protect (ircode_t *code, bool protect) { if (marray_size(code->registers) == 0) return REGISTER_ERROR; uint32_t value = (uint32_t)marray_pop(code->registers); - if (protect) code->state[value] = true; - else if (value >= code->nlocals) code->state[value] = false; + if (protect) REG_SET(code->state, value); + else if (value >= code->nlocals) REG_CLR(code->state, value); if (protect && value >= code->nlocals) { - bool *context = marray_last(code->context); - context[value] = true; + unsigned char *context = marray_last(code->context); + REG_SET(context, value); } DEBUG_REGISTER("POP REGISTER %d", value); @@ -478,28 +481,32 @@ uint32_t ircode_register_pop_context_protect (ircode_t *code, bool protect) { bool ircode_register_protect_outside_context (ircode_t *code, uint32_t nreg) { if (nreg < code->nlocals) return true; - if (!code->state[nreg]) return false; - bool *context = marray_last(code->context); - context[nreg] = false; + if (!REG_TEST(code->state, nreg)) return false; + unsigned char *context = marray_last(code->context); + REG_CLR(context, nreg); return true; } void ircode_register_protect_in_context (ircode_t *code, uint32_t nreg) { - assert(code->state[nreg]); - bool *context = marray_last(code->context); - context[nreg] = true; + assert(REG_TEST(code->state, nreg)); + unsigned char *context = marray_last(code->context); + REG_SET(context, nreg); } // MARK: - static uint32_t ircode_register_new (ircode_t *code) { - for (uint32_t i=0; istate[i] == false) { - code->state[i] = true; - return i; + // scan bytes to find one with a free bit (not 0xFF) + for (uint32_t b = 0; b < REG_BITMASK_SIZE; ++b) { + unsigned char avail = (unsigned char)~code->state[b]; + if (avail) { + uint32_t bit = 0; + while (!(avail & (1u << bit))) ++bit; + uint32_t reg = (b << 3) | bit; + REG_SET(code->state, reg); + return reg; } } - // 0 means no registers available code->error = true; return 0; } @@ -513,12 +520,14 @@ uint32_t ircode_register_push (ircode_t *code, uint32_t nreg) { } uint32_t ircode_register_first_temp_available (ircode_t *code) { - for (uint32_t i=0; istate[i] == false) { - return i; + for (uint32_t b = 0; b < REG_BITMASK_SIZE; ++b) { + unsigned char avail = (unsigned char)~code->state[b]; + if (avail) { + uint32_t bit = 0; + while (!(avail & (1u << bit))) ++bit; + return (b << 3) | bit; } } - // 0 means no registers available code->error = true; return 0; } @@ -545,13 +554,13 @@ uint32_t ircode_register_pop (ircode_t *code) { void ircode_register_clear (ircode_t *code, uint32_t nreg) { if (nreg == REGISTER_ERROR) return; // cleanup busy mask only if it is a temp register - if (nreg >= code->nlocals) code->state[nreg] = false; + if (nreg >= code->nlocals) REG_CLR(code->state, nreg); } void ircode_register_set (ircode_t *code, uint32_t nreg) { if (nreg == REGISTER_ERROR) return; // set busy mask only if it is a temp register - if (nreg >= code->nlocals) code->state[nreg] = true; + if (nreg >= code->nlocals) REG_SET(code->state, nreg); } uint32_t ircode_register_last (ircode_t *code) { @@ -579,20 +588,20 @@ uint32_t ircode_register_count (ircode_t *code) { // MARK: - void ircode_register_temp_protect (ircode_t *code, uint32_t nreg) { - code->skipclear[nreg] = true; + REG_SET(code->skipclear, nreg); DEBUG_REGISTER("SET SKIP REGISTER %d", nreg); } void ircode_register_temp_unprotect (ircode_t *code, uint32_t nreg) { - code->skipclear[nreg] = false; + REG_CLR(code->skipclear, nreg); DEBUG_REGISTER("UNSET SKIP REGISTER %d", nreg); } void ircode_register_temps_clear (ircode_t *code) { - // clear all temporary registers (if not protected) - for (uint32_t i=code->nlocals; i<=code->maxtemp; ++i) { - if (!code->skipclear[i]) { - code->state[i] = false; + // clear all temporary registers (if not protected by skipclear) + for (uint32_t i = code->nlocals; i <= code->maxtemp; ++i) { + if (!REG_TEST(code->skipclear, i)) { + REG_CLR(code->state, i); DEBUG_REGISTER("CLEAR TEMP REGISTER %d", i); } } diff --git a/src/compiler/gravity_lexer.c b/src/compiler/gravity_lexer.c index a10ac2e0..9f79f132 100644 --- a/src/compiler/gravity_lexer.c +++ b/src/compiler/gravity_lexer.c @@ -40,8 +40,8 @@ typedef enum { // LEXER macros #define NEXT lexer->buffer[lexer->offset++]; ++lexer->position; INC_COL #define PEEK_CURRENT ((lexer->offset < lexer->length) ? (int)lexer->buffer[lexer->offset] : 0) -#define PEEK_NEXT ((lexer->offset < lexer->length) ? lexer->buffer[lexer->offset+1] : 0) -#define PEEK_NEXT2 ((lexer->offset+1 < lexer->length) ? lexer->buffer[lexer->offset+2] : 0) +#define PEEK_NEXT ((lexer->offset + 1 < lexer->length) ? lexer->buffer[lexer->offset+1] : 0) +#define PEEK_NEXT2 ((lexer->offset+2 < lexer->length) ? lexer->buffer[lexer->offset+2] : 0) #define INC_LINE ++lexer->lineno; RESET_COL #define INC_COL ++lexer->colno #define DEC_COL --lexer->colno diff --git a/src/compiler/gravity_optimizer.c b/src/compiler/gravity_optimizer.c index ba8251d0..5f6eaaab 100644 --- a/src/compiler/gravity_optimizer.c +++ b/src/compiler/gravity_optimizer.c @@ -28,7 +28,7 @@ // http://www.mathsisfun.com/binary-decimal-hexadecimal-converter.html #define OPCODE_SET(op,code) op = (code & 0x3F) << 26 #define OPCODE_SET_TWO8bit_ONE10bit(op,code,a,b,c) op = (code & 0x3F) << 26; op += (a & 0xFF) << 18; op += (b & 0xFF) << 10; op += (c & 0x3FF) -#define OPCODE_SET_FOUR8bit(op,a,b,c,d) op = (a & 0xFF) << 24; op += (b & 0xFF) << 16; op += (c & 0xFF) << 8; op += (d & 0xFF) +#define OPCODE_SET_FOUR8bit(op,code,a,b,c,d) op = (code & 0x3F) << 26; op += (a & 0xFF) << 18; op += (b & 0xFF) << 10; op += (c & 0xFF) << 2; op += (d & 0x03) #define OPCODE_SET_ONE8bit_SIGN_ONE17bit(op,code,a,s,n) op = (code & 0x3F) << 26; op += (a & 0xFF) << 18; op += (s & 0x01) << 17; op += (n & 0x1FFFF) #define OPCODE_SET_SIGN_ONE25bit(op,code,s,a) op = (code & 0x3F) << 26; op += (s & 0x01) << 25; op += (a & 0x1FFFFFF) #define OPCODE_SET_ONE8bit_ONE18bit(op,code,a,n) op = (code & 0x3F) << 26; op += (a & 0xFF) << 18; op += (n & 0x3FFFF) @@ -295,7 +295,7 @@ static bool optimize_const_instruction (inst_t *inst, inst_t *inst1, inst_t *ins // 00005 ADD 2 2 3 // inst points to a MATH instruction but registers are not the same as the LOADI instructions // so no optimizations must be performed - if (!(inst->p2 == inst1->p1 && inst->p3 == inst2->p2)) return false; + if (!(inst->p2 == inst1->p1 && inst->p3 == inst2->p1)) return false; // compute operands if (type == DOUBLE_TAG) { @@ -373,11 +373,11 @@ static bool optimize_neg_instruction (ircode_t *code, inst_t *inst, uint32_t i) if (inst1->tag == INT_TAG) { int64_t n = inst1->n; if (n > 131072) return false; - inst1->p1 = inst->p2; + inst1->p1 = inst->p1; inst1->n = -(int64_t)n; } else if (inst1->tag == DOUBLE_TAG) { double d = inst1->d; - inst1->p1 = inst->p2; + inst1->p1 = inst->p1; inst1->d = -d; } else { return false; diff --git a/src/compiler/gravity_parser.c b/src/compiler/gravity_parser.c index aa931f91..0c3a1c53 100644 --- a/src/compiler/gravity_parser.c +++ b/src/compiler/gravity_parser.c @@ -439,6 +439,7 @@ static gnode_t *parse_file_expression (gravity_parser_t *parser) { gravity_lexer_next(lexer); // consume TOK_OP_DOT const char *identifier = parse_identifier(parser); if (!identifier) { + cstring_array_free(list); mem_free(list); return NULL; } @@ -741,7 +742,7 @@ static gnode_t *parse_analyze_literal_string (gravity_parser_t *parser, gtoken_s gnode_r *r = NULL; // analyze s (of length len) for escaped characters or for interpolations - char *buffer = mem_alloc(NULL, len+1); + char *buffer = mem_alloc(NULL, len*4+1); uint32_t length = 0; for (uint32_t i=0; iuplist) ? (uint32_t)marray_size(*node->uplist) : 0; if (nupvalues > MAX_UPVALUES) REPORT_ERROR(node, "Maximum number of upvalues reached in function %s (max:%d found:%d).", - node->identifier, MAX_LOCALS, nupvalues); + node->identifier, MAX_UPVALUES, nupvalues); POP_DECLARATION(); diff --git a/src/optionals/gravity_opt_env.c b/src/optionals/gravity_opt_env.c index 9512a2fe..95f000c8 100644 --- a/src/optionals/gravity_opt_env.c +++ b/src/optionals/gravity_opt_env.c @@ -77,16 +77,22 @@ static bool gravity_env_set(gravity_vm *vm, gravity_value_t *args, uint16_t narg static bool gravity_env_keys(gravity_vm *vm, gravity_value_t *args, uint16_t nparams, uint32_t rindex) { #pragma unused(args, nparams) + #if defined(_WIN32) + extern char **_environ; + char **environ_ptr = _environ; + #else extern char **environ; + char **environ_ptr = environ; + #endif gravity_list_t *keys = gravity_list_new(vm, 16); - - for (char **env = environ; *env; ++env) { + + for (char **env = environ_ptr; *env; ++env) { char *entry = *env; // env is in the form key=value uint32_t len = 0; - for (uint32_t i=0; entry[len]; ++i, ++len) { - if (entry[i] == '=') break; + while (entry[len] && entry[len] != '=') { + ++len; } gravity_value_t key = VALUE_FROM_STRING(vm, entry, len); marray_push(gravity_value_t, keys->array, key); diff --git a/src/optionals/gravity_opt_file.c b/src/optionals/gravity_opt_file.c index f7c8508c..62a29f33 100644 --- a/src/optionals/gravity_opt_file.c +++ b/src/optionals/gravity_opt_file.c @@ -132,6 +132,7 @@ static bool internal_file_buildpath (gravity_vm *vm, gravity_value_t *args, uint } gravity_value_t string = VALUE_FROM_STRING(vm, result, (uint32_t)strlen(result)); + mem_free(result); RETURN_VALUE(string, rindex); } @@ -185,6 +186,7 @@ static void scan_directory (gravity_vm *vm, char *path, bool recursive, gravity_ char *full_path = file_buildpath(target_file, path); if (recursive && (is_directory(full_path))) { scan_directory(vm, full_path, recursive, closure, n, true); + mem_free(full_path); continue; } @@ -253,7 +255,7 @@ static bool internal_file_open (gravity_vm *vm, gravity_value_t *args, uint16_t */ // 1 parameter of type string is required - if (nargs > 1 && !VALUE_ISA_STRING(args[1])) { + if (nargs < 2 || !VALUE_ISA_STRING(args[1])) { RETURN_ERROR("A path parameter of type String is required."); } char *path = VALUE_AS_STRING(args[1])->s; @@ -270,6 +272,7 @@ static bool internal_file_open (gravity_vm *vm, gravity_value_t *args, uint16_t gravity_file_t *instance = gravity_ifile_new(vm, file); if (instance == NULL) { + fclose(file); RETURN_VALUE(VALUE_FROM_NULL, rindex); } @@ -291,15 +294,17 @@ static bool internal_file_iread (gravity_vm *vm, gravity_value_t *args, uint16_t if (VALUE_ISA_INT(args[1])) n = VALUE_AS_INT(args[1]); else str = VALUE_AS_STRING(args[1]); - + + if (n <= 0) RETURN_ERROR("Invalid read size."); + char *buffer = (char *)mem_alloc(NULL, n); if (!buffer) { - RETURN_ERROR("Not enought memory to allocate required buffer."); + RETURN_ERROR("Not enough memory to allocate required buffer."); } // args[1] was a number so read up-to n characters if (str == NULL) { - nread = fread(buffer, (size_t)n, 1, instance->file); + nread = fread(buffer, 1, (size_t)n, instance->file); } else { // read up-until s character was found (or EOF) // taking in account buffer b resizing @@ -318,6 +323,7 @@ static bool internal_file_iread (gravity_vm *vm, gravity_value_t *args, uint16_t if (ptr + 2 >= eptr) { char *nbuf; + if ((size_t)n > SIZE_MAX / 2) break; size_t nbufsiz = n * 2; ssize_t d = ptr - buffer; if ((nbuf = mem_realloc(NULL, buffer, nbufsiz)) == NULL) break; @@ -352,7 +358,7 @@ static bool internal_file_iwrite (gravity_vm *vm, gravity_value_t *args, uint16_ gravity_file_t *instance = VALUE_AS_FILE(args[0]); gravity_string_t *data = VALUE_AS_STRING(args[1]); - size_t nwritten = fwrite(data->s, data->len, 1, instance->file); + size_t nwritten = fwrite(data->s, 1, data->len, instance->file); RETURN_VALUE(VALUE_FROM_INT(nwritten), rindex); } diff --git a/src/optionals/gravity_opt_json.c b/src/optionals/gravity_opt_json.c index 88461972..91f82ae3 100644 --- a/src/optionals/gravity_opt_json.c +++ b/src/optionals/gravity_opt_json.c @@ -32,21 +32,59 @@ static bool JSON_stringify (gravity_vm *vm, gravity_value_t *args, uint16_t narg // extract value gravity_value_t value = GET_VALUE(1); - // special case for string because it can be huge (and must be quoted) + // special case for string because it can be huge (and must be quoted + escaped) if (VALUE_ISA_STRING(value)) { - const int nchars = 5; const char *v = VALUE_AS_STRING(value)->s; size_t vlen = VALUE_AS_STRING(value)->len; - // string must be quoted - if (vlen < 4096-nchars) { - char vbuffer2[4096]; - vlen = snprintf(vbuffer2, sizeof(vbuffer2), "\"%s\"", v); - RETURN_VALUE(VALUE_FROM_STRING(vm, vbuffer2, (uint32_t)vlen), rindex); + // calculate escaped length + size_t escaped_len = 0; + for (size_t k = 0; k < vlen; ++k) { + unsigned char c = (unsigned char)v[k]; + switch (c) { + case '"': case '\\': case '\b': case '\f': + case '\n': case '\r': case '\t': + escaped_len += 2; break; + default: + escaped_len += (c < 0x20) ? 6 : 1; break; + } + } + + // allocate: escaped_len + 2 quotes + 1 null + size_t alloc_size = escaped_len + 3; + char stack_buf[4096]; + bool use_heap = (alloc_size > sizeof(stack_buf)); + char *buf = use_heap ? (char *)mem_alloc(NULL, alloc_size) : stack_buf; + + // write escaped string + size_t pos = 0; + buf[pos++] = '"'; + for (size_t k = 0; k < vlen; ++k) { + unsigned char c = (unsigned char)v[k]; + switch (c) { + case '"': buf[pos++] = '\\'; buf[pos++] = '"'; break; + case '\\': buf[pos++] = '\\'; buf[pos++] = '\\'; break; + case '\b': buf[pos++] = '\\'; buf[pos++] = 'b'; break; + case '\f': buf[pos++] = '\\'; buf[pos++] = 'f'; break; + case '\n': buf[pos++] = '\\'; buf[pos++] = 'n'; break; + case '\r': buf[pos++] = '\\'; buf[pos++] = 'r'; break; + case '\t': buf[pos++] = '\\'; buf[pos++] = 't'; break; + default: + if (c < 0x20) { + pos += snprintf(buf + pos, 7, "\\u%04x", c); + } else { + buf[pos++] = (char)c; + } + break; + } + } + buf[pos++] = '"'; + buf[pos] = '\0'; + + if (use_heap) { + RETURN_VALUE(VALUE_FROM_OBJECT(gravity_string_new(vm, buf, (uint32_t)pos, (uint32_t)alloc_size)), rindex); } else { - char *vbuffer2 = mem_alloc(NULL, vlen + nchars); - vlen = snprintf(vbuffer2, vlen + nchars, "\"%s\"", v); - RETURN_VALUE(VALUE_FROM_OBJECT(gravity_string_new(vm, vbuffer2, (uint32_t)vlen, 0)), rindex); + RETURN_VALUE(VALUE_FROM_STRING(vm, buf, (uint32_t)pos), rindex); } } diff --git a/src/optionals/gravity_opt_math.c b/src/optionals/gravity_opt_math.c index 99f6de5c..373b9886 100644 --- a/src/optionals/gravity_opt_math.c +++ b/src/optionals/gravity_opt_math.c @@ -41,7 +41,7 @@ #define ASIN asinf #define ACOS acosf #define ATAN atanf -#define ATAN2 atan2 +#define ATAN2 atan2f #define CEIL ceilf #define FLOOR floorf #define ROUND roundf @@ -229,22 +229,22 @@ static bool math_xrt (gravity_vm *vm, gravity_value_t *args, uint16_t nargs, uin } if (VALUE_ISA_INT(value) && VALUE_ISA_INT(base)) { - gravity_float_t computed_value = (gravity_float_t)pow((gravity_float_t)value.n, 1.0/base.n); + gravity_float_t computed_value = (gravity_float_t)POW((gravity_float_t)value.n, 1.0/base.n); RETURN_VALUE(VALUE_FROM_FLOAT(computed_value), rindex); } if (VALUE_ISA_INT(value) && VALUE_ISA_FLOAT(base)) { - gravity_float_t computed_value = (gravity_float_t)pow((gravity_float_t)value.n, 1.0/base.f); + gravity_float_t computed_value = (gravity_float_t)POW((gravity_float_t)value.n, 1.0/base.f); RETURN_VALUE(VALUE_FROM_FLOAT(computed_value), rindex); } if (VALUE_ISA_FLOAT(value) && VALUE_ISA_INT(base)) { - gravity_float_t computed_value = (gravity_float_t)pow((gravity_float_t)value.f, 1.0/base.n); + gravity_float_t computed_value = (gravity_float_t)POW((gravity_float_t)value.f, 1.0/base.n); RETURN_VALUE(VALUE_FROM_FLOAT(computed_value), rindex); } if (VALUE_ISA_FLOAT(value) && VALUE_ISA_FLOAT(base)) { - gravity_float_t computed_value = (gravity_float_t)pow((gravity_float_t)value.f, 1.0/base.f); + gravity_float_t computed_value = (gravity_float_t)POW((gravity_float_t)value.f, 1.0/base.f); RETURN_VALUE(VALUE_FROM_FLOAT(computed_value), rindex); } @@ -497,6 +497,10 @@ static bool math_logx (gravity_vm *vm, gravity_value_t *args, uint16_t nargs, ui RETURN_VALUE(VALUE_FROM_INT(0), rindex); } + // get base as float, check for domain error (base=1 causes division by zero since log(1)=0) + gravity_float_t base_f = VALUE_ISA_INT(base) ? (gravity_float_t)base.n : (VALUE_ISA_FLOAT(base) ? base.f : 0.0); + if (base_f == 1.0) RETURN_VALUE(VALUE_FROM_UNDEFINED, rindex); + if (VALUE_ISA_INT(value) && VALUE_ISA_INT(base)) { gravity_float_t computed_value = (gravity_float_t)LOG((gravity_float_t)value.n)/(gravity_float_t)LOG((gravity_float_t)base.n); RETURN_VALUE(VALUE_FROM_FLOAT(computed_value), rindex); @@ -647,7 +651,7 @@ static bool math_round (gravity_vm *vm, gravity_value_t *args, uint16_t nargs, u } if (ndigits) { - double d = pow(10.0, (double)ndigits); + double d = POW(10.0, (double)ndigits); gravity_float_t f = (gravity_float_t)(ROUND((gravity_float_t)value.f * (gravity_float_t)d)) / (gravity_float_t)d; // convert f to string @@ -656,15 +660,15 @@ static bool math_round (gravity_vm *vm, gravity_value_t *args, uint16_t nargs, u // trunc c string to the requested ndigits char *p = buffer; - while (p) { - if (p[0] == '.') { + while (*p) { + if (*p == '.') { ++p; gravity_int_t n = 0; - while (p && n < ndigits) { + while (*p && n < ndigits) { ++p; ++n; } - if (p) p[0] = 0; + *p = 0; break; } ++p; @@ -911,7 +915,8 @@ static bool math_random (gravity_vm *vm, gravity_value_t *args, uint16_t nargs, gravity_int_t n0 = (gravity_int_t)(rnd * (gravity_float_t)GRAVITY_INT_MAX); if (n1 > n2) {gravity_int_t temp = n1; n1 = n2; n2 = temp;} // swap numbers if min > max - gravity_int_t n = (gravity_int_t)(n0 % (n2 + 1 - n1) + n1); + gravity_int_t range = n2 - n1; // avoid overflow on n2 + 1 when n2 == GRAVITY_INT_MAX + gravity_int_t n = (gravity_int_t)(n0 % (range + 1) + n1); RETURN_VALUE(VALUE_FROM_INT(n), rindex); } diff --git a/src/runtime/gravity_core.c b/src/runtime/gravity_core.c index c6dc7315..d47a9223 100644 --- a/src/runtime/gravity_core.c +++ b/src/runtime/gravity_core.c @@ -218,7 +218,9 @@ static gravity_value_t convert_map2string (gravity_vm *vm, gravity_map_t *map) { // check if buffer needs to be reallocated if (len1 + len2 + pos + 4 > len) { len = (len1 + len2 + pos + 4) + len; - buffer = mem_realloc(NULL, buffer, len); + char *_tmp = mem_realloc(NULL, buffer, len); + if (!_tmp) { mem_free(buffer); return VALUE_FROM_ERROR("Out of memory"); } + buffer = _tmp; } // copy key string to new buffer @@ -274,7 +276,9 @@ static gravity_value_t convert_list2string (gravity_vm *vm, gravity_list_t *list // check if buffer needs to be reallocated if (len1+pos+2 > len) { len = (len1+pos+2) + len; - buffer = mem_realloc(NULL, buffer, len); + char *_tmp = mem_realloc(NULL, buffer, len); + if (!_tmp) { mem_free(buffer); return VALUE_FROM_ERROR("Out of memory"); } + buffer = _tmp; } // copy string to new buffer @@ -1024,7 +1028,7 @@ static bool list_storeat (gravity_vm *vm, gravity_value_t *args, uint16_t nargs, for (int32_t i=count; i<=(index+MIN_LIST_RESIZE); ++i) { marray_set(list->array, i, VALUE_FROM_NULL); } - marray_set(list->array, index, value); + // value is set unconditionally below } marray_set(list->array, index, value); @@ -1097,6 +1101,8 @@ static bool list_iterator_next (gravity_vm *vm, gravity_value_t *args, uint16_t #pragma unused(vm, nargs) gravity_list_t *list = VALUE_AS_LIST(GET_VALUE(0)); register int32_t index = (int32_t)VALUE_AS_INT(GET_VALUE(1)); + size_t count = marray_size(list->array); + if (index < 0 || (size_t)index >= count) RETURN_VALUE(VALUE_FROM_NULL, rindex); RETURN_VALUE(marray_get(list->array, index), rindex); } @@ -1602,7 +1608,7 @@ static bool range_iterator (gravity_vm *vm, gravity_value_t *args, uint16_t narg #pragma unused(vm, nargs) gravity_range_t *range = VALUE_AS_RANGE(GET_VALUE(0)); - // check for invalid range first + // check for empty/backward range (half-open ranges like 0..<0 create from > to) if (range->to < range->from) RETURN_VALUE(VALUE_FROM_FALSE, rindex); // check for start of iteration @@ -1641,7 +1647,9 @@ static bool range_contains (gravity_vm *vm, gravity_value_t *args, uint16_t narg // check error condition if (!VALUE_ISA_INT(value)) RETURN_ERROR("A numeric value is expected."); - RETURN_VALUE(VALUE_FROM_BOOL((value.n >= range->from) && (value.n <= range->to)), rindex); + gravity_int_t lo = (range->from < range->to) ? range->from : range->to; + gravity_int_t hi = (range->from < range->to) ? range->to : range->from; + RETURN_VALUE(VALUE_FROM_BOOL((value.n >= lo) && (value.n <= hi)), rindex); } static bool range_exec (gravity_vm *vm, gravity_value_t *args, uint16_t nargs, uint32_t rindex) { @@ -1812,8 +1820,7 @@ static bool function_exec (gravity_vm *vm, gravity_value_t *args, uint16_t nargs // DEFAULT_MINSTACK_SIZE is 256 and in case of a 256 function arguments a maximum registers error would be returned // so I can assume to be always safe here while (nargs < func->nparams) { - uint32_t index = (func->nparams - nargs); - args[index] = VALUE_FROM_UNDEFINED; + args[nargs] = VALUE_FROM_UNDEFINED; ++nargs; } @@ -2128,17 +2135,17 @@ static bool int_random (gravity_vm *vm, gravity_value_t *args, uint16_t nargs, u already_seeded = true; } - int r; + gravity_int_t r; // if num1 is lower, consider it min, otherwise, num2 is min if (num1 < num2) { // returns a random integer between num1 and num2 inclusive - r = (int)((rand() % (num2 - num1 + 1)) + num1); + r = (gravity_int_t)((rand() % (num2 - num1 + 1)) + num1); } else if (num1 > num2) { - r = (int)((rand() % (num1 - num2 + 1)) + num2); + r = (gravity_int_t)((rand() % (num1 - num2 + 1)) + num2); } else { - r = (int)num1; + r = num1; } RETURN_VALUE(VALUE_FROM_INT(r), rindex); } @@ -2447,28 +2454,22 @@ static bool string_count (gravity_vm *vm, gravity_value_t *args, uint16_t nargs, gravity_string_t *main_str = VALUE_AS_STRING(GET_VALUE(0)); gravity_string_t *str_to_count = VALUE_AS_STRING(GET_VALUE(1)); - int j = 0; + // empty search string: return 0 + if (str_to_count->len == 0) RETURN_VALUE(VALUE_FROM_INT(0), rindex); + int count = 0; + const char *p = main_str->s; + uint32_t remaining = main_str->len; + uint32_t needle_len = str_to_count->len; - // iterate through whole string - for (int i = 0; i < main_str->len; ++i) { - if (main_str->s[i] == str_to_count->s[j]) { - // if the characters match and we are on the last character of the search - // string, then we have found a match - if (j == str_to_count->len - 1) { - ++count; - j = 0; - continue; - } - } - // reset if it isn't a match - else { - j = 0; - continue; - } - // move forward in the search string if we found a match but we aren't - // finished checking all the characters of the search string yet - ++j; + // find all non-overlapping occurrences using strstr + while (remaining >= needle_len) { + char *found = string_strnstr(p, str_to_count->s, remaining); + if (!found) break; + ++count; + uint32_t advance = (uint32_t)(found - p) + needle_len; + p = found + needle_len; + remaining -= advance; } RETURN_VALUE(VALUE_FROM_INT(count), rindex); @@ -2520,7 +2521,7 @@ static bool string_upper (gravity_vm *vm, gravity_value_t *args, uint16_t nargs, // if no arguments passed, change the whole string to uppercase if (nargs == 1) { - for (int i = 0; i <= main_str->len; ++i) { + for (int i = 0; i < main_str->len; ++i) { ret[i] = toupper(ret[i]); } } @@ -2559,7 +2560,7 @@ static bool string_lower (gravity_vm *vm, gravity_value_t *args, uint16_t nargs, // if no arguments passed, change the whole string to lowercase if (nargs == 1) { - for (int i = 0; i <= main_str->len; ++i) { + for (int i = 0; i < main_str->len; ++i) { ret[i] = tolower(ret[i]); } } @@ -2630,7 +2631,7 @@ static bool string_loadat (gravity_vm *vm, gravity_value_t *args, uint16_t nargs // Reverse the string, and reverse the indices first_index = original_len - first_index -1; - // reverse the String + // reverse the String (UTF-8 aware: reverse whole bytes first, then fix multi-byte sequences) int i = original_len - 1; int j = 0; char c; @@ -2641,6 +2642,39 @@ static bool string_loadat (gravity_vm *vm, gravity_value_t *args, uint16_t nargs --i; ++j; } + // fix multi-byte UTF-8 sequences that were reversed byte-by-byte + for (uint32_t k = 0; k < original_len; ) { + unsigned char ch = (unsigned char)original[k]; + int seq_len = 1; + if (ch >= 0xF0) seq_len = 4; + else if (ch >= 0xE0) seq_len = 3; + else if (ch >= 0xC0) seq_len = 2; + // after byte-reversal, lead bytes of multi-byte sequences end up at the end + // of their sequence; detect continuation bytes (10xxxxxx) which indicate a + // reversed multi-byte sequence and find the lead byte to determine length + if ((ch & 0xC0) == 0x80) { + // continuation byte: scan forward to find the lead byte + int end = k + 1; + while (end < (int)original_len && ((unsigned char)original[end] & 0xC0) == 0x80) ++end; + if (end < (int)original_len) { + unsigned char lead = (unsigned char)original[end]; + if (lead >= 0xF0) seq_len = 4; + else if (lead >= 0xE0) seq_len = 3; + else if (lead >= 0xC0) seq_len = 2; + // reverse the bytes within this multi-byte sequence to restore correct order + int lo = k, hi = k + seq_len - 1; + while (lo < hi) { + char tmp = original[lo]; + original[lo] = original[hi]; + original[hi] = tmp; + ++lo; --hi; + } + } + k += seq_len; + } else { + k += seq_len; + } + } gravity_value_t s = VALUE_FROM_STRING(vm, original + first_index, substr_len); mem_free(original); @@ -2764,9 +2798,10 @@ static bool string_loop (gravity_vm *vm, gravity_value_t *args, uint16_t nargs, nanotime_t t1 = nanotime(); while (i < n) { - gravity_value_t v_str = VALUE_FROM_STRING(vm, str + i, 1); + uint32_t clen = utf8_charbytes(str + i, 0); + gravity_value_t v_str = VALUE_FROM_STRING(vm, str + i, clen); if (!gravity_vm_runclosure(vm, closure, value, &v_str, 1)) return false; - ++i; + i += clen; } nanotime_t t2 = nanotime(); RETURN_VALUE(VALUE_FROM_INT(t2-t1), rindex); @@ -2790,9 +2825,12 @@ static bool string_iterator (gravity_vm *vm, gravity_value_t *args, uint16_t nar // compute new value gravity_int_t index = value.n; + if (index < 0 || (uint32_t)index >= string->len) RETURN_VALUE(VALUE_FROM_FALSE, rindex); if (index+1 < string->len) { uint32_t n = utf8_charbytes(string->s + index, 0); index += n; + // after advancing, check if new index is still within bounds + if ((uint32_t)index >= string->len) RETURN_VALUE(VALUE_FROM_FALSE, rindex); } else { RETURN_VALUE(VALUE_FROM_FALSE, rindex); } @@ -2804,7 +2842,9 @@ static bool string_iterator (gravity_vm *vm, gravity_value_t *args, uint16_t nar static bool string_iterator_next (gravity_vm *vm, gravity_value_t *args, uint16_t nargs, uint32_t rindex) { #pragma unused(vm, nargs) gravity_string_t *string = VALUE_AS_STRING(GET_VALUE(0)); - int32_t index = (int32_t)VALUE_AS_INT(GET_VALUE(1)); + gravity_int_t raw_index = VALUE_AS_INT(GET_VALUE(1)); + if (raw_index < 0 || (uint32_t)raw_index >= string->len) RETURN_VALUE(VALUE_FROM_NULL, rindex); + int32_t index = (int32_t)raw_index; uint32_t n = utf8_charbytes(string->s + index, 0); RETURN_VALUE(VALUE_FROM_STRING(vm, string->s + index, n), rindex); } @@ -3017,7 +3057,7 @@ static bool fiber_elapsed_time (gravity_vm *vm, gravity_value_t *args, uint16_t } static bool fiber_abort (gravity_vm *vm, gravity_value_t *args, uint16_t nargs, uint32_t rindex) { - gravity_value_t msg = (nargs > 0) ? GET_VALUE(1) : VALUE_FROM_NULL; + gravity_value_t msg = (nargs > 1) ? GET_VALUE(1) : VALUE_FROM_NULL; if (!VALUE_ISA_STRING(msg)) RETURN_ERROR("Fiber.abort expects a string as argument."); gravity_string_t *s = VALUE_AS_STRING(msg); @@ -3170,7 +3210,10 @@ static bool system_input (gravity_vm *vm, gravity_value_t *args, uint16_t nargs, char buffer[1024]; if (fgets(buffer, sizeof(buffer), stdin) != NULL) { // remove trailing newline captured by fgets (default true) - if (remove_trailing) buffer[strlen(buffer) - 1] = 0; + if (remove_trailing) { + size_t len = strlen(buffer); + if (len > 0 && buffer[len - 1] == '\n') buffer[len - 1] = 0; + } RETURN_VALUE(VALUE_FROM_CSTRING(vm, buffer), rindex); } diff --git a/src/runtime/gravity_vm.c b/src/runtime/gravity_vm.c index af8d71c2..617354ca 100644 --- a/src/runtime/gravity_vm.c +++ b/src/runtime/gravity_vm.c @@ -742,10 +742,10 @@ static bool gravity_vm_exec (gravity_vm *vm) { // decode operation DECODE_BINARY_OPERATION(r1,r2,r3); - // check fast comparison only if both values are boolean OR if one of them is undefined + // check fast equality for boolean/undefined (only valid for EQ/NEQ, not ordered comparisons) DEFINE_STACK_VARIABLE(v2,r2); DEFINE_STACK_VARIABLE(v3,r3); - if ((VALUE_ISA_BOOL(v2) && (VALUE_ISA_BOOL(v3))) || (VALUE_ISA_UNDEFINED(v2) || (VALUE_ISA_UNDEFINED(v3)))) { + if ((op == EQ || op == NEQ) && ((VALUE_ISA_BOOL(v2) && (VALUE_ISA_BOOL(v3))) || (VALUE_ISA_UNDEFINED(v2) || (VALUE_ISA_UNDEFINED(v3))))) { register gravity_int_t eq_result = (v2.isa == v3.isa) && (v2.n == v3.n); SETVALUE(r1, VALUE_FROM_BOOL((op == EQ) ? eq_result : !eq_result)); DISPATCH(); @@ -1837,11 +1837,13 @@ void gravity_vm_setslot (gravity_vm *vm, gravity_value_t value, uint32_t index) return; } + if (!vm->fiber->nframes) return; gravity_callframe_t *frame = &(vm->fiber->frames[vm->fiber->nframes-1]); frame->stackstart[index] = value; } gravity_value_t gravity_vm_getslot (gravity_vm *vm, uint32_t index) { + if (!vm->fiber->nframes) return VALUE_FROM_NULL; gravity_callframe_t *frame = &(vm->fiber->frames[vm->fiber->nframes-1]); return frame->stackstart[index]; } diff --git a/src/runtime/gravity_vmmacros.h b/src/runtime/gravity_vmmacros.h index 7a332b1d..a1dadeda 100644 --- a/src/runtime/gravity_vmmacros.h +++ b/src/runtime/gravity_vmmacros.h @@ -31,7 +31,7 @@ #define OPCODE_GET_ONE26bit(op, n) n = (op & 0x3FFFFFF) #define OPCODE_GET_ONE8bit_ONE10bit(op,r1,r3) r1 = (op >> 18) & 0xFF; r3 = (op & 0x3FF) #define OPCODE_GET_THREE8bit(op,r1,r2,r3) OPCODE_GET_TWO8bit_ONE10bit(op,r1,r2,r3) -#define OPCODE_GET_FOUR8bit(op,r1,r2,r3,r4) r1 = (op >> 24) & 0xFF; r2 = (op >> 16) & 0xFF; r3 = (op >> 8) & 0xFF; r4 = (op & 0xFF) +#define OPCODE_GET_FOUR8bit(op,r1,r2,r3,r4) r1 = (op >> 18) & 0xFF; r2 = (op >> 10) & 0xFF; r3 = (op >> 2) & 0xFF; r4 = (op & 0x03) #define OPCODE_GET_THREE8bit_ONE2bit(op,r1,r2,r3,r4) r1 = (op >> 18) & 0xFF; r2 = (op >> 10) & 0xFF; r3 = (op >> 2) & 0xFF; r4 = (op & 0x03) #define GRAVITY_VM_DEBUG 0 // print each VM instruction diff --git a/src/shared/gravity_array.h b/src/shared/gravity_array.h index eb44ad13..958a393f 100644 --- a/src/shared/gravity_array.h +++ b/src/shared/gravity_array.h @@ -28,13 +28,15 @@ #define marray_dec(v) (--(v).n) #define marray_nset(v,N) ((v).n = N) #define marray_push(type, v, x) do {if ((v).n == (v).m) { \ - (v).m = (v).m? (v).m<<1 : MARRAY_DEFAULT_SIZE; \ - void *_tmp = realloc((v).p, sizeof(type) * (v).m); \ - if (_tmp) (v).p = (type*)_tmp;} \ - if ((v).p) (v).p[(v).n++] = (x);} while(0) -#define marray_resize(type, v, n) do { (v).m += (n); (v).p = (type*)realloc((v).p, sizeof(type) * (v).m); } while(0) -#define marray_resize0(type, v, n) do { (v).p = (type*)realloc((v).p, sizeof(type) * ((v).m+(n))); \ - (v).m ? memset((v).p+(v).m, 0, (sizeof(type) * (n))) : memset((v).p, 0, (sizeof(type) * (n))); (v).m += (n); } while(0) + size_t _newm = (v).m? (v).m<<1 : MARRAY_DEFAULT_SIZE; \ + void *_tmp = realloc((v).p, sizeof(type) * _newm); \ + if (_tmp) { (v).p = (type*)_tmp; (v).m = _newm; }} \ + if ((v).p && (v).n < (v).m) (v).p[(v).n++] = (x);} while(0) +#define marray_resize(type, v, n) do { void *_tmp = realloc((v).p, sizeof(type) * ((v).m+(n))); \ + if (_tmp) { (v).p = (type*)_tmp; (v).m += (n); }} while(0) +#define marray_resize0(type, v, n) do { void *_tmp = realloc((v).p, sizeof(type) * ((v).m+(n))); \ + if (_tmp) { (v).p = (type*)_tmp; \ + memset((v).p+(v).m, 0, (sizeof(type) * (n))); (v).m += (n); }} while(0) #define marray_npop(v,k) ((v).n -= k) #define marray_reset(v,k) ((v).n = k) #define marray_reset0(v) marray_reset(v, 0) diff --git a/src/shared/gravity_hash.c b/src/shared/gravity_hash.c index 425f25cd..f8b34a2f 100644 --- a/src/shared/gravity_hash.c +++ b/src/shared/gravity_hash.c @@ -195,7 +195,8 @@ void gravity_hash_free (gravity_hash_t *hashtable) { uint32_t gravity_hash_memsize (gravity_hash_t *hashtable) { uint32_t size = sizeof(gravity_hash_t); - size += hashtable->size * sizeof(hash_node_t); + size += hashtable->size * sizeof(hash_node_t*); + size += hashtable->count * sizeof(hash_node_t); return size; } @@ -331,9 +332,8 @@ uint32_t gravity_hash_compute_int (gravity_int_t n) { } uint32_t gravity_hash_compute_float (gravity_float_t f) { - char buffer[24]; - // was %g but we don't like scientific notation nor the missing .0 in case of float number with no decimals - snprintf(buffer, sizeof(buffer), "%f", f); + char buffer[32]; + snprintf(buffer, sizeof(buffer), "%.17g", f); return murmur3_32(buffer, (uint32_t)strlen(buffer), HASH_SEED_VALUE); } diff --git a/src/shared/gravity_value.c b/src/shared/gravity_value.c index 8bcc8e42..8e373e03 100644 --- a/src/shared/gravity_value.c +++ b/src/shared/gravity_value.c @@ -126,7 +126,10 @@ bool gravity_class_grow (gravity_class_t *c, uint32_t n) { gravity_value_t *new_ivars = (gravity_value_t *)mem_alloc(NULL, new_nivars * sizeof(gravity_value_t)); if (!new_ivars) return false; for (uint32_t i=0; iivars) mem_free(c->ivars); + if (c->ivars) { + memcpy(new_ivars, c->ivars, c->nivars * sizeof(gravity_value_t)); + mem_free(c->ivars); + } c->ivars = new_ivars; c->nivars = new_nivars; return true; @@ -596,6 +599,9 @@ uint16_t gravity_function_cpool_add (gravity_vm *vm, gravity_function_t *f, grav } } + // safety check: cpool index must fit in uint16_t + if (n >= UINT16_MAX) return UINT16_MAX; + // vm is required here because I cannot know in advance if v is already in the pool or not // and value object v must be added to the VM only once if ((vm) && (gravity_value_isobject(v))) gravity_vm_transfer(vm, VALUE_AS_OBJECT(v)); diff --git a/src/shared/gravity_value.h b/src/shared/gravity_value.h index 900312f9..f357345f 100644 --- a/src/shared/gravity_value.h +++ b/src/shared/gravity_value.h @@ -66,8 +66,8 @@ extern "C" { #endif -#define GRAVITY_VERSION "0.9.0" // git tag 0.9.0 -#define GRAVITY_VERSION_NUMBER 0x000900 // git push --tags +#define GRAVITY_VERSION "0.9.5" // git tag 0.9.5 +#define GRAVITY_VERSION_NUMBER 0x000905 // git push --tags #define GRAVITY_BUILD_DATE __DATE__ #ifndef GRAVITY_ENABLE_DOUBLE diff --git a/src/utils/gravity_debug.c b/src/utils/gravity_debug.c index 4f5c2640..c66db227 100644 --- a/src/utils/gravity_debug.c +++ b/src/utils/gravity_debug.c @@ -34,17 +34,18 @@ const char *opcode_name (opcode_t op) { "BOR", "BXOR", "BNOT", "MAPNEW", "LISTNEW", "RANGENEW", "SETLIST", "CLOSURE", "CLOSE", "CHECK", "RESERVED2", "RESERVED3", "RESERVED4", "RESERVED5", "RESERVED6"}; + if ((unsigned)op >= sizeof(optable)/sizeof(optable[0])) return "UNKNOWN"; return optable[op]; } -#define DUMP_VM(buffer, bindex, ...) bindex += snprintf(&buffer[bindex], balloc-bindex, "%06u\t", pc); \ - bindex += snprintf(&buffer[bindex], balloc-bindex, __VA_ARGS__); \ - bindex += snprintf(&buffer[bindex], balloc-bindex, "\n"); +#define DUMP_VM(buffer, bindex, ...) if (bindex < balloc) bindex += snprintf(&buffer[bindex], balloc-bindex, "%06u\t", pc); \ + if (bindex < balloc) bindex += snprintf(&buffer[bindex], balloc-bindex, __VA_ARGS__); \ + if (bindex < balloc) bindex += snprintf(&buffer[bindex], balloc-bindex, "\n"); -#define DUMP_VM_NOCR(buffer, bindex, ...) bindex += snprintf(&buffer[bindex], balloc-bindex, "%06u\t", pc); \ - bindex += snprintf(&buffer[bindex], balloc-bindex, __VA_ARGS__); +#define DUMP_VM_NOCR(buffer, bindex, ...) if (bindex < balloc) bindex += snprintf(&buffer[bindex], balloc-bindex, "%06u\t", pc); \ + if (bindex < balloc) bindex += snprintf(&buffer[bindex], balloc-bindex, __VA_ARGS__); -#define DUMP_VM_RAW(buffer, bindex, ...) bindex += snprintf(&buffer[bindex], balloc-bindex, __VA_ARGS__); +#define DUMP_VM_RAW(buffer, bindex, ...) if (bindex < balloc) bindex += snprintf(&buffer[bindex], balloc-bindex, __VA_ARGS__); const char *gravity_disassemble (gravity_vm *vm, gravity_function_t *f, const char *bcode, uint32_t blen, bool deserialize) { uint32_t *ip = NULL; @@ -248,8 +249,9 @@ const char *gravity_disassemble (gravity_vm *vm, gravity_function_t *f, const ch ++pc; } + if (ip && deserialize) mem_free(ip); return buffer; - + abort_disassemble: if (ip && deserialize) mem_free(ip); if (buffer) mem_free(buffer); diff --git a/src/utils/gravity_json.c b/src/utils/gravity_json.c index a1801c8c..30dd47c2 100755 --- a/src/utils/gravity_json.c +++ b/src/utils/gravity_json.c @@ -158,7 +158,8 @@ static void json_write_escaped (json_t *json, const char *buffer, size_t len, bo return; } - char *new_buffer = mem_alloc(NULL, len*2); + if (len > SIZE_MAX / 6) return; + char *new_buffer = mem_alloc(NULL, len*6+1); size_t j = 0; assert(new_buffer); @@ -173,7 +174,14 @@ static void json_write_escaped (json_t *json, const char *buffer, size_t len, bo case '\r': JSON_ESCAPE ('r'); continue; case '\t': JSON_ESCAPE ('t'); continue; - default: new_buffer[j] = c; ++j;break; + default: + if ((unsigned char)c < 0x20) { + // escape other control characters as \uXXXX + j += snprintf(&new_buffer[j], 7, "\\u%04x", (unsigned char)c); + } else { + new_buffer[j] = c; ++j; + } + break; }; } diff --git a/src/utils/gravity_utils.c b/src/utils/gravity_utils.c index 9ff34361..a428903b 100644 --- a/src/utils/gravity_utils.c +++ b/src/utils/gravity_utils.c @@ -127,7 +127,7 @@ bool file_delete (const char *path) { } char *file_read(const char *path, size_t *len) { - int fd = 0; + int fd = -1; off_t fsize = 0; size_t fsize2 = 0; char *buffer = NULL; @@ -186,7 +186,7 @@ char *file_buildpath (const char *filename, const char *dirpath) { if (!full_path) return NULL; #ifdef WIN32 - PathCombineA(full_path, filename, dirpath); + PathCombineA(full_path, dirpath, filename); #else // check if PATH_SEPARATOR exists in dirpath if ((len2) && (dirpath[len2-1] != PATH_SEPARATOR)) @@ -213,6 +213,8 @@ char *file_name_frompath (const char *path) { break; } } + // if no separator found, the entire path is the filename + if (!name) name = string_dup(buffer); mem_free(buffer); return name; } @@ -363,6 +365,7 @@ int string_cmp (const char *s1, const char *s2) { } char *string_dup (const char *s1) { + if (!s1) return NULL; size_t len = (size_t)strlen(s1); char*s = (char *)mem_alloc(NULL, len + 1); if (!s) return NULL; @@ -660,11 +663,12 @@ int64_t number_from_bin (const char *s, uint32_t len) { // sanity check on len if (len > 64) return 0; - int64_t value = 0; + uint64_t value = 0; for (uint32_t i=0; i false); // 1 > 0 = true + var r4 = (true < false); // 1 < 0 = false + var r5 = (true <= true); // 1 <= 1 = true + var r6 = (true >= true); // 1 >= 1 = true + var r7 = (false < true); // 0 < 1 = true + var r8 = (false >= true); // 0 >= 1 = false + + if (!r1) return false; + if (!r2) return false; + if (!r3) return false; + if (r4) return false; + if (!r5) return false; + if (!r6) return false; + if (!r7) return false; + if (r8) return false; + + return true; +} diff --git a/test/unittest/bugfix_buildpath.gravity b/test/unittest/bugfix_buildpath.gravity new file mode 100644 index 00000000..87e11b3c --- /dev/null +++ b/test/unittest/bugfix_buildpath.gravity @@ -0,0 +1,15 @@ +#unittest { + name: "File.buildpath constructs correct path."; + result: true; +}; + +func main() { + var p = File.buildpath("test.txt", "/tmp"); + if (p != "/tmp/test.txt") return false; + + // with trailing separator + var p2 = File.buildpath("file.dat", "/var/data/"); + if (p2 != "/var/data/file.dat") return false; + + return true; +} diff --git a/test/unittest/bugfix_class_inherit_ivars.gravity b/test/unittest/bugfix_class_inherit_ivars.gravity new file mode 100644 index 00000000..0fb22270 --- /dev/null +++ b/test/unittest/bugfix_class_inherit_ivars.gravity @@ -0,0 +1,27 @@ +#unittest { + name: "Class grow preserves inherited instance variables."; + result: 42; +}; + +class Base { + var value; + func init() { + value = 42; + } +} + +class Child : Base { + var extra; + func init() { + super.init(); + extra = 100; + } + func getBase() { + return value; + } +} + +func main() { + var c = Child(); + return c.getBase(); +} diff --git a/test/unittest/bugfix_const_folding.gravity b/test/unittest/bugfix_const_folding.gravity new file mode 100644 index 00000000..8b95f424 --- /dev/null +++ b/test/unittest/bugfix_const_folding.gravity @@ -0,0 +1,32 @@ +#unittest { + name: "Constant folding produces correct results."; + result: true; +}; + +func main() { + // these constant expressions should be folded by the optimizer + var a = 10 + 20; + if (a != 30) return false; + + var b = 100 - 25; + if (b != 75) return false; + + var c = 6 * 7; + if (c != 42) return false; + + var d = 100 / 4; + if (d != 25) return false; + + // mixed int/float + var e = 10 + 2.5; + if (e != 12.5) return false; + + var f = 3.0 * 4.0; + if (f != 12.0) return false; + + // nested constant expressions + var g = 2 + 3 + 4; + if (g != 9) return false; + + return true; +} diff --git a/test/unittest/bugfix_fiber_abort_noargs.gravity b/test/unittest/bugfix_fiber_abort_noargs.gravity new file mode 100644 index 00000000..57506b54 --- /dev/null +++ b/test/unittest/bugfix_fiber_abort_noargs.gravity @@ -0,0 +1,8 @@ +#unittest { + name: "Fiber.abort with no string arg errors."; + error: RUNTIME; +}; + +func main() { + Fiber.abort(); +} diff --git a/test/unittest/bugfix_file_open_noargs.gravity b/test/unittest/bugfix_file_open_noargs.gravity new file mode 100644 index 00000000..d87b831a --- /dev/null +++ b/test/unittest/bugfix_file_open_noargs.gravity @@ -0,0 +1,9 @@ +#unittest { + name: "File open with no args should not crash."; + error: RUNTIME; +}; + +func main() { + var f = File.open(); + return f; +} diff --git a/test/unittest/bugfix_file_read_negative.gravity b/test/unittest/bugfix_file_read_negative.gravity new file mode 100644 index 00000000..fd8361fb --- /dev/null +++ b/test/unittest/bugfix_file_read_negative.gravity @@ -0,0 +1,11 @@ +#unittest { + name: "File read with negative size should not crash."; + error: RUNTIME; +}; + +func main() { + var f = File.open("test/unittest/bugfix_file_read_negative.gravity", "r"); + var data = f.read(-1); + f.close(); + return data; +} diff --git a/test/unittest/bugfix_file_readwrite.gravity b/test/unittest/bugfix_file_readwrite.gravity new file mode 100644 index 00000000..8042543d --- /dev/null +++ b/test/unittest/bugfix_file_readwrite.gravity @@ -0,0 +1,27 @@ +#unittest { + name: "File read/write returns correct byte count."; + result: true; +}; + +func main() { + var path = "/tmp/_gravity_rw_test.txt"; + var data = "Hello, Gravity!"; + + // write data and check byte count returned + var f = File.open(path, "w"); + if (!f) return false; + var nwritten = f.write(data); + f.close(); + if (nwritten != data.length) return false; + + // read data back and verify content and length + var f2 = File.open(path, "r"); + if (!f2) return false; + var content = f2.read(100); + f2.close(); + + if (content.length != data.length) return false; + if (content != data) return false; + + return true; +} diff --git a/test/unittest/bugfix_func_default_args.gravity b/test/unittest/bugfix_func_default_args.gravity new file mode 100644 index 00000000..259a53e1 --- /dev/null +++ b/test/unittest/bugfix_func_default_args.gravity @@ -0,0 +1,27 @@ +#unittest { + name: "Function default args fill correct slots."; + result: true; +}; + +func add3(a, b, c) { + if (c == undefined) c = 10; + if (b == undefined) b = 20; + return a + b + c; +} + +func main() { + // call with all args + var r1 = add3(1, 2, 3); + if (r1 != 6) return false; + + // call with fewer args — missing slots should be undefined, not overwrite existing + var r2 = add3(100, 200); + // a=100, b=200, c=undefined->10 + if (r2 != 310) return false; + + var r3 = add3(5); + // a=5, b=undefined->20, c=undefined->10 + if (r3 != 35) return false; + + return true; +} diff --git a/test/unittest/bugfix_inline_exec.gravity b/test/unittest/bugfix_inline_exec.gravity new file mode 100644 index 00000000..1611108f --- /dev/null +++ b/test/unittest/bugfix_inline_exec.gravity @@ -0,0 +1,20 @@ +#unittest { + name: "Math.atan2 returns correct values."; + result: true; +}; + +func main() { + // atan2(0, 1) = 0 + var r1 = Math.atan2(0, 1); + if (r1 != 0) return false; + + // atan2(1, 0) should be pi/2 ~ 1.5707... + var r2 = Math.atan2(1, 0); + if (r2 < 1.57 || r2 > 1.58) return false; + + // atan2(1, 1) should be pi/4 ~ 0.7853... + var r3 = Math.atan2(1, 1); + if (r3 < 0.78 || r3 > 0.79) return false; + + return true; +} diff --git a/test/unittest/bugfix_int_random_range.gravity b/test/unittest/bugfix_int_random_range.gravity new file mode 100644 index 00000000..f8267f64 --- /dev/null +++ b/test/unittest/bugfix_int_random_range.gravity @@ -0,0 +1,28 @@ +#unittest { + name: "Int.random stays within range."; + result: true; +}; + +func main() { + // test that random values stay within bounds + var i = 0; + while (i < 100) { + var r = Int.random(10, 20); + if (r < 10 || r > 20) return false; + i += 1; + } + + // test reversed arguments + i = 0; + while (i < 100) { + var r = Int.random(20, 10); + if (r < 10 || r > 20) return false; + i += 1; + } + + // test equal bounds + var r = Int.random(5, 5); + if (r != 5) return false; + + return true; +} diff --git a/test/unittest/bugfix_json_stringify_escape.gravity b/test/unittest/bugfix_json_stringify_escape.gravity new file mode 100644 index 00000000..e769b8ca --- /dev/null +++ b/test/unittest/bugfix_json_stringify_escape.gravity @@ -0,0 +1,36 @@ +#unittest { + name: "JSON.stringify escapes special characters."; + result: true; +}; + +func main() { + // basic string + var r1 = JSON.stringify("hello"); + if (r1 != "\"hello\"") return false; + + // string with embedded quote: must be escaped as \" + var r2 = JSON.stringify("say \"hi\""); + if (r2 != "\"say \\\"hi\\\"\"") return false; + + // string with backslash: must be escaped as \\ + var r3 = JSON.stringify("a\\b"); + if (r3 != "\"a\\\\b\"") return false; + + // string with newline: must be escaped as \n + var r4 = JSON.stringify("line1\nline2"); + if (r4 != "\"line1\\nline2\"") return false; + + // string with tab: must be escaped as \t + var r5 = JSON.stringify("col1\tcol2"); + if (r5 != "\"col1\\tcol2\"") return false; + + // no special chars + var r6 = JSON.stringify("plain"); + if (r6 != "\"plain\"") return false; + + // empty string + var r7 = JSON.stringify(""); + if (r7 != "\"\"") return false; + + return true; +} diff --git a/test/unittest/bugfix_json_stringify_long.gravity b/test/unittest/bugfix_json_stringify_long.gravity new file mode 100644 index 00000000..abb664a7 --- /dev/null +++ b/test/unittest/bugfix_json_stringify_long.gravity @@ -0,0 +1,19 @@ +#unittest { + name: "JSON.stringify with string."; + result: true; +}; + +func main() { + // test basic JSON stringify with a string + var s = "hello world"; + var json = JSON.stringify(s); + // JSON.stringify wraps in quotes: "..." + if (json.length != s.length + 2) return false; + if (json != "\"hello world\"") return false; + + // empty string + var json2 = JSON.stringify(""); + if (json2 != "\"\"") return false; + + return true; +} diff --git a/test/unittest/bugfix_list_next_bounds.gravity b/test/unittest/bugfix_list_next_bounds.gravity new file mode 100644 index 00000000..07120832 --- /dev/null +++ b/test/unittest/bugfix_list_next_bounds.gravity @@ -0,0 +1,29 @@ +#unittest { + name: "List.next with invalid index returns null."; + result: true; +}; + +func main() { + var list = [10, 20, 30]; + + // valid indices + if (list.next(0) != 10) return false; + if (list.next(1) != 20) return false; + if (list.next(2) != 30) return false; + + // out-of-bounds: should return null, not crash + var r1 = list.next(100); + if (r1 != null) return false; + + var r2 = list.next(-1); + if (r2 != null) return false; + + // normal for-in still works + var sum = 0; + for (var x in list) { + sum += x; + } + if (sum != 60) return false; + + return true; +} diff --git a/test/unittest/bugfix_list_storeat_resize.gravity b/test/unittest/bugfix_list_storeat_resize.gravity new file mode 100644 index 00000000..39e78aac --- /dev/null +++ b/test/unittest/bugfix_list_storeat_resize.gravity @@ -0,0 +1,19 @@ +#unittest { + name: "List store-at with resize sets value correctly."; + result: true; +}; + +func main() { + var list = [0, 1, 2]; + + // store beyond current bounds, triggering resize + list[10] = 42; + + if (list[10] != 42) return false; + if (list.count != 11) return false; + + // elements between old end and new index should be null + if (list[5] != null) return false; + + return true; +} diff --git a/test/unittest/bugfix_math_logx_base1.gravity b/test/unittest/bugfix_math_logx_base1.gravity new file mode 100644 index 00000000..04cae708 --- /dev/null +++ b/test/unittest/bugfix_math_logx_base1.gravity @@ -0,0 +1,18 @@ +#unittest { + name: "Math.logx with base 1 returns undefined."; + result: true; +}; + +func main() { + // logx(base=1, value) should not crash (division by zero) + // it returns undefined since log(1) = 0 + var r = Math.logx(1, 100); + if (r != undefined) return false; + + // normal cases should still work + var r2 = Math.logx(10, 100); + // log10(100) = 2.0 + if (r2 < 1.99 || r2 > 2.01) return false; + + return true; +} diff --git a/test/unittest/bugfix_math_random_range.gravity b/test/unittest/bugfix_math_random_range.gravity new file mode 100644 index 00000000..745fc94c --- /dev/null +++ b/test/unittest/bugfix_math_random_range.gravity @@ -0,0 +1,19 @@ +#unittest { + name: "Math.random integer range correctness."; + result: true; +}; + +func main() { + var i = 0; + while (i < 100) { + var r = Math.random(5, 15); + if (r < 5 || r > 15) return false; + i += 1; + } + + // equal bounds + var r2 = Math.random(7, 7); + if (r2 != 7) return false; + + return true; +} diff --git a/test/unittest/bugfix_math_round.gravity b/test/unittest/bugfix_math_round.gravity new file mode 100644 index 00000000..3e973469 --- /dev/null +++ b/test/unittest/bugfix_math_round.gravity @@ -0,0 +1,11 @@ +#unittest { + name: "Math round edge cases."; + result: true; +}; + +func main() { + var a = Math.round(3.14159, 2) == 3.14; + var b = Math.round(2.5, 0) == 3; + var c = Math.round(100.0, 0) == 100; + return a && b && c; +} diff --git a/test/unittest/bugfix_math_round_precision.gravity b/test/unittest/bugfix_math_round_precision.gravity new file mode 100644 index 00000000..ff9ccc2c --- /dev/null +++ b/test/unittest/bugfix_math_round_precision.gravity @@ -0,0 +1,24 @@ +#unittest { + name: "Math.round with decimal digits."; + result: true; +}; + +func main() { + // round to 2 decimal places + var r1 = Math.round(3.14159, 2); + if (r1 < 3.13 || r1 > 3.15) return false; + + // round to 0 decimal places (integer) + var r2 = Math.round(3.7, 0); + if (r2 < 3.9 || r2 > 4.1) return false; + + // round to 1 decimal place + var r3 = Math.round(2.55, 1); + if (r3 < 2.5 || r3 > 2.7) return false; + + // negative value + var r4 = Math.round(-1.5); + if (r4 < -2.1 || r4 > -1.4) return false; + + return true; +} diff --git a/test/unittest/bugfix_math_xrt.gravity b/test/unittest/bugfix_math_xrt.gravity new file mode 100644 index 00000000..47978881 --- /dev/null +++ b/test/unittest/bugfix_math_xrt.gravity @@ -0,0 +1,28 @@ +#unittest { + name: "Math.xrt computes correct root values."; + result: true; +}; + +func main() { + // square root of 9: xrt(base, value) = value^(1/base) + var r1 = Math.xrt(2, 9); + if (r1 < 2.99 || r1 > 3.01) return false; + + // cube root of 27 + var r2 = Math.xrt(3, 27); + if (r2 < 2.99 || r2 > 3.01) return false; + + // 4th root of 16 + var r3 = Math.xrt(4, 16); + if (r3 < 1.99 || r3 > 2.01) return false; + + // float base: cube root of 8.0 + var r4 = Math.xrt(3, 8.0); + if (r4 < 1.99 || r4 > 2.01) return false; + + // float value and float base + var r5 = Math.xrt(3.0, 27.0); + if (r5 < 2.99 || r5 > 3.01) return false; + + return true; +} diff --git a/test/unittest/bugfix_neg_optimize.gravity b/test/unittest/bugfix_neg_optimize.gravity new file mode 100644 index 00000000..d3c9e2d0 --- /dev/null +++ b/test/unittest/bugfix_neg_optimize.gravity @@ -0,0 +1,31 @@ +#unittest { + name: "NEG optimization produces correct result."; + result: true; +}; + +func main() { + // The optimizer folds LOADI+NEG into a single negated LOADI. + // Before fix, the result was written to the wrong register. + var a = -42; + if (a != -42) return false; + + var b = -1; + if (b != -1) return false; + + // verify in expressions + var c = 10 + -3; + if (c != 7) return false; + + var d = -100 + 50; + if (d != -50) return false; + + // double negation + var e = -(-5); + if (e != 5) return false; + + // float + var f = -3.14; + if (f > -3.13 || f < -3.15) return false; + + return true; +} diff --git a/test/unittest/bugfix_range_backward_forin.gravity b/test/unittest/bugfix_range_backward_forin.gravity new file mode 100644 index 00000000..2c1fe8ef --- /dev/null +++ b/test/unittest/bugfix_range_backward_forin.gravity @@ -0,0 +1,34 @@ +#unittest { + name: "Backward range loop iterates correctly."; + result: true; +}; + +func main() { + // backward range via .loop() should iterate in descending order + var values = []; + Range(5, 1).loop(func(i) { + values.push(i); + }); + if (values.count != 5) return false; + if (values[0] != 5) return false; + if (values[1] != 4) return false; + if (values[2] != 3) return false; + if (values[3] != 2) return false; + if (values[4] != 1) return false; + + // forward range loop still works + var sum = 0; + Range(1, 5).loop(func(i) { + sum += i; + }); + if (sum != 15) return false; + + // single-element range + var count = 0; + Range(3, 3).loop(func(i) { + count += 1; + }); + if (count != 1) return false; + + return true; +} diff --git a/test/unittest/bugfix_range_contains.gravity b/test/unittest/bugfix_range_contains.gravity new file mode 100644 index 00000000..b2fff478 --- /dev/null +++ b/test/unittest/bugfix_range_contains.gravity @@ -0,0 +1,24 @@ +#unittest { + name: "Range.contains works for backward ranges."; + result: true; +}; + +func main() { + // forward range contains + var r1 = Range(1, 10); + if (!r1.contains(5)) return false; + if (!r1.contains(1)) return false; + if (!r1.contains(10)) return false; + if (r1.contains(0)) return false; + if (r1.contains(11)) return false; + + // backward range contains + var r2 = Range(10, 1); + if (!r2.contains(5)) return false; + if (!r2.contains(1)) return false; + if (!r2.contains(10)) return false; + if (r2.contains(0)) return false; + if (r2.contains(11)) return false; + + return true; +} diff --git a/test/unittest/bugfix_reverse_range.gravity b/test/unittest/bugfix_reverse_range.gravity new file mode 100644 index 00000000..45b2e80c --- /dev/null +++ b/test/unittest/bugfix_reverse_range.gravity @@ -0,0 +1,13 @@ +#unittest { + name: "Reverse range loop iteration."; + result: 55; +}; + +func main() { + var sum = 0; + var r = Range(10, 1); + r.loop(func(i) { + sum += i; + }); + return sum; +} diff --git a/test/unittest/bugfix_string_count.gravity b/test/unittest/bugfix_string_count.gravity new file mode 100644 index 00000000..c1fbf9f7 --- /dev/null +++ b/test/unittest/bugfix_string_count.gravity @@ -0,0 +1,30 @@ +#unittest { + name: "String.count with various inputs."; + result: true; +}; + +func main() { + var s = "hello world hello"; + + // count occurrences of a substring + var c1 = s.count("hello"); + if (c1 != 2) return false; + + // count single character ("hello world hello" has 5 l's) + var c2 = s.count("l"); + if (c2 != 5) return false; + + // count something not present + var c3 = s.count("xyz"); + if (c3 != 0) return false; + + // count empty string should return 0 (not crash) + var c4 = s.count(""); + if (c4 != 0) return false; + + // count on empty string + var c5 = "".count("a"); + if (c5 != 0) return false; + + return true; +} diff --git a/test/unittest/bugfix_string_count_partial.gravity b/test/unittest/bugfix_string_count_partial.gravity new file mode 100644 index 00000000..e88e394b --- /dev/null +++ b/test/unittest/bugfix_string_count_partial.gravity @@ -0,0 +1,35 @@ +#unittest { + name: "String.count finds matches after partial match failure."; + result: true; +}; + +func main() { + // basic match + if ("hello".count("l") != 2) return false; + + // match after partial match failure: "aab".count("ab") must find "ab" at position 1 + if ("aab".count("ab") != 1) return false; + + // another partial match case: "aaab".count("aab") should find 1 + if ("aaab".count("aab") != 1) return false; + + // multiple non-overlapping matches + if ("abcabc".count("abc") != 2) return false; + + // no match + if ("hello".count("xyz") != 0) return false; + + // match at end + if ("foobar".count("bar") != 1) return false; + + // single character repeated + if ("aaaa".count("aa") != 2) return false; + + // empty search string + if ("hello".count("") != 0) return false; + + // full string match + if ("abc".count("abc") != 1) return false; + + return true; +} diff --git a/test/unittest/bugfix_string_iteration.gravity b/test/unittest/bugfix_string_iteration.gravity new file mode 100644 index 00000000..c4ee9151 --- /dev/null +++ b/test/unittest/bugfix_string_iteration.gravity @@ -0,0 +1,27 @@ +#unittest { + name: "String iteration collects all characters."; + result: true; +}; + +func main() { + var s = "abcde"; + var count = 0; + var collected = ""; + + for (var c in s) { + collected = collected + c; + count += 1; + } + + if (count != 5) return false; + if (collected != "abcde") return false; + + // empty string iteration + var count2 = 0; + for (var c in "") { + count2 += 1; + } + if (count2 != 0) return false; + + return true; +} diff --git a/test/unittest/bugfix_string_loop_utf8.gravity b/test/unittest/bugfix_string_loop_utf8.gravity new file mode 100644 index 00000000..221d6715 --- /dev/null +++ b/test/unittest/bugfix_string_loop_utf8.gravity @@ -0,0 +1,28 @@ +#unittest { + name: "String.loop iterates UTF-8 characters correctly."; + result: true; +}; + +func main() { + // ASCII string loop + var count = 0; + var collected = ""; + "abc".loop(func(c) { + collected = collected + c; + count += 1; + }); + if (count != 3) return false; + if (collected != "abc") return false; + + // multi-byte UTF-8: £ is 2 bytes + var count2 = 0; + var collected2 = ""; + "£A£".loop(func(c) { + collected2 = collected2 + c; + count2 += 1; + }); + if (count2 != 3) return false; + if (collected2 != "£A£") return false; + + return true; +} diff --git a/test/unittest/bugfix_string_reverse_utf8.gravity b/test/unittest/bugfix_string_reverse_utf8.gravity new file mode 100644 index 00000000..c0a4d4f0 --- /dev/null +++ b/test/unittest/bugfix_string_reverse_utf8.gravity @@ -0,0 +1,24 @@ +#unittest { + name: "String reverse subscript preserves ASCII."; + result: true; +}; + +func main() { + var s = "abcde"; + + // reverse subscript: s[4...0] should give "edcba" + var r = s[4...0]; + if (r != "edcba") return false; + + // single char via range + var s2 = "x"; + var r2 = s2[0...0]; + if (r2 != "x") return false; + + // two char reverse + var s3 = "ab"; + var r3 = s3[1...0]; + if (r3 != "ba") return false; + + return true; +} diff --git a/test/unittest/bugfix_string_upper_lower.gravity b/test/unittest/bugfix_string_upper_lower.gravity new file mode 100644 index 00000000..513db0ab --- /dev/null +++ b/test/unittest/bugfix_string_upper_lower.gravity @@ -0,0 +1,11 @@ +#unittest { + name: "String upper and lower correctness."; + result: true; +}; + +func main() { + var s = "Hello World"; + var u = s.upper(); + var l = s.lower(); + return (u == "HELLO WORLD") && (l == "hello world"); +} From 38b2a57d0dcb35ae4382c7d4b648585d409b9c16 Mon Sep 17 00:00:00 2001 From: Marco Bambini Date: Tue, 24 Feb 2026 05:38:20 +0100 Subject: [PATCH 06/37] Update README.md --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 1a1f0c4b..5e954326 100644 --- a/README.md +++ b/README.md @@ -86,12 +86,12 @@ Requires a C99 compiler. No external dependencies. ## Usage ```bash -./gravity file.gravity # Compile and execute a source file -./gravity -c file.gravity # Compile to bytecode (outputs gravity.json) -./gravity -o out.json -c file.gravity # Compile to a specific output file -./gravity -x gravity.json # Execute precompiled bytecode -./gravity -i 'return 2 + 3' # Execute inline code -./gravity -t test/unittest # Run unit tests +./gravity file.gravity # Compile and execute a source file +./gravity -c file.gravity # Compile to bytecode (outputs gravity.json) +./gravity -o out.json -c file.gravity # Compile to a specific output file +./gravity -x gravity.json # Execute precompiled bytecode +./gravity -i 'return 2 + 3' # Execute inline code +./gravity -t test/unittest # Run unit tests ``` ## Project Structure From 0e42cb82974bdc07d8396223ce7546c7a4f99ff5 Mon Sep 17 00:00:00 2001 From: Marco Bambini Date: Tue, 24 Feb 2026 05:42:36 +0100 Subject: [PATCH 07/37] Update ARCHITECTURE.md --- ARCHITECTURE.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index d229dae7..6fb7748f 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -77,11 +77,11 @@ src/ Source Code │ ▼ -┌─────────┐ +┌──────────┐ │ Lexer │ Character stream → Token stream └────┬─────┘ ▼ -┌─────────┐ +┌──────────┐ │ Parser │ Token stream → Abstract Syntax Tree └────┬─────┘ ▼ @@ -412,12 +412,14 @@ The lookup traverses the declaration stack from innermost to outermost: What can be declared inside each construct: ``` - func var enum class module + │ func var enum class module +------------------------------------------------- func │ YES YES NO YES YES var │ YES NO NO YES YES enum │ YES NO NO YES YES class │ YES NO NO YES YES module │ NO NO NO NO NO +------------------------------------------------- ``` --- From dd8158958ae0b018de6bbaf3cdb6fb354c536fbb Mon Sep 17 00:00:00 2001 From: Larry Barchett Date: Thu, 19 Feb 2026 00:05:37 -0500 Subject: [PATCH 08/37] fix(core): move gravity_opt_free after refcount check to prevent double-free MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit gravity_opt_free() was called unconditionally at the top of gravity_core_free(), before the refcount guard. This causes a double-free when gravity_compiler_reset() is called before teardown. gravity_compiler_reset() runs a mini-VM internally (internal_vm_cleanup) which frees GC objects via the GC callback — including the optionals. When gravity_core_free() is subsequently called, gravity_opt_free() then attempts to free already-freed memory. The fix moves gravity_opt_free() to after the refcount check, so optionals are only freed when the last VM is being torn down. Reproducer: call gravity_compiler_reset() on a compiler instance, then call gravity_core_free(). Crashes reliably under AddressSanitizer. --- src/runtime/gravity_core.c | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/runtime/gravity_core.c b/src/runtime/gravity_core.c index d47a9223..e629d95f 100644 --- a/src/runtime/gravity_core.c +++ b/src/runtime/gravity_core.c @@ -3634,14 +3634,15 @@ void gravity_core_init (void) { } void gravity_core_free (void) { - // free optionals first - gravity_opt_free(); - if (!core_inited) return; // check if others VM are still running if (--refcount) return; + // free optionals after refcount check — avoids double-free when mini-VM + // in gravity_compiler_reset() has already freed GC objects via internal_vm_cleanup + gravity_opt_free(); + // this function should never be called // it is just called when we need to internally check for memory leaks From 18b9195598d9b944376754c6d1ad76e38a4adca1 Mon Sep 17 00:00:00 2001 From: Marco Bambini Date: Tue, 14 Apr 2026 14:00:20 +0200 Subject: [PATCH 09/37] =?UTF-8?q?Bump=20version=20to=200.9.6=20=E2=80=94?= =?UTF-8?q?=20OOM=20safety,=20init-chain=20fix,=20docs,=20and=20test=20sui?= =?UTF-8?q?te=20improvements?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Runtime / VM: - Add configurable fiber stack size limit (DEFAULT_MAXSTACK_SIZE = 1M slots). Infinite recursion and stack exhaustion now produce a clean RUNTIME error instead of a hard crash or silent failure. - Expose GRAVITY_VM_MAXSTACK key via gravity_vm_get/set for runtime tuning. - Fix gravity_fiber_reassign stack growth: large register-window in $moduleinit could push stacktop past the initial allocation (issue #437). Compiler: - Fix class $init chain infinite recursion: parent $init helpers ($init2, $init3, …) previously used a dynamic name lookup against self, which resolved to the wrong override in subclass context. Now emits a direct static closure reference (LOADK) via ircode_patch_init_direct. Tests: - Re-enable test/disabled/heap.gravity and test/disabled/loop1.gravity, moved to test/unittest/. Both now pass with the new OOM error reporting. - Fix two bugs in heap.gravity (wrong variable assigned, wrong constructor called). - Add regression test for issue #437 (bugfix_stack_overflow_large_regwin). - Delete test/disabled/ directory. Docs: - Add CHANGELOG.md covering all versions from 0.2.8 to 0.9.6. - Update README: CMake build instructions, Testing section, Embedding API example, CHANGELOG link, replace Gitter with GitHub Discussions. - Update CLAUDE.md: document fuzzy/infiniteloop test dirs and gravity_vm_get/set. --- CHANGELOG.md | 194 ++++++++++++++++++ CLAUDE.md | 5 + README.md | 71 ++++++- src/compiler/gravity_codegen.c | 16 +- src/compiler/gravity_ircode.c | 51 +++++ src/compiler/gravity_ircode.h | 1 + src/runtime/gravity_vm.c | 29 ++- src/runtime/gravity_vm.h | 1 + src/runtime/gravity_vmmacros.h | 2 +- src/shared/gravity_value.c | 18 ++ src/shared/gravity_value.h | 5 +- test/disabled/README.txt | 3 - ...bugfix_stack_overflow_large_regwin.gravity | 25 +++ test/{disabled => unittest}/heap.gravity | 5 +- test/{disabled => unittest}/loop1.gravity | 0 15 files changed, 404 insertions(+), 22 deletions(-) create mode 100644 CHANGELOG.md delete mode 100644 test/disabled/README.txt create mode 100644 test/unittest/bugfix_stack_overflow_large_regwin.gravity rename test/{disabled => unittest}/heap.gravity (95%) rename test/{disabled => unittest}/loop1.gravity (100%) diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..4021ee84 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,194 @@ +# Changelog + +All notable changes to Gravity are documented in this file. + +## [0.9.6] - Unreleased + +### Fixed +- **Stack overflow now produces a clean runtime error** instead of a hard crash (SIGSEGV). Infinite recursion and pathological call depths are caught by a configurable stack size limit before the process runs out of memory. +- **Class `$init` chain infinite recursion** — parent-class `$init` helpers (`$init2`, `$init3`, …) previously used a dynamic name lookup against `self`, which resolved to the wrong (overriding) function when called from a subclass, producing infinite recursion. The compiler now emits a direct static closure reference (`LOADK`) so dispatch is always to the correct ancestor function. +- **Fiber stack growth in `gravity_fiber_reassign`** — a large register-window allocation in `$moduleinit` could cause the initial stack pointer to overshoot `DEFAULT_MINSTACK_SIZE` (256 slots), leaving `stacktop` pointing into unallocated memory (issue #437). +- **`gravity_opt_free` double-free** — optional module cleanup now checks the reference count before freeing (PR #436). + +### Added +- `GRAVITY_VM_MAXSTACK` — runtime-configurable maximum fiber stack size (default 1 048 576 slots / 16 MB). Readable and writable via `gravity_vm_get` / `gravity_vm_set` with key `"maxStack"`. +- Re-enabled two previously disabled tests (`heap.gravity`, `loop1.gravity`) — both now pass with the new OOM error reporting. + +### Changed +- Version bumped to **0.9.6** (`GRAVITY_VERSION`, `GRAVITY_VERSION_NUMBER`). + +--- + +## [0.9.5] - 2024 + +### Fixed +- Numerous memory leaks and use-after-free errors throughout the compiler and runtime. +- Memory safety improvements across GC, value handling, and object lifecycle. +- Clang build compatibility (PR #435). + +### Changed +- Documentation updates: ARCHITECTURE.md rewritten; README refreshed. + +--- + +## [0.9.0] - 2023 + +### Fixed +- Several memory leaks plugged across the compiler pipeline. +- Missing Makefile dependencies (PR #431). +- `File.read()` now returns `null` when zero characters are read. +- Replaced unsafe `printf` calls with `snprintf`. +- Fixed issue #394. + +### Added +- New unit test for leak-related regression coverage. + +--- + +## [0.8.5] - 2022 + +### Fixed +- Setter issue that affected several unit tests. +- File read size mismatch due to line-ending differences on Windows (PR #365). +- Compilation failure introduced by `O_BINARY` on non-Windows platforms. + +### Added +- Unit tests integrated into CI (PR #378). +- BSD shared-object build support; removed `WITH_GETLINE` (PR #375). + +--- + +## [0.8.4] - 2022 + +### Fixed +- Issue #379. +- Hash table header organisation (PR #369). + +--- + +## [0.8.3] - 2021 + +### Fixed +- Regression introduced by lazy-loading of superclasses in 0.8.2. + +--- + +## [0.8.2] - 2021 + +### Added +- Lazy loading of extern superclasses at runtime. +- Preliminary support for instance `deinit` (destructor). +- `System.input()` (PR #342). +- `ENV.argc` / `ENV.argv` properties (PR #343). +- ObjC binding example. +- C++ binding example. +- Ternary expression and `switch` statement codegen (PR #336). +- Optional `File` class (cross-platform). +- `gravity_instance_lookup_real_property` helper. +- `gravity_config.h` for platform-specific configuration (PR #301). +- Improved CMake: supports CLI, shared lib, and static lib targets (PR #299). + +### Fixed +- Inner class constructor returning wrong instance. +- Computed property (setter) bug. +- Sign-conversion and char-type warnings flagged by sanitizers. +- `stat` return-value check. +- `uint32_t`-to-`char` conversion in `utf8_encode`. +- `size_t` comparison against negative value in `file_read`. +- Various Windows / MSVC compatibility fixes. +- Implicit `long`-to-`double` conversion warning under Clang. +- Emscripten include-guard fix. + +### Changed +- Improved superclass type checking in the semantic analyser. +- Improved error handling and detection (0.8.0). +- `File.eof` renamed from `isEOF`. + +--- + +## [0.7.9] - 2020 + +### Fixed +- Improved error handling in the VM and runtime. + +### Added +- `vm` back-reference stored on `gravity_closure_t`. +- Computed-goto support for Clang on Windows. +- `DISPATCH_INNER` macro for `do/while(0)` loops without computed goto. +- `xdata` parameter on `delegate->optional_classes` (PR #307). + +--- + +## [0.7.8] - 2020 + +### Added +- Preliminary `Struct` support. +- `bind` method fix; unit test added. + +### Fixed +- Possible GC issue (unit test added). + +--- + +## [0.7.7] - 2020 + +### Fixed +- `super` keyword resolution issue; unit test added. + +--- + +## [0.7.6] - 2020 + +### Changed +- Optional classes renamed for consistency. + +### Fixed +- Setter unwanted side effect. + +--- + +## [0.7.5] - 2020 + +### Improved +- `float`/`double` to `String` conversion accuracy. +- Various core methods. + +--- + +## [0.7.4] - 2020 + +### Fixed +- `String.length` is now UTF-8 aware; `String.bytes` added (unit test added). +- Function returning address of local variable on Windows (`directory_read`). +- Division-by-zero warning suppression in GCC. +- Const output-buffer issue on Windows (`WideCharToMultiByte`). + +### Added +- More BSD targets in `make` and CMake. + +--- + +## [0.7.0] - 2019 + +### Added +- `String.split` and string iteration are now Unicode-aware; unit test added. +- Support for local `enum` declarations; unit test added. + +### Fixed +- Local class declarations. +- Superclass resolution edge cases. +- `continue` keyword inside `for` loops. +- `self` parameter in complex postfix expressions. +- Comparison between different object types no longer raises a spurious runtime error. + +--- + +## [0.6.x] - 2018–2019 + +Series of incremental releases adding language features (closures, ranges, maps, lists, optional modules) and fixing compiler and runtime issues. See git history for per-commit details. + +--- + +## [0.5.x] - 2017–2018 + +Initial public release series establishing the core language, VM, and compiler pipeline. diff --git a/CLAUDE.md b/CLAUDE.md index ac6258ea..1f68242c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -29,6 +29,10 @@ Compiler flags: `-std=gnu99 -fgnu89-inline -fPIC -DBUILD_GRAVITY_API` ./gravity -i 'print("hello")' # Inline execution ``` +The `test/` directory also contains: +- `test/fuzzy/` — randomised fuzzing inputs; all must compile/run without crashing +- `test/infiniteloop/` — programs that must terminate with a `RUNTIME` error (not hang) + CI runs: `make && test/unittest/run_all.sh` ## Architecture @@ -59,6 +63,7 @@ Core API in `src/runtime/` and example usage in `examples/example.c`: - `gravity_compiler_create/run` — compile source to closures - `gravity_vm_new/runmain/runclosure` — create VM and execute code - `gravity_vm_loadfile/loadbuffer` — load from file or memory +- `gravity_vm_get/gravity_vm_set` — read/write VM configuration at runtime (e.g. `GRAVITY_VM_MAXSTACK` to cap fiber stack growth) ## Code Style diff --git a/README.md b/README.md index 5e954326..d5acebed 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,7 @@ func main() { ## Building +**Make (Linux / macOS / BSD)** ```bash make # Build the gravity CLI executable make mode=debug # Debug build with symbols @@ -81,6 +82,15 @@ make example # Build the C embedding API example make clean # Clean all build artifacts ``` +**CMake (cross-platform, including Windows)** +```bash +cmake -B build +cmake --build build +# Optionally disable the CLI and build the library only: +cmake -B build -DBUILD_CLI=OFF +cmake --build build +``` + Requires a C99 compiler. No external dependencies. ## Usage @@ -94,6 +104,16 @@ Requires a C99 compiler. No external dependencies. ./gravity -t test/unittest # Run unit tests ``` +## Testing + +```bash +./gravity -t test/unittest # Run all unit tests via the VM +./test/unittest/run_all.sh # Run all unit tests via shell script (with per-test timeouts) +./gravity test/unittest/somefile.gravity # Run a single test file +``` + +The `test/` directory also contains `fuzzy/` (randomised fuzzing inputs) and `infiniteloop/` (tests that must terminate with a runtime error rather than hang). + ## Project Structure ``` @@ -108,6 +128,48 @@ src/ For a comprehensive technical deep-dive into the implementation, see [ARCHITECTURE.md](ARCHITECTURE.md). +## Embedding API + +Gravity is designed to be embedded inside a host application. The complete API lives in `src/runtime/gravity_vm.h` and `src/compiler/gravity_compiler.h`. A minimal example: + +```c +#include "gravity_compiler.h" +#include "gravity_core.h" +#include "gravity_vm.h" + +static void report_error(gravity_vm *vm, error_type_t type, + const char *description, error_desc_t desc, void *xdata) { + printf("%s\n", description); +} + +int main(void) { + const char *source = "func main() { return 6 * 7; }"; + + gravity_delegate_t delegate = {.error_callback = report_error}; + + // compile + gravity_compiler_t *compiler = gravity_compiler_create(&delegate); + gravity_closure_t *closure = gravity_compiler_run(compiler, source, strlen(source), 0, true, true); + + // create VM and transfer compiler-owned objects into it + gravity_vm *vm = gravity_vm_new(&delegate); + gravity_compiler_transfer(compiler, vm); + gravity_compiler_free(compiler); + + // execute and read result + if (gravity_vm_runmain(vm, closure)) { + gravity_value_t result = gravity_vm_result(vm); + gravity_value_dump(vm, result, NULL, 0); // prints: 42 + } + + gravity_vm_free(vm); + gravity_core_free(); + return 0; +} +``` + +See [`examples/example.c`](examples/example.c) and the [embedding documentation](https://gravity-lang.org) for the full bridging API. + ## Special thanks Gravity was supported by a couple of open-source projects. The inspiration for closures comes from the elegant Lua programming language; specifically from the document Closures in Lua. For fibers, upvalues handling and some parts of the garbage collector, my gratitude goes to Bob Nystrom and his excellent Wren programming language. A very special thanks should also go to my friend **Andrea Donetti** who helped me debugging and testing various aspects of the language. @@ -118,8 +180,15 @@ The Getting Started [![Join the chat at https://gitter.im/gravity-lang/](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/gravity-lang/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) + +[![GitHub Discussions](https://img.shields.io/badge/discussions-GitHub-blue)](https://github.com/marcobambini/gravity/discussions) + +Questions, ideas, and general discussion are welcome in [GitHub Discussions](https://github.com/marcobambini/gravity/discussions). ## Contributing Contributions to Gravity are welcomed and encouraged!
diff --git a/src/compiler/gravity_codegen.c b/src/compiler/gravity_codegen.c index 14835352..54654fe7 100644 --- a/src/compiler/gravity_codegen.c +++ b/src/compiler/gravity_codegen.c @@ -929,11 +929,19 @@ static void process_constructor (gvisitor_t *self, gravity_class_t *c, gnode_t * char name[256]; snprintf(name, sizeof(name), "%s%d", CLASS_INTERNAL_INIT_NAME, ninit++); - // add new internal init to class and call it from main $init function - // super_init should not be duplicated here because class hash table values are not freed (only keys are freed) + // keep the class binding for serialisation compatibility gravity_class_bind(c, name, VALUE_FROM_OBJECT(super_init)); - uint16_t index = gravity_function_cpool_add(NULL, internal_init_function, VALUE_FROM_CSTRING(GET_VM(), name)); - ircode_patch_init((ircode_t *)internal_init_function->bytecode, index); + + // store the closure directly in the constant pool so the call does not + // need a runtime name lookup via LOAD from self. A dynamic lookup + // lets parent $initN names resolve against the subclass's hash table, + // where they alias to different (deeper) $init functions, causing + // infinite recursion when a subclass is instantiated. + // gravity_closure_new already registers the closure with GET_VM(); pass + // NULL to gravity_function_cpool_add to avoid registering it a second time + gravity_closure_t *super_closure = gravity_closure_new(GET_VM(), super_init); + uint16_t index = gravity_function_cpool_add(NULL, internal_init_function, VALUE_FROM_OBJECT(super_closure)); + ircode_patch_init_direct((ircode_t *)internal_init_function->bytecode, index); } super = super->superclass; } diff --git a/src/compiler/gravity_ircode.c b/src/compiler/gravity_ircode.c index 2aa9621e..6157a38b 100644 --- a/src/compiler/gravity_ircode.c +++ b/src/compiler/gravity_ircode.c @@ -205,6 +205,57 @@ void ircode_patch_init (ircode_t *code, uint16_t index) { code->list = list; } +void ircode_patch_init_direct (ircode_t *code, uint16_t index) { + // prepend call instructions to code — like ircode_patch_init but loads the + // callable directly from the constant pool instead of doing a dynamic name + // lookup via LOAD from self. This avoids the name-aliasing bug where a + // parent's $init resolved $initN against the subclass's hash table at + // runtime and ended up calling itself recursively. + // + // LOADK temp index (load closure stored at cpool[index] directly) + // MOVE temp+1 0 (self as first argument) + // CALL temp temp 1 + + // load constant (the closure itself, no LOAD-from-self step) + uint32_t dest = ircode_register_push_temp(code); + inst_t *inst1 = inst_new(LOADK, dest, index, 0, NO_TAG, 0, 0.0, 0); + + // prepare parameter (self) + uint32_t dest2 = ircode_register_push_temp(code); + inst_t *inst2 = inst_new(MOVE, dest2, 0, 0, NO_TAG, 0, 0.0, 0); + ircode_register_pop(code); + + // execute call + inst_t *inst3 = inst_new(CALL, dest, dest, 1, NO_TAG, 0, 0.0, 0); + + // pop temps used + ircode_register_pop(code); + + // create new instruction list + code_r *list = mem_alloc(NULL, sizeof(code_r)); + marray_init(*list); + + // add newly created instructions + marray_push(inst_t*, *list, inst1); + marray_push(inst_t*, *list, inst2); + marray_push(inst_t*, *list, inst3); + + // then copy original instructions + code_r *orig_list = code->list; + uint32_t count = ircode_count(code); + for (uint32_t i=0; ilist); + + // replace dest list with the newly created list + code->list = list; +} + uint8_t opcode_numop (opcode_t op) { switch (op) { case HALT: return 0; diff --git a/src/compiler/gravity_ircode.h b/src/compiler/gravity_ircode.h index cb490dd5..83b59527 100644 --- a/src/compiler/gravity_ircode.h +++ b/src/compiler/gravity_ircode.h @@ -63,6 +63,7 @@ inst_t *ircode_get (ircode_t *code, uint32_t index); bool ircode_iserror (ircode_t *code); uint32_t ircode_ntemps (ircode_t *code); void ircode_patch_init (ircode_t *code, uint16_t index); +void ircode_patch_init_direct (ircode_t *code, uint16_t index); void ircode_pop_context (ircode_t *code); void ircode_push_context (ircode_t *code); diff --git a/src/runtime/gravity_vm.c b/src/runtime/gravity_vm.c index 617354ca..04887864 100644 --- a/src/runtime/gravity_vm.c +++ b/src/runtime/gravity_vm.c @@ -46,6 +46,9 @@ struct gravity_vm { // recursion gravity_int_t maxrecursion; // maximum recursive depth gravity_int_t recursioncount; // recursion counter + + // stack limit + uint32_t maxstacksize; // maximum fiber stack size (in slots) to prevent OOM from unbounded recursion // anonymous names uint32_t nanon; // counter for anonymous classes (used in object_bind) @@ -227,8 +230,7 @@ static gravity_callframe_t *gravity_new_callframe (gravity_vm *vm, gravity_fiber uint32_t new_size = fiber->framesalloc * 2; void *ptr = mem_realloc(NULL, fiber->frames, sizeof(gravity_callframe_t) * new_size); if (!ptr) { - // frames reallocation failed means that there is a very high probability to be into an infinite loop - report_runtime_error(vm, GRAVITY_ERROR_RUNTIME, "Infinite loop detected. Current execution must be aborted."); + report_runtime_error(vm, GRAVITY_ERROR_RUNTIME, "Out of memory: call frame stack could not be grown."); return NULL; } fiber->frames = (gravity_callframe_t *)ptr; @@ -244,12 +246,11 @@ static gravity_callframe_t *gravity_new_callframe (gravity_vm *vm, gravity_fiber } static bool gravity_check_stack (gravity_vm *vm, gravity_fiber_t *fiber, uint32_t stacktopdelta, gravity_value_t **stackstart) { - #pragma unused(vm) if (stacktopdelta == 0) return true; - + // update stacktop pointer before a call fiber->stacktop += stacktopdelta; - + // check stack size uint32_t stack_size = (uint32_t)(fiber->stacktop - fiber->stack); uint32_t stack_needed = MAXNUM(stack_size, DEFAULT_MINSTACK_SIZE); @@ -259,13 +260,20 @@ static bool gravity_check_stack (gravity_vm *vm, gravity_fiber_t *fiber, uint32_ // perform stack reallocation (power_of2_ceil returns 0 if argument is bigger than 2^31) uint32_t new_size = power_of2_ceil(fiber->stackalloc + stack_needed); bool size_condition = (new_size && (uint64_t)new_size >= (uint64_t)(fiber->stackalloc + stack_needed) && ((sizeof(gravity_value_t) * new_size) < SIZE_MAX)); - void *ptr = (size_condition) ? mem_realloc(NULL, fiber->stack, sizeof(gravity_value_t) * new_size) : NULL; + + // enforce the configurable stack size limit to prevent unbounded growth (e.g. infinite recursion) + if (!size_condition || new_size > vm->maxstacksize) { + fiber->stacktop -= stacktopdelta; + report_runtime_error(vm, GRAVITY_ERROR_RUNTIME, "Out of memory: fiber stack exceeded the maximum allowed size (%u slots).", vm->maxstacksize); + return false; + } + + void *ptr = mem_realloc(NULL, fiber->stack, sizeof(gravity_value_t) * new_size); if (!ptr) { // restore stacktop to previous state fiber->stacktop -= stacktopdelta; - // stack reallocation failed means that there is a very high probability to be into an infinite loop - // so return false and let the calling function (vm_exec) raise a runtime error + report_runtime_error(vm, GRAVITY_ERROR_RUNTIME, "Out of memory: fiber stack reallocation failed."); return false; } @@ -1188,7 +1196,7 @@ static bool gravity_vm_exec (gravity_vm *vm) { uint32_t _rneed = FN_COUNTREG(closure->f, r3); uint32_t stacktopdelta = (uint32_t)MAXNUM(stackstart + rwin + _rneed - fiber->stacktop, 0); if (!gravity_check_stack(vm, fiber, stacktopdelta, &stackstart)) { - RUNTIME_ERROR("Infinite loop detected. Current execution must be aborted."); + RUNTIME_ERROR("Out of memory: fiber stack could not be grown."); } // if less arguments are passed then fill the holes with UNDEFINED values @@ -1524,6 +1532,7 @@ gravity_vm *gravity_vm_new (gravity_delegate_t *delegate) { vm->fiber = gravity_fiber_new(vm, NULL, 0, 0); vm->maxccalls = MAX_CCALLS; vm->maxrecursion = 0; // default is no limit + vm->maxstacksize = DEFAULT_MAXSTACK_SIZE; vm->pc = 0; vm->delegate = (delegate) ? delegate : &empty_delegate; @@ -1910,6 +1919,7 @@ gravity_value_t gravity_vm_get (gravity_vm *vm, const char *key) { if (strcmp(key, GRAVITY_VM_MAXCALLS) == 0) return VALUE_FROM_INT(vm->maxccalls); if (strcmp(key, GRAVITY_VM_MAXBLOCK) == 0) return VALUE_FROM_INT(vm->maxmemblock); if (strcmp(key, GRAVITY_VM_MAXRECURSION) == 0) return VALUE_FROM_INT(vm->maxrecursion); + if (strcmp(key, GRAVITY_VM_MAXSTACK) == 0) return VALUE_FROM_INT(vm->maxstacksize); } return VALUE_FROM_NULL; } @@ -1923,6 +1933,7 @@ bool gravity_vm_set (gravity_vm *vm, const char *key, gravity_value_t value) { if ((strcmp(key, GRAVITY_VM_MAXCALLS) == 0) && VALUE_ISA_INT(value)) {vm->maxccalls = (uint32_t)VALUE_AS_INT(value); return true;} if ((strcmp(key, GRAVITY_VM_MAXBLOCK) == 0) && VALUE_ISA_INT(value)) {vm->maxmemblock = (uint32_t)VALUE_AS_INT(value); return true;} if ((strcmp(key, GRAVITY_VM_MAXRECURSION) == 0) && VALUE_ISA_INT(value)) {vm->maxrecursion = (uint32_t)VALUE_AS_INT(value); return true;} + if ((strcmp(key, GRAVITY_VM_MAXSTACK) == 0) && VALUE_ISA_INT(value)) {vm->maxstacksize = (uint32_t)VALUE_AS_INT(value); return true;} } return false; } diff --git a/src/runtime/gravity_vm.h b/src/runtime/gravity_vm.h index a77a2fcb..9dcb98b2 100644 --- a/src/runtime/gravity_vm.h +++ b/src/runtime/gravity_vm.h @@ -23,6 +23,7 @@ extern "C" { #define GRAVITY_VM_MAXCALLS "maxCCalls" #define GRAVITY_VM_MAXBLOCK "maxBlock" #define GRAVITY_VM_MAXRECURSION "maxRecursionDepth" +#define GRAVITY_VM_MAXSTACK "maxStack" typedef void (*vm_cleanup_cb) (gravity_vm *vm); typedef bool (*vm_filter_cb) (gravity_object_t *obj); diff --git a/src/runtime/gravity_vmmacros.h b/src/runtime/gravity_vmmacros.h index a1dadeda..81ee8890 100644 --- a/src/runtime/gravity_vmmacros.h +++ b/src/runtime/gravity_vmmacros.h @@ -242,7 +242,7 @@ uint32_t _w = FN_COUNTREG(func, frame->nargs); \ uint32_t _rneed = FN_COUNTREG(_c->f, _N); \ uint32_t stacktopdelta = (uint32_t)MAXNUM(stackstart + _w + _rneed - fiber->stacktop, 0); \ - if (!gravity_check_stack(vm, fiber, stacktopdelta, &stackstart)) return false; \ + if (!gravity_check_stack(vm, fiber, stacktopdelta, &stackstart)) RUNTIME_ERROR("Out of memory: fiber stack could not be grown."); \ if (vm->aborted) return false #define PREPARE_FUNC_CALL1(_c,_v1,_i,_w) PREPARE_FUNC_CALLN(_c,_i,_w,1); \ diff --git a/src/shared/gravity_value.c b/src/shared/gravity_value.c index 8e373e03..3a859ba1 100644 --- a/src/shared/gravity_value.c +++ b/src/shared/gravity_value.c @@ -1423,6 +1423,24 @@ void gravity_fiber_reassign (gravity_fiber_t *fiber, gravity_closure_t *closure, // update stacktop in order to be GC friendly fiber->stacktop += FN_COUNTREG(closure->f, nargs); + + // ensure the stack is large enough for the new frame's register window; + // gravity_check_stack is only called on CALL instructions so it would not + // catch an overflow that occurs while executing the frame itself + uint32_t stack_used = (uint32_t)(fiber->stacktop - fiber->stack); + if (stack_used > fiber->stackalloc) { + uint32_t new_size = power_of2_ceil(stack_used); + if (!new_size || new_size < stack_used) new_size = stack_used; + gravity_value_t *new_stack = (gravity_value_t *)mem_realloc(NULL, fiber->stack, sizeof(gravity_value_t) * new_size); + if (new_stack) { + ptrdiff_t offset = new_stack - fiber->stack; + fiber->stack = new_stack; + fiber->stackalloc = new_size; + // adjust all pointers that referenced the old stack address + fiber->stacktop += offset; + frame->stackstart += offset; // frame 0 stackstart is always at the base + } + } } void gravity_fiber_reset (gravity_fiber_t *fiber) { diff --git a/src/shared/gravity_value.h b/src/shared/gravity_value.h index f357345f..7903cb6e 100644 --- a/src/shared/gravity_value.h +++ b/src/shared/gravity_value.h @@ -66,8 +66,8 @@ extern "C" { #endif -#define GRAVITY_VERSION "0.9.5" // git tag 0.9.5 -#define GRAVITY_VERSION_NUMBER 0x000905 // git push --tags +#define GRAVITY_VERSION "0.9.6" // git tag 0.9.6 +#define GRAVITY_VERSION_NUMBER 0x000906 // git push --tags #define GRAVITY_BUILD_DATE __DATE__ #ifndef GRAVITY_ENABLE_DOUBLE @@ -132,6 +132,7 @@ extern "C" { #define DEFAULT_CONTEXT_SIZE 256 // default VM context entries (can grow) #define DEFAULT_MINSTRING_SIZE 32 // minimum string allocation size #define DEFAULT_MINSTACK_SIZE 256 // sizeof(gravity_value_t) * 256 = 16 * 256 => 4 KB +#define DEFAULT_MAXSTACK_SIZE 1048576 // sizeof(gravity_value_t) * 1048576 = 16 * 1048576 => 16 MB #define DEFAULT_MINCFRAME_SIZE 32 // sizeof(gravity_callframe_t) * 48 = 32 * 48 => 1.5 KB #define DEFAULT_CG_THRESHOLD (5*1024*1024) // 5MB #define DEFAULT_CG_MINTHRESHOLD (1024*1024) // 1MB diff --git a/test/disabled/README.txt b/test/disabled/README.txt deleted file mode 100644 index 37af91fd..00000000 --- a/test/disabled/README.txt +++ /dev/null @@ -1,3 +0,0 @@ -- loop1.gravity has been disabled because it is not possible to detect such infinite loop. For more information https://www.quora.com/Is-it-possible-to-detect-and-stop-an-infinite-loop-when-writing-a-program - -- heap.gravity needs further investigation but I suspect it cannot be detected too \ No newline at end of file diff --git a/test/unittest/bugfix_stack_overflow_large_regwin.gravity b/test/unittest/bugfix_stack_overflow_large_regwin.gravity new file mode 100644 index 00000000..8ca30d96 --- /dev/null +++ b/test/unittest/bugfix_stack_overflow_large_regwin.gravity @@ -0,0 +1,25 @@ +#unittest { + name: "Fiber stack grows correctly when $moduleinit register window exceeds stackalloc (issue #437)."; + result: true; +}; + +// Regression test for issue #437: heap-buffer-overflow in gravity_vm_exec. +// +// The many empty-string literals below force $moduleinit's ntemp to ~255 because +// the compiler allocates a fresh temp register for each one without reuse, giving +// FN_COUNTREG($moduleinit, 0) = 255. gravity_fiber_reassign() then bumps fiber->stacktop +// by 255 (for $moduleinit) and later by ~6 (for main), totalling 261 > DEFAULT_MINSTACK_SIZE +// (256). Before the fix the buffer was never grown, so recursive calls advanced the frame's +// stackstart until writes landed past fiber->stack[255], corrupting the heap. +// The fix makes gravity_fiber_reassign() realloc the stack whenever stack_used > stackalloc. + +func countdown(n) { + if (n <= 0) return true; + return countdown(n - 1); +} + +func main() { + return countdown(100); +} + +'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' diff --git a/test/disabled/heap.gravity b/test/unittest/heap.gravity similarity index 95% rename from test/disabled/heap.gravity rename to test/unittest/heap.gravity index c460410a..87de509c 100644 --- a/test/disabled/heap.gravity +++ b/test/unittest/heap.gravity @@ -12,12 +12,13 @@ class Vector { if (!a) a = 0; if (!b) b = 0; if (!c) c = 0; - x = a; y = b; z =self } + x = a; y = b; z = c; + } public func + (v) { if (v is Int) {return Vector(x+v, y+v, z+v); } else if (v is Vector) { - return String(x+v.x, y+v.y, z+v.z); + return Vector(x+v.x, y+v.y, z+v.z); } return null; } diff --git a/test/disabled/loop1.gravity b/test/unittest/loop1.gravity similarity index 100% rename from test/disabled/loop1.gravity rename to test/unittest/loop1.gravity From 93930acf9a79576c99fc8bb4d3ddbc1cd79622f3 Mon Sep 17 00:00:00 2001 From: Marco Bambini Date: Tue, 14 Apr 2026 14:04:54 +0200 Subject: [PATCH 10/37] Mark 0.9.6 as released in CHANGELOG --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4021ee84..5909c19d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ All notable changes to Gravity are documented in this file. -## [0.9.6] - Unreleased +## [0.9.6] - 2026-04-14 ### Fixed - **Stack overflow now produces a clean runtime error** instead of a hard crash (SIGSEGV). Infinite recursion and pathological call depths are caught by a configurable stack size limit before the process runs out of memory. From 914793a29db79e0ac9688ae863cf37b94bcbad38 Mon Sep 17 00:00:00 2001 From: Marco Bambini Date: Tue, 14 Apr 2026 14:57:22 +0200 Subject: [PATCH 11/37] Fix float precision loss in JSON serialization and cpool deduplication (issue #420) Two root causes behind 'RUNTIME ERROR: Unknown LOADK index' on -c/-x path: 1. gravity_json.c: float constants were serialized with '%f' (6 decimal places), silently rounding small values like -0.000000004 to -0.000000. When two distinct floats rounded to the same string, one was dropped from the JSON pool, leaving the bytecode referencing a non-existent index. Fixed by switching to '%.17g' (17 significant digits, full IEEE 754 double round-trip), with a '.0' suffix appended for whole-number values so they deserialize as float rather than integer. 2. gravity_value.c: gravity_function_cpool_add used the epsilon-based gravity_value_equals (EPSILON=1e-6) to detect duplicate constants. Any two floats differing by less than 1e-6 were merged into one cpool entry, causing index mismatches at runtime. The cpool now uses exact bit-level comparison (v.f != v2.f) for float values before falling back to the fuzzy equality check used for all other types. Also fix run_all.sh to work on macOS where GNU 'timeout' is not available: detect 'timeout', 'gtimeout' (brew coreutils), or fall back to a pure-bash background kill-watcher that returns exit code 124 on expiry. --- src/shared/gravity_value.c | 8 ++++++++ src/utils/gravity_json.c | 14 ++++++++++++-- test/unittest/run_all.sh | 28 +++++++++++++++++++++++++++- 3 files changed, 47 insertions(+), 3 deletions(-) diff --git a/src/shared/gravity_value.c b/src/shared/gravity_value.c index 3a859ba1..7bf3c4dd 100644 --- a/src/shared/gravity_value.c +++ b/src/shared/gravity_value.c @@ -593,6 +593,14 @@ uint16_t gravity_function_cpool_add (gravity_vm *vm, gravity_function_t *f, grav size_t n = marray_size(f->cpool); for (size_t i=0; icpool, i); + // Float constants must match exactly at the bit level so that distinct + // small values (e.g. -4e-9 vs -5e-11) are never merged due to the + // epsilon-based gravity_value_equals comparison. + if (v.isa == gravity_class_float && v2.isa == gravity_class_float) { + if (v.f != v2.f) continue; + gravity_value_free(NULL, v); + return (uint16_t)i; + } if (gravity_value_equals(v,v2)) { gravity_value_free(NULL, v); return (uint16_t)i; diff --git a/src/utils/gravity_json.c b/src/utils/gravity_json.c index 30dd47c2..e494de8a 100755 --- a/src/utils/gravity_json.c +++ b/src/utils/gravity_json.c @@ -299,8 +299,18 @@ void json_add_double (json_t *json, const char *key, double value) { json_check_comma(json); char buffer[512]; - // was %g but we don't like scientific notation nor the missing .0 in case of float number with no decimals - size_t len = snprintf(buffer, sizeof(buffer), "%f", value); + // Use %.17g for a lossless double round-trip (17 significant digits covers + // the full IEEE 754 range without precision loss that occurred with "%f"). + // If the result contains no decimal point or exponent, append ".0" so the + // value is deserialized as a float rather than an integer. + size_t len = snprintf(buffer, sizeof(buffer), "%.17g", value); + bool has_dot_or_exp = false; + for (size_t i = 0; i < len; i++) { + if (buffer[i] == '.' || buffer[i] == 'e' || buffer[i] == 'E') { has_dot_or_exp = true; break; } + } + if (!has_dot_or_exp && len + 2 < sizeof(buffer)) { + buffer[len++] = '.'; buffer[len++] = '0'; buffer[len] = '\0'; + } if (key) { json_write_raw (json, key, strlen(key), true, true); diff --git a/test/unittest/run_all.sh b/test/unittest/run_all.sh index 9156b7a9..7f3ffbe5 100755 --- a/test/unittest/run_all.sh +++ b/test/unittest/run_all.sh @@ -10,6 +10,32 @@ set -u -o pipefail readonly SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" readonly GRAVITY_BIN=$SCRIPT_DIR/../../gravity + +# Resolve a portable timeout implementation. +# Preference order: timeout (Linux/GNU coreutils) → gtimeout (macOS + brew +# install coreutils) → pure-bash fallback using a background kill watcher. +if command -v timeout &>/dev/null; then + run_timeout() { timeout "$@"; } +elif command -v gtimeout &>/dev/null; then + run_timeout() { gtimeout "$@"; } +else + run_timeout() { + local t=$1; shift + "$@" & + local pid=$! + ( sleep "$t" && kill "$pid" 2>/dev/null ) & + local watcher=$! + wait "$pid" 2>/dev/null + local rc=$? + kill "$watcher" 2>/dev/null + wait "$watcher" 2>/dev/null + # If the process was killed by our watcher (SIGTERM = 143) mimic the + # standard timeout exit code of 124 so the caller can detect timeouts. + [[ $rc -eq 143 ]] && return 124 + return $rc + } +fi + files=$(find $SCRIPT_DIR -iname "*.gravity" | grep -v disabled) tests_total=$(echo "$files" | wc -l) tests_success=0 @@ -24,7 +50,7 @@ for test in $files; do if [[ "$test" =~ "mem" || "$test" =~ "recursion" ]]; then timeout=10 fi - timeout $timeout "$GRAVITY_BIN" "$test" + run_timeout $timeout "$GRAVITY_BIN" "$test" res=$? if [[ $res -eq 0 ]]; then tests_success=$(($tests_success+1)) From 26720f5957ff9db8f42e9c0741296e81fb4d2cda Mon Sep 17 00:00:00 2001 From: Marco Bambini Date: Tue, 14 Apr 2026 14:58:59 +0200 Subject: [PATCH 12/37] Fix gravity_optionals.h always defining all optional module guards (issue #426) The four #ifndef GRAVITY_INCLUDE_* / #define ... / #endif blocks unconditionally defined every optional-module guard the moment the header was included, making the guards useless for embedders who want to exclude specific modules. Fix: remove the unconditional defines from gravity_optionals.h so the guard macros must be explicitly set by the includer. The three Gravity source files that want all optionals enabled (gravity_vm.c, gravity.c, gravity_parser.c) now define all four GRAVITY_INCLUDE_* macros before including gravity_optionals.h. Embedders can now selectively include only the modules they need. --- src/cli/gravity.c | 4 ++++ src/compiler/gravity_parser.c | 4 ++++ src/optionals/gravity_optionals.h | 16 ---------------- src/runtime/gravity_vm.c | 4 ++++ 4 files changed, 12 insertions(+), 16 deletions(-) diff --git a/src/cli/gravity.c b/src/cli/gravity.c index 3b0e91ae..f928c0f6 100644 --- a/src/cli/gravity.c +++ b/src/cli/gravity.c @@ -7,6 +7,10 @@ // #include "gravity_compiler.h" +#define GRAVITY_INCLUDE_MATH +#define GRAVITY_INCLUDE_JSON +#define GRAVITY_INCLUDE_ENV +#define GRAVITY_INCLUDE_FILE #include "gravity_optionals.h" #include "gravity_utils.h" #include "gravity_core.h" diff --git a/src/compiler/gravity_parser.c b/src/compiler/gravity_parser.c index 0c3a1c53..29a82c2d 100644 --- a/src/compiler/gravity_parser.c +++ b/src/compiler/gravity_parser.c @@ -7,6 +7,10 @@ // #include "gravity_symboltable.h" +#define GRAVITY_INCLUDE_MATH +#define GRAVITY_INCLUDE_JSON +#define GRAVITY_INCLUDE_ENV +#define GRAVITY_INCLUDE_FILE #include "gravity_optionals.h" #include "gravity_parser.h" #include "gravity_macros.h" diff --git a/src/optionals/gravity_optionals.h b/src/optionals/gravity_optionals.h index 00226294..8bdd78cf 100644 --- a/src/optionals/gravity_optionals.h +++ b/src/optionals/gravity_optionals.h @@ -9,10 +9,6 @@ #ifndef __GRAVITY_OPTIONALS__ #define __GRAVITY_OPTIONALS__ -#ifndef GRAVITY_INCLUDE_MATH -#define GRAVITY_INCLUDE_MATH -#endif - #ifdef GRAVITY_INCLUDE_MATH #define GRAVITY_MATH_REGISTER(_vm) gravity_math_register(_vm) #define GRAVITY_MATH_FREE() gravity_math_free() @@ -26,10 +22,6 @@ #define GRAVITY_ISMATH_CLASS(_c) false #endif -#ifndef GRAVITY_INCLUDE_JSON -#define GRAVITY_INCLUDE_JSON -#endif - #ifdef GRAVITY_INCLUDE_JSON #define GRAVITY_JSON_REGISTER(_vm) gravity_json_register(_vm) #define GRAVITY_JSON_FREE() gravity_json_free() @@ -43,10 +35,6 @@ #define GRAVITY_ISJSON_CLASS(_c) false #endif -#ifndef GRAVITY_INCLUDE_ENV -#define GRAVITY_INCLUDE_ENV -#endif - #ifdef GRAVITY_INCLUDE_ENV #define GRAVITY_ENV_REGISTER(_vm) gravity_env_register(_vm) #define GRAVITY_ENV_FREE() gravity_env_free() @@ -60,10 +48,6 @@ #define GRAVITY_ISENV_CLASS(_c) false #endif -#ifndef GRAVITY_INCLUDE_FILE -#define GRAVITY_INCLUDE_FILE -#endif - #ifdef GRAVITY_INCLUDE_FILE #define GRAVITY_FILE_REGISTER(_vm) gravity_file_register(_vm) #define GRAVITY_FILE_FREE() gravity_file_free() diff --git a/src/runtime/gravity_vm.c b/src/runtime/gravity_vm.c index 04887864..e90b0d20 100644 --- a/src/runtime/gravity_vm.c +++ b/src/runtime/gravity_vm.c @@ -16,6 +16,10 @@ #include "gravity_opcodes.h" #include "gravity_memory.h" #include "gravity_vmmacros.h" +#define GRAVITY_INCLUDE_MATH +#define GRAVITY_INCLUDE_JSON +#define GRAVITY_INCLUDE_ENV +#define GRAVITY_INCLUDE_FILE #include "gravity_optionals.h" // MARK: Internals - From f75af899811d68b1adf182a3233fd73310000a81 Mon Sep 17 00:00:00 2001 From: Marco Bambini Date: Tue, 14 Apr 2026 15:00:51 +0200 Subject: [PATCH 13/37] Fix Makefile dependency errors (issue #413) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four problems corrected: 1. 'gravity' and 'example' were declared .PHONY despite producing real files, causing make to unconditionally rebuild them on every invocation regardless of whether any source had changed. Removed both from .PHONY. Only 'all', 'clean', and 'lib' are genuinely phony targets. 2. 'lib: gravity' was a wrong dependency — building the shared library only requires the compiled object files, not the gravity CLI executable. Changed to 'lib: $(OBJ)'. 3. GRAVITY_SRC (src/cli/gravity.c) and EXAMPLE_SRC (examples/example.c) were passed directly to the linker step rather than compiled separately, so -MMD never generated .d files for them. Header changes in those files would not trigger rebuilds. Both are now compiled to .o first (GRAVITY_OBJ / EXAMPLE_OBJ) so make tracks their header dependencies correctly via the generated .d files. 4. 'make clean' removed libgravity.so and gravity.dll but missed libgravity.dylib (macOS). Added it to the clean target along with the new GRAVITY_OBJ and EXAMPLE_OBJ intermediates. --- Makefile | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/Makefile b/Makefile index 20298e62..04e19e0d 100644 --- a/Makefile +++ b/Makefile @@ -16,7 +16,9 @@ SRC = $(wildcard $(COMPILER_DIR)*.c) \ INCLUDE = -I$(COMPILER_DIR) -I$(RUNTIME_DIR) -I$(SHARED_DIR) -I$(UTILS_DIR) -I$(OPT_DIR) CFLAGS = $(INCLUDE) -std=gnu99 -fgnu89-inline -fPIC -DBUILD_GRAVITY_API -MMD OBJ = $(SRC:.c=.o) -DEP = $(OBJ:.o=.d) +GRAVITY_OBJ = $(GRAVITY_SRC:.c=.o) +EXAMPLE_OBJ = $(EXAMPLE_SRC:.c=.o) +DEP = $(OBJ:.o=.d) $(GRAVITY_OBJ:.o=.d) $(EXAMPLE_OBJ:.o=.d) ifeq ($(OS),Windows_NT) # Windows @@ -59,18 +61,18 @@ endif all: gravity -gravity: $(OBJ) $(GRAVITY_SRC) +gravity: $(OBJ) $(GRAVITY_OBJ) $(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS) - -example: $(OBJ) $(EXAMPLE_SRC) + +example: $(OBJ) $(EXAMPLE_OBJ) $(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS) -lib: gravity +lib: $(OBJ) $(CC) -shared -o $(LIBTARGET) $(OBJ) $(LDFLAGS) clean: - rm -f $(OBJ) $(DEP) gravity example libgravity.so gravity.dll + rm -f $(OBJ) $(GRAVITY_OBJ) $(EXAMPLE_OBJ) $(DEP) gravity example libgravity.dylib libgravity.so gravity.dll -.PHONY: all clean gravity example +.PHONY: all clean lib -include $(DEP) From c66b6902684c75f948c8ee4f8c0c6489362583ee Mon Sep 17 00:00:00 2001 From: Marco Bambini Date: Tue, 14 Apr 2026 15:04:01 +0200 Subject: [PATCH 14/37] =?UTF-8?q?Bump=20version=20to=200.9.7=20=E2=80=94?= =?UTF-8?q?=20bug=20fixes=20for=20float=20precision,=20optional=20modules,?= =?UTF-8?q?=20and=20build=20system?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 14 ++++++++++++++ src/shared/gravity_value.h | 4 ++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5909c19d..a18dba80 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,20 @@ All notable changes to Gravity are documented in this file. +## [0.9.7] - 2026-04-14 + +### Fixed +- **Float precision loss in JSON bytecode serialization** — float constants were written with `%f` (6 decimal places), silently rounding small values like `-0.000000004` to zero and causing `RUNTIME ERROR: Unknown LOADK index` on the `-c`/`-x` (compile + execute bytecode) path. Switched to `%.17g` for full IEEE 754 double round-trip precision (issue #420). +- **Float constant deduplication in cpool** — `gravity_function_cpool_add` used the epsilon-based `gravity_value_equals` (EPSILON = 1e-6) to detect duplicate constants, incorrectly merging distinct small floats into a single pool entry. The cpool now uses exact bit-level comparison for float values (issue #420). +- **`gravity_optionals.h` unconditionally defined all optional-module guards** — the `#ifndef GRAVITY_INCLUDE_*` blocks always defined every guard, making it impossible to exclude modules at compile time. The guards are now left undefined by default; embedders define only the modules they need. The Gravity CLI and runtime define all four (issue #426). +- **Makefile dependency errors** — four issues: `gravity` and `example` were incorrectly listed as `.PHONY` targets (causing unconditional rebuilds); `lib` depended on the `gravity` executable instead of just `$(OBJ)`; `gravity.c` and `example.c` were compiled only during the link step so `-MMD` never generated `.d` header-dependency files for them; `make clean` did not remove `libgravity.dylib` on macOS (issue #413). +- **`run_all.sh` portability** — the test runner used GNU `timeout` which is not available on macOS. The script now auto-detects `timeout`, `gtimeout` (Homebrew coreutils), or falls back to a pure-bash kill-watcher. + +### Changed +- Version bumped to **0.9.7** (`GRAVITY_VERSION`, `GRAVITY_VERSION_NUMBER`). + +--- + ## [0.9.6] - 2026-04-14 ### Fixed diff --git a/src/shared/gravity_value.h b/src/shared/gravity_value.h index 7903cb6e..8c54dddc 100644 --- a/src/shared/gravity_value.h +++ b/src/shared/gravity_value.h @@ -66,8 +66,8 @@ extern "C" { #endif -#define GRAVITY_VERSION "0.9.6" // git tag 0.9.6 -#define GRAVITY_VERSION_NUMBER 0x000906 // git push --tags +#define GRAVITY_VERSION "0.9.7" // git tag 0.9.7 +#define GRAVITY_VERSION_NUMBER 0x000907 // git push --tags #define GRAVITY_BUILD_DATE __DATE__ #ifndef GRAVITY_ENABLE_DOUBLE From aa18dbc89ac48f8ad8935797545ccb2d70818e0c Mon Sep 17 00:00:00 2001 From: Martin Miralles-Cordal Date: Mon, 25 May 2026 13:18:13 -0400 Subject: [PATCH 15/37] Change install instructions to be more readily packagable. - Install now copies headers. - Build considers `BUILD_SHARED_LIBS` when deciding to build `gravityapi` target. - Install now works when CLI is disabled. - Local in-source install on Windows is now a CMake option. - Install paths propagated to parent scope so CLI install can see them. --- CMakeLists.txt | 4 ++- src/CMakeLists.txt | 59 ++++++++++++++++++++++++++++++++++++------ src/cli/CMakeLists.txt | 6 +++-- 3 files changed, 58 insertions(+), 11 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index e4c71724..afd1be07 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,9 +1,11 @@ -cmake_minimum_required(VERSION 3.0) +cmake_minimum_required(VERSION 3.10) project(gravity VERSION 1.0 LANGUAGES C) set(CMAKE_C_STANDARD 99) option(BUILD_CLI "Build the command line interface" ON) +option(BUILD_SHARED_LIBS "Build shared libraries" ON) +option(WINDOWS_LOCAL_INSTALL "CMake install installs local to this checkout" ON) # ---------------------------------------------------------------- # Library diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 87afba5f..8ab04e61 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,5 +1,7 @@ # Library +include(GNUInstallDirs) + SET(COMPILER_DIR compiler/) SET(RUNTIME_DIR runtime/) SET(SHARED_DIR shared/) @@ -20,9 +22,10 @@ set(GRAVITY_DEPENDENT_LIBS "") set(GRAVITY_PRIVATE_DEFINITIONS "") set(GRAVITY_PRIVATE_COMPILE_OPTIONS "") -set(GRAVITY_INSTALL_RUNTIME_PATH "/usr/local/bin") # Gravity executable install path -set(GRAVITY_INSTALL_LIB_PATH "lib") # Gravity shared library install path -set(GRAVITY_INSTALL_LIB_STATIC_PATH "lib") # Gravity static library install path +set(GRAVITY_INSTALL_RUNTIME_PATH ${CMAKE_INSTALL_BINDIR}) # Gravity executable install path +set(GRAVITY_INSTALL_LIB_PATH ${CMAKE_INSTALL_LIBDIR}) # Gravity shared library install path +set(GRAVITY_INSTALL_LIB_STATIC_PATH ${CMAKE_INSTALL_LIBDIR}) # Gravity static library install path +set(GRAVITY_INSTALL_INCLUDE_PATH ${CMAKE_INSTALL_INCLUDEDIR}) # Gravity headers install path # ---------------------------------------------------------------- if(MSVC) @@ -39,20 +42,26 @@ if(MSVC) # warning C4068: unknown pragma list(APPEND GRAVITY_PRIVATE_COMPILE_OPTIONS "/wd4068") + if (WINDOWS_LOCAL_INSTALL) # make Windows installs local set(GRAVITY_INSTALL_RUNTIME_PATH "${CMAKE_CURRENT_SOURCE_DIR}/bin") set(GRAVITY_INSTALL_LIB_PATH "${CMAKE_CURRENT_SOURCE_DIR}/bin/lib") set(GRAVITY_INSTALL_LIB_STATIC_PATH "${CMAKE_CURRENT_SOURCE_DIR}/bin/lib") + set(GRAVITY_INSTALL_INCLUDE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/bin/include") + endif() elseif(MINGW) # for path functions list(APPEND GRAVITY_DEPENDENT_LIBS "shlwapi") + if (WINDOWS_LOCAL_INSTALL) # make Windows installs local set(GRAVITY_INSTALL_RUNTIME_PATH "${CMAKE_CURRENT_SOURCE_DIR}/bin") set(GRAVITY_INSTALL_LIB_PATH "${CMAKE_CURRENT_SOURCE_DIR}/bin/lib") set(GRAVITY_INSTALL_LIB_STATIC_PATH "${CMAKE_CURRENT_SOURCE_DIR}/bin/lib") + set(GRAVITY_INSTALL_INCLUDE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/bin/include") + endif() elseif(${CMAKE_SYSTEM_NAME} MATCHES "Darwin|NetBSD|BSD|DragonFly|Linux") @@ -62,23 +71,57 @@ elseif(${CMAKE_SYSTEM_NAME} MATCHES "Darwin|NetBSD|BSD|DragonFly|Linux") endif() # ---------------------------------------------------------------- -add_library(gravityapi SHARED ${SRC_FILES}) +if (BUILD_SHARED_LIBS) + add_library(gravityapi SHARED ${SRC_FILES}) + target_compile_definitions(gravityapi PUBLIC BUILD_GRAVITY_API) +endif () add_library(gravityapi_s STATIC ${SRC_FILES}) -target_compile_definitions(gravityapi PUBLIC BUILD_GRAVITY_API) - # ---------------------------------------------------------------- -set(GRAVITY_TARGETS gravityapi gravityapi_s) +if (BUILD_SHARED_LIBS) + set(GRAVITY_TARGETS gravityapi gravityapi_s) +else() + set(GRAVITY_TARGETS gravityapi_s) +endif() foreach(target ${GRAVITY_TARGETS}) target_link_libraries(${target} PRIVATE ${GRAVITY_DEPENDENT_LIBS}) target_compile_definitions(${target} PRIVATE ${GRAVITY_PRIVATE_DEFINITIONS}) target_compile_options(${target} PRIVATE ${GRAVITY_PRIVATE_COMPILE_OPTIONS}) - target_include_directories(${target} PUBLIC ${GRAVITY_INCLUDE_DIR}) + target_include_directories(${target} PUBLIC + $ + $ + $ + $ + $ + $) set_target_properties(${target} PROPERTIES ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib" LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib" ) endforeach() + +# copy final install paths to parent scope for use in cli install +set(GRAVITY_INSTALL_RUNTIME_PATH ${GRAVITY_INSTALL_RUNTIME_PATH} PARENT_SCOPE) +set(GRAVITY_INSTALL_LIB_PATH ${GRAVITY_INSTALL_LIB_PATH} PARENT_SCOPE) +set(GRAVITY_INSTALL_LIB_STATIC_PATH ${GRAVITY_INSTALL_LIB_STATIC_PATH} PARENT_SCOPE) +set(GRAVITY_INSTALL_INCLUDE_PATH ${GRAVITY_INSTALL_INCLUDE_PATH} PARENT_SCOPE) + +set(GRAVITYAPI_INSTALL_TARGETS ${GRAVITY_TARGETS}) + +file(GLOB GRAVITY_HEADERS + "${COMPILER_DIR}/*.h" "${RUNTIME_DIR}/*.h" "${SHARED_DIR}/*.h" "${UTILS_DIR}/*.h" "${OPT_DIR}/*.h") +install(FILES ${GRAVITY_HEADERS} + DESTINATION ${GRAVITY_INSTALL_INCLUDE_PATH}) +install(TARGETS ${GRAVITYAPI_INSTALL_TARGETS} + EXPORT gravity-targets + ARCHIVE DESTINATION ${GRAVITY_INSTALL_LIB_STATIC_PATH} + LIBRARY DESTINATION ${GRAVITY_INSTALL_LIB_PATH} + RUNTIME DESTINATION ${GRAVITY_INSTALL_RUNTIME_PATH} + BUNDLE DESTINATION ${GRAVITY_INSTALL_RUNTIME_PATH}) +install(EXPORT gravity-targets + FILE gravity-config.cmake + NAMESPACE gravity:: + DESTINATION ${GRAVITY_INSTALL_LIB_PATH}/cmake/gravity) diff --git a/src/cli/CMakeLists.txt b/src/cli/CMakeLists.txt index 82bc6e0f..581ed012 100644 --- a/src/cli/CMakeLists.txt +++ b/src/cli/CMakeLists.txt @@ -12,7 +12,9 @@ set_target_properties(gravity PROPERTIES ) # Install -install(TARGETS gravity ${GRAVITY_TARGETS} +install(TARGETS gravity + EXPORT gravity-targets RUNTIME DESTINATION ${GRAVITY_INSTALL_RUNTIME_PATH} LIBRARY DESTINATION ${GRAVITY_INSTALL_LIB_PATH} - ARCHIVE DESTINATION ${GRAVITY_INSTALL_LIB_STATIC_PATH}) + ARCHIVE DESTINATION ${GRAVITY_INSTALL_LIB_STATIC_PATH} + BUNDLE DESTINATION ${GRAVITY_INSTALL_RUNTIME_PATH}) From 53bdbf7c83ca0bc5960f0f9107b5c7144032df07 Mon Sep 17 00:00:00 2001 From: Martin Miralles-Cordal Date: Mon, 25 May 2026 13:32:47 -0400 Subject: [PATCH 16/37] Add self to CONTRIBUTORS. --- CONTRIBUTORS | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTORS b/CONTRIBUTORS index bf434e89..ce53c7dc 100644 --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -6,4 +6,5 @@ Saša Barišić Steven Hall Brandon Ray Filippo Costa -Matan Silver \ No newline at end of file +Matan Silver +Martin Miralles-Cordal From eb67d223c45238cd47e116c3ceda1c5900d94c4a Mon Sep 17 00:00:00 2001 From: orbisai0security Date: Tue, 26 May 2026 04:34:30 +0000 Subject: [PATCH 17/37] fix: V-001 security vulnerability Automated security fix generated by OrbisAI Security --- binding/shared/console.c | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/binding/shared/console.c b/binding/shared/console.c index 40e81ee9..5dab942e 100644 --- a/binding/shared/console.c +++ b/binding/shared/console.c @@ -20,9 +20,7 @@ const char *current_filepath (const char *base, const char *target_file) { // __FILE__ macro contains full path to main.c file // for example: /Users/marco/SQLabs/Butterfly/gravity/main/main.c - snprintf(buffer, strlen(base) - skip, "%s", base); - strcat(buffer, "/shared/"); - strcat(buffer, target_file); + snprintf(buffer, sizeof(buffer), "%.*s/shared/%s", (int)(strlen(base) - skip - 1), base, target_file); return buffer; } From 91ab40bdc74ee9e4fac2dab41f5162e225a92452 Mon Sep 17 00:00:00 2001 From: Larry Barchett Date: Sun, 19 Jul 2026 00:07:18 -0400 Subject: [PATCH 18/37] fix: computed properties leaked by double macro expansion in gravity_core_init MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit VALUE_FROM_OBJECT() evaluates its argument twice when GRAVITY_USE_HIDDEN_INITIALIZERS is not set: #define VALUE_FROM_OBJECT(obj) ((gravity_value_t){.isa = ((gravity_object_t *)(obj)->isa), .p = (gravity_object_t *)(obj)}) Six bind sites in gravity_core_init called computed_property_create() inline inside that macro (Object.class, Object.meta, Int.min/max meta, Float.min/max meta), so each of those computed properties was created twice: one copy bound, the duplicate orphaned (~2.2KB leaked per gravity_core_init). Use the same temp-variable pattern the rest of gravity_core_init already uses for every other computed property. Also free the nine computed properties missing from gravity_core_free's manual free list (Object.class, Object.meta, Range.from, Range.to, String.bytes, Int meta min/max, Float meta min/max) — previously leaked on every core init/free cycle. --- src/runtime/gravity_core.c | 34 ++++++++++++++++++++++++++++------ 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/src/runtime/gravity_core.c b/src/runtime/gravity_core.c index e629d95f..9a661d5b 100644 --- a/src/runtime/gravity_core.c +++ b/src/runtime/gravity_core.c @@ -3376,8 +3376,13 @@ void gravity_core_init (void) { gravity_class_bind(gravity_class_object, "clone", NEW_CLOSURE_VALUE(object_clone)); // INTROSPECTION support added to OBJECT CLASS - gravity_class_bind(gravity_class_object, "class", VALUE_FROM_OBJECT(computed_property_create(NULL, NEW_FUNCTION(object_class), NULL))); - gravity_class_bind(gravity_class_object, "meta", VALUE_FROM_OBJECT(computed_property_create(NULL, NEW_FUNCTION(object_meta), NULL))); + // NOTE: VALUE_FROM_OBJECT is a macro that can evaluate its argument twice + // (non GRAVITY_USE_HIDDEN_INITIALIZERS build), so computed_property_create + // must never be called inline inside it (it would create a leaked duplicate) + gravity_closure_t *object_class_closure = computed_property_create(NULL, NEW_FUNCTION(object_class), NULL); + gravity_class_bind(gravity_class_object, "class", VALUE_FROM_OBJECT(object_class_closure)); + gravity_closure_t *object_meta_closure = computed_property_create(NULL, NEW_FUNCTION(object_meta), NULL); + gravity_class_bind(gravity_class_object, "meta", VALUE_FROM_OBJECT(object_meta_closure)); gravity_class_bind(gravity_class_object, "respondTo", NEW_CLOSURE_VALUE(object_respond)); gravity_class_bind(gravity_class_object, "methods", NEW_CLOSURE_VALUE(object_methods)); gravity_class_bind(gravity_class_object, "properties", NEW_CLOSURE_VALUE(object_properties)); @@ -3481,8 +3486,10 @@ void gravity_core_init (void) { gravity_class_t *int_meta = gravity_class_get_meta(gravity_class_int); gravity_class_bind(int_meta, "random", NEW_CLOSURE_VALUE(int_random)); gravity_class_bind(int_meta, GRAVITY_INTERNAL_EXEC_NAME, NEW_CLOSURE_VALUE(int_exec)); - gravity_class_bind(int_meta, "min", VALUE_FROM_OBJECT(computed_property_create(NULL, NEW_FUNCTION(int_min), NULL))); - gravity_class_bind(int_meta, "max", VALUE_FROM_OBJECT(computed_property_create(NULL, NEW_FUNCTION(int_max), NULL))); + gravity_closure_t *int_min_closure = computed_property_create(NULL, NEW_FUNCTION(int_min), NULL); + gravity_class_bind(int_meta, "min", VALUE_FROM_OBJECT(int_min_closure)); + gravity_closure_t *int_max_closure = computed_property_create(NULL, NEW_FUNCTION(int_max), NULL); + gravity_class_bind(int_meta, "max", VALUE_FROM_OBJECT(int_max_closure)); // FLOAT CLASS gravity_class_bind(gravity_class_float, GRAVITY_OPERATOR_ADD_NAME, NEW_CLOSURE_VALUE(operator_float_add)); @@ -3506,8 +3513,10 @@ void gravity_core_init (void) { // Meta gravity_class_t *float_meta = gravity_class_get_meta(gravity_class_float); gravity_class_bind(float_meta, GRAVITY_INTERNAL_EXEC_NAME, NEW_CLOSURE_VALUE(float_exec)); - gravity_class_bind(float_meta, "min", VALUE_FROM_OBJECT(computed_property_create(NULL, NEW_FUNCTION(float_min), NULL))); - gravity_class_bind(float_meta, "max", VALUE_FROM_OBJECT(computed_property_create(NULL, NEW_FUNCTION(float_max), NULL))); + gravity_closure_t *float_min_closure = computed_property_create(NULL, NEW_FUNCTION(float_min), NULL); + gravity_class_bind(float_meta, "min", VALUE_FROM_OBJECT(float_min_closure)); + gravity_closure_t *float_max_closure = computed_property_create(NULL, NEW_FUNCTION(float_max), NULL); + gravity_class_bind(float_meta, "max", VALUE_FROM_OBJECT(float_max_closure)); // BOOL CLASS gravity_class_bind(gravity_class_bool, GRAVITY_OPERATOR_ADD_NAME, NEW_CLOSURE_VALUE(operator_bool_add)); @@ -3658,6 +3667,19 @@ void gravity_core_free (void) { computed_property_free(gravity_class_float, "degrees", true); gravity_class_t *system_meta = gravity_class_get_meta(gravity_class_system); computed_property_free(system_meta, GRAVITY_VM_GCENABLED, true); + // these computed properties are also created in gravity_core_init but were + // missing from this free list (leaked on every core init/free cycle) + computed_property_free(gravity_class_object, "class", true); + computed_property_free(gravity_class_object, "meta", true); + computed_property_free(gravity_class_range, "from", true); + computed_property_free(gravity_class_range, "to", true); + computed_property_free(gravity_class_string, "bytes", true); + gravity_class_t *int_meta_cp = gravity_class_get_meta(gravity_class_int); + computed_property_free(int_meta_cp, "min", true); + computed_property_free(int_meta_cp, "max", true); + gravity_class_t *float_meta_cp = gravity_class_get_meta(gravity_class_float); + computed_property_free(float_meta_cp, "min", true); + computed_property_free(float_meta_cp, "max", true); gravity_class_free_core(NULL, gravity_class_get_meta(gravity_class_int)); gravity_class_free_core(NULL, gravity_class_int); From 380aa4dda2e01f9120c48c0f1ce8fffdf23dbad8 Mon Sep 17 00:00:00 2001 From: Larry Barchett Date: Sun, 19 Jul 2026 00:07:18 -0400 Subject: [PATCH 19/37] fix: gray-list buffer orphaned by marray_init ordering in gravity_vm_new gravity_vm_new calls gravity_gc_setenabled(vm, true) before marray_init(vm->graylist). Enabling the GC can trigger a collection (gravity_gc_check -> gravity_gc_start), which grows the graylist buffer via marray_push/realloc. The marray_init that follows then zeroes the array struct, orphaning that buffer (leaked once per VM). Initialize graylist/gctemp before enabling the GC. --- src/runtime/gravity_vm.c | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/runtime/gravity_vm.c b/src/runtime/gravity_vm.c index e90b0d20..9458bbaa 100644 --- a/src/runtime/gravity_vm.c +++ b/src/runtime/gravity_vm.c @@ -1543,12 +1543,15 @@ gravity_vm *gravity_vm_new (gravity_delegate_t *delegate) { vm->context = gravity_hash_create(DEFAULT_CONTEXT_SIZE, gravity_value_hash, gravity_value_equals, NULL, NULL); // garbage collector + // graylist/gctemp MUST be initialized before gravity_gc_setenabled: enabling the GC + // can trigger a collection that grows the graylist buffer, and a marray_init after + // that would zero the pointer and orphan (leak) the allocation. + marray_init(vm->graylist); + marray_init(vm->gctemp); gravity_gc_setenabled(vm, true); gravity_gc_setvalues(vm, DEFAULT_CG_THRESHOLD, DEFAULT_CG_MINTHRESHOLD, DEFAULT_CG_RATIO); vm->memallocated = 0; vm->maxmemblock = MAX_MEMORY_BLOCK; - marray_init(vm->graylist); - marray_init(vm->gctemp); // init base and core gravity_core_register(vm); From 1b9bbf3ad5749e2a3434e6ad073c6e93c24207b6 Mon Sep 17 00:00:00 2001 From: Marco Bambini Date: Wed, 5 Aug 2026 09:19:48 +0200 Subject: [PATCH 20/37] fix: heap out-of-bounds read in parse_number_expression (#446) parse_number_expression checked for a 0b/0o/0x prefix by reading value[1] whenever value[0] was '0', without first confirming the token is at least 2 bytes long. token.value points directly into the caller's source buffer (lexer->buffer + lexer->offset) and is not separately zero terminated, so a source whose last byte is a lone '0' caused a 1-byte read past the end of the buffer. The CLI is unaffected because file_read over-allocates by one byte and zero terminates, but gravity_compiler_run accepts an explicit length and embedders may legitimately pass an exact-size, non terminated buffer. Confirmed with ASan on a 3-byte malloc holding "x=0": ERROR: AddressSanitizer: heap-buffer-overflow READ of size 1 at 0x6020000000d3 thread T0 #0 parse_number_expression gravity_parser.c:684 Guard the prefix check with token.bytes > 1. Co-Authored-By: Claude Opus 5 --- src/compiler/gravity_parser.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/compiler/gravity_parser.c b/src/compiler/gravity_parser.c index 29a82c2d..2fbc24ac 100644 --- a/src/compiler/gravity_parser.c +++ b/src/compiler/gravity_parser.c @@ -680,7 +680,9 @@ static gnode_t *parse_number_expression (gravity_parser_t *parser, gtoken_s toke int64_t n = 0; double d = 0; - if (value[0] == '0') { + // token.value points directly into the source buffer, which is not guaranteed to be + // zero terminated, so make sure the token is at least 2 bytes before peeking at value[1] + if ((token.bytes > 1) && (value[0] == '0')) { int c = toupper(value[1]); if (c == 'B') {type = decode_number_binary(token, &n); goto report_node;} else if (c == 'O') {type = decode_number_octal(token, &n); goto report_node;} From 97b92c20de5500f3b58e9d93bd3e2ef333f5ca05 Mon Sep 17 00:00:00 2001 From: Marco Bambini Date: Wed, 5 Aug 2026 09:35:56 +0200 Subject: [PATCH 21/37] fix: optional classes never released due to unbalanced refcount gravity_opt_register() runs once per gravity_core_register(), but gravity_opt_free() is reached only on the last gravity_core_free(), when the core refcount hits zero. In the usual embedding flow (a mini VM for the compiler plus a real VM) each optional class therefore ends up with a refcount of 2 while only one decrement ever happens, so Math/ENV/JSON/File and their metaclasses are never freed. They are not owned by any VM garbage collector, so nothing else can reclaim them. Track the number of outstanding registrations and balance them all when the last VM is torn down. gravity_opt_free() still runs only after the refcount check, preserving the ordering introduced by dd81589, and each optional keeps its own refcount and NULL guard so it is still freed exactly once. Verified with macOS leaks on examples/example.c: allocations owned by gravity and still live at exit drop from 347 blocks / 31,698 bytes to zero, and the total live heap goes from 534 nodes / 66 KB to 187 nodes / 31 KB. Running a script through the CLI improves the same way (537 nodes / 67 KB to 190 nodes / 31 KB). Unit tests pass 352/352, also under AddressSanitizer, together with a stress program covering repeated teardown and re-init, two concurrent VMs with a partial teardown, a reused compiler, and an extra unbalanced gravity_core_free(). Co-Authored-By: Claude Opus 5 --- src/runtime/gravity_core.c | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/runtime/gravity_core.c b/src/runtime/gravity_core.c index 9a661d5b..5848b001 100644 --- a/src/runtime/gravity_core.c +++ b/src/runtime/gravity_core.c @@ -68,6 +68,7 @@ static bool core_inited = false; // initialize global classes just once static uint32_t refcount = 0; // protect deallocation of global classes +static uint32_t opt_refcount = 0; // outstanding gravity_opt_register calls (not yet balanced by a gravity_opt_free) // boxed gravity_class_t *gravity_class_int; @@ -3650,7 +3651,15 @@ void gravity_core_free (void) { // free optionals after refcount check — avoids double-free when mini-VM // in gravity_compiler_reset() has already freed GC objects via internal_vm_cleanup - gravity_opt_free(); + // each optional class keeps its own refcount, incremented once per gravity_opt_register + // (so once per gravity_core_register), but this point is reached only on the very last + // teardown: balance every outstanding registration here, otherwise the optional classes + // would survive with a non-zero refcount and never be released (they are not owned by + // any VM garbage collector, so nothing else can free them) + while (opt_refcount) { + gravity_opt_free(); + --opt_refcount; + } // this function should never be called // it is just called when we need to internally check for memory leaks @@ -3755,6 +3764,7 @@ const char **gravity_core_identifiers (void) { void gravity_core_register (gravity_vm *vm) { gravity_core_init(); gravity_opt_register(vm); + ++opt_refcount; ++refcount; if (!vm) return; From 9b337c3eae5833c3956bed1fc01c21c14fd443f2 Mon Sep 17 00:00:00 2001 From: Marco Bambini Date: Wed, 5 Aug 2026 09:44:41 +0200 Subject: [PATCH 22/37] fix: harden the JSON executable loader and scanner (#444, #447, #448) gravity_vm_loadbuffer() trusted the shape of the JSON it was handed, and the scanner underneath it had several out of bounds and overflow issues. All of them are reachable through `gravity -x` with a hand written file. gravity_vm_loadbuffer: - a top level entry was tested for emptiness through u.object.length before its type was checked, reading an unset union member whenever the entry was not an object - a deserialized function may legitimately have a NULL identifier (missing field, or an anonymous function serialized as $anon_), and strlen() was called on it unconditionally: {"x":{"type":"function"}} was enough to crash the loader (#444). A top level function must be named, so a NULL identifier is now a load error - string_casencmp(identifier, INITMODULE_NAME, strlen(identifier)) compared only as many characters as the identifier is long, so any prefix of $moduleinit was accepted as the module initializer. Use string_cmp() for a full comparison json_parse_ex: - the "\uXXXX" escape, the trailing surrogate, and the true/false/null literals all checked one byte less than they consume, so each could read one byte past the end of a buffer that is not NUL terminated (#448). gravity_vm_loadbuffer accepts exactly such a buffer - during the first pass u.object.values is a byte tally, not a pointer, and was incremented through a json_char pointer: undefined behaviour on a null pointer, which traps under -fsanitize=undefined (#448). Keep the tally in a uintptr_t, which also stops it truncating on LLP64 targets - integer and exponent accumulators were multiplied without any range check, overflowing signed 64-bit on a long digit run (#447). The integer accumulator now reports an out of range literal, the exponent saturates, and fraction digits below the precision of a double are dropped while keeping the fraction scale in sync murmur3_32 read the key four bytes at a time through a uint32_t pointer, which is undefined for an unaligned key and faults outright on strict alignment targets. memcpy each block instead; byte order and therefore every hash value is unchanged, and compilers fold it back to a single unaligned load. Adds test/loadbuffer/: one malformed executable per rejection path plus a valid round trip as a positive control, and json_bounds.c for what the CLI cannot reach, since the CLI always hands the loader a NUL terminated buffer while the API does not. Build it with `make jsontest`, ideally under a sanitizer. Co-Authored-By: Claude Opus 5 --- .gitignore | 1 + .travis.yml | 1 + CLAUDE.md | 4 +- Makefile | 12 +- src/runtime/gravity_vm.c | 13 +- src/shared/gravity_hash.c | 9 +- src/utils/gravity_json.c | 72 ++++- src/utils/gravity_json.h | 5 + test/loadbuffer/anon_identifier.json | 1 + test/loadbuffer/entry_is_an_array.json | 1 + test/loadbuffer/entry_is_empty.json | 1 + test/loadbuffer/entry_not_an_object.json | 1 + test/loadbuffer/identifier_not_a_string.json | 1 + test/loadbuffer/identifier_twice.json | 1 + test/loadbuffer/json_bounds.c | 279 +++++++++++++++++++ test/loadbuffer/missing_identifier.json | 1 + test/loadbuffer/root_not_an_object.json | 1 + test/loadbuffer/run_all.sh | 131 +++++++++ test/loadbuffer/truncated.json | 1 + test/loadbuffer/unknown_object_type.json | 1 + test/loadbuffer/valid_roundtrip.gravity | 18 ++ 21 files changed, 537 insertions(+), 18 deletions(-) create mode 100644 test/loadbuffer/anon_identifier.json create mode 100644 test/loadbuffer/entry_is_an_array.json create mode 100644 test/loadbuffer/entry_is_empty.json create mode 100644 test/loadbuffer/entry_not_an_object.json create mode 100644 test/loadbuffer/identifier_not_a_string.json create mode 100644 test/loadbuffer/identifier_twice.json create mode 100644 test/loadbuffer/json_bounds.c create mode 100644 test/loadbuffer/missing_identifier.json create mode 100644 test/loadbuffer/root_not_an_object.json create mode 100755 test/loadbuffer/run_all.sh create mode 100644 test/loadbuffer/truncated.json create mode 100644 test/loadbuffer/unknown_object_type.json create mode 100644 test/loadbuffer/valid_roundtrip.gravity diff --git a/.gitignore b/.gitignore index 8c8c306a..e399e013 100644 --- a/.gitignore +++ b/.gitignore @@ -286,6 +286,7 @@ paket-files/ *.x86_64 *.hex gravity +jsontest # Debug files *.dSYM/ diff --git a/.travis.yml b/.travis.yml index ef1fb47d..d7d2a5fd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,3 +8,4 @@ compiler: script: - make - test/unittest/run_all.sh + - test/loadbuffer/run_all.sh diff --git a/CLAUDE.md b/CLAUDE.md index 1f68242c..75ca6647 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -32,8 +32,10 @@ Compiler flags: `-std=gnu99 -fgnu89-inline -fPIC -DBUILD_GRAVITY_API` The `test/` directory also contains: - `test/fuzzy/` — randomised fuzzing inputs; all must compile/run without crashing - `test/infiniteloop/` — programs that must terminate with a `RUNTIME` error (not hang) +- `test/loadbuffer/` — malformed JSON executables for the `gravity -x` loader; each must be + rejected as a load error without crashing. Run with `test/loadbuffer/run_all.sh` -CI runs: `make && test/unittest/run_all.sh` +CI runs: `make && test/unittest/run_all.sh && test/loadbuffer/run_all.sh` ## Architecture diff --git a/Makefile b/Makefile index 04e19e0d..d3077742 100644 --- a/Makefile +++ b/Makefile @@ -5,6 +5,7 @@ UTILS_DIR = src/utils/ OPT_DIR = src/optionals/ GRAVITY_SRC = src/cli/gravity.c EXAMPLE_SRC = examples/example.c +JSONTEST_SRC = test/loadbuffer/json_bounds.c CC ?= gcc SRC = $(wildcard $(COMPILER_DIR)*.c) \ @@ -18,7 +19,8 @@ CFLAGS = $(INCLUDE) -std=gnu99 -fgnu89-inline -fPIC -DBUILD_GRAVITY_API -MMD OBJ = $(SRC:.c=.o) GRAVITY_OBJ = $(GRAVITY_SRC:.c=.o) EXAMPLE_OBJ = $(EXAMPLE_SRC:.c=.o) -DEP = $(OBJ:.o=.d) $(GRAVITY_OBJ:.o=.d) $(EXAMPLE_OBJ:.o=.d) +JSONTEST_OBJ = $(JSONTEST_SRC:.c=.o) +DEP = $(OBJ:.o=.d) $(GRAVITY_OBJ:.o=.d) $(EXAMPLE_OBJ:.o=.d) $(JSONTEST_OBJ:.o=.d) ifeq ($(OS),Windows_NT) # Windows @@ -67,11 +69,17 @@ gravity: $(OBJ) $(GRAVITY_OBJ) example: $(OBJ) $(EXAMPLE_OBJ) $(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS) +# bounds tests for the JSON scanner, see test/loadbuffer/json_bounds.c. +# Build it with a sanitizer to catch out of bounds reads: +# make jsontest CC="clang -fsanitize=address,undefined" +jsontest: $(OBJ) $(JSONTEST_OBJ) + $(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS) + lib: $(OBJ) $(CC) -shared -o $(LIBTARGET) $(OBJ) $(LDFLAGS) clean: - rm -f $(OBJ) $(GRAVITY_OBJ) $(EXAMPLE_OBJ) $(DEP) gravity example libgravity.dylib libgravity.so gravity.dll + rm -f $(OBJ) $(GRAVITY_OBJ) $(EXAMPLE_OBJ) $(JSONTEST_OBJ) $(DEP) gravity example jsontest libgravity.dylib libgravity.so gravity.dll .PHONY: all clean lib diff --git a/src/runtime/gravity_vm.c b/src/runtime/gravity_vm.c index 9458bbaa..00c98cb9 100644 --- a/src/runtime/gravity_vm.c +++ b/src/runtime/gravity_vm.c @@ -2090,10 +2090,11 @@ gravity_closure_t *gravity_vm_loadbuffer (gravity_vm *vm, const char *buffer, si uint32_t n = json->u.object.length; for (uint32_t i=0; iu.object.values[i].value; - if (entry->u.object.length == 0) continue; - // each entry must be an object + // each entry must be an object (checked before reading any object only + // field, otherwise a non object entry would read an unset union member) if (entry->type != json_object) goto abort_load; + if (entry->u.object.length == 0) continue; gravity_object_t *obj = gravity_object_deserialize(vm, entry); if (!obj) goto abort_load; @@ -2107,8 +2108,14 @@ gravity_closure_t *gravity_vm_loadbuffer (gravity_vm *vm, const char *buffer, si if (OBJECT_ISA_FUNCTION(obj)) { gravity_function_t *f = (gravity_function_t *)obj; const char *identifier = f->identifier; + + // a deserialized function is allowed to have a NULL identifier (missing identifier + // field or anonymous function, serialized as $anon_ and restored as NULL) but a top + // level function must be named because it is either $moduleinit or a global value + if (!identifier) goto abort_load; + gravity_closure_t *cl = gravity_closure_new(vm, f); - if (string_casencmp(identifier, INITMODULE_NAME, strlen(identifier)) == 0) { + if (string_cmp(identifier, INITMODULE_NAME) == 0) { closure = cl; } else { gravity_vm_setvalue(vm, identifier, VALUE_FROM_OBJECT(cl)); diff --git a/src/shared/gravity_hash.c b/src/shared/gravity_hash.c index f8b34a2f..3d13ba0f 100644 --- a/src/shared/gravity_hash.c +++ b/src/shared/gravity_hash.c @@ -111,9 +111,14 @@ static uint32_t murmur3_32 (const char *key, uint32_t len, uint32_t seed) { uint32_t hash = seed; const int nblocks = len / 4; - const uint32_t *blocks = (const uint32_t *) key; for (int i = 0; i < nblocks; i++) { - uint32_t k = blocks[i]; + // key is a plain byte buffer with no alignment guarantee, so each block must be + // copied out instead of read through a uint32_t pointer: a misaligned load is + // undefined behaviour and faults outright on strict-alignment targets. The byte + // order is unchanged, so hash values are identical to the previous code, and + // compilers fold this memcpy back into a single unaligned load + uint32_t k; + memcpy(&k, key + (i * 4), sizeof(k)); k *= c1; k = ROT32(k, r1); k *= c2; diff --git a/src/utils/gravity_json.c b/src/utils/gravity_json.c index e494de8a..43566037 100755 --- a/src/utils/gravity_json.c +++ b/src/utils/gravity_json.c @@ -466,6 +466,28 @@ static void * json_alloc (json_state * state, unsigned long size, int zero) return state->settings.memory_alloc (size, zero, state->settings.user_data); } +/* During the first pass u.object.values does not hold a pointer yet: it is used as + a counter for the total size of the object key strings, and only new_value() + turns it into a real allocation on the second pass. Incrementing it as a pointer + means doing arithmetic on a null pointer, which is undefined behaviour and traps + any build compiled with -fsanitize=undefined (issue #448). Keep the tally in a + uintptr_t instead, copied in and out of the field so no aliasing rule is broken + and the layout of json_value is unchanged. uintptr_t also avoids truncating the + count on LLP64 targets, where unsigned long is narrower than a pointer. */ + +static uintptr_t object_name_bytes_get (const json_value * value) +{ + uintptr_t bytes; + memcpy (&bytes, &value->u.object.values, sizeof (bytes)); + return bytes; +} + +static void object_name_bytes_add (json_value * value, uintptr_t size) +{ + uintptr_t bytes = object_name_bytes_get (value) + size; + memcpy (&value->u.object.values, &bytes, sizeof (bytes)); +} + static int new_value (json_state * state, json_value ** top, json_value ** root, json_value ** alloc, json_type type) @@ -505,12 +527,12 @@ static int new_value (json_state * state, values_size = sizeof (*value->u.object.values) * value->u.object.length; if (! (value->u.object.values = (json_object_entry *) json_alloc - (state, values_size + ((unsigned long) value->u.object.values), 0)) ) + (state, values_size + (unsigned long) object_name_bytes_get (value), 0)) ) { return 0; } - value->_reserved.object_mem = (*(char **) &value->u.object.values) + values_size; + value->_reserved.object_mem = (char *) value->u.object.values + values_size; value->u.object.length = 0; break; @@ -585,6 +607,10 @@ static const long flag_line_comment = 1 << 13, flag_block_comment = 1 << 14; +/* an exponent past this already saturates a double to inf or 0, so the + accumulator can be clamped here instead of being allowed to overflow */ +#define json_num_e_max 1000000 + json_value * json_parse_ex (json_settings * settings, const json_char * json, size_t length, @@ -664,7 +690,10 @@ json_value * json_parse_ex (json_settings * settings, case 't': string_add ('\t'); break; case 'u': - if (end - state.ptr < 4 || + /* state.ptr sits on the `u`, so the four hex digits are at + state.ptr[1..4]: the last one is in range only when at + least 5 bytes are left */ + if (end - state.ptr < 5 || (uc_b1 = hex_value (*++ state.ptr)) == 0xFF || (uc_b2 = hex_value (*++ state.ptr)) == 0xFF || (uc_b3 = hex_value (*++ state.ptr)) == 0xFF || @@ -681,7 +710,8 @@ json_value * json_parse_ex (json_settings * settings, if ((uchar & 0xF800) == 0xD800) { json_uchar uchar2; - if (end - state.ptr < 6 || (*++ state.ptr) != '\\' || (*++ state.ptr) != 'u' || + /* likewise the trailing surrogate spans state.ptr[1..6] */ + if (end - state.ptr < 7 || (*++ state.ptr) != '\\' || (*++ state.ptr) != 'u' || (uc_b1 = hex_value (*++ state.ptr)) == 0xFF || (uc_b2 = hex_value (*++ state.ptr)) == 0xFF || (uc_b3 = hex_value (*++ state.ptr)) == 0xFF || @@ -772,7 +802,7 @@ json_value * json_parse_ex (json_settings * settings, case json_object: if (state.first_pass) - (*(json_char **) &top->u.object.values) += string_length + 1; + object_name_bytes_add (top, string_length + 1); else { top->u.object.values [top->u.object.length].name @@ -781,7 +811,8 @@ json_value * json_parse_ex (json_settings * settings, top->u.object.values [top->u.object.length].name_length = string_length; - (*(json_char **) &top->_reserved.object_mem) += string_length + 1; + top->_reserved.object_mem = + (char *) top->_reserved.object_mem + string_length + 1; } flags |= flag_seek_value | flag_need_colon; @@ -960,7 +991,8 @@ json_value * json_parse_ex (json_settings * settings, case 't': - if ((end - state.ptr) < 3 || *(++ state.ptr) != 'r' || + /* state.ptr sits on the `t`, so `rue` spans state.ptr[1..3] */ + if ((end - state.ptr) < 4 || *(++ state.ptr) != 'r' || *(++ state.ptr) != 'u' || *(++ state.ptr) != 'e') { goto e_unknown_value; @@ -976,7 +1008,8 @@ json_value * json_parse_ex (json_settings * settings, case 'f': - if ((end - state.ptr) < 4 || *(++ state.ptr) != 'a' || + /* `alse` spans state.ptr[1..4] */ + if ((end - state.ptr) < 5 || *(++ state.ptr) != 'a' || *(++ state.ptr) != 'l' || *(++ state.ptr) != 's' || *(++ state.ptr) != 'e') { @@ -991,7 +1024,8 @@ json_value * json_parse_ex (json_settings * settings, case 'n': - if ((end - state.ptr) < 3 || *(++ state.ptr) != 'u' || + /* `ull` spans state.ptr[1..3] */ + if ((end - state.ptr) < 4 || *(++ state.ptr) != 'u' || *(++ state.ptr) != 'l' || *(++ state.ptr) != 'l') { goto e_unknown_value; @@ -1119,14 +1153,32 @@ json_value * json_parse_ex (json_settings * settings, else { flags |= flag_num_e_got_sign; - num_e = (num_e * 10) + (b - '0'); + + if (num_e <= json_num_e_max) + num_e = (num_e * 10) + (b - '0'); + continue; } + if (top->u.integer > (JSON_INT_MAX - (b - '0')) / 10) + { snprintf (error, sizeof(error), "%d:%d: Integer literal is out of range", line_and_col); + goto e_failed; + } + top->u.integer = (top->u.integer * 10) + (b - '0'); continue; } + /* a double holds ~17 significant digits, so once the accumulator + is full the remaining digits fall below the precision of the + result: drop them, and keep num_digits (the fraction scale) in + sync by not counting them */ + if (num_fraction > (JSON_INT_MAX - (b - '0')) / 10) + { + -- num_digits; + continue; + } + num_fraction = (num_fraction * 10) + (b - '0'); continue; } diff --git a/src/utils/gravity_json.h b/src/utils/gravity_json.h index 4917dd4e..518d0a1f 100755 --- a/src/utils/gravity_json.h +++ b/src/utils/gravity_json.h @@ -96,6 +96,11 @@ void json_set_option (json_t *json, json_opt_mask option_value); #endif #endif +/* largest value json_int_t can hold; override alongside json_int_t */ +#ifndef JSON_INT_MAX + #define JSON_INT_MAX INT64_MAX +#endif + #include #ifdef __cplusplus diff --git a/test/loadbuffer/anon_identifier.json b/test/loadbuffer/anon_identifier.json new file mode 100644 index 00000000..7f5c71bf --- /dev/null +++ b/test/loadbuffer/anon_identifier.json @@ -0,0 +1 @@ +{"main":{"type":"function","identifier":"$anon_0x600000004000"}} \ No newline at end of file diff --git a/test/loadbuffer/entry_is_an_array.json b/test/loadbuffer/entry_is_an_array.json new file mode 100644 index 00000000..3355ea9f --- /dev/null +++ b/test/loadbuffer/entry_is_an_array.json @@ -0,0 +1 @@ +{"main":[1,2,3]} \ No newline at end of file diff --git a/test/loadbuffer/entry_is_empty.json b/test/loadbuffer/entry_is_empty.json new file mode 100644 index 00000000..d9b3f1f2 --- /dev/null +++ b/test/loadbuffer/entry_is_empty.json @@ -0,0 +1 @@ +{"main":{}} \ No newline at end of file diff --git a/test/loadbuffer/entry_not_an_object.json b/test/loadbuffer/entry_not_an_object.json new file mode 100644 index 00000000..f021479c --- /dev/null +++ b/test/loadbuffer/entry_not_an_object.json @@ -0,0 +1 @@ +{"main":123} \ No newline at end of file diff --git a/test/loadbuffer/identifier_not_a_string.json b/test/loadbuffer/identifier_not_a_string.json new file mode 100644 index 00000000..c79aa62b --- /dev/null +++ b/test/loadbuffer/identifier_not_a_string.json @@ -0,0 +1 @@ +{"main":{"type":"function","identifier":42}} \ No newline at end of file diff --git a/test/loadbuffer/identifier_twice.json b/test/loadbuffer/identifier_twice.json new file mode 100644 index 00000000..ab46d7ca --- /dev/null +++ b/test/loadbuffer/identifier_twice.json @@ -0,0 +1 @@ +{"main":{"type":"function","identifier":"main","identifier":"main"}} \ No newline at end of file diff --git a/test/loadbuffer/json_bounds.c b/test/loadbuffer/json_bounds.c new file mode 100644 index 00000000..b06875c3 --- /dev/null +++ b/test/loadbuffer/json_bounds.c @@ -0,0 +1,279 @@ +// Regression tests for the bounds of the JSON scanner (issue #448). +// +// gravity_vm_loadbuffer() and json_parse() take an explicit length and make no +// promise that the buffer is NUL terminated, so every lookahead in the scanner has +// to stay inside [buffer, buffer + length). The CLI happens to hide any mistake +// here because file_read() over-allocates one byte and writes a terminator, so +// these cases can only be reached through the C API -- which is why they live in a +// C test instead of a .gravity or .json fixture. +// +// Every input below is copied into an exact sized heap allocation, so an over-read +// of even a single byte is caught when this is built with -fsanitize=address: +// +// make jsontest CC="clang -fsanitize=address,undefined" +// ./jsontest +// +// Without a sanitizer the test still checks the behaviour the bounds bugs broke: +// truncated input must be rejected and well formed input must survive unchanged. + +#include +#include +#include +#include "gravity_json.h" +#include "gravity_vm.h" +#include "gravity_core.h" + +static int failures = 0; +static int checks = 0; + +static void check (int ok, const char *what) { + ++checks; + if (ok) return; + ++failures; + printf("Fail! %s\n", what); +} + +// parse a copy of buffer that is exactly len bytes long and has no terminator +static json_value *parse_exact (const char *buffer, size_t len) { + char *exact = (char *)malloc(len ? len : 1); + if (!exact) { printf("Fail! out of memory\n"); exit(1); } + memcpy(exact, buffer, len); + + json_value *value = json_parse(exact, len); + + free(exact); + return value; +} + +// Same, but with one extra byte placed just past the end that the scanner is not +// allowed to look at. trap is chosen to complete the truncated token, so a parser +// that reads it returns a value instead of an error: that turns an out of bounds +// read into a wrong answer this test can see without help from a sanitizer. +static json_value *parse_with_trap (const char *buffer, size_t len, char trap) { + char *padded = (char *)malloc(len + 1); + if (!padded) { printf("Fail! out of memory\n"); exit(1); } + memcpy(padded, buffer, len); + padded[len] = trap; + + json_value *value = json_parse(padded, len); + + free(padded); + return value; +} + +// --------------------------------------------------------------------------- +// truncated values: the scanner used to read one byte past the buffer while +// looking ahead at "true"/"false"/"null" and at \uXXXX escapes +// --------------------------------------------------------------------------- + +static const char *const truncated[] = { + "tru", "fals", "nul", // literals cut one byte short + "t", "tr", "f", "fa", "fal", "n", "nu", // and cut shorter still + "\"\\uD80", "\"\\u00", "\"\\u0", "\"\\u", // \uXXXX escape cut short + "\"\\ud800\\ud80", "\"\\ud800\\u", "\"\\ud800\\\\", // trailing surrogate cut short + "[tru", "[fals", "[nul", "{\"a\":tru", "{\"a\":\"\\uD80", +}; + +static void test_truncated (void) { + for (size_t i = 0; i < sizeof(truncated) / sizeof(truncated[0]); ++i) { + json_value *value = parse_exact(truncated[i], strlen(truncated[i])); + char what[128]; + snprintf(what, sizeof(what), "truncated input `%s` was accepted", truncated[i]); + check(value == NULL, what); + json_value_free(value); + } + + // A literal cut one byte short, with the missing byte sitting just past the + // end of the buffer. Reading it completes the literal at top level, so the + // parse succeeds and the check below fails on a build with no sanitizer. + // Only these three are decisive: completing a truncated \uXXXX escape still + // leaves the string unterminated, so the escape guards are covered by the + // exact sized allocations above and need ASan to be seen. + static const struct { const char *text; char trap; } traps[] = { + {"tru", 'e'}, + {"fals", 'e'}, + {"nul", 'l'}, + }; + + for (size_t i = 0; i < sizeof(traps) / sizeof(traps[0]); ++i) { + json_value *value = parse_with_trap(traps[i].text, strlen(traps[i].text), traps[i].trap); + char what[128]; + snprintf(what, sizeof(what), "`%s` was completed by reading the byte past the buffer", + traps[i].text); + check(value == NULL, what); + json_value_free(value); + } +} + +// --------------------------------------------------------------------------- +// every prefix of every seed: wherever the buffer stops, the scanner must not +// read past it. A prefix is allowed to parse or to be rejected, so the only +// assertion here is that the full seed still parses -- for the prefixes the +// sanitizer is the oracle. +// --------------------------------------------------------------------------- + +static const char *const seeds[] = { + "{\"key\":\"value\"}", "[1,2,3]", "true", "false", "null", "\"\\u0041\\uD83D\\uDE00\"", + "{\"a\":{\"b\":[1,-2.5e+3,null,false]}}", "-0.0e-1", "[[[]]]", "{\"\":\"\"}", + "\"\\\\\\\"\\/\\b\\f\\n\\r\\t\"", " \t\r\n{\"x\":true} ", +}; + +static void test_every_prefix (void) { + for (size_t i = 0; i < sizeof(seeds) / sizeof(seeds[0]); ++i) { + size_t full = strlen(seeds[i]); + + for (size_t len = 0; len < full; ++len) + json_value_free(parse_exact(seeds[i], len)); + + json_value *value = parse_exact(seeds[i], full); + char what[128]; + snprintf(what, sizeof(what), "valid input `%s` was rejected", seeds[i]); + check(value != NULL, what); + json_value_free(value); + } +} + +// --------------------------------------------------------------------------- +// escapes and literals that end exactly on the last byte are legal and must not +// be rejected by an over-corrected bounds check +// --------------------------------------------------------------------------- + +static void test_exact_fit (void) { + static const struct { const char *text; json_type type; } cases[] = { + {"true", json_boolean}, + {"false", json_boolean}, + {"null", json_null}, + {"\"\\u0041\"", json_string}, + {"\"\\uD83D\\uDE00\"", json_string}, + }; + + for (size_t i = 0; i < sizeof(cases) / sizeof(cases[0]); ++i) { + json_value *value = parse_exact(cases[i].text, strlen(cases[i].text)); + char what[128]; + snprintf(what, sizeof(what), "`%s` ending on the last byte was rejected", cases[i].text); + check(value != NULL && value->type == cases[i].type, what); + json_value_free(value); + } + + // check the decoded content too, so a bounds fix cannot pass by silently + // dropping the last escape + json_value *value = parse_exact("\"\\u0041\"", 8); + check(value != NULL && value->type == json_string + && value->u.string.length == 1 && value->u.string.ptr[0] == 'A', + "\\u0041 did not decode to `A`"); + json_value_free(value); + + // U+1F600 encodes to the four UTF-8 bytes F0 9F 98 80 + value = parse_exact("\"\\uD83D\\uDE00\"", 14); + check(value != NULL && value->type == json_string + && value->u.string.length == 4 + && memcmp(value->u.string.ptr, "\xF0\x9F\x98\x80", 4) == 0, + "surrogate pair did not decode to U+1F600"); + json_value_free(value); +} + +// --------------------------------------------------------------------------- +// objects: the first pass tallies the size of the key strings inside +// u.object.values, which used to be done with arithmetic on a null pointer +// --------------------------------------------------------------------------- + +static void test_object_keys (void) { + static const char text[] = + "{\"a\":1,\"bb\":2,\"ccc\":3,\"\":4,\"dddddddddddddddddddd\":5}"; + static const char *const names[] = {"a", "bb", "ccc", "", "dddddddddddddddddddd"}; + + json_value *value = parse_exact(text, sizeof(text) - 1); + check(value != NULL && value->type == json_object, "object with mixed key lengths was rejected"); + if (!value || value->type != json_object) { json_value_free(value); return; } + + check(value->u.object.length == 5, "wrong number of object entries"); + + for (unsigned int i = 0; i < value->u.object.length && i < 5; ++i) { + json_object_entry *entry = &value->u.object.values[i]; + char what[160]; + snprintf(what, sizeof(what), "entry %u: expected key `%s`, got `%s`", i, names[i], entry->name); + check(entry->name_length == strlen(names[i]) + && strcmp(entry->name, names[i]) == 0, what); + snprintf(what, sizeof(what), "entry %u: expected value %u", i, i + 1); + check(entry->value != NULL && entry->value->type == json_integer + && entry->value->u.integer == (json_int_t)(i + 1), what); + } + + json_value_free(value); +} + +// --------------------------------------------------------------------------- +// the input reported in issue #448, driven through the entry point it came in by +// --------------------------------------------------------------------------- + +// 520 bytes of malformed bytecode: many double quotes and NUL bytes, so the +// parser is still mid-scan when the buffer ends +static const char issue448_poc[] = + "0a20202020202020207b0a2020222222222222222222222222222222000000a5a5a5a7a5a5a5a5" + "a5a5a5a5a5a5a5a5a5a5a4756e63206910697436000010006e322c6e332c6e34290a202020207b" + "0a202020202020202078203d206e66756e202020202020202079203d206e32bb0a202043202020" + "2020773f32206e0100102020202020202020202020202020202020202020202020202020202020" + "2020202020202020202000000100206e333b0a2020202020202020201718171717172020203078" + "394cae2869207d0a0a20171716fa17171717101717181717171720202030783939393939393939" + "393939393939393939393939393939393939393939282828a82828202020766172743600001000" + "6e322c392828282828282020207661727436000010006e322c6e332c6e332c6e34290a3e202069" + "3b0a3b0a0000020000a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a4756e632069106974360026" + "262626742020202020773f32206e332828282828282828282828282828282020207661723b0a20" + "20202020202020202020202020202020202020202020ff6820303d20313b0a2020202020202020" + "7d0a0a202020202020202072657475726f20662b963b0a202020207d1f7d0a0a0a66756e63206d" + "61696e28290a7b0a202020207661722020202020ff68202b3e37313b0a20202020002020207d0a" + "0a20202020202061202b20623b"; + +static int hex_digit (char c) { + if (c >= '0' && c <= '9') return c - '0'; + if (c >= 'a' && c <= 'f') return c - 'a' + 10; + return -1; +} + +static void report_error (gravity_vm *vm, error_type_t error_type, const char *description, + error_desc_t error_desc, void *xdata) { + #pragma unused(vm, error_type, description, error_desc, xdata) +} + +static void test_loadbuffer (void) { + const size_t hexlen = sizeof(issue448_poc) - 1; + const size_t len = hexlen / 2; + + check(hexlen % 2 == 0 && len == 520, "issue #448 sample is not 520 bytes"); + + // an exact sized buffer, exactly as a fuzz harness or an embedder loading + // bytecode out of mapped memory would pass it + char *poc = (char *)malloc(len); + if (!poc) { printf("Fail! out of memory\n"); exit(1); } + for (size_t i = 0; i < len; ++i) + poc[i] = (char)((hex_digit(issue448_poc[i * 2]) << 4) | hex_digit(issue448_poc[i * 2 + 1])); + + gravity_delegate_t delegate = {.error_callback = report_error}; + gravity_vm *vm = gravity_vm_new(&delegate); + check(vm != NULL, "unable to create a VM"); + + if (vm) { + gravity_closure_t *closure = gravity_vm_loadbuffer(vm, poc, len); + check(closure == NULL, "issue #448 sample was accepted by gravity_vm_loadbuffer"); + + // the degenerate case of the same path + closure = gravity_vm_loadbuffer(vm, "", 0); + check(closure == NULL, "empty buffer was accepted by gravity_vm_loadbuffer"); + + gravity_vm_free(vm); + } + + free(poc); + gravity_core_free(); +} + +int main (void) { + test_truncated(); + test_every_prefix(); + test_exact_fit(); + test_object_keys(); + test_loadbuffer(); + + printf("Checks run successfully: %d/%d. %d failed\n", checks - failures, checks, failures); + return (failures == 0) ? 0 : 1; +} diff --git a/test/loadbuffer/missing_identifier.json b/test/loadbuffer/missing_identifier.json new file mode 100644 index 00000000..00a591b5 --- /dev/null +++ b/test/loadbuffer/missing_identifier.json @@ -0,0 +1 @@ +{"main":{"type":"function"}} \ No newline at end of file diff --git a/test/loadbuffer/root_not_an_object.json b/test/loadbuffer/root_not_an_object.json new file mode 100644 index 00000000..3a26a2e5 --- /dev/null +++ b/test/loadbuffer/root_not_an_object.json @@ -0,0 +1 @@ +[1,2,3] \ No newline at end of file diff --git a/test/loadbuffer/run_all.sh b/test/loadbuffer/run_all.sh new file mode 100755 index 00000000..814d8bff --- /dev/null +++ b/test/loadbuffer/run_all.sh @@ -0,0 +1,131 @@ +#!/bin/bash + +# Regression tests for the JSON executable loader (gravity -x / gravity_vm_loadbuffer). +# +# Every .json file in this directory is a malformed JSON executable: loading it must +# be reported as a load error and must never crash the process. See issue #444, where +# {"x":{"type":"function"}} made gravity_vm_loadbuffer() call strlen() on the NULL +# identifier of a top level function. +# +# valid_roundtrip.gravity is the positive control: it is compiled and then executed +# through the very same loader, so the checks above cannot pass just because the +# loader started rejecting every input. +# +# json_bounds.c covers what the CLI cannot reach: gravity_vm_loadbuffer() accepts a +# buffer that is not NUL terminated, while the CLI always hands it one that is. Run +# `make jsontest` to build it, ideally with a sanitizer (see the file header). + +set -u -o pipefail + +readonly SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" +readonly GRAVITY_BIN=$SCRIPT_DIR/../../gravity +readonly LOAD_ERROR="Error while loading compile file" + +if [[ ! -x "$GRAVITY_BIN" ]]; then + echo "gravity executable not found in $(dirname "$GRAVITY_BIN"), run make first" + exit 1 +fi + +# Same portable timeout resolution used by test/unittest/run_all.sh: +# timeout (Linux/GNU coreutils) → gtimeout (macOS + brew install coreutils) → +# pure-bash fallback using a background kill watcher. +if command -v timeout &>/dev/null; then + run_timeout() { timeout "$@"; } +elif command -v gtimeout &>/dev/null; then + run_timeout() { gtimeout "$@"; } +else + run_timeout() { + local t=$1; shift + "$@" & + local pid=$! + ( sleep "$t" && kill "$pid" 2>/dev/null ) & + local watcher=$! + wait "$pid" 2>/dev/null + local rc=$? + kill "$watcher" 2>/dev/null + wait "$watcher" 2>/dev/null + [[ $rc -eq 143 ]] && return 124 + return $rc + } +fi + +tests_success=0 +tests_fail=0 + +report_success() { + echo "Success!" + tests_success=$(($tests_success+1)) +} + +report_fail() { + echo "Fail! $1" + tests_fail=$(($tests_fail+1)) +} + +# a malformed executable must be rejected, not crash +for test in "$SCRIPT_DIR"/*.json; do + echo "Testing $(basename "$test")..." + output=$(run_timeout 10 "$GRAVITY_BIN" -x "$test" 2>&1) + res=$? + + if [[ $res -eq 124 ]]; then + report_fail "timeout" + elif [[ $res -ge 128 ]]; then + # 128+n means the process was killed by signal n (139 = SIGSEGV) + report_fail "killed by signal $(($res-128))" + elif [[ "$output" != *"$LOAD_ERROR"* ]]; then + report_fail "malformed input was not rejected: $output" + else + report_success + fi +done + +# a well formed executable must still load and run +readonly ROUNDTRIP_SRC=$SCRIPT_DIR/valid_roundtrip.gravity +readonly ROUNDTRIP_OUT=$(mktemp -d)/valid_roundtrip.json + +echo "Testing $(basename "$ROUNDTRIP_SRC")..." +output=$(run_timeout 10 "$GRAVITY_BIN" -c "$ROUNDTRIP_SRC" -o "$ROUNDTRIP_OUT" 2>&1) +if [[ ! -f "$ROUNDTRIP_OUT" ]]; then + report_fail "unable to compile $ROUNDTRIP_SRC: $output" +else + output=$(run_timeout 10 "$GRAVITY_BIN" -x "$ROUNDTRIP_OUT" 2>&1) + res=$? + if [[ $res -ne 0 ]]; then + report_fail "exit code $res" + elif [[ "$output" == *"$LOAD_ERROR"* ]]; then + report_fail "valid executable was rejected: $output" + elif [[ "$output" != *"(INT) 0"* ]]; then + report_fail "unexpected result: $output" + else + report_success + fi +fi +rm -rf "$(dirname "$ROUNDTRIP_OUT")" + +# scanner bounds tests, only if they have been built (make jsontest) +readonly JSONTEST_BIN=$SCRIPT_DIR/../../jsontest + +echo "Testing json_bounds..." +if [[ ! -x "$JSONTEST_BIN" ]]; then + echo "Skipped: run 'make jsontest' to build it" +else + output=$(run_timeout 60 "$JSONTEST_BIN" 2>&1) + res=$? + + if [[ $res -eq 124 ]]; then + report_fail "timeout" + elif [[ $res -ge 128 ]]; then + report_fail "killed by signal $(($res-128))" + elif [[ $res -ne 0 ]]; then + report_fail "$output" + else + report_success + fi +fi + +tests_total=$(($tests_success+$tests_fail)) +echo "Tests run successfully: $tests_success/$tests_total. $tests_fail failed" + +[[ $tests_fail -ne 0 ]] && exit 1 +exit 0 diff --git a/test/loadbuffer/truncated.json b/test/loadbuffer/truncated.json new file mode 100644 index 00000000..759a47cb --- /dev/null +++ b/test/loadbuffer/truncated.json @@ -0,0 +1 @@ +{"main":{"type":"fun \ No newline at end of file diff --git a/test/loadbuffer/unknown_object_type.json b/test/loadbuffer/unknown_object_type.json new file mode 100644 index 00000000..1700a835 --- /dev/null +++ b/test/loadbuffer/unknown_object_type.json @@ -0,0 +1 @@ +{"main":{"type":"nosuchtype","identifier":"main"}} \ No newline at end of file diff --git a/test/loadbuffer/valid_roundtrip.gravity b/test/loadbuffer/valid_roundtrip.gravity new file mode 100644 index 00000000..3f4ba6ec --- /dev/null +++ b/test/loadbuffer/valid_roundtrip.gravity @@ -0,0 +1,18 @@ +// Positive control for run_all.sh: this file is compiled to a JSON executable and +// then executed with -x, so that the checks on malformed input cannot pass simply +// because gravity_vm_loadbuffer() started rejecting everything. + +var double = func(a) { return a * 2; }; + +class Counter { + var value = 5; + func next() { + return func() { return value + 2; }; + } +} + +func main() { + var c = Counter(); + if (double(c.next()()) != 14) return 1; + return 0; +} From 6330961df3882b5916729bfd00603f99a720e18b Mon Sep 17 00:00:00 2001 From: Marco Bambini Date: Wed, 5 Aug 2026 13:27:34 +0200 Subject: [PATCH 23/37] fix: core reference leaked by every gravity_compiler_run gravity_compiler_run() called gravity_core_register() on each invocation, but only gravity_compiler_free() ever released a reference. An embedder reusing one compiler for N compilations left N-1 unreleased core references, so gravity_core_free() never reached refcount 0 and neither the core classes nor the optional ones (Math/ENV/JSON/File) were freed (~107 KB retained at exit). On a successful compile the compiler was never reset, so the next run also overwrote compiler->vm and orphaned the previous mini VM struct (320 bytes per extra compilation). The mini VM holds no per-compilation state (it is just a container for the transfer/cleanup callbacks), so create it once and reuse it, and pair the core reference it takes with a release wherever the mini VM is freed. The refcount keeps the core alive for a real VM created with gravity_vm_new, which owns its own reference. This also removes the unconditional gravity_core_free() in gravity_compiler_free(): it released a reference the compiler may never have taken, so freeing a never-run compiler while a VM was alive freed the core out from under it (segfault). Verified with a program compiling N times on one compiler followed by a full teardown: retained heap at exit is now 534 nodes / 66 KB for N=1, 3 and 10, identical to the single-compile flow in examples/example.c, with no unreachable blocks. Unit tests 350/350, ASan/UBSan build of examples/example.c and of the test suite report nothing. Co-Authored-By: Claude Opus 5 --- src/compiler/gravity_compiler.c | 37 ++++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/src/compiler/gravity_compiler.c b/src/compiler/gravity_compiler.c index 8b0df9ec..f771813c 100644 --- a/src/compiler/gravity_compiler.c +++ b/src/compiler/gravity_compiler.c @@ -77,7 +77,7 @@ gravity_compiler_t *gravity_compiler_create (gravity_delegate_t *delegate) { return compiler; } -static void gravity_compiler_reset (gravity_compiler_t *compiler, bool free_core) { +static void gravity_compiler_reset (gravity_compiler_t *compiler) { // free memory for array of strings storage if (compiler->storage) { cstring_array_each(compiler->storage, {mem_free((void *)val);}); @@ -89,15 +89,20 @@ static void gravity_compiler_reset (gravity_compiler_t *compiler, bool free_core if (compiler->parser) gravity_parser_free(compiler->parser); // at the end free mini VM and objects array - if (compiler->vm) gravity_vm_free(compiler->vm); + if (compiler->vm) { + gravity_vm_free(compiler->vm); + + // release the core reference taken by gravity_compiler_run when the mini VM was created, + // so that register/release stay balanced no matter how many times the compiler is run. + // Core is really freed only when its refcount drops to zero, so a real VM created with + // gravity_vm_new (which owns its own reference) keeps core and optionals alive. + gravity_core_free(); + } if (compiler->objects) { marray_destroy(*compiler->objects); mem_free((void*)compiler->objects); } - // feel free to free core if someone requires it - if (free_core) gravity_core_free(); - // reset internal pointers compiler->vm = NULL; compiler->ast = NULL; @@ -107,7 +112,7 @@ static void gravity_compiler_reset (gravity_compiler_t *compiler, bool free_core } void gravity_compiler_free (gravity_compiler_t *compiler) { - gravity_compiler_reset(compiler, true); + gravity_compiler_reset(compiler); mem_free(compiler); } @@ -147,11 +152,19 @@ gravity_closure_t *gravity_compiler_run (gravity_compiler_t *compiler, const cha if (compiler->ast) gnode_free(compiler->ast); if (!compiler->objects) compiler->objects = void_array_create(); - // CODEGEN requires a mini vm in order to be able to handle garbage collector - compiler->vm = gravity_vm_newmini(); - gravity_vm_setdata(compiler->vm, (void *)compiler); - gravity_vm_set_callbacks(compiler->vm, internal_vm_transfer, internal_vm_cleanup); - gravity_core_register(compiler->vm); + // CODEGEN requires a mini vm in order to be able to handle garbage collector. + // The mini VM is just a container for the transfer/cleanup callbacks (it holds no + // per-compilation state) so it is created once and reused by every subsequent run + // of the same compiler: creating a new one here would orphan the previous one and + // would take an extra core reference that nothing releases. + if (!compiler->vm) { + compiler->vm = gravity_vm_newmini(); + gravity_vm_setdata(compiler->vm, (void *)compiler); + gravity_vm_set_callbacks(compiler->vm, internal_vm_transfer, internal_vm_cleanup); + + // core reference owned by the mini VM, released by gravity_compiler_reset + gravity_core_register(compiler->vm); + } // STEP 0: CREATE PARSER compiler->parser = gravity_parser_create(source, len, fileid, is_static); @@ -180,7 +193,7 @@ gravity_closure_t *gravity_compiler_run (gravity_compiler_t *compiler, const cha if (f) return gravity_closure_new(compiler->vm, f); abort_compilation: - gravity_compiler_reset(compiler, false); + gravity_compiler_reset(compiler); return NULL; } From bfc9f491177c380a108c0c9eb92554356344912d Mon Sep 17 00:00:00 2001 From: Marco Bambini Date: Wed, 5 Aug 2026 09:39:56 +0200 Subject: [PATCH 24/37] fix: Int overflow and float remainder constant folding (#443) Int arithmetic in Gravity wraps around on overflow, which is what the runtime operators and the constant folder have always produced, but the wrap was performed directly on signed operands: signed overflow is undefined behaviour in C, so every one of those sites traps under -fsanitize=undefined and is at the mercy of the optimizer elsewhere. Add GRAVITY_INT_ADD/SUB/MUL/NEG/DIV/REM in gravity_value.h, which do the arithmetic on the unsigned counterpart and convert back. The values are unchanged, they are simply no longer undefined. Both operands are widened to 64bit so a single definition serves either GRAVITY_ENABLE_INT64 setting. All three paths now go through them, so the folded constant and the runtime cannot drift apart: - CHECK_FAST_BINARY_MATH / CHECK_FAST_UNARY_MATH take the Int operation as a macro parameter and apply it on the Int path only, the Float one keeps using the plain operator - operator_int_add/sub/mul/neg/div/rem in gravity_core.c - optimize_const_instruction in gravity_optimizer.c GRAVITY_INT_DIV and GRAVITY_INT_REM also cover GRAVITY_INT_MIN op -1, which does not merely wrap: on x86 idiv faults and the process dies with SIGFPE, both while folding and at runtime. Separately, optimize_const_instruction folded a floating point REM by truncating both operands to int64_t. That divided by zero for any 0 < |divisor| < 1, and disagreed with the runtime on every operand with a fractional part: 2.5 % 2.0 folded to 0 where the VM evaluates 0.5. Fold with remainder(), which is what operator_float_rem computes. REM is also dispatched on the class of the left operand, so a mixed Int/Float expression runs operator_int_rem and does not follow float semantics at all. Those are left to the runtime instead of folded. Adds a unit test which compares each folded expression against the same operation evaluated at runtime, so the two paths cannot silently diverge again. Co-Authored-By: Claude Opus 5 --- src/compiler/gravity_optimizer.c | 24 +++++++++++---- src/runtime/gravity_core.c | 9 +++--- src/runtime/gravity_vm.c | 10 +++---- src/runtime/gravity_vmmacros.h | 16 ++++++---- src/shared/gravity_value.h | 21 +++++++++++++ .../bugfix_const_folding_float_rem.gravity | 30 +++++++++++++++++++ 6 files changed, 89 insertions(+), 21 deletions(-) create mode 100644 test/unittest/bugfix_const_folding_float_rem.gravity diff --git a/src/compiler/gravity_optimizer.c b/src/compiler/gravity_optimizer.c index 5f6eaaab..4601b653 100644 --- a/src/compiler/gravity_optimizer.c +++ b/src/compiler/gravity_optimizer.c @@ -8,6 +8,7 @@ // Some optimizations taken from: http://www.compileroptimizations.com/ +#include #include "gravity_hash.h" #include "gravity_optimizer.h" #include "gravity_opcodes.h" @@ -308,19 +309,22 @@ static bool optimize_const_instruction (inst_t *inst, inst_t *inst1, inst_t *ins // perform operation switch (inst->op) { + // the Int cases must wrap exactly like the runtime operators do (see operator_int_add, + // operator_int_sub and operator_int_mul), otherwise folding an expression at compile time + // would give a different result than evaluating it at runtime case ADD: if (type == DOUBLE_TAG) d = d1 + d2; - else n = n1 + n2; + else n = GRAVITY_INT_ADD(n1, n2); break; case SUB: if (type == DOUBLE_TAG) d = d1 - d2; - else n = n1 - n2; + else n = GRAVITY_INT_SUB(n1, n2); break; case MUL: if (type == DOUBLE_TAG) d = d1 * d2; - else n = n1 * n2; + else n = GRAVITY_INT_MUL(n1, n2); break; case DIV: @@ -330,17 +334,25 @@ static bool optimize_const_instruction (inst_t *inst, inst_t *inst1, inst_t *ins d = d1 / d2; } else { if (n2 == 0) return false; - n = n1 / n2; + n = GRAVITY_INT_DIV(n1, n2); } break; case REM: if (type == DOUBLE_TAG) { + // REM is dispatched on the class of the left operand, so a mixed + // Int/Float expression runs operator_int_rem and does not follow + // float semantics at all: leave those to the runtime + if (inst1->tag != inst2->tag) return false; if (d2 == 0.0) return false; - d = (double)((int64_t)d1 % (int64_t)d2); + // must match operator_float_rem, which computes an IEEE remainder. + // truncating both operands to int64 instead divided by zero for any + // 0 < |d2| < 1, and disagreed with the runtime on every operand with + // a fractional part: 2.5 % 2.0 folded to 0 but evaluated to 0.5 + d = remainder(d1, d2); } else { if (n2 == 0) return false; - n = n1 % n2; + n = GRAVITY_INT_REM(n1, n2); } break; diff --git a/src/runtime/gravity_core.c b/src/runtime/gravity_core.c index 9a661d5b..4fece9ae 100644 --- a/src/runtime/gravity_core.c +++ b/src/runtime/gravity_core.c @@ -2021,14 +2021,14 @@ static bool operator_int_add (gravity_vm *vm, gravity_value_t *args, uint16_t na #pragma unused (nargs) DECLARE_2VARIABLES(v1, v2, 0, 1); INTERNAL_CONVERT_INT(v2, true); - RETURN_VALUE(VALUE_FROM_INT(v1.n + v2.n), rindex); + RETURN_VALUE(VALUE_FROM_INT(GRAVITY_INT_ADD(v1.n, v2.n)), rindex); } static bool operator_int_sub (gravity_vm *vm, gravity_value_t *args, uint16_t nargs, uint32_t rindex) { #pragma unused (nargs) DECLARE_2VARIABLES(v1, v2, 0, 1); INTERNAL_CONVERT_INT(v2, true); - RETURN_VALUE(VALUE_FROM_INT(v1.n - v2.n), rindex); + RETURN_VALUE(VALUE_FROM_INT(GRAVITY_INT_SUB(v1.n, v2.n)), rindex); } static bool operator_int_div (gravity_vm *vm, gravity_value_t *args, uint16_t nargs, uint32_t rindex) { @@ -2044,7 +2044,7 @@ static bool operator_int_mul (gravity_vm *vm, gravity_value_t *args, uint16_t na #pragma unused (nargs) DECLARE_2VARIABLES(v1, v2, 0, 1); INTERNAL_CONVERT_INT(v2, true); - RETURN_VALUE(VALUE_FROM_INT(v1.n * v2.n), rindex); + RETURN_VALUE(VALUE_FROM_INT(GRAVITY_INT_MUL(v1.n, v2.n)), rindex); } static bool operator_int_rem (gravity_vm *vm, gravity_value_t *args, uint16_t nargs, uint32_t rindex) { @@ -2074,7 +2074,8 @@ static bool operator_int_or (gravity_vm *vm, gravity_value_t *args, uint16_t nar static bool operator_int_neg (gravity_vm *vm, gravity_value_t *args, uint16_t nargs, uint32_t rindex) { #pragma unused(vm, nargs) - RETURN_VALUE(VALUE_FROM_INT(-GET_VALUE(0).n), rindex); + // negating GRAVITY_INT_MIN overflows just like the binary operators above + RETURN_VALUE(VALUE_FROM_INT(GRAVITY_INT_NEG(GET_VALUE(0).n)), rindex); } static bool operator_int_not (gravity_vm *vm, gravity_value_t *args, uint16_t nargs, uint32_t rindex) { diff --git a/src/runtime/gravity_vm.c b/src/runtime/gravity_vm.c index 9458bbaa..eab7bf61 100644 --- a/src/runtime/gravity_vm.c +++ b/src/runtime/gravity_vm.c @@ -903,7 +903,7 @@ static bool gravity_vm_exec (gravity_vm *vm) { DECODE_BINARY_OPERATION(r1, r2, r3); // check fast math operation first (only in case of int and float) - CHECK_FAST_BINARY_MATH(r1, r2, r3, v2, v3, +, NO_CHECK); + CHECK_FAST_BINARY_MATH(r1, r2, r3, v2, v3, +, GRAVITY_INT_ADD, NO_CHECK); // fast math operation cannot be performed so let's try with a regular call // prepare function call for binary operation @@ -922,7 +922,7 @@ static bool gravity_vm_exec (gravity_vm *vm) { DECODE_BINARY_OPERATION(r1, r2, r3); // check fast math operation first (only in case of int and float) - CHECK_FAST_BINARY_MATH(r1, r2, r3, v2, v3, -, NO_CHECK); + CHECK_FAST_BINARY_MATH(r1, r2, r3, v2, v3, -, GRAVITY_INT_SUB, NO_CHECK); // prepare function call for binary operation PREPARE_FUNC_CALL2(closure, v2, v3, GRAVITY_SUB_INDEX, rwin); @@ -951,7 +951,7 @@ static bool gravity_vm_exec (gravity_vm *vm) { #pragma warning (push) #pragma warning (disable: 4723) #endif - CHECK_FAST_BINARY_MATH(r1, r2, r3, v2, v3, /, CHECK_ZERO(v3)); + CHECK_FAST_BINARY_MATH(r1, r2, r3, v2, v3, /, GRAVITY_INT_DIV, CHECK_ZERO(v3)); #if defined(__clang__) #pragma clang diagnostic pop #elif defined(__GNUC__) @@ -976,7 +976,7 @@ static bool gravity_vm_exec (gravity_vm *vm) { DECODE_BINARY_OPERATION(r1, r2, r3); // check fast math operation first (only in case of int and float) - CHECK_FAST_BINARY_MATH(r1, r2, r3, v2, v3, *, NO_CHECK); + CHECK_FAST_BINARY_MATH(r1, r2, r3, v2, v3, *, GRAVITY_INT_MUL, NO_CHECK); // prepare function call for binary operation PREPARE_FUNC_CALL2(closure, v2, v3, GRAVITY_MUL_INDEX, rwin); @@ -1050,7 +1050,7 @@ static bool gravity_vm_exec (gravity_vm *vm) { #pragma unused(r3) // check fast bool operation first (only if it is int or float) - CHECK_FAST_UNARY_MATH(r1, r2, v2, -); + CHECK_FAST_UNARY_MATH(r1, r2, v2, -, GRAVITY_INT_NEG); // prepare function call for binary operation PREPARE_FUNC_CALL1(closure, v2, GRAVITY_NEG_INDEX, rwin); diff --git a/src/runtime/gravity_vmmacros.h b/src/runtime/gravity_vmmacros.h index 81ee8890..47aefb1f 100644 --- a/src/runtime/gravity_vmmacros.h +++ b/src/runtime/gravity_vmmacros.h @@ -184,6 +184,10 @@ // FAST MATH MACROS #define FMATH_BIN_INT(_r1,_v2,_v3,_OP) do {SETVALUE(_r1, VALUE_FROM_INT(_v2 _OP _v3)); DISPATCH_INNER();} while(0) +// Int math overflows are undefined behaviour, so the operation goes through the GRAVITY_INT_* +// helpers (see gravity_value.h) instead of applying the operator directly. The helper form is +// only needed for the Int path: the Float one keeps using the plain operator +#define FMATH_BIN_INT_OP(_r1,_v2,_v3,_INTOP) do {SETVALUE(_r1, VALUE_FROM_INT(_INTOP(_v2, _v3))); DISPATCH_INNER();} while(0) #define FMATH_BIN_FLOAT(_r1,_v2,_v3,_OP) do {SETVALUE(_r1, VALUE_FROM_FLOAT(_v2 _OP _v3)); DISPATCH_INNER();} while(0) #define FMATH_BIN_BOOL(_r1,_v2,_v3,_OP) do {SETVALUE(_r1, VALUE_FROM_BOOL(_v2 _OP _v3)); DISPATCH_INNER();} while(0) @@ -202,14 +206,14 @@ if (VALUE_ISA_BOOL(v2)) {SETVALUE(r1, VALUE_FROM_BOOL(OP v2.n)); DISPATCH();} // fast math only for INT and FLOAT -#define CHECK_FAST_BINARY_MATH(r1,r2,r3,v2,v3,OP,_CHECK) \ +#define CHECK_FAST_BINARY_MATH(r1,r2,r3,v2,v3,OP,_INTOP,_CHECK) \ DEFINE_STACK_VARIABLE(v2,r2); \ DEFINE_STACK_VARIABLE(v3,r3); \ _CHECK; \ if (VALUE_ISA_INT(v2)) { \ - if (VALUE_ISA_INT(v3)) FMATH_BIN_INT(r1, v2.n, v3.n, OP); \ + if (VALUE_ISA_INT(v3)) FMATH_BIN_INT_OP(r1, v2.n, v3.n, _INTOP); \ if (VALUE_ISA_FLOAT(v3)) FMATH_BIN_FLOAT(r1, v2.n, v3.f, OP); \ - if (VALUE_ISA_NULL(v3)) FMATH_BIN_INT(r1, v2.n, 0, OP); \ + if (VALUE_ISA_NULL(v3)) FMATH_BIN_INT_OP(r1, v2.n, 0, _INTOP); \ if (VALUE_ISA_STRING(v3)) RUNTIME_ERROR("Right operand must be a number (use the number() method)."); \ } else if (VALUE_ISA_FLOAT(v2)) { \ if (VALUE_ISA_FLOAT(v3)) FMATH_BIN_FLOAT(r1, v2.f, v3.f, OP); \ @@ -218,15 +222,15 @@ if (VALUE_ISA_STRING(v3)) RUNTIME_ERROR("Right operand must be a number (use the number() method)."); \ } -#define CHECK_FAST_UNARY_MATH(r1,r2,v2,OP) DEFINE_STACK_VARIABLE(v2,r2); \ - if (VALUE_ISA_INT(v2)) {SETVALUE(r1, VALUE_FROM_INT(OP v2.n)); DISPATCH();} \ +#define CHECK_FAST_UNARY_MATH(r1,r2,v2,OP,_INTOP) DEFINE_STACK_VARIABLE(v2,r2); \ + if (VALUE_ISA_INT(v2)) {SETVALUE(r1, VALUE_FROM_INT(_INTOP(v2.n))); DISPATCH();} \ if (VALUE_ISA_FLOAT(v2)) {SETVALUE(r1, VALUE_FROM_FLOAT(OP v2.f)); DISPATCH();} #define CHECK_FAST_BINARY_REM(r1,r2,r3,v2,v3) DEFINE_STACK_VARIABLE(v2,r2); \ DEFINE_STACK_VARIABLE(v3,r3); \ CHECK_ZERO(v3); \ - if (VALUE_ISA_INT(v2) && VALUE_ISA_INT(v3)) FMATH_BIN_INT(r1, v2.n, v3.n, %) + if (VALUE_ISA_INT(v2) && VALUE_ISA_INT(v3)) FMATH_BIN_INT_OP(r1, v2.n, v3.n, GRAVITY_INT_REM) #define CHECK_FAST_BINARY_BIT(r1,r2,r3,v2,v3,OP) DEFINE_STACK_VARIABLE(v2,r2); \ DEFINE_STACK_VARIABLE(v3,r3); \ diff --git a/src/shared/gravity_value.h b/src/shared/gravity_value.h index 8c54dddc..cce67406 100644 --- a/src/shared/gravity_value.h +++ b/src/shared/gravity_value.h @@ -184,6 +184,27 @@ typedef int32_t gravity_int_t; #define GRAVITY_INT_MIN (-GRAVITY_INT_MAX-1) #endif +// Int arithmetic wraps around on overflow, which is what the runtime operators and the constant +// folder in gravity_optimizer.c have always produced. Signed overflow is undefined behaviour in C +// though, so the wrap has to be done on the unsigned counterpart and converted back: the value is +// unchanged, it is just no longer undefined. Builds compiled with -fsanitize=undefined used to +// trap on a plain `a + b` here. +// Both operands are widened to 64bit so a single definition serves either configuration: with +// GRAVITY_ENABLE_INT64 the unsigned math wraps at 64bit, without it the 32bit operands cannot +// overflow the intermediate and the assignment back to gravity_int_t truncates as before. +// Keep these in sync with the folded results in optimize_const_instruction(). +#define GRAVITY_INT_ADD(a,b) ((int64_t)((uint64_t)(a) + (uint64_t)(b))) +#define GRAVITY_INT_SUB(a,b) ((int64_t)((uint64_t)(a) - (uint64_t)(b))) +#define GRAVITY_INT_MUL(a,b) ((int64_t)((uint64_t)(a) * (uint64_t)(b))) +#define GRAVITY_INT_NEG(a) ((int64_t)(0 - (uint64_t)(a))) + +// GRAVITY_INT_MIN/-1 overflows too, and on x86 it does not just wrap: the idiv instruction +// faults and the process dies with SIGFPE. These mirror the results operator_int_div and +// operator_int_rem already return for those operands, so the VM fast path and the method +// dispatch path agree. Both arguments are evaluated more than once, so pass plain lvalues. +#define GRAVITY_INT_DIV(a,b) ((((a) == GRAVITY_INT_MIN) && ((b) == -1)) ? GRAVITY_INT_MIN : (a) / (b)) +#define GRAVITY_INT_REM(a,b) ((((a) == GRAVITY_INT_MIN) && ((b) == -1)) ? 0 : (a) % (b)) + // Forward references (an object ptr is just its isa pointer) typedef struct gravity_class_s gravity_class_t; typedef struct gravity_class_s gravity_object_t; diff --git a/test/unittest/bugfix_const_folding_float_rem.gravity b/test/unittest/bugfix_const_folding_float_rem.gravity new file mode 100644 index 00000000..99011e4d --- /dev/null +++ b/test/unittest/bugfix_const_folding_float_rem.gravity @@ -0,0 +1,30 @@ +#unittest { + name: "Float constant folding agrees with the runtime remainder operator."; + result: true; +}; + +// every expression below is folded by the optimizer at compile time, so it is +// compared against the same operation evaluated at runtime through rem() +func rem(a, b) { + return a % b; +} + +func main() { + // 0 < |divisor| < 1 truncated to an integer 0 and divided by zero, which is + // a SIGFPE where integer division by zero traps + if (1.0 % 0.5 != rem(1.0, 0.5)) return false; + if (7.5 % 0.25 != rem(7.5, 0.25)) return false; + + // any operand with a fractional part folded to a different value than the + // one the runtime computes + if (2.5 % 2.0 != rem(2.5, 2.0)) return false; + if (9.75 % 4.5 != rem(9.75, 4.5)) return false; + if (-7.5 % 2.0 != rem(-7.5, 2.0)) return false; + + // REM is dispatched on the class of the left operand, so these keep following + // integer semantics even though the right operand is a float + if (5 % 1.5 != rem(5, 1.5)) return false; + if (17 % 5 != rem(17, 5)) return false; + + return true; +} From 60d759cb033dabc28e99860ac813e080624170fc Mon Sep 17 00:00:00 2001 From: Marco Bambini Date: Wed, 5 Aug 2026 14:38:47 +0200 Subject: [PATCH 25/37] ci: add a GitHub Actions build, test and sanitizer workflow Travis is no longer running for this repository. Add a GitHub Actions workflow that builds with gcc and clang on Linux and macOS and runs the unit tests, the -t assertion pass, and the JSON loader tests. A second job builds with -fsanitize=address,undefined and runs the unit tests, the fuzzing corpus and the JSON loader tests through it, so the memory safety fixes stay covered. The unit test timeouts in run_all.sh are calibrated for an optimized build, so the sanitizer job runs the tests directly with a generous per test timeout instead. Also run the -t assertion pass in .travis.yml for parity. Co-Authored-By: Claude Opus 5 --- .github/workflows/build-and-test.yml | 91 ++++++++++++++++++++++++++++ .travis.yml | 1 + 2 files changed, 92 insertions(+) create mode 100644 .github/workflows/build-and-test.yml diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml new file mode 100644 index 00000000..1a10192d --- /dev/null +++ b/.github/workflows/build-and-test.yml @@ -0,0 +1,91 @@ +name: "Build and test" + +on: + push: + branches: [master] + pull_request: + branches: [master] + +jobs: + build: + name: ${{ matrix.os }} / ${{ matrix.cc }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest] + cc: [gcc, clang] + + steps: + - uses: actions/checkout@v4 + + - name: Build + run: make CC=${{ matrix.cc }} + + # run_all.sh runs each file in its own process and only catches crashes and + # timeouts, while -t checks the result declared in every #unittest block + - name: Unit tests + run: test/unittest/run_all.sh + + - name: Unit test assertions + run: ./gravity -t test/unittest + + - name: JSON executable loader tests + run: test/loadbuffer/run_all.sh + + sanitizers: + name: address + undefined sanitizer + runs-on: ubuntu-latest + env: + # CC is used to both compile and link, so the sanitizer flags belong here + SAN_CC: "clang -fsanitize=address,undefined" + UBSAN_OPTIONS: "print_stacktrace=1:halt_on_error=1" + # this job is about out of bounds and undefined behaviour: leak detection is + # left off so that a leak somewhere else cannot mask a memory safety report + ASAN_OPTIONS: "detect_leaks=0" + + steps: + - uses: actions/checkout@v4 + + - name: Build gravity + run: make CC="$SAN_CC" + + - name: Build jsontest + run: make jsontest CC="$SAN_CC" + + # test/unittest/run_all.sh gives each test 0.1s, which is calibrated for an + # -O2 build and too tight once the binary is instrumented, so the unit tests + # are run here directly with a generous per test timeout + - name: Unit tests + run: | + status=0 + for test in $(find test/unittest -name '*.gravity' | grep -v disabled); do + if ! timeout 60 ./gravity "$test"; then + echo "Fail! $test" + status=1 + fi + done + exit $status + + - name: Unit test assertions + run: ./gravity -t test/unittest + + # the fuzzing corpus has no runner of its own: every input must compile and + # run without crashing, which is exactly what a sanitized build checks + - name: Fuzzing corpus + run: | + status=0 + for test in $(find test/fuzzy -name '*.gravity'); do + # || keeps the failure out of set -e, which the default shell enables + res=0 + timeout 60 ./gravity "$test" > /dev/null 2>&1 || res=$? + # a fuzzed input is allowed to be rejected, but never to crash + if [[ $res -ge 128 ]]; then + echo "Fail! $test killed by signal $(($res-128))" + status=1 + fi + done + exit $status + + - name: JSON executable loader tests + run: test/loadbuffer/run_all.sh diff --git a/.travis.yml b/.travis.yml index d7d2a5fd..da9e4642 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,4 +8,5 @@ compiler: script: - make - test/unittest/run_all.sh + - ./gravity -t test/unittest - test/loadbuffer/run_all.sh From 6cbf589a48e41bae3f35a28aeda0dec22d36b933 Mon Sep 17 00:00:00 2001 From: Marco Bambini Date: Wed, 5 Aug 2026 14:45:07 +0200 Subject: [PATCH 26/37] fix: double free of the inline source buffer in the CLI gravity -i built the wrapped source in a heap buffer and passed it to gravity_compiler_run with is_static false. That hands the buffer to the lexer, which frees it in parser_run once parsing is done, so the free in the CLI cleanup path was a second free of the same pointer. The file paths do not hit this because they never free the buffer returned by file_read: only the inline buffer was tracked and freed. Clear the pointer once ownership has passed to the compiler. The free in cleanup stays for the paths that bail out before compilation starts. Co-Authored-By: Claude Opus 5 --- src/cli/gravity.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/cli/gravity.c b/src/cli/gravity.c index f928c0f6..6dceb4c2 100644 --- a/src/cli/gravity.c +++ b/src/cli/gravity.c @@ -464,7 +464,12 @@ int main (int argc, const char* argv[]) { compiler = gravity_compiler_create(&delegate); // compile source code into a closure + // is_static is false, so the lexer takes ownership of source_code and frees it in + // parser_run, whether or not the compilation succeeded: drop our reference to the + // inline buffer here or cleanup would free it a second time closure = gravity_compiler_run(compiler, source_code, size, 0, false, true); + source_code = NULL; + inline_buffer = NULL; if (!closure) goto cleanup; // check if closure needs to be serialized From 1236a3c3c42e7bfbf4830d7a818ad172b32a2b89 Mon Sep 17 00:00:00 2001 From: Marco Bambini Date: Wed, 5 Aug 2026 15:49:02 +0200 Subject: [PATCH 27/37] docs: correct the default bytecode output file name The CLI compiles to gravity.g (DEFAULT_OUTPUT), but the usage text and both README.md and CLAUDE.md still documented the older gravity.json. Print DEFAULT_OUTPUT in the usage text so the two cannot drift again. Also gitignore gravity.g, which a plain gravity -c drops in the working directory. Co-Authored-By: Claude Opus 5 --- .gitignore | 4 ++++ CLAUDE.md | 4 ++-- README.md | 4 ++-- src/cli/gravity.c | 2 +- 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index e399e013..fcc2bff6 100644 --- a/.gitignore +++ b/.gitignore @@ -287,6 +287,10 @@ paket-files/ *.hex gravity jsontest +example + +# Default output of gravity -c +gravity.g # Debug files *.dSYM/ diff --git a/CLAUDE.md b/CLAUDE.md index 75ca6647..016cedf8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -24,8 +24,8 @@ Compiler flags: `-std=gnu99 -fgnu89-inline -fPIC -DBUILD_GRAVITY_API` ./gravity -t test/unittest # Run all unit tests ./test/unittest/run_all.sh # Run all tests via shell script (with timeouts) ./gravity test/unittest/somefile.gravity # Run a single test file -./gravity -c test.gravity # Compile only (produces gravity.json) -./gravity -x gravity.json # Execute compiled bytecode +./gravity -c test.gravity # Compile only (produces gravity.g) +./gravity -x gravity.g # Execute compiled bytecode ./gravity -i 'print("hello")' # Inline execution ``` diff --git a/README.md b/README.md index d5acebed..8b1a234d 100644 --- a/README.md +++ b/README.md @@ -97,9 +97,9 @@ Requires a C99 compiler. No external dependencies. ```bash ./gravity file.gravity # Compile and execute a source file -./gravity -c file.gravity # Compile to bytecode (outputs gravity.json) +./gravity -c file.gravity # Compile to bytecode (outputs gravity.g) ./gravity -o out.json -c file.gravity # Compile to a specific output file -./gravity -x gravity.json # Execute precompiled bytecode +./gravity -x gravity.g # Execute precompiled bytecode ./gravity -i 'return 2 + 3' # Execute inline code ./gravity -t test/unittest # Run unit tests ``` diff --git a/src/cli/gravity.c b/src/cli/gravity.c index 6dceb4c2..86495a7b 100644 --- a/src/cli/gravity.c +++ b/src/cli/gravity.c @@ -270,7 +270,7 @@ static void print_help (void) { printf(" --version show version information and exit\n"); printf(" --help show command line usage and exit\n"); printf(" -c input_file compile input_file\n"); - printf(" -o output_file specify output file name (default to gravity.json)\n"); + printf(" -o output_file specify output file name (default to %s)\n", DEFAULT_OUTPUT); printf(" -x input_file execute input_file (JSON format expected)\n"); printf(" -i source_code compile and execute source_code string\n"); printf(" -q don't print result and execution time\n"); From 970757dec07745cd302147c2c0b6270945c93fd0 Mon Sep 17 00:00:00 2001 From: Marco Bambini Date: Wed, 5 Aug 2026 15:54:55 +0200 Subject: [PATCH 28/37] =?UTF-8?q?Bump=20version=20to=200.9.8=20=E2=80=94?= =?UTF-8?q?=20memory=20safety=20and=20JSON=20loader=20hardening?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Security and memory safety release covering the compiler crash and the bytecode loader crashes reported in #442, #443, #444, #446, #447 and #448, each with a regression test. Co-Authored-By: Claude Opus 5 --- CHANGELOG.md | 27 +++++++++++++++++++++++++++ src/shared/gravity_value.h | 4 ++-- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a18dba80..a5d36d54 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,33 @@ All notable changes to Gravity are documented in this file. +## [0.9.8] - 2026-08-05 + +Security and memory-safety release. Every issue below was found by external +reporters fuzzing the compiler and the bytecode loader, and each fix ships with +a regression test. + +### Fixed +- **NULL dereference in `gravity_vm_loadbuffer`** — a serialized function object without an `identifier` field, such as `{"x":{"type":"function"}}`, reached `strlen(NULL)` and crashed the process. The loader now validates the structure of every JSON executable before using it: the root and each entry must be objects, the identifier must be present exactly once and be a string, and unknown object types are rejected. Malformed input is reported as a load error instead of crashing (issue #444). +- **Signed 64-bit integer overflow in `json_parse_ex`** — the integer and exponent accumulators multiplied by 10 per digit with no range check, so any literal longer than 19 significant digits overflowed. Signed overflow is undefined behaviour: the parser stored a wrapped value, and builds compiled with `-fsanitize=undefined` trapped with SIGILL. Both accumulators are now range-checked and over-long literals are rejected (issue #447). +- **Pointer-arithmetic overflow in the JSON scan loop** — `for (state.ptr = json; ; ++state.ptr)` incremented unconditionally, so input that ended while the scanner was still inside a string or comment advanced the pointer past one-past-the-end, which is undefined behaviour. The loop now stops at the end of the buffer regardless of scanner state (issue #448). +- **Heap out-of-bounds read in `parse_number_expression`** — the `0x`/`0b`/`0o` prefix check read `value[1]` without confirming the token was at least two bytes, so a source file whose last token was a bare `0` read one byte past the buffer. The prefix is only inspected when the token is long enough (issue #446). +- **Compiler crash (SIGFPE) folding a floating-point remainder** — `optimize_const_instruction` folded `%` by truncating both operands to `int64_t`, so any divisor with `0 < |divisor| < 1` became an integer division by zero and killed the compiler on `1 % 0.5`. Float remainder is now folded with `remainder()`, matching `operator_float_rem`, and mixed Int/Float remainders are left to the runtime because REM dispatches on the class of the left operand. This also fixes a silent wrong answer: `5.5 % 2.0` folded to `1` where the VM evaluates `-0.5` (issue #443). +- **Undefined behaviour in Int arithmetic** — Gravity Ints wrap on overflow, but the wrap was performed on signed operands in the VM fast path, in the `operator_int_*` methods and in the constant folder, which is undefined in C and traps under `-fsanitize=undefined`. All three paths now go through new `GRAVITY_INT_ADD/SUB/MUL/NEG/DIV/REM` helpers that compute on the unsigned counterpart. The helpers also handle `GRAVITY_INT_MIN op -1`, which on x86 faults in `idiv` rather than merely wrapping (issue #443). +- **Optional classes never released** — `gravity_core_free` decremented the refcount of the optional classes without the matching balance, so `Math`, `File`, `JSON` and `ENV` were leaked by every embedder that created and destroyed a VM (issue #442). +- **Core reference leaked by every `gravity_compiler_run`** — the compiler took a reference to the core classes on each run and never released it, so the count never returned to zero and the core was never torn down (issue #442). +- **Double free of the inline source buffer** — `gravity -i` passed its heap-allocated wrapper source to `gravity_compiler_run` with `is_static` false, which hands the buffer to the lexer; the lexer freed it in `parser_run` and the CLI freed the same pointer again on the way out, aborting every inline run under a hardened allocator. + +### Added +- `test/loadbuffer/` — a suite of malformed JSON executables that must each be rejected as a load error without crashing, plus `json_bounds.c` (`make jsontest`), 60 bounds checks that drive the JSON scanner directly. Run with `test/loadbuffer/run_all.sh`. +- A GitHub Actions workflow building with gcc and clang on Linux and macOS, and a second job that builds with `-fsanitize=address,undefined` and runs the unit tests, the fuzzing corpus and the loader tests through it. + +### Changed +- Version bumped to **0.9.8** (`GRAVITY_VERSION`, `GRAVITY_VERSION_NUMBER`). +- The usage text now prints the real default output file name, `gravity.g`; `README.md` and `CLAUDE.md` documented a stale `gravity.json`. + +--- + ## [0.9.7] - 2026-04-14 ### Fixed diff --git a/src/shared/gravity_value.h b/src/shared/gravity_value.h index cce67406..249bb548 100644 --- a/src/shared/gravity_value.h +++ b/src/shared/gravity_value.h @@ -66,8 +66,8 @@ extern "C" { #endif -#define GRAVITY_VERSION "0.9.7" // git tag 0.9.7 -#define GRAVITY_VERSION_NUMBER 0x000907 // git push --tags +#define GRAVITY_VERSION "0.9.8" // git tag 0.9.8 +#define GRAVITY_VERSION_NUMBER 0x000908 // git push --tags #define GRAVITY_BUILD_DATE __DATE__ #ifndef GRAVITY_ENABLE_DOUBLE From 3cf4876bdc856bec6ac9d87fcd5b9aee4d4be80a Mon Sep 17 00:00:00 2001 From: Marco Bambini Date: Wed, 5 Aug 2026 16:05:02 +0200 Subject: [PATCH 29/37] fix: mirror the runtime float precision when folding a remainder optimize_const_instruction folded a float REM with remainder() whatever GRAVITY_ENABLE_DOUBLE was set to, while operator_float_rem switches to remainderf() when gravity_float_t is a float. An IEEE remainder is exact, so the two agree in practice, but the folder claimed to match the runtime without mirroring its conditional. Make the claim literally true. No change to the default build, where GRAVITY_ENABLE_DOUBLE is 1. Co-Authored-By: Claude Opus 5 --- src/compiler/gravity_optimizer.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/compiler/gravity_optimizer.c b/src/compiler/gravity_optimizer.c index 4601b653..9a9b3575 100644 --- a/src/compiler/gravity_optimizer.c +++ b/src/compiler/gravity_optimizer.c @@ -349,7 +349,13 @@ static bool optimize_const_instruction (inst_t *inst, inst_t *inst1, inst_t *ins // truncating both operands to int64 instead divided by zero for any // 0 < |d2| < 1, and disagreed with the runtime on every operand with // a fractional part: 2.5 % 2.0 folded to 0 but evaluated to 0.5 + // mirror the runtime conditional rather than relying on an IEEE + // remainder being exact in either precision + #if GRAVITY_ENABLE_DOUBLE d = remainder(d1, d2); + #else + d = (double)remainderf((float)d1, (float)d2); + #endif } else { if (n2 == 0) return false; n = GRAVITY_INT_REM(n1, n2); From 6e8d00e3f59ea9de0f5cc959a7089b73d116903c Mon Sep 17 00:00:00 2001 From: Marco Bambini Date: Wed, 5 Aug 2026 16:34:37 +0200 Subject: [PATCH 30/37] fix: heap buffer overflow in list_storeat when the resize fails Storing past the end of a list reallocates the backing array through marray_resize, which leaves both p and m untouched when the realloc fails. The guard tested list->array.p for NULL, but a failed realloc keeps the old, smaller, non NULL buffer in place, so it never fired. marray_nset then set the count to the requested index and the fill loop wrote past the end of the allocation. Check the capacity actually obtained instead, so the out of memory case is reported as the runtime error it was always meant to be. The fill loop is bounded by that capacity too: it agrees with the old index+MIN_LIST_RESIZE bound for every list the runtime builds today, since they all carry spare capacity, but the loop should not have to rely on that. This is reachable from a script, x[4444444444444444444] = 0 is enough, and it is what several inputs in test/fuzzy do. They were never run in CI before the sanitizer job was added. CI: cap a single allocation at 1GB in the sanitizer job so those inputs fail cleanly rather than pushing the runner into the OOM killer, which is what made the job die with exit code 143. Pin abort_on_error so a finding arrives as a signal on every platform instead of the bare exit code 1 the runtime uses on Linux, which the fuzzing step could not tell apart from an input the interpreter simply rejected, and scan the output for sanitizer reports too. Co-Authored-By: Claude Opus 5 --- .github/workflows/build-and-test.yml | 23 +++++++--- CHANGELOG.md | 10 +++++ src/runtime/gravity_core.c | 12 +++++- .../bugfix_list_storeat_grow_bounds.gravity | 42 +++++++++++++++++++ 4 files changed, 80 insertions(+), 7 deletions(-) create mode 100644 test/unittest/bugfix_list_storeat_grow_bounds.gravity diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 1a10192d..0493fc14 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -39,10 +39,17 @@ jobs: env: # CC is used to both compile and link, so the sanitizer flags belong here SAN_CC: "clang -fsanitize=address,undefined" - UBSAN_OPTIONS: "print_stacktrace=1:halt_on_error=1" + UBSAN_OPTIONS: "print_stacktrace=1:halt_on_error=1:abort_on_error=1" # this job is about out of bounds and undefined behaviour: leak detection is - # left off so that a leak somewhere else cannot mask a memory safety report - ASAN_OPTIONS: "detect_leaks=0" + # left off so that a leak somewhere else cannot mask a memory safety report. + # abort_on_error makes a finding arrive as SIGABRT on every platform, instead of + # the plain exit code 1 the runtime defaults to on Linux, which is indistinguishable + # from an input the interpreter simply rejected. + # Some inputs in test/fuzzy ask for a list index in the 10^9 range, which is a + # single allocation of several GB and enough to push the runner into the OOM + # killer: cap one allocation and let it fail, which the interpreter reports as a + # normal runtime error. Without the cap this job dies with exit code 143 + ASAN_OPTIONS: "detect_leaks=0:abort_on_error=1:allocator_may_return_null=1:max_allocation_size_mb=1024" steps: - uses: actions/checkout@v4 @@ -78,10 +85,16 @@ jobs: for test in $(find test/fuzzy -name '*.gravity'); do # || keeps the failure out of set -e, which the default shell enables res=0 - timeout 60 ./gravity "$test" > /dev/null 2>&1 || res=$? - # a fuzzed input is allowed to be rejected, but never to crash + out=$(timeout 60 ./gravity "$test" 2>&1) || res=$? + # a fuzzed input is allowed to be rejected, and to run out of memory, but + # never to crash and never to trip a sanitizer if [[ $res -ge 128 ]]; then echo "Fail! $test killed by signal $(($res-128))" + echo "$out" | head -40 + status=1 + elif grep -qE 'ERROR: (Address|Undefined|Leak)Sanitizer|runtime error:' <<< "$out"; then + echo "Fail! $test tripped a sanitizer" + echo "$out" | head -40 status=1 fi done diff --git a/CHANGELOG.md b/CHANGELOG.md index a5d36d54..a16d58c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,16 @@ All notable changes to Gravity are documented in this file. +## [Unreleased] + +### Fixed +- **Heap buffer overflow in `list_storeat` when growing the list fails** — storing past the end of a list reallocates the backing array, but `marray_resize` leaves both the pointer and the capacity untouched when the `realloc` fails, so the existing `if (!list->array.p)` guard never fired: the old, smaller buffer is still there and still non-NULL. The count was then set to the requested index and the fill loop wrote well past the end of the allocation. The check now tests the capacity actually obtained, and the out-of-memory case is reported as `Not enough memory to resize List.` as intended. Reachable from a script: `x[4444444444444444444] = 0` asks for a single multi-gigabyte allocation. + +### Changed +- The sanitizer CI job caps a single allocation at 1 GB (`max_allocation_size_mb`) so the pathological allocations in `test/fuzzy` fail cleanly instead of pushing the runner into the OOM killer, and pins `abort_on_error` so a sanitizer finding arrives as a signal on every platform rather than as the bare exit code 1 the runtime defaults to on Linux. The fuzzing step also scans the output for sanitizer reports, which the exit code alone does not reliably convey. + +--- + ## [0.9.8] - 2026-08-05 Security and memory-safety release. Every issue below was found by external diff --git a/src/runtime/gravity_core.c b/src/runtime/gravity_core.c index a419d483..a688ec40 100644 --- a/src/runtime/gravity_core.c +++ b/src/runtime/gravity_core.c @@ -1024,9 +1024,17 @@ static bool list_storeat (gravity_vm *vm, gravity_value_t *args, uint16_t nargs, if ((uint32_t)index >= count) { // handle list resizing here marray_resize(gravity_value_t, list->array, index-count+MIN_LIST_RESIZE); - if (!list->array.p) RETURN_ERROR("Not enough memory to resize List."); + // marray_resize leaves both p and m untouched when the realloc fails, so p is still + // the old, smaller (and non NULL) buffer: checking it for NULL does not detect the + // failure, and the writes below would then run past the end of that allocation. + // Check the capacity actually obtained instead + if (marray_max(list->array) <= (size_t)index) RETURN_ERROR("Not enough memory to resize List."); marray_nset(list->array, index+1); - for (int32_t i=count; i<=(index+MIN_LIST_RESIZE); ++i) { + // fill the gap left by the resize. The bound is the capacity actually obtained + // rather than index+MIN_LIST_RESIZE: the two agree only as long as the array had + // spare capacity before the resize, which holds for every list the runtime builds + // today but is not something this loop should have to depend on + for (size_t i=count; iarray); ++i) { marray_set(list->array, i, VALUE_FROM_NULL); } // value is set unconditionally below diff --git a/test/unittest/bugfix_list_storeat_grow_bounds.gravity b/test/unittest/bugfix_list_storeat_grow_bounds.gravity new file mode 100644 index 00000000..3aa59a7c --- /dev/null +++ b/test/unittest/bugfix_list_storeat_grow_bounds.gravity @@ -0,0 +1,42 @@ +#unittest { + name: "List store-at grows correctly from several starting capacities."; + result: true; +}; + +// list_storeat grows the backing array when the index is past the end, and how +// much spare capacity it has to work with depends on how the list was built. +// Exercise the grow path from starting points that differ in that respect. +// The out of memory branch of the same function cannot be reached from a script: +// it is covered by test/fuzzy under the allocation cap the sanitizer CI job sets. +func check (list, index) { + list[index] = 42; + + if (list.count != index + 1) return false; + if (list[index] != 42) return false; + // everything the resize skipped over has to read back as null + if (list[index - 1] != null) return false; + + return true; +} + +func main() { + // List(n) allocates exactly n, so the array is full before the store + if (!check(List(8), 9)) return false; + if (!check(List(8), 200)) return false; + + // a literal list, then one grown by repeated pushes + if (!check([0, 1, 2], 10)) return false; + + var pushed = []; + for (var i in 0..<40) pushed.push(i); + if (!check(pushed, 500)) return false; + + // growing the same list repeatedly must keep it consistent + var repeated = []; + for (var i in 1...20) { + repeated[i * 7] = 42; + if (repeated.count != i * 7 + 1) return false; + } + + return true; +} From 94ba22869e57857a8e9e1f35e6f9ebbd818f1c21 Mon Sep 17 00:00:00 2001 From: Marco Bambini Date: Wed, 5 Aug 2026 16:44:55 +0200 Subject: [PATCH 31/37] ci: remove the CodeQL workflow, let the compiler check formats instead CodeQL ran on github/codeql-action@v1, deprecated since January 2023 and no longer updated or supported, so it kept reporting green without being a current analysis. It is not a required status check and code scanning default setup is not configured, so the workflow was its only trigger. Before removing it, fix its one open finding: report_error was passing a size_t to a %d conversion in gravity_codegen.c, which reads 32 bits of a 64bit variadic argument. Use %zu. The compiler could not catch that because report_error only forwards its arguments to vsnprintf. Annotate all three of them with the printf format attribute on gcc and clang, so every call site is type checked from now on and this class fails the build rather than needing an external analyser. The tree is clean under it today. Co-Authored-By: Claude Opus 5 --- .github/workflows/codeql-analysis.yml | 71 --------------------------- CHANGELOG.md | 4 ++ src/compiler/gravity_codegen.c | 7 ++- src/compiler/gravity_semacheck1.c | 3 ++ src/compiler/gravity_semacheck2.c | 3 ++ 5 files changed, 16 insertions(+), 72 deletions(-) delete mode 100644 .github/workflows/codeql-analysis.yml diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml deleted file mode 100644 index c7502db6..00000000 --- a/.github/workflows/codeql-analysis.yml +++ /dev/null @@ -1,71 +0,0 @@ -# For most projects, this workflow file will not need changing; you simply need -# to commit it to your repository. -# -# You may wish to alter this file to override the set of languages analyzed, -# or to provide custom queries or build logic. -name: "CodeQL" - -on: - push: - branches: [master] - pull_request: - # The branches below must be a subset of the branches above - branches: [master] - schedule: - - cron: '0 18 * * 4' - -jobs: - analyze: - name: Analyze - runs-on: ubuntu-latest - - strategy: - fail-fast: false - matrix: - # Override automatic language detection by changing the below list - # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python'] - language: ['cpp'] - # Learn more... - # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection - - steps: - - name: Checkout repository - uses: actions/checkout@v2 - with: - # We must fetch at least the immediate parents so that if this is - # a pull request then we can checkout the head. - fetch-depth: 2 - - # If this run was triggered by a pull request event, then checkout - # the head of the pull request instead of the merge commit. - - run: git checkout HEAD^2 - if: ${{ github.event_name == 'pull_request' }} - - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v1 - with: - languages: ${{ matrix.language }} - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. - # queries: ./path/to/local/query, your-org/your-repo/queries@main - - # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). - # If this step fails, then you should remove it and run the build manually (see below) - - name: Autobuild - uses: github/codeql-action/autobuild@v1 - - # ℹ️ Command-line programs to run using the OS shell. - # 📚 https://git.io/JvXDl - - # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines - # and modify them (or add more) to build your code if your project - # uses a compiled language - - #- run: | - # make bootstrap - # make release - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 diff --git a/CHANGELOG.md b/CHANGELOG.md index a16d58c1..4ff1717a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,8 +5,12 @@ All notable changes to Gravity are documented in this file. ## [Unreleased] ### Fixed +- **Wrong format specifier in a codegen error message** — `report_error(..., "Invalid argument expression at index %d.", j+1)` passed a `size_t` to a `%d` conversion, which reads only 32 bits of a 64-bit argument: undefined behaviour in a variadic call. The three `report_error` helpers now carry `__attribute__((format(printf, ...)))` on gcc and clang, so the compiler type-checks every call site and this class of mistake fails the build instead of needing an external analyser to spot it. - **Heap buffer overflow in `list_storeat` when growing the list fails** — storing past the end of a list reallocates the backing array, but `marray_resize` leaves both the pointer and the capacity untouched when the `realloc` fails, so the existing `if (!list->array.p)` guard never fired: the old, smaller buffer is still there and still non-NULL. The count was then set to the requested index and the fill loop wrote well past the end of the allocation. The check now tests the capacity actually obtained, and the out-of-memory case is reported as `Not enough memory to resize List.` as intended. Reachable from a script: `x[4444444444444444444] = 0` asks for a single multi-gigabyte allocation. +### Removed +- The CodeQL workflow. It ran on `github/codeql-action@v1`, deprecated since January 2023 and no longer updated, so it kept reporting green without being a current analysis. Its one open finding is fixed above, and the compiler now checks that class directly. The `build-and-test` workflow, including its address + undefined sanitizer job, is unaffected. + ### Changed - The sanitizer CI job caps a single allocation at 1 GB (`max_allocation_size_mb`) so the pathological allocations in `test/fuzzy` fail cleanly instead of pushing the runner into the OOM killer, and pins `abort_on_error` so a sanitizer finding arrives as a signal on every platform rather than as the bare exit code 1 the runtime defaults to on Linux. The fuzzing step also scans the output for sanitizer reports, which the exit code alone does not reliably convey. diff --git a/src/compiler/gravity_codegen.c b/src/compiler/gravity_codegen.c index 54654fe7..49cf7de7 100644 --- a/src/compiler/gravity_codegen.c +++ b/src/compiler/gravity_codegen.c @@ -67,6 +67,11 @@ typedef struct codegen_t codegen_t; } while(0) // MARK: - +// the format attribute lets the compiler type check the variadic arguments, which it +// cannot do on its own for a function that just forwards them to vsnprintf +#if defined(__GNUC__) || defined(__clang__) +__attribute__((format(printf, 3, 4))) +#endif static void report_error (gvisitor_t *self, gnode_t *node, const char *format, ...) { codegen_t *current = (codegen_t *)self->data; @@ -1552,7 +1557,7 @@ static void visit_postfix_expr (gvisitor_t *self, gnode_postfix_expr_t *node) { VISIT_MOVE_OPT(arg); uint32_t nreg = ircode_register_pop_context_protect(code, true); if (nreg == REGISTER_ERROR) { - report_error(self, (gnode_t *)arg, "Invalid argument expression at index %d.", j+1); + report_error(self, (gnode_t *)arg, "Invalid argument expression at index %zu.", j+1); goto cleanup; } diff --git a/src/compiler/gravity_semacheck1.c b/src/compiler/gravity_semacheck1.c index e3ed082e..f4947e23 100644 --- a/src/compiler/gravity_semacheck1.c +++ b/src/compiler/gravity_semacheck1.c @@ -29,6 +29,9 @@ static int ident =0; // MARK: - +#if defined(__GNUC__) || defined(__clang__) +__attribute__((format(printf, 3, 4))) +#endif static void report_error (gvisitor_t *self, gnode_t *node, const char *format, ...) { // TODO: add lasterror here like in semacheck2 diff --git a/src/compiler/gravity_semacheck2.c b/src/compiler/gravity_semacheck2.c index 324f3ee0..6130bc0a 100644 --- a/src/compiler/gravity_semacheck2.c +++ b/src/compiler/gravity_semacheck2.c @@ -48,6 +48,9 @@ typedef struct semacheck_t semacheck_t; // MARK: - +#if defined(__GNUC__) || defined(__clang__) +__attribute__((format(printf, 4, 5))) +#endif static void report_error (gvisitor_t *self, error_type_t error_type, gnode_t *node, const char *format, ...) { semacheck_t *current = (semacheck_t *)self->data; From daefac8a9778b32327779bbe5f59c5f1a944656f Mon Sep 17 00:00:00 2001 From: Marco Bambini Date: Wed, 5 Aug 2026 17:19:12 +0200 Subject: [PATCH 32/37] fix: count a CR+LF pair as a single line break in the lexer is_newline() looked at PEEK_CURRENT for the character following c, which only holds where the caller had already consumed c (the comment scanner and skip_line). In gravity_lexer_next, lexer_scan_number and lexer_scan_string c was still the character at the current offset, so the CR of a CR+LF pair never saw the LF next to it: the pair was counted as two line breaks and every row the compiler reported drifted by one per line read so far. On a source file saved on Windows an error on row 5 was reported on row 9. The lookahead is now passed in explicitly, so each call site says which two characters follow the one being examined, and newline_length() returns the size of the terminator without consuming anything. The two callers that still hold the offset skip the whole sequence themselves, and the string scanner keeps every byte of it inside the token so that a literal spanning CR+LF lines is not shortened by one byte per line. NEL and LS are left exactly as they were: PEEK_NEXT and PEEK_NEXT2 read plain char, so their comparisons are still false where char is signed. That is pre-existing and out of scope here. Reported in #389, and this is the fix proposed in #401 by Matthew Asplund (mwasplund) with the string and number scanners corrected. Co-Authored-By: Claude Opus 5 --- .gitattributes | 4 ++ src/compiler/gravity_lexer.c | 80 +++++++++++++++++------- test/unittest/bugfix_crlf_lineno.gravity | 18 ++++++ 3 files changed, 81 insertions(+), 21 deletions(-) create mode 100644 test/unittest/bugfix_crlf_lineno.gravity diff --git a/.gitattributes b/.gitattributes index 34fc246d..7ea96794 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,6 @@ *.gravity linguist-language=swift *.gravity linguist-vendored + +# this test is about how CR+LF line endings are counted, so it has to reach the +# working directory byte for byte, whatever core.autocrlf is set to +test/unittest/bugfix_crlf_lineno.gravity -text diff --git a/src/compiler/gravity_lexer.c b/src/compiler/gravity_lexer.c index 9f79f132..392d0adb 100644 --- a/src/compiler/gravity_lexer.c +++ b/src/compiler/gravity_lexer.c @@ -73,36 +73,42 @@ static bool is_whitespace (int c) { return ((c == ' ') || (c == '\t') || (c == '\v') || (c == '\f')); } -static bool is_newline (gravity_lexer_t *lexer, int c) { +// Length in bytes of the line terminator that starts at c, or 0 when c does not start one. +// n and n2 are the two characters that follow c in the buffer: the caller must supply them +// because it is not always the case that c is still the character sitting at lexer->offset. +// Nothing is consumed here, so every caller stays in charge of how far it advances, which +// is what keeps the line counter, the offset and the token length in sync. +static uint32_t newline_length (int c, int n, int n2) { // CR: Carriage Return, U+000D (UTF-8 in hex: 0D) // LF: Line Feed, U+000A (UTF-8 in hex: 0A) // CR+LF: CR (U+000D) followed by LF (U+000A) (UTF-8 in hex: 0D0A) // LF - if (c == 0x0A) return true; + if (c == 0x0A) return 1; // CR+LF or CR - if (c == 0x0D) { - if (PEEK_CURRENT == 0x0A) {NEXT;} - return true; - } + if (c == 0x0D) return (n == 0x0A) ? 2 : 1; // UTF-8 cases https://en.wikipedia.org/wiki/Newline#Unicode // NEL: Next Line, U+0085 (UTF-8 in hex: C285) - if ((c == 0xC2) && (PEEK_CURRENT == 0x85)) { - NEXT; - return true; - } + if ((c == 0xC2) && (n == 0x85)) return 2; // LS: Line Separator, U+2028 (UTF-8 in hex: E280A8) - if ((c == 0xE2) && (PEEK_CURRENT == 0x80) && (PEEK_NEXT == 0xA8)) { - NEXT; NEXT; - return true; - } + if ((c == 0xE2) && (n == 0x80) && (n2 == 0xA8)) return 3; // and probably more not handled here - return false; + return 0; +} + +// Variant for the callers that have already consumed c: it eats the remaining bytes of a +// multi byte terminator so that the lexer is left sitting right after it. +static bool is_newline (gravity_lexer_t *lexer, int c, int n, int n2) { + uint32_t nlen = newline_length(c, n, n2); + if (nlen == 0) return false; + + for (uint32_t i = 1; i < nlen; ++i) {NEXT;} + return true; } static bool is_comment (int c1, int c2) { @@ -209,14 +215,15 @@ static gtoken_t lexer_scan_comment(gravity_lexer_t *lexer) { int c = 0; next_utf8(lexer, &c); + // c has just been consumed, so the characters that follow it start at the current offset if (isLineComment){ - if (is_newline(lexer, c)) {INC_LINE; break;} + if (is_newline(lexer, c, PEEK_CURRENT, PEEK_NEXT)) {INC_LINE; break;} } else { if (IS_EOF) break; int c2 = PEEK_CURRENT; if ((c == '/') && (c2 == '*')) ++count; if ((c == '*') && (c2 == '/')) {--count; NEXT; INC_TOKLEN; if (count == 0) break;} - if (is_newline(lexer, c)) {INC_LINE;} + if (is_newline(lexer, c, c2, PEEK_NEXT)) {INC_LINE;} } } @@ -304,7 +311,9 @@ static gtoken_t lexer_scan_number(gravity_lexer_t *lexer) { if (IS_EOF) goto report_token; if (is_digit(c, ntype)) goto accept_char; if (is_whitespace(c)) goto report_token; - if (is_newline(lexer, c)) goto report_token; + // c is still the character at the current offset and the terminator is not part of the + // number: leave it in place, gravity_lexer_next is the one that counts and skips it + if (newline_length(c, PEEK_NEXT, PEEK_NEXT2)) goto report_token; if (expAllowed) { if ((c == expChar) && (!expFound)) {expFound = true; signAllowed = true; goto accept_char;} @@ -347,7 +356,21 @@ static gtoken_t lexer_scan_string(gravity_lexer_t *lexer) { while ((c2 = (unsigned char)PEEK_CURRENT) != c) { if (IS_EOF) return lexer_error(lexer, "Unexpected EOF inside a string literal"); - if (is_newline(lexer, c2)) INC_LINE; + + // a line terminator inside a string literal is part of the literal, so its bytes are + // consumed here (and counted into the token) instead of being left to next_utf8: a + // CR+LF pair must advance the line counter once but still contribute both its bytes + uint32_t nlen = newline_length(c2, PEEK_NEXT, PEEK_NEXT2); + if (nlen) { + for (uint32_t i = 0; i < nlen; ++i) {INC_OFFSET; INC_TOKBYTES;} + // CR+LF is two characters, NEL and LS are one + uint32_t nchars = ((c2 == 0x0D) && (nlen == 2)) ? 2 : 1; + for (uint32_t i = 0; i < nchars; ++i) {INC_POSITION; INC_TOKUTF8LEN;} + INC_LINE; + + if (IS_EOF) return lexer_error(lexer, "Unexpected EOF inside a string literal"); + continue; + } // handle escaped characters if (c2 == '\\') { @@ -569,6 +592,7 @@ gtoken_t gravity_lexer_peek (gravity_lexer_t *lexer) { gtoken_t gravity_lexer_next (gravity_lexer_t *lexer) { int c; + uint32_t nlen; gtoken_t token; // reset cached value @@ -579,7 +603,20 @@ gtoken_t gravity_lexer_next (gravity_lexer_t *lexer) { c = PEEK_CURRENT; if (is_whitespace(c)) {INC_OFFSET_POSITION; goto loop;} - if (is_newline(lexer, c)) {INC_OFFSET_POSITION; INC_LINE; goto loop;} + + // c is still the character at the current offset, so the two that follow it are + // PEEK_NEXT and PEEK_NEXT2. Skipping the whole terminator in one go is what keeps a + // CR+LF pair a single line: reading it as two separate breaks is issue #389 + nlen = newline_length(c, PEEK_NEXT, PEEK_NEXT2); + if (nlen) { + for (uint32_t i = 0; i < nlen; ++i) {INC_OFFSET;} + // CR+LF is two characters, NEL and LS are one + INC_POSITION; + if ((c == 0x0D) && (nlen == 2)) INC_POSITION; + INC_LINE; + goto loop; + } + if (is_comment(c, PEEK_NEXT)) {lexer_scan_comment(lexer); goto loop;} if (is_semicolon(c)) {token = lexer_scan_semicolon(lexer); goto return_result;} @@ -622,7 +659,8 @@ void gravity_lexer_skip_line (gravity_lexer_t *lexer) { while (!IS_EOF) { int c = 0; next_utf8(lexer, &c); - if (is_newline(lexer, c)) { + // c has just been consumed, so the characters that follow it start at the current offset + if (is_newline(lexer, c, PEEK_CURRENT, PEEK_NEXT)) { INC_LINE; break; } diff --git a/test/unittest/bugfix_crlf_lineno.gravity b/test/unittest/bugfix_crlf_lineno.gravity new file mode 100644 index 00000000..53f86032 --- /dev/null +++ b/test/unittest/bugfix_crlf_lineno.gravity @@ -0,0 +1,18 @@ +#unittest { + name: "Row reported with CR+LF line endings (issue #389)."; + error: SEMANTIC; + error_row: 17; + error_col: 17; +}; + +// This file is stored with CR+LF line endings on purpose, see .gitattributes. +// A CR+LF pair is a single line break: counting the CR and the LF as two of +// them used to shift every row the compiler reports, by one per line read so +// far, which made error messages on Windows sources point nowhere near the +// offending statement. + +func main() { + var a = 1; + var b = 2; + return a + b + undefined_symbol_here; +} From a8ea371eec165e558c39016edaa4ccdeeb8cb8bd Mon Sep 17 00:00:00 2001 From: Marco Bambini Date: Wed, 5 Aug 2026 17:22:11 +0200 Subject: [PATCH 33/37] build: add a staticlib target to the Makefile lib already builds the shared library out of $(OBJ), which is every source but the CLI entry point. staticlib archives the same objects, so embedders who link gravity statically no longer have to drive ar by hand. Requested in #427 by Jock Murphy (jockm). The clean fix from that pull request is already in: clean removes the shared library under each of its platform specific names. Co-Authored-By: Claude Opus 5 --- CLAUDE.md | 1 + Makefile | 11 +++++++++-- README.md | 1 + 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 016cedf8..f62ab438 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -12,6 +12,7 @@ Gravity is a dynamically typed, embeddable programming language written in porta make # Build the gravity CLI executable make mode=debug # Debug build (-g -O0 -DDEBUG) make lib # Build shared library (libgravity.dylib/so/dll) +make staticlib # Build static library (libgravity.a) make example # Build the C API example make clean # Clean all build artifacts ``` diff --git a/Makefile b/Makefile index d3077742..05561297 100644 --- a/Makefile +++ b/Makefile @@ -22,6 +22,9 @@ EXAMPLE_OBJ = $(EXAMPLE_SRC:.c=.o) JSONTEST_OBJ = $(JSONTEST_SRC:.c=.o) DEP = $(OBJ:.o=.d) $(GRAVITY_OBJ:.o=.d) $(EXAMPLE_OBJ:.o=.d) $(JSONTEST_OBJ:.o=.d) +# the static library has the same name everywhere, only the shared one is platform specific +SLIBTARGET = libgravity.a + ifeq ($(OS),Windows_NT) # Windows LIBTARGET = gravity.dll @@ -78,9 +81,13 @@ jsontest: $(OBJ) $(JSONTEST_OBJ) lib: $(OBJ) $(CC) -shared -o $(LIBTARGET) $(OBJ) $(LDFLAGS) +# static counterpart of lib: same objects, so the CLI entry point is left out here too +staticlib: $(OBJ) + $(AR) rcs $(SLIBTARGET) $(OBJ) + clean: - rm -f $(OBJ) $(GRAVITY_OBJ) $(EXAMPLE_OBJ) $(JSONTEST_OBJ) $(DEP) gravity example jsontest libgravity.dylib libgravity.so gravity.dll + rm -f $(OBJ) $(GRAVITY_OBJ) $(EXAMPLE_OBJ) $(JSONTEST_OBJ) $(DEP) gravity example jsontest libgravity.dylib libgravity.so $(SLIBTARGET) gravity.dll -.PHONY: all clean lib +.PHONY: all clean lib staticlib -include $(DEP) diff --git a/README.md b/README.md index 8b1a234d..8cf101d1 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,7 @@ func main() { make # Build the gravity CLI executable make mode=debug # Debug build with symbols make lib # Build shared library (libgravity.dylib/so/dll) +make staticlib # Build static library (libgravity.a) make example # Build the C embedding API example make clean # Clean all build artifacts ``` From 40a26f2c81051af00544e7a667ff5b1084b9937c Mon Sep 17 00:00:00 2001 From: Marco Bambini Date: Wed, 5 Aug 2026 17:22:24 +0200 Subject: [PATCH 34/37] fix: guard the Windows code paths with _WIN32, not WIN32 gravity_utils.h picks DIRREF and windows.h on _WIN32, but gravity_utils.c, gravity_opt_file.c and the CLI guarded their Windows bodies with WIN32, which is not a compiler defined macro. The Visual Studio projects define it in the two 32 bit configurations of gravity.vcxproj alone, so every x64 build compiled the POSIX bodies against the Windows header: opendir and readdir do not exist under MSVC and DIRREF is a HANDLE there, so the two halves cannot even agree on a type. MinGW and tcc land in the same place. _WIN32 is defined by MSVC, MinGW and tcc on both 32 and 64 bit, and is what the header already tests. WIN32_FIND_DATA and friends are Windows API type names and are left alone. From #411 by tDwtp, which is the pull request that reported it. Co-Authored-By: Claude Opus 5 --- src/cli/gravity.c | 2 +- src/optionals/gravity_opt_file.c | 2 +- src/utils/gravity_utils.c | 20 ++++++++++---------- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/cli/gravity.c b/src/cli/gravity.c index 86495a7b..456eb7dc 100644 --- a/src/cli/gravity.c +++ b/src/cli/gravity.c @@ -175,7 +175,7 @@ static const char *unittest_read (const char *path, size_t *size, uint32_t *file static void unittest_scan (const char *folder_path, unittest_data *data) { DIRREF dir = directory_init(folder_path); if (!dir) return; - #ifdef WIN32 + #ifdef _WIN32 char outbuffer[MAX_PATH]; #else char *outbuffer = NULL; diff --git a/src/optionals/gravity_opt_file.c b/src/optionals/gravity_opt_file.c index 62a29f33..0802296c 100644 --- a/src/optionals/gravity_opt_file.c +++ b/src/optionals/gravity_opt_file.c @@ -175,7 +175,7 @@ static void scan_directory (gravity_vm *vm, char *path, bool recursive, gravity_ if (n) *n = *n + 1; } - #ifdef WIN32 + #ifdef _WIN32 char buffer[MAX_PATH]; #else char *buffer = NULL; diff --git a/src/utils/gravity_utils.c b/src/utils/gravity_utils.c index a428903b..15c02abc 100644 --- a/src/utils/gravity_utils.c +++ b/src/utils/gravity_utils.c @@ -95,7 +95,7 @@ double millitime (nanotime_t tstart, nanotime_t tend) { // MARK: - I/O Functions - int64_t file_size (const char *path) { - #ifdef WIN32 + #ifdef _WIN32 WIN32_FILE_ATTRIBUTE_DATA fileInfo; if (GetFileAttributesExA(path, GetFileExInfoStandard, (void*)&fileInfo) == 0) return -1; return (int64_t)(((__int64)fileInfo.nFileSizeHigh) << 32 ) + fileInfo.nFileSizeLow; @@ -107,7 +107,7 @@ int64_t file_size (const char *path) { } bool file_exists (const char *path) { - #ifdef WIN32 + #ifdef _WIN32 if (GetFileAttributesA(path) != INVALID_FILE_ATTRIBUTES) return true; #else if (access(path, F_OK) == 0) return true; @@ -117,7 +117,7 @@ bool file_exists (const char *path) { } bool file_delete (const char *path) { - #ifdef WIN32 + #ifdef _WIN32 return DeleteFileA(path); #else if (unlink(path) == 0) return true; @@ -136,7 +136,7 @@ char *file_read(const char *path, size_t *len) { if (fsize < 0) goto abort_read; int oflags = O_RDONLY; - #ifdef WIN32 + #ifdef _WIN32 // Only Windows needs to understand the difference between text and binary, so only Windows defines O_BINARY oflags |= O_BINARY; #endif @@ -185,7 +185,7 @@ char *file_buildpath (const char *filename, const char *dirpath) { char *full_path = (char *)mem_alloc(NULL, len); if (!full_path) return NULL; - #ifdef WIN32 + #ifdef _WIN32 PathCombineA(full_path, dirpath, filename); #else // check if PATH_SEPARATOR exists in dirpath @@ -222,7 +222,7 @@ char *file_name_frompath (const char *path) { // MARK: - Directory Functions - bool is_directory (const char *path) { - #ifdef WIN32 + #ifdef _WIN32 DWORD dwAttrs = GetFileAttributesA(path); if (dwAttrs == INVALID_FILE_ATTRIBUTES) return false; if (dwAttrs & FILE_ATTRIBUTE_DIRECTORY) return true; @@ -237,7 +237,7 @@ bool is_directory (const char *path) { } bool directory_create (const char *path) { - #ifdef WIN32 + #ifdef _WIN32 CreateDirectoryA(path, NULL); #else mode_t saved = umask(0); @@ -249,7 +249,7 @@ bool directory_create (const char *path) { } DIRREF directory_init (const char *dirpath) { - #ifdef WIN32 + #ifdef _WIN32 WIN32_FIND_DATAW findData; WCHAR path[MAX_PATH]; WCHAR dirpathW[MAX_PATH]; @@ -272,7 +272,7 @@ char *directory_read (DIRREF ref, char *win32buffer) { if (ref == NULL) return NULL; while (1) { - #ifdef WIN32 + #ifdef _WIN32 WIN32_FIND_DATAA findData; if (FindNextFileA(ref, &findData) == 0) { @@ -304,7 +304,7 @@ char *directory_read_extend (DIRREF ref, char *win32buffer) { if (ref == NULL) return NULL; while (1) { - #ifdef WIN32 + #ifdef _WIN32 WIN32_FIND_DATAA findData; if (FindNextFileA(ref, &findData) == 0) { From ff0bf657ed1cf9a50d0de60e4d1f93e9e83ef20c Mon Sep 17 00:00:00 2001 From: SpectralDragon Date: Wed, 26 Aug 2026 23:20:39 +0300 Subject: [PATCH 35/37] Update SwiftSyntax for Swift 6.2 --- Package.resolved | 6 +++--- Package.swift | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Package.resolved b/Package.resolved index 49d2bdae..5756e5be 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,13 +1,13 @@ { - "originHash" : "16dbe5d9ebceca9abf9a60de3cddddfc02b9519722f35044b3def7d42da25808", + "originHash" : "9e524592b3010690e3c0227fc3d8562f9649012634423deadc3ff624ea6f1eb3", "pins" : [ { "identity" : "swift-syntax", "kind" : "remoteSourceControl", "location" : "https://github.com/swiftlang/swift-syntax", "state" : { - "revision" : "0687f71944021d616d34d922343dcef086855920", - "version" : "600.0.1" + "revision" : "4799286537280063c85a32f09884cfbca301b1a1", + "version" : "602.0.0" } } ], diff --git a/Package.swift b/Package.swift index 279dbda2..3fbe6993 100644 --- a/Package.swift +++ b/Package.swift @@ -19,7 +19,7 @@ let package = Package( ) ], dependencies: [ - .package(url: "https://github.com/swiftlang/swift-syntax", from: "600.0.1") + .package(url: "https://github.com/swiftlang/swift-syntax", from: "602.0.0") ], targets: [ .executableTarget( From c0f827a7aa14e15b13d8f8712f9c76116cbe242a Mon Sep 17 00:00:00 2001 From: SpectralDragon Date: Thu, 27 Aug 2026 01:12:03 +0300 Subject: [PATCH 36/37] Fix bridged instance cleanup during VM teardown --- .../GravityVirtualMachineTests.swift | 19 +++++++++++++++++++ .../GravityVirtualMachine+Bridge.swift | 11 ++++++++++- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/Tests/GravityTests/GravityVirtualMachineTests.swift b/Tests/GravityTests/GravityVirtualMachineTests.swift index 96b557fb..5a34e83b 100644 --- a/Tests/GravityTests/GravityVirtualMachineTests.swift +++ b/Tests/GravityTests/GravityVirtualMachineTests.swift @@ -42,8 +42,27 @@ struct GravityVirtualMachineTests { #expect(delegate.errors.isEmpty) #expect(result.toInteger == 42) } + + @Test("Releases bridged Swift instances during VM teardown") + func releasesBridgedInstancesDuringTeardown() throws { + let delegate = TestVirtualMachineDelegate() + weak var releasedObject: TeardownProbe? + + do { + let virtualMachine = GravityVirtualMachine(settings: .init(), delegate: delegate) + try virtualMachine.bindClass(with: TeardownProbe.self) + let object = TeardownProbe() + releasedObject = object + virtualMachine.setValue(object, forKey: "probe") + } + + #expect(releasedObject == nil) + } } +@GSExportable +private final class TeardownProbe {} + private final class TestVirtualMachineDelegate: GravityVirtualMachineDelegate { private(set) var errors: [String] = [] diff --git a/binding/GravitySwift/GravityVirtualMachine+Bridge.swift b/binding/GravitySwift/GravityVirtualMachine+Bridge.swift index e0b0b4b7..534b6213 100644 --- a/binding/GravitySwift/GravityVirtualMachine+Bridge.swift +++ b/binding/GravitySwift/GravityVirtualMachine+Bridge.swift @@ -39,7 +39,16 @@ func errorCallback( } func bridgeFree(_ vmPointer: OpaquePointer?, objptr: UnsafeMutablePointer?) { - guard let vm = GravityVirtualMachine.getVM(vmPointer!) else { fatalError("Cannot found Virtual Machine") } + guard let vmPointer, let objptr else { + return + } + guard let vm = GravityVirtualMachine.getVM(vmPointer) else { + let value = gravity_value_from_object(objptr) + if let xData = gravity_value_xdata(value) { + Unmanaged.fromOpaque(xData).release() + } + return + } let value = GSValue(object: objptr, in: vm) if let delegate = vm.delegate as? GravityMemoryControlVMDelegate { From 0c239828ebeb5ffd0b03b7a914ac5dd4e4c2c781 Mon Sep 17 00:00:00 2001 From: SpectralDragon Date: Thu, 27 Aug 2026 01:16:10 +0300 Subject: [PATCH 37/37] Pass existing Gravity values through bridge methods --- .../GravityVirtualMachineTests.swift | 27 +++++++++++++++++++ binding/GravitySwift/GSValue.swift | 2 ++ 2 files changed, 29 insertions(+) diff --git a/Tests/GravityTests/GravityVirtualMachineTests.swift b/Tests/GravityTests/GravityVirtualMachineTests.swift index 5a34e83b..9eefee4b 100644 --- a/Tests/GravityTests/GravityVirtualMachineTests.swift +++ b/Tests/GravityTests/GravityVirtualMachineTests.swift @@ -58,11 +58,38 @@ struct GravityVirtualMachineTests { #expect(releasedObject == nil) } + + @Test("Returns an existing Gravity value from a Swift method") + func returnsGravityValueFromSwiftMethod() throws { + let delegate = TestVirtualMachineDelegate() + let virtualMachine = GravityVirtualMachine(settings: .init(), delegate: delegate) + try virtualMachine.bindClass(with: ValueEcho.self) + virtualMachine.setValue(ValueEcho(), forKey: "echo") + let binary = virtualMachine.loadGravityFile(from: """ + extern var echo; + + func main() { + return echo.value([40, 2])[1]; + } + """) + + let result = try #require(virtualMachine.execute(binary)) + + #expect(delegate.errors.isEmpty) + #expect(result.toInteger == 2) + } } @GSExportable private final class TeardownProbe {} +@GSExportable +private final class ValueEcho { + func value(_ value: GSValue) -> GSValue { + value + } +} + private final class TestVirtualMachineDelegate: GravityVirtualMachineDelegate { private(set) var errors: [String] = [] diff --git a/binding/GravitySwift/GSValue.swift b/binding/GravitySwift/GSValue.swift index 3adfecb8..e375105d 100644 --- a/binding/GravitySwift/GSValue.swift +++ b/binding/GravitySwift/GSValue.swift @@ -78,6 +78,8 @@ public extension GSValue { self.init(double: double, in: vm) } else if let bool = object as? Bool { self.init(boolean: bool, in: vm) + } else if let value = object as? GSValue { + self.init(value: value.value, in: vm) } else if let exportType = object as? GSExportable & AnyObject { self.init(value: exportType, in: vm) } else if let exportType = object as? GSExportable {