fn proxy
This commit is contained in:
101
source/quickjs.c
101
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
|
||||
/* 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 */
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
178
tests/suite.cm
178
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"
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user