Skip to content

Codex/sync upstream 0.9.8 - #1

Merged
SpectralDragon merged 48 commits into
masterfrom
codex/sync-upstream-0.9.8
Aug 27, 2026
Merged

Codex/sync upstream 0.9.8#1
SpectralDragon merged 48 commits into
masterfrom
codex/sync-upstream-0.9.8

Conversation

@SpectralDragon

Copy link
Copy Markdown
Collaborator

No description provided.

marcobambini and others added 30 commits January 24, 2026 09:22
…ts, and documentation

## Compiler

- Lexer: fix off-by-one in PEEK_NEXT/PEEK_NEXT2 boundary checks
- Parser: fix memory leaks in parse_file_expression, parse_variable_declaration,
  and string interpolation handling; fix undersized buffer for escaped strings
- Semacheck2: fix wrong constant in upvalue limit error message (MAX_LOCALS → MAX_UPVALUES)
- Codegen: fix wrong variable name in for-loop register allocation (temp2 → temp3);
  replace multiple early-return paths in visit_postfix_expr with goto cleanup to
  prevent leaking self_list/args arrays; fix register clobbering in
  LOCATION_CLASS_IVAR_OUTER assignment; remove unnecessary NOP for empty statements;
  add VISIT_MOVE_OPT macro to consolidate pragma patterns
- Optimizer: fix constant folding checking wrong register (inst2->p2 → inst2->p1);
  fix NEG optimization writing to wrong destination register (inst->p2 → inst->p1);
  fix OPCODE_SET_FOUR8bit macro missing opcode field
- IR code: convert register allocation from bool[256] array to compact bitmask
  (32 bytes), improving cache efficiency; add bounds checks on empty label stacks;
  add assertion after inst_new allocation

## Runtime / VM

- Fix boolean fast-path comparison incorrectly applied to ordered comparisons
  (LT, GT, LEQ, GEQ) — now restricted to EQ/NEQ only
- Fix crash in gravity_vm_setslot/gravity_vm_getslot when no frames exist
- Fix OPCODE_GET_FOUR8bit macro inconsistency with encoder
- Fix list_iterator_next out-of-bounds read (add bounds check)
- Fix list_storeat duplicate marray_set after resize
- Fix range_contains not handling reversed ranges correctly
- Fix function_exec default-arg filling using wrong index formula
- Fix int_random truncation from gravity_int_t to int
- Fix string_count broken partial-match logic (rewrite using string_strnstr)
- Fix string_upper/string_lower off-by-one (iterated one byte past end)
- Fix string_loadat UTF-8 corruption when reversing multi-byte sequences
- Fix string_loop iterating byte-by-byte instead of by UTF-8 character
- Fix string_iterator/string_iterator_next missing bounds checks
- Fix fiber_abort wrong nargs check (args[0] is self, not the message)
- Fix system_input blindly stripping last char instead of checking for newline
- Fix convert_map2string/convert_list2string losing original pointer on realloc failure
- Fix gravity_class_grow discarding existing ivar values when growing
- Add overflow guard to gravity_function_cpool_add (uint16_t limit)

## Optional Modules

- Math: fix atan2 → atan2f for float builds; use POW macro consistently in
  math_xrt and math_round; fix division-by-zero in math_logx when base=1;
  fix integer overflow in math_random when range endpoint is GRAVITY_INT_MAX;
  fix pointer arithmetic bugs in math_round string truncation
- File: fix memory leak in file_buildpath (result string not freed); fix leak
  in scan_directory recursive path; fix wrong nargs check in file_open; fix
  leaked FILE* when instance creation fails; fix crash on negative read size;
  fix swapped fread/fwrite arguments (size vs count); fix integer overflow
  in read buffer resize; fix typo "enought" → "enough"
- JSON: rewrite string escaping in JSON.stringify to properly escape backslashes,
  quotes, control characters, and handle large strings
- ENV: add Windows compatibility for environ access; simplify key-length loop

## Utilities

- Debug: add buffer overflow protection in DUMP_VM macros; add bounds check in
  opcode_name; fix memory leak in gravity_disassemble
- JSON serializer: fix escape buffer too small (len*2 → len*6+1); add control
  character escaping (\uXXXX); add overflow check
- Utils: fix uninitialized fd in file_read; fix swapped PathCombineA arguments on
  Windows; fix file_name_frompath returning NULL when no separator found; add NULL
  check in string_dup; fix signed arithmetic and missing digit validation in
  number_from_bin

