fn proxy
This commit is contained in:
111
source/quickjs.c
111
source/quickjs.c
@@ -13534,6 +13534,8 @@ static JSValue JS_CallInternal_OLD(JSContext *caller_ctx, JSValueConst func_obj,
|
|||||||
CASE(OP_call_method):
|
CASE(OP_call_method):
|
||||||
CASE(OP_tail_call_method):
|
CASE(OP_tail_call_method):
|
||||||
{
|
{
|
||||||
|
BOOL is_proxy = FALSE;
|
||||||
|
|
||||||
call_argc = get_u16(pc);
|
call_argc = get_u16(pc);
|
||||||
pc += 2;
|
pc += 2;
|
||||||
call_argv = sp - call_argc;
|
call_argv = sp - call_argc;
|
||||||
@@ -13542,8 +13544,49 @@ static JSValue JS_CallInternal_OLD(JSContext *caller_ctx, JSValueConst func_obj,
|
|||||||
/* Record call site */
|
/* Record call site */
|
||||||
profile_record_call_site(rt, b, (uint32_t)(pc - b->byte_code_buf));
|
profile_record_call_site(rt, b, (uint32_t)(pc - b->byte_code_buf));
|
||||||
#endif
|
#endif
|
||||||
ret_val = JS_CallInternal_OLD(ctx, call_argv[-1], call_argv[-2],
|
/* Proxy method-call: detect [bytecode_func, "name", ...args]
|
||||||
JS_NULL, call_argc, call_argv, 0);
|
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)))
|
if (unlikely(JS_IsException(ret_val)))
|
||||||
goto exception;
|
goto exception;
|
||||||
if (opcode == OP_tail_call_method)
|
if (opcode == OP_tail_call_method)
|
||||||
@@ -13581,11 +13624,35 @@ static JSValue JS_CallInternal_OLD(JSContext *caller_ctx, JSValueConst func_obj,
|
|||||||
CASE(OP_apply):
|
CASE(OP_apply):
|
||||||
{
|
{
|
||||||
int magic;
|
int magic;
|
||||||
|
BOOL is_proxy = FALSE;
|
||||||
|
|
||||||
magic = get_u16(pc);
|
magic = get_u16(pc);
|
||||||
pc += 2;
|
pc += 2;
|
||||||
sf->cur_pc = pc;
|
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)))
|
if (unlikely(JS_IsException(ret_val)))
|
||||||
goto exception;
|
goto exception;
|
||||||
JS_FreeValue(ctx, sp[-3]);
|
JS_FreeValue(ctx, sp[-3]);
|
||||||
@@ -14468,12 +14535,16 @@ static JSValue JS_CallInternal_OLD(JSContext *caller_ctx, JSValueConst func_obj,
|
|||||||
#endif
|
#endif
|
||||||
obj = sp[-1];
|
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) {
|
if (JS_VALUE_GET_TAG(obj) == JS_TAG_OBJECT) {
|
||||||
JSObject *fp = JS_VALUE_GET_OBJ(obj);
|
JSObject *fp = JS_VALUE_GET_OBJ(obj);
|
||||||
if (fp->class_id == JS_CLASS_BYTECODE_FUNCTION) {
|
if (fp->class_id == JS_CLASS_BYTECODE_FUNCTION) {
|
||||||
JS_ThrowTypeError(ctx, "cannot get property of function");
|
val = JS_AtomToValue(ctx, atom); /* "name" */
|
||||||
goto exception;
|
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;
|
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) {
|
if (JS_VALUE_GET_TAG(sp[-2]) == JS_TAG_OBJECT) {
|
||||||
JSObject *fp = JS_VALUE_GET_OBJ(sp[-2]);
|
JSObject *fp = JS_VALUE_GET_OBJ(sp[-2]);
|
||||||
if (fp->class_id == JS_CLASS_BYTECODE_FUNCTION) {
|
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). */
|
||||||
goto exception;
|
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 */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
178
tests/suite.cm
178
tests/suite.cm
@@ -2047,4 +2047,182 @@ return {
|
|||||||
if (min_result != 3) throw "number.min should work"
|
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"
|
||||||
|
},
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user