mirror of
https://github.com/zeek/zeek.git
synced 2025-10-02 22:58:20 +00:00
Merge remote-tracking branch 'origin/topic/jsiwek/misc-lambda-fixes'
* origin/topic/jsiwek/misc-lambda-fixes: GH-725: fix logic for finding a lambda's usage of outer IDs Change record field anonymous functions to use lambda expressions
This commit is contained in:
commit
3ce1c9ffd6
12 changed files with 103 additions and 58 deletions
19
CHANGES
19
CHANGES
|
@ -1,4 +1,23 @@
|
||||||
|
|
||||||
|
3.2.0-dev.332 | 2020-03-31 15:34:52 -0700
|
||||||
|
|
||||||
|
* GH-725: fix logic for finding a lambda's usage of outer IDs (Jon Siwek, Corelight)
|
||||||
|
|
||||||
|
* Change record field anonymous functions to use lambda expressions
|
||||||
|
|
||||||
|
There was an alternate syntax to assign anonymous functions to record
|
||||||
|
fields that was never migrated to use the new lambda expression
|
||||||
|
machinery (and so didn't allow referencing variables in outer scope):
|
||||||
|
|
||||||
|
type myrec: record {
|
||||||
|
foo: function(a: string);
|
||||||
|
};
|
||||||
|
|
||||||
|
local o = "o";
|
||||||
|
local mr = myrec($foo(a: string) = { print a + o; });
|
||||||
|
|
||||||
|
(Jon Siwek, Corelight)
|
||||||
|
|
||||||
3.2.0-dev.329 | 2020-03-31 08:48:10 -0700
|
3.2.0-dev.329 | 2020-03-31 08:48:10 -0700
|
||||||
|
|
||||||
* Mark input/output message classes as final, since nothing should be inheriting from them (Tim Wojtulewicz, Corelight)
|
* Mark input/output message classes as final, since nothing should be inheriting from them (Tim Wojtulewicz, Corelight)
|
||||||
|
|
2
VERSION
2
VERSION
|
@ -1 +1 @@
|
||||||
3.2.0-dev.329
|
3.2.0-dev.332
|
||||||
|
|
|
@ -4311,6 +4311,11 @@ LambdaExpr::LambdaExpr(std::unique_ptr<function_ingredients> arg_ing,
|
||||||
id->SetConst();
|
id->SetConst();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Scope* LambdaExpr::GetScope() const
|
||||||
|
{
|
||||||
|
return ingredients->scope.get();
|
||||||
|
}
|
||||||
|
|
||||||
IntrusivePtr<Val> LambdaExpr::Eval(Frame* f) const
|
IntrusivePtr<Val> LambdaExpr::Eval(Frame* f) const
|
||||||
{
|
{
|
||||||
auto lamb = make_intrusive<BroFunc>(
|
auto lamb = make_intrusive<BroFunc>(
|
||||||
|
|
|
@ -64,6 +64,7 @@ extern const char* expr_name(BroExprTag t);
|
||||||
template <class T> class IntrusivePtr;
|
template <class T> class IntrusivePtr;
|
||||||
class Stmt;
|
class Stmt;
|
||||||
class Frame;
|
class Frame;
|
||||||
|
class Scope;
|
||||||
class ListExpr;
|
class ListExpr;
|
||||||
class NameExpr;
|
class NameExpr;
|
||||||
class IndexExpr;
|
class IndexExpr;
|
||||||
|
@ -815,6 +816,8 @@ public:
|
||||||
IntrusivePtr<Val> Eval(Frame* f) const override;
|
IntrusivePtr<Val> Eval(Frame* f) const override;
|
||||||
TraversalCode Traverse(TraversalCallback* cb) const override;
|
TraversalCode Traverse(TraversalCallback* cb) const override;
|
||||||
|
|
||||||
|
Scope* GetScope() const;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void ExprDescribe(ODesc* d) const override;
|
void ExprDescribe(ODesc* d) const override;
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,6 @@
|
||||||
#include "Scope.h"
|
#include "Scope.h"
|
||||||
#include "Type.h"
|
#include "Type.h"
|
||||||
#include "File.h"
|
#include "File.h"
|
||||||
#include "Scope.h"
|
|
||||||
#include "Traverse.h"
|
#include "Traverse.h"
|
||||||
#include "Val.h"
|
#include "Val.h"
|
||||||
#include "zeekygen/Manager.h"
|
#include "zeekygen/Manager.h"
|
||||||
|
|
41
src/Var.cc
41
src/Var.cc
|
@ -419,32 +419,25 @@ void begin_func(ID* id, const char* module_name, function_flavor flavor,
|
||||||
class OuterIDBindingFinder : public TraversalCallback {
|
class OuterIDBindingFinder : public TraversalCallback {
|
||||||
public:
|
public:
|
||||||
OuterIDBindingFinder(Scope* s)
|
OuterIDBindingFinder(Scope* s)
|
||||||
: scope(s) { }
|
{
|
||||||
|
scopes.emplace_back(s);
|
||||||
|
}
|
||||||
|
|
||||||
TraversalCode PreExpr(const Expr*) override;
|
TraversalCode PreExpr(const Expr*) override;
|
||||||
TraversalCode PostExpr(const Expr*) override;
|
TraversalCode PostExpr(const Expr*) override;
|
||||||
|
|
||||||
Scope* scope;
|
std::vector<Scope*> scopes;
|
||||||
vector<const NameExpr*> outer_id_references;
|
vector<const NameExpr*> outer_id_references;
|
||||||
int lambda_depth = 0;
|
|
||||||
// Note: think we really ought to toggle this to false to prevent
|
|
||||||
// considering locals within inner-lambdas as "outer", but other logic
|
|
||||||
// for "selective cloning" and locating IDs in the closure chain may
|
|
||||||
// depend on current behavior and also needs to be changed.
|
|
||||||
bool search_inner_lambdas = true;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
TraversalCode OuterIDBindingFinder::PreExpr(const Expr* expr)
|
TraversalCode OuterIDBindingFinder::PreExpr(const Expr* expr)
|
||||||
{
|
{
|
||||||
if ( expr->Tag() == EXPR_LAMBDA )
|
if ( expr->Tag() == EXPR_LAMBDA )
|
||||||
++lambda_depth;
|
{
|
||||||
|
auto le = static_cast<const LambdaExpr*>(expr);
|
||||||
if ( lambda_depth > 0 && ! search_inner_lambdas )
|
scopes.emplace_back(le->GetScope());
|
||||||
// Don't inspect the bodies of inner lambdas as they will have their
|
|
||||||
// own traversal to find outer IDs and we don't want to detect
|
|
||||||
// references to local IDs inside and accidentally treat them as
|
|
||||||
// "outer" since they can't be found in current scope.
|
|
||||||
return TC_CONTINUE;
|
return TC_CONTINUE;
|
||||||
|
}
|
||||||
|
|
||||||
if ( expr->Tag() != EXPR_NAME )
|
if ( expr->Tag() != EXPR_NAME )
|
||||||
return TC_CONTINUE;
|
return TC_CONTINUE;
|
||||||
|
@ -454,7 +447,10 @@ TraversalCode OuterIDBindingFinder::PreExpr(const Expr* expr)
|
||||||
if ( e->Id()->IsGlobal() )
|
if ( e->Id()->IsGlobal() )
|
||||||
return TC_CONTINUE;
|
return TC_CONTINUE;
|
||||||
|
|
||||||
|
for ( const auto& scope : scopes )
|
||||||
if ( scope->Lookup(e->Id()->Name()) )
|
if ( scope->Lookup(e->Id()->Name()) )
|
||||||
|
// Shadowing is not allowed, so if it's found at inner scope, it's
|
||||||
|
// not something we have to worry about also being at outer scope.
|
||||||
return TC_CONTINUE;
|
return TC_CONTINUE;
|
||||||
|
|
||||||
outer_id_references.push_back(e);
|
outer_id_references.push_back(e);
|
||||||
|
@ -464,10 +460,7 @@ TraversalCode OuterIDBindingFinder::PreExpr(const Expr* expr)
|
||||||
TraversalCode OuterIDBindingFinder::PostExpr(const Expr* expr)
|
TraversalCode OuterIDBindingFinder::PostExpr(const Expr* expr)
|
||||||
{
|
{
|
||||||
if ( expr->Tag() == EXPR_LAMBDA )
|
if ( expr->Tag() == EXPR_LAMBDA )
|
||||||
{
|
scopes.pop_back();
|
||||||
--lambda_depth;
|
|
||||||
assert(lambda_depth >= 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
return TC_CONTINUE;
|
return TC_CONTINUE;
|
||||||
}
|
}
|
||||||
|
@ -476,16 +469,6 @@ void end_func(IntrusivePtr<Stmt> body)
|
||||||
{
|
{
|
||||||
auto ingredients = std::make_unique<function_ingredients>(pop_scope(), std::move(body));
|
auto ingredients = std::make_unique<function_ingredients>(pop_scope(), std::move(body));
|
||||||
|
|
||||||
if ( streq(ingredients->id->Name(), "anonymous-function") )
|
|
||||||
{
|
|
||||||
OuterIDBindingFinder cb(ingredients->scope.get());
|
|
||||||
ingredients->body->Traverse(&cb);
|
|
||||||
|
|
||||||
for ( size_t i = 0; i < cb.outer_id_references.size(); ++i )
|
|
||||||
cb.outer_id_references[i]->Error(
|
|
||||||
"referencing outer function IDs not supported");
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( ingredients->id->HasVal() )
|
if ( ingredients->id->HasVal() )
|
||||||
ingredients->id->ID_Val()->AsFunc()->AddBody(
|
ingredients->id->ID_Val()->AsFunc()->AddBody(
|
||||||
ingredients->body,
|
ingredients->body,
|
||||||
|
|
21
src/parse.y
21
src/parse.y
|
@ -57,7 +57,7 @@
|
||||||
%type <ic> init_class
|
%type <ic> init_class
|
||||||
%type <expr> opt_init
|
%type <expr> opt_init
|
||||||
%type <val> TOK_CONSTANT
|
%type <val> TOK_CONSTANT
|
||||||
%type <expr> expr opt_expr init anonymous_function index_slice opt_deprecated
|
%type <expr> expr opt_expr init anonymous_function lambda_body index_slice opt_deprecated
|
||||||
%type <event_expr> event
|
%type <event_expr> event
|
||||||
%type <stmt> stmt stmt_list func_body for_head
|
%type <stmt> stmt stmt_list func_body for_head
|
||||||
%type <type> type opt_type enum_body
|
%type <type> type opt_type enum_body
|
||||||
|
@ -512,11 +512,9 @@ expr:
|
||||||
begin_func(func_id, current_module.c_str(), FUNC_FLAVOR_FUNCTION,
|
begin_func(func_id, current_module.c_str(), FUNC_FLAVOR_FUNCTION,
|
||||||
0, {AdoptRef{}, $3});
|
0, {AdoptRef{}, $3});
|
||||||
}
|
}
|
||||||
func_body
|
lambda_body
|
||||||
{
|
{
|
||||||
$$ = new FieldAssignExpr($2,
|
$$ = new FieldAssignExpr($2, IntrusivePtr{AdoptRef{}, $6});
|
||||||
make_intrusive<ConstExpr>(
|
|
||||||
IntrusivePtr<Val>{NewRef{}, func_id->ID_Val()}));
|
|
||||||
Unref(func_id);
|
Unref(func_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1237,9 +1235,7 @@ func_body:
|
||||||
}
|
}
|
||||||
;
|
;
|
||||||
|
|
||||||
anonymous_function:
|
lambda_body:
|
||||||
TOK_FUNCTION begin_func
|
|
||||||
|
|
||||||
'{'
|
'{'
|
||||||
{
|
{
|
||||||
saved_in_init.push_back(in_init);
|
saved_in_init.push_back(in_init);
|
||||||
|
@ -1254,20 +1250,25 @@ anonymous_function:
|
||||||
|
|
||||||
'}'
|
'}'
|
||||||
{
|
{
|
||||||
set_location(@1, @7);
|
set_location(@1, @5);
|
||||||
|
|
||||||
// Code duplication here is sad but needed. end_func actually instantiates the function
|
// Code duplication here is sad but needed. end_func actually instantiates the function
|
||||||
// and associates it with an ID. We perform that association later and need to return
|
// and associates it with an ID. We perform that association later and need to return
|
||||||
// a lambda expression.
|
// a lambda expression.
|
||||||
|
|
||||||
// Gather the ingredients for a BroFunc from the current scope
|
// Gather the ingredients for a BroFunc from the current scope
|
||||||
auto ingredients = std::make_unique<function_ingredients>(IntrusivePtr{NewRef{}, current_scope()}, IntrusivePtr{AdoptRef{}, $5});
|
auto ingredients = std::make_unique<function_ingredients>(IntrusivePtr{NewRef{}, current_scope()}, IntrusivePtr{AdoptRef{}, $3});
|
||||||
id_list outer_ids = gather_outer_ids(pop_scope().get(), ingredients->body.get());
|
id_list outer_ids = gather_outer_ids(pop_scope().get(), ingredients->body.get());
|
||||||
|
|
||||||
$$ = new LambdaExpr(std::move(ingredients), std::move(outer_ids));
|
$$ = new LambdaExpr(std::move(ingredients), std::move(outer_ids));
|
||||||
}
|
}
|
||||||
;
|
;
|
||||||
|
|
||||||
|
anonymous_function:
|
||||||
|
TOK_FUNCTION begin_func lambda_body
|
||||||
|
{ $$ = $3; }
|
||||||
|
;
|
||||||
|
|
||||||
begin_func:
|
begin_func:
|
||||||
func_params
|
func_params
|
||||||
{
|
{
|
||||||
|
|
2
testing/btest/Baseline/language.lambda-nested-copy/out
Normal file
2
testing/btest/Baseline/language.lambda-nested-copy/out
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
106
|
||||||
|
106
|
1
testing/btest/Baseline/language.lambda-record-field/out
Normal file
1
testing/btest/Baseline/language.lambda-record-field/out
Normal file
|
@ -0,0 +1 @@
|
||||||
|
helloworld
|
|
@ -39,7 +39,7 @@ print A::outfile, Left;
|
||||||
print A::outfile, A::left;
|
print A::outfile, A::left;
|
||||||
print A::outfile, Right;
|
print A::outfile, Right;
|
||||||
print A::outfile, A::right;
|
print A::outfile, A::right;
|
||||||
}, pred=anonymous-function
|
}, pred=lambda_<10906653936191056190>
|
||||||
{
|
{
|
||||||
print A::outfile, ============PREDICATE============;
|
print A::outfile, ============PREDICATE============;
|
||||||
print A::outfile, A::typ;
|
print A::outfile, A::typ;
|
||||||
|
@ -134,7 +134,7 @@ print A::outfile, Left;
|
||||||
print A::outfile, A::left;
|
print A::outfile, A::left;
|
||||||
print A::outfile, Right;
|
print A::outfile, Right;
|
||||||
print A::outfile, A::right;
|
print A::outfile, A::right;
|
||||||
}, pred=anonymous-function
|
}, pred=lambda_<10906653936191056190>
|
||||||
{
|
{
|
||||||
print A::outfile, ============PREDICATE============;
|
print A::outfile, ============PREDICATE============;
|
||||||
print A::outfile, A::typ;
|
print A::outfile, A::typ;
|
||||||
|
@ -241,7 +241,7 @@ print A::outfile, Left;
|
||||||
print A::outfile, A::left;
|
print A::outfile, A::left;
|
||||||
print A::outfile, Right;
|
print A::outfile, Right;
|
||||||
print A::outfile, A::right;
|
print A::outfile, A::right;
|
||||||
}, pred=anonymous-function
|
}, pred=lambda_<10906653936191056190>
|
||||||
{
|
{
|
||||||
print A::outfile, ============PREDICATE============;
|
print A::outfile, ============PREDICATE============;
|
||||||
print A::outfile, A::typ;
|
print A::outfile, A::typ;
|
||||||
|
@ -468,7 +468,7 @@ print A::outfile, Left;
|
||||||
print A::outfile, A::left;
|
print A::outfile, A::left;
|
||||||
print A::outfile, Right;
|
print A::outfile, Right;
|
||||||
print A::outfile, A::right;
|
print A::outfile, A::right;
|
||||||
}, pred=anonymous-function
|
}, pred=lambda_<10906653936191056190>
|
||||||
{
|
{
|
||||||
print A::outfile, ============PREDICATE============;
|
print A::outfile, ============PREDICATE============;
|
||||||
print A::outfile, A::typ;
|
print A::outfile, A::typ;
|
||||||
|
@ -593,7 +593,7 @@ print A::outfile, Left;
|
||||||
print A::outfile, A::left;
|
print A::outfile, A::left;
|
||||||
print A::outfile, Right;
|
print A::outfile, Right;
|
||||||
print A::outfile, A::right;
|
print A::outfile, A::right;
|
||||||
}, pred=anonymous-function
|
}, pred=lambda_<10906653936191056190>
|
||||||
{
|
{
|
||||||
print A::outfile, ============PREDICATE============;
|
print A::outfile, ============PREDICATE============;
|
||||||
print A::outfile, A::typ;
|
print A::outfile, A::typ;
|
||||||
|
@ -718,7 +718,7 @@ print A::outfile, Left;
|
||||||
print A::outfile, A::left;
|
print A::outfile, A::left;
|
||||||
print A::outfile, Right;
|
print A::outfile, Right;
|
||||||
print A::outfile, A::right;
|
print A::outfile, A::right;
|
||||||
}, pred=anonymous-function
|
}, pred=lambda_<10906653936191056190>
|
||||||
{
|
{
|
||||||
print A::outfile, ============PREDICATE============;
|
print A::outfile, ============PREDICATE============;
|
||||||
print A::outfile, A::typ;
|
print A::outfile, A::typ;
|
||||||
|
@ -843,7 +843,7 @@ print A::outfile, Left;
|
||||||
print A::outfile, A::left;
|
print A::outfile, A::left;
|
||||||
print A::outfile, Right;
|
print A::outfile, Right;
|
||||||
print A::outfile, A::right;
|
print A::outfile, A::right;
|
||||||
}, pred=anonymous-function
|
}, pred=lambda_<10906653936191056190>
|
||||||
{
|
{
|
||||||
print A::outfile, ============PREDICATE============;
|
print A::outfile, ============PREDICATE============;
|
||||||
print A::outfile, A::typ;
|
print A::outfile, A::typ;
|
||||||
|
@ -968,7 +968,7 @@ print A::outfile, Left;
|
||||||
print A::outfile, A::left;
|
print A::outfile, A::left;
|
||||||
print A::outfile, Right;
|
print A::outfile, Right;
|
||||||
print A::outfile, A::right;
|
print A::outfile, A::right;
|
||||||
}, pred=anonymous-function
|
}, pred=lambda_<10906653936191056190>
|
||||||
{
|
{
|
||||||
print A::outfile, ============PREDICATE============;
|
print A::outfile, ============PREDICATE============;
|
||||||
print A::outfile, A::typ;
|
print A::outfile, A::typ;
|
||||||
|
@ -1198,7 +1198,7 @@ print A::outfile, Left;
|
||||||
print A::outfile, A::left;
|
print A::outfile, A::left;
|
||||||
print A::outfile, Right;
|
print A::outfile, Right;
|
||||||
print A::outfile, A::right;
|
print A::outfile, A::right;
|
||||||
}, pred=anonymous-function
|
}, pred=lambda_<10906653936191056190>
|
||||||
{
|
{
|
||||||
print A::outfile, ============PREDICATE============;
|
print A::outfile, ============PREDICATE============;
|
||||||
print A::outfile, A::typ;
|
print A::outfile, A::typ;
|
||||||
|
@ -1251,7 +1251,7 @@ print A::outfile, Left;
|
||||||
print A::outfile, A::left;
|
print A::outfile, A::left;
|
||||||
print A::outfile, Right;
|
print A::outfile, Right;
|
||||||
print A::outfile, A::right;
|
print A::outfile, A::right;
|
||||||
}, pred=anonymous-function
|
}, pred=lambda_<10906653936191056190>
|
||||||
{
|
{
|
||||||
print A::outfile, ============PREDICATE============;
|
print A::outfile, ============PREDICATE============;
|
||||||
print A::outfile, A::typ;
|
print A::outfile, A::typ;
|
||||||
|
@ -1304,7 +1304,7 @@ print A::outfile, Left;
|
||||||
print A::outfile, A::left;
|
print A::outfile, A::left;
|
||||||
print A::outfile, Right;
|
print A::outfile, Right;
|
||||||
print A::outfile, A::right;
|
print A::outfile, A::right;
|
||||||
}, pred=anonymous-function
|
}, pred=lambda_<10906653936191056190>
|
||||||
{
|
{
|
||||||
print A::outfile, ============PREDICATE============;
|
print A::outfile, ============PREDICATE============;
|
||||||
print A::outfile, A::typ;
|
print A::outfile, A::typ;
|
||||||
|
@ -1357,7 +1357,7 @@ print A::outfile, Left;
|
||||||
print A::outfile, A::left;
|
print A::outfile, A::left;
|
||||||
print A::outfile, Right;
|
print A::outfile, Right;
|
||||||
print A::outfile, A::right;
|
print A::outfile, A::right;
|
||||||
}, pred=anonymous-function
|
}, pred=lambda_<10906653936191056190>
|
||||||
{
|
{
|
||||||
print A::outfile, ============PREDICATE============;
|
print A::outfile, ============PREDICATE============;
|
||||||
print A::outfile, A::typ;
|
print A::outfile, A::typ;
|
||||||
|
@ -1410,7 +1410,7 @@ print A::outfile, Left;
|
||||||
print A::outfile, A::left;
|
print A::outfile, A::left;
|
||||||
print A::outfile, Right;
|
print A::outfile, Right;
|
||||||
print A::outfile, A::right;
|
print A::outfile, A::right;
|
||||||
}, pred=anonymous-function
|
}, pred=lambda_<10906653936191056190>
|
||||||
{
|
{
|
||||||
print A::outfile, ============PREDICATE============;
|
print A::outfile, ============PREDICATE============;
|
||||||
print A::outfile, A::typ;
|
print A::outfile, A::typ;
|
||||||
|
@ -1463,7 +1463,7 @@ print A::outfile, Left;
|
||||||
print A::outfile, A::left;
|
print A::outfile, A::left;
|
||||||
print A::outfile, Right;
|
print A::outfile, Right;
|
||||||
print A::outfile, A::right;
|
print A::outfile, A::right;
|
||||||
}, pred=anonymous-function
|
}, pred=lambda_<10906653936191056190>
|
||||||
{
|
{
|
||||||
print A::outfile, ============PREDICATE============;
|
print A::outfile, ============PREDICATE============;
|
||||||
print A::outfile, A::typ;
|
print A::outfile, A::typ;
|
||||||
|
|
19
testing/btest/language/lambda-nested-copy.zeek
Normal file
19
testing/btest/language/lambda-nested-copy.zeek
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
# @TEST-EXEC: zeek -b %INPUT >out
|
||||||
|
# @TEST-EXEC: btest-diff out
|
||||||
|
|
||||||
|
local outer = 100;
|
||||||
|
|
||||||
|
local lambda = function()
|
||||||
|
{
|
||||||
|
local inner = function(a: count, b: count, c: count, d: count, e: count, f: count)
|
||||||
|
{
|
||||||
|
print outer + f;
|
||||||
|
};
|
||||||
|
|
||||||
|
inner(1, 2, 3, 4, 5, 6);
|
||||||
|
};
|
||||||
|
|
||||||
|
lambda();
|
||||||
|
|
||||||
|
local copyLambda = copy(copy(copy(lambda)));
|
||||||
|
copyLambda();
|
13
testing/btest/language/lambda-record-field.zeek
Normal file
13
testing/btest/language/lambda-record-field.zeek
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
# @TEST-EXEC: zeek -b %INPUT >out
|
||||||
|
# @TEST-EXEC: btest-diff out
|
||||||
|
|
||||||
|
type myrec: record {
|
||||||
|
foo: function(a: string);
|
||||||
|
};
|
||||||
|
|
||||||
|
event zeek_init()
|
||||||
|
{
|
||||||
|
local w = "world";
|
||||||
|
local mr = myrec($foo(a: string) = { print a + w; });
|
||||||
|
mr$foo("hello");
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue