Files
scriptc/packages/compiler/native/comptime.c
Chris Tate ee9f0d64eb feat: ship the self-hosted native scriptc CLI (#585)
* feat: ship the self-hosted native scriptc CLI

- Run the shared compiler and CLI natively, including library builds and compile-time evaluation.
- Install native platform packages and ship relocatable standalone distributions.
- Preserve program semantics and reuse validated build artifacts for fast rebuilds.

* fix: complete native CLI integration and validation

- Preserve library diagnostics and run recursive compiler work on the main stack.
- Restore packaged helper permissions and exercise native npm and Wasm library builds.
- Recover interrupted Sandbox status probes within the original command deadline.
2026-09-30 02:17:56 -05:00

190 lines
7.4 KiB
C

/* Isolated JavaScript evaluation for comptime callbacks. No filesystem,
* process, module loader, or console bindings enter the guest context. */
#ifndef _WIN32
#define _POSIX_C_SOURCE 200809L
#endif
#include "quickjs.h"
#include <errno.h>
#include <math.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#ifdef _WIN32
#include <windows.h>
#else
#include <time.h>
#endif
static uint64_t now_ms(void) {
#ifdef _WIN32
return GetTickCount64();
#else
struct timespec ts;
if (clock_gettime(CLOCK_MONOTONIC, &ts) != 0) return 0;
return (uint64_t)ts.tv_sec * 1000 + (uint64_t)ts.tv_nsec / 1000000;
#endif
}
typedef struct { uint64_t deadline; int interrupted; } Budget;
static int interrupt(JSRuntime *rt, void *opaque) {
(void)rt;
Budget *budget = opaque;
if (now_ms() >= budget->deadline) budget->interrupted = 1;
return budget->interrupted;
}
typedef struct {
char *data;
size_t length, capacity;
void *active[512];
size_t depth;
} Output;
static int append(Output *out, const char *text) {
size_t size = strlen(text);
if (size > 64 * 1024 * 1024 - out->length) return -1;
size_t required = out->length + size + 1;
if (required > out->capacity) {
size_t capacity = out->capacity ? out->capacity : 1024;
while (capacity < required) capacity *= 2;
char *data = realloc(out->data, capacity);
if (!data) return -1;
out->data = data;
out->capacity = capacity;
}
memcpy(out->data + out->length, text, size + 1);
out->length += size;
return 0;
}
static int quote(JSContext *ctx, Output *out, JSValueConst value) {
size_t length;
const uint16_t *text = JS_ToCStringLenUTF16(ctx, &length, value);
if (!text) return -1;
int status = append(out, "\"");
for (size_t i = 0; status == 0 && i < length; i++) {
char escaped[7];
unsigned ch = text[i];
if (ch >= 32 && ch < 127 && ch != '"' && ch != '\\') {
escaped[0] = (char)ch; escaped[1] = 0;
} else snprintf(escaped, sizeof(escaped), "\\u%04x", ch);
status = append(out, escaped);
}
JS_FreeCStringUTF16(ctx, text);
return status == 0 ? append(out, "\"") : status;
}
/* Serialize through engine APIs, outside the callback's mutable globals.
* The guest cannot replace JSON.stringify or install a toJSON hook that
* changes the value the compiler validates. */
static int encode(JSContext *ctx, Output *out, JSValueConst value) {
if (JS_IsException(value)) return -1;
if (JS_IsUndefined(value)) return append(out, "[\"undefined\"]");
if (JS_IsNull(value)) return append(out, "[\"null\"]");
if (JS_IsBool(value)) return append(out, JS_ToBool(ctx, value) ? "[\"boolean\",true]" : "[\"boolean\",false]");
if (JS_IsFunction(ctx, value)) return append(out, "[\"function\"]");
if (JS_IsSymbol(value)) return append(out, "[\"symbol\"]");
if (JS_IsString(value) || JS_IsNumber(value) || JS_IsBigInt(value)) {
const char *tag = JS_IsString(value) ? "[\"string\"," : JS_IsNumber(value) ? "[\"number\"," : "[\"bigint\",";
if (append(out, tag) != 0) return -1;
double number = 1;
if (JS_IsNumber(value) && JS_ToFloat64(ctx, &number, value) < 0) return -1;
int status = number == 0 && signbit(number) ? append(out, "\"-0\"") : quote(ctx, out, value);
return status == 0 ? append(out, "]") : status;
}
if (out->depth == 512) { JS_ThrowTypeError(ctx, "compile-time result is nested too deeply"); return -1; }
void *identity = JS_VALUE_GET_PTR(value);
for (size_t i = 0; i < out->depth; i++) {
if (out->active[i] == identity) { JS_ThrowTypeError(ctx, "cyclic compile-time result"); return -1; }
}
out->active[out->depth++] = identity;
int status = 0;
if (JS_IsArray(value)) {
int64_t length = 0;
if (JS_GetLength(ctx, value, &length) < 0 || length < 0 || length > 1000000) status = -1;
if (status == 0) status = append(out, "[\"array\",[");
for (int64_t i = 0; status == 0 && i < length; i++) {
if (i != 0) status = append(out, ",");
JSValue item = JS_GetPropertyUint32(ctx, value, (uint32_t)i);
if (status == 0) status = encode(ctx, out, item);
JS_FreeValue(ctx, item);
}
} else {
JSPropertyEnum *properties = NULL;
uint32_t length = 0;
status = JS_GetOwnPropertyNames(ctx, &properties, &length, value, JS_GPN_STRING_MASK | JS_GPN_ENUM_ONLY);
if (status == 0) status = append(out, "[\"object\",[");
for (uint32_t i = 0; status == 0 && i < length; i++) {
if (i != 0) status = append(out, ",");
if (status == 0) status = append(out, "[");
JSValue key = JS_AtomToString(ctx, properties[i].atom);
if (status == 0) status = quote(ctx, out, key);
JS_FreeValue(ctx, key);
if (status == 0) status = append(out, ",");
JSValue item = JS_GetProperty(ctx, value, properties[i].atom);
if (status == 0) status = encode(ctx, out, item);
JS_FreeValue(ctx, item);
if (status == 0) status = append(out, "]");
}
JS_FreePropertyEnum(ctx, properties, length);
}
out->depth--;
return status == 0 ? append(out, "]]") : status;
}
int main(int argc, char **argv) {
if (argc != 3) { fputs("usage: scriptc-comptime <javascript> <timeout-ms>\n", stderr); return 2; }
char *end = NULL;
errno = 0;
long timeout = strtol(argv[2], &end, 10);
if (errno || !end || *end || timeout < 1 || timeout > 60000) {
fputs("invalid compile-time budget\n", stderr); return 2;
}
FILE *file = fopen(argv[1], "rb");
if (!file) { perror("compile-time input"); return 2; }
if (fseek(file, 0, SEEK_END) != 0) { fclose(file); return 2; }
long size = ftell(file);
if (size < 0 || size > 16 * 1024 * 1024 || fseek(file, 0, SEEK_SET) != 0) { fclose(file); return 2; }
char *source = malloc((size_t)size + 1);
if (!source) { fclose(file); return 2; }
if (fread(source, 1, (size_t)size, file) != (size_t)size) { free(source); fclose(file); return 2; }
fclose(file);
source[size] = 0;
JSRuntime *rt = JS_NewRuntime();
if (!rt) { free(source); return 2; }
JS_SetMemoryLimit(rt, 256 * 1024 * 1024);
JS_SetMaxStackSize(rt, 2 * 1024 * 1024);
Budget budget = { now_ms() + (uint64_t)timeout, 0 };
JS_SetInterruptHandler(rt, interrupt, &budget);
JSContext *ctx = JS_NewContext(rt);
if (!ctx) { JS_FreeRuntime(rt); free(source); return 2; }
JSValue result = JS_Eval(ctx, source, (size_t)size, "comptime", JS_EVAL_TYPE_GLOBAL);
free(source);
int status = 0;
Output out = {0};
if (JS_IsException(result) || encode(ctx, &out, result) != 0) {
free(out.data);
memset(&out, 0, sizeof(out));
JSValue exception = JS_GetException(ctx);
if (budget.interrupted) status = append(&out, "[\"error\",\"ERR_SCRIPT_EXECUTION_TIMEOUT\",\"compile-time evaluation timed out\"]");
else {
JSValue message = JS_GetPropertyStr(ctx, exception, "message");
JSValue detail = JS_IsUndefined(message) ? JS_IsNull(exception)
? JS_NewString(ctx, "compile-time result exceeds the output limit") : JS_DupValue(ctx, exception) : JS_DupValue(ctx, message);
status = append(&out, "[\"error\",\"\",");
if (status == 0) status = quote(ctx, &out, detail);
if (status == 0) status = append(&out, "]");
JS_FreeValue(ctx, detail);
JS_FreeValue(ctx, message);
}
JS_FreeValue(ctx, exception);
}
if (status == 0 && (fwrite(out.data, 1, out.length, stdout) != out.length || fputc('\n', stdout) == EOF)) status = 2;
free(out.data);
JS_FreeValue(ctx, result);
JS_FreeContext(ctx);
JS_FreeRuntime(rt);
return status == 0 ? 0 : 2;
}