This commit is contained in:
2026-01-11 23:13:58 -06:00
parent ffe7b61ae2
commit b039b0c4ba
2 changed files with 280 additions and 9 deletions

View File

@@ -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
/* 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;
/* 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");
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");
/* 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 */
}
}

View File

@@ -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"
},
}