diff --git a/.obj-rt/cutils.bytecode.lto.o b/.obj-rt/cutils.bytecode.lto.o new file mode 100644 index 000000000..e3de041bd Binary files /dev/null and b/.obj-rt/cutils.bytecode.lto.o differ diff --git a/.obj-rt/cutils.bytecode.lto.o.d b/.obj-rt/cutils.bytecode.lto.o.d new file mode 100644 index 000000000..4025fc9e8 --- /dev/null +++ b/.obj-rt/cutils.bytecode.lto.o.d @@ -0,0 +1 @@ +.obj-rt/cutils.bytecode.lto.o: cutils.c cutils.h diff --git a/.obj-rt/dtoa.bytecode.lto.o b/.obj-rt/dtoa.bytecode.lto.o new file mode 100644 index 000000000..439d135ca Binary files /dev/null and b/.obj-rt/dtoa.bytecode.lto.o differ diff --git a/.obj-rt/dtoa.bytecode.lto.o.d b/.obj-rt/dtoa.bytecode.lto.o.d new file mode 100644 index 000000000..18ab15d87 --- /dev/null +++ b/.obj-rt/dtoa.bytecode.lto.o.d @@ -0,0 +1 @@ +.obj-rt/dtoa.bytecode.lto.o: dtoa.c cutils.h dtoa.h diff --git a/.obj-rt/libregexp.bytecode.lto.o b/.obj-rt/libregexp.bytecode.lto.o new file mode 100644 index 000000000..54b8e05cd Binary files /dev/null and b/.obj-rt/libregexp.bytecode.lto.o differ diff --git a/.obj-rt/libregexp.bytecode.lto.o.d b/.obj-rt/libregexp.bytecode.lto.o.d new file mode 100644 index 000000000..27ad3a9ef --- /dev/null +++ b/.obj-rt/libregexp.bytecode.lto.o.d @@ -0,0 +1,2 @@ +.obj-rt/libregexp.bytecode.lto.o: libregexp.c cutils.h libregexp.h \ + libunicode.h libregexp-opcode.h diff --git a/.obj-rt/libunicode.bytecode.lto.o b/.obj-rt/libunicode.bytecode.lto.o new file mode 100644 index 000000000..bb5ebfddf Binary files /dev/null and b/.obj-rt/libunicode.bytecode.lto.o differ diff --git a/.obj-rt/libunicode.bytecode.lto.o.d b/.obj-rt/libunicode.bytecode.lto.o.d new file mode 100644 index 000000000..a5c5103dc --- /dev/null +++ b/.obj-rt/libunicode.bytecode.lto.o.d @@ -0,0 +1,2 @@ +.obj-rt/libunicode.bytecode.lto.o: libunicode.c cutils.h libunicode.h \ + libunicode-table.h diff --git a/.obj-rt/quickjs-libc.bytecode.lto.o b/.obj-rt/quickjs-libc.bytecode.lto.o new file mode 100644 index 000000000..ed6ef0664 Binary files /dev/null and b/.obj-rt/quickjs-libc.bytecode.lto.o differ diff --git a/.obj-rt/quickjs-libc.bytecode.lto.o.d b/.obj-rt/quickjs-libc.bytecode.lto.o.d new file mode 100644 index 000000000..6851cf8d7 --- /dev/null +++ b/.obj-rt/quickjs-libc.bytecode.lto.o.d @@ -0,0 +1,2 @@ +.obj-rt/quickjs-libc.bytecode.lto.o: quickjs-libc.c cutils.h list.h \ + quickjs-libc.h quickjs.h diff --git a/.obj-rt/quickjs.bytecode.lto.o b/.obj-rt/quickjs.bytecode.lto.o new file mode 100644 index 000000000..e8c32aceb Binary files /dev/null and b/.obj-rt/quickjs.bytecode.lto.o differ diff --git a/.obj-rt/quickjs.bytecode.lto.o.d b/.obj-rt/quickjs.bytecode.lto.o.d new file mode 100644 index 000000000..2a5d905e1 --- /dev/null +++ b/.obj-rt/quickjs.bytecode.lto.o.d @@ -0,0 +1,2 @@ +.obj-rt/quickjs.bytecode.lto.o: quickjs.c cutils.h list.h quickjs.h \ + libregexp.h libunicode.h dtoa.h quickjs-atom.h quickjs-opcode.h diff --git a/Makefile b/Makefile index 95885e374..ade95402d 100644 --- a/Makefile +++ b/Makefile @@ -59,6 +59,7 @@ TEST262_COMMIT?=5c8206929d81b2d3d727ca6aac56c18358c8d790 TEST262_SINCE?=2025-09-01 OBJDIR=.obj +OBJDIR_RT=.obj-rt ifdef CONFIG_ASAN OBJDIR:=$(OBJDIR)/asan @@ -257,6 +258,10 @@ LIBS+=$(EXTRA_LIBS) $(OBJDIR): mkdir -p $(OBJDIR) $(OBJDIR)/examples $(OBJDIR)/tests +$(OBJDIR_RT): + mkdir -p $(OBJDIR_RT) + mkdir -p $(OBJDIR_RT)/examples $(OBJDIR_RT)/tests + qjs$(EXE): $(QJS_OBJS) $(CC) $(LDFLAGS) $(LDEXPORT) -o $@ $^ $(LIBS) @@ -311,6 +316,15 @@ endif # CONFIG_LTO libquickjs.fuzz.a: $(patsubst %.o, %.fuzz.o, $(QJS_LIB_OBJS)) $(AR) rcs $@ $^ +QJS_BYTECODE_OBJS=$(patsubst $(OBJDIR)/%.o, $(OBJDIR_RT)/%.bytecode.o, $(QJS_LIB_OBJS)) +QJS_BYTECODE_LTO_OBJS=$(patsubst $(OBJDIR)/%.o, $(OBJDIR_RT)/%.bytecode.lto.o, $(QJS_LIB_OBJS)) + +libquickjs-bytecode.a: $(QJS_BYTECODE_OBJS) + $(AR) rcs $@ $^ + +libquickjs-bytecode.lto.a: $(QJS_BYTECODE_LTO_OBJS) + $(AR) rcs $@ $^ + repl.c: $(QJSC) repl.js $(QJSC) -s -c -o $@ -m repl.js @@ -353,6 +367,12 @@ $(OBJDIR)/%.fuzz.o: %.c | $(OBJDIR) $(OBJDIR)/%.check.o: %.c | $(OBJDIR) $(CC) $(CFLAGS) -DCONFIG_CHECK_JSVALUE -c -o $@ $< +$(OBJDIR_RT)/%.bytecode.o: %.c | $(OBJDIR_RT) + $(CC) $(CFLAGS_NOLTO) -DCONFIG_BYTECODE_ONLY_RUNTIME -MMD -MF $(OBJDIR_RT)/$(@F).d -c -o $@ $< + +$(OBJDIR_RT)/%.bytecode.lto.o: %.c | $(OBJDIR_RT) + $(CC) $(CFLAGS_OPT) -DCONFIG_BYTECODE_ONLY_RUNTIME -MMD -MF $(OBJDIR_RT)/$(@F).d -c -o $@ $< + regexp_test: libregexp.c libunicode.c cutils.c $(CC) $(LDFLAGS) $(CFLAGS) -DTEST -o $@ libregexp.c libunicode.c cutils.c $(LIBS) @@ -374,8 +394,10 @@ install: all install -m755 qjs$(EXE) qjsc$(EXE) "$(DESTDIR)$(PREFIX)/bin" mkdir -p "$(DESTDIR)$(PREFIX)/lib/quickjs" install -m644 libquickjs.a "$(DESTDIR)$(PREFIX)/lib/quickjs" + install -m644 libquickjs-bytecode.a "$(DESTDIR)$(PREFIX)/lib/quickjs" ifdef CONFIG_LTO install -m644 libquickjs.lto.a "$(DESTDIR)$(PREFIX)/lib/quickjs" + install -m644 libquickjs-bytecode.lto.a "$(DESTDIR)$(PREFIX)/lib/quickjs" endif mkdir -p "$(DESTDIR)$(PREFIX)/include/quickjs" install -m644 quickjs.h quickjs-libc.h "$(DESTDIR)$(PREFIX)/include/quickjs" @@ -511,6 +533,18 @@ testall: all test microbench test2o test2 testall-complete: testall +test-bytecode-runtime: libquickjs-bytecode.lto.a qjsc$(EXE) qjs$(EXE) + $(QJSC) -fno-eval -fno-regexp -fno-json -fno-module-loader \ + -o /tmp/test-bytecode-rt tests/test_bytecode_runtime.js + @nm /tmp/test-bytecode-rt | grep -E ' T (__JS_EvalInternal|js_parse_|js_compile_|js_evalScript|js_loadScript|js_std_parseExtJSON|js_worker_ctor)' \ + && (echo "FAIL: forbidden symbols found in bytecode-only binary" && exit 1) \ + || echo "PASS: no forbidden symbols" + @/tmp/test-bytecode-rt > /tmp/test-bytecode-rt.out + @./qjs$(EXE) tests/test_bytecode_runtime.js > /tmp/test-bytecode-rt.expected + @diff /tmp/test-bytecode-rt.out /tmp/test-bytecode-rt.expected \ + && echo "PASS: bytecode round-trip equivalence" \ + || (echo "FAIL: bytecode round-trip equivalence" && exit 1) + node-test: node tests/test_closure.js node tests/test_language.js diff --git a/qjsc.c b/qjsc.c index e55ca61ce..4d1a13b3d 100644 --- a/qjsc.c +++ b/qjsc.c @@ -21,6 +21,10 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ +#ifdef CONFIG_BYTECODE_ONLY_RUNTIME +#error "qjsc must be built with the full QuickJS engine" +#endif + #include #include #include @@ -79,6 +83,15 @@ static const FeatureEntry feature_list[] = { { "weakref", "WeakRef" }, }; +#define FE_MASK(i) ((uint64_t)1 << (i)) +#define BYTECODE_ONLY_TRIGGER_MASK \ + (FE_MASK(1) | FE_MASK(3) | FE_MASK(4) | FE_MASK(FE_MODULE_LOADER)) + +static BOOL runtime_needs_parser(void) +{ + return (feature_bitmap & BYTECODE_ONLY_TRIGGER_MASK) != 0; +} + void namelist_add(namelist_t *lp, const char *name, const char *short_name, int flags) { @@ -476,7 +489,10 @@ static int output_executable(const char *out_filename, const char *cfilename, } lto_suffix = ""; - bn_suffix = ""; + if (runtime_needs_parser()) + bn_suffix = ""; + else + bn_suffix = "-bytecode"; arg = argv; *arg++ = CONFIG_CC; diff --git a/quickjs-libc.c b/quickjs-libc.c index c24b6d53e..c33fdae21 100644 --- a/quickjs-libc.c +++ b/quickjs-libc.c @@ -431,6 +431,7 @@ uint8_t *js_load_file(JSContext *ctx, size_t *pbuf_len, const char *filename) } /* load and evaluate a file */ +#ifndef CONFIG_BYTECODE_ONLY_RUNTIME static JSValue js_loadScript(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { @@ -454,6 +455,7 @@ static JSValue js_loadScript(JSContext *ctx, JSValueConst this_val, JS_FreeCString(ctx, filename); return ret; } +#endif /* load a file as a UTF-8 encoded string */ static JSValue js_std_loadFile(JSContext *ctx, JSValueConst this_val, @@ -694,6 +696,7 @@ JSModuleDef *js_module_loader(JSContext *ctx, return NULL; } res = js_module_test_json(ctx, attributes); +#ifndef CONFIG_BYTECODE_ONLY_RUNTIME if (has_suffix(module_name, ".json") || res > 0) { /* compile as JSON or JSON5 depending on "type" */ JSValue val; @@ -723,6 +726,12 @@ JSModuleDef *js_module_loader(JSContext *ctx, m = JS_VALUE_GET_PTR(func_val); JS_FreeValue(ctx, func_val); } +#else + js_free(ctx, buf); + JS_ThrowReferenceError(ctx, "could not load module filename '%s' (bytecode only runtime)", + module_name); + return NULL; +#endif } return m; } @@ -870,6 +879,7 @@ static int get_bool_option(JSContext *ctx, BOOL *pbool, return 0; } +#ifndef CONFIG_BYTECODE_ONLY_RUNTIME static JSValue js_evalScript(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { @@ -918,6 +928,7 @@ static JSValue js_evalScript(JSContext *ctx, JSValueConst this_val, } return ret; } +#endif static JSClassID js_std_file_class_id; @@ -957,6 +968,7 @@ static JSValue js_std_strerror(JSContext *ctx, JSValueConst this_val, return JS_NewString(ctx, strerror(err)); } +#ifndef CONFIG_BYTECODE_ONLY_RUNTIME static JSValue js_std_parseExtJSON(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { @@ -971,6 +983,7 @@ static JSValue js_std_parseExtJSON(JSContext *ctx, JSValueConst this_val, JS_FreeCString(ctx, str); return obj; } +#endif static JSValue js_new_std_file(JSContext *ctx, FILE *f, BOOL close_in_finalizer, @@ -1640,8 +1653,10 @@ static const JSCFunctionListEntry js_std_error_props[] = { static const JSCFunctionListEntry js_std_funcs[] = { JS_CFUNC_DEF("exit", 1, js_std_exit ), JS_CFUNC_DEF("gc", 0, js_std_gc ), +#ifndef CONFIG_BYTECODE_ONLY_RUNTIME JS_CFUNC_DEF("evalScript", 1, js_evalScript ), JS_CFUNC_DEF("loadScript", 1, js_loadScript ), +#endif JS_CFUNC_DEF("getenv", 1, js_std_getenv ), JS_CFUNC_DEF("setenv", 1, js_std_setenv ), JS_CFUNC_DEF("unsetenv", 1, js_std_unsetenv ), @@ -1649,7 +1664,9 @@ static const JSCFunctionListEntry js_std_funcs[] = { JS_CFUNC_DEF("urlGet", 1, js_std_urlGet ), JS_CFUNC_DEF("loadFile", 1, js_std_loadFile ), JS_CFUNC_DEF("strerror", 1, js_std_strerror ), +#ifndef CONFIG_BYTECODE_ONLY_RUNTIME JS_CFUNC_DEF("parseExtJSON", 1, js_std_parseExtJSON ), +#endif /* FILE I/O */ JS_CFUNC_DEF("open", 2, js_std_open ), @@ -3578,6 +3595,7 @@ static JSClassDef js_worker_class = { .gc_mark = js_worker_mark, }; +#if defined(USE_WORKER) && !defined(CONFIG_BYTECODE_ONLY_RUNTIME) static void *worker_func(void *opaque) { WorkerFuncArgs *args = opaque; @@ -3743,6 +3761,7 @@ static JSValue js_worker_ctor(JSContext *ctx, JSValueConst new_target, JS_FreeValue(ctx, obj); return JS_EXCEPTION; } +#endif static JSValue js_worker_postMessage(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) @@ -3976,7 +3995,7 @@ static int js_os_init(JSContext *ctx, JSModuleDef *m) { os_poll_func = js_os_poll; -#ifdef USE_WORKER +#if defined(USE_WORKER) && !defined(CONFIG_BYTECODE_ONLY_RUNTIME) { JSRuntime *rt = JS_GetRuntime(ctx); JSThreadState *ts = JS_GetRuntimeOpaque(rt); @@ -4002,7 +4021,7 @@ static int js_os_init(JSContext *ctx, JSModuleDef *m) JS_SetModuleExport(ctx, m, "Worker", obj); } -#endif /* USE_WORKER */ +#endif /* USE_WORKER && !CONFIG_BYTECODE_ONLY_RUNTIME */ return JS_SetModuleExportList(ctx, m, js_os_funcs, countof(js_os_funcs)); @@ -4015,7 +4034,7 @@ JSModuleDef *js_init_module_os(JSContext *ctx, const char *module_name) if (!m) return NULL; JS_AddModuleExportList(ctx, m, js_os_funcs, countof(js_os_funcs)); -#ifdef USE_WORKER +#if defined(USE_WORKER) && !defined(CONFIG_BYTECODE_ONLY_RUNTIME) JS_AddModuleExport(ctx, m, "Worker"); #endif return m; @@ -4087,8 +4106,10 @@ void js_std_add_helpers(JSContext *ctx, int argc, char **argv) JS_SetPropertyStr(ctx, global_obj, "print", JS_NewCFunction(ctx, js_print, "print", 1)); +#ifndef CONFIG_BYTECODE_ONLY_RUNTIME JS_SetPropertyStr(ctx, global_obj, "__loadScript", JS_NewCFunction(ctx, js_loadScript, "__loadScript", 1)); +#endif JS_FreeValue(ctx, global_obj); } diff --git a/quickjs.c b/quickjs.c index 2b33bfa5f..16c6bf96f 100644 --- a/quickjs.c +++ b/quickjs.c @@ -1195,8 +1195,10 @@ static int JS_ToInt32Free(JSContext *ctx, int32_t *pres, JSValue val); static int JS_ToFloat64Free(JSContext *ctx, double *pres, JSValue val); static int JS_ToUint8ClampFree(JSContext *ctx, int32_t *pres, JSValue val); static JSValue js_new_string8_len(JSContext *ctx, const char *buf, int len); +#ifndef CONFIG_BYTECODE_ONLY_RUNTIME static JSValue js_compile_regexp(JSContext *ctx, JSValueConst pattern, JSValueConst flags); +#endif static JSValue JS_NewRegexp(JSContext *ctx, JSValue pattern, JSValue bc); static void gc_decref(JSRuntime *rt); static int JS_NewClass1(JSRuntime *rt, JSClassID class_id, @@ -35439,6 +35441,7 @@ static int add_global_variables(JSContext *ctx, JSFunctionDef *fd) /* create a function object from a function definition. The function definition is freed. All the child functions are also created. It must be done this way to resolve all the variables. */ +#ifndef CONFIG_BYTECODE_ONLY_RUNTIME static JSValue js_create_function(JSContext *ctx, JSFunctionDef *fd) { JSValue func_obj; @@ -35697,6 +35700,7 @@ static JSValue js_create_function(JSContext *ctx, JSFunctionDef *fd) js_free_function_def(ctx, fd); return JS_EXCEPTION; } +#endif static void free_function_bytecode(JSRuntime *rt, JSFunctionBytecode *b) { @@ -36493,6 +36497,7 @@ static __exception int js_parse_function_decl(JSParseState *s, JS_PARSE_EXPORT_NONE, NULL); } +#ifndef CONFIG_BYTECODE_ONLY_RUNTIME static __exception int js_parse_program(JSParseState *s) { JSFunctionDef *fd = s->cur_func; @@ -36544,6 +36549,7 @@ static __exception int js_parse_program(JSParseState *s) return 0; } +#endif static void js_parse_init(JSContext *ctx, JSParseState *s, const char *input, size_t input_len, @@ -36603,6 +36609,7 @@ JSValue JS_EvalFunction(JSContext *ctx, JSValue fun_obj) } /* 'input' must be zero terminated i.e. input[input_len] = '\0'. */ +#ifndef CONFIG_BYTECODE_ONLY_RUNTIME static JSValue __JS_EvalInternal(JSContext *ctx, JSValueConst this_obj, const char *input, size_t input_len, const char *filename, int flags, int scope_idx) @@ -36717,6 +36724,7 @@ static JSValue __JS_EvalInternal(JSContext *ctx, JSValueConst this_obj, JS_FreeValue(ctx, JS_MKPTR(JS_TAG_MODULE, m)); return JS_EXCEPTION; } +#endif /* the indirection is needed to make 'eval' optional */ static JSValue JS_EvalInternal(JSContext *ctx, JSValueConst this_obj, @@ -46870,6 +46878,7 @@ static void js_regexp_finalizer(JSRuntime *rt, JSValue val) } /* create a string containing the RegExp bytecode */ +#ifndef CONFIG_BYTECODE_ONLY_RUNTIME static JSValue js_compile_regexp(JSContext *ctx, JSValueConst pattern, JSValueConst flags) { @@ -46947,6 +46956,7 @@ static JSValue js_compile_regexp(JSContext *ctx, JSValueConst pattern, js_free(ctx, re_bytecode_buf); return ret; } +#endif /* fast regexp creation */ static JSValue JS_NewRegexp(JSContext *ctx, JSValue pattern, JSValue bc) @@ -47103,7 +47113,11 @@ static JSValue js_regexp_constructor(JSContext *ctx, JSValueConst new_target, obj = js_create_from_ctor(ctx, new_target, JS_CLASS_REGEXP); if (JS_IsException(obj)) goto fail; +#ifndef CONFIG_BYTECODE_ONLY_RUNTIME bc = js_compile_regexp(ctx, pattern, flags); +#else + bc = JS_UNDEFINED; +#endif if (JS_IsException(bc)) goto fail; JS_FreeValue(ctx, flags); @@ -47142,7 +47156,11 @@ static JSValue js_regexp_compile(JSContext *ctx, JSValueConst this_val, pattern = JS_ToString(ctx, pattern1); if (JS_IsException(pattern)) goto fail; +#ifndef CONFIG_BYTECODE_ONLY_RUNTIME bc = js_compile_regexp(ctx, pattern, flags1); +#else + bc = JS_UNDEFINED; +#endif if (JS_IsException(bc)) goto fail; } @@ -48566,7 +48584,9 @@ static const JSCFunctionListEntry js_regexp_string_iterator_proto_funcs[] = { void JS_AddIntrinsicRegExpCompiler(JSContext *ctx) { +#ifndef CONFIG_BYTECODE_ONLY_RUNTIME ctx->compile_regexp = js_compile_regexp; +#endif } int JS_AddIntrinsicRegExp(JSContext *ctx) @@ -55479,7 +55499,9 @@ int JS_AddIntrinsicDate(JSContext *ctx) int JS_AddIntrinsicEval(JSContext *ctx) { +#ifndef CONFIG_BYTECODE_ONLY_RUNTIME ctx->eval_internal = __JS_EvalInternal; +#endif return 0; } diff --git a/tests/test_bytecode_runtime.js b/tests/test_bytecode_runtime.js new file mode 100644 index 000000000..660ea3fc7 --- /dev/null +++ b/tests/test_bytecode_runtime.js @@ -0,0 +1,35 @@ + +console.log("Hello from bytecode runtime!"); + +function fib(n) { + if (n <= 1) return n; + return fib(n - 1) + fib(n - 2); +} + +console.log("fib(10) =", fib(10)); + +const obj = { + a: 1, + b: [1, 2, 3], + c: { d: "hello" } +}; + +console.log("obj.c.d =", obj.c.d); + +// test Function.prototype.toString +function testFunc() { return 42; } +console.log("testFunc.toString().length > 0:", testFunc.toString().length > 0); + +// test class +class Point { + constructor(x, y) { + this.x = x; + this.y = y; + } + toString() { + return `(${this.x}, ${this.y})`; + } +} + +const p = new Point(10, 20); +console.log("p.toString() =", p.toString());