GDScript: Allow using self in lambdas

This commit is contained in:
George Marques 2022-04-20 14:22:22 -03:00
parent 690fefe43e
commit 01d13ab2c1
No known key found for this signature in database
GPG Key ID: 046BD46A3201E43D
16 changed files with 251 additions and 23 deletions

View File

@ -262,6 +262,7 @@ class GDScriptInstance : public ScriptInstance {
friend class GDScript;
friend class GDScriptFunction;
friend class GDScriptLambdaCallable;
friend class GDScriptLambdaSelfCallable;
friend class GDScriptCompiler;
friend struct GDScriptUtilityFunctionsDefinitions;

View File

@ -2498,12 +2498,17 @@ void GDScriptAnalyzer::reduce_call(GDScriptParser::CallNode *p_call, bool p_is_a
}
if (is_self && parser->current_function != nullptr && parser->current_function->is_static && !is_static) {
push_error(vformat(R"*(Cannot call non-static function "%s()" from static function "%s()".)*", p_call->function_name, parser->current_function->identifier->name), p_call);
// Get the parent function above any lambda.
GDScriptParser::FunctionNode *parent_function = parser->current_function;
while (parent_function->source_lambda) {
parent_function = parent_function->source_lambda->parent_function;
}
push_error(vformat(R"*(Cannot call non-static function "%s()" from static function "%s()".)*", p_call->function_name, parent_function->identifier->name), p_call);
} else if (!is_self && base_type.is_meta_type && !is_static) {
base_type.is_meta_type = false; // For `to_string()`.
push_error(vformat(R"*(Cannot call non-static function "%s()" on the class "%s" directly. Make an instance instead.)*", p_call->function_name, base_type.to_string()), p_call);
} else if (is_self && !is_static && !lambda_stack.is_empty()) {
push_error(vformat(R"*(Cannot call non-static function "%s()" from a lambda function.)*", p_call->function_name), p_call);
} else if (is_self && !is_static) {
mark_lambda_use_self();
}
call_type = return_type;
@ -2636,10 +2641,10 @@ void GDScriptAnalyzer::reduce_get_node(GDScriptParser::GetNodeNode *p_get_node)
if (!ClassDB::is_parent_class(parser->current_class->base_type.native_type, result.native_type)) {
push_error(R"*(Cannot use shorthand "get_node()" notation ("$") on a class that isn't a node.)*", p_get_node);
} else if (!lambda_stack.is_empty()) {
push_error(R"*(Cannot use shorthand "get_node()" notation ("$") inside a lambda. Use a captured variable instead.)*", p_get_node);
}
mark_lambda_use_self();
p_get_node->set_datatype(result);
}
@ -2854,21 +2859,25 @@ void GDScriptAnalyzer::reduce_identifier_from_base(GDScriptParser::IdentifierNod
MethodBind *getter = ClassDB::get_method(native, getter_name);
if (getter != nullptr) {
p_identifier->set_datatype(type_from_property(getter->get_return_info()));
p_identifier->source = GDScriptParser::IdentifierNode::INHERITED_VARIABLE;
}
return;
}
if (ClassDB::get_method_info(native, name, &method_info)) {
// Method is callable.
p_identifier->set_datatype(make_callable_type(method_info));
p_identifier->source = GDScriptParser::IdentifierNode::INHERITED_VARIABLE;
return;
}
if (ClassDB::get_signal(native, name, &method_info)) {
// Signal is a type too.
p_identifier->set_datatype(make_signal_type(method_info));
p_identifier->source = GDScriptParser::IdentifierNode::INHERITED_VARIABLE;
return;
}
if (ClassDB::has_enum(native, name)) {
p_identifier->set_datatype(make_native_enum_type(native, name));
p_identifier->source = GDScriptParser::IdentifierNode::MEMBER_CONSTANT;
return;
}
bool valid = false;
@ -2877,6 +2886,7 @@ void GDScriptAnalyzer::reduce_identifier_from_base(GDScriptParser::IdentifierNod
p_identifier->is_constant = true;
p_identifier->reduced_value = int_constant;
p_identifier->set_datatype(type_from_variant(int_constant, p_identifier));
p_identifier->source = GDScriptParser::IdentifierNode::MEMBER_CONSTANT;
return;
}
}
@ -2927,7 +2937,11 @@ void GDScriptAnalyzer::reduce_identifier(GDScriptParser::IdentifierNode *p_ident
p_identifier->reduced_value = p_identifier->constant_source->initializer->reduced_value;
found_source = true;
break;
case GDScriptParser::IdentifierNode::INHERITED_VARIABLE:
mark_lambda_use_self();
break;
case GDScriptParser::IdentifierNode::MEMBER_VARIABLE:
mark_lambda_use_self();
p_identifier->variable_source->usages++;
[[fallthrough]];
case GDScriptParser::IdentifierNode::LOCAL_VARIABLE:
@ -2958,18 +2972,37 @@ void GDScriptAnalyzer::reduce_identifier(GDScriptParser::IdentifierNode *p_ident
}
if (found_source) {
if ((p_identifier->source == GDScriptParser::IdentifierNode::MEMBER_VARIABLE || p_identifier->source == GDScriptParser::IdentifierNode::INHERITED_VARIABLE) && parser->current_function && parser->current_function->is_static) {
// Get the parent function above any lambda.
GDScriptParser::FunctionNode *parent_function = parser->current_function;
while (parent_function->source_lambda) {
parent_function = parent_function->source_lambda->parent_function;
}
push_error(vformat(R"*(Cannot access instance variable "%s" from the static function "%s()".)*", p_identifier->name, parent_function->identifier->name), p_identifier);
}
if (!lambda_stack.is_empty()) {
// If the identifier is a member variable (including the native class properties), we consider the lambda to be using `self`, so we keep a reference to the current instance.
if (p_identifier->source == GDScriptParser::IdentifierNode::MEMBER_VARIABLE || p_identifier->source == GDScriptParser::IdentifierNode::INHERITED_VARIABLE) {
mark_lambda_use_self();
return; // No need to capture.
}
// If the identifier is local, check if it's any kind of capture by comparing their source function.
// Only capture locals and members and enum values. Constants are still accessible from the lambda using the script reference.
if (p_identifier->source == GDScriptParser::IdentifierNode::UNDEFINED_SOURCE || p_identifier->source == GDScriptParser::IdentifierNode::MEMBER_CONSTANT || lambda_stack.is_empty()) {
// Only capture locals and enum values. Constants are still accessible from the lambda using the script reference. If not, this method is done.
if (p_identifier->source == GDScriptParser::IdentifierNode::UNDEFINED_SOURCE || p_identifier->source == GDScriptParser::IdentifierNode::MEMBER_CONSTANT) {
return;
}
GDScriptParser::FunctionNode *function_test = lambda_stack.back()->get()->function;
// Make sure we aren't capturing variable in the same lambda.
// This also add captures for nested lambdas.
while (function_test != nullptr && function_test != p_identifier->source_function && function_test->source_lambda != nullptr && !function_test->source_lambda->captures_indices.has(p_identifier->name)) {
function_test->source_lambda->captures_indices[p_identifier->name] = function_test->source_lambda->captures.size();
function_test->source_lambda->captures.push_back(p_identifier);
function_test = function_test->source_lambda->parent_function;
}
}
return;
}
@ -3149,6 +3182,7 @@ void GDScriptAnalyzer::reduce_preload(GDScriptParser::PreloadNode *p_preload) {
void GDScriptAnalyzer::reduce_self(GDScriptParser::SelfNode *p_self) {
p_self->is_constant = false;
p_self->set_datatype(type_from_metatype(parser->current_class->get_datatype()));
mark_lambda_use_self();
}
void GDScriptAnalyzer::reduce_subscript(GDScriptParser::SubscriptNode *p_subscript) {
@ -4121,6 +4155,12 @@ void GDScriptAnalyzer::mark_node_unsafe(const GDScriptParser::Node *p_node) {
#endif
}
void GDScriptAnalyzer::mark_lambda_use_self() {
for (GDScriptParser::LambdaNode *lambda : lambda_stack) {
lambda->use_self = true;
}
}
bool GDScriptAnalyzer::class_exists(const StringName &p_class) const {
return ClassDB::class_exists(p_class) && ClassDB::is_class_exposed(p_class);
}

View File

@ -42,7 +42,7 @@ class GDScriptAnalyzer {
HashMap<String, Ref<GDScriptParserRef>> depended_parsers;
const GDScriptParser::EnumNode *current_enum = nullptr;
List<const GDScriptParser::LambdaNode *> lambda_stack;
List<GDScriptParser::LambdaNode *> lambda_stack;
// Tests for detecting invalid overloading of script members
static _FORCE_INLINE_ bool has_member_name_conflict_in_script_class(const StringName &p_name, const GDScriptParser::ClassNode *p_current_class_node);
@ -115,6 +115,7 @@ class GDScriptAnalyzer {
bool is_type_compatible(const GDScriptParser::DataType &p_target, const GDScriptParser::DataType &p_source, bool p_allow_implicit_conversion = false, const GDScriptParser::Node *p_source_node = nullptr);
void push_error(const String &p_message, const GDScriptParser::Node *p_origin);
void mark_node_unsafe(const GDScriptParser::Node *p_node);
void mark_lambda_use_self();
bool class_exists(const StringName &p_class) const;
Ref<GDScriptParserRef> get_parser_for(const String &p_path);
#ifdef DEBUG_ENABLED

View File

@ -1211,8 +1211,8 @@ void GDScriptByteCodeGenerator::write_call_script_function(const Address &p_targ
append(p_function_name);
}
void GDScriptByteCodeGenerator::write_lambda(const Address &p_target, GDScriptFunction *p_function, const Vector<Address> &p_captures) {
append(GDScriptFunction::OPCODE_CREATE_LAMBDA, 1 + p_captures.size());
void GDScriptByteCodeGenerator::write_lambda(const Address &p_target, GDScriptFunction *p_function, const Vector<Address> &p_captures, bool p_use_self) {
append(p_use_self ? GDScriptFunction::OPCODE_CREATE_SELF_LAMBDA : GDScriptFunction::OPCODE_CREATE_LAMBDA, 1 + p_captures.size());
for (int i = 0; i < p_captures.size(); i++) {
append(p_captures[i]);
}

View File

@ -470,7 +470,7 @@ public:
virtual void write_call_self(const Address &p_target, const StringName &p_function_name, const Vector<Address> &p_arguments) override;
virtual void write_call_self_async(const Address &p_target, const StringName &p_function_name, const Vector<Address> &p_arguments) override;
virtual void write_call_script_function(const Address &p_target, const Address &p_base, const StringName &p_function_name, const Vector<Address> &p_arguments) override;
virtual void write_lambda(const Address &p_target, GDScriptFunction *p_function, const Vector<Address> &p_captures) override;
virtual void write_lambda(const Address &p_target, GDScriptFunction *p_function, const Vector<Address> &p_captures, bool p_use_self) override;
virtual void write_construct(const Address &p_target, Variant::Type p_type, const Vector<Address> &p_arguments) override;
virtual void write_construct_array(const Address &p_target, const Vector<Address> &p_arguments) override;
virtual void write_construct_typed_array(const Address &p_target, const GDScriptDataType &p_element_type, const Vector<Address> &p_arguments) override;

View File

@ -131,7 +131,7 @@ public:
virtual void write_call_self(const Address &p_target, const StringName &p_function_name, const Vector<Address> &p_arguments) = 0;
virtual void write_call_self_async(const Address &p_target, const StringName &p_function_name, const Vector<Address> &p_arguments) = 0;
virtual void write_call_script_function(const Address &p_target, const Address &p_base, const StringName &p_function_name, const Vector<Address> &p_arguments) = 0;
virtual void write_lambda(const Address &p_target, GDScriptFunction *p_function, const Vector<Address> &p_captures) = 0;
virtual void write_lambda(const Address &p_target, GDScriptFunction *p_function, const Vector<Address> &p_captures, bool p_use_self) = 0;
virtual void write_construct(const Address &p_target, Variant::Type p_type, const Vector<Address> &p_arguments) = 0;
virtual void write_construct_array(const Address &p_target, const Vector<Address> &p_arguments) = 0;
virtual void write_construct_typed_array(const Address &p_target, const GDScriptDataType &p_element_type, const Vector<Address> &p_arguments) = 0;

View File

@ -1197,7 +1197,7 @@ GDScriptCodeGenerator::Address GDScriptCompiler::_parse_expression(CodeGen &code
return GDScriptCodeGenerator::Address();
}
gen->write_lambda(result, function, captures);
gen->write_lambda(result, function, captures, lambda->use_self);
for (int i = 0; i < captures.size(); i++) {
if (captures[i].mode == GDScriptCodeGenerator::Address::TEMPORARY) {

View File

@ -792,6 +792,25 @@ void GDScriptFunction::disassemble(const Vector<String> &p_code_lines) const {
incr = 3 + captures_count;
} break;
case OPCODE_CREATE_SELF_LAMBDA: {
int captures_count = _code_ptr[ip + 1 + instr_var_args];
GDScriptFunction *lambda = _lambdas_ptr[_code_ptr[ip + 2 + instr_var_args]];
text += DADDR(1 + captures_count);
text += "create self lambda from ";
text += lambda->name.operator String();
text += "function, captures (";
for (int i = 0; i < captures_count; i++) {
if (i > 0) {
text += ", ";
}
text += DADDR(1 + i);
}
text += ")";
incr = 3 + captures_count;
} break;
case OPCODE_JUMP: {
text += "jump ";
text += itos(_code_ptr[ip + 1]);

View File

@ -299,6 +299,7 @@ public:
OPCODE_AWAIT,
OPCODE_AWAIT_RESUME,
OPCODE_CREATE_LAMBDA,
OPCODE_CREATE_SELF_LAMBDA,
OPCODE_JUMP,
OPCODE_JUMP_IF,
OPCODE_JUMP_IF_NOT,

View File

@ -93,3 +93,81 @@ GDScriptLambdaCallable::GDScriptLambdaCallable(Ref<GDScript> p_script, GDScriptF
h = (uint32_t)hash_djb2_one_64((uint64_t)this);
}
bool GDScriptLambdaSelfCallable::compare_equal(const CallableCustom *p_a, const CallableCustom *p_b) {
// Lambda callables are only compared by reference.
return p_a == p_b;
}
bool GDScriptLambdaSelfCallable::compare_less(const CallableCustom *p_a, const CallableCustom *p_b) {
// Lambda callables are only compared by reference.
return p_a < p_b;
}
uint32_t GDScriptLambdaSelfCallable::hash() const {
return h;
}
String GDScriptLambdaSelfCallable::get_as_text() const {
if (function->get_name() != StringName()) {
return function->get_name().operator String() + "(lambda)";
}
return "(anonymous lambda)";
}
CallableCustom::CompareEqualFunc GDScriptLambdaSelfCallable::get_compare_equal_func() const {
return compare_equal;
}
CallableCustom::CompareLessFunc GDScriptLambdaSelfCallable::get_compare_less_func() const {
return compare_less;
}
ObjectID GDScriptLambdaSelfCallable::get_object() const {
return object->get_instance_id();
}
void GDScriptLambdaSelfCallable::call(const Variant **p_arguments, int p_argcount, Variant &r_return_value, Callable::CallError &r_call_error) const {
#ifdef DEBUG_ENABLED
if (object->get_script_instance() == nullptr || object->get_script_instance()->get_language() != GDScriptLanguage::get_singleton()) {
ERR_PRINT("Trying to call a lambda with an invalid instance.");
r_call_error.error = Callable::CallError::CALL_ERROR_INSTANCE_IS_NULL;
return;
}
#endif
int captures_amount = captures.size();
if (captures_amount > 0) {
Vector<const Variant *> args;
args.resize(p_argcount + captures_amount);
for (int i = 0; i < captures_amount; i++) {
args.write[i] = &captures[i];
}
for (int i = 0; i < p_argcount; i++) {
args.write[i + captures_amount] = p_arguments[i];
}
r_return_value = function->call(static_cast<GDScriptInstance *>(object->get_script_instance()), args.ptrw(), args.size(), r_call_error);
r_call_error.argument -= captures_amount;
} else {
r_return_value = function->call(static_cast<GDScriptInstance *>(object->get_script_instance()), p_arguments, p_argcount, r_call_error);
}
}
GDScriptLambdaSelfCallable::GDScriptLambdaSelfCallable(Ref<RefCounted> p_self, GDScriptFunction *p_function, const Vector<Variant> &p_captures) {
reference = p_self;
object = p_self.ptr();
function = p_function;
captures = p_captures;
h = (uint32_t)hash_djb2_one_64((uint64_t)this);
}
GDScriptLambdaSelfCallable::GDScriptLambdaSelfCallable(Object *p_self, GDScriptFunction *p_function, const Vector<Variant> &p_captures) {
object = p_self;
function = p_function;
captures = p_captures;
h = (uint32_t)hash_djb2_one_64((uint64_t)this);
}

View File

@ -62,4 +62,29 @@ public:
virtual ~GDScriptLambdaCallable() = default;
};
// Lambda callable that references a particular object, so it can use `self` in the body.
class GDScriptLambdaSelfCallable : public CallableCustom {
GDScriptFunction *function = nullptr;
Ref<RefCounted> reference; // For objects that are RefCounted, keep a reference.
Object *object = nullptr; // For non RefCounted objects, use a direct pointer.
uint32_t h;
Vector<Variant> captures;
static bool compare_equal(const CallableCustom *p_a, const CallableCustom *p_b);
static bool compare_less(const CallableCustom *p_a, const CallableCustom *p_b);
public:
uint32_t hash() const override;
String get_as_text() const override;
CompareEqualFunc get_compare_equal_func() const override;
CompareLessFunc get_compare_less_func() const override;
ObjectID get_object() const override;
void call(const Variant **p_arguments, int p_argcount, Variant &r_return_value, Callable::CallError &r_call_error) const override;
GDScriptLambdaSelfCallable(Ref<RefCounted> p_self, GDScriptFunction *p_function, const Vector<Variant> &p_captures);
GDScriptLambdaSelfCallable(Object *p_self, GDScriptFunction *p_function, const Vector<Variant> &p_captures);
virtual ~GDScriptLambdaSelfCallable() = default;
};
#endif // GDSCRIPT_LAMBDA_CALLABLE

View File

@ -2200,9 +2200,6 @@ GDScriptParser::ExpressionNode *GDScriptParser::parse_self(ExpressionNode *p_pre
if (current_function && current_function->is_static) {
push_error(R"(Cannot use "self" inside a static function.)");
}
if (in_lambda) {
push_error(R"(Cannot use "self" inside a lambda.)");
}
SelfNode *self = alloc_node<SelfNode>();
self->current_class = current_class;
return self;

View File

@ -767,6 +767,7 @@ public:
LOCAL_BIND, // Pattern bind.
MEMBER_VARIABLE,
MEMBER_CONSTANT,
INHERITED_VARIABLE,
};
Source source = UNDEFINED_SOURCE;
@ -800,6 +801,7 @@ public:
FunctionNode *parent_function = nullptr;
Vector<IdentifierNode *> captures;
Map<StringName, int> captures_indices;
bool use_self = false;
bool has_name() const {
return function && function->identifier;

View File

@ -306,6 +306,7 @@ void (*type_init_function_table[])(Variant *) = {
&&OPCODE_AWAIT, \
&&OPCODE_AWAIT_RESUME, \
&&OPCODE_CREATE_LAMBDA, \
&&OPCODE_CREATE_SELF_LAMBDA, \
&&OPCODE_JUMP, \
&&OPCODE_JUMP_IF, \
&&OPCODE_JUMP_IF_NOT, \
@ -2277,6 +2278,41 @@ Variant GDScriptFunction::call(GDScriptInstance *p_instance, const Variant **p_a
}
DISPATCH_OPCODE;
OPCODE(OPCODE_CREATE_SELF_LAMBDA) {
CHECK_SPACE(2 + instr_arg_count);
GD_ERR_BREAK(p_instance == nullptr);
ip += instr_arg_count;
int captures_count = _code_ptr[ip + 1];
GD_ERR_BREAK(captures_count < 0);
int lambda_index = _code_ptr[ip + 2];
GD_ERR_BREAK(lambda_index < 0 || lambda_index >= _lambdas_count);
GDScriptFunction *lambda = _lambdas_ptr[lambda_index];
Vector<Variant> captures;
captures.resize(captures_count);
for (int i = 0; i < captures_count; i++) {
GET_INSTRUCTION_ARG(arg, i);
captures.write[i] = *arg;
}
GDScriptLambdaSelfCallable *callable;
if (Object::cast_to<RefCounted>(p_instance->owner)) {
callable = memnew(GDScriptLambdaSelfCallable(Ref<RefCounted>(Object::cast_to<RefCounted>(p_instance->owner)), lambda, captures));
} else {
callable = memnew(GDScriptLambdaSelfCallable(p_instance->owner, lambda, captures));
}
GET_INSTRUCTION_ARG(result, captures_count);
*result = Callable(callable);
ip += 3;
}
DISPATCH_OPCODE;
OPCODE(OPCODE_JUMP) {
CHECK_SPACE(2);
int to = _code_ptr[ip + 1];

View File

@ -0,0 +1,23 @@
var member = "foo"
func bar():
print("bar")
func test():
var lambda1 = func():
print(member)
lambda1.call()
var lambda2 = func():
var nested = func():
print(member)
nested.call()
lambda2.call()
var lambda3 = func():
bar()
lambda3.call()
var lambda4 = func():
return self
print(lambda4.call() == self)

View File

@ -0,0 +1,5 @@
GDTEST_OK
foo
foo
bar
true