summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFabrice Bellard <fabrice@bellard.org>2025-04-05 18:05:15 +0200
committerFabrice Bellard <fabrice@bellard.org>2025-04-05 18:06:08 +0200
commit159fe289e3b26727f7d85ce062689afb668230c1 (patch)
tree6f6fe163432c5320a38e4e817e6b0f9faeb09267
parentc1bf4e99db34ab123a7da0cc6892aa5523ed406d (diff)
downloadquickjs-159fe289e3b26727f7d85ce062689afb668230c1.tar.gz
quickjs-159fe289e3b26727f7d85ce062689afb668230c1.zip
fixed module cyclic imports (#329)
-rw-r--r--Makefile1
-rw-r--r--quickjs.c60
-rw-r--r--tests/assert.js49
-rw-r--r--tests/fixture_cyclic_import.js2
-rw-r--r--tests/test_cyclic_import.js12
5 files changed, 110 insertions, 14 deletions
diff --git a/Makefile b/Makefile
index 3655b29..a3abc98 100644
--- a/Makefile
+++ b/Makefile
@@ -435,6 +435,7 @@ test: qjs
./qjs tests/test_bigint.js
./qjs tests/test_std.js
./qjs tests/test_worker.js
+ ./qjs tests/test_cyclic_import.js
ifdef CONFIG_SHARED_LIBS
./qjs tests/test_bjson.js
./qjs examples/test_point.js
diff --git a/quickjs.c b/quickjs.c
index ec81c2c..1df5eda 100644
--- a/quickjs.c
+++ b/quickjs.c
@@ -7428,12 +7428,14 @@ static int JS_AutoInitProperty(JSContext *ctx, JSObject *p, JSAtom prop,
JSValue val;
JSContext *realm;
JSAutoInitFunc *func;
-
+ JSAutoInitIDEnum id;
+
if (js_shape_prepare_update(ctx, p, &prs))
return -1;
realm = js_autoinit_get_realm(pr);
- func = js_autoinit_func_table[js_autoinit_get_id(pr)];
+ id = js_autoinit_get_id(pr);
+ func = js_autoinit_func_table[id];
/* 'func' shall not modify the object properties 'pr' */
val = func(realm, p, prop, pr->u.init.opaque);
js_autoinit_free(ctx->rt, pr);
@@ -7441,7 +7443,15 @@ static int JS_AutoInitProperty(JSContext *ctx, JSObject *p, JSAtom prop,
pr->u.value = JS_UNDEFINED;
if (JS_IsException(val))
return -1;
- pr->u.value = val;
+ if (id == JS_AUTOINIT_ID_MODULE_NS &&
+ JS_VALUE_GET_TAG(val) == JS_TAG_STRING) {
+ /* WARNING: a varref is returned as a string ! */
+ prs->flags |= JS_PROP_VARREF;
+ pr->u.var_ref = JS_VALUE_GET_PTR(val);
+ pr->u.var_ref->header.ref_count++;
+ } else {
+ pr->u.value = val;
+ }
return 0;
}
@@ -27600,7 +27610,7 @@ static void js_resolve_export_throw_error(JSContext *ctx,
typedef enum {
EXPORTED_NAME_AMBIGUOUS,
EXPORTED_NAME_NORMAL,
- EXPORTED_NAME_NS,
+ EXPORTED_NAME_DELAYED,
} ExportedNameEntryEnum;
typedef struct ExportedNameEntry {
@@ -27609,7 +27619,6 @@ typedef struct ExportedNameEntry {
union {
JSExportEntry *me; /* using when the list is built */
JSVarRef *var_ref; /* EXPORTED_NAME_NORMAL */
- JSModuleDef *module; /* for EXPORTED_NAME_NS */
} u;
} ExportedNameEntry;
@@ -27719,7 +27728,29 @@ static JSValue js_module_ns_autoinit(JSContext *ctx, JSObject *p, JSAtom atom,
void *opaque)
{
JSModuleDef *m = opaque;
- return JS_GetModuleNamespace(ctx, m);
+ JSResolveResultEnum res;
+ JSExportEntry *res_me;
+ JSModuleDef *res_m;
+ JSVarRef *var_ref;
+
+ res = js_resolve_export(ctx, &res_m, &res_me, m, atom);
+ if (res != JS_RESOLVE_RES_FOUND) {
+ /* fail safe: normally no error should happen here except for memory */
+ js_resolve_export_throw_error(ctx, res, m, atom);
+ return JS_EXCEPTION;
+ }
+ if (res_me->local_name == JS_ATOM__star_) {
+ return JS_GetModuleNamespace(ctx, res_m->req_module_entries[res_me->u.req_module_idx].module);
+ } else {
+ if (res_me->u.local.var_ref) {
+ var_ref = res_me->u.local.var_ref;
+ } else {
+ JSObject *p1 = JS_VALUE_GET_OBJ(res_m->func_obj);
+ var_ref = p1->u.func.var_refs[res_me->u.local.var_idx];
+ }
+ /* WARNING: a varref is returned as a string ! */
+ return JS_MKPTR(JS_TAG_STRING, var_ref);
+ }
}
static JSValue js_build_module_ns(JSContext *ctx, JSModuleDef *m)
@@ -27764,17 +27795,18 @@ static JSValue js_build_module_ns(JSContext *ctx, JSModuleDef *m)
en->export_type = EXPORTED_NAME_AMBIGUOUS;
} else {
if (res_me->local_name == JS_ATOM__star_) {
- en->export_type = EXPORTED_NAME_NS;
- en->u.module = res_m->req_module_entries[res_me->u.req_module_idx].module;
+ en->export_type = EXPORTED_NAME_DELAYED;
} else {
- en->export_type = EXPORTED_NAME_NORMAL;
if (res_me->u.local.var_ref) {
en->u.var_ref = res_me->u.local.var_ref;
} else {
JSObject *p1 = JS_VALUE_GET_OBJ(res_m->func_obj);
- p1 = JS_VALUE_GET_OBJ(res_m->func_obj);
en->u.var_ref = p1->u.func.var_refs[res_me->u.local.var_idx];
}
+ if (en->u.var_ref == NULL)
+ en->export_type = EXPORTED_NAME_DELAYED;
+ else
+ en->export_type = EXPORTED_NAME_NORMAL;
}
}
}
@@ -27798,13 +27830,13 @@ static JSValue js_build_module_ns(JSContext *ctx, JSModuleDef *m)
pr->u.var_ref = var_ref;
}
break;
- case EXPORTED_NAME_NS:
- /* the exported namespace must be created on demand */
+ case EXPORTED_NAME_DELAYED:
+ /* the exported namespace or reference may depend on
+ circular references, so we resolve it lazily */
if (JS_DefineAutoInitProperty(ctx, obj,
en->export_name,
JS_AUTOINIT_ID_MODULE_NS,
- en->u.module, JS_PROP_ENUMERABLE | JS_PROP_WRITABLE) < 0)
- goto fail;
+ m, JS_PROP_ENUMERABLE | JS_PROP_WRITABLE) < 0)
break;
default:
break;
diff --git a/tests/assert.js b/tests/assert.js
new file mode 100644
index 0000000..c8240c8
--- /dev/null
+++ b/tests/assert.js
@@ -0,0 +1,49 @@
+export function assert(actual, expected, message) {
+ if (arguments.length === 1)
+ expected = true;
+
+ if (typeof actual === typeof expected) {
+ if (actual === expected) {
+ if (actual !== 0 || (1 / actual) === (1 / expected))
+ return;
+ }
+ if (typeof actual === 'number') {
+ if (isNaN(actual) && isNaN(expected))
+ return;
+ }
+ if (typeof actual === 'object') {
+ if (actual !== null && expected !== null
+ && actual.constructor === expected.constructor
+ && actual.toString() === expected.toString())
+ return;
+ }
+ }
+ throw Error("assertion failed: got |" + actual + "|" +
+ ", expected |" + expected + "|" +
+ (message ? " (" + message + ")" : ""));
+}
+
+export function assertThrows(err, func)
+{
+ var ex;
+ ex = false;
+ try {
+ func();
+ } catch(e) {
+ ex = true;
+ assert(e instanceof err);
+ }
+ assert(ex, true, "exception expected");
+}
+
+export function assertArrayEquals(a, b)
+{
+ if (!Array.isArray(a) || !Array.isArray(b))
+ return assert(false);
+
+ assert(a.length, b.length);
+
+ a.forEach((value, idx) => {
+ assert(b[idx], value);
+ });
+}
diff --git a/tests/fixture_cyclic_import.js b/tests/fixture_cyclic_import.js
new file mode 100644
index 0000000..bac80d8
--- /dev/null
+++ b/tests/fixture_cyclic_import.js
@@ -0,0 +1,2 @@
+import * as a from "./test_cyclic_import.js"
+export function f(x) { return 2 * a.g(x) }
diff --git a/tests/test_cyclic_import.js b/tests/test_cyclic_import.js
new file mode 100644
index 0000000..bf51d9b
--- /dev/null
+++ b/tests/test_cyclic_import.js
@@ -0,0 +1,12 @@
+/*---
+negative:
+ phase: resolution
+ type: SyntaxError
+---*/
+// FIXME(bnoordhuis) shouldn't throw SyntaxError but that's still better
+// than segfaulting, see https://github.com/quickjs-ng/quickjs/issues/567
+import {assert} from "./assert.js"
+import {f} from "./fixture_cyclic_import.js"
+export {f}
+export function g(x) { return x + 1 }
+assert(f(1), 4)