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/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml new file mode 100644 index 00000000..0493fc14 --- /dev/null +++ b/.github/workflows/build-and-test.yml @@ -0,0 +1,104 @@ +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: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. + # 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 + + - 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 + 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 + exit $status + + - name: JSON executable loader tests + run: test/loadbuffer/run_all.sh 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/.gitignore b/.gitignore index c913d716..976acc1c 100644 --- a/.gitignore +++ b/.gitignore @@ -286,6 +286,11 @@ paket-files/ *.x86_64 *.hex gravity +jsontest +example + +# Default output of gravity -c +gravity.g # Debug files *.dSYM/ @@ -325,6 +330,7 @@ binding/GravityObjC/GravityObjC.xcodeproj/project.xcworkspace/xcshareddata/IDEWo gravity.xcodeproj/xcuserdata/marco.xcuserdatad/xcschemes/gravity.xcscheme *.xcscheme *.xcscheme - .build/ .swiftpm/ +gravity.xcodeproj/xcuserdata/marco.xcuserdatad/xcschemes/gravity.xcscheme +*.d diff --git a/.travis.yml b/.travis.yml index ef1fb47d..da9e4642 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,3 +8,5 @@ compiler: script: - make - test/unittest/run_all.sh + - ./gravity -t test/unittest + - test/loadbuffer/run_all.sh diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 00000000..6fb7748f --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,1595 @@ +# 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/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..4ff1717a --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,249 @@ +# Changelog + +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. + +--- + +## [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 +- **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 +- **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 new file mode 100644 index 00000000..f62ab438 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,75 @@ +# 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 staticlib # Build static library (libgravity.a) +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.g) +./gravity -x gravity.g # Execute compiled bytecode +./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) +- `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 && test/loadbuffer/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 +- `gravity_vm_get/gravity_vm_set` — read/write VM configuration at runtime (e.g. `GRAVITY_VM_MAXSTACK` to cap fiber stack growth) + +## 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/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/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 diff --git a/Makefile b/Makefile index 20298e62..05561297 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) \ @@ -16,8 +17,14 @@ 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) +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 @@ -59,18 +66,28 @@ 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 +# 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) +# 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) $(DEP) gravity example 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 gravity example +.PHONY: all clean lib staticlib -include $(DEP) 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 db82654b..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( @@ -53,6 +53,11 @@ let package = Package( .product(name: "SwiftCompilerPlugin", package: "swift-syntax") ], path: "binding/GravitySwiftMacros" + ), + .testTarget( + name: "GravityTests", + dependencies: ["Gravity"], + path: "Tests/GravityTests" ) ], cLanguageStandard: .gnu99, diff --git a/README.md b/README.md index 781c6d2f..8cf101d1 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

