diff --git a/internal/engine.cm b/internal/engine.cm index 0221bcd5..9694f0cd 100644 --- a/internal/engine.cm +++ b/internal/engine.cm @@ -72,7 +72,7 @@ function use_core(path) { var script = text(script_blob) var mod = `(function setup_module(use){${script}})` var fn = js.eval('core:' + path, mod) - var result = fn.call(sym, use_core); + var result = call(fn,sym, use_core) use_cache[cache_key] = result; return result; } @@ -432,7 +432,7 @@ $_.start = function start(cb, program, ...args) { if (!program) return var id = guid() - if (args.length == 1 && Array.isArray(args[0])) args = args[0] + if (args.length == 1 && is_array(args[0])) args = args[0] var startup = { id, overling: $_.self, @@ -786,7 +786,7 @@ $_.clock(_ => { // Call with signature: setup_module(args, use, ...capabilities) // The script wrapper builds $_ from the injected capabilities for backward compatibility - var val = locator.symbol.call(null, _cell.args.arg, use_fn, ...vals) + var val = call(locator.symbol, null, _cell.args.arg, use_fn, ...vals) if (val) throw new Error('Program must not return anything'); diff --git a/internal/shop.cm b/internal/shop.cm index 1c0cd8f7..b59acd72 100644 --- a/internal/shop.cm +++ b/internal/shop.cm @@ -782,6 +782,12 @@ function make_use_fn(pkg) { } } +// Call a C module loader and execute the entrypoint +function call_c_module(c_resolve) { + var entry = c_resolve.symbol() // load symbol (returns function) + return entry(null, my$_) // execute it to get exports/context +} + function execute_module(info) { var c_resolve = info.c_resolve @@ -792,25 +798,30 @@ function execute_module(info) if (mod_resolve.scope < 900) { var context = null if (c_resolve.scope < 900) { - context = c_resolve.symbol(null, my$_) + context = call_c_module(c_resolve) } - + // Get file info to determine inject list var file_info = Shop.file_info(mod_resolve.path) var inject = Shop.script_inject_for(file_info) var vals = inject_values(inject) var pkg = file_info.package var use_fn = make_use_fn(pkg) - + // Call with signature: setup_module(args, use, ...capabilities) // args is null for module loading - used = mod_resolve.symbol.call(context, null, use_fn, ...vals) + used = call(mod_resolve.symbol, context, null, use_fn, ...vals) } else if (c_resolve.scope < 900) { // C only - used = c_resolve.symbol(null, my$_) + used = call_c_module(c_resolve) } else { throw new Error(`Module ${info.path} could not be found`) - } if (!used) + } + + if (is_function(used)) + throw new Error('C module loader returned a function; did you forget to call it?') + + if (!used) throw new Error(`Module ${info} returned null`) // stone(used) diff --git a/source/quickjs.c b/source/quickjs.c index 65dacd75..04cbd34c 100644 --- a/source/quickjs.c +++ b/source/quickjs.c @@ -13252,6 +13252,15 @@ static JSValue JS_CallInternal_OLD(JSContext *caller_ctx, JSValueConst func_obj, { JSValue val; + /* User-defined functions don't support property access in cell script */ + if (JS_VALUE_GET_TAG(sp[-1]) == JS_TAG_OBJECT) { + JSObject *fp = JS_VALUE_GET_OBJ(sp[-1]); + if (fp->class_id == JS_CLASS_BYTECODE_FUNCTION) { + JS_ThrowTypeError(ctx, "cannot get property of function"); + goto exception; + } + } + sf->cur_pc = pc; val = JS_GetProperty(ctx, sp[-1], JS_ATOM_length); if (unlikely(JS_IsException(val))) @@ -14370,6 +14379,15 @@ static JSValue JS_CallInternal_OLD(JSContext *caller_ctx, JSValueConst func_obj, #endif obj = sp[-1]; + /* User-defined functions don't support property access in cell script */ + if (JS_VALUE_GET_TAG(obj) == JS_TAG_OBJECT) { + JSObject *fp = JS_VALUE_GET_OBJ(obj); + if (fp->class_id == JS_CLASS_BYTECODE_FUNCTION) { + JS_ThrowTypeError(ctx, "cannot get property of function"); + goto exception; + } + } + /* Monomorphic IC fast path: shape-guarded own-property lookup */ if (likely(JS_VALUE_GET_TAG(obj) == JS_TAG_OBJECT)) { JSObject *p = JS_VALUE_GET_OBJ(obj); @@ -14450,6 +14468,15 @@ static JSValue JS_CallInternal_OLD(JSContext *caller_ctx, JSValueConst func_obj, #endif obj = sp[-1]; + /* User-defined functions don't support property access in cell script */ + if (JS_VALUE_GET_TAG(obj) == JS_TAG_OBJECT) { + JSObject *fp = JS_VALUE_GET_OBJ(obj); + if (fp->class_id == JS_CLASS_BYTECODE_FUNCTION) { + JS_ThrowTypeError(ctx, "cannot get property of function"); + goto exception; + } + } + /* Monomorphic IC fast path */ if (likely(JS_VALUE_GET_TAG(obj) == JS_TAG_OBJECT)) { JSObject *p = JS_VALUE_GET_OBJ(obj); @@ -14516,6 +14543,15 @@ static JSValue JS_CallInternal_OLD(JSContext *caller_ctx, JSValueConst func_obj, /* Record property access site */ profile_record_prop_site(rt, b, (uint32_t)(pc - b->byte_code_buf), atom); #endif + /* User-defined functions don't support property assignment in cell script */ + if (JS_VALUE_GET_TAG(sp[-2]) == JS_TAG_OBJECT) { + JSObject *fp = JS_VALUE_GET_OBJ(sp[-2]); + if (fp->class_id == JS_CLASS_BYTECODE_FUNCTION) { + JS_ThrowTypeError(ctx, "cannot set property of function"); + goto exception; + } + } + ret = JS_SetPropertyInternal(ctx, sp[-2], atom, sp[-1], sp[-2], JS_PROP_THROW_STRICT); JS_FreeValue(ctx, sp[-2]); @@ -14653,6 +14689,15 @@ static JSValue JS_CallInternal_OLD(JSContext *caller_ctx, JSValueConst func_obj, { JSValue val; + /* User-defined functions don't support property access in cell script */ + if (JS_VALUE_GET_TAG(sp[-2]) == JS_TAG_OBJECT) { + JSObject *fp = JS_VALUE_GET_OBJ(sp[-2]); + if (fp->class_id == JS_CLASS_BYTECODE_FUNCTION) { + JS_ThrowTypeError(ctx, "cannot get property of function"); + goto exception; + } + } + sf->cur_pc = pc; val = JS_GetPropertyValue(ctx, sp[-2], sp[-1]); JS_FreeValue(ctx, sp[-2]); @@ -14667,6 +14712,15 @@ static JSValue JS_CallInternal_OLD(JSContext *caller_ctx, JSValueConst func_obj, { JSValue val; + /* User-defined functions don't support property access in cell script */ + if (JS_VALUE_GET_TAG(sp[-2]) == JS_TAG_OBJECT) { + JSObject *fp = JS_VALUE_GET_OBJ(sp[-2]); + if (fp->class_id == JS_CLASS_BYTECODE_FUNCTION) { + JS_ThrowTypeError(ctx, "cannot get property of function"); + goto exception; + } + } + sf->cur_pc = pc; val = JS_GetPropertyValue(ctx, sp[-2], sp[-1]); sp[-1] = val; @@ -14679,6 +14733,15 @@ static JSValue JS_CallInternal_OLD(JSContext *caller_ctx, JSValueConst func_obj, { JSValue val; + /* User-defined functions don't support property access in cell script */ + if (JS_VALUE_GET_TAG(sp[-2]) == JS_TAG_OBJECT) { + JSObject *fp = JS_VALUE_GET_OBJ(sp[-2]); + if (fp->class_id == JS_CLASS_BYTECODE_FUNCTION) { + JS_ThrowTypeError(ctx, "cannot get property of function"); + goto exception; + } + } + switch (JS_VALUE_GET_TAG(sp[-2])) { case JS_TAG_INT: case JS_TAG_STRING: @@ -14747,6 +14810,15 @@ static JSValue JS_CallInternal_OLD(JSContext *caller_ctx, JSValueConst func_obj, { int ret; + /* User-defined functions don't support property assignment in cell script */ + if (JS_VALUE_GET_TAG(sp[-3]) == JS_TAG_OBJECT) { + JSObject *fp = JS_VALUE_GET_OBJ(sp[-3]); + if (fp->class_id == JS_CLASS_BYTECODE_FUNCTION) { + JS_ThrowTypeError(ctx, "cannot set property of function"); + goto exception; + } + } + sf->cur_pc = pc; ret = JS_SetPropertyValue(ctx, sp[-3], sp[-2], sp[-1], JS_PROP_THROW_STRICT); JS_FreeValue(ctx, sp[-3]); @@ -29320,13 +29392,19 @@ static JSValue js_function_toString(JSContext *ctx, JSValueConst this_val, return JS_NewStringLen(ctx, b->debug.source, b->debug.source_len); } } - + { JSValue name; const char *pref, *suff; - pref = "function "; + pref = "function "; suff = "() {\n [native code]\n}"; - name = JS_GetProperty(ctx, this_val, JS_ATOM_name); + /* Get name directly from structure rather than via property access */ + if (js_class_has_bytecode(p->class_id)) { + JSFunctionBytecode *b = p->u.func.function_bytecode; + name = JS_AtomToString(ctx, b->func_name); + } else { + name = JS_AtomToString(ctx, JS_ATOM_empty_string); + } if (JS_IsNull(name)) name = JS_AtomToString(ctx, JS_ATOM_empty_string); return JS_ConcatString3(ctx, pref, name, suff); @@ -35480,6 +35558,7 @@ static const JSCFunctionListEntry js_symbol_proto_funcs[] = { static JSValue js_map_constructor(JSContext *ctx, JSValueConst new_target, int argc, JSValueConst *argv, int magic) { + JSMapState *s; JSValue obj, adder = JS_NULL, iter = JS_NULL, next_method = JS_NULL; JSValueConst arr; @@ -35570,11 +35649,13 @@ static JSValue js_map_constructor(JSContext *ctx, JSValueConst new_target, JS_FreeValue(ctx, iter); JS_FreeValue(ctx, adder); } + return obj; fail_close: /* close the iterator object, preserving pending exception */ JS_IteratorClose(ctx, iter, TRUE); fail: + JS_FreeValue(ctx, next_method); JS_FreeValue(ctx, iter); JS_FreeValue(ctx, adder); @@ -37184,6 +37265,42 @@ static JSValue js_cell_text(JSContext *ctx, JSValueConst this_val, return string_buffer_end(b); } + /* Handle function - return source or native stub */ + if (JS_IsFunction(ctx, arg)) { + JSObject *p = JS_VALUE_GET_OBJ(arg); + if (js_class_has_bytecode(p->class_id)) { + JSFunctionBytecode *b = p->u.func.function_bytecode; + if (b->has_debug && b->debug.source) { + return JS_NewStringLen(ctx, b->debug.source, b->debug.source_len); + } + } + /* Native function - generate stub */ + const char *pref = "function "; + const char *suff = "() {\n [native code]\n}"; + const char *name = NULL; + JSObject *fp = JS_VALUE_GET_OBJ(arg); + if (js_class_has_bytecode(fp->class_id)) { + JSFunctionBytecode *fb = fp->u.func.function_bytecode; + name = JS_AtomToCString(ctx, fb->func_name); + } + if (!name) name = ""; + size_t plen = strlen(pref); + size_t nlen = strlen(name); + size_t slen = strlen(suff); + char *result = js_malloc(ctx, plen + nlen + slen + 1); + if (!result) { + if (name[0]) JS_FreeCString(ctx, name); + return JS_EXCEPTION; + } + memcpy(result, pref, plen); + memcpy(result + plen, name, nlen); + memcpy(result + plen + nlen, suff, slen + 1); + JSValue ret = JS_NewString(ctx, result); + js_free(ctx, result); + if (name[0]) JS_FreeCString(ctx, name); + return ret; + } + /* Handle null */ if (JS_IsNull(arg)) return JS_NULL; @@ -39530,12 +39647,19 @@ static JSValue js_cell_length(JSContext *ctx, JSValueConst this_val, if (JS_IsNull(val)) return JS_NULL; - /* Functions return arity */ + /* Functions return arity (accessed directly, not via properties) */ if (JS_IsFunction(ctx, val)) { - JSValue len = JS_GetPropertyStr(ctx, val, "length"); - if (JS_IsException(len)) - return len; - return len; + JSObject *p = JS_VALUE_GET_OBJ(val); + switch (p->class_id) { + case JS_CLASS_BYTECODE_FUNCTION: + return JS_NewInt32(ctx, p->u.func.function_bytecode->defined_arg_count); + case JS_CLASS_C_FUNCTION: + return JS_NewInt32(ctx, p->u.cfunc.length); + case JS_CLASS_C_FUNCTION_DATA: + return JS_NewInt32(ctx, p->u.c_function_data_record->length); + default: + return JS_NewInt32(ctx, 0); + } } int tag = JS_VALUE_GET_TAG(val); @@ -39579,6 +39703,31 @@ static JSValue js_cell_length(JSContext *ctx, JSValueConst this_val, return JS_NULL; } +/* ============================================================================ + * call() function - call a function with explicit this and arguments + * ============================================================================ */ + +/* call(func, this_val, ...args) */ +static JSValue js_cell_call(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + if (argc < 1) + return JS_ThrowTypeError(ctx, "call requires a function argument"); + + JSValue func = argv[0]; + if (!JS_IsFunction(ctx, func)) + return JS_ThrowTypeError(ctx, "first argument must be a function"); + + JSValue this_arg = JS_NULL; + if (argc >= 2) + this_arg = argv[1]; + + int call_argc = argc > 2 ? argc - 2 : 0; + JSValueConst *call_argv = call_argc > 0 ? &argv[2] : NULL; + + return JS_Call(ctx, func, this_arg, call_argc, call_argv); +} + /* ============================================================================ * is_* type checking functions * ============================================================================ */ @@ -40011,6 +40160,11 @@ void JS_AddIntrinsicBaseObjects(JSContext *ctx) JS_NewCFunction(ctx, js_cell_length, "length", 1), JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE); + /* call() function */ + JS_DefinePropertyValueStr(ctx, ctx->global_obj, "call", + JS_NewCFunction(ctx, js_cell_call, "call", 3), + JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE); + /* is_* type checking functions */ JS_DefinePropertyValueStr(ctx, ctx->global_obj, "is_array", JS_NewCFunction(ctx, js_cell_is_array, "is_array", 1), diff --git a/tests/suite.cm b/tests/suite.cm index 867753ce..d33286ae 100644 --- a/tests/suite.cm +++ b/tests/suite.cm @@ -1966,4 +1966,85 @@ return { if (result != null) throw "object get with null key should return null" }, + // ============================================================================ + // FUNCTION AS VALUE (not object) - functions should not have properties + // ============================================================================ + + test_function_property_get_throws: function() { + var fn = function(a, b) { return a + b } + var caught = false + try { + var x = fn.length + } catch (e) { + caught = true + } + if (!caught) throw "getting property on function should throw" + }, + + test_function_property_set_throws: function() { + var fn = function() {} + var caught = false + try { + fn.foo = 123 + } catch (e) { + caught = true + } + if (!caught) throw "setting property on function should throw" + }, + + test_function_bracket_access_throws: function() { + var fn = function() {} + var caught = false + try { + var x = fn["length"] + } catch (e) { + caught = true + } + if (!caught) throw "bracket access on function should throw" + }, + + test_length_returns_function_arity: function() { + var fn0 = function() { return 1 } + var fn1 = function(a) { return a } + var fn2 = function(a, b) { return a + b } + var fn3 = function(a, b, c) { return a + b + c } + + if (length(fn0) != 0) throw "length(fn0) should be 0" + if (length(fn1) != 1) throw "length(fn1) should be 1" + if (length(fn2) != 2) throw "length(fn2) should be 2" + if (length(fn3) != 3) throw "length(fn3) should be 3" + }, + + test_text_returns_function_source: function() { + var fn = function(x) { return x * 2 } + var src = text(fn) + if (src.indexOf("function") == -1) throw "text(fn) should contain 'function'" + if (src.indexOf("return") == -1) throw "text(fn) should contain function body" + }, + + test_call_invokes_function: function() { + var fn = function(a, b) { return a + b } + var result = call(fn, null, 3, 4) + if (result != 7) throw "call(fn, null, 3, 4) should return 7" + }, + + test_call_with_this_binding: function() { + var obj = { value: 10 } + var fn = function(x) { return this.value + x } + var result = call(fn, obj, 5) + if (result != 15) throw "call(fn, obj, 5) should return 15" + }, + + test_call_no_args: function() { + var fn = function() { return 42 } + var result = call(fn, null) + if (result != 42) throw "call(fn, null) should return 42" + }, + + test_builtin_function_properties_still_work: function() { + // Built-in functions like number, text, array should still have properties + var min_result = number.min(5, 3) + if (min_result != 3) throw "number.min should work" + }, + }