diff --git a/src/ant/ant/antObject.cc b/src/ant/ant/antObject.cc index 808c9ba19..1a486c915 100644 --- a/src/ant/ant/antObject.cc +++ b/src/ant/ant/antObject.cc @@ -409,15 +409,15 @@ class AnnotationEval : public tl::Eval { public: - AnnotationEval (const Object &obj, const db::DFTrans &t) + AnnotationEval (const ant::Object &obj, const db::DFTrans &t) : m_obj (obj), m_trans (t) { } - const Object &obj () const { return m_obj; } + const ant::Object &obj () const { return m_obj; } const db::DFTrans &trans () const { return m_trans; } private: - const Object &m_obj; + const ant::Object &m_obj; db::DFTrans m_trans; }; diff --git a/src/gsi/gsi/gsiDeclTl.cc b/src/gsi/gsi/gsiDeclTl.cc index c3deae536..0b0fb4619 100644 --- a/src/gsi/gsi/gsiDeclTl.cc +++ b/src/gsi/gsi/gsiDeclTl.cc @@ -430,6 +430,18 @@ public: // .. nothing yet .. } + ExpressionWrapper (tl::Eval *parent) + : tl::Eval (parent) + { + // .. nothing yet .. + } + + ExpressionWrapper (tl::Eval *global, tl::Eval *parent) + : tl::Eval (global, parent) + { + // .. nothing yet .. + } + void parse (const std::string &e) { mp_expr.reset (0); @@ -452,15 +464,106 @@ private: std::unique_ptr mp_expr; }; +class FunctionBody + : public gsi::ObjectBase, public tl::EvalFunction +{ +public: + FunctionBody () + : tl::EvalFunction (), m_supports_keyword_parameters (false), m_min_args (0), m_max_args (std::numeric_limits::max ()) + { } + + bool supports_keyword_parameters () const + { + return m_supports_keyword_parameters; + } + + void set_with_kwargs (bool f) + { + m_supports_keyword_parameters = f; + } + + bool with_kwargs () const + { + return m_supports_keyword_parameters; + } + + void set_min_args (unsigned int a) + { + m_min_args = a; + } + + unsigned int min_args () const + { + return m_min_args; + } + + void set_max_args (unsigned int a) + { + m_max_args = a; + } + + unsigned int max_args () const + { + return m_max_args; + } + + tl::Variant do_execute (const std::vector &, const std::map &) const + { + return tl::Variant (); + } + + virtual void execute (const tl::ExpressionParserContext &context, tl::Variant &out, const std::vector &args, const std::map *kwargs) const + { + if (args.size () < m_min_args) { + context.error (tl::sprintf (tl::to_string (tr ("Too few arguments (got %d, expected %d at least)")), int (args.size ()), int (m_min_args))); + } + if (args.size () > m_max_args) { + context.error (tl::sprintf (tl::to_string (tr ("Too many arguments (got %d, expected %d at most)")), int (args.size ()), int (m_max_args))); + } + + static std::map no_args; + const std::map &kwa = kwargs ? *kwargs : no_args; + if (execute_cb.can_issue ()) { + out = execute_cb.issue &, const std::map &> (&FunctionBody::do_execute, args, kwa); + } else { + out = FunctionBody::do_execute (args, kwa); + } + } + + gsi::Callback execute_cb; + +private: + bool m_supports_keyword_parameters; + unsigned int m_min_args, m_max_args; +}; + } -static tl::Variant eval_expr (const std::string &e) +static tl::Variant eval_expr (const std::string &e, const std::map &variables) { ExpressionWrapper expr; + for (std::map::const_iterator v = variables.begin (); v != variables.end (); ++v) { + expr.set_var (v->first, v->second); + } expr.parse (e); return expr.eval (); } +static ExpressionWrapper *new_expr0 () +{ + return new ExpressionWrapper (); +} + +static ExpressionWrapper *new_expr1a (tl::Eval *parent) +{ + return new ExpressionWrapper (parent); +} + +static ExpressionWrapper *new_expr1b (tl::Eval *global, tl::Eval *parent) +{ + return new ExpressionWrapper (global, parent); +} + static ExpressionWrapper *new_expr1 (const std::string &e) { std::unique_ptr expr (new ExpressionWrapper ()); @@ -481,12 +584,76 @@ static ExpressionWrapper *new_expr2 (const std::string &e, const std::mapkeep (); + eval->define_function (name, func); +} + +Class decl_FunctionBody ("tl", "FunctionBody", + gsi::method ("with_kwargs=", &FunctionBody::set_with_kwargs, gsi::arg ("f"), + "@brief Sets a value indicating whether this function accepts keyword arguments.\n" + "By default, the function will not accept keyword arguments and the 'kwargs' argument " + "is always empty upon 'execute'. Set this attribute to 'true' to enable keyword arguments." + ) + + gsi::method ("with_kwargs", &FunctionBody::with_kwargs, + "@brief Gets a value indicating whether this function accepts keyword arguments.\n" + ) + + gsi::method ("min_args=", &FunctionBody::set_min_args, gsi::arg ("a"), + "@brief Specifies the minimum number of arguments expected by this function.\n" + ) + + gsi::method ("min_args", &FunctionBody::min_args, + "@brief Gets the minimum number of arguments expected by this function.\n" + ) + + gsi::method ("max_args=", &FunctionBody::set_max_args, gsi::arg ("a"), + "@brief Specifies the maximum number of arguments expected by this function.\n" + ) + + gsi::method ("max_args", &FunctionBody::max_args, + "@brief Gets the maximum number of arguments expected by this function.\n" + ) + + gsi::callback ("execute", &FunctionBody::do_execute, &FunctionBody::execute_cb, gsi::arg ("args"), gsi::arg ("kwargs"), + "@brief Implements the function\n" + "@param args The function arguments\n" + "@param kwargs The keyword arguments (only present if \\with_kwargs is true)" + "Reimplement this method to provide the function's implementation. The number of arguments is " + "between \\min_args and \\max_args.\n" + ), + "@brief Defines a function for use in expressions.\n" + "This class provides an interface for implementing custom functions for expressions. " + "See \\ExpressionContext#func for a use case.\n" + "\n" + "This class has been introduced in version 0.29.6." +); + Class decl_ExpressionContext ("tl", "ExpressionContext", gsi::method ("var", &tl::Eval::set_var, gsi::arg ("name"), gsi::arg ("value"), "@brief Defines a variable with the given name and value\n" ) + + gsi::method_ext ("func", &def_func, gsi::arg ("name"), gsi::arg ("body"), + "@brief Defines a function with the given name and function body\n" + "The function body is an implementation of the \\FunctionBody class. To use it, create a subclass, i.e.\n" + "\n" + "@code\n" + "class MyFunction < RBA::FunctionBody\n" + " def initialize\n" + " self.min_args = self.max_args = 1 # one argument\n" + " self.with_kwargs = false\n" + " end\n" + " def execute(args, kwargs)\n" + " return args[0] + 1 # one more\n" + " end\n" + "end\n" + "\n" + "ctx = ExpressionContext::new\n" + "ctx.func('myfunction', MyFunction::new)\n" + "ctx.eval('myfunction(17)') # gives 18\n" + "@endcode\n" + "\n" + "This method has been introduced in version 0.29.6." + ) + gsi::method ("global_var", &tl::Eval::set_global_var, gsi::arg ("name"), gsi::arg ("value"), "@brief Defines a global variable with the given name and value\n" + "Global variables are available to all expressions sharing the same global context." ) + gsi::method ("eval", &tl::Eval::eval, gsi::arg ("expr"), "@brief Compiles and evaluates the given expression in this context\n" @@ -494,18 +661,63 @@ Class decl_ExpressionContext ("tl", "ExpressionContext", ), "@brief Represents the context of an expression evaluation\n" "\n" - "The context provides a variable namespace for the expression evaluation.\n" + "The context provides a variable and function namespace for the expression evaluation.\n" "\n" "This class has been introduced in version 0.26 when \\Expression was separated into the execution and context part.\n" ); Class decl_ExpressionWrapper (decl_ExpressionContext, "tl", "Expression", + gsi::constructor ("new", &new_expr0, + "@brief Creates a new expression evaluator for late compilation\n" + "Use \\var to define variables, \\func to define functions and use \\text= to compile an expression.\n" + "\n" + "This constructor has been added in version 0.29.6." + ) + + gsi::constructor ("new", &new_expr1a, gsi::arg ("parent"), + "@brief Creates a new expression object with a parent context.\n" + "Variables and functions not found in this context are looked up in the parent context. " + "The parent context itself can have another parent context. Parent contexts allow " + "sharing variables among multiple expressions.\n" + "\n" + "Note that the expression object will not hold a strong reference to the parent context. It " + "will get lost of the variable holding the parent context goes out of scope.\n" + "\n" + "This constructor has been introduced in version 0.29.6." + ) + + gsi::constructor ("new", &new_expr1b, gsi::arg ("global"), gsi::arg ("parent"), + "@brief Creates a new expression object with a parent and a global context.\n" + "Variables and functions not found in this context are looked up in the parent context and " + "finally in the global context.\n" + "The parent and the global context themselves can have another parent contexts. Parent and global contexts allow " + "sharing variables among multiple expressions.\n" + "\n" + "Without a global context given, the expressions will use the global context singleton that is available to " + "all expressions.\n" + "\n" + "Note that the expression object will not hold a strong reference to the parent or global context. These contexts " + "will get lost of a variable holding them goes out of scope.\n" + "\n" + "This constructor has been introduced in version 0.29.6." + ) + gsi::constructor ("new", &new_expr1, gsi::arg ("expr"), "@brief Creates an expression evaluator\n" + "This is a convenience constructor that is equivalent to:\n" + "@code\n" + "e = RBA::Expression::new\n" + "e.text = expr\n" + "..." + "@/code\n" ) + gsi::constructor ("new", &new_expr2, gsi::arg ("expr"), gsi::arg ("variables"), "@brief Creates an expression evaluator\n" - "This version of the constructor takes a hash of variables available to the expressions." + "This version of the constructor takes a hash of variables available to the expressions. " + "It is a convenience constructor that is equivalent to:\n" + "@code\n" + "e = RBA::Expression::new\n" + "variables.each { |n,v| e.var(n, v) }\n" + "e.text = expr\n" + "..." + "@/code\n" ) + gsi::method ("text=", &ExpressionWrapper::parse, gsi::arg ("expr"), "@brief Sets the given text as the expression." @@ -513,9 +725,13 @@ Class decl_ExpressionWrapper (decl_ExpressionContext, "tl", " gsi::method ("eval", &ExpressionWrapper::eval, "@brief Evaluates the current expression and returns the result\n" ) + - gsi::method ("eval", &eval_expr, gsi::arg ("expr"), - "@brief A convience function to evaluate the given expression and directly return the result\n" - "This is a static method that does not require instantiation of the expression object first." + gsi::method ("eval", &eval_expr, gsi::arg ("expr"), gsi::arg ("variables", std::map (), "{}"), + "@brief A convience function to evaluate the given expression and directly returns the result\n" + "@param expr The expression to evaluate\n" + "@param variables The variables to use in the expression\n" + "This is a static method that does not require instantiation of the expression object first.\n" + "\n" + "The variable argument has been added in version 0.29.6.\n" ), "@brief Evaluation of Expressions\n" "\n" @@ -524,8 +740,42 @@ Class decl_ExpressionWrapper (decl_ExpressionContext, "tl", " "inside a script client. This class is provided mainly for testing purposes.\n" "\n" "An expression is 'compiled' into an Expression object and can be evaluated multiple times.\n" + "The Expression object is based on the \\ExpressionContext object which provides a namespace for variables \n" + "and functions.\n" + "\n" + "The basic use model for the Expression object is this:\n" + "\n" + "@code\n" + "e = RBA::Expression::new\n" + "e.var('A', 17)\n" + "e.text = 'A + 1'\n" + "e.eval # gives 18\n" + "e.var('A', 2)\n" + "e.eval # gives 3\n" + "@/code\n" + "\n" + "Expressions allow to share variables among multiple expressions through parent contexts:\n" + "\n" + "@code\n" + "pc = RBA::ExpressionContext::new\n" + "pc.var('A', 17)\n" + "\n" + "e1 = RBA::Expression::new(pc)\n" + "e1.text = 'A + 1'\n" + "e1.eval # gives 18\n" + "\n" + "e2 = RBA::Expression::new(pc)\n" + "e2.text = 'A + 2'\n" + "e2.eval # gives 19\n" + "\n" + "# modifying 'A' in pc changes input for both expressions\n" + "pc.var('A', 2)\n" + "e1.eval # gives 3\n" + "e2.eval # gives 4\n" + "@/code\n" "\n" "This class has been introduced in version 0.25. In version 0.26 it was separated into execution and context.\n" + "In version 0.29.6, the context was significantly enhanced towards parent contexts and functions.\n" ); static tl::GlobPattern *new_glob_pattern (const std::string &s) diff --git a/src/tl/tl/tlExpression.cc b/src/tl/tl/tlExpression.cc index 35eaa01a4..9da470c35 100644 --- a/src/tl/tl/tlExpression.cc +++ b/src/tl/tl/tlExpression.cc @@ -83,7 +83,7 @@ ExpressionParserContext::ExpressionParserContext (const Expression *expr, const } void -ExpressionParserContext::error (const std::string &message) +ExpressionParserContext::error (const std::string &message) const { throw EvalError (message, *this); } diff --git a/src/tl/tl/tlExpression.h b/src/tl/tl/tlExpression.h index 3912d0a5b..b1a6d5f6a 100644 --- a/src/tl/tl/tlExpression.h +++ b/src/tl/tl/tlExpression.h @@ -137,7 +137,7 @@ public: /** * @brief Reimplementation of tl::Extractor's error method */ - virtual void error (const std::string &message); + virtual void error (const std::string &message) const; /** * @brief Gets a string indication where we are currently @@ -383,6 +383,7 @@ private: * @brief Provides the context for the expression parser and evaluation */ class TL_PUBLIC Eval + : public tl::Object { public: /** @@ -568,7 +569,7 @@ public: */ tl::Eval *global () { - return mp_global; + return mp_global.get (); } /** @@ -576,13 +577,13 @@ public: */ tl::Eval *parent () { - return mp_parent; + return mp_parent.get (); } private: friend class Expression; - Eval *mp_parent, *mp_global; + tl::weak_ptr mp_parent, mp_global; std::map m_local_vars; std::map m_local_functions; bool m_sloppy; diff --git a/src/tl/tl/tlString.cc b/src/tl/tl/tlString.cc index d6bb80ede..9b1c4937d 100644 --- a/src/tl/tl/tlString.cc +++ b/src/tl/tl/tlString.cc @@ -1582,11 +1582,11 @@ Extractor::skip () } void -Extractor::error (const std::string &msg) +Extractor::error (const std::string &msg) const { std::string m (msg); - if (at_end ()) { + if (! *m_cp) { m += tl::to_string (tr (", but text ended")); } else { m += tl::to_string (tr (" here: ")); diff --git a/src/tl/tl/tlString.h b/src/tl/tl/tlString.h index 8af7926a3..e77b7b72b 100644 --- a/src/tl/tl/tlString.h +++ b/src/tl/tl/tlString.h @@ -786,7 +786,7 @@ public: /** * @brief Throw an error with a context information */ - virtual void error (const std::string &msg); + virtual void error (const std::string &msg) const; /** * @brief Some syntactic sugar diff --git a/testdata/ruby/tlTest.rb b/testdata/ruby/tlTest.rb index e2acc863d..99f315f2f 100644 --- a/testdata/ruby/tlTest.rb +++ b/testdata/ruby/tlTest.rb @@ -220,8 +220,66 @@ class Tl_TestClass < TestBase end + class MyFunction < RBA::FunctionBody + def initialize + self.with_kwargs = false + self.min_args = 1 + self.max_args = 1 + end + def execute(args, kwargs) + return args[0] + 1 + end + end + + # Functions + def test_3_FunctionsInExpressions + + e = RBA::ExpressionContext::new + e.func("f", MyFunction::new) + + self.assert_equal(e.eval("f(17)"), 18) + + # now with embedded expression + e = RBA::Expression::new + e.func("f", MyFunction::new) + + e.var("A", nil) + e.text = "f(A)" + + e.var("A", 1) + self.assert_equal(e.eval, 2) + + e.var("A", 4) + self.assert_equal(e.eval, 5) + + end + + # Parent contexts + def test_4_FunctionsInExpressions + + pc = RBA::ExpressionContext::new + pc.var("A", 17) + + e1 = RBA::Expression::new(pc) + e1.text = "A + 1" + self.assert_equal(e1.eval, 18) + + e2 = RBA::Expression::new(pc) + e2.text = "A + 2" + self.assert_equal(e2.eval, 19) + + pc.var("A", 4) + self.assert_equal(e1.eval, 5) + self.assert_equal(e2.eval, 6) + + pc._destroy + + self.assert_equal(e1.eval, 5) + + end + # Glob pattern - def test_3_GlobPattern + def test_10_GlobPattern pat = RBA::GlobPattern::new("a*b") @@ -290,7 +348,7 @@ class Tl_TestClass < TestBase end # Recipe - def test_4_Recipe + def test_20_Recipe # make sure there isn't a second instance GC.start