diff --git a/source/quickjs.c b/source/quickjs.c index 04cbd34c..e829a795 100644 --- a/source/quickjs.c +++ b/source/quickjs.c @@ -13534,6 +13534,8 @@ static JSValue JS_CallInternal_OLD(JSContext *caller_ctx, JSValueConst func_obj, CASE(OP_call_method): CASE(OP_tail_call_method): { + BOOL is_proxy = FALSE; + call_argc = get_u16(pc); pc += 2; call_argv = sp - call_argc; @@ -13542,8 +13544,49 @@ static JSValue JS_CallInternal_OLD(JSContext *caller_ctx, JSValueConst func_obj, /* Record call site */ profile_record_call_site(rt, b, (uint32_t)(pc - b->byte_code_buf)); #endif - ret_val = JS_CallInternal_OLD(ctx, call_argv[-1], call_argv[-2], - JS_NULL, call_argc, call_argv, 0); + /* Proxy method-call: detect [bytecode_func, "name", ...args] + and rewrite as func("name", [args]) */ + if (JS_VALUE_GET_TAG(call_argv[-2]) == JS_TAG_OBJECT) { + JSObject *fp = JS_VALUE_GET_OBJ(call_argv[-2]); + if (fp->class_id == JS_CLASS_BYTECODE_FUNCTION) { + if (!JS_IsFunction(ctx, call_argv[-1])) { + int t = JS_VALUE_GET_TAG(call_argv[-1]); + if (t == JS_TAG_STRING || t == JS_TAG_SYMBOL) + is_proxy = TRUE; + } + } + } + + if (is_proxy) { + JSValue name = call_argv[-1]; + JSValue args = JS_NewArray(ctx); + if (unlikely(JS_IsException(args))) + goto exception; + + /* Move args into the array, then null out stack slots. */ + for(i = 0; i < call_argc; i++) { + int r = JS_DefinePropertyValue(ctx, args, __JS_AtomFromUInt32(i), call_argv[i], + JS_PROP_C_W_E | JS_PROP_THROW); + call_argv[i] = JS_NULL; + if (unlikely(r < 0)) { + JS_FreeValue(ctx, args); + goto exception; + } + } + + { + JSValue proxy_argv[2]; + proxy_argv[0] = name; /* still owned by stack; freed by normal cleanup */ + proxy_argv[1] = args; + + ret_val = JS_CallInternal_OLD(ctx, call_argv[-2], JS_NULL, + JS_NULL, 2, proxy_argv, 0); + JS_FreeValue(ctx, args); + } + } else { + ret_val = JS_CallInternal_OLD(ctx, call_argv[-1], call_argv[-2], + JS_NULL, call_argc, call_argv, 0); + } if (unlikely(JS_IsException(ret_val))) goto exception; if (opcode == OP_tail_call_method) @@ -13581,11 +13624,35 @@ static JSValue JS_CallInternal_OLD(JSContext *caller_ctx, JSValueConst func_obj, CASE(OP_apply): { int magic; + BOOL is_proxy = FALSE; + magic = get_u16(pc); pc += 2; sf->cur_pc = pc; - ret_val = js_function_apply(ctx, sp[-3], 2, (JSValueConst *)&sp[-2], magic); + /* Proxy method-call with spread: detect ["name", bytecode_func, args_array] + and rewrite as func("name", args_array) */ + 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) { + if (!JS_IsFunction(ctx, sp[-3])) { + int t = JS_VALUE_GET_TAG(sp[-3]); + if (t == JS_TAG_STRING || t == JS_TAG_SYMBOL) + is_proxy = TRUE; + } + } + } + + if (is_proxy) { + JSValue proxy_argv[2]; + proxy_argv[0] = sp[-3]; /* name */ + proxy_argv[1] = sp[-1]; /* args array already built by bytecode */ + + ret_val = JS_CallInternal_OLD(ctx, sp[-2], JS_NULL, + JS_NULL, 2, proxy_argv, 0); + } else { + ret_val = js_function_apply(ctx, sp[-3], 2, (JSValueConst *)&sp[-2], magic); + } if (unlikely(JS_IsException(ret_val))) goto exception; JS_FreeValue(ctx, sp[-3]); @@ -14468,12 +14535,16 @@ 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 */ + /* Proxy method-call sugar: func.name(...) -> func("name", [args...]) + OP_get_field2 is only emitted when a call immediately follows. */ 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; + val = JS_AtomToValue(ctx, atom); /* "name" */ + if (unlikely(JS_IsException(val))) + goto exception; + *sp++ = val; /* stack becomes [func, "name"] */ + goto get_field2_done; } } @@ -14712,12 +14783,34 @@ static JSValue JS_CallInternal_OLD(JSContext *caller_ctx, JSValueConst func_obj, { JSValue val; - /* User-defined functions don't support property access in cell script */ + /* Proxy method-call sugar for bracket calls: func[key](...) */ 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; + /* Keep [func, key] and normalize key to property-key (string/symbol). */ + switch (JS_VALUE_GET_TAG(sp[-1])) { + case JS_TAG_INT: + /* Convert integer to string */ + sf->cur_pc = pc; + ret_val = JS_ToString(ctx, sp[-1]); + if (JS_IsException(ret_val)) + goto exception; + JS_FreeValue(ctx, sp[-1]); + sp[-1] = ret_val; + break; + case JS_TAG_STRING: + case JS_TAG_SYMBOL: + break; + default: + sf->cur_pc = pc; + ret_val = JS_ToPropertyKey(ctx, sp[-1]); + if (JS_IsException(ret_val)) + goto exception; + JS_FreeValue(ctx, sp[-1]); + sp[-1] = ret_val; + break; + } + BREAK; /* skip JS_GetPropertyValue, keep [func, key] on stack */ } } diff --git a/tests/suite.cm b/tests/suite.cm index d33286ae..cee92bfb 100644 --- a/tests/suite.cm +++ b/tests/suite.cm @@ -2047,4 +2047,182 @@ return { if (min_result != 3) throw "number.min should work" }, + // ============================================================================ + // FUNCTION PROXY - Method call sugar for bytecode functions + // ============================================================================ + + test_function_proxy_basic: function() { + var proxy = function(name, args) { + return `called:${name}:${length(args)}` + } + var result = proxy.foo() + if (result != "called:foo:0") throw "basic proxy call failed" + }, + + test_function_proxy_with_one_arg: function() { + var proxy = function(name, args) { + return `${name}-${args[0]}` + } + var result = proxy.test("value") + if (result != "test-value") throw "proxy with one arg failed" + }, + + test_function_proxy_with_multiple_args: function() { + var proxy = function(name, args) { + var sum = 0 + for (var i = 0; i < length(args); i++) { + sum = sum + args[i] + } + return `${name}:${sum}` + } + var result = proxy.add(1, 2, 3, 4) + if (result != "add:10") throw "proxy with multiple args failed" + }, + + test_function_proxy_bracket_notation: function() { + var proxy = function(name, args) { + return `bracket:${name}` + } + var result = proxy["myMethod"]() + if (result != "bracket:myMethod") throw "proxy bracket notation failed" + }, + + test_function_proxy_dynamic_method_name: function() { + var proxy = function(name, args) { + return name + } + var methodName = "dynamic" + var result = proxy[methodName]() + if (result != "dynamic") throw "proxy dynamic method name failed" + }, + + test_function_proxy_dispatch_to_record: function() { + var my_record = { + greet: function(name) { + return `Hello, ${name}` + }, + add: function(a, b) { + return a + b + } + } + + var proxy = function(name, args) { + if (is_function(my_record[name])) { + return fn.apply(my_record[name], args) + } + throw `unknown method: ${name}` + } + + if (proxy.greet("World") != "Hello, World") throw "proxy dispatch greet failed" + if (proxy.add(3, 4) != 7) throw "proxy dispatch add failed" + }, + + test_function_proxy_unknown_method_throws: function() { + var proxy = function(name, args) { + throw `no such method: ${name}` + } + var caught = false + try { + proxy.nonexistent() + } catch (e) { + caught = true + if (e.indexOf("no such method") == -1) throw "wrong error message" + } + if (!caught) throw "proxy should throw for unknown method" + }, + + test_function_proxy_is_function: function() { + var proxy = function(name, args) { + return name + } + if (!is_function(proxy)) throw "proxy should be a function" + }, + + test_function_proxy_length_is_2: function() { + var proxy = function(name, args) { + return name + } + if (length(proxy) != 2) throw "proxy function should have length 2" + }, + + test_function_proxy_property_read_still_throws: function() { + var fn = function() { return 1 } + var caught = false + try { + var x = fn.someProp + } catch (e) { + caught = true + } + if (!caught) throw "reading property from function (not method call) should throw" + }, + + test_function_proxy_nested_calls: function() { + var outer = function(name, args) { + if (name == "inner") { + return args[0].double(5) + } + return "outer:" + name + } + var inner = function(name, args) { + if (name == "double") { + return args[0] * 2 + } + return "inner:" + name + } + var result = outer.inner(inner) + if (result != 10) throw "nested proxy calls failed" + }, + + test_function_proxy_returns_null: function() { + var proxy = function(name, args) { + return null + } + var result = proxy.anything() + if (result != null) throw "proxy returning null failed" + }, + + test_function_proxy_returns_object: function() { + var proxy = function(name, args) { + return {method: name, argCount: length(args)} + } + var result = proxy.test(1, 2, 3) + if (result.method != "test") throw "proxy returning object method failed" + if (result.argCount != 3) throw "proxy returning object argCount failed" + }, + + test_function_proxy_returns_function: function() { + var proxy = function(name, args) { + return function() { return name } + } + var result = proxy.getFn() + if (result() != "getFn") throw "proxy returning function failed" + }, + + test_function_proxy_args_array_is_real_array: function() { + var proxy = function(name, args) { + if (!is_array(args)) throw "args should be array" + args.push(4) + return length(args) + } + var result = proxy.test(1, 2, 3) + if (result != 4) throw "proxy args should be modifiable array" + }, + + test_function_proxy_no_this_binding: function() { + var proxy = function(name, args) { + return this + } + var result = proxy.test() + if (result != null) throw "proxy should have null this" + }, + + test_function_proxy_integer_bracket_key: function() { + var proxy = function(name, args) { + return `key:${name}` + } + var result = proxy[42]() + if (result != "key:42") throw "proxy with integer bracket key failed" + }, + } +