@@ -8,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 @@ -54,7 +52,7 @@ func main() { ``` ## Features -* multipass compiler +* multipass compiler with optimizer * dynamic typing * classes and inheritance * higher-order functions and classes @@ -62,25 +60,136 @@ 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 + +**Make (Linux / macOS / BSD)** +```bash +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 +``` + +**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 + +```bash +./gravity file.gravity # Compile and execute a source file +./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.g # Execute precompiled bytecode +./gravity -i 'return 2 + 3' # Execute inline code +./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 + +``` +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). + +## 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. ## 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) * Gravity is the scripting language for the Untold game engine (https://youtu.be/OGrWq8jpK14?t=58) +## Changelog + +See [CHANGELOG.md](CHANGELOG.md) for a summary of changes across versions. + ## Community -Seems like a good idea to make a group chat for people to discuss Gravity.
[![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/Tests/GravityTests/GravityVirtualMachineTests.swift b/Tests/GravityTests/GravityVirtualMachineTests.swift new file mode 100644 index 00000000..9eefee4b --- /dev/null +++ b/Tests/GravityTests/GravityVirtualMachineTests.swift @@ -0,0 +1,173 @@ +import Gravity +import Testing + +@Suite("Gravity virtual machine", .serialized) +struct GravityVirtualMachineTests { + @Test("Executes a script and returns its result") + func executesScript() throws { + let delegate = TestVirtualMachineDelegate() + let virtualMachine = GravityVirtualMachine(settings: .init(), delegate: delegate) + let binary = virtualMachine.loadGravityFile(from: """ + func main() { + return 40 + 2; + } + """) + + let result = try #require(virtualMachine.execute(binary)) + + #expect(delegate.errors.isEmpty) + #expect(result.isInteger) + #expect(result.toInteger == 42) + } + + @Test("Calls a method on a script instance") + func callsInstanceMethod() throws { + let delegate = TestVirtualMachineDelegate() + let virtualMachine = GravityVirtualMachine(settings: .init(), delegate: delegate) + let binary = virtualMachine.loadGravityFile(from: """ + class MovementSystem { + func update(deltaTime) { + return deltaTime * 2; + } + } + + func main() { + return MovementSystem(); + } + """) + let system = try #require(virtualMachine.execute(binary)) + + let result = try #require(system.callMethod(named: "update", with: [21])) + + #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) + } + + @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] = [] + + func virtualMachineLoadFile( + _ virtualMachine: GravityVirtualMachine, + file: String, + fileId: inout UInt32, + isStatic: inout Bool + ) -> String? { + nil + } + + func virtualMachine( + _ virtualMachine: GravityVirtualMachine, + didErrorWith message: String, + errorType: error_type_t, + errorDescription: error_desc_t + ) { + errors.append(message) + } + + func virtualMachineDidReciveLog(_ virtualMachine: GravityVirtualMachine, message: String) {} + + func virtualMachineDidClearLog(_ virtualMachine: GravityVirtualMachine) {} + + func virtualMachineBridgeEquals( + _ virtualMachine: GravityVirtualMachine, + lhsValue: GSValue, + rhsValue: GSValue + ) -> Bool { + false + } + + func virtualMachine( + _ virtualMachine: GravityVirtualMachine, + didExecuteIn ctx: GSValue, + arguments: [GSValue], + argumentsCount: Int16, + vIndex: UInt32 + ) -> Bool { + false + } + + func virtualMachine( + _ virtualMachine: GravityVirtualMachine, + didSetValue value: GSValue, + in target: GSValue, + forKey key: String + ) -> Bool { + false + } + + func virtualMachine( + _ virtualMachine: GravityVirtualMachine, + didGetValueFrom target: GSValue, + forKey key: String + ) throws -> GSValue? { + nil + } + + func virtualMachine( + _ virtualMachine: GravityVirtualMachine, + didSetUndefValue value: GSValue, + in target: GSValue, + forKey key: String + ) -> Bool { + false + } + + func virtualMachine( + _ virtualMachine: GravityVirtualMachine, + didGetUndefValueFrom target: GSValue, + forKey key: String + ) throws -> GSValue? { + nil + } + + func virtualMachine(_ virtualMachine: GravityVirtualMachine, didRequestStringWith length: UInt32) -> String { + "" + } +} diff --git a/Tests/gravity-langTests/gravity_langTests.swift b/Tests/gravity-langTests/gravity_langTests.swift deleted file mode 100644 index 93eba8f6..00000000 --- a/Tests/gravity-langTests/gravity_langTests.swift +++ /dev/null @@ -1,11 +0,0 @@ -import XCTest -@testable import gravity_lang - -final class gravity_langTests: XCTestCase { - func testExample() throws { - // This is an example of a functional test case. - // Use XCTAssert and related functions to verify your tests produce the correct - // results. - XCTAssertEqual(gravity_lang().text, "Hello, World!") - } -} diff --git a/binding/GravitySwift/GSValue.swift b/binding/GravitySwift/GSValue.swift index 63903e70..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 { @@ -333,7 +335,7 @@ public extension GSValue { let instance = self.toGravityInstance let closure = name.withCString { ptr in - gravity_instance_lookup_event(instance, name.toPointer()) + gravity_instance_lookup_event(instance, ptr) } guard let closure = closure else { @@ -400,11 +402,16 @@ extension GSValue: Equatable { public extension GSValue { @discardableResult func callMethod(named name: String, with args: [Any]) -> GSValue? { - if self.hasMethod(named: name) { + guard self.isInstance else { + return nil + } + + let instance = self.toGravityInstance + guard let closure = name.withCString({ ptr in + gravity_instance_lookup_event(instance, ptr) + }) else { return nil } - - let closure = self.toGravityClosure let arguments = args.map { GSValue(object: $0, in: self.vm) } return self.vm.execute(closure: closure, sender: self, params: arguments) @@ -436,11 +443,11 @@ public extension GSValue { } var isInteger: Bool { - return gravity_value_isa_float(self.value) + return gravity_value_isa_int(self.value) } var isDouble: Bool { - return gravity_value_isa_int(self.value) + return gravity_value_isa_float(self.value) } var isFunction: Bool { diff --git a/binding/GravitySwift/GravityVirtualMachine+Bridge.swift b/binding/GravitySwift/GravityVirtualMachine+Bridge.swift index 16c77f30..534b6213 100644 --- a/binding/GravitySwift/GravityVirtualMachine+Bridge.swift +++ b/binding/GravitySwift/GravityVirtualMachine+Bridge.swift @@ -12,17 +12,7 @@ func bridgeOptionalClasses(_ xdata: UnsafeMutableRawPointer?) -> UnsafeMutablePo return nil } - var names = vm.registredClasses().map { $0.toPointer() } - if names.isEmpty { - return nil - } - - let pointer = names.withUnsafeMutableBufferPointer { buffer in - let pointer = UnsafeMutablePointer?>.allocate(capacity: buffer.count) - pointer.moveInitialize(from: buffer.baseAddress!, count: buffer.count) - return pointer - } - return pointer + return vm.optionalClassNamesPointer() } func logCallback(_ vmPointer: OpaquePointer?, message: UnsafePointer?, xdata: UnsafeMutableRawPointer?) { @@ -49,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 { @@ -322,4 +321,3 @@ func bridgeGetUndefValue( return GravityReturn.error(error.localizedDescription, rIndex: Int32(vindex), vm: vm) } } - diff --git a/binding/GravitySwift/GravityVirtualMachine.swift b/binding/GravitySwift/GravityVirtualMachine.swift index b4c23b9d..1238b7a2 100644 --- a/binding/GravitySwift/GravityVirtualMachine.swift +++ b/binding/GravitySwift/GravityVirtualMachine.swift @@ -11,6 +11,8 @@ import Foundation /// Gravity Virtual Machine. public final class GravityVirtualMachine { private var bridgeClassDescriptors: [String: GravityBridgeClassDescriptor] = [:] + private var optionalClassNameStorage: [UnsafeMutablePointer] = [] + private var optionalClassList: UnsafeMutablePointer?>? public struct Settings { public var reportNullErrors: Bool @@ -64,6 +66,7 @@ public final class GravityVirtualMachine { deinit { Self.unregister(self) gravity_vm_free(self.vmPtr) + releaseOptionalClassNames() } // MARK: - Public @@ -180,6 +183,8 @@ public extension GravityVirtualMachine { self.setValue(descriptor.gClass, forKey: descriptor.registredName) self.bridgeClassDescriptors[descriptor.registredName] = descriptor } + + rebuildOptionalClassNames() } /// Set value to gravity virtual machine. @@ -251,21 +256,86 @@ extension GravityVirtualMachine { func registredClasses() -> [String] { return Array(bridgeClassDescriptors.keys) } + + func optionalClassNamesPointer() -> UnsafeMutablePointer?> { + if optionalClassList == nil { + rebuildOptionalClassNames() + } + return optionalClassList! + } + + private func rebuildOptionalClassNames() { + releaseOptionalClassNames() + + optionalClassNameStorage = bridgeClassDescriptors.keys.sorted().map { name in + let bytes = name.utf8CString + let pointer = UnsafeMutablePointer.allocate(capacity: bytes.count) + bytes.withUnsafeBufferPointer { buffer in + pointer.initialize(from: buffer.baseAddress!, count: buffer.count) + } + return pointer + } + + let list = UnsafeMutablePointer?>.allocate(capacity: optionalClassNameStorage.count + 1) + for (index, pointer) in optionalClassNameStorage.enumerated() { + list[index] = UnsafePointer(pointer) + } + list[optionalClassNameStorage.count] = nil + optionalClassList = list + } + + private func releaseOptionalClassNames() { + optionalClassList?.deallocate() + optionalClassList = nil + optionalClassNameStorage.forEach { $0.deallocate() } + optionalClassNameStorage.removeAll(keepingCapacity: false) + } } extension GravityVirtualMachine { - nonisolated(unsafe) private static var virtualMachines: [GravityVirtualMachine] = [] + private final class WeakVirtualMachine { + weak var value: GravityVirtualMachine? + + init(_ value: GravityVirtualMachine) { + self.value = value + } + } + + private final class VirtualMachineRegistry: @unchecked Sendable { + private let lock = NSLock() + private var virtualMachines: [OpaquePointer: WeakVirtualMachine] = [:] + + func virtualMachine(for pointer: OpaquePointer) -> GravityVirtualMachine? { + lock.lock() + defer { lock.unlock() } + return virtualMachines[pointer]?.value + } + + func register(_ virtualMachine: GravityVirtualMachine) { + lock.lock() + defer { lock.unlock() } + virtualMachines[virtualMachine.vmPtr] = WeakVirtualMachine(virtualMachine) + } + + func unregister(_ virtualMachine: GravityVirtualMachine) { + lock.lock() + defer { lock.unlock() } + virtualMachines.removeValue(forKey: virtualMachine.vmPtr) + } + } + + private static let registry = VirtualMachineRegistry() nonisolated static func getVM(_ pointer: OpaquePointer) -> GravityVirtualMachine? { - self.virtualMachines.first(where: { $0.vmPtr == pointer }) + registry.virtualMachine(for: pointer) } nonisolated static func register(_ vm: GravityVirtualMachine) { - self.virtualMachines.append(vm) + registry.register(vm) } nonisolated static func unregister(_ vm: GravityVirtualMachine) { - self.virtualMachines.removeAll(where: { $0.vmPtr == vm.vmPtr }) + registry.unregister(vm) } } 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; } 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 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 @@ - - - - + + - - + $ + $ + $ + $ + $) 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}) diff --git a/src/cli/gravity.c b/src/cli/gravity.c index 0f122fc5..456eb7dc 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" @@ -171,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; @@ -183,13 +187,14 @@ static void unittest_scan (const char *folder_path, unittest_data *data) { const char *full_path = file_buildpath(target_file, folder_path); if (is_directory(full_path)) { // skip disabled folder - if (strcmp(target_file, "disabled") == 0) continue; + if (strcmp(target_file, "disabled") == 0) {mem_free(full_path); continue;} unittest_scan(full_path, data); + mem_free(full_path); continue; } - + // test only files with a .gravity extension - if (strstr(full_path, ".gravity") == NULL) continue; + if (strstr(full_path, ".gravity") == NULL) {mem_free(full_path); continue;} data->is_fuzzy = (strstr(full_path, "/fuzzy/") != NULL); // load source code @@ -234,7 +239,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; @@ -265,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"); @@ -425,6 +430,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,17 +454,22 @@ 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 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 @@ -494,6 +506,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 06e84004..213aaa2f 100644 --- a/src/compiler/gravity_codegen.c +++ b/src/compiler/gravity_codegen.c @@ -60,7 +60,18 @@ 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: - +// 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; @@ -77,6 +88,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); @@ -515,6 +527,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); @@ -739,8 +757,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."); @@ -820,9 +838,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 - @@ -919,11 +934,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; } @@ -1439,6 +1462,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 @@ -1453,17 +1482,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; @@ -1505,7 +1535,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) @@ -1515,39 +1545,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; + report_error(self, (gnode_t *)arg, "Invalid argument expression at index %zu.", j+1); + 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 @@ -1579,14 +1607,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; } } @@ -1598,27 +1626,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) { @@ -1626,21 +1658,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); } @@ -1695,21 +1729,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) { @@ -1897,15 +1940,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."); @@ -2142,6 +2185,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_compiler.c b/src/compiler/gravity_compiler.c index ce8f8373..e1797619 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; } diff --git a/src/compiler/gravity_ircode.c b/src/compiler/gravity_ircode.c index 6f2d33ad..f6199e00 100644 --- a/src/compiler/gravity_ircode.c +++ b/src/compiler/gravity_ircode.c @@ -11,8 +11,14 @@ #include "../utils/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; } @@ -197,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; @@ -293,7 +352,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); @@ -357,18 +416,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 +499,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 +518,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 +532,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 +571,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 +605,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 +639,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_ircode.h b/src/compiler/gravity_ircode.h index cc23ecc4..76ab9df0 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/compiler/gravity_lexer.c b/src/compiler/gravity_lexer.c index 5b1050a4..fb629f20 100644 --- a/src/compiler/gravity_lexer.c +++ b/src/compiler/gravity_lexer.c @@ -39,9 +39,9 @@ typedef enum { // LEXER macros #define NEXT lexer->buffer[lexer->offset++]; ++lexer->position; INC_COL -#define PEEK_CURRENT ((int)lexer->buffer[lexer->offset]) -#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_CURRENT ((lexer->offset < lexer->length) ? (int)lexer->buffer[lexer->offset] : 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 @@ -69,71 +69,77 @@ 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) { +// 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 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 +155,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 +178,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; @@ -184,9 +190,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; @@ -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,16 +356,33 @@ 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 == '\\') { 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; } @@ -566,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 @@ -576,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;} @@ -619,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/src/compiler/gravity_optimizer.c b/src/compiler/gravity_optimizer.c index 2baaa922..de2f0c09 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 "../shared/gravity_hash.h" #include "gravity_optimizer.h" #include "../shared/gravity_opcodes.h" @@ -20,15 +21,15 @@ #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)) // 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) @@ -222,7 +223,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 +237,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 +255,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; @@ -295,7 +296,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) { @@ -308,32 +309,57 @@ 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: // 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 = GRAVITY_INT_DIV(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) { + // 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; + // 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 + // 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); + } break; default: @@ -365,11 +391,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 8636510e..ca9db6fe 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 "../optionals/gravity_optionals.h" #include "gravity_parser.h" #include "../shared/gravity_macros.h" @@ -168,6 +172,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); @@ -438,6 +443,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; } @@ -674,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;} @@ -740,7 +748,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; idata; @@ -792,7 +795,7 @@ static void visit_function_decl (gvisitor_t *self, gnode_function_decl_t *node) // check upvalue limit uint32_t nupvalues = (node->uplist) ? (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 fd0ae0b5..b51599ba 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."); } @@ -81,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 09a0167a..2d36dfb7 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."); } @@ -132,12 +132,13 @@ 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); } 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 +149,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."); } @@ -174,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; @@ -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; } @@ -218,12 +220,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."); } @@ -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); } @@ -280,7 +283,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."); } @@ -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; @@ -344,15 +350,15 @@ 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."); } 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); } @@ -360,7 +366,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 59d955e5..b941a78e 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); } } @@ -133,8 +171,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 acaf04ff..c739dc45 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 @@ -223,23 +223,28 @@ 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); + 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_INT(base)) { - gravity_float_t computed_value = (gravity_float_t)pow((gravity_float_t)value.f, 1.0/base.f); + 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) { @@ -493,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); @@ -643,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 @@ -652,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; @@ -907,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/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_core.c b/src/runtime/gravity_core.c index c349874c..395cef69 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; @@ -173,7 +174,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); @@ -218,7 +219,9 @@ static inline gravity_value_t convert_map2string (gravity_vm *vm, gravity_map_t // 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 @@ -249,7 +252,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); @@ -274,7 +277,9 @@ static inline gravity_value_t convert_list2string (gravity_vm *vm, gravity_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 @@ -297,7 +302,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 +325,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 +348,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 +375,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 @@ -1019,12 +1024,20 @@ 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); } - marray_set(list->array, index, value); + // value is set unconditionally below } marray_set(list->array, index, value); @@ -1097,6 +1110,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 +1617,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 +1656,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 +1829,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; } @@ -2014,14 +2030,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) { @@ -2037,7 +2053,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) { @@ -2067,7 +2083,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) { @@ -2128,17 +2145,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 +2464,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); @@ -2487,7 +2498,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); @@ -2516,7 +2531,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]); } } @@ -2555,7 +2570,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]); } } @@ -2626,7 +2641,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; @@ -2637,6 +2652,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); @@ -2760,9 +2808,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); @@ -2786,9 +2835,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); } @@ -2800,7 +2852,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); } @@ -3013,7 +3067,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); @@ -3166,7 +3220,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); } @@ -3329,8 +3386,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)); @@ -3434,8 +3496,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)); @@ -3459,8 +3523,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)); @@ -3587,14 +3653,23 @@ 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 + // 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 @@ -3610,6 +3685,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); @@ -3685,6 +3773,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; diff --git a/src/runtime/gravity_vm.c b/src/runtime/gravity_vm.c index e7953e23..1aede479 100644 --- a/src/runtime/gravity_vm.c +++ b/src/runtime/gravity_vm.c @@ -16,6 +16,10 @@ #include "../shared/gravity_opcodes.h" #include "../shared/gravity_memory.h" #include "../runtime/gravity_vmmacros.h" +#define GRAVITY_INCLUDE_MATH +#define GRAVITY_INCLUDE_JSON +#define GRAVITY_INCLUDE_ENV +#define GRAVITY_INCLUDE_FILE #include "../optionals/gravity_optionals.h" // MARK: Internals - @@ -46,6 +50,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) @@ -219,7 +226,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 @@ -227,8 +234,7 @@ static inline gravity_callframe_t *gravity_new_callframe (gravity_vm *vm, gravit 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; @@ -243,13 +249,12 @@ 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) { - #pragma unused(vm) +static bool gravity_check_stack (gravity_vm *vm, gravity_fiber_t *fiber, uint32_t stacktopdelta, gravity_value_t **stackstart) { 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 +264,20 @@ static inline bool gravity_check_stack (gravity_vm *vm, gravity_fiber_t *fiber, // 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; } @@ -742,10 +754,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(); @@ -891,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 @@ -910,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); @@ -939,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__) @@ -964,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); @@ -1038,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); @@ -1188,7 +1200,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,18 +1536,22 @@ 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; 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); @@ -1562,22 +1578,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); } @@ -1837,11 +1853,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]; } @@ -1908,6 +1926,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; } @@ -1921,6 +1940,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; } @@ -2053,6 +2073,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; @@ -2066,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; @@ -2083,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)); @@ -2096,12 +2127,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 +2150,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/runtime/gravity_vm.h b/src/runtime/gravity_vm.h index df87ff43..9560eaaa 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 7a332b1d..47aefb1f 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 @@ -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); \ @@ -242,7 +246,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_array.h b/src/shared/gravity_array.h index 38702642..958a393f 100644 --- a/src/shared/gravity_array.h +++ b/src/shared/gravity_array.h @@ -27,13 +27,16 @@ #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) { \ - (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 +#define marray_push(type, v, x) do {if ((v).n == (v).m) { \ + 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 1aa908c4..3d13ba0f 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; @@ -111,9 +111,14 @@ static inline 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; @@ -195,7 +200,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; } @@ -203,7 +209,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, @@ -331,9 +337,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_macros.h b/src/shared/gravity_macros.h index c20b3ab1..385d798d 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 ad0e85a5..fcfbda90 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 9d1431aa..5746e244 100644 --- a/src/shared/gravity_value.c +++ b/src/shared/gravity_value.c @@ -121,11 +121,17 @@ 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) { + 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; } @@ -158,6 +164,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 +217,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 +283,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 +367,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 +437,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 @@ -393,7 +458,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; @@ -410,13 +475,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) { @@ -528,12 +593,23 @@ 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; } } + // 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)); @@ -1355,6 +1431,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) { @@ -1846,13 +1940,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; } @@ -2359,7 +2453,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); @@ -2381,7 +2475,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); @@ -2395,13 +2489,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); @@ -2421,30 +2515,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/shared/gravity_value.h b/src/shared/gravity_value.h index 565967fc..a95ced78 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.8" // git tag 0.9.8 +#define GRAVITY_VERSION_NUMBER 0x000908 // 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 @@ -132,13 +132,14 @@ 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 +#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,9 +181,30 @@ 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 +// 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; @@ -379,7 +401,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 +486,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_debug.c b/src/utils/gravity_debug.c index 989c126a..6f25713d 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 fa78933b..cd6714aa 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; }; } @@ -291,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); @@ -448,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) @@ -487,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; @@ -567,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, @@ -646,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 || @@ -663,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 || @@ -754,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 @@ -763,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; @@ -942,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; @@ -958,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') { @@ -973,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; @@ -1101,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/src/utils/gravity_utils.c b/src/utils/gravity_utils.c index 7be4db81..50ffc46e 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; @@ -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; @@ -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,8 +185,8 @@ char *file_buildpath (const char *filename, const char *dirpath) { char *full_path = (char *)mem_alloc(NULL, len); if (!full_path) return NULL; - #ifdef WIN32 - PathCombineA(full_path, filename, dirpath); + #ifdef _WIN32 + PathCombineA(full_path, dirpath, filename); #else // check if PATH_SEPARATOR exists in dirpath if ((len2) && (dirpath[len2-1] != PATH_SEPARATOR)) @@ -200,27 +200,29 @@ 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; } } + // if no separator found, the entire path is the filename + if (!name) name = string_dup(buffer); + mem_free(buffer); return name; } // 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; @@ -235,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); @@ -247,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]; @@ -270,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) { @@ -302,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) { @@ -358,11 +360,12 @@ 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); } 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; @@ -506,7 +509,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 @@ -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 +#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; +} diff --git a/test/unittest/bugfix_bool_compare.gravity b/test/unittest/bugfix_bool_compare.gravity new file mode 100644 index 00000000..4e2911ad --- /dev/null +++ b/test/unittest/bugfix_bool_compare.gravity @@ -0,0 +1,29 @@ +#unittest { + name: "Bool ordered comparison."; + result: true; +}; + +func main() { + // equality (was already correct) + var r1 = (true == true); + var r2 = (true != false); + + // ordered comparisons (were broken: fast-path only checked equality) + var r3 = (true > 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_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; +} 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; +} 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_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; +} 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_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/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"); +} 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/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/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/disabled/loop1.gravity b/test/unittest/loop1.gravity similarity index 100% rename from test/disabled/loop1.gravity rename to test/unittest/loop1.gravity 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/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)) 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; +}