Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ make # release build -> src/eigenscript (HTTP/MODEL/DB off)
make test # build + full suite (tests/run_all_tests.sh)
make asan # ASan+UBSan build (same binary path!)
make http # http+model variant — run tests/test_http_server.sh
make zlib # DEFLATE codecs (inflate/deflate builtins) via system zlib (-lz)
make jit-smoke # standalone emitter tests (jit_smoke.c stubs all helpers)
make freestanding-check # 2-stage symbol gate for the EigenOS profile (docs/FREESTANDING.md)
make freestanding-libc-diff # mini-libc/libm vs glibc oracle (src/freestanding/)
Expand Down
16 changes: 15 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ PREFIX := $(HOME)/.local
LSP_SOURCES := $(SRC_DIR)/eigenlsp.c $(filter-out $(CLI_ONLY),$(SOURCES))
LSP_BINARY := $(SRC_DIR)/eigenlsp

.PHONY: all build full http gfx lib amalgamation tsan test install install-gfx clean coverage coverage-clean fuzz fuzz-run lsp jit-smoke embed-smoke asan valgrind pgo freestanding-check freestanding-libc-diff print-%
.PHONY: all build full http gfx zlib lib amalgamation tsan test install install-gfx clean coverage coverage-clean fuzz fuzz-run lsp jit-smoke embed-smoke asan valgrind pgo freestanding-check freestanding-libc-diff print-%

# Introspection helper: `make print-SOURCES` echoes a variable's value.
# tests/test_leak_guard.sh derives its ASan build source list from the
Expand Down Expand Up @@ -79,6 +79,20 @@ http:
$(LDFLAGS)
@echo "EigenScript $(VERSION) (http+model, no db) built. Binary: $$(du -sh $(BINARY) | cut -f1)"

# Build with the DEFLATE codecs (inflate/deflate builtins, #684) linked
# against the system zlib. Same opt-in pattern as `make http`: the
# default build stays zero-dependency and the four builtins raise
# "compiled without zlib support" there.
zlib:
$(CC) $(CFLAGS) -o $(BINARY) $(SOURCES) \
-DEIGENSCRIPT_EXT_HTTP=0 \
-DEIGENSCRIPT_EXT_MODEL=0 \
-DEIGENSCRIPT_EXT_DB=0 \
-DEIGENSCRIPT_EXT_ZLIB=1 \
-DEIGENSCRIPT_VERSION='"$(VERSION)"' \
$(LDFLAGS) -lz
@echo "EigenScript $(VERSION) (zlib) built. Binary: $$(du -sh $(BINARY) | cut -f1)"

gfx:
$(CC) $(CFLAGS) -o $(BINARY) $(SOURCES) $(SRC_DIR)/ext_gfx.c \
-DEIGENSCRIPT_EXT_HTTP=0 \
Expand Down
22 changes: 22 additions & 0 deletions docs/BUILTINS.md
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,28 @@ For serialization: reconstruct strings/floats from raw bytes (the inverse of an
Buffers also support direct indexing (`buf[i]`, `buf[i] is val`) and
compound assignment (`buf[i] += val`).

### Compression

DEFLATE codecs (#684), thin wrappers over the system zlib. Byte
representation mirrors `read_bytes`/`write_bytes`: input is a list of
ints 0–255 (values taken mod 256) or a buffer; output is always a fresh
list of ints 0–255. Corrupt or truncated input raises a catchable
`value` error; decompressed output is capped at 256 MiB (`limit`,
zip-bomb bound).

Requires the `zlib` build (`make zlib`, `-DEIGENSCRIPT_EXT_ZLIB=1
-lz`) — the minimal build stays zero-dependency: the four names are
still registered there (so `type of inflate` is `builtin` and the
sandbox allowlist can name them) but every call raises `value`:
"compiled without zlib support". Feature-detect with try/catch.

| Name | Signature | Description |
|------|-----------|-------------|
| `inflate` | `inflate of <list\|buffer>` | Raw DEFLATE decompression (windowBits −15) — the ZIP member format, so `.xlsx`/`.ods` entries are readable. Dual of `deflate`. |
| `deflate` | `deflate of <list\|buffer>` | Raw DEFLATE compression (windowBits −15, default level). Dual of `inflate`. |
| `zlib_inflate` | `zlib_inflate of <list\|buffer>` | Wrapped decompression with windowBits 15+32: auto-detects **zlib AND gzip** headers — this is what makes plain `.gz` files readable (`read_bytes of path` then `zlib_inflate`). Dual of `zlib_deflate`. |
| `zlib_deflate` | `zlib_deflate of <list\|buffer>` | zlib-wrapped compression (RFC 1950 header, default level). Dual of `zlib_inflate`. |

### JSON

| Name | Signature | Description |
Expand Down
230 changes: 230 additions & 0 deletions src/builtins.c
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@
#include "model_internal.h"
#endif

#if EIGENSCRIPT_EXT_ZLIB
#include <zlib.h>
#endif

/* How many bindings the runtime itself installs.
*
* register_builtins fills the global env from slot 0 upward and nothing in the
Expand Down Expand Up @@ -4238,6 +4242,8 @@ static const char *SANDBOX_ALLOW[] = {
"text_builder_to_string", "text_builder_part_count",
/* json (string <-> value, pure) */
"json_build", "json_decode", "json_encode", "json_path", "json_raw",
/* DEFLATE codecs (pure bytes <-> bytes; #684) */
"deflate", "inflate", "zlib_deflate", "zlib_inflate",
/* path string manipulation (no fs access) */
"path_base", "path_dir", "path_ext", "path_join",
/* type / value utilities */
Expand Down Expand Up @@ -5993,6 +5999,224 @@ Value* builtin_f64_from_bytes(Value *arg) {
return make_num(d);
}

/* ---- DEFLATE codecs (inflate/deflate, #684) ----
* Thin wrappers over the system zlib (-lz), gated behind
* EIGENSCRIPT_EXT_ZLIB — the same EIGENSCRIPT_EXT_* mechanism the http
* variant uses. Default OFF so the minimal build stays zero-dependency;
* compiled without zlib the four names stay registered but raise a
* catchable runtime error, so a script can feature-detect with
* try/catch instead of dying on "undefined variable".
*
* Byte representation mirrors read_bytes/write_bytes exactly: input is
* a list of ints 0-255 (values taken mod 256, non-numbers read as 0)
* or a VAL_BUFFER; output is always a fresh list of ints 0-255.
*
* inflate/deflate are the RAW DEFLATE pair (windowBits -15) — the ZIP
* member format, so .xlsx/.ods entries are readable. zlib_inflate/
* zlib_deflate are the zlib-wrapped pair; zlib_inflate uses windowBits
* 15+32, which auto-detects zlib AND gzip headers — that is what makes
* plain .gz files readable.
*/
#if EIGENSCRIPT_EXT_ZLIB

/* Inflate is an amplifier: a few KB of DEFLATE can expand without bound
* (zip bomb). Cap the decompressed size like the other size caps
* (read_bytes 10 MB, read_bytes_buf 512 MB): over the cap is a loud,
* catchable `limit` error, never silent truncation. 256 MiB matches the
* sandbox_run default allocation budget. */
#define EIGS_INFLATE_MAX_OUT ((unsigned long)256 * 1024 * 1024)

/* Shared argument extraction for the four codecs: accept the byte
* representations write_bytes accepts and copy them into a malloc'd
* byte array. Returns 1 on success; on a wrong-shape argument raises
* `type` and returns 0. */
static int zlib_bytes_arg(Value *arg, const char *who,
unsigned char **out, size_t *out_n) {
*out = NULL;
*out_n = 0;
int n = 0;
Value **items = NULL;
double *bufd = NULL;
if (arg && arg->type == VAL_LIST) {
n = arg->data.list.count;
items = arg->data.list.items;
} else if (arg && arg->type == VAL_BUFFER) {
n = arg->data.buffer.count;
bufd = arg->data.buffer.data;
} else {
rt_error(EK_TYPE, 0,
"%s requires a list of byte values (0-255) or a buffer, got %s",
who, val_type_name(arg ? arg->type : VAL_NULL));
return 0;
}
unsigned char *b = xmalloc((size_t)(n > 0 ? n : 1));
for (int i = 0; i < n; i++) {
double dv = items ? (items[i] && items[i]->type == VAL_NUM ? items[i]->data.num : 0.0)
: bufd[i];
b[i] = (unsigned char)((int)dv & 0xFF);
}
*out = b;
*out_n = (size_t)n;
return 1;
}

/* Wrap a finished byte buffer as the list-of-ints result value (the
* read_bytes shape). Takes ownership of nothing; caller still frees. */
static Value *zlib_bytes_result(const unsigned char *buf, unsigned long n) {
Value *result = make_list((int)n);
for (unsigned long i = 0; i < n; i++)
list_append_owned(result, make_num((double)buf[i]));
return result;
}

/* Shared inflate core. window_bits selects the wrapper (-15 raw,
* 15+32 zlib/gzip auto-detect). A corrupt or truncated stream raises a
* catchable `value` error; output over EIGS_INFLATE_MAX_OUT raises
* `limit` (the zip-bomb bound). */
static Value *zlib_inflate_impl(const char *who, int window_bits, Value *arg) {
unsigned char *src;
size_t src_n;
if (!zlib_bytes_arg(arg, who, &src, &src_n)) return make_null();

z_stream zs;
memset(&zs, 0, sizeof(zs));
if (inflateInit2(&zs, window_bits) != Z_OK) {
free(src);
rt_error(EK_INTERNAL, 0, "%s: inflateInit2 failed", who);
return make_null();
}
size_t cap = src_n * 3 + 64;
if (cap > EIGS_INFLATE_MAX_OUT) cap = EIGS_INFLATE_MAX_OUT;
unsigned char *out = xmalloc(cap);
int zrc = Z_OK;
for (;;) {
if (zs.avail_in == 0 && zs.total_in < src_n) {
/* uInt is 32-bit: feed a >4 GiB input in chunks. */
zs.next_in = src + zs.total_in;
unsigned long rem = src_n - zs.total_in;
zs.avail_in = (uInt)(rem > UINT_MAX ? UINT_MAX : rem);
}
if (zs.total_out == cap) {
if (cap >= EIGS_INFLATE_MAX_OUT) break; /* limit raise below */
size_t ncap = cap * 2;
if (ncap > EIGS_INFLATE_MAX_OUT) ncap = EIGS_INFLATE_MAX_OUT;
out = xrealloc(out, ncap);
cap = ncap;
}
zs.next_out = out + zs.total_out;
zs.avail_out = (uInt)(cap - zs.total_out);
zrc = inflate(&zs, Z_NO_FLUSH);
if (zrc == Z_STREAM_END) break;
if (zrc != Z_OK) break;
if (zs.avail_out != 0 && zs.total_in == src_n) {
/* Output not full yet zlib made no progress: input ran out
* mid-stream — truncated. */
zrc = Z_BUF_ERROR;
break;
}
}
if (zrc != Z_STREAM_END) {
if (zrc == Z_OK && zs.total_out >= EIGS_INFLATE_MAX_OUT) {
inflateEnd(&zs);
free(out);
free(src);
rt_error(EK_LIMIT, 0,
"%s: decompressed output exceeds the %lu-byte cap",
who, EIGS_INFLATE_MAX_OUT);
return make_null();
}
const char *msg = zs.msg;
inflateEnd(&zs);
free(out);
free(src);
rt_error(EK_VALUE, 0, "%s: invalid or truncated compressed stream (%s)",
who, msg ? msg : "unexpected end of input");
return make_null();
}
unsigned long n = zs.total_out;
inflateEnd(&zs);
free(src);
Value *result = zlib_bytes_result(out, n);
free(out);
return result;
}

/* Shared deflate core (dual of zlib_inflate_impl). The output buffer is
* deflateBound-sized up front, so a single Z_FINISH pass always fits. */
static Value *zlib_deflate_impl(const char *who, int window_bits, Value *arg) {
unsigned char *src;
size_t src_n;
if (!zlib_bytes_arg(arg, who, &src, &src_n)) return make_null();

z_stream zs;
memset(&zs, 0, sizeof(zs));
if (deflateInit2(&zs, Z_DEFAULT_COMPRESSION, Z_DEFLATED, window_bits,
8, Z_DEFAULT_STRATEGY) != Z_OK) {
free(src);
rt_error(EK_INTERNAL, 0, "%s: deflateInit2 failed", who);
return make_null();
}
uLong bound = deflateBound(&zs, (uLong)src_n);
unsigned char *out = xmalloc(bound > 0 ? bound : 1);
size_t pos = 0;
int zrc = Z_OK;
for (;;) {
if (zs.avail_in == 0 && pos < src_n) {
unsigned long rem = src_n - pos;
uInt chunk = (uInt)(rem > UINT_MAX ? UINT_MAX : rem);
zs.next_in = src + pos;
zs.avail_in = chunk;
pos += chunk;
}
int flush = (pos == src_n && zs.avail_in == 0) ? Z_FINISH : Z_NO_FLUSH;
zs.next_out = out + zs.total_out;
zs.avail_out = (uInt)(bound - zs.total_out);
zrc = deflate(&zs, flush);
if (zrc == Z_STREAM_END) break;
if (zrc != Z_OK && zrc != Z_BUF_ERROR) break;
if (flush == Z_FINISH) break; /* cannot happen with bound space */
}
if (zrc != Z_STREAM_END) {
const char *msg = zs.msg;
deflateEnd(&zs);
free(out);
free(src);
rt_error(EK_INTERNAL, 0, "%s: deflate failed (%s)",
who, msg ? msg : "unknown zlib error");
return make_null();
}
unsigned long n = zs.total_out;
deflateEnd(&zs);
free(src);
Value *result = zlib_bytes_result(out, n);
free(out);
return result;
}

Value* builtin_inflate(Value *arg) { return zlib_inflate_impl("inflate", -15, arg); }
Value* builtin_zlib_inflate(Value *arg) { return zlib_inflate_impl("zlib_inflate", 15 + 32, arg); }
Value* builtin_deflate(Value *arg) { return zlib_deflate_impl("deflate", -15, arg); }
Value* builtin_zlib_deflate(Value *arg) { return zlib_deflate_impl("zlib_deflate", 15, arg); }

#else /* !EIGENSCRIPT_EXT_ZLIB */

/* Minimal build: the names exist so scripts can feature-detect (and the
* sandbox allowlist can name real builtins), but every call raises a
* clear catchable error pointing at the zlib build. */
static Value *zlib_unavailable(const char *who) {
rt_error(EK_VALUE, 0,
"%s: compiled without zlib support (rebuild with `make zlib`)",
who);
return make_null();
}

Value* builtin_inflate(Value *arg) { (void)arg; return zlib_unavailable("inflate"); }
Value* builtin_zlib_inflate(Value *arg) { (void)arg; return zlib_unavailable("zlib_inflate"); }
Value* builtin_deflate(Value *arg) { (void)arg; return zlib_unavailable("deflate"); }
Value* builtin_zlib_deflate(Value *arg) { (void)arg; return zlib_unavailable("zlib_deflate"); }

#endif /* EIGENSCRIPT_EXT_ZLIB */

/* write_bytes of [path, data, append?] — write raw bytes to a file.
* `data` is a list of byte ints or a buffer (values taken mod 256). `append`
* (optional, default 0): 0 truncates the file, nonzero appends. Returns the
Expand Down Expand Up @@ -6963,6 +7187,12 @@ void register_builtins(Env *env) {
env_set_local_owned(env, "str_from_bytes", make_builtin(builtin_str_from_bytes));
env_set_local_owned(env, "f64_to_bytes", make_builtin(builtin_f64_to_bytes));
env_set_local_owned(env, "f64_from_bytes", make_builtin(builtin_f64_from_bytes));
/* DEFLATE codecs (#684) — registered unconditionally; the bodies
* raise "compiled without zlib support" when EIGENSCRIPT_EXT_ZLIB=0. */
env_set_local_owned(env, "inflate", make_builtin(builtin_inflate));
env_set_local_owned(env, "zlib_inflate", make_builtin(builtin_zlib_inflate));
env_set_local_owned(env, "deflate", make_builtin(builtin_deflate));
env_set_local_owned(env, "zlib_deflate", make_builtin(builtin_zlib_deflate));
env_set_local_owned(env, "buf_copy", make_builtin(builtin_buf_copy));
env_set_local_owned(env, "buf_mix", make_builtin(builtin_buf_mix));
env_set_local_owned(env, "buf_scale_range", make_builtin(builtin_buf_scale_range));
Expand Down
7 changes: 7 additions & 0 deletions src/eigenscript.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@
#ifndef EIGENSCRIPT_EXT_GFX
#define EIGENSCRIPT_EXT_GFX 0
#endif
/* DEFLATE codecs (inflate/deflate via the system zlib, -lz). Default OFF
* like GFX: the minimal build stays zero-dependency — the four builtins
* stay registered but raise "compiled without zlib support" until
* `make zlib` opts in (same EIGENSCRIPT_EXT_* gating mechanism as http). */
#ifndef EIGENSCRIPT_EXT_ZLIB
#define EIGENSCRIPT_EXT_ZLIB 0
#endif

/* Freestanding profile (docs/FREESTANDING.md) — the no-libc/EigenOS
* carve-out. Compiles out everything that needs a host OS beyond the
Expand Down
41 changes: 41 additions & 0 deletions tests/run_all_tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2424,6 +2424,47 @@ else
fi
echo ""

# [123] DEFLATE codecs (inflate/deflate + zlib-wrapped duals, #684) —
# probe-gated like [44] HTTP: the minimal build keeps the names as
# "compiled without zlib support" stubs (zero-dependency posture), so the
# real-codec suite only runs under the `make zlib` binary.
ZLIB_PROBE_FILE=$(mktemp /tmp/eigs_zlib_probe_XXXXXX.eigs)
cat > "$ZLIB_PROBE_FILE" <<'PROBE'
d is deflate of [0]
print of d
PROBE
ZLIB_PROBE_OUT=$(./eigenscript "$ZLIB_PROBE_FILE" 2>&1)
rm -f "$ZLIB_PROBE_FILE"

if ! echo "$ZLIB_PROBE_OUT" | grep -q "compiled without zlib support"; then
echo "[123] DEFLATE Codecs (#684, 22 checks)"
INF_OUTPUT=$(./eigenscript ../tests/test_inflate.eigs 2>&1); INF_OUTPUT_RC=$?
if rc_ok "$INF_OUTPUT_RC" "$INF_OUTPUT" && echo "$INF_OUTPUT" | grep -q "DEFLATE_ALL_PASS"; then
TOTAL=$((TOTAL + 22))
PASS=$((PASS + 22))
echo " PASS: all 22 inflate/deflate checks"
else
TOTAL=$((TOTAL + 22))
FAIL=$((FAIL + 22))
echo " FAIL: inflate/deflate tests"
echo "$INF_OUTPUT" | grep -iE "assert|error|FAIL" | head -5
fi
echo ""
else
# Minimal build: the four names stay registered but must raise the
# documented catchable error (the zero-dependency gating contract).
echo "[123] DEFLATE Codecs (#684) — minimal build, stub check (1 check)"
TOTAL=$((TOTAL + 1))
if echo "$ZLIB_PROBE_OUT" | grep -q "deflate: compiled without zlib support"; then
PASS=$((PASS + 1))
echo " PASS: zlib-gated stub raises 'compiled without zlib support'"
else
FAIL=$((FAIL + 1))
echo " FAIL: zlib stub missing or mis-phrased (probe: '$ZLIB_PROBE_OUT')"
fi
echo ""
fi

# [65] sort_by builtin
echo "[65] Sort By (9 checks)"
SBY_OUTPUT=$(./eigenscript ../tests/test_sort_by.eigs 2>&1); SBY_OUTPUT_RC=$?
Expand Down
Loading
Loading