## Shared / Core Data Structures

- gravity_array.h: fix marray_push updating capacity before confirming realloc
  succeeded; add bounds check before write; fix marray_resize/marray_resize0 to
  check realloc result
- gravity_hash.c: fix gravity_hash_memsize counting buckets as nodes; improve
  float hashing precision (%f → %.17g)

## CLI

- Fix memory leaks in unittest_scan (full_path not freed on skip/recurse)
- Fix inline execution buffer leak

## Documentation

- Add comprehensive ARCHITECTURE.md covering the full compilation pipeline, VM
  internals, value system, garbage collector, instruction set, and embedding API
- Add CLAUDE.md with project conventions for Claude Code
- Update README.md: add Building, Usage, and Project Structure sections; update
  line counts to reflect current codebase size; enhance Features list; add links
  to ARCHITECTURE.md

## Tests

- Add 30 new unit tests covering all major bug fixes: bool comparison, string
  operations (count, upper/lower, UTF-8 iteration/reversal), math functions
  (round, logx, xrt, random), file I/O, JSON escaping, range operations,
  list bounds, fiber abort, optimizer constant folding, and more

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…le-free

gravity_opt_free() was called unconditionally at the top of
gravity_core_free(), before the refcount guard. This causes a double-free
when gravity_compiler_reset() is called before teardown.

gravity_compiler_reset() runs a mini-VM internally (internal_vm_cleanup)
which frees GC objects via the GC callback — including the optionals.
When gravity_core_free() is subsequently called, gravity_opt_free() then
attempts to free already-freed memory.

The fix moves gravity_opt_free() to after the refcount check, so optionals
are only freed when the last VM is being torn down.

Reproducer: call gravity_compiler_reset() on a compiler instance, then
call gravity_core_free(). Crashes reliably under AddressSanitizer.
…vity-opt-free

fix(core): move gravity_opt_free after refcount check to prevent double-free
…ite improvements

Runtime / VM:
- Add configurable fiber stack size limit (DEFAULT_MAXSTACK_SIZE = 1M slots).
  Infinite recursion and stack exhaustion now produce a clean RUNTIME error
  instead of a hard crash or silent failure.
