Skip to content

Commit 914793a

Browse files
committed
Fix float precision loss in JSON serialization and cpool deduplication (issue #420)
Two root causes behind 'RUNTIME ERROR: Unknown LOADK index' on -c/-x path: 1. gravity_json.c: float constants were serialized with '%f' (6 decimal places), silently rounding small values like -0.000000004 to -0.000000. When two distinct floats rounded to the same string, one was dropped from the JSON pool, leaving the bytecode referencing a non-existent index. Fixed by switching to '%.17g' (17 significant digits, full IEEE 754 double round-trip), with a '.0' suffix appended for whole-number values so they deserialize as float rather than integer. 2. gravity_value.c: gravity_function_cpool_add used the epsilon-based gravity_value_equals (EPSILON=1e-6) to detect duplicate constants. Any two floats differing by less than 1e-6 were merged into one cpool entry, causing index mismatches at runtime. The cpool now uses exact bit-level comparison (v.f != v2.f) for float values before falling back to the fuzzy equality check used for all other types. Also fix run_all.sh to work on macOS where GNU 'timeout' is not available: detect 'timeout', 'gtimeout' (brew coreutils), or fall back to a pure-bash background kill-watcher that returns exit code 124 on expiry.
1 parent 93930ac commit 914793a

3 files changed

Lines changed: 47 additions & 3 deletions

File tree

src/shared/gravity_value.c

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -593,6 +593,14 @@ uint16_t gravity_function_cpool_add (gravity_vm *vm, gravity_function_t *f, grav
593593
size_t n = marray_size(f->cpool);
594594
for (size_t i=0; i<n; i++) {
595595
gravity_value_t v2 = marray_get(f->cpool, i);
596+
// Float constants must match exactly at the bit level so that distinct
597+
// small values (e.g. -4e-9 vs -5e-11) are never merged due to the
598+
// epsilon-based gravity_value_equals comparison.
599+
if (v.isa == gravity_class_float && v2.isa == gravity_class_float) {
600+
if (v.f != v2.f) continue;
601+
gravity_value_free(NULL, v);
602+
return (uint16_t)i;
603+
}
596604
if (gravity_value_equals(v,v2)) {
597605
gravity_value_free(NULL, v);
598606
return (uint16_t)i;

src/utils/gravity_json.c

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -299,8 +299,18 @@ void json_add_double (json_t *json, const char *key, double value) {
299299
json_check_comma(json);
300300

301301
char buffer[512];
302-
// was %g but we don't like scientific notation nor the missing .0 in case of float number with no decimals
303-
size_t len = snprintf(buffer, sizeof(buffer), "%f", value);
302+
// Use %.17g for a lossless double round-trip (17 significant digits covers
303+
// the full IEEE 754 range without precision loss that occurred with "%f").
304+
// If the result contains no decimal point or exponent, append ".0" so the
305+
// value is deserialized as a float rather than an integer.
306+
size_t len = snprintf(buffer, sizeof(buffer), "%.17g", value);
307+
bool has_dot_or_exp = false;
308+
for (size_t i = 0; i < len; i++) {
309+
if (buffer[i] == '.' || buffer[i] == 'e' || buffer[i] == 'E') { has_dot_or_exp = true; break; }
310+
}
311+
if (!has_dot_or_exp && len + 2 < sizeof(buffer)) {
312+
buffer[len++] = '.'; buffer[len++] = '0'; buffer[len] = '\0';
313+
}
304314

305315
if (key) {
306316
json_write_raw (json, key, strlen(key), true, true);

test/unittest/run_all.sh

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,32 @@ set -u -o pipefail
1010

1111
readonly SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
1212
readonly GRAVITY_BIN=$SCRIPT_DIR/../../gravity
13+
14+
# Resolve a portable timeout implementation.
15+
# Preference order: timeout (Linux/GNU coreutils) → gtimeout (macOS + brew
16+
# install coreutils) → pure-bash fallback using a background kill watcher.
17+
if command -v timeout &>/dev/null; then
18+
run_timeout() { timeout "$@"; }
19+
elif command -v gtimeout &>/dev/null; then
20+
run_timeout() { gtimeout "$@"; }
21+
else
22+
run_timeout() {
23+
local t=$1; shift
24+
"$@" &
25+
local pid=$!
26+
( sleep "$t" && kill "$pid" 2>/dev/null ) &
27+
local watcher=$!
28+
wait "$pid" 2>/dev/null
29+
local rc=$?
30+
kill "$watcher" 2>/dev/null
31+
wait "$watcher" 2>/dev/null
32+
# If the process was killed by our watcher (SIGTERM = 143) mimic the
33+
# standard timeout exit code of 124 so the caller can detect timeouts.
34+
[[ $rc -eq 143 ]] && return 124
35+
return $rc
36+
}
37+
fi
38+
1339
files=$(find $SCRIPT_DIR -iname "*.gravity" | grep -v disabled)
1440
tests_total=$(echo "$files" | wc -l)
1541
tests_success=0
@@ -24,7 +50,7 @@ for test in $files; do
2450
if [[ "$test" =~ "mem" || "$test" =~ "recursion" ]]; then
2551
timeout=10
2652
fi
27-
timeout $timeout "$GRAVITY_BIN" "$test"
53+
run_timeout $timeout "$GRAVITY_BIN" "$test"
2854
res=$?
2955
if [[ $res -eq 0 ]]; then
3056
tests_success=$(($tests_success+1))

0 commit comments

Comments
 (0)