- Expose GRAVITY_VM_MAXSTACK key via gravity_vm_get/set for runtime tuning.
- Fix gravity_fiber_reassign stack growth: large register-window in $moduleinit
  could push stacktop past the initial allocation (issue marcobambini#437).

Compiler:
- Fix class $init chain infinite recursion: parent $init helpers ($init2,
  $init3, …) previously used a dynamic name lookup against self, which
  resolved to the wrong override in subclass context. Now emits a direct
  static closure reference (LOADK) via ircode_patch_init_direct.

Tests:
- Re-enable test/disabled/heap.gravity and test/disabled/loop1.gravity,
  moved to test/unittest/. Both now pass with the new OOM error reporting.
- Fix two bugs in heap.gravity (wrong variable assigned, wrong constructor called).
- Add regression test for issue marcobambini#437 (bugfix_stack_overflow_large_regwin).
- Delete test/disabled/ directory.

Docs:
- Add CHANGELOG.md covering all versions from 0.2.8 to 0.9.6.
- Update README: CMake build instructions, Testing section, Embedding API
  example, CHANGELOG link, replace Gitter with GitHub Discussions.
- Update CLAUDE.md: document fuzzy/infiniteloop test dirs and gravity_vm_get/set.
…n (issue marcobambini#420)

Two root causes behind 'RUNTIME ERROR: Unknown LOADK index' on -c/-x path:

1. gravity_json.c: float constants were serialized with '%f' (6 decimal
   places), silently rounding small values like -0.000000004 to -0.000000.
   When two distinct floats rounded to the same string, one was dropped from
   the JSON pool, leaving the bytecode referencing a non-existent index.
   Fixed by switching to '%.17g' (17 significant digits, full IEEE 754
   double round-trip), with a '.0' suffix appended for whole-number values
   so they deserialize as float rather than integer.

2. gravity_value.c: gravity_function_cpool_add used the epsilon-based
   gravity_value_equals (EPSILON=1e-6) to detect duplicate constants.
   Any two floats differing by less than 1e-6 were merged into one cpool
   entry, causing index mismatches at runtime. The cpool now uses exact
   bit-level comparison (v.f != v2.f) for float values before falling back
   to the fuzzy equality check used for all other types.

Also fix run_all.sh to work on macOS where GNU 'timeout' is not available:
detect 'timeout', 'gtimeout' (brew coreutils), or fall back to a pure-bash
background kill-watcher that returns exit code 124 on expiry.
…ssue marcobambini#426)

The four #ifndef GRAVITY_INCLUDE_* / #define ... / #endif blocks
unconditionally defined every optional-module guard the moment the header
was included, making the guards useless for embedders who want to exclude
specific modules.

Fix: remove the unconditional defines from gravity_optionals.h so the
guard macros must be explicitly set by the includer. The three Gravity
source files that want all optionals enabled (gravity_vm.c, gravity.c,
gravity_parser.c) now define all four GRAVITY_INCLUDE_* macros before
including gravity_optionals.h. Embedders can now selectively include only
the modules they need.
Four problems corrected:

1. 'gravity' and 'example' were declared .PHONY despite producing real
   files, causing make to unconditionally rebuild them on every invocation
   regardless of whether any source had changed. Removed both from .PHONY.
   Only 'all', 'clean', and 'lib' are genuinely phony targets.

2. 'lib: gravity' was a wrong dependency — building the shared library only
   requires the compiled object files, not the gravity CLI executable.
   Changed to 'lib: $(OBJ)'.

3. GRAVITY_SRC (src/cli/gravity.c) and EXAMPLE_SRC (examples/example.c)
   were passed directly to the linker step rather than compiled separately,
   so -MMD never generated .d files for them. Header changes in those files
   would not trigger rebuilds. Both are now compiled to .o first
   (GRAVITY_OBJ / EXAMPLE_OBJ) so make tracks their header dependencies
   correctly via the generated .d files.

4. 'make clean' removed libgravity.so and gravity.dll but missed
   libgravity.dylib (macOS). Added it to the clean target along with the
   new GRAVITY_OBJ and EXAMPLE_OBJ intermediates.
- Install now copies headers.
- Build considers `BUILD_SHARED_LIBS` when deciding to build `gravityapi` target.
- Install now works when CLI is disabled.
- Local in-source install on Windows is now a CMake option.
- Install paths propagated to parent scope so CLI install can see them.
…packaging

Change CMake install instructions to be more readily packagable.
Automated security fix generated by OrbisAI Security
…-strcat-buffer-overflow

fix: add buffer-length check in console.c
…core_init

VALUE_FROM_OBJECT() evaluates its argument twice when
GRAVITY_USE_HIDDEN_INITIALIZERS is not set:

    #define VALUE_FROM_OBJECT(obj) ((gravity_value_t){.isa = ((gravity_object_t *)(obj)->isa), .p = (gravity_object_t *)(obj)})

Six bind sites in gravity_core_init called computed_property_create()
inline inside that macro (Object.class, Object.meta, Int.min/max meta,
Float.min/max meta), so each of those computed properties was created
twice: one copy bound, the duplicate orphaned (~2.2KB leaked per
gravity_core_init).

Use the same temp-variable pattern the rest of gravity_core_init already
uses for every other computed property.

Also free the nine computed properties missing from gravity_core_free's
manual free list (Object.class, Object.meta, Range.from, Range.to,
String.bytes, Int meta min/max, Float meta min/max) — previously leaked
on every core init/free cycle.
gravity_vm_new calls gravity_gc_setenabled(vm, true) before
marray_init(vm->graylist). Enabling the GC can trigger a collection
(gravity_gc_check -> gravity_gc_start), which grows the graylist buffer
via marray_push/realloc. The marray_init that follows then zeroes the
array struct, orphaning that buffer (leaked once per VM).

Initialize graylist/gctemp before enabling the GC.
Fix three VM memory leaks: computed-property double creation, missing frees in gravity_core_free, orphaned GC gray-list buffer
…#446)

parse_number_expression checked for a 0b/0o/0x prefix by reading value[1]
whenever value[0] was '0', without first confirming the token is at least
2 bytes long. token.value points directly into the caller's source buffer
(lexer->buffer + lexer->offset) and is not separately zero terminated, so
a source whose last byte is a lone '0' caused a 1-byte read past the end
of the buffer.

The CLI is unaffected because file_read over-allocates by one byte and
zero terminates, but gravity_compiler_run accepts an explicit length and
embedders may legitimately pass an exact-size, non terminated buffer.
Confirmed with ASan on a 3-byte malloc holding "x=0":

  ERROR: AddressSanitizer: heap-buffer-overflow
  READ of size 1 at 0x6020000000d3 thread T0
      #0 parse_number_expression gravity_parser.c:684

Guard the prefix check with token.bytes > 1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…er-oob-read

fix: heap out-of-bounds read in parse_number_expression (marcobambini#446)
gravity_opt_register() runs once per gravity_core_register(), but
gravity_opt_free() is reached only on the last gravity_core_free(), when the
core refcount hits zero. In the usual embedding flow (a mini VM for the
compiler plus a real VM) each optional class therefore ends up with a
refcount of 2 while only one decrement ever happens, so Math/ENV/JSON/File
and their metaclasses are never freed. They are not owned by any VM garbage
collector, so nothing else can reclaim them.

Track the number of outstanding registrations and balance them all when the
last VM is torn down. gravity_opt_free() still runs only after the refcount
check, preserving the ordering introduced by dd81589, and each optional keeps
its own refcount and NULL guard so it is still freed exactly once.

Verified with macOS leaks on examples/example.c: allocations owned by gravity
and still live at exit drop from 347 blocks / 31,698 bytes to zero, and the
total live heap goes from 534 nodes / 66 KB to 187 nodes / 31 KB. Running a
script through the CLI improves the same way (537 nodes / 67 KB to 190 nodes
/ 31 KB). Unit tests pass 352/352, also under AddressSanitizer, together with
a stress program covering repeated teardown and re-init, two concurrent VMs
with a partial teardown, a reused compiler, and an extra unbalanced
gravity_core_free().

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…arcobambini#447, marcobambini#448)

gravity_vm_loadbuffer() trusted the shape of the JSON it was handed, and
the scanner underneath it had several out of bounds and overflow issues.
All of them are reachable through `gravity -x` with a hand written file.

gravity_vm_loadbuffer:
  - a top level entry was tested for emptiness through u.object.length
    before its type was checked, reading an unset union member whenever
    the entry was not an object
  - a deserialized function may legitimately have a NULL identifier
    (missing field, or an anonymous function serialized as $anon_), and
    strlen() was called on it unconditionally: {"x":{"type":"function"}}
    was enough to crash the loader (marcobambini#444). A top level function must be
    named, so a NULL identifier is now a load error
  - string_casencmp(identifier, INITMODULE_NAME, strlen(identifier))
    compared only as many characters as the identifier is long, so any
    prefix of $moduleinit was accepted as the module initializer. Use
    string_cmp() for a full comparison

json_parse_ex:
  - the "\uXXXX" escape, the trailing surrogate, and the true/false/null
    literals all checked one byte less than they consume, so each could
    read one byte past the end of a buffer that is not NUL terminated
    (marcobambini#448). gravity_vm_loadbuffer accepts exactly such a buffer
  - during the first pass u.object.values is a byte tally, not a pointer,
    and was incremented through a json_char pointer: undefined behaviour
    on a null pointer, which traps under -fsanitize=undefined (marcobambini#448).
    Keep the tally in a uintptr_t, which also stops it truncating on
    LLP64 targets
  - integer and exponent accumulators were multiplied without any range
    check, overflowing signed 64-bit on a long digit run (marcobambini#447). The
    integer accumulator now reports an out of range literal, the exponent
    saturates, and fraction digits below the precision of a double are
    dropped while keeping the fraction scale in sync

murmur3_32 read the key four bytes at a time through a uint32_t pointer,
which is undefined for an unaligned key and faults outright on strict
alignment targets. memcpy each block instead; byte order and therefore
every hash value is unchanged, and compilers fold it back to a single
unaligned load.

Adds test/loadbuffer/: one malformed executable per rejection path plus a
valid round trip as a positive control, and json_bounds.c for what the
CLI cannot reach, since the CLI always hands the loader a NUL terminated
buffer while the API does not. Build it with `make jsontest`, ideally
under a sanitizer.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
gravity_compiler_run() called gravity_core_register() on each invocation,
but only gravity_compiler_free() ever released a reference. An embedder
reusing one compiler for N compilations left N-1 unreleased core
references, so gravity_core_free() never reached refcount 0 and neither
the core classes nor the optional ones (Math/ENV/JSON/File) were freed
(~107 KB retained at exit).

On a successful compile the compiler was never reset, so the next run
also overwrote compiler->vm and orphaned the previous mini VM struct
(320 bytes per extra compilation).

The mini VM holds no per-compilation state (it is just a container for
the transfer/cleanup callbacks), so create it once and reuse it, and pair
the core reference it takes with a release wherever the mini VM is freed.
The refcount keeps the core alive for a real VM created with
gravity_vm_new, which owns its own reference.

This also removes the unconditional gravity_core_free() in
gravity_compiler_free(): it released a reference the compiler may never
have taken, so freeing a never-run compiler while a VM was alive freed
the core out from under it (segfault).

Verified with a program compiling N times on one compiler followed by a
full teardown: retained heap at exit is now 534 nodes / 66 KB for N=1,
3 and 10, identical to the single-compile flow in examples/example.c,
with no unreachable blocks. Unit tests 350/350, ASan/UBSan build of
examples/example.c and of the test suite report nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
)

Int arithmetic in Gravity wraps around on overflow, which is what the
runtime operators and the constant folder have always produced, but the
wrap was performed directly on signed operands: signed overflow is
undefined behaviour in C, so every one of those sites traps under
-fsanitize=undefined and is at the mercy of the optimizer elsewhere.

Add GRAVITY_INT_ADD/SUB/MUL/NEG/DIV/REM in gravity_value.h, which do the
arithmetic on the unsigned counterpart and convert back. The values are
unchanged, they are simply no longer undefined. Both operands are widened
to 64bit so a single definition serves either GRAVITY_ENABLE_INT64
setting. All three paths now go through them, so the folded constant and
the runtime cannot drift apart:

  - CHECK_FAST_BINARY_MATH / CHECK_FAST_UNARY_MATH take the Int operation
    as a macro parameter and apply it on the Int path only, the Float one
    keeps using the plain operator
  - operator_int_add/sub/mul/neg/div/rem in gravity_core.c
  - optimize_const_instruction in gravity_optimizer.c

GRAVITY_INT_DIV and GRAVITY_INT_REM also cover GRAVITY_INT_MIN op -1,
which does not merely wrap: on x86 idiv faults and the process dies with
SIGFPE, both while folding and at runtime.

Separately, optimize_const_instruction folded a floating point REM by
truncating both operands to int64_t. That divided by zero for any
0 < |divisor| < 1, and disagreed with the runtime on every operand with a
fractional part: 2.5 % 2.0 folded to 0 where the VM evaluates 0.5. Fold
with remainder(), which is what operator_float_rem computes.

REM is also dispatched on the class of the left operand, so a mixed
Int/Float expression runs operator_int_rem and does not follow float
semantics at all. Those are left to the runtime instead of folded.

Adds a unit test which compares each folded expression against the same
operation evaluated at runtime, so the two paths cannot silently diverge
again.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
marcobambini and others added 18 commits August 5, 2026 14:38
Fix Int overflow and float remainder constant folding (marcobambini#443).
Fix optional classes never released due to unbalanced refcount (marcobambini#442).
Fix core reference leaked by every gravity_compiler_run (marcobambini#442).
Travis is no longer running for this repository. Add a GitHub Actions
workflow that builds with gcc and clang on Linux and macOS and runs the
unit tests, the -t assertion pass, and the JSON loader tests.

A second job builds with -fsanitize=address,undefined and runs the unit
tests, the fuzzing corpus and the JSON loader tests through it, so the
memory safety fixes stay covered. The unit test timeouts in run_all.sh
are calibrated for an optimized build, so the sanitizer job runs the
tests directly with a generous per test timeout instead.

Also run the -t assertion pass in .travis.yml for parity.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
gravity -i built the wrapped source in a heap buffer and passed it to
gravity_compiler_run with is_static false. That hands the buffer to the
lexer, which frees it in parser_run once parsing is done, so the free in
the CLI cleanup path was a second free of the same pointer.

The file paths do not hit this because they never free the buffer
returned by file_read: only the inline buffer was tracked and freed.

Clear the pointer once ownership has passed to the compiler. The free in
cleanup stays for the paths that bail out before compilation starts.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The CLI compiles to gravity.g (DEFAULT_OUTPUT), but the usage text and
both README.md and CLAUDE.md still documented the older gravity.json.
Print DEFAULT_OUTPUT in the usage text so the two cannot drift again.

Also gitignore gravity.g, which a plain gravity -c drops in the working
directory.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Security and memory safety release covering the compiler crash and the
bytecode loader crashes reported in marcobambini#442, marcobambini#443, marcobambini#444, marcobambini#446, marcobambini#447 and marcobambini#448,
each with a regression test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
optimize_const_instruction folded a float REM with remainder() whatever
GRAVITY_ENABLE_DOUBLE was set to, while operator_float_rem switches to
remainderf() when gravity_float_t is a float. An IEEE remainder is exact,
so the two agree in practice, but the folder claimed to match the runtime
without mirroring its conditional. Make the claim literally true.

No change to the default build, where GRAVITY_ENABLE_DOUBLE is 1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Storing past the end of a list reallocates the backing array through
marray_resize, which leaves both p and m untouched when the realloc fails.
The guard tested list->array.p for NULL, but a failed realloc keeps the old,
smaller, non NULL buffer in place, so it never fired. marray_nset then set
the count to the requested index and the fill loop wrote past the end of the
allocation.

Check the capacity actually obtained instead, so the out of memory case is
reported as the runtime error it was always meant to be. The fill loop is
bounded by that capacity too: it agrees with the old index+MIN_LIST_RESIZE
bound for every list the runtime builds today, since they all carry spare
capacity, but the loop should not have to rely on that.

This is reachable from a script, x[4444444444444444444] = 0 is enough, and
it is what several inputs in test/fuzzy do. They were never run in CI before
the sanitizer job was added.

CI: cap a single allocation at 1GB in the sanitizer job so those inputs fail
cleanly rather than pushing the runner into the OOM killer, which is what
made the job die with exit code 143. Pin abort_on_error so a finding arrives
as a signal on every platform instead of the bare exit code 1 the runtime
uses on Linux, which the fuzzing step could not tell apart from an input the
interpreter simply rejected, and scan the output for sanitizer reports too.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CodeQL ran on github/codeql-action@v1, deprecated since January 2023 and no
longer updated or supported, so it kept reporting green without being a
current analysis. It is not a required status check and code scanning default
setup is not configured, so the workflow was its only trigger.

Before removing it, fix its one open finding: report_error was passing a
size_t to a %d conversion in gravity_codegen.c, which reads 32 bits of a 64bit
variadic argument. Use %zu.

The compiler could not catch that because report_error only forwards its
arguments to vsnprintf. Annotate all three of them with the printf format
attribute on gcc and clang, so every call site is type checked from now on and
this class fails the build rather than needing an external analyser. The tree
is clean under it today.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
is_newline() looked at PEEK_CURRENT for the character following c, which
only holds where the caller had already consumed c (the comment scanner and
skip_line). In gravity_lexer_next, lexer_scan_number and lexer_scan_string
c was still the character at the current offset, so the CR of a CR+LF pair
never saw the LF next to it: the pair was counted as two line breaks and
every row the compiler reported drifted by one per line read so far. On a
source file saved on Windows an error on row 5 was reported on row 9.

The lookahead is now passed in explicitly, so each call site says which two
characters follow the one being examined, and newline_length() returns the
size of the terminator without consuming anything. The two callers that
still hold the offset skip the whole sequence themselves, and the string
scanner keeps every byte of it inside the token so that a literal spanning
CR+LF lines is not shortened by one byte per line.

NEL and LS are left exactly as they were: PEEK_NEXT and PEEK_NEXT2 read
plain char, so their comparisons are still false where char is signed. That
is pre-existing and out of scope here.

Reported in marcobambini#389, and this is the fix proposed in marcobambini#401 by Matthew Asplund
(mwasplund) with the string and number scanners corrected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
lib already builds the shared library out of $(OBJ), which is every source
but the CLI entry point. staticlib archives the same objects, so embedders
who link gravity statically no longer have to drive ar by hand.

Requested in marcobambini#427 by Jock Murphy (jockm). The clean fix from that pull
request is already in: clean removes the shared library under each of its
platform specific names.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
gravity_utils.h picks DIRREF and windows.h on _WIN32, but gravity_utils.c,
gravity_opt_file.c and the CLI guarded their Windows bodies with WIN32,
which is not a compiler defined macro. The Visual Studio projects define it
in the two 32 bit configurations of gravity.vcxproj alone, so every x64
build compiled the POSIX bodies against the Windows header: opendir and
readdir do not exist under MSVC and DIRREF is a HANDLE there, so the two
halves cannot even agree on a type. MinGW and tcc land in the same place.

_WIN32 is defined by MSVC, MinGW and tcc on both 32 and 64 bit, and is what
the header already tests. WIN32_FIND_DATA and friends are Windows API type
names and are left alone.

From marcobambini#411 by tDwtp, which is the pull request that reported it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@SpectralDragon
SpectralDragon merged commit c1df978 into master Aug 27, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants