From 8ca7b19fdf2abc8e41f4ea46716c5fc7d4460eb7 Mon Sep 17 00:00:00 2001 From: Johannes Wolf Date: Sun, 14 Sep 2025 21:48:28 +0200 Subject: [PATCH 01/50] tree: Working on making simfil more exception free --- include/simfil/error.h | 10 +++ include/simfil/expression.h | 4 +- include/simfil/function.h | 20 +++--- include/simfil/operator.h | 106 ++++++++++++---------------- include/simfil/result.h | 7 +- include/simfil/transient.h | 25 ++++--- include/simfil/typed-meta-type.h | 27 +++---- include/simfil/types.h | 14 ++-- include/simfil/value.h | 4 +- src/completion.cpp | 8 +-- src/completion.h | 8 +-- src/expected.h | 2 +- src/expressions.cpp | 117 +++++++++++++++++++------------ src/expressions.h | 65 +++++++++-------- src/function.cpp | 32 +++++---- src/parser.cpp | 2 +- src/simfil.cpp | 11 +-- src/types.cpp | 33 ++++----- test/common.cpp | 6 +- test/common.hpp | 6 +- test/simfil.cpp | 4 ++ 21 files changed, 280 insertions(+), 231 deletions(-) diff --git a/include/simfil/error.h b/include/simfil/error.h index 6dd372f6..0aa33355 100644 --- a/include/simfil/error.h +++ b/include/simfil/error.h @@ -12,12 +12,22 @@ struct Token; struct Error { enum Type { + // Parser Errors ParserError, InvalidType, InvalidExpression, ExpectedEOF, NullModel, IOError, + + // Evaluation errors + UnknownFunction, + RuntimeError, + InternalError, + InvalidOperator, + InvalidOperands, + + Unimplemented, }; explicit Error(Type type); diff --git a/include/simfil/expression.h b/include/simfil/expression.h index a2298055..57b8bd61 100644 --- a/include/simfil/expression.h +++ b/include/simfil/expression.h @@ -51,7 +51,7 @@ class Expr virtual auto toString() const -> std::string = 0; /* Evaluation wrapper */ - auto eval(Context ctx, Value val, const ResultFn& res) -> Result + auto eval(Context ctx, Value val, const ResultFn& res) -> tl::expected { if (ctx.canceled()) return Result::Stop; @@ -79,7 +79,7 @@ class Expr private: /* Abstract evaluation implementation */ - virtual auto ieval(Context ctx, const Value& value, const ResultFn& result) -> Result = 0; + virtual auto ieval(Context ctx, const Value& value, const ResultFn& result) -> tl::expected = 0; SourceLocation sourceLocation_; }; diff --git a/include/simfil/function.h b/include/simfil/function.h index a7332a3a..9a84e575 100644 --- a/include/simfil/function.h +++ b/include/simfil/function.h @@ -76,7 +76,7 @@ class Function virtual ~Function() = default; virtual auto ident() const -> const FnInfo& = 0; - virtual auto eval(Context, Value, const std::vector&, const ResultFn&) const -> Result = 0; + virtual auto eval(Context, Value, const std::vector&, const ResultFn&) const -> tl::expected = 0; }; class CountFn : public Function @@ -87,7 +87,7 @@ class CountFn : public Function CountFn(); auto ident() const -> const FnInfo& override; - auto eval(Context, Value, const std::vector&, const ResultFn&) const -> Result override; + auto eval(Context, Value, const std::vector&, const ResultFn&) const -> tl::expected override; }; class TraceFn : public Function @@ -98,7 +98,7 @@ class TraceFn : public Function TraceFn(); auto ident() const -> const FnInfo& override; - auto eval(Context, Value, const std::vector&, const ResultFn&) const -> Result override; + auto eval(Context, Value, const std::vector&, const ResultFn&) const -> tl::expected override; }; class RangeFn : public Function @@ -109,7 +109,7 @@ class RangeFn : public Function RangeFn(); auto ident() const -> const FnInfo& override; - auto eval(Context, Value, const std::vector&, const ResultFn&) const -> Result override; + auto eval(Context, Value, const std::vector&, const ResultFn&) const -> tl::expected override; }; class ReFn : public Function @@ -120,7 +120,7 @@ class ReFn : public Function ReFn(); auto ident() const -> const FnInfo& override; - auto eval(Context, Value, const std::vector&, const ResultFn&) const -> Result override; + auto eval(Context, Value, const std::vector&, const ResultFn&) const -> tl::expected override; }; class ArrFn : public Function @@ -131,7 +131,7 @@ class ArrFn : public Function ArrFn(); auto ident() const -> const FnInfo& override; - auto eval(Context, Value, const std::vector&, const ResultFn&) const -> Result override; + auto eval(Context, Value, const std::vector&, const ResultFn&) const -> tl::expected override; }; class SplitFn : public Function @@ -142,7 +142,7 @@ class SplitFn : public Function SplitFn(); auto ident() const -> const FnInfo& override; - auto eval(Context, Value, const std::vector&, const ResultFn&) const -> Result override; + auto eval(Context, Value, const std::vector&, const ResultFn&) const -> tl::expected override; }; class SelectFn : public Function @@ -153,7 +153,7 @@ class SelectFn : public Function SelectFn(); auto ident() const -> const FnInfo& override; - auto eval(Context, Value, const std::vector&, const ResultFn&) const -> Result override; + auto eval(Context, Value, const std::vector&, const ResultFn&) const -> tl::expected override; }; class SumFn : public Function @@ -164,7 +164,7 @@ class SumFn : public Function SumFn(); auto ident() const -> const FnInfo& override; - auto eval(Context, Value, const std::vector&, const ResultFn&) const -> Result override; + auto eval(Context, Value, const std::vector&, const ResultFn&) const -> tl::expected override; }; class KeysFn : public Function @@ -175,7 +175,7 @@ class KeysFn : public Function KeysFn(); auto ident() const -> const FnInfo& override; - auto eval(Context, Value, const std::vector&, const ResultFn&) const -> Result override; + auto eval(Context, Value, const std::vector&, const ResultFn&) const -> tl::expected override; }; /** Utility functions for working with arguments*/ diff --git a/include/simfil/operator.h b/include/simfil/operator.h index dc276cbf..faebdbd2 100644 --- a/include/simfil/operator.h +++ b/include/simfil/operator.h @@ -2,10 +2,12 @@ #pragma once -#include "fmt/format.h" +#include "simfil/error.h" #include "simfil/value.h" #include "exception-handler.h" +#include "fmt/format.h" +#include #include #include #include @@ -25,19 +27,6 @@ struct InvalidOperands { } }; -/** - * Exception for invalid operand types. - */ -struct InvalidOperandsError : std::exception -{ - std::string operatorName; - - explicit InvalidOperandsError(std::string_view opName) - : operatorName(opName) - {} -}; - - #define NAME(str) \ static const char * name() { \ return str; \ @@ -628,21 +617,21 @@ namespace impl { template -Value makeOperatorResult(_CType&& value) +inline auto makeOperatorResult(_CType&& value) -> tl::expected { return Value::make(std::forward<_CType>(value)); } template -inline Value makeOperatorResult(Value value) +inline auto makeOperatorResult(Value value) -> tl::expected { return value; } template -inline Value makeOperatorResult(InvalidOperands) +inline auto makeOperatorResult(InvalidOperands) -> tl::expected { - raise(_Operator::name()); + return tl::unexpected(Error::InvalidOperands, fmt::format("Invalid operands for operator '{}'", _Operator::name())); } } @@ -650,34 +639,23 @@ inline Value makeOperatorResult(InvalidOperands) template struct UnaryOperatorDispatcher { - static auto dispatch(const Value& value) -> Value + static auto dispatch(const Value& value) -> tl::expected { - try { - if (value.isa(ValueType::TransientObject)) { - const auto& obj = value.as(); - return obj.meta->unaryOp(_Operator::name(), obj); - } - - return value.visit(UnaryOperatorDispatcher()); - } catch (const InvalidOperandsError& err) { - std::string ltype; - try { - ltype = UnaryOperatorDispatcher::dispatch(value).toString(); - } catch (...) { - ltype = valueType2String(value.type); - } - raise("Invalid operand "s + ltype + - " for operator "s + std::string(err.operatorName)); + if (value.isa(ValueType::TransientObject)) { + const auto& obj = value.as(); + return obj.meta->unaryOp(_Operator::name(), obj); } + + return value.visit(UnaryOperatorDispatcher()); } - auto operator()(UndefinedType) -> Value + auto operator()(UndefinedType) -> tl::expected { return Value::undef(); } template - auto operator()(const _Value& rhs) -> Value + auto operator()(const _Value& rhs) -> tl::expected { return impl::makeOperatorResult<_Operator>(_Operator()(rhs)); } @@ -691,13 +669,13 @@ struct BinaryOperatorDispatcherRHS : lhs(lhs) {} - auto operator()(UndefinedType) -> Value + auto operator()(UndefinedType) -> tl::expected { return Value::undef(); } template - auto operator()(const Right& rhs) -> Value + auto operator()(const Right& rhs) -> tl::expected { return impl::makeOperatorResult(Operator()(lhs, rhs)); } @@ -708,48 +686,52 @@ struct BinaryOperatorDispatcher { const Value& value; - static auto dispatch(const Value& lhs, const Value& rhs) -> Value + static auto dispatch(const Value& lhs, const Value& rhs) -> tl::expected { - try { + auto result = ([&]() -> tl::expected { if (lhs.isa(ValueType::TransientObject)) { - if (rhs.isa(ValueType::Undef)) + if (rhs.isa(ValueType::Undef)) { return Value::undef(); - const auto& obj = lhs.as(); - return obj.meta->binaryOp(Operator::name(), obj, rhs); + } else { + const auto& obj = lhs.as(); + return obj.meta->binaryOp(Operator::name(), obj, rhs); + } } - - if (rhs.isa(ValueType::TransientObject)) { - if (lhs.isa(ValueType::Undef)) + else if (rhs.isa(ValueType::TransientObject)) { + if (lhs.isa(ValueType::Undef)) { return Value::undef(); - const auto& obj = rhs.as(); - return obj.meta->binaryOp(Operator::name(), lhs, obj); + } else { + const auto& obj = rhs.as(); + return obj.meta->binaryOp(Operator::name(), lhs, obj); + } } - - return lhs.visit(BinaryOperatorDispatcher(rhs)); - } catch (const InvalidOperandsError& err) { - std::string ltype, rtype; - try { - ltype = UnaryOperatorDispatcher::dispatch(lhs).toString(); - rtype = UnaryOperatorDispatcher::dispatch(rhs).toString(); - } catch (...) { - ltype = valueType2String(lhs.type); - rtype = valueType2String(rhs.type); + else { + return lhs.visit(BinaryOperatorDispatcher(rhs)); } - raise(fmt::format("Invalid operands {} and {} for operator {}", ltype, rtype, err.operatorName)); + })(); + + // Try to find the operand types + if (!result) { + auto ltype = UnaryOperatorDispatcher::dispatch(lhs).value_or(Value::strref("unknown")).toString(); + auto rtype = UnaryOperatorDispatcher::dispatch(rhs).value_or(Value::strref("unknown")).toString(); + + return tl::unexpected(Error::InvalidOperands, fmt::format("Invalid operands {} and {} for operator {}", ltype, rtype, Operator::name())); } + + return result; } explicit BinaryOperatorDispatcher(const Value& value) : value(value) {} - auto operator()(UndefinedType) -> Value + auto operator()(UndefinedType) -> tl::expected { return Value::undef(); } template - auto operator()(const Left& lhs) -> Value + auto operator()(const Left& lhs) -> tl::expected { return value.visit(BinaryOperatorDispatcherRHS(lhs)); } diff --git a/include/simfil/result.h b/include/simfil/result.h index ded79f99..afc3908a 100644 --- a/include/simfil/result.h +++ b/include/simfil/result.h @@ -1,6 +1,9 @@ #pragma once +#include + #include "simfil/environment.h" +#include "simfil/error.h" namespace simfil { @@ -15,7 +18,7 @@ struct ResultFn { virtual ~ResultFn() = default; - virtual auto operator()(Context ctx, Value value) const -> Result = 0; + virtual auto operator()(Context ctx, Value value) const -> tl::expected = 0; }; template @@ -27,7 +30,7 @@ struct LambdaResultFn final : ResultFn : lambda(std::move(ref)) {} - auto operator()(Context ctx, Value value) const -> Result override + auto operator()(Context ctx, Value value) const -> tl::expected override { return lambda(std::move(ctx), std::move(value)); } diff --git a/include/simfil/transient.h b/include/simfil/transient.h index 03f27de9..d907a6a9 100644 --- a/include/simfil/transient.h +++ b/include/simfil/transient.h @@ -4,6 +4,9 @@ #include #include +#include + +#include "error.h" namespace simfil { @@ -20,7 +23,7 @@ struct MetaType * - Make Value::node optionally owning its ptr * - Set ModelNode instance for objects you create */ - MetaType(std::string ident) + explicit MetaType(std::string ident) : ident(std::move(ident)) {} @@ -32,12 +35,12 @@ struct MetaType virtual auto deinit(void*) const -> void = 0; /* Operators */ - virtual auto unaryOp(std::string_view op, const TransientObject&) const -> Value = 0; - virtual auto binaryOp(std::string_view op, const TransientObject&, const Value&) const -> Value = 0; - virtual auto binaryOp(std::string_view op, const Value&, const TransientObject&) const -> Value = 0; + virtual auto unaryOp(std::string_view op, const TransientObject&) const -> tl::expected = 0; + virtual auto binaryOp(std::string_view op, const TransientObject&, const Value&) const -> tl::expected = 0; + virtual auto binaryOp(std::string_view op, const Value&, const TransientObject&) const -> tl::expected = 0; /* Unpack */ - virtual auto unpack(const TransientObject&, std::function) const -> void = 0; /* cb return false to stop! */ + virtual auto unpack(const TransientObject&, std::function) const -> tl::expected = 0; /* cb return false to stop! */ }; /** @@ -61,19 +64,19 @@ struct TransientObject return *this; } - TransientObject(TransientObject&& other) - : meta(std::move(other.meta)) - , data(std::move(other.data)) + TransientObject(TransientObject&& other) noexcept + : meta(other.meta) + , data(other.data) { other.meta = nullptr; other.data = nullptr; } - auto operator=(TransientObject&& other) + TransientObject& operator=(TransientObject&& other) noexcept { meta->deinit(data); - meta = std::move(other.meta); - data = std::move(other.data); + meta = other.meta; + data = other.data; other.meta = nullptr; other.data = nullptr; return *this; diff --git a/include/simfil/typed-meta-type.h b/include/simfil/typed-meta-type.h index 7dee909a..3506e094 100644 --- a/include/simfil/typed-meta-type.h +++ b/include/simfil/typed-meta-type.h @@ -5,6 +5,7 @@ #include "value.h" #include "operator.h" #include "exception-handler.h" +#include "transient.h" namespace simfil { @@ -17,7 +18,7 @@ namespace simfil template struct TypedMetaType : MetaType { - TypedMetaType(std::string ident) + explicit TypedMetaType(std::string ident) : MetaType(std::move(ident)) {} @@ -35,48 +36,48 @@ struct TypedMetaType : MetaType return nullptr; } - auto init() const -> void* + auto init() const -> void* override { return new Type(); } - auto copy(void* ptr) const -> void* + auto copy(void* ptr) const -> void* override { return new Type(*((Type*)ptr)); } - auto deinit(void* ptr) const -> void + auto deinit(void* ptr) const -> void override { delete (Type*)ptr; } - auto unaryOp(std::string_view op, const TransientObject& obj) const -> Value + auto unaryOp(std::string_view op, const TransientObject& obj) const -> tl::expected override { return unaryOp(op, *(const Type*)obj.data); } - auto binaryOp(std::string_view op, const TransientObject& obj, const Value& v) const -> Value + auto binaryOp(std::string_view op, const TransientObject& obj, const Value& v) const -> tl::expected override { return binaryOp(op, *(const Type*)obj.data, v); } - auto binaryOp(std::string_view op, const Value& v, const TransientObject& obj) const -> Value + auto binaryOp(std::string_view op, const Value& v, const TransientObject& obj) const -> tl::expected override { return binaryOp(op, v, *(const Type*)obj.data); } - auto unpack(const TransientObject& obj, std::function fn) const -> void + auto unpack(const TransientObject& obj, std::function fn) const -> tl::expected override { return unpack(*(const Type*)obj.data, fn); } - virtual auto unaryOp(std::string_view op, const Type&) const -> Value = 0; - virtual auto binaryOp(std::string_view op, const Type&, const Value&) const -> Value = 0; - virtual auto binaryOp(std::string_view op, const Value&, const Type&) const -> Value = 0; + virtual auto unaryOp(std::string_view op, const Type&) const -> tl::expected = 0; + virtual auto binaryOp(std::string_view op, const Type&, const Value&) const -> tl::expected = 0; + virtual auto binaryOp(std::string_view op, const Value&, const Type&) const -> tl::expected = 0; - virtual auto unpack(const Type&, std::function fn) const -> void + virtual auto unpack(const Type&, std::function fn) const -> tl::expected { - raise("..."); + return tl::unexpected(Error::Unimplemented, "Type has no unpack operator"); } }; diff --git a/include/simfil/types.h b/include/simfil/types.h index 2117f38f..683d668e 100644 --- a/include/simfil/types.h +++ b/include/simfil/types.h @@ -36,11 +36,11 @@ class IRangeType : public TypedMetaType auto make(int64_t a, int64_t b) -> Value; - auto unaryOp(std::string_view op, const IRange& self) const -> Value override; - auto binaryOp(std::string_view op, const IRange& l, const Value& r) const -> Value override; - auto binaryOp(std::string_view op, const Value& l, const IRange& r) const -> Value override; + auto unaryOp(std::string_view op, const IRange& self) const -> tl::expected override; + auto binaryOp(std::string_view op, const IRange& l, const Value& r) const -> tl::expected override; + auto binaryOp(std::string_view op, const Value& l, const IRange& r) const -> tl::expected override; - auto unpack(const IRange& , std::function res) const -> void override; + auto unpack(const IRange& , std::function res) const -> tl::expected override; }; struct Re @@ -58,9 +58,9 @@ class ReType : public TypedMetaType auto make(std::string_view expr) -> Value; - auto unaryOp(std::string_view op, const Re&) const -> Value override; - auto binaryOp(std::string_view op, const Re&, const Value&) const -> Value override; - auto binaryOp(std::string_view op, const Value&, const Re&) const -> Value override; + auto unaryOp(std::string_view op, const Re&) const -> tl::expected override; + auto binaryOp(std::string_view op, const Re&, const Value&) const -> tl::expected override; + auto binaryOp(std::string_view op, const Value&, const Re&) const -> tl::expected override; }; } diff --git a/include/simfil/value.h b/include/simfil/value.h index fd8bcf7a..e6a550da 100644 --- a/include/simfil/value.h +++ b/include/simfil/value.h @@ -367,8 +367,8 @@ class Value if (isa(ValueType::TransientObject)) { const auto& obj = std::get(value); if (obj.meta) { - if (Value vv = obj.meta->unaryOp("string", obj); vv.isa(ValueType::String)) - return vv.as(); + if (auto vv = obj.meta->unaryOp("string", obj); vv && vv->isa(ValueType::String)) + return vv->template as(); return "<"s + obj.meta->ident + ">"s; } } diff --git a/src/completion.cpp b/src/completion.cpp index eceb1d38..cf3269e6 100644 --- a/src/completion.cpp +++ b/src/completion.cpp @@ -136,7 +136,7 @@ auto CompletionFieldOrWordExpr::type() const -> Type return Type::PATH; } -auto CompletionFieldOrWordExpr::ieval(Context ctx, const Value& val, const ResultFn& res) -> Result +auto CompletionFieldOrWordExpr::ieval(Context ctx, const Value& val, const ResultFn& res) -> tl::expected { if (ctx.phase == Context::Phase::Compilation) return res(ctx, Value::undef()); @@ -245,7 +245,7 @@ auto CompletionAndExpr::type() const -> Type return Type::VALUE; } -auto CompletionAndExpr::ieval(Context ctx, const Value& val, const ResultFn& res) -> Result +auto CompletionAndExpr::ieval(Context ctx, const Value& val, const ResultFn& res) -> tl::expected { if (left_) (void)left_->eval(ctx, val, LambdaResultFn([](const Context&, const Value&) { @@ -305,7 +305,7 @@ auto CompletionOrExpr::type() const -> Type return Type::VALUE; } -auto CompletionOrExpr::ieval(Context ctx, const Value& val, const ResultFn& res) -> Result +auto CompletionOrExpr::ieval(Context ctx, const Value& val, const ResultFn& res) -> tl::expected { if (left_) (void)left_->eval(ctx, val, LambdaResultFn([](const Context&, const Value&) { @@ -352,7 +352,7 @@ auto CompletionWordExpr::type() const -> Type return Type::VALUE; } -auto CompletionWordExpr::ieval(Context ctx, const Value& val, const ResultFn& res) -> Result +auto CompletionWordExpr::ieval(Context ctx, const Value& val, const ResultFn& res) -> tl::expected { if (ctx.phase == Context::Phase::Compilation) return res(ctx, Value::undef()); diff --git a/src/completion.h b/src/completion.h index d6842799..e620efb3 100644 --- a/src/completion.h +++ b/src/completion.h @@ -48,7 +48,7 @@ class CompletionFieldOrWordExpr : public Expr CompletionFieldOrWordExpr(std::string prefix, Completion* comp, const Token& token, bool inPath); auto type() const -> Type override; - auto ieval(Context ctx, const Value& value, const ResultFn& result) -> Result override; + auto ieval(Context ctx, const Value& value, const ResultFn& result) -> tl::expected override; auto clone() const -> std::unique_ptr override; auto accept(ExprVisitor& v) -> void override; auto toString() const -> std::string override; @@ -64,7 +64,7 @@ class CompletionAndExpr : public Expr CompletionAndExpr(ExprPtr left, ExprPtr right, const Completion* comp); auto type() const -> Type override; - auto ieval(Context ctx, const Value& val, const ResultFn& res) -> Result override; + auto ieval(Context ctx, const Value& val, const ResultFn& res) -> tl::expected override; void accept(ExprVisitor& v) override; auto clone() const -> ExprPtr override; auto toString() const -> std::string override; @@ -78,7 +78,7 @@ class CompletionOrExpr : public Expr CompletionOrExpr(ExprPtr left, ExprPtr right, const Completion* comp); auto type() const -> Type override; - auto ieval(Context ctx, const Value& val, const ResultFn& res) -> Result override; + auto ieval(Context ctx, const Value& val, const ResultFn& res) -> tl::expected override; void accept(ExprVisitor& v) override; auto clone() const -> ExprPtr override; auto toString() const -> std::string override; @@ -92,7 +92,7 @@ class CompletionWordExpr : public Expr CompletionWordExpr(std::string prefix, Completion* comp, const Token& token); auto type() const -> Type override; - auto ieval(Context ctx, const Value& value, const ResultFn& result) -> Result override; + auto ieval(Context ctx, const Value& value, const ResultFn& result) -> tl::expected override; auto clone() const -> std::unique_ptr override; auto accept(ExprVisitor& v) -> void override; auto toString() const -> std::string override; diff --git a/src/expected.h b/src/expected.h index 2c1d4cc5..7964a7cc 100644 --- a/src/expected.h +++ b/src/expected.h @@ -1,6 +1,6 @@ #pragma once -#include "tl/expected.hpp" +#include // Helper macro for bubbling-up tl::expected errors. #define TRY_EXPECTED(res) \ diff --git a/src/expressions.cpp b/src/expressions.cpp index 42fe857b..24e05805 100644 --- a/src/expressions.cpp +++ b/src/expressions.cpp @@ -32,7 +32,7 @@ struct CountedResultFn : ResultFn CountedResultFn(const CountedResultFn&) = delete; CountedResultFn(CountedResultFn&&) = delete; - auto operator()(Context ctx, Value vv) const -> Result override + auto operator()(Context ctx, Value vv) const -> tl::expected override { assert(!finished); ++calls; @@ -59,7 +59,7 @@ auto boolify(const Value& v) -> bool * Undef if any argument is Undef. */ if (v.isa(ValueType::Undef)) return false; - return UnaryOperatorDispatcher::dispatch(v).as(); + return UnaryOperatorDispatcher::dispatch(v).value_or(Value::f()).as(); } } @@ -71,7 +71,7 @@ auto WildcardExpr::type() const -> Type return Type::PATH; } -auto WildcardExpr::ieval(Context ctx, const Value& val, const ResultFn& ores) -> Result +auto WildcardExpr::ieval(Context ctx, const Value& val, const ResultFn& ores) -> tl::expected { if (ctx.phase == Context::Phase::Compilation) return ores(ctx, Value::undef()); @@ -131,7 +131,7 @@ auto AnyChildExpr::type() const -> Type return Type::PATH; } -auto AnyChildExpr::ieval(Context ctx, const Value& val, const ResultFn& res) -> Result +auto AnyChildExpr::ieval(Context ctx, const Value& val, const ResultFn& res) -> tl::expected { if (ctx.phase == Context::Phase::Compilation) return res(ctx, Value::undef()); @@ -187,7 +187,7 @@ auto FieldExpr::type() const -> Type return Type::PATH; } -auto FieldExpr::ieval(Context ctx, const Value& val, const ResultFn& res) -> Result +auto FieldExpr::ieval(Context ctx, const Value& val, const ResultFn& res) -> tl::expected { if (ctx.phase != Context::Compilation) evaluations_++; @@ -254,7 +254,7 @@ auto MultiConstExpr::constant() const -> bool return true; } -auto MultiConstExpr::ieval(Context ctx, const Value&, const ResultFn& res) -> Result +auto MultiConstExpr::ieval(Context ctx, const Value&, const ResultFn& res) -> tl::expected { for (const auto& v : values_) { if (res(ctx, v) == Result::Stop) @@ -300,7 +300,7 @@ auto ConstExpr::constant() const -> bool return true; } -auto ConstExpr::ieval(Context ctx, const Value&, const ResultFn& res) -> Result +auto ConstExpr::ieval(Context ctx, const Value&, const ResultFn& res) -> tl::expected { return res(ctx, value_); } @@ -332,12 +332,12 @@ auto SubscriptExpr::type() const -> Type return Type::SUBSCRIPT; } -auto SubscriptExpr::ieval(Context ctx, const Value& val, const ResultFn& ores) -> Result +auto SubscriptExpr::ieval(Context ctx, const Value& val, const ResultFn& ores) -> tl::expected { auto res = CountedResultFn(ores, ctx); auto r = left_->eval(ctx, val, LambdaResultFn([this, &val, &res](Context ctx, Value lval) { - return index_->eval(ctx, val, LambdaResultFn([this, &res, &lval](Context ctx, const Value& ival) { + return index_->eval(ctx, val, LambdaResultFn([this, &res, &lval](Context ctx, const Value& ival) -> tl::expected { /* Field subscript */ if (lval.node) { ModelNode::Ptr node; @@ -359,7 +359,10 @@ auto SubscriptExpr::ieval(Context ctx, const Value& val, const ResultFn& ores) - else ctx.env->warn("Invalid subscript index type "s + valueType2String(ival.type), this->toString()); } else { - return res(ctx, BinaryOperatorDispatcher::dispatch(lval, ival)); + auto v = BinaryOperatorDispatcher::dispatch(lval, ival); + if (!v) + return tl::unexpected(std::move(v.error())); + return res(ctx, std::move(v.value())); } return Result::Continue; @@ -399,18 +402,21 @@ auto SubExpr::type() const -> Type return Type::SUBEXPR; } -auto SubExpr::ieval(Context ctx, const Value& val, const ResultFn& ores) -> Result +auto SubExpr::ieval(Context ctx, const Value& val, const ResultFn& ores) -> tl::expected { /* Do not return null unless we have _no_ matching value. */ auto res = CountedResultFn(ores, ctx); - auto r = left_->eval(ctx, val, LambdaResultFn([this, &res](Context ctx, const Value& lv) { - return sub_->eval(ctx, lv, LambdaResultFn([&res, &lv](const Context& ctx, const Value& vv) { + auto r = left_->eval(ctx, val, LambdaResultFn([this, &res](Context ctx, const Value& lv) -> tl::expected { + return sub_->eval(ctx, lv, LambdaResultFn([&res, &lv](const Context& ctx, const Value& vv) -> tl::expected { auto bv = UnaryOperatorDispatcher::dispatch(vv); - if (bv.isa(ValueType::Undef)) + if (!bv) + return tl::unexpected(std::move(bv.error())); + + if (bv->isa(ValueType::Undef)) return Result::Continue; - if (bv.isa(ValueType::Bool) && bv.as()) + if (bv->isa(ValueType::Bool) && bv->template as()) return res(ctx, lv); return Result::Continue; @@ -444,7 +450,7 @@ auto AnyExpr::type() const -> Type return Type::VALUE; } -auto AnyExpr::ieval(Context ctx, const Value& val, const ResultFn& res) -> Result +auto AnyExpr::ieval(Context ctx, const Value& val, const ResultFn& res) -> tl::expected { auto subctx = ctx; auto result = false; /* At least one value is true */ @@ -514,7 +520,7 @@ auto EachExpr::type() const -> Type return Type::VALUE; } -auto EachExpr::ieval(Context ctx, const Value& val, const ResultFn& res) -> Result +auto EachExpr::ieval(Context ctx, const Value& val, const ResultFn& res) -> tl::expected { auto subctx = ctx; auto result = true; /* All values are true */ @@ -584,21 +590,23 @@ auto CallExpression::type() const -> Type return Type::VALUE; } -auto CallExpression::ieval(Context ctx, const Value& val, const ResultFn& res) -> Result +auto CallExpression::ieval(Context ctx, const Value& val, const ResultFn& res) -> tl::expected { if (!fn_) fn_ = ctx.env->findFunction(name_); if (!fn_) - raise("Unknown function "s + name_); + return tl::unexpected(Error::UnknownFunction, fmt::format("Unknown function '{}'", name_)); auto anyval = false; auto result = fn_->eval(ctx, val, args_, LambdaResultFn([&res, &anyval](const Context& ctx, Value vv) { anyval = true; return res(ctx, std::move(vv)); })); - + if (!result) + return result; if (!anyval) - return res(ctx, Value::null()); /* Expressions _must_ return at least one value! */ + return tl::unexpected(Error::InternalError, "Function did not call result callback"); + return result; } @@ -648,11 +656,11 @@ auto PathExpr::type() const -> Type return Type::PATH; } -auto PathExpr::ieval(Context ctx, const Value& val, const ResultFn& ores) -> Result +auto PathExpr::ieval(Context ctx, const Value& val, const ResultFn& ores) -> tl::expected { auto res = CountedResultFn(ores, ctx); - auto r = left_->eval(ctx, val, LambdaResultFn([this, &res](Context ctx, Value v) { + auto r = left_->eval(ctx, val, LambdaResultFn([this, &res](Context ctx, Value v) -> tl::expected { if (v.isa(ValueType::Undef)) return Result::Continue; @@ -661,7 +669,7 @@ auto PathExpr::ieval(Context ctx, const Value& val, const ResultFn& ores) -> Res ++hits_; - return right_->eval(ctx, std::move(v), LambdaResultFn([this, &res](Context ctx, Value vv) { + return right_->eval(ctx, std::move(v), LambdaResultFn([this, &res](Context ctx, Value vv) -> tl::expected { if (vv.isa(ValueType::Undef)) return Result::Continue; @@ -699,7 +707,7 @@ auto UnpackExpr::type() const -> Type return Type::VALUE; } -auto UnpackExpr::ieval(Context ctx, const Value& val, const ResultFn& res) -> Result +auto UnpackExpr::ieval(Context ctx, const Value& val, const ResultFn& res) -> tl::expected { auto anyval = false; auto r = sub_->eval(ctx, val, LambdaResultFn([&res, &anyval](Context ctx, Value v) { @@ -751,19 +759,22 @@ auto UnaryWordOpExpr::type() const -> Type return Type::VALUE; } -auto UnaryWordOpExpr::ieval(Context ctx, const Value& val, const ResultFn& res) -> Result +auto UnaryWordOpExpr::ieval(Context ctx, const Value& val, const ResultFn& res) -> tl::expected { - return left_->eval(ctx, val, LambdaResultFn([this, &res](const Context& ctx, Value val) { + return left_->eval(ctx, val, LambdaResultFn([this, &res](const Context& ctx, Value val) -> tl::expected { if (val.isa(ValueType::Undef)) return res(ctx, std::move(val)); if (val.isa(ValueType::TransientObject)) { const auto& obj = val.as(); - return res(ctx, obj.meta->unaryOp(ident_, obj)); + auto v = obj.meta->unaryOp(ident_, obj); + if (!v) + return tl::unexpected(std::move(v.error())); + return res(ctx, std::move(v.value())); } - raise(fmt::format("Invalid operator '{}' for value of type {}", - ident_, valueType2String(val.type))); + return tl::unexpected(Error::InvalidOperator, + fmt::format("Invalid operator '{}' for value of type {}", ident_, valueType2String(val.type))); })); } @@ -793,25 +804,32 @@ auto BinaryWordOpExpr::type() const -> Type return Type::VALUE; } -auto BinaryWordOpExpr::ieval(Context ctx, const Value& val, const ResultFn& res) -> Result +auto BinaryWordOpExpr::ieval(Context ctx, const Value& val, const ResultFn& res) -> tl::expected { return left_->eval(ctx, val, LambdaResultFn([this, &res, &val](const Context& ctx, const Value& lval) { - return right_->eval(ctx, val, LambdaResultFn([this, &res, &lval](const Context& ctx, const Value& rval) { + return right_->eval(ctx, val, LambdaResultFn([this, &res, &lval](const Context& ctx, const Value& rval) -> tl::expected { if (lval.isa(ValueType::Undef) || rval.isa(ValueType::Undef)) return res(ctx, Value::undef()); if (lval.isa(ValueType::TransientObject)) { const auto& obj = lval.as(); - return res(ctx, obj.meta->binaryOp(ident_, obj, rval)); + auto v = obj.meta->binaryOp(ident_, obj, rval); + if (!v) + return tl::unexpected(std::move(v.error())); + return res(ctx, std::move(v.value())); } if (rval.isa(ValueType::TransientObject)) { const auto& obj = rval.as(); - return res(ctx, obj.meta->binaryOp(ident_, lval, obj)); + auto v = obj.meta->binaryOp(ident_, obj, rval); + if (!v) + return tl::unexpected(std::move(v.error())); + return res(ctx, std::move(v.value())); } - raise(fmt::format("Invalid operator '{}' for values of type {} and {}", - ident_, valueType2String(lval.type), valueType2String(rval.type))); + return tl::unexpected(Error::InvalidOperator, + fmt::format("Invalid operator '{}' for values of type {} and {}", + ident_, valueType2String(lval.type), valueType2String(rval.type))); })); })); } @@ -844,16 +862,20 @@ auto AndExpr::type() const -> Type return Type::VALUE; } -auto AndExpr::ieval(Context ctx, const Value& val, const ResultFn& res) -> Result +auto AndExpr::ieval(Context ctx, const Value& val, const ResultFn& res) -> tl::expected { /* Operator and behaves like in lua: - * 'a and b' returns a if 'not a?' else b is returned */ - return left_->eval(ctx, val, LambdaResultFn([this, &res, &val](const Context& ctx, Value lval) { + * 'a and b' returns a if 'not a?' else b is returned */ + return left_->eval(ctx, val, LambdaResultFn([this, &res, &val](const Context& ctx, Value lval) -> tl::expected { if (lval.isa(ValueType::Undef)) return res(ctx, lval); - if (auto v = UnaryOperatorDispatcher::dispatch(lval); v.isa(ValueType::Bool)) - if (!v.as()) + auto v = UnaryOperatorDispatcher::dispatch(lval); + if (!v) + return tl::unexpected(std::move(v.error())); + + if (v->isa(ValueType::Bool)) + if (!v->template as()) return res(ctx, std::move(lval)); return right_->eval(ctx, val, LambdaResultFn([&res](const Context& ctx, Value rval) { @@ -890,17 +912,22 @@ auto OrExpr::type() const -> Type return Type::VALUE; } -auto OrExpr::ieval(Context ctx, const Value& val, const ResultFn& res) -> Result +auto OrExpr::ieval(Context ctx, const Value& val, const ResultFn& res) -> tl::expected { /* Operator or behaves like in lua: * 'a or b' returns a if 'a?' else b is returned */ - return left_->eval(ctx, val, LambdaResultFn([this, &res, &val](Context ctx, Value lval) { + return left_->eval(ctx, val, LambdaResultFn([this, &res, &val](Context ctx, Value lval) -> tl::expected { if (lval.isa(ValueType::Undef)) return res(ctx, lval); ++leftEvaluations_; - if (auto v = UnaryOperatorDispatcher::dispatch(lval); v.isa(ValueType::Bool)) - if (v.as()) + + auto v = UnaryOperatorDispatcher::dispatch(lval); + if (!v) + return tl::unexpected(std::move(v.error())); + + if (v->isa(ValueType::Bool)) + if (v->template as()) return res(ctx, std::move(lval)); ++rightEvaluations_; diff --git a/src/expressions.h b/src/expressions.h index 6bf22e20..552c9c94 100644 --- a/src/expressions.h +++ b/src/expressions.h @@ -76,7 +76,7 @@ class WildcardExpr : public Expr WildcardExpr(); auto type() const -> Type override; - auto ieval(Context ctx, const Value& val, const ResultFn& ores) -> Result override; + auto ieval(Context ctx, const Value& val, const ResultFn& ores) -> tl::expected override; auto clone() const -> ExprPtr override; void accept(ExprVisitor& v) override; auto toString() const -> std::string override; @@ -91,7 +91,7 @@ class AnyChildExpr : public Expr AnyChildExpr(); auto type() const -> Type override; - auto ieval(Context ctx, const Value& val, const ResultFn& res) -> Result override; + auto ieval(Context ctx, const Value& val, const ResultFn& res) -> tl::expected override; auto clone() const -> ExprPtr override; void accept(ExprVisitor& v) override; auto toString() const -> std::string override; @@ -104,7 +104,7 @@ class FieldExpr : public Expr FieldExpr(std::string name, const Token& token); auto type() const -> Type override; - auto ieval(Context ctx, const Value& val, const ResultFn& res) -> Result override; + auto ieval(Context ctx, const Value& val, const ResultFn& res) -> tl::expected override; auto clone() const -> ExprPtr override; void accept(ExprVisitor& v) override; auto toString() const -> std::string override; @@ -126,7 +126,7 @@ class MultiConstExpr : public Expr auto type() const -> Type override; auto constant() const -> bool override; - auto ieval(Context ctx, const Value&, const ResultFn& res) -> Result override; + auto ieval(Context ctx, const Value&, const ResultFn& res) -> tl::expected override; auto clone() const -> ExprPtr override; void accept(ExprVisitor& v) override; auto toString() const -> std::string override; @@ -146,7 +146,7 @@ class ConstExpr : public Expr auto type() const -> Type override; auto constant() const -> bool override; - auto ieval(Context ctx, const Value&, const ResultFn& res) -> Result override; + auto ieval(Context ctx, const Value&, const ResultFn& res) -> tl::expected override; auto clone() const -> ExprPtr override; void accept(ExprVisitor& v) override; auto toString() const -> std::string override; @@ -161,7 +161,7 @@ class SubscriptExpr : public Expr SubscriptExpr(ExprPtr left, ExprPtr index); auto type() const -> Type override; - auto ieval(Context ctx, const Value& val, const ResultFn& ores) -> Result override; + auto ieval(Context ctx, const Value& val, const ResultFn& ores) -> tl::expected override; auto clone() const -> ExprPtr override; void accept(ExprVisitor& v) override; auto toString() const -> std::string override; @@ -177,7 +177,7 @@ class SubExpr : public Expr SubExpr(ExprPtr left, ExprPtr sub); auto type() const -> Type override; - auto ieval(Context ctx, const Value& val, const ResultFn& ores) -> Result override; + auto ieval(Context ctx, const Value& val, const ResultFn& ores) -> tl::expected override; auto toString() const -> std::string override; auto clone() const -> ExprPtr override; void accept(ExprVisitor& v) override; @@ -191,7 +191,7 @@ class AnyExpr : public Expr explicit AnyExpr(std::vector args); auto type() const -> Type override; - auto ieval(Context ctx, const Value& val, const ResultFn& res) -> Result override; + auto ieval(Context ctx, const Value& val, const ResultFn& res) -> tl::expected override; auto clone() const -> ExprPtr override; void accept(ExprVisitor& v) override; auto toString() const -> std::string override; @@ -209,7 +209,7 @@ class EachExpr : public Expr explicit EachExpr(std::vector args); auto type() const -> Type override; - auto ieval(Context ctx, const Value& val, const ResultFn& res) -> Result override; + auto ieval(Context ctx, const Value& val, const ResultFn& res) -> tl::expected override; auto clone() const -> ExprPtr override; void accept(ExprVisitor& v) override; auto toString() const -> std::string override; @@ -227,7 +227,7 @@ class CallExpression : public Expr CallExpression(std::string name, std::vector args); auto type() const -> Type override; - auto ieval(Context ctx, const Value& val, const ResultFn& res) -> Result override; + auto ieval(Context ctx, const Value& val, const ResultFn& res) -> tl::expected override; auto clone() const -> ExprPtr override; void accept(ExprVisitor& v) override; auto toString() const -> std::string override; @@ -244,7 +244,7 @@ class PathExpr : public Expr PathExpr(ExprPtr left, ExprPtr right); auto type() const -> Type override; - auto ieval(Context ctx, const Value& val, const ResultFn& ores) -> Result override; + auto ieval(Context ctx, const Value& val, const ResultFn& ores) -> tl::expected override; auto clone() const -> ExprPtr override; void accept(ExprVisitor& v) override; auto toString() const -> std::string override; @@ -266,7 +266,7 @@ class UnpackExpr : public Expr explicit UnpackExpr(ExprPtr sub); auto type() const -> Type override; - auto ieval(Context ctx, const Value& val, const ResultFn& res) -> Result override; + auto ieval(Context ctx, const Value& val, const ResultFn& res) -> tl::expected override; auto clone() const -> ExprPtr override; void accept(ExprVisitor& v) override; auto toString() const -> std::string override; @@ -290,10 +290,13 @@ class UnaryExpr : public Expr return Type::VALUE; } - auto ieval(Context ctx, const Value& val, const ResultFn& res) -> Result override + auto ieval(Context ctx, const Value& val, const ResultFn& res) -> tl::expected override { - return sub_->eval(ctx, val, LambdaResultFn([&](Context ctx, Value vv) { - return res(ctx, UnaryOperatorDispatcher::dispatch(std::move(vv))); + return sub_->eval(ctx, val, LambdaResultFn([&](Context ctx, Value vv) -> tl::expected { + auto v = UnaryOperatorDispatcher::dispatch(std::move(vv)); + if (!v) + return tl::unexpected(std::move(v.error())); + return res(ctx, std::move(v.value())); })); } @@ -339,15 +342,17 @@ class BinaryExpr : public Expr return Type::VALUE; } - auto ieval(Context ctx, const Value& val, const ResultFn& res) -> Result override + auto ieval(Context ctx, const Value& val, const ResultFn& res) -> tl::expected override { return left_->eval(ctx, val, LambdaResultFn([this, &res, &val](Context ctx, Value lv) { leftTypes_.set(lv.type); - return right_->eval(ctx, val, LambdaResultFn([this, &res, &lv](Context ctx, Value rv) { + return right_->eval(ctx, val, LambdaResultFn([this, &res, &lv](Context ctx, Value rv) -> tl::expected { rightTypes_.set(rv.type); - return res(ctx, BinaryOperatorDispatcher::dispatch(std::move(lv), - std::move(rv))); + auto v = BinaryOperatorDispatcher::dispatch(std::move(lv), std::move(rv)); + if (!v) + return tl::unexpected(std::move(v.error())); + return res(ctx, std::move(v.value())); })); })); } @@ -415,22 +420,26 @@ class ComparisonExpr : public ComparisonExprBase public: using ComparisonExprBase::ComparisonExprBase; - auto ieval(Context ctx, const Value& val, const ResultFn& res) -> Result override + auto ieval(Context ctx, const Value& val, const ResultFn& res) -> tl::expected override { return left_->eval(ctx, val, LambdaResultFn([this, &res, &val](Context ctx, Value lv) { leftTypes_.set(lv.type); - return right_->eval(ctx, val, LambdaResultFn([this, &res, &lv](Context ctx, Value rv) { + return right_->eval(ctx, val, LambdaResultFn([this, &res, &lv](Context ctx, Value rv) -> tl::expected { rightTypes_.set(rv.type); auto operatorResult = BinaryOperatorDispatcher::dispatch(std::move(lv), std::move(rv)); - if (operatorResult.isa(ValueType::Bool)) { - if (operatorResult.template as()) + if (!operatorResult) + return tl::unexpected(std::move(operatorResult.error())); + + if (operatorResult->isa(ValueType::Bool)) { + if (operatorResult->template as()) ++trueResults_; else ++falseResults_; } - return res(ctx, operatorResult); + return res(ctx, std::move(operatorResult.value()) +); })); })); } @@ -495,7 +504,7 @@ class UnaryWordOpExpr : public Expr UnaryWordOpExpr(std::string ident, ExprPtr left); auto type() const -> Type override; - auto ieval(Context ctx, const Value& val, const ResultFn& res) -> Result override; + auto ieval(Context ctx, const Value& val, const ResultFn& res) -> tl::expected override; void accept(ExprVisitor& v) override; auto clone() const -> ExprPtr override; auto toString() const -> std::string override; @@ -510,7 +519,7 @@ class BinaryWordOpExpr : public Expr BinaryWordOpExpr(std::string ident, ExprPtr left, ExprPtr right); auto type() const -> Type override; - auto ieval(Context ctx, const Value& val, const ResultFn& res) -> Result override; + auto ieval(Context ctx, const Value& val, const ResultFn& res) -> tl::expected override; void accept(ExprVisitor& v) override; auto clone() const -> ExprPtr override; auto toString() const -> std::string override; @@ -525,7 +534,7 @@ class AndExpr : public Expr AndExpr(ExprPtr left, ExprPtr right); auto type() const -> Type override; - auto ieval(Context ctx, const Value& val, const ResultFn& res) -> Result override; + auto ieval(Context ctx, const Value& val, const ResultFn& res) -> tl::expected override; void accept(ExprVisitor& v) override; auto clone() const -> ExprPtr override; auto toString() const -> std::string override; @@ -539,7 +548,7 @@ class OrExpr : public Expr OrExpr(ExprPtr left, ExprPtr right); auto type() const -> Type override; - auto ieval(Context ctx, const Value& val, const ResultFn& res) -> Result override; + auto ieval(Context ctx, const Value& val, const ResultFn& res) -> tl::expected override; void accept(ExprVisitor& v) override; auto clone() const -> ExprPtr override; auto toString() const -> std::string override; diff --git a/src/function.cpp b/src/function.cpp index 84dc938c..4c84a347 100644 --- a/src/function.cpp +++ b/src/function.cpp @@ -135,7 +135,7 @@ auto boolify(const Value& v) -> bool * Undef if any argument is Undef. */ if (v.isa(ValueType::Undef)) return false; - return UnaryOperatorDispatcher::dispatch(v).as(); + return UnaryOperatorDispatcher::dispatch(v).value_or(Value::f()).as(); } } @@ -152,7 +152,7 @@ auto CountFn::ident() const -> const FnInfo& return info; } -auto CountFn::eval(Context ctx, Value val, const std::vector& args, const ResultFn& res) const -> Result +auto CountFn::eval(Context ctx, Value val, const std::vector& args, const ResultFn& res) const -> tl::expected { if (args.empty()) raise("count(...) expects one argument; got "s + std::to_string(args.size())); @@ -195,7 +195,7 @@ auto TraceFn::ident() const -> const FnInfo& return info; } -auto TraceFn::eval(Context ctx, Value val, const std::vector& args, const ResultFn& res) const -> Result +auto TraceFn::eval(Context ctx, Value val, const std::vector& args, const ResultFn& res) const -> tl::expected { Value name = Value::undef(); Value limit = Value::undef(); @@ -255,7 +255,7 @@ auto RangeFn::ident() const -> const FnInfo& return info; } -auto RangeFn::eval(Context ctx, Value val, const std::vector& args, const ResultFn& res) const -> Result +auto RangeFn::eval(Context ctx, Value val, const std::vector& args, const ResultFn& res) const -> tl::expected { if (args.size() != 2) raise("range(begin, end) expects 2 arguments; got "s + std::to_string(args.size())); @@ -289,7 +289,7 @@ auto ReFn::ident() const -> const FnInfo& return info; } -auto ReFn::eval(Context ctx, Value val, const std::vector& args, const ResultFn& res) const -> Result +auto ReFn::eval(Context ctx, Value val, const std::vector& args, const ResultFn& res) const -> tl::expected { if (args.size() != 1) raise("re(expr) expects 1 arguments; got "s + std::to_string(args.size())); @@ -325,7 +325,7 @@ auto ArrFn::ident() const -> const FnInfo& } -auto ArrFn::eval(Context ctx, Value val, const std::vector& args, const ResultFn& res) const -> Result +auto ArrFn::eval(Context ctx, Value val, const std::vector& args, const ResultFn& res) const -> tl::expected { if (args.empty()) return res(ctx, Value::null()); @@ -398,7 +398,7 @@ ContainerType split(std::string_view what, } } -auto SplitFn::eval(Context ctx, Value val, const std::vector& args, const ResultFn& res) const -> Result +auto SplitFn::eval(Context ctx, Value val, const std::vector& args, const ResultFn& res) const -> tl::expected { Value str = Value::undef(); Value sep = Value::undef(); @@ -436,7 +436,7 @@ auto SelectFn::ident() const -> const FnInfo& return info; } -auto SelectFn::eval(Context ctx, Value val, const std::vector& args, const ResultFn& res) const -> Result +auto SelectFn::eval(Context ctx, Value val, const std::vector& args, const ResultFn& res) const -> tl::expected { Value idx = Value::undef(); Value cnt = Value::undef(); @@ -455,7 +455,7 @@ auto SelectFn::eval(Context ctx, Value val, const std::vector& args, co if (icnt <= 0) icnt = std::numeric_limits::max(); - auto result = args[0]->eval(ctx, val, LambdaResultFn([&, n = -1](Context ctx, Value vv) mutable { + auto result = args[0]->eval(ctx, val, LambdaResultFn([&, n = -1](Context ctx, Value vv) mutable -> tl::expected { ++n; if (ctx.phase == Context::Phase::Compilation) if (vv.isa(ValueType::Undef)) @@ -483,7 +483,7 @@ auto SumFn::ident() const -> const FnInfo& return info; } -auto SumFn::eval(Context ctx, Value val, const std::vector& args, const ResultFn& res) const -> Result +auto SumFn::eval(Context ctx, Value val, const std::vector& args, const ResultFn& res) const -> tl::expected { if (args.empty() || args.size() > 3) raise("sum: Expected at least 1 argument; got "s + std::to_string(args.size())); @@ -498,7 +498,7 @@ auto SumFn::eval(Context ctx, Value val, const std::vector& args, const return Result::Continue; })); - (void)args[0]->eval(ctx, val, LambdaResultFn([&, n = 0](Context ctx, Value vv) mutable { + (void)args[0]->eval(ctx, val, LambdaResultFn([&, n = 0](Context ctx, Value vv) mutable -> tl::expected { if (subexpr) { auto ov = model_ptr::make(vv); ov->set(StringPool::OverlaySum, sum); @@ -515,7 +515,11 @@ auto SumFn::eval(Context ctx, Value val, const std::vector& args, const if (sum.isa(ValueType::Null)) { sum = std::move(vv); } else { - sum = BinaryOperatorDispatcher::dispatch(sum, vv); + if (auto newSum = BinaryOperatorDispatcher::dispatch(sum, vv)) { + sum = std::move(newSum.value()); + } else { + return tl::unexpected(std::move(newSum.error())); + } } } @@ -538,12 +542,12 @@ auto KeysFn::ident() const -> const FnInfo& return info; } -auto KeysFn::eval(Context ctx, Value val, const std::vector& args, const ResultFn& res) const -> Result +auto KeysFn::eval(Context ctx, Value val, const std::vector& args, const ResultFn& res) const -> tl::expected { if (args.size() != 1) raise("keys: Expected 1 argument; got "s + std::to_string(args.size())); - auto result = args[0]->eval(ctx, val, LambdaResultFn([&res](Context ctx, Value vv) { + auto result = args[0]->eval(ctx, val, LambdaResultFn([&res](Context ctx, Value vv) -> tl::expected { if (ctx.phase == Context::Phase::Compilation) if (vv.isa(ValueType::Undef)) return res(ctx, std::move(vv)); diff --git a/src/parser.cpp b/src/parser.cpp index b8cd8c36..e03361c3 100644 --- a/src/parser.cpp +++ b/src/parser.cpp @@ -24,7 +24,7 @@ class NOOPExpr : public Expr return Type::FIELD; } - auto ieval(Context ctx, const Value& val, const ResultFn& ores) -> Result override + auto ieval(Context ctx, const Value& val, const ResultFn& ores) -> tl::expected override { return Result::Stop; } diff --git a/src/simfil.cpp b/src/simfil.cpp index 481eb605..bc435ded 100644 --- a/src/simfil.cpp +++ b/src/simfil.cpp @@ -873,17 +873,20 @@ auto eval(Environment& env, const AST& ast, const ModelNode& node, Diagnostics* auto mutableAST = ast.expr().clone(); - std::vector res; - mutableAST->eval(ctx, Value::field(node), LambdaResultFn([&res](Context, Value value) { - res.push_back(std::move(value)); + std::vector values; + auto res = mutableAST->eval(ctx, Value::field(node), LambdaResultFn([&values](Context, Value value) { + values.push_back(std::move(value)); return Result::Continue; })); + if (!res) + return tl::unexpected(std::move(res.error())); + if (diag) { diag->collect(*mutableAST); } - return res; + return values; } auto diagnostics(Environment& env, const AST& ast, const Diagnostics& diag) -> expected, Error> diff --git a/src/types.cpp b/src/types.cpp index 696541a1..24d41cf1 100644 --- a/src/types.cpp +++ b/src/types.cpp @@ -2,7 +2,7 @@ #include "simfil/model/nodes.h" #include "simfil/operator.h" -#include "fmt/core.h" +#include "fmt/format.h" namespace simfil { @@ -22,7 +22,7 @@ auto IRangeType::make(int64_t a, int64_t b) -> Value return {ValueType::TransientObject, std::move(obj)}; } -auto IRangeType::unaryOp(std::string_view op, const IRange& self) const -> Value +auto IRangeType::unaryOp(std::string_view op, const IRange& self) const -> tl::expected { if (op == OperatorTypeof::name()) return Value::make(ident); @@ -36,16 +36,16 @@ auto IRangeType::unaryOp(std::string_view op, const IRange& self) const -> Value if (op == OperatorLen::name()) return Value::make(static_cast(self.high() - self.low())); - raise(op); + return tl::unexpected(Error::InvalidOperands, fmt::format("Invalid operands for operator '{}'", op)); } -auto IRangeType::binaryOp(std::string_view op, const IRange& l, const Value& r) const -> Value +auto IRangeType::binaryOp(std::string_view op, const IRange& l, const Value& r) const -> tl::expected { /* Range ==/!= operator checks if the other operand is _in_ the range (for int/float) */ if (op == OperatorNeq::name()) { auto res = binaryOp(OperatorEq::name(), l, r); - if (res.isa(ValueType::Bool)) - return Value::make(!res.as()); + if (res->isa(ValueType::Bool)) + return Value::make(!res->template as()); assert(0); } @@ -62,18 +62,18 @@ auto IRangeType::binaryOp(std::string_view op, const IRange& l, const Value& r) return Value::f(); } - raise(op); + return tl::unexpected(Error::InvalidOperands, fmt::format("Invalid operands for operator '{}'", op)); } -auto IRangeType::binaryOp(std::string_view op, const Value& l, const IRange& r) const -> Value +auto IRangeType::binaryOp(std::string_view op, const Value& l, const IRange& r) const -> tl::expected { if (op == OperatorEq::name() || op == OperatorNeq::name()) return binaryOp(op, r, l); - raise(op); + return tl::unexpected(Error::InvalidOperands, fmt::format("Invalid operands for operator '{}'", op)); } -auto IRangeType::unpack(const IRange& self, std::function res) const -> void +auto IRangeType::unpack(const IRange& self, std::function res) const -> tl::expected { auto begin = self.begin; auto end = self.end; @@ -85,8 +85,9 @@ auto IRangeType::unpack(const IRange& self, std::function res) cons do { i += step; if (!res(Value(ValueType::Int, static_cast(i)))) - return; + return {}; } while (i != end); + return {}; } ReType ReType::Type; @@ -104,7 +105,7 @@ auto ReType::make(std::string_view expr) -> Value return Value(ValueType::TransientObject, std::move(obj)); } -auto ReType::unaryOp(std::string_view op, const Re& self) const -> Value +auto ReType::unaryOp(std::string_view op, const Re& self) const -> tl::expected { if (op == OperatorTypeof::name()) return Value::make(ident); @@ -115,15 +116,15 @@ auto ReType::unaryOp(std::string_view op, const Re& self) const -> Value if (op == OperatorAsString::name()) return Value::make(self.str); - raise(op); + return tl::unexpected(Error::InvalidOperands, fmt::format("Invalid operands for operator '{}'", op)); } -auto ReType::binaryOp(std::string_view op, const Re& l, const Value& r) const -> Value +auto ReType::binaryOp(std::string_view op, const Re& l, const Value& r) const -> tl::expected { return binaryOp(op, r, l); } -auto ReType::binaryOp(std::string_view op, const Value& l, const Re& r) const -> Value +auto ReType::binaryOp(std::string_view op, const Value& l, const Re& r) const -> tl::expected { if (l.isa(ValueType::Null)) return Value::null(); @@ -144,7 +145,7 @@ auto ReType::binaryOp(std::string_view op, const Value& l, const Re& r) const -> if (op == OperatorNeq::name()) return Value::t(); - raise(op); + return tl::unexpected(Error::InvalidOperands, fmt::format("Invalid operands for operator '{}'", op)); } } diff --git a/test/common.cpp b/test/common.cpp index 092db1ca..3aab9827 100644 --- a/test/common.cpp +++ b/test/common.cpp @@ -33,8 +33,10 @@ auto JoinedResult(std::string_view query, std::optional json) -> st INFO("AST: " << (*ast)->expr().toString()); auto res = eval(env, **ast, *model->root(0), nullptr); - if (!res) - INFO(res.error().message); + if (!res) { + INFO("ERROR: " << res.error().message); + return fmt::format("ERROR: {}", res.error().message); + } REQUIRE(res); std::string vals; diff --git a/test/common.hpp b/test/common.hpp index d2c6ac80..585b52c1 100644 --- a/test/common.hpp +++ b/test/common.hpp @@ -63,17 +63,17 @@ class PanicFn : public simfil::Function { static const FnInfo info{ "panic", - "Thrown an exception", + "Thrown an error", "panic()" }; return info; } - auto eval(Context ctx, Value, const std::vector&, const ResultFn& res) const -> Result override + auto eval(Context ctx, Value, const std::vector&, const ResultFn& res) const -> tl::expected override { if (ctx.phase != Context::Phase::Compilation) - throw std::runtime_error("Panic!"); + return tl::unexpected(Error::RuntimeError, "Panic!"); return res(ctx, Value::undef()); } diff --git a/test/simfil.cpp b/test/simfil.cpp index da30e481..957e811c 100644 --- a/test/simfil.cpp +++ b/test/simfil.cpp @@ -281,6 +281,10 @@ TEST_CASE("UtilityFns", "[ast.functions]") { REQUIRE_AST("Trace(1)", "(Trace 1)"); } +TEST_CASE("PanicFunction", "[eval.panic-function]") { + REQUIRE_RESULT("panic()", "ERROR: Panic!"); +} + TEST_CASE("OperatorOrShortCircuit", "[eval.operator-or-short-circuit]") { REQUIRE_RESULT("true or panic()", "true"); } From d269973119c95489a5125424afa8535550087bf4 Mon Sep 17 00:00:00 2001 From: Johannes Wolf Date: Mon, 15 Sep 2025 00:23:47 +0200 Subject: [PATCH 02/50] tree: Make division by zero exception free --- include/simfil/error.h | 1 + include/simfil/operator.h | 42 +++++++++++++++++++++++++-------------- src/simfil.cpp | 7 +++---- test/common.cpp | 8 ++++---- test/complex.cpp | 2 +- test/simfil.cpp | 9 +++++---- 6 files changed, 41 insertions(+), 28 deletions(-) diff --git a/include/simfil/error.h b/include/simfil/error.h index 0aa33355..b712429c 100644 --- a/include/simfil/error.h +++ b/include/simfil/error.h @@ -21,6 +21,7 @@ struct Error IOError, // Evaluation errors + DivisionByZero, UnknownFunction, RuntimeError, InternalError, diff --git a/include/simfil/operator.h b/include/simfil/operator.h index faebdbd2..0e9b2477 100644 --- a/include/simfil/operator.h +++ b/include/simfil/operator.h @@ -11,6 +11,7 @@ #include #include #include +#include namespace simfil { @@ -419,31 +420,31 @@ struct OperatorDiv DENY_OTHER() NULL_AS_NULL() - auto operator()(int64_t l, int64_t r) const -> int64_t + auto operator()(int64_t l, int64_t r) const -> tl::expected { if (r == 0) - raise("Division by zero"); + return tl::unexpected(Error::DivisionByZero, "Division by zero"); return static_cast(l / r); } - auto operator()(int64_t l, double r) const -> double + auto operator()(int64_t l, double r) const -> tl::expected { if (r == 0) - raise("Division by zero"); + return tl::unexpected(Error::DivisionByZero, "Division by zero"); return static_cast(l / r); } - auto operator()(double l, int64_t r) const -> double + auto operator()(double l, int64_t r) const -> tl::expected { if (r == 0) - raise("Division by zero"); + return tl::unexpected(Error::DivisionByZero, "Division by zero"); return static_cast(l / r); } - auto operator()(double l, double r) const -> double + auto operator()(double l, double r) const -> tl::expected { if (r == 0) - raise("Division by zero"); + return tl::unexpected(Error::DivisionByZero, "Division by zero"); return static_cast(l / r); } }; @@ -454,10 +455,10 @@ struct OperatorMod DENY_OTHER() NULL_AS_NULL() - auto operator()(int64_t l, int64_t r) const -> int64_t + auto operator()(int64_t l, int64_t r) const -> tl::expected { if (r == 0) - raise("Division by zero"); + return tl::unexpected(Error::DivisionByZero, "Division by zero"); return static_cast(l % r); } }; @@ -628,6 +629,14 @@ inline auto makeOperatorResult(Value value) -> tl::expected return value; } +template +inline auto makeOperatorResult(tl::expected<_CType, Error> value) -> tl::expected +{ + if (!value) + return tl::unexpected(std::move(value.error())); + return Value::make(std::forward<_CType>(std::move(value.value()))); +} + template inline auto makeOperatorResult(InvalidOperands) -> tl::expected { @@ -710,12 +719,15 @@ struct BinaryOperatorDispatcher } })(); - // Try to find the operand types if (!result) { - auto ltype = UnaryOperatorDispatcher::dispatch(lhs).value_or(Value::strref("unknown")).toString(); - auto rtype = UnaryOperatorDispatcher::dispatch(rhs).value_or(Value::strref("unknown")).toString(); - - return tl::unexpected(Error::InvalidOperands, fmt::format("Invalid operands {} and {} for operator {}", ltype, rtype, Operator::name())); + // Try to find the operand types + auto& error = result.error(); + if (error.type == Error::InvalidOperands) { + auto ltype = UnaryOperatorDispatcher::dispatch(lhs).value_or(Value::strref("unknown")).toString(); + auto rtype = UnaryOperatorDispatcher::dispatch(rhs).value_or(Value::strref("unknown")).toString(); + return tl::unexpected(Error::InvalidOperands, + fmt::format("Invalid operands {} and {} for operator {}", ltype, rtype, Operator::name())); + } } return result; diff --git a/src/simfil.cpp b/src/simfil.cpp index bc435ded..de22f7cc 100644 --- a/src/simfil.cpp +++ b/src/simfil.cpp @@ -157,7 +157,7 @@ static auto simplifyOrForward(Environment* env, expected expr) - std::deque values; auto stub = Context(env, Context::Phase::Compilation); - (void)(*expr)->eval(stub, Value::undef(), LambdaResultFn([&, n = 0](Context ctx, Value vv) mutable { + auto res = (*expr)->eval(stub, Value::undef(), LambdaResultFn([&, n = 0](Context ctx, Value vv) mutable { n += 1; if ((n <= MultiConstExpr::Limit) && (!vv.isa(ValueType::Undef) || vv.node)) { values.push_back(std::move(vv)); @@ -167,6 +167,7 @@ static auto simplifyOrForward(Environment* env, expected expr) - values.clear(); return Result::Stop; })); + TRY_EXPECTED(res); /* Warn about constant results */ if (!values.empty() && std::ranges::all_of(values.begin(), values.end(), [](const Value& v) { @@ -878,9 +879,7 @@ auto eval(Environment& env, const AST& ast, const ModelNode& node, Diagnostics* values.push_back(std::move(value)); return Result::Continue; })); - - if (!res) - return tl::unexpected(std::move(res.error())); + TRY_EXPECTED(res); if (diag) { diag->collect(*mutableAST); diff --git a/test/common.cpp b/test/common.cpp index 3aab9827..e1fed529 100644 --- a/test/common.cpp +++ b/test/common.cpp @@ -26,9 +26,10 @@ auto JoinedResult(std::string_view query, std::optional json) -> st env.functions["panic"] = &panicFn; auto ast = compile(env, query, false); - if (!ast) - INFO(ast.error().message); - REQUIRE(ast.has_value()); + if (!ast) { + INFO("ERROR: " << ast.error().message); + return fmt::format("ERROR: {}", ast.error().message); + } INFO("AST: " << (*ast)->expr().toString()); @@ -37,7 +38,6 @@ auto JoinedResult(std::string_view query, std::optional json) -> st INFO("ERROR: " << res.error().message); return fmt::format("ERROR: {}", res.error().message); } - REQUIRE(res); std::string vals; for (const auto& vv : *res) { diff --git a/test/complex.cpp b/test/complex.cpp index 0362cdd3..57f78e30 100644 --- a/test/complex.cpp +++ b/test/complex.cpp @@ -76,7 +76,7 @@ TEST_CASE("Regular Expression", "[complex.regexp]") { } TEST_CASE("Runtime Error", "[complex.runtime-error]") { - REQUIRE_THROWS(JoinedResult("1 / (nonexisting as int)", invoice)); /* Division by zero */ + REQUIRE_RESULT("1 / (nonexisting as int)", "ERROR: Division by zero"); } TEST_CASE("Multimap JSON", "[multimap.serialization]") { diff --git a/test/simfil.cpp b/test/simfil.cpp index 957e811c..a596c69b 100644 --- a/test/simfil.cpp +++ b/test/simfil.cpp @@ -61,18 +61,19 @@ TEST_CASE("OperatorConst", "[ast.operator]") { REQUIRE_AST("1/null", "null"); REQUIRE_AST("null/1", "null"); - auto GetError = [&](std::string_view query) { + auto GetError = [&](std::string_view query) -> std::string { Environment env(Environment::WithNewStringCache); auto ast = compile(env, query); REQUIRE(!ast); if (!ast) - throw ast.error(); + return ast.error().message; + return "Ok"; }; /* Division by zero */ - CHECK_THROWS(GetError("1/0")); - CHECK_THROWS(GetError("1%0")); + REQUIRE(GetError("1/0") == "Division by zero"); + REQUIRE(GetError("1%0") == "Division by zero"); /* String */ REQUIRE_AST("'a'+null", "\"anull\""); From 4af05473a5462c211be2e29640f9ada1bedc9df7 Mon Sep 17 00:00:00 2001 From: Johannes Wolf Date: Tue, 23 Sep 2025 00:58:16 +0200 Subject: [PATCH 03/50] model: Remove some model exceptions --- examples/minimal/main.cpp | 8 +- include/simfil/error.h | 11 +++ include/simfil/model/arena.h | 23 +++-- include/simfil/model/bitsery-traits.h | 10 ++- include/simfil/model/json.h | 9 +- include/simfil/model/model.h | 17 ++-- include/simfil/model/nodes.h | 25 +++--- include/simfil/model/nodes.impl.h | 22 +++-- include/simfil/model/string-pool.h | 11 ++- include/simfil/overlay.h | 2 +- include/simfil/sourcelocation.h | 2 + include/simfil/value.h | 1 + repl/repl.cpp | 19 +++- src/expressions.cpp | 2 +- src/function.cpp | 121 ++++++++++++++------------ src/model/json.cpp | 51 +++++++---- src/model/model.cpp | 120 ++++++++++++++----------- src/model/nodes.cpp | 73 ++++++++++------ src/model/string-pool.cpp | 20 +++-- src/overlay.cpp | 3 +- test/arena.cpp | 3 +- test/common.cpp | 18 ++-- test/complex.cpp | 27 +++--- test/diagnostics.cpp | 8 +- test/performance.cpp | 4 +- test/simfil.cpp | 62 ++++++++----- 26 files changed, 422 insertions(+), 250 deletions(-) diff --git a/examples/minimal/main.cpp b/examples/minimal/main.cpp index 23da1331..75d4c464 100644 --- a/examples/minimal/main.cpp +++ b/examples/minimal/main.cpp @@ -32,7 +32,13 @@ int main() } // Evalualte query and get result of type simfil::Value. - auto result = simfil::eval(env, **ast, *model->root(0), nullptr); + auto root = model->root(0); + if (!root) { + std::cerr << root.error().message << '\n'; + return -1; + } + + auto result = simfil::eval(env, **ast, **root, nullptr); if (!result) { std::cerr << result.error().message << '\n'; return -1; diff --git a/include/simfil/error.h b/include/simfil/error.h index b712429c..46b1070d 100644 --- a/include/simfil/error.h +++ b/include/simfil/error.h @@ -27,6 +27,15 @@ struct Error InternalError, InvalidOperator, InvalidOperands, + InvalidArguments, + ExpectedSingleValue, + TypeMissmatch, + + // Model related runtime errors + StringPoolOverflow, + EncodeDecodeError, + FieldNotFound, + IndexOutOfRange, Unimplemented, }; @@ -36,6 +45,8 @@ struct Error Error(Type type, std::string message, SourceLocation location); Error(Type type, std::string message, const Token& token); + auto operator==(const Error& o) const -> bool = default; + Type type; SourceLocation location; std::string message; diff --git a/include/simfil/model/arena.h b/include/simfil/model/arena.h index a9994fbd..9a18d70f 100644 --- a/include/simfil/model/arena.h +++ b/include/simfil/model/arena.h @@ -2,13 +2,16 @@ #pragma once +#include #include #include #include #include +#include #include -#include "../exception-handler.h" +#include "simfil/exception-handler.h" +#include "simfil/error.h" // Define this to enable array arena read-write locking. // #define ARRAY_ARENA_THREAD_SAFE @@ -101,11 +104,13 @@ class ArrayArena * @return A reference to the element at the specified index. * @throws std::out_of_range if the index is out of the array bounds. */ - ElementType_& at(ArrayIndex const& a, size_t const& i) { - return at_impl(*this, a, i); + tl::expected, Error> + at(ArrayIndex const& a, size_t const& i) { + return at_impl(*this, a, i); } - ElementType_ const& at(ArrayIndex const& a, size_t const& i) const { - return at_impl(*this, a, i); + tl::expected, Error> + at(ArrayIndex const& a, size_t const& i) const { + return at_impl(*this, a, i); } /** @@ -207,7 +212,8 @@ class ArrayArena : arena_(arena), array_index_(array_index), elem_index_(elem_index) {} ElementRef operator*() { - return arena_.at(array_index_, elem_index_); + // Unchecked access! + return *arena_.at(array_index_, elem_index_); } ArrayIterator& operator++() { @@ -385,7 +391,8 @@ class ArrayArena } template - static ElementTypeRef at_impl(Self& self, ArrayIndex const& a, size_t const& i) + static tl::expected, Error> + at_impl(Self& self, ArrayIndex const& a, size_t const& i) { #ifdef ARRAY_ARENA_THREAD_SAFE std::shared_lock guard(self.lock_); @@ -396,7 +403,7 @@ class ArrayArena if (remaining < current->capacity && remaining < current->size) return self.data_[current->offset + remaining]; if (current->next == InvalidArrayIndex) - raise("Index out of range"); + return tl::unexpected(Error::IndexOutOfRange, "index out of range"); remaining -= current->capacity; current = &self.continuations_[current->next]; } diff --git a/include/simfil/model/bitsery-traits.h b/include/simfil/model/bitsery-traits.h index 15fe8f71..7c306d41 100644 --- a/include/simfil/model/bitsery-traits.h +++ b/include/simfil/model/bitsery-traits.h @@ -1,11 +1,13 @@ #pragma once +#include #include #include #include #include #include +#include "bitsery/details/adapter_common.h" #include "nodes.h" #include "arena.h" @@ -58,8 +60,12 @@ struct ArrayArenaExt for (simfil::ArrayIndex i = 0; i < numArrays; ++i) { auto size = arena.size(i); s.value4b(size); - for (size_t j = 0; j < size; ++j) - fnc(s, const_cast(arena.at(i, j))); + for (size_t j = 0; j < size; ++j) { + if (auto value = arena.at(i, j)) + fnc(s, const_cast(value->get())); + else + raise(std::move(value.error())); // Bitsery does not support propagating errors + } } } diff --git a/include/simfil/model/json.h b/include/simfil/model/json.h index 0a8af63d..8e3ee5a2 100644 --- a/include/simfil/model/json.h +++ b/include/simfil/model/json.h @@ -7,12 +7,15 @@ #endif #include "model.h" +#include "simfil/error.h" + +#include namespace simfil::json { -void parse(std::istream& input, ModelPoolPtr const& model); -void parse(const std::string& input, ModelPoolPtr const& model); -ModelPoolPtr parse(const std::string& input); +auto parse(std::istream& input, ModelPoolPtr const& model) -> tl::expected; +auto parse(const std::string& input, ModelPoolPtr const& model) -> tl::expected; +auto parse(const std::string& input) -> tl::expected; } diff --git a/include/simfil/model/model.h b/include/simfil/model/model.h index 39eeab7a..951bbb8b 100644 --- a/include/simfil/model/model.h +++ b/include/simfil/model/model.h @@ -2,6 +2,7 @@ #pragma once #include "simfil/model/string-pool.h" +#include "tl/expected.hpp" #if defined(SIMFIL_WITH_MODEL_JSON) # include "nlohmann/json.hpp" #endif @@ -55,7 +56,7 @@ class Model : public std::enable_shared_from_this * Get a callback with the actual class of the given node. * This facilitates the Virtual Function Table role of the Model. */ - virtual void resolve(ModelNode const& n, ResolveFn const& cb) const; + virtual tl::expected resolve(ModelNode const& n, ResolveFn const& cb) const; /** Add a small scalar value and get its model node view */ ModelNode::Ptr newSmallValue(bool value); @@ -116,19 +117,19 @@ class ModelPool : public Model * Get a callback with the actual class of the given node. * This facilitates the Virtual Function Table role of the ModelPool. */ - void resolve(ModelNode const& n, ResolveFn const& cb) const override; + tl::expected resolve(ModelNode const& n, ResolveFn const& cb) const override; /** Clear all columns and roots */ virtual void clear(); - /** Check for errors, throw if there are any */ - void validate() const; + /** Check for errors and returns them */ + tl::expected validate() const; /** Get number of root nodes */ [[nodiscard]] size_t numRoots() const; /** Get specific root node */ - [[nodiscard]] ModelNode::Ptr root(size_t const& i) const; + [[nodiscard]] tl::expected root(size_t const& i) const; /** Designate a model node index as a root */ void addRoot(ModelNode::Ptr const& rootNode); @@ -166,13 +167,13 @@ class ModelPool : public Model * Note: This will potentially create new field entries in the newDict, * for field names which were not there before. */ - virtual void setStrings(std::shared_ptr const& strings); + virtual auto setStrings(std::shared_ptr const& strings) -> tl::expected; std::optional lookupStringId(StringId id) const override; /** Serialization */ - virtual void write(std::ostream& outputStream); - virtual void read(std::istream& inputStream); + virtual tl::expected write(std::ostream& outputStream); + virtual tl::expected read(std::istream& inputStream); #if defined(SIMFIL_WITH_MODEL_JSON) /** JSON Serialization */ diff --git a/include/simfil/model/nodes.h b/include/simfil/model/nodes.h index 72e34078..0516af64 100644 --- a/include/simfil/model/nodes.h +++ b/include/simfil/model/nodes.h @@ -5,7 +5,6 @@ #include #include #include -#include #include "arena.h" #include "string-pool.h" @@ -500,7 +499,7 @@ struct Array : public BaseArray /** * Append all elements from `other` to this array. */ - Array& extend(model_ptr const& other); + tl::expected extend(model_ptr const& other); protected: Array() = default; @@ -518,7 +517,8 @@ struct BaseObject : public MandatoryDerivedModelNodeBase template requires std::derived_from - BaseObject& addField(std::string_view const& name, model_ptr const& value) { + tl::expected>, Error> + addField(std::string_view const& name, model_ptr const& value) { return addFieldInternal(name, static_cast(value)); } @@ -556,7 +556,8 @@ struct BaseObject : public MandatoryDerivedModelNodeBase BaseObject(ModelConstPtr pool, ModelNodeAddress); BaseObject(ArrayIndex members, ModelConstPtr pool, ModelNodeAddress); - BaseObject& addFieldInternal(std::string_view const& name, ModelNode::Ptr const& value={}); + tl::expected>, Error> + addFieldInternal(std::string_view const& name, ModelNode::Ptr const& value={}); Storage* storage_ = nullptr; ArrayIndex members_ = 0; @@ -573,19 +574,19 @@ struct Object : public BaseObject using BaseObject::get; using BaseObject::addField; - Object& addBool(std::string_view const& name, bool value); - Object& addField(std::string_view const& name, uint16_t value); - Object& addField(std::string_view const& name, int16_t value); - Object& addField(std::string_view const& name, int64_t const& value); - Object& addField(std::string_view const& name, double const& value); - Object& addField(std::string_view const& name, std::string_view const& value); + tl::expected addBool(std::string_view const& name, bool value); + tl::expected addField(std::string_view const& name, uint16_t value); + tl::expected addField(std::string_view const& name, int16_t value); + tl::expected addField(std::string_view const& name, int64_t const& value); + tl::expected addField(std::string_view const& name, double const& value); + tl::expected addField(std::string_view const& name, std::string_view const& value); - [[nodiscard]] ModelNode::Ptr get(std::string_view const& fieldName) const; + [[nodiscard]] tl::expected get(std::string_view const& fieldName) const; /** * Adopt all fields from the `other` object into this one. */ - Object& extend(model_ptr const& other); + tl::expected extend(model_ptr const& other); protected: Object() = default; diff --git a/include/simfil/model/nodes.impl.h b/include/simfil/model/nodes.impl.h index 775e2dce..9b302760 100644 --- a/include/simfil/model/nodes.impl.h +++ b/include/simfil/model/nodes.impl.h @@ -1,5 +1,7 @@ #pragma once #include "nodes.h" +#include "tl/expected.hpp" +#include namespace simfil { @@ -24,7 +26,9 @@ ModelNode::Ptr BaseArray::at(int64_t i) const { if (i < 0 || i >= (int64_t)storage_->size(members_)) return {}; - return ModelNode::Ptr::make(model_, storage_->at(members_, i)); + if (auto value = storage_->at(members_, i); value) + return ModelNode::Ptr::make(model_, value->get()); + return {}; } template @@ -96,7 +100,9 @@ ModelNode::Ptr BaseObject::at(int64_t i) const { if (i < 0 || i >= (int64_t)storage_->size(members_)) return {}; - return ModelNode::Ptr::make(model_, storage_->at(members_, i).node_); + if (auto value = storage_->at(members_, i); value) + return ModelNode::Ptr::make(model_, value->get().node_); + return {}; } template @@ -104,7 +110,9 @@ StringId BaseObject::keyAt(int64_t i) const { if (i < 0 || i >= (int64_t)storage_->size(members_)) return {}; - return storage_->at(members_, i).name_; + if (auto value = storage_->at(members_, i); value) + return value->get().name_; + return {}; } template @@ -146,13 +154,15 @@ bool BaseObject::iterate(const ModelNode::IterCallback } template -BaseObject& BaseObject::addFieldInternal( +tl::expected>, Error> BaseObject::addFieldInternal( std::string_view const& name, ModelNode::Ptr const& value) { auto fieldId = model().strings()->emplace(name); - storage_->emplace_back(members_, fieldId, value->addr()); + if (!fieldId) + return tl::unexpected(std::move(fieldId.error())); + storage_->emplace_back(members_, *fieldId, value->addr()); return *this; } -} // namespace simfil \ No newline at end of file +} // namespace simfil diff --git a/include/simfil/model/string-pool.h b/include/simfil/model/string-pool.h index 14412ed4..1887b565 100644 --- a/include/simfil/model/string-pool.h +++ b/include/simfil/model/string-pool.h @@ -11,6 +11,9 @@ #include #include #include +#include + +#include "simfil/error.h" namespace simfil { @@ -45,7 +48,7 @@ struct StringPool /// Use this function to lookup a stored string, or insert it /// if it doesn't exist yet. - StringId emplace(std::string_view const& str); + auto emplace(std::string_view const& str) -> tl::expected; /// Returns the ID of the given string, or `Empty` if /// no such string was ever inserted. @@ -67,12 +70,12 @@ struct StringPool size_t misses() const; /// Add a static key-string mapping - Warning: Not thread-safe. - void addStaticKey(StringId k, std::string const& v); + void addStaticKey(StringId id, std::string const& value); /// Serialization - write to stream, starting from a specific /// id offset if necessary (for partial serialisation). - virtual void write(std::ostream& outputStream, StringId offset = {}) const; // NOLINT - virtual void read(std::istream& inputStream); + virtual auto write(std::ostream& outputStream, StringId offset = {}) const -> tl::expected; // NOLINT + virtual auto read(std::istream& inputStream) -> tl::expected; /// Check if the content of the string pools is logically identical. bool operator== (StringPool const& other) const; diff --git a/include/simfil/overlay.h b/include/simfil/overlay.h index 2539550f..14fe57a5 100644 --- a/include/simfil/overlay.h +++ b/include/simfil/overlay.h @@ -14,7 +14,7 @@ struct OverlayNodeStorage final : public Model explicit OverlayNodeStorage(Value const& val) : value_(val) {} // NOLINT - void resolve(ModelNode const& n, ResolveFn const& cb) const override; + tl::expected resolve(ModelNode const& n, ResolveFn const& cb) const override; }; /** Node for injecting member fields */ diff --git a/include/simfil/sourcelocation.h b/include/simfil/sourcelocation.h index 6d412ef1..a3017501 100644 --- a/include/simfil/sourcelocation.h +++ b/include/simfil/sourcelocation.h @@ -19,6 +19,8 @@ struct SourceLocation {} SourceLocation(const SourceLocation&) = default; + + auto operator==(const SourceLocation& o) const -> bool = default; }; } diff --git a/include/simfil/value.h b/include/simfil/value.h index e6a550da..027fce2b 100644 --- a/include/simfil/value.h +++ b/include/simfil/value.h @@ -4,6 +4,7 @@ #include #include #include +#include #include "model/nodes.h" #include "transient.h" diff --git a/repl/repl.cpp b/repl/repl.cpp index ddd6c04b..7fe29c59 100644 --- a/repl/repl.cpp +++ b/repl/repl.cpp @@ -85,7 +85,7 @@ char* command_generator(const char* text, int state) if (!root) return nullptr; - auto comp = simfil::complete(*current_env, query, rl_point, *root, opts); + auto comp = simfil::complete(*current_env, query, rl_point, **root, opts); if (!comp) return nullptr; @@ -142,7 +142,11 @@ static auto eval_mt(simfil::Environment& env, const simfil::AST& ast, const std: if (!model->numRoots()) { model->addRoot(simfil::ModelNode::Ptr()); - auto res = simfil::eval(env, ast, *model->root(0), &diag); + auto root = model->root(0); + if (!root) + return result; + + auto res = simfil::eval(env, ast, **root, &diag); if (res) result[0] = std::move(*res); // TODO: Output eval errors @@ -163,7 +167,13 @@ static auto eval_mt(simfil::Environment& env, const simfil::AST& ast, const std: size_t next; while ((next = idx++) < model->numRoots()) { try { - auto res = simfil::eval(env, ast, *model->root(next), &diag); + auto root = model->root(next); + if (!root) { + std::cerr << "Error: " << root.error().message << "\n"; + continue; + } + + auto res = simfil::eval(env, ast, **root, &diag); if (res) result[next] = std::move(*res); // TODO: Output eval errors @@ -226,7 +236,8 @@ int main(int argc, char *argv[]) #if defined(SIMFIL_WITH_MODEL_JSON) std::cout << "Parsing " << filename << "\n"; auto f = std::ifstream(std::string(filename)); - simfil::json::parse(f, model); + if (auto res = simfil::json::parse(f, model); !res) + std::cerr << "Error: " << res.error().message << "\n"; #endif }; diff --git a/src/expressions.cpp b/src/expressions.cpp index 24e05805..c53a6467 100644 --- a/src/expressions.cpp +++ b/src/expressions.cpp @@ -821,7 +821,7 @@ auto BinaryWordOpExpr::ieval(Context ctx, const Value& val, const ResultFn& res) if (rval.isa(ValueType::TransientObject)) { const auto& obj = rval.as(); - auto v = obj.meta->binaryOp(ident_, obj, rval); + auto v = obj.meta->binaryOp(ident_, lval, obj); if (!v) return tl::unexpected(std::move(v.error())); return res(ctx, std::move(v.value())); diff --git a/src/function.cpp b/src/function.cpp index 4c84a347..1d6d3abe 100644 --- a/src/function.cpp +++ b/src/function.cpp @@ -8,40 +8,13 @@ #include "simfil/types.h" #include "simfil/overlay.h" #include "fmt/core.h" +#include "tl/expected.hpp" #include -#include namespace simfil { -using namespace std::string_literals; - -auto ArgumentCountError::what() const noexcept -> const char* -{ - if (min < max) - msg = fmt::format("{}: Expected {} to {} arguments; got {}", - fn->ident().ident, min, max, have); - else - msg = fmt::format("{}: Expected {} arguments; got {}", - fn->ident().ident, min, have); - return msg.c_str(); -} - -auto ArgumentValueCountError::what() const noexcept -> const char* -{ - msg = fmt::format("{}: Argument {} must be a single value", - fn->ident().ident, index); - return msg.c_str(); -} - -auto ArgumentTypeError::what() const noexcept -> const char* -{ - msg = fmt::format("{}: Expected argument {} to be of type {}; got {}", - fn->ident().ident, index, want, have); - return msg.c_str(); -} - namespace { @@ -53,27 +26,32 @@ struct ArgParser Context ctx; std::size_t idx = 0; bool anyUndef = false; + std::optional error; - ArgParser(const std::string& functionName, Context ctx, Value val, const std::vector& args, size_t idx = 0) - : functionName(functionName) + ArgParser(std::string functionName, Context ctx, Value val, const std::vector& args, size_t idx = 0) + : functionName(std::move(functionName)) , args(args) , value(std::move(val)) , ctx(ctx) , idx(idx) { if (args.size() < idx) - raise(functionName + ": too few arguments"s); + error = Error(Error::InvalidArguments, fmt::format("too few arguments for function {}", this->functionName)); } auto arg(const char* name, ValueType type, Value& outValue) -> ArgParser& { - if (args.size() <= idx) - raise(functionName + ": missing argument "s + name); + if (args.size() <= idx) { + error = Error(Error::InvalidArguments, fmt::format("missing argumnt {} for function {}", name, this->functionName)); + return *this; + } auto subctx = ctx; - args[idx]->eval(subctx, value, LambdaResultFn([&, n = 0](Context, Value vv) mutable { - if (++n > 1) - raise(functionName + ": argument "s + name + " must return a single value"s); + auto res = args[idx]->eval(subctx, value, LambdaResultFn([&, n = 0](Context, Value vv) mutable -> tl::expected { + if (++n > 1) { + return tl::unexpected(Error::ExpectedSingleValue, + fmt::format("expeted single argument value for argument {} for function {}", name, functionName)); + } if (vv.isa(ValueType::Undef)) { anyUndef = true; @@ -81,14 +59,19 @@ struct ArgParser return Result::Continue; } - if (!vv.isa(type)) - raise(functionName + ": invalid value type for argument '"s + name + "'"s); + if (!vv.isa(type)) { + return tl::unexpected(Error::TypeMissmatch, + fmt::format("invalid type for argument {} for function {}", name, functionName)); + } outValue = std::move(vv); return Result::Continue; })); + if (!res) + error = std::move(res.error()); + ++idx; return *this; } @@ -101,9 +84,11 @@ struct ArgParser } auto subctx = ctx; - args[idx]->eval(subctx, value, LambdaResultFn([&, n = 0](Context, Value vv) mutable { - if (++n > 1) - raise(fmt::format("{}: argument {} must return a single value", functionName, name)); + auto res = args[idx]->eval(subctx, value, LambdaResultFn([&, n = 0](Context, Value vv) mutable -> tl::expected { + if (++n > 1) { + return tl::unexpected(Error::ExpectedSingleValue, + fmt::format("{}: argument {} must return a single value", functionName, name)); + } if (vv.isa(ValueType::Undef)) { anyUndef = true; @@ -112,19 +97,24 @@ struct ArgParser } if (!vv.isa(type)) - raise(fmt::format("{}: invalid value type for argument", functionName, name)); + return tl::unexpected(Error::TypeMissmatch, + fmt::format("{}: invalid value type for argument", functionName, name)); outValue = std::move(vv); return Result::Continue; })); + if (!res) + error = std::move(res.error()); ++idx; return *this; } [[nodiscard]] - auto ok() const -> bool + auto ok() const -> tl::expected { + if (error) + return tl::unexpected(error.value()); return !anyUndef; } }; @@ -155,7 +145,8 @@ auto CountFn::ident() const -> const FnInfo& auto CountFn::eval(Context ctx, Value val, const std::vector& args, const ResultFn& res) const -> tl::expected { if (args.empty()) - raise("count(...) expects one argument; got "s + std::to_string(args.size())); + return tl::unexpected(Error::InvalidArguments, + fmt::format("function 'count' expects one argument, got {}", args.size())); auto subctx = ctx; auto undef = false; /* At least one value is undef */ @@ -200,11 +191,13 @@ auto TraceFn::eval(Context ctx, Value val, const std::vector& args, con Value name = Value::undef(); Value limit = Value::undef(); - (void)ArgParser("trace", ctx, val, args, 1) + auto ok = ArgParser("trace", ctx, val, args, 1) /* Skip arg 0 */ .opt("limit", ValueType::Int, limit, Value::make(static_cast(-1))) .opt("name", ValueType::String, name, Value::make(args[0]->toString())) .ok(); + if (!ok) + return tl::unexpected(std::move(ok.error())); /* Never run in compilation phase */ if (ctx.phase == Context::Phase::Compilation) @@ -258,7 +251,8 @@ auto RangeFn::ident() const -> const FnInfo& auto RangeFn::eval(Context ctx, Value val, const std::vector& args, const ResultFn& res) const -> tl::expected { if (args.size() != 2) - raise("range(begin, end) expects 2 arguments; got "s + std::to_string(args.size())); + return tl::unexpected(Error::InvalidArguments, + fmt::format("function 'range' expects 2 arguments, got {}", args.size())); Value begin = Value::undef(); Value end = Value::undef(); @@ -267,8 +261,9 @@ auto RangeFn::eval(Context ctx, Value val, const std::vector& args, con .arg("begin", ValueType::Int, begin) .arg("end", ValueType::Int, end) .ok(); - if (!ok) + return tl::unexpected(std::move(ok.error())); + if (!ok.value()) return res(ctx, Value::undef()); auto ibegin = begin.as(); @@ -292,10 +287,11 @@ auto ReFn::ident() const -> const FnInfo& auto ReFn::eval(Context ctx, Value val, const std::vector& args, const ResultFn& res) const -> tl::expected { if (args.size() != 1) - raise("re(expr) expects 1 arguments; got "s + std::to_string(args.size())); + return tl::unexpected(Error::InvalidArguments, + fmt::format("'re' expects 1 argument, got {}", args.size())); auto subctx = ctx; - return args[0]->eval(subctx, val, LambdaResultFn([&](Context, Value vv) { + return args[0]->eval(subctx, val, LambdaResultFn([&](Context, Value vv) -> tl::expected { if (vv.isa(ValueType::Undef)) return res(ctx, Value::undef()); @@ -307,7 +303,8 @@ auto ReFn::eval(Context ctx, Value val, const std::vector& args, const if (const auto obj = vv.as(); obj.meta == &ReType::Type) return res(ctx, std::move(vv)); - raise("re: invalid value type for argument 'expr'"s); + return tl::unexpected(Error::TypeMissmatch, + fmt::format("invalid type for argument 'expr' for function 're'")); })); } @@ -331,10 +328,12 @@ auto ArrFn::eval(Context ctx, Value val, const std::vector& args, const return res(ctx, Value::null()); for (const auto& arg : args) { - if (arg->eval(ctx, val, LambdaResultFn([&res](Context ctx, Value vv) { + if (auto r = arg->eval(ctx, val, LambdaResultFn([&res](Context ctx, Value vv) { return res(ctx, std::move(vv)); - })) == Result::Stop) + })); r && *r == Result::Stop) return Result::Stop; + else if (!r) + return tl::unexpected(r.error()); } return Result::Continue; @@ -409,15 +408,19 @@ auto SplitFn::eval(Context ctx, Value val, const std::vector& args, con .arg("separator", ValueType::String, sep) .opt("keepEmpty", ValueType::Bool, keepEmpty, Value::t()) .ok(); + if (!ok) + return tl::unexpected(std::move(ok.error())); auto subctx = ctx; - if (!ok) + if (!ok.value()) return res(subctx, Value::undef()); auto items = split(str.as(), sep.as(), !keepEmpty.as()); for (auto&& item : items) { - if (res(subctx, Value::make(std::move(item))) == Result::Stop) + if (auto r = res(subctx, Value::make(std::move(item))); r && *r == Result::Stop) break; + else if (!r) + return tl::unexpected(r.error()); } return Result::Continue; @@ -446,8 +449,10 @@ auto SelectFn::eval(Context ctx, Value val, const std::vector& args, co .arg("index", ValueType::Int, idx) .opt("limit", ValueType::Int, cnt, Value::make(static_cast(1))) .ok(); - if (!ok) + return tl::unexpected(std::move(ok.error())); + + if (!ok.value()) return res(ctx, Value::undef()); auto iidx = idx.as(); @@ -486,7 +491,8 @@ auto SumFn::ident() const -> const FnInfo& auto SumFn::eval(Context ctx, Value val, const std::vector& args, const ResultFn& res) const -> tl::expected { if (args.empty() || args.size() > 3) - raise("sum: Expected at least 1 argument; got "s + std::to_string(args.size())); + return tl::unexpected(Error::InvalidArguments, + fmt::format("'sum' expects at least 1 argument, got {}", args.size())); Value sum = Value::make(static_cast(0)); @@ -545,7 +551,8 @@ auto KeysFn::ident() const -> const FnInfo& auto KeysFn::eval(Context ctx, Value val, const std::vector& args, const ResultFn& res) const -> tl::expected { if (args.size() != 1) - raise("keys: Expected 1 argument; got "s + std::to_string(args.size())); + return tl::unexpected(Error::InvalidArguments, + fmt::format("'keys' expects 1 argument got {}", args.size())); auto result = args[0]->eval(ctx, val, LambdaResultFn([&res](Context ctx, Value vv) -> tl::expected { if (ctx.phase == Context::Phase::Compilation) diff --git a/src/model/json.cpp b/src/model/json.cpp index 9248fb1e..c140bc7f 100644 --- a/src/model/json.cpp +++ b/src/model/json.cpp @@ -5,11 +5,13 @@ #include +#include "../expected.h" + namespace simfil::json { using json = nlohmann::json; -static ModelNode::Ptr build(const json& j, ModelPool & model) +static auto build(const json& j, ModelPool & model) -> tl::expected { switch (j.type()) { case json::value_t::null: @@ -23,45 +25,64 @@ static ModelNode::Ptr build(const json& j, ModelPool & model) case json::value_t::number_integer: return model.newValue(j.get()); case json::value_t::string: - return model.newValue((StringId)model.strings()->emplace(j.get())); + if (auto stringId = model.strings()->emplace(j.get()); stringId) + return model.newValue((StringId)*stringId); + else + return tl::unexpected(stringId.error()); default: break; } if (j.is_object()) { auto object = model.newObject(j.size()); - for (auto&& [key, value] : j.items()) - object->addField(key, build(value, model)); + for (auto&& [key, value] : j.items()) { + auto child = build(value, model); + TRY_EXPECTED(child); + object->addField(key, *child); + } return object; } if (j.is_array()) { auto array = model.newArray(j.size()); - for (const auto& value : j) - array->append(build(value, model)); + for (const auto& value : j) { + auto child = build(value, model); + TRY_EXPECTED(child); + array->append(*child); + } return array; } return {}; } -void parse(std::istream& input, ModelPoolPtr const& model) +auto parse(std::istream& input, ModelPoolPtr const& model) -> tl::expected { - model->addRoot(build(json::parse(input), *model)); - model->validate(); + auto root = build(json::parse(input), *model); + if (!root) + return tl::unexpected(root.error()); + model->addRoot(*root); + return model->validate(); } -void parse(const std::string& input, ModelPoolPtr const& model) +auto parse(const std::string& input, ModelPoolPtr const& model) -> tl::expected { - model->addRoot(build(json::parse(input), *model)); - model->validate(); + auto root = build(json::parse(input), *model); + if (!root) + return tl::unexpected(root.error()); + model->addRoot(*root); + return model->validate(); } -ModelPoolPtr parse(const std::string& input) +auto parse(const std::string& input) -> tl::expected { auto model = std::make_shared(); - model->addRoot(build(json::parse(input), *model)); - model->validate(); + auto root = build(json::parse(input), *model); + if (!root) + return tl::unexpected(root.error()); + model->addRoot(*root); + if (auto res = model->validate(); !res) + return tl::unexpected(res.error()); return model; } diff --git a/src/model/model.cpp b/src/model/model.cpp index 55344e05..b805cb04 100644 --- a/src/model/model.cpp +++ b/src/model/model.cpp @@ -14,11 +14,12 @@ #include #include #include +#include namespace simfil { -void Model::resolve(const ModelNode& n, const ResolveFn& cb) const +tl::expected Model::resolve(const ModelNode& n, const ResolveFn& cb) const { switch (n.addr_.column()) { case Null: @@ -37,8 +38,10 @@ void Model::resolve(const ModelNode& n, const ResolveFn& cb) const cb(ValueNode(n)); break; default: - raise(fmt::format("Bad column reference: col={}", (uint16_t)n.addr_.column())); + return tl::unexpected(Error::RuntimeError, + fmt::format("Bad column reference: col={}", (uint16_t)n.addr_.column())); } + return {}; } struct ModelPool::Impl @@ -124,31 +127,28 @@ std::vector ModelPool::checkForErrors() const std::function validateModelNode = [&](ModelNode::Ptr node) { - try { - if (node->type() == ValueType::Object) { - if (node->addr().column() == Objects) - if (!validateArrayIndex(node->addr().index(), "object", impl_->columns_.objectMemberArrays_)) - return; - for (auto const& [fieldName, fieldValue] : node->fields()) { - validatePooledString(fieldName); - validateModelNode(fieldValue); - } - } - else if (node->type() == ValueType::Array) { - if (node->addr().column() == Arrays) - if (!validateArrayIndex(node->addr().index(), "arrays", impl_->columns_.arrayMemberArrays_)) - return; - for (auto const& member : *node) - validateModelNode(member); - } - else if (node->addr().column() == PooledString) { - validatePooledString(static_cast(node->addr().index())); + if (node->type() == ValueType::Object) { + if (node->addr().column() == Objects) + if (!validateArrayIndex(node->addr().index(), "object", impl_->columns_.objectMemberArrays_)) + return; + for (auto const& [fieldName, fieldValue] : node->fields()) { + validatePooledString(fieldName); + validateModelNode(fieldValue); } - resolve(*node, Lambda([](auto&&) {})); } - catch (std::exception& e) { - errors.emplace_back(e.what()); + else if (node->type() == ValueType::Array) { + if (node->addr().column() == Arrays) + if (!validateArrayIndex(node->addr().index(), "arrays", impl_->columns_.arrayMemberArrays_)) + return; + for (auto const& member : *node) + validateModelNode(member); } + else if (node->addr().column() == PooledString) { + validatePooledString(static_cast(node->addr().index())); + } + + if (auto res = resolve(*node, Lambda([](auto&&) {})); !res) + errors.emplace_back(res.error().message); }; // Validate objects @@ -161,18 +161,19 @@ std::vector ModelPool::checkForErrors() const // Validate roots for (auto i = 0; i < numRoots(); ++i) - validateModelNode(root(i)); + if (auto node = root(i)) + validateModelNode(*node); return errors; } -void ModelPool::validate() const +auto ModelPool::validate() const -> tl::expected { auto errors = checkForErrors(); if (!errors.empty()) { - raise( - fmt::format("Model Error(s): {}", fmt::join(errors, ", "))); + return tl::unexpected(Error::RuntimeError, fmt::format("Model Error(s): {}", fmt::join(errors, ", "))); } + return {}; } void ModelPool::clear() @@ -192,17 +193,14 @@ void ModelPool::clear() clear_and_shrink(columns.arrayMemberArrays_); } -void ModelPool::resolve(ModelNode const& n, ResolveFn const& cb) const +tl::expected ModelPool::resolve(ModelNode const& n, ResolveFn const& cb) const { - auto get = [&n](auto const& vec) -> auto& { + auto checkBounds = [&n](auto const& vec) -> std::optional { auto idx = n.addr_.index(); if (idx >= vec.size()) - raise( - fmt::format( - "Bad node reference: col={}, i={}", - (uint16_t)n.addr_.column(), idx - )); - return vec[idx]; + return Error(Error::RuntimeError, fmt::format("bad node reference: col={}, i={}", + (uint16_t)n.addr_.column(), idx)); + return {}; }; switch (n.addr_.column()) { @@ -215,17 +213,26 @@ void ModelPool::resolve(ModelNode const& n, ResolveFn const& cb) const break; } case Int64: { - auto& val = get(impl_->columns_.i64_); + if (auto err = checkBounds(impl_->columns_.i64_)) + return tl::unexpected(*err); + auto idx = n.addr().index(); + auto& val = impl_->columns_.i64_[idx]; cb(ValueNode(val, shared_from_this())); break; } case Double: { - auto& val = get(impl_->columns_.double_); + if (auto err = checkBounds(impl_->columns_.double_)) + return tl::unexpected(*err); + auto idx = n.addr().index(); + auto& val = impl_->columns_.double_[idx]; cb(ValueNode(val, shared_from_this())); break; } case String: { - auto& val = get(impl_->columns_.strings_); + auto idx = n.addr().index(); + if (auto err = checkBounds(impl_->columns_.strings_)) + return tl::unexpected(*err); + auto& val = impl_->columns_.strings_[idx]; cb(ValueNode( // TODO: Make sure that the string view is not turned into a string here. std::string_view(impl_->columns_.stringData_).substr(val.offset_, val.length_), @@ -237,17 +244,19 @@ void ModelPool::resolve(ModelNode const& n, ResolveFn const& cb) const cb(ValueNode(str.value_or(std::string_view{}), shared_from_this())); break; } - default: Model::resolve(n, cb); + default: + return Model::resolve(n, cb); } + return {}; } size_t ModelPool::numRoots() const { return impl_->columns_.roots_.size(); } -ModelNode::Ptr ModelPool::root(size_t const& i) const { +tl::expected ModelPool::root(size_t const& i) const { if ((i < 0) || (i >= impl_->columns_.roots_.size())) - raise("Root index does not exist."); + return tl::unexpected(Error::RuntimeError, "Root index does not exist."); return ModelNode(shared_from_this(), impl_->columns_.roots_.at(i)); } @@ -339,23 +348,29 @@ std::shared_ptr ModelPool::strings() const return impl_->strings_; } -void ModelPool::setStrings(std::shared_ptr const& strings) +auto ModelPool::setStrings(std::shared_ptr const& strings) -> tl::expected { if (!strings) - raise("Attempt to call ModelPool::setStrings(nullptr)!"); + return tl::unexpected(Error::RuntimeError, "Attempt to call ModelPool::setStrings(nullptr)!"); auto oldStrings = impl_->strings_; impl_->strings_ = strings; if (!oldStrings || *strings == *oldStrings) - return; + return {}; // Translate object field IDs to the new dictionary. for (auto memberArray : impl_->columns_.objectMemberArrays_) { for (auto& member : memberArray) { - if (auto resolvedName = oldStrings->resolve(member.name_)) - member.name_ = strings->emplace(*resolvedName); + if (auto resolvedName = oldStrings->resolve(member.name_)) { + auto stringId = strings->emplace(*resolvedName); + if (!stringId) + return tl::unexpected(std::move(stringId.error())); + member.name_ = *stringId; + } } } + + return {}; } std::optional ModelPool::lookupStringId(const simfil::StringId id) const @@ -371,19 +386,21 @@ Array::Storage& ModelPool::arrayMemberStorage() { return impl_->columns_.arrayMemberArrays_; } -void ModelPool::write(std::ostream& outputStream) { +tl::expected ModelPool::write(std::ostream& outputStream) { bitsery::Serializer s(outputStream); impl_->readWrite(s); + return {}; } -void ModelPool::read(std::istream& inputStream) { +tl::expected ModelPool::read(std::istream& inputStream) { bitsery::Deserializer s(inputStream); impl_->readWrite(s); if (s.adapter().error() != bitsery::ReaderError::NoError) { - raise(fmt::format( + return tl::unexpected(Error::EncodeDecodeError, fmt::format( "Failed to read ModelPool: Error {}", static_cast>(s.adapter().error()))); } + return {}; } #if defined(SIMFIL_WITH_MODEL_JSON) @@ -392,7 +409,8 @@ nlohmann::json ModelPool::toJson() const auto roots = nlohmann::json::array(); const auto n = numRoots(); for (auto i = 0u; i < n; ++i) { - roots.push_back(root(i)->toJson()); + if (auto node = root(i)) + roots.push_back(node.value()->toJson()); } return roots; diff --git a/src/model/nodes.cpp b/src/model/nodes.cpp index a30a6b23..2a4ccc6f 100644 --- a/src/model/nodes.cpp +++ b/src/model/nodes.cpp @@ -3,6 +3,11 @@ #include "simfil/value.h" #include "simfil/model/nodes.h" +#include "../expected.h" +#include "tl/expected.hpp" + +#include + namespace simfil { @@ -238,64 +243,80 @@ Array& Array::append(int64_t const& value) {storage_->push_back(members_, model( Array& Array::append(double const& value) {storage_->push_back(members_, model().newValue(value)->addr()); return *this;} Array& Array::append(std::string_view const& value) {storage_->push_back(members_, model().newValue(value)->addr()); return *this;} -Array& Array::extend(model_ptr const& other) { +tl::expected Array::extend(model_ptr const& other) { auto otherSize = other->size(); for (auto i = 0u; i < otherSize; ++i) { - storage_->push_back(members_, storage_->at(other->members_, i)); + if (auto value = storage_->at(other->members_, i)) + storage_->push_back(members_, *value); + else + return tl::unexpected(std::move(value.error())); } - return *this; + return {}; } /** Model Node impls for an object. */ -ModelNode::Ptr Object::get(std::string_view const& fieldName) const { +tl::expected Object::get(std::string_view const& fieldName) const { auto fieldId = model().strings()->emplace(fieldName); - return get(fieldId); + TRY_EXPECTED(fieldId); + auto field = get(*fieldId); + if (!field) + return tl::unexpected(Error::FieldNotFound, fmt::format("No such field {}", fieldName)); + return field; } -Object& Object::addBool(std::string_view const& name, bool value) { +tl::expected Object::addBool(std::string_view const& name, bool value) { auto fieldId = model().strings()->emplace(name); - storage_->emplace_back(members_, fieldId, model().newSmallValue(value)->addr()); - return *this; + TRY_EXPECTED(fieldId); + storage_->emplace_back(members_, *fieldId, model().newSmallValue(value)->addr()); + return {}; } -Object& Object::addField(std::string_view const& name, uint16_t value) { +tl::expected Object::addField(std::string_view const& name, uint16_t value) { auto fieldId = model().strings()->emplace(name); - storage_->emplace_back(members_, fieldId, model().newSmallValue(value)->addr()); - return *this; + TRY_EXPECTED(fieldId); + storage_->emplace_back(members_, *fieldId, model().newSmallValue(value)->addr()); + return {}; } -Object& Object::addField(std::string_view const& name, int16_t value) { +tl::expected Object::addField(std::string_view const& name, int16_t value) { auto fieldId = model().strings()->emplace(name); - storage_->emplace_back(members_, fieldId, model().newSmallValue(value)->addr()); - return *this; + TRY_EXPECTED(fieldId); + storage_->emplace_back(members_, *fieldId, model().newSmallValue(value)->addr()); + return {}; } -Object& Object::addField(std::string_view const& name, int64_t const& value) { +tl::expected Object::addField(std::string_view const& name, int64_t const& value) { auto fieldId = model().strings()->emplace(name); - storage_->emplace_back(members_, fieldId, model().newValue(value)->addr()); - return *this; + TRY_EXPECTED(fieldId); + storage_->emplace_back(members_, *fieldId, model().newValue(value)->addr()); + return {}; } -Object& Object::addField(std::string_view const& name, double const& value) { +tl::expected Object::addField(std::string_view const& name, double const& value) { auto fieldId = model().strings()->emplace(name); - storage_->emplace_back(members_, fieldId, model().newValue(value)->addr()); - return *this; + TRY_EXPECTED(fieldId); + storage_->emplace_back(members_, *fieldId, model().newValue(value)->addr()); + return {}; } -Object& Object::addField(std::string_view const& name, std::string_view const& value) { +tl::expected Object::addField(std::string_view const& name, std::string_view const& value) { auto fieldId = model().strings()->emplace(name); - storage_->emplace_back(members_, fieldId, model().newValue(value)->addr()); - return *this; + TRY_EXPECTED(fieldId); + storage_->emplace_back(members_, *fieldId, model().newValue(value)->addr()); + return {}; } -Object& Object::extend(model_ptr const& other) +tl::expected Object::extend(model_ptr const& other) { auto otherSize = other->size(); for (auto i = 0u; i < otherSize; ++i) { - storage_->push_back(members_, storage_->at(other->members_, i)); + if (auto value = storage_->at(other->members_, i)) + storage_->push_back(members_, *value); + else + return tl::unexpected(std::move(value.error())); } - return *this; + return {}; } } diff --git a/src/model/string-pool.cpp b/src/model/string-pool.cpp index 8c3a11db..a4c4f976 100644 --- a/src/model/string-pool.cpp +++ b/src/model/string-pool.cpp @@ -1,5 +1,6 @@ #include "simfil/model/string-pool.h" #include "simfil/exception-handler.h" +#include "simfil/error.h" #include #include @@ -89,7 +90,7 @@ StringPool::StringPool(const StringPool& other) cacheMisses_ = other.cacheMisses_.load(); } -StringId StringPool::emplace(std::string_view const& str) +auto StringPool::emplace(std::string_view const& str) -> tl::expected { { std::shared_lock lock(stringStoreMutex_); @@ -112,7 +113,7 @@ StringId StringPool::emplace(std::string_view const& str) auto& storedString = storedStrings_.emplace_back(str); StringId id = nextId_++; if (nextId_ < id) { - raise("StringPool id overflow!"); + return tl::unexpected(Error::StringPoolOverflow, "StringPool id overflow!"); } idForString_.emplace(storedString, id); stringForId_.emplace(id, storedString); @@ -177,7 +178,7 @@ void StringPool::addStaticKey(StringId id, const std::string& value) stringForId_.emplace(id, storedString); } -void StringPool::write(std::ostream& outputStream, const StringId offset) const // NOLINT +auto StringPool::write(std::ostream& outputStream, const StringId offset) const -> tl::expected // NOLINT { std::shared_lock stringStoreReadAccess(stringStoreMutex_); bitsery::Serializer s(outputStream); @@ -201,9 +202,11 @@ void StringPool::write(std::ostream& outputStream, const StringId offset) const s.text1b(it->second, std::numeric_limits::max()); } } + + return {}; } -void StringPool::read(std::istream& inputStream) +auto StringPool::read(std::istream& inputStream) -> tl::expected { std::unique_lock stringStoreWriteAccess_(stringStoreMutex_); bitsery::Deserializer s(inputStream); @@ -232,10 +235,13 @@ void StringPool::read(std::istream& inputStream) } if (s.adapter().error() != bitsery::ReaderError::NoError) { - raise(fmt::format( - "Failed to read StringPool: Error {}", - static_cast>(s.adapter().error()))); + return tl::unexpected( + Error::EncodeDecodeError, + fmt::format("Failed to read StringPool: Error {}", + static_cast>(s.adapter().error()))); } + + return {}; } bool StringPool::operator==(const StringPool &other) const { diff --git a/src/overlay.cpp b/src/overlay.cpp index 612ea8ec..ab0e533d 100644 --- a/src/overlay.cpp +++ b/src/overlay.cpp @@ -3,9 +3,10 @@ namespace simfil { -void OverlayNodeStorage::resolve(ModelNode const& n, ResolveFn const& cb) const +tl::expected OverlayNodeStorage::resolve(ModelNode const& n, ResolveFn const& cb) const { cb(OverlayNode(n)); + return {}; } OverlayNode::OverlayNode(Value const& val) diff --git a/test/arena.cpp b/test/arena.cpp index a9a7afe3..12af9abe 100644 --- a/test/arena.cpp +++ b/test/arena.cpp @@ -75,7 +75,8 @@ TEST_CASE("ArrayArena clear and shrink_to_fit", "[ArrayArena]") { arena.clear(); ArrayIndex array2 = arena.new_array(2); REQUIRE(array2 == 0); - REQUIRE_THROWS_AS(arena.at(array1, 0), std::out_of_range); + REQUIRE(!arena.at(array1, 0)); + REQUIRE(arena.at(array1, 0).error().type == Error::IndexOutOfRange); } SECTION("shrink_to_fit") { diff --git a/test/common.cpp b/test/common.cpp index e1fed529..c1263b28 100644 --- a/test/common.cpp +++ b/test/common.cpp @@ -21,7 +21,8 @@ auto Compile(std::string_view query, bool autoWildcard) -> ASTPtr auto JoinedResult(std::string_view query, std::optional json) -> std::string { auto model = simfil::json::parse(std::string(json.value_or(TestModel))); - Environment env(model->strings()); + REQUIRE(model); + Environment env(model.value()->strings()); env.functions["panic"] = &panicFn; @@ -33,7 +34,8 @@ auto JoinedResult(std::string_view query, std::optional json) -> st INFO("AST: " << (*ast)->expr().toString()); - auto res = eval(env, **ast, *model->root(0), nullptr); + auto root = model.value()->root(0); + auto res = eval(env, **ast, **root, nullptr); if (!res) { INFO("ERROR: " << res.error().message); return fmt::format("ERROR: {}", res.error().message); @@ -51,16 +53,19 @@ auto JoinedResult(std::string_view query, std::optional json) -> st auto CompleteQuery(std::string_view query, size_t point, std::optional json) -> std::vector { auto model = simfil::json::parse(json.value_or(TestModel)); - Environment env(model->strings()); + REQUIRE(model); + Environment env(model.value()->strings()); CompletionOptions opts; - return complete(env, query, point, *model->root(0), opts).value_or(std::vector()); + auto root = model.value()->root(0); + return complete(env, query, point, **root, opts).value_or(std::vector()); } auto GetDiagnosticMessages(std::string_view query) -> std::vector { auto model = simfil::json::parse(TestModel); - Environment env(model->strings()); + REQUIRE(model); + Environment env(model.value()->strings()); env.functions["panic"] = &panicFn; @@ -72,7 +77,8 @@ auto GetDiagnosticMessages(std::string_view query) -> std::vectorexpr().toString()); Diagnostics diag; - auto res = eval(env, **ast, *model->root(0), &diag); + auto root = model.value()->root(0); + auto res = eval(env, **ast, **root, &diag); if (!res) INFO(res.error().message); REQUIRE(res); diff --git a/test/complex.cpp b/test/complex.cpp index 57f78e30..bffd18b9 100644 --- a/test/complex.cpp +++ b/test/complex.cpp @@ -104,17 +104,18 @@ TEST_CASE("Multimap JSON", "[multimap.serialization]") { TEST_CASE("Serialization", "[complex.serialization]") { auto model = json::parse(invoice); + REQUIRE(model); SECTION("Test Model write/read") { std::stringstream stream; - model->write(stream); + model.value()->write(stream); auto recoveredModel = std::make_shared(); recoveredModel->read(stream); - CHECK_THROWS(recoveredModel->validate()); - recoveredModel->setStrings(model->strings()); - recoveredModel->validate(); + REQUIRE(!recoveredModel->validate()); + recoveredModel->setStrings(model.value()->strings()); + REQUIRE(recoveredModel->validate()); std::function require_equals = [&](auto l, auto r) { @@ -134,22 +135,24 @@ TEST_CASE("Serialization", "[complex.serialization]") { } }; - REQUIRE(model->numRoots() == recoveredModel->numRoots()); - require_equals(model->root(0), recoveredModel->root(0)); + REQUIRE(model.value()->numRoots() == recoveredModel->numRoots()); + REQUIRE(model.value()->root(0)); + REQUIRE(recoveredModel->root(0)); + require_equals(*model.value()->root(0), *recoveredModel->root(0)); } SECTION("Test Fields write/read") { std::stringstream stream; - model->strings()->write(stream); + REQUIRE(model.value()->strings()->write(stream)); const auto recoveredFields = std::make_shared(); - recoveredFields->read(stream); + REQUIRE(recoveredFields->read(stream)); - REQUIRE(model->strings()->size() == recoveredFields->size()); - REQUIRE(model->strings()->highest() == recoveredFields->highest()); - REQUIRE(model->strings()->bytes() == recoveredFields->bytes()); + REQUIRE(model.value()->strings()->size() == recoveredFields->size()); + REQUIRE(model.value()->strings()->highest() == recoveredFields->highest()); + REQUIRE(model.value()->strings()->bytes() == recoveredFields->bytes()); for (StringId sId = 0; sId <= recoveredFields->highest(); ++sId) - REQUIRE(model->strings()->resolve(sId) == recoveredFields->resolve(StringId(sId))); + REQUIRE(model.value()->strings()->resolve(sId) == recoveredFields->resolve(StringId(sId))); } } diff --git a/test/diagnostics.cpp b/test/diagnostics.cpp index 9638391c..65f8005f 100644 --- a/test/diagnostics.cpp +++ b/test/diagnostics.cpp @@ -1,6 +1,7 @@ #include #include +#include "catch2/catch_test_macros.hpp" #include "common.hpp" auto FindMessage(std::string_view query, std::string_view needle) { @@ -40,13 +41,16 @@ TEST_CASE("OrShortCircuit", "[diag.suppress-short-circuitted-or]") { TEST_CASE("DiagnosticsSerialization", "[diag.serialization]") { auto model = simfil::json::parse(TestModel); - Environment env(model->strings()); + REQUIRE(model); + Environment env(model.value()->strings()); // Create two diagnostic messages. Diagnostics originalDiag; auto ast = compile(env, "**.number == \"string\" or **.number == \"string\""); REQUIRE(ast.has_value()); - eval(env, **ast, *model->root(0), &originalDiag); + auto root = model.value()->root(0); + REQUIRE(root); + eval(env, **ast, **root, &originalDiag); std::stringstream stream; auto writeResult = originalDiag.write(stream); diff --git a/test/performance.cpp b/test/performance.cpp index cc1b2a8a..5f9261df 100644 --- a/test/performance.cpp +++ b/test/performance.cpp @@ -119,7 +119,9 @@ static auto result(const ModelPoolPtr& model, std::string_view query) REQUIRE(ast.has_value()); INFO("AST: " << (*ast)->expr().toString()); - return eval(env, **ast, *model->root(0), nullptr); + auto root = model->root(0); + REQUIRE(root); + return eval(env, **ast, **root, nullptr); } static auto joined_result(const ModelPoolPtr& model, std::string_view query) diff --git a/test/simfil.cpp b/test/simfil.cpp index a596c69b..768d66a3 100644 --- a/test/simfil.cpp +++ b/test/simfil.cpp @@ -189,8 +189,9 @@ TEST_CASE("CompareIncompatibleTypesFields", "[ast.compare-incompatible-types-fie )json"; const auto model = simfil::json::parse(doc); + REQUIRE(model); auto test = [&model](auto query) { - Environment env(model->strings()); + Environment env(model.value()->strings()); auto ast = compile(env, query, false); if (!ast) @@ -200,7 +201,10 @@ TEST_CASE("CompareIncompatibleTypesFields", "[ast.compare-incompatible-types-fie INFO("AST: " << (*ast)->expr().toString()); - return eval(env, **ast, *model->root(0), nullptr).value().front().template as(); + auto root = model.value()->root(0); + REQUIRE(root); + + return eval(env, **ast, **root, nullptr).value().front().template as(); }; /* Test some field with different value types for different objects */ @@ -436,25 +440,25 @@ TEST_CASE("Model Pool Validation", "[model.validation]") { // Recognize dangling object member pointer pool->clear(); pool->newObject()->addField("good", ModelNode::Ptr::make(pool, ModelNodeAddress{ModelPool::Objects, 666})); - REQUIRE_THROWS(pool->validate()); + REQUIRE(!pool->validate()); // Recognize dangling array member pointer pool->clear(); pool->newObject()->addField("good", ModelNode::Ptr::make(pool, ModelNodeAddress{ModelPool::Arrays, 666})); - REQUIRE_THROWS(pool->validate()); + REQUIRE(!pool->validate()); // Recognize dangling root pool->clear(); pool->addRoot(ModelNode::Ptr::make(pool, ModelNodeAddress{ModelPool::Objects, 666})); - REQUIRE_THROWS(pool->validate()); + REQUIRE(!pool->validate()); // An empty model should be valid pool->clear(); - REQUIRE_NOTHROW(pool->validate()); + REQUIRE(pool->validate()); // An empty object should also be valid pool->newObject()->addField("good", pool->newObject()); - REQUIRE_NOTHROW(pool->validate()); + REQUIRE(pool->validate()); } TEST_CASE("Procedural Object Node", "[model.procedural]") { @@ -492,20 +496,36 @@ TEST_CASE("Object/Array Extend", "[model.extend]") { testObjectB->addField("height", (int64_t)220); testObjectB->addField("age", (int64_t)55); - REQUIRE(testObjectA->size() == 2); - REQUIRE(testObjectB->size() == 2); - REQUIRE(Value(testObjectA->get("name")->value()).toString() == "hans"); - REQUIRE(Value(testObjectA->get("occupation")->value()).toString() == "baker"); - REQUIRE(!testObjectA->get("height")); - REQUIRE(!testObjectA->get("age")); - testObjectA->extend(testObjectB); - - REQUIRE(testObjectA->size() == 4); - REQUIRE(testObjectB->size() == 2); - REQUIRE(Value(testObjectA->get("name")->value()).toString() == "hans"); - REQUIRE(Value(testObjectA->get("occupation")->value()).toString() == "baker"); - REQUIRE(Value(testObjectA->get("height")->value()).as() == 220ll); - REQUIRE(Value(testObjectA->get("age")->value()).as() == 55ll); + { + REQUIRE(testObjectA->size() == 2); + REQUIRE(testObjectB->size() == 2); + auto nameValue = testObjectA->get("name"); + REQUIRE(nameValue); + REQUIRE(Value(nameValue.value()->value()).toString() == "hans"); + auto occupationValue = testObjectA->get("occupation"); + REQUIRE(occupationValue); + REQUIRE(Value(occupationValue.value()->value()).toString() == "baker"); + REQUIRE(!testObjectA->get("height")); + REQUIRE(!testObjectA->get("age")); + testObjectA->extend(testObjectB); + } + + { + REQUIRE(testObjectA->size() == 4); + REQUIRE(testObjectB->size() == 2); + auto nameValue = testObjectA->get("name"); + REQUIRE(nameValue); + REQUIRE(Value(nameValue.value()->value()).toString() == "hans"); + auto occupationValue = testObjectA->get("occupation"); + REQUIRE(occupationValue); + REQUIRE(Value(occupationValue.value()->value()).toString() == "baker"); + auto heightValue = testObjectA->get("height"); + REQUIRE(heightValue); + REQUIRE(Value(heightValue.value()->value()).as() == 220ll); + auto ageValue = testObjectA->get("age"); + REQUIRE(ageValue); + REQUIRE(Value(ageValue.value()->value()).as() == 55ll); + } } SECTION("Extend array") From d373da7e7bbd78647677a3f89e9dd81d5f5079ac Mon Sep 17 00:00:00 2001 From: Johannes Wolf Date: Tue, 23 Sep 2025 14:56:24 +0200 Subject: [PATCH 04/50] perf: Add some branch hints --- include/simfil/value.h | 56 ++++++++++++++++++++++++++++-------------- src/expected.h | 2 +- src/expressions.cpp | 51 +++++++++++++++++++------------------- src/function.cpp | 18 +++++++------- 4 files changed, 73 insertions(+), 54 deletions(-) diff --git a/include/simfil/value.h b/include/simfil/value.h index 027fce2b..0e3a0a29 100644 --- a/include/simfil/value.h +++ b/include/simfil/value.h @@ -217,7 +217,7 @@ template struct ValueAs { template - static auto get(const VariantType& v) -> decltype(auto) + static inline auto get(const VariantType& v) noexcept -> decltype(auto) { return std::get::Type>(v); } @@ -323,10 +323,22 @@ class Value Value& operator=(const Value&) = default; Value& operator=(Value&&) = default; - [[nodiscard]] auto isa(ValueType test) const + [[nodiscard]] auto isa(ValueType test) const noexcept { return type == test; } + + [[nodiscard]] bool asBool() const noexcept { + return std::get(value); + } + + [[nodiscard]] int64_t asInt() const noexcept { + return std::get(value); + } + + [[nodiscard]] double asFloat() const noexcept { + return std::get(value); + } template [[nodiscard]] auto as() const -> decltype(auto) @@ -334,31 +346,35 @@ class Value return ValueAs::get(value); } - [[nodiscard]] auto isBool(bool v) const + [[nodiscard]] auto isBool(bool v) const noexcept { - return isa(ValueType::Bool) && this->as() == v; + return type == ValueType::Bool && asBool() == v; } template [[nodiscard]] auto visit(Visitor fn) const { - if (type == ValueType::Undef) + switch (type) { + case ValueType::Undef: return fn(UndefinedType{}); - if (type == ValueType::Null) + case ValueType::Null: return fn(NullType{}); - if (type == ValueType::Bool) - return fn(this->template as()); - if (type == ValueType::Int) - return fn(this->template as()); - if (type == ValueType::Float) - return fn(this->template as()); - if (type == ValueType::String) + case ValueType::Bool: + return fn(asBool()); + case ValueType::Int: + return fn(asInt()); + case ValueType::Float: + return fn(asFloat()); + case ValueType::String: return fn(this->template as()); - if (type == ValueType::TransientObject) + case ValueType::TransientObject: return fn(this->template as()); - if (type == ValueType::Object || type == ValueType::Array) { - if (node) return fn(*node); - else return fn(NullType{}); + case ValueType::Object: + case ValueType::Array: + if (node) [[likely]] + return fn(*node); + else + return fn(NullType{}); } return fn(UndefinedType{}); } @@ -376,7 +392,8 @@ class Value return visit(impl::ValueToString()); } - [[nodiscard]] auto getScalar() { + [[nodiscard]] auto getScalar() noexcept + { struct { void operator() (std::monostate const& v) {result = v;} void operator() (bool const& v) {result = v;} @@ -392,7 +409,8 @@ class Value } /// Get the string_view of this Value if it has one. - std::string_view const* stringViewValue() { + std::string_view const* stringViewValue() noexcept + { return std::get_if(&value); } diff --git a/src/expected.h b/src/expected.h index 7964a7cc..93ee1f36 100644 --- a/src/expected.h +++ b/src/expected.h @@ -5,7 +5,7 @@ // Helper macro for bubbling-up tl::expected errors. #define TRY_EXPECTED(res) \ do { \ - if (!(res).has_value()) { \ + if (!(res).has_value()) [[unlikely]] { \ return tl::unexpected(std::move((res).error())); \ } \ } while (false) diff --git a/src/expressions.cpp b/src/expressions.cpp index c53a6467..f4912523 100644 --- a/src/expressions.cpp +++ b/src/expressions.cpp @@ -83,28 +83,29 @@ auto WildcardExpr::ieval(Context ctx, const Value& val, const ResultFn& ores) -> Context& ctx; ResultFn& res; - auto iterate(ModelNode const& val, int depth) + [[nodiscard]] auto iterate(ModelNode const& val) noexcept -> Result { - if (val.type() == ValueType::Null) + if (val.type() == ValueType::Null) [[unlikely]] return Result::Continue; - if (res(ctx, Value::field(val)) == Result::Stop) - return Result::Stop; + auto result = res(ctx, Value::field(val)); + if (!result || *result == Result::Stop) [[unlikely]] + return result ? *result : Result::Stop; - auto result = Result::Continue; - val.iterate(ModelNode::IterLambda([&, this](const auto& subNode) { - if (iterate(subNode, depth + 1) == Result::Stop) { - result = Result::Stop; + Result finalResult = Result::Continue; + val.iterate(ModelNode::IterLambda([&, this](const auto& subNode) -> bool { + if (iterate(subNode) == Result::Stop) { + finalResult = Result::Stop; return false; } return true; })); - return result; - }; + return finalResult; + } }; - auto r = Iterate{ctx, res}.iterate(*val.node, 0); + auto r = val.node ? Iterate{ctx, res}.iterate(*val.node) : Result::Continue; res.ensureCall(); return r; } @@ -139,10 +140,9 @@ auto AnyChildExpr::ieval(Context ctx, const Value& val, const ResultFn& res) -> if (!val.node || !val.node->size()) return res(ctx, Value::null()); - auto result = Result::Continue; - val.node->iterate(ModelNode::IterLambda([&](auto subNode) { - if (res(ctx, Value::field(std::move(subNode))) == Result::Stop) { - result = Result::Stop; + val.node->iterate(ModelNode::IterLambda([&](auto subNode) -> bool { + auto result = res(ctx, Value::field(std::move(subNode))); + if (!result || *result == Result::Stop) { return false; } return true; @@ -202,13 +202,13 @@ auto FieldExpr::ieval(Context ctx, const Value& val, const ResultFn& res) -> tl: if (!val.node) return res(ctx, Value::null()); - if (!nameId_) + if (!nameId_) [[unlikely]] { nameId_ = ctx.env->strings()->get(name_); - - if (!nameId_) - /* If the field name is not in the string cache, then there - is no field with that name. */ - return res(ctx, Value::null()); + if (!nameId_) + /* If the field name is not in the string cache, then there + is no field with that name. */ + return res(ctx, Value::null()); + } /* Enter sub-node */ if (auto sub = val.node->get(nameId_)) { @@ -410,7 +410,7 @@ auto SubExpr::ieval(Context ctx, const Value& val, const ResultFn& ores) -> tl:: auto r = left_->eval(ctx, val, LambdaResultFn([this, &res](Context ctx, const Value& lv) -> tl::expected { return sub_->eval(ctx, lv, LambdaResultFn([&res, &lv](const Context& ctx, const Value& vv) -> tl::expected { auto bv = UnaryOperatorDispatcher::dispatch(vv); - if (!bv) + if (!bv) [[unlikely]] return tl::unexpected(std::move(bv.error())); if (bv->isa(ValueType::Undef)) @@ -592,10 +592,11 @@ auto CallExpression::type() const -> Type auto CallExpression::ieval(Context ctx, const Value& val, const ResultFn& res) -> tl::expected { - if (!fn_) + if (!fn_) [[unlikely]] { fn_ = ctx.env->findFunction(name_); - if (!fn_) - return tl::unexpected(Error::UnknownFunction, fmt::format("Unknown function '{}'", name_)); + if (!fn_) + return tl::unexpected(Error::UnknownFunction, fmt::format("Unknown function '{}'", name_)); + } auto anyval = false; auto result = fn_->eval(ctx, val, args_, LambdaResultFn([&res, &anyval](const Context& ctx, Value vv) { diff --git a/src/function.cpp b/src/function.cpp index 1d6d3abe..3a40c38b 100644 --- a/src/function.cpp +++ b/src/function.cpp @@ -48,7 +48,7 @@ struct ArgParser auto subctx = ctx; auto res = args[idx]->eval(subctx, value, LambdaResultFn([&, n = 0](Context, Value vv) mutable -> tl::expected { - if (++n > 1) { + if (++n > 1) [[unlikely]] { return tl::unexpected(Error::ExpectedSingleValue, fmt::format("expeted single argument value for argument {} for function {}", name, functionName)); } @@ -59,7 +59,7 @@ struct ArgParser return Result::Continue; } - if (!vv.isa(type)) { + if (!vv.isa(type)) [[unlikely]] { return tl::unexpected(Error::TypeMissmatch, fmt::format("invalid type for argument {} for function {}", name, functionName)); } @@ -69,7 +69,7 @@ struct ArgParser return Result::Continue; })); - if (!res) + if (!res) [[unlikely]] error = std::move(res.error()); ++idx; @@ -103,7 +103,7 @@ struct ArgParser outValue = std::move(vv); return Result::Continue; })); - if (!res) + if (!res) [[unlikely]] error = std::move(res.error()); ++idx; @@ -113,8 +113,8 @@ struct ArgParser [[nodiscard]] auto ok() const -> tl::expected { - if (error) - return tl::unexpected(error.value()); + if (error) [[unlikely]] + return tl::unexpected(*error); return !anyUndef; } }; @@ -263,7 +263,7 @@ auto RangeFn::eval(Context ctx, Value val, const std::vector& args, con .ok(); if (!ok) return tl::unexpected(std::move(ok.error())); - if (!ok.value()) + if (!ok.value()) [[unlikely]] return res(ctx, Value::undef()); auto ibegin = begin.as(); @@ -412,7 +412,7 @@ auto SplitFn::eval(Context ctx, Value val, const std::vector& args, con return tl::unexpected(std::move(ok.error())); auto subctx = ctx; - if (!ok.value()) + if (!ok.value()) [[unlikely]] return res(subctx, Value::undef()); auto items = split(str.as(), sep.as(), !keepEmpty.as()); @@ -452,7 +452,7 @@ auto SelectFn::eval(Context ctx, Value val, const std::vector& args, co if (!ok) return tl::unexpected(std::move(ok.error())); - if (!ok.value()) + if (!ok.value()) [[unlikely]] return res(ctx, Value::undef()); auto iidx = idx.as(); From 7d2e54c25154b753427a4e4b5d80776c6c4b4110 Mon Sep 17 00:00:00 2001 From: Johannes Wolf Date: Tue, 23 Sep 2025 18:33:49 +0200 Subject: [PATCH 05/50] perf: Improve result callback performance --- include/simfil/result.h | 4 ++-- src/expressions.cpp | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/include/simfil/result.h b/include/simfil/result.h index afc3908a..92029867 100644 --- a/include/simfil/result.h +++ b/include/simfil/result.h @@ -18,7 +18,7 @@ struct ResultFn { virtual ~ResultFn() = default; - virtual auto operator()(Context ctx, Value value) const -> tl::expected = 0; + virtual auto operator()(Context ctx, const Value& value) const noexcept -> tl::expected = 0; }; template @@ -30,7 +30,7 @@ struct LambdaResultFn final : ResultFn : lambda(std::move(ref)) {} - auto operator()(Context ctx, Value value) const -> tl::expected override + auto operator()(Context ctx, const Value& value) const noexcept -> tl::expected override { return lambda(std::move(ctx), std::move(value)); } diff --git a/src/expressions.cpp b/src/expressions.cpp index f4912523..9179ddf5 100644 --- a/src/expressions.cpp +++ b/src/expressions.cpp @@ -32,11 +32,11 @@ struct CountedResultFn : ResultFn CountedResultFn(const CountedResultFn&) = delete; CountedResultFn(CountedResultFn&&) = delete; - auto operator()(Context ctx, Value vv) const -> tl::expected override + auto operator()(Context ctx, const Value& vv) const noexcept -> tl::expected override { assert(!finished); ++calls; - return fn(ctx, std::move(vv)); + return fn(ctx, vv); } /* NOTE: You _must_ call finish before destruction! */ From 38a14f8b860e8889a58190e1e1a58a1d0fd5ac9c Mon Sep 17 00:00:00 2001 From: Johannes Wolf Date: Tue, 23 Sep 2025 21:57:21 +0200 Subject: [PATCH 06/50] value: Remove unused getters --- include/simfil/value.h | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/include/simfil/value.h b/include/simfil/value.h index 0e3a0a29..2b4d1858 100644 --- a/include/simfil/value.h +++ b/include/simfil/value.h @@ -332,14 +332,6 @@ class Value return std::get(value); } - [[nodiscard]] int64_t asInt() const noexcept { - return std::get(value); - } - - [[nodiscard]] double asFloat() const noexcept { - return std::get(value); - } - template [[nodiscard]] auto as() const -> decltype(auto) { @@ -362,9 +354,9 @@ class Value case ValueType::Bool: return fn(asBool()); case ValueType::Int: - return fn(asInt()); + return fn(this->template as()); case ValueType::Float: - return fn(asFloat()); + return fn(this->template as()); case ValueType::String: return fn(this->template as()); case ValueType::TransientObject: From 173a4d29906636d8f27af0e7311418a28bad8530 Mon Sep 17 00:00:00 2001 From: Johannes Wolf Date: Tue, 23 Sep 2025 22:22:19 +0200 Subject: [PATCH 07/50] value: Move ModelNode::Ptr into the Value Variant --- include/simfil/value.h | 81 +++++++++++++++++++++++++++++++++--------- src/completion.cpp | 2 +- src/expressions.cpp | 20 +++++------ src/function.cpp | 8 ++--- src/overlay.cpp | 14 ++++---- src/simfil.cpp | 2 +- 6 files changed, 87 insertions(+), 40 deletions(-) diff --git a/include/simfil/value.h b/include/simfil/value.h index 2b4d1858..fb36f768 100644 --- a/include/simfil/value.h +++ b/include/simfil/value.h @@ -170,6 +170,11 @@ struct ValueType4CType { static constexpr ValueType Type = ValueType::Object; }; +template <> +struct ValueType4CType { + static constexpr ValueType Type = ValueType::Object; +}; + template struct ValueTypeInfo; @@ -237,6 +242,30 @@ struct ValueAs } }; +template <> +struct ValueAs +{ + template + static auto get(const VariantType& v) -> ModelNode::Ptr + { + if (auto nodePtr = std::get_if(&v)) + return *nodePtr; + return {}; + } +}; + +template <> +struct ValueAs +{ + template + static auto get(const VariantType& v) -> ModelNode::Ptr + { + if (auto nodePtr = std::get_if(&v)) + return *nodePtr; + return {}; + } +}; + class Value { public: @@ -278,20 +307,33 @@ class Value static auto field(const ModelNode& node) -> Value { - return {node.type(), node.value(), model_ptr(node)}; + const auto type = node.type(); + if (type == ValueType::Object || type == ValueType::Array) { + return Value{type, model_ptr(node)}; + } else { + return Value{node.value()}; + } } static auto field(ModelNode&& node) -> Value { - auto type = node.type(); - auto value = node.value(); - return {type, std::move(value), model_ptr(std::move(node))}; + const auto type = node.type(); + if (type == ValueType::Object || type == ValueType::Array) { + return {type, model_ptr(std::move(node))}; + } else { + return Value{node.value()}; + } } template static auto field(const model_ptr& node) -> Value { - return {node->type(), node->value(), node}; + const auto type = node->type(); + if (type == ValueType::Object || type == ValueType::Array) { + return {type, model_ptr(node)}; + } else { + return Value{node->value()}; + } } Value(ValueType type) // NOLINT @@ -304,17 +346,12 @@ class Value , value(std::forward(value)) {} - Value(ValueType type, ScalarValueType&& value_, ModelNode::Ptr node) - : type(type), node(std::move(node)) - { - std::visit([this](auto&& v){value = v;}, value_); - } Value(ScalarValueType&& value_) // NOLINT { std::visit([this](auto&& v){ type = ValueType4CType>::Type; - value = v; + value = std::forward(v); }, value_); } @@ -363,8 +400,8 @@ class Value return fn(this->template as()); case ValueType::Object: case ValueType::Array: - if (node) [[likely]] - return fn(*node); + if (auto nodePtr = std::get_if(&value); nodePtr && *nodePtr) [[likely]] + return fn(**nodePtr); else return fn(NullType{}); } @@ -400,12 +437,23 @@ class Value return scalarVisitor.result; } - /// Get the string_view of this Value if it has one. std::string_view const* stringViewValue() noexcept { return std::get_if(&value); } + ModelNode::Ptr const* nodePtr() const noexcept + { + return std::get_if(&value); + } + + ModelNode const* node() const noexcept + { + if (auto ptr = nodePtr(); ptr) + return &**ptr; + return nullptr; + } + ValueType type; std::variant< std::monostate, @@ -414,9 +462,8 @@ class Value double, std::string, std::string_view, - TransientObject> value; - - ModelNode::Ptr node; + TransientObject, + ModelNode::Ptr> value; }; template diff --git a/src/completion.cpp b/src/completion.cpp index cf3269e6..2e2bddb5 100644 --- a/src/completion.cpp +++ b/src/completion.cpp @@ -147,7 +147,7 @@ auto CompletionFieldOrWordExpr::ieval(Context ctx, const Value& val, const Resul const auto caseSensitive = comp_->options.smartCase && containsUppercaseCharacter(prefix_); // First we try to complete fields - for (StringId id : val.node->fieldNames()) { + for (StringId id : val.node()->fieldNames()) { if (comp_->size() >= comp_->limit) return Result::Stop; diff --git a/src/expressions.cpp b/src/expressions.cpp index 9179ddf5..0bcb2fbf 100644 --- a/src/expressions.cpp +++ b/src/expressions.cpp @@ -105,7 +105,7 @@ auto WildcardExpr::ieval(Context ctx, const Value& val, const ResultFn& ores) -> } }; - auto r = val.node ? Iterate{ctx, res}.iterate(*val.node) : Result::Continue; + auto r = val.nodePtr() ? Iterate{ctx, res}.iterate(**val.nodePtr()) : Result::Continue; res.ensureCall(); return r; } @@ -137,10 +137,10 @@ auto AnyChildExpr::ieval(Context ctx, const Value& val, const ResultFn& res) -> if (ctx.phase == Context::Phase::Compilation) return res(ctx, Value::undef()); - if (!val.node || !val.node->size()) + if (!val.node() || !val.node()->size()) return res(ctx, Value::null()); - val.node->iterate(ModelNode::IterLambda([&](auto subNode) -> bool { + val.node()->iterate(ModelNode::IterLambda([&](auto subNode) -> bool { auto result = res(ctx, Value::field(std::move(subNode))); if (!result || *result == Result::Stop) { return false; @@ -199,7 +199,7 @@ auto FieldExpr::ieval(Context ctx, const Value& val, const ResultFn& res) -> tl: if (name_ == "_") return res(ctx, val); - if (!val.node) + if (!val.node()) return res(ctx, Value::null()); if (!nameId_) [[unlikely]] { @@ -211,7 +211,7 @@ auto FieldExpr::ieval(Context ctx, const Value& val, const ResultFn& res) -> tl: } /* Enter sub-node */ - if (auto sub = val.node->get(nameId_)) { + if (auto sub = val.node()->get(nameId_)) { hits_++; return res(ctx, Value::field(*sub)); } @@ -339,19 +339,19 @@ auto SubscriptExpr::ieval(Context ctx, const Value& val, const ResultFn& ores) - auto r = left_->eval(ctx, val, LambdaResultFn([this, &val, &res](Context ctx, Value lval) { return index_->eval(ctx, val, LambdaResultFn([this, &res, &lval](Context ctx, const Value& ival) -> tl::expected { /* Field subscript */ - if (lval.node) { + if (lval.node()) { ModelNode::Ptr node; /* Array subscript */ if (ival.isa(ValueType::Int)) { auto index = ival.as(); - node = lval.node->at(index); + node = lval.node()->at(index); } /* String subscript */ else if (ival.isa(ValueType::String)) { auto key = ival.as(); if (auto keyStrId = ctx.env->strings()->get(key)) - node = lval.node->get(keyStrId); + node = lval.node()->get(keyStrId); } if (node) @@ -665,7 +665,7 @@ auto PathExpr::ieval(Context ctx, const Value& val, const ResultFn& ores) -> tl: if (v.isa(ValueType::Undef)) return Result::Continue; - if (v.isa(ValueType::Null) && !v.node) + if (v.isa(ValueType::Null) && !v.node()) return Result::Continue; ++hits_; @@ -674,7 +674,7 @@ auto PathExpr::ieval(Context ctx, const Value& val, const ResultFn& ores) -> tl: if (vv.isa(ValueType::Undef)) return Result::Continue; - if (vv.isa(ValueType::Null) && !vv.node) + if (vv.isa(ValueType::Null) && !vv.node()) return Result::Continue; return res(ctx, std::move(vv)); diff --git a/src/function.cpp b/src/function.cpp index 3a40c38b..abcc8204 100644 --- a/src/function.cpp +++ b/src/function.cpp @@ -554,13 +554,13 @@ auto KeysFn::eval(Context ctx, Value val, const std::vector& args, cons return tl::unexpected(Error::InvalidArguments, fmt::format("'keys' expects 1 argument got {}", args.size())); - auto result = args[0]->eval(ctx, val, LambdaResultFn([&res](Context ctx, Value vv) -> tl::expected { + auto result = args[0]->eval(ctx, val, LambdaResultFn([&res](Context ctx, const Value& vv) -> tl::expected { if (ctx.phase == Context::Phase::Compilation) if (vv.isa(ValueType::Undef)) - return res(ctx, std::move(vv)); + return res(ctx, vv); - if (vv.node) - for (auto&& fieldName : vv.node->fieldNames()) { + if (vv.nodePtr()) + for (auto&& fieldName : vv.node()->fieldNames()) { if (auto key = ctx.env->stringPool->resolve(fieldName)) { if (res(ctx, Value::strref(*key)) == Result::Stop) return Result::Stop; diff --git a/src/overlay.cpp b/src/overlay.cpp index ab0e533d..45358272 100644 --- a/src/overlay.cpp +++ b/src/overlay.cpp @@ -38,30 +38,30 @@ auto OverlayNode::set(StringId const& key, Value const& child) -> void { auto iter = model().overlayChildren_.find(key); if (iter != model().overlayChildren_.end()) { - if (iter->second.node) - return iter->second.node; + if (iter->second.nodePtr()) + return *iter->second.nodePtr(); return ValueNode(iter->second.getScalar()); } - return model().value_.node->get(key); + return model().value_.node()->get(key); } [[nodiscard]] ModelNode::Ptr OverlayNode::at(int64_t i) const { - return model().value_.node->at(i); + return model().value_.node()->at(i); } [[nodiscard]] StringId OverlayNode::keyAt(int64_t i) const { - return model().value_.node->keyAt(i); + return model().value_.node()->keyAt(i); } [[nodiscard]] uint32_t OverlayNode::size() const { - return model().value_.node->size(); + return model().value_.node()->size(); } [[nodiscard]] bool OverlayNode::iterate(IterCallback const& cb) const { - return model().value_.node->iterate(cb); + return model().value_.node()->iterate(cb); } } diff --git a/src/simfil.cpp b/src/simfil.cpp index de22f7cc..02490a94 100644 --- a/src/simfil.cpp +++ b/src/simfil.cpp @@ -159,7 +159,7 @@ static auto simplifyOrForward(Environment* env, expected expr) - auto stub = Context(env, Context::Phase::Compilation); auto res = (*expr)->eval(stub, Value::undef(), LambdaResultFn([&, n = 0](Context ctx, Value vv) mutable { n += 1; - if ((n <= MultiConstExpr::Limit) && (!vv.isa(ValueType::Undef) || vv.node)) { + if ((n <= MultiConstExpr::Limit) && (!vv.isa(ValueType::Undef) || vv.nodePtr())) { values.push_back(std::move(vv)); return Result::Continue; } From f4c15795c32c86c2e2a9e673e2e804f8a10b99d7 Mon Sep 17 00:00:00 2001 From: Johannes Wolf Date: Wed, 24 Sep 2025 00:19:22 +0200 Subject: [PATCH 08/50] expr: Support better forwarding of values --- include/simfil/expression.h | 37 +++++++++++++++++++++----- include/simfil/operator.h | 4 +-- include/simfil/result.h | 10 +++++++ src/expressions.cpp | 52 +++++++++++++++++++++++++++---------- src/expressions.h | 4 +++ src/function.cpp | 16 ++++++------ src/simfil.cpp | 4 +-- 7 files changed, 95 insertions(+), 32 deletions(-) diff --git a/include/simfil/expression.h b/include/simfil/expression.h index 57b8bd61..696d82f2 100644 --- a/include/simfil/expression.h +++ b/include/simfil/expression.h @@ -50,17 +50,35 @@ class Expr /* Debug */ virtual auto toString() const -> std::string = 0; - /* Evaluation wrapper */ - auto eval(Context ctx, Value val, const ResultFn& res) -> tl::expected + auto eval(Context ctx, const Value& val, const ResultFn& res) -> tl::expected { if (ctx.canceled()) return Result::Stop; - auto dbg = ctx.env->debug; - if (dbg) dbg->evalBegin(*this, ctx, val, res); - auto r = ieval(ctx, val, res); - if (dbg) dbg->evalEnd(*this); - return r; + if (auto dbg = ctx.env->debug) [[unlikely]] { + auto dbgVal = Value{val}; + dbg->evalBegin(*this, ctx, dbgVal, res); + auto r = ieval(ctx, std::move(dbgVal), res); + dbg->evalEnd(*this); + return r; + } + + return ieval(ctx, val, res); + } + + auto eval(Context ctx, Value&& val, const ResultFn& res) -> tl::expected + { + if (ctx.canceled()) + return Result::Stop; + + if (auto dbg = ctx.env->debug) [[unlikely]] { + dbg->evalBegin(*this, ctx, val, res); + auto r = ieval(ctx, std::move(val), res); + dbg->evalEnd(*this); + return r; + } + + return ieval(ctx, std::move(val), res); } /* Recursive clone */ @@ -80,6 +98,11 @@ class Expr private: /* Abstract evaluation implementation */ virtual auto ieval(Context ctx, const Value& value, const ResultFn& result) -> tl::expected = 0; + + /* Move-optimized evaluation implementation */ + virtual auto ieval(Context ctx, Value&& value, const ResultFn& result) -> tl::expected { + return ieval(ctx, value, result); + } SourceLocation sourceLocation_; }; diff --git a/include/simfil/operator.h b/include/simfil/operator.h index 0e9b2477..041c93e9 100644 --- a/include/simfil/operator.h +++ b/include/simfil/operator.h @@ -663,8 +663,8 @@ struct UnaryOperatorDispatcher return Value::undef(); } - template - auto operator()(const _Value& rhs) -> tl::expected + template + auto operator()(const Value_& rhs) -> tl::expected { return impl::makeOperatorResult<_Operator>(_Operator()(rhs)); } diff --git a/include/simfil/result.h b/include/simfil/result.h index 92029867..d5055b74 100644 --- a/include/simfil/result.h +++ b/include/simfil/result.h @@ -19,6 +19,7 @@ struct ResultFn virtual ~ResultFn() = default; virtual auto operator()(Context ctx, const Value& value) const noexcept -> tl::expected = 0; + virtual auto operator()(Context ctx, Value&& value) const noexcept -> tl::expected = 0; }; template @@ -31,6 +32,15 @@ struct LambdaResultFn final : ResultFn {} auto operator()(Context ctx, const Value& value) const noexcept -> tl::expected override + { + if constexpr (std::is_invocable_v) { + return lambda(std::move(ctx), value); + } else { + return lambda(std::move(ctx), Value{value}); + } + } + + auto operator()(Context ctx, Value&& value) const noexcept -> tl::expected override { return lambda(std::move(ctx), std::move(value)); } diff --git a/src/expressions.cpp b/src/expressions.cpp index 0bcb2fbf..4ccff9e6 100644 --- a/src/expressions.cpp +++ b/src/expressions.cpp @@ -39,6 +39,13 @@ struct CountedResultFn : ResultFn return fn(ctx, vv); } + auto operator()(Context ctx, Value&& vv) const noexcept -> tl::expected override + { + assert(!finished); + ++calls; + return fn(ctx, std::move(vv)); + } + /* NOTE: You _must_ call finish before destruction! */ auto ensureCall() { @@ -188,16 +195,21 @@ auto FieldExpr::type() const -> Type } auto FieldExpr::ieval(Context ctx, const Value& val, const ResultFn& res) -> tl::expected +{ + return ieval(ctx, Value{val}, res); +} + +auto FieldExpr::ieval(Context ctx, Value&& val, const ResultFn& res) -> tl::expected { if (ctx.phase != Context::Compilation) evaluations_++; if (val.isa(ValueType::Undef)) - return res(ctx, val); + return res(ctx, std::move(val)); /* Special case: _ points to the current node */ if (name_ == "_") - return res(ctx, val); + return res(ctx, std::move(val)); if (!val.node()) return res(ctx, Value::null()); @@ -335,8 +347,7 @@ auto SubscriptExpr::type() const -> Type auto SubscriptExpr::ieval(Context ctx, const Value& val, const ResultFn& ores) -> tl::expected { auto res = CountedResultFn(ores, ctx); - - auto r = left_->eval(ctx, val, LambdaResultFn([this, &val, &res](Context ctx, Value lval) { + auto r = left_->eval(ctx, val, LambdaResultFn([this, &val, &res](Context ctx, const Value& lval) { return index_->eval(ctx, val, LambdaResultFn([this, &res, &lval](Context ctx, const Value& ival) -> tl::expected { /* Field subscript */ if (lval.node()) { @@ -403,6 +414,11 @@ auto SubExpr::type() const -> Type } auto SubExpr::ieval(Context ctx, const Value& val, const ResultFn& ores) -> tl::expected +{ + return ieval(ctx, Value{val}, ores); +} + +auto SubExpr::ieval(Context ctx, Value&& val, const ResultFn& ores) -> tl::expected { /* Do not return null unless we have _no_ matching value. */ auto res = CountedResultFn(ores, ctx); @@ -591,6 +607,11 @@ auto CallExpression::type() const -> Type } auto CallExpression::ieval(Context ctx, const Value& val, const ResultFn& res) -> tl::expected +{ + return ieval(ctx, Value{val}, res); +} + +auto CallExpression::ieval(Context ctx, Value&& val, const ResultFn& res) -> tl::expected { if (!fn_) [[unlikely]] { fn_ = ctx.env->findFunction(name_); @@ -599,7 +620,7 @@ auto CallExpression::ieval(Context ctx, const Value& val, const ResultFn& res) - } auto anyval = false; - auto result = fn_->eval(ctx, val, args_, LambdaResultFn([&res, &anyval](const Context& ctx, Value vv) { + auto result = fn_->eval(ctx, std::move(val), args_, LambdaResultFn([&res, &anyval](const Context& ctx, Value&& vv) { anyval = true; return res(ctx, std::move(vv)); })); @@ -658,10 +679,15 @@ auto PathExpr::type() const -> Type } auto PathExpr::ieval(Context ctx, const Value& val, const ResultFn& ores) -> tl::expected +{ + return ieval(ctx, Value{val}, ores); +} + +auto PathExpr::ieval(Context ctx, Value&& val, const ResultFn& ores) -> tl::expected { auto res = CountedResultFn(ores, ctx); - auto r = left_->eval(ctx, val, LambdaResultFn([this, &res](Context ctx, Value v) -> tl::expected { + auto r = left_->eval(ctx, std::move(val), LambdaResultFn([this, &res](Context ctx, Value&& v) -> tl::expected { if (v.isa(ValueType::Undef)) return Result::Continue; @@ -670,7 +696,7 @@ auto PathExpr::ieval(Context ctx, const Value& val, const ResultFn& ores) -> tl: ++hits_; - return right_->eval(ctx, std::move(v), LambdaResultFn([this, &res](Context ctx, Value vv) -> tl::expected { + return right_->eval(ctx, std::move(v), LambdaResultFn([this, &res](Context ctx, Value&& vv) -> tl::expected { if (vv.isa(ValueType::Undef)) return Result::Continue; @@ -711,7 +737,7 @@ auto UnpackExpr::type() const -> Type auto UnpackExpr::ieval(Context ctx, const Value& val, const ResultFn& res) -> tl::expected { auto anyval = false; - auto r = sub_->eval(ctx, val, LambdaResultFn([&res, &anyval](Context ctx, Value v) { + auto r = sub_->eval(ctx, val, LambdaResultFn([&res, &anyval](Context ctx, Value&& v) { if (v.isa(ValueType::TransientObject)) { const auto& obj = v.as(); auto r = Result::Continue; @@ -762,7 +788,7 @@ auto UnaryWordOpExpr::type() const -> Type auto UnaryWordOpExpr::ieval(Context ctx, const Value& val, const ResultFn& res) -> tl::expected { - return left_->eval(ctx, val, LambdaResultFn([this, &res](const Context& ctx, Value val) -> tl::expected { + return left_->eval(ctx, val, LambdaResultFn([this, &res](const Context& ctx, Value&& val) -> tl::expected { if (val.isa(ValueType::Undef)) return res(ctx, std::move(val)); @@ -867,7 +893,7 @@ auto AndExpr::ieval(Context ctx, const Value& val, const ResultFn& res) -> tl::e { /* Operator and behaves like in lua: * 'a and b' returns a if 'not a?' else b is returned */ - return left_->eval(ctx, val, LambdaResultFn([this, &res, &val](const Context& ctx, Value lval) -> tl::expected { + return left_->eval(ctx, val, LambdaResultFn([this, &res, &val](const Context& ctx, Value&& lval) -> tl::expected { if (lval.isa(ValueType::Undef)) return res(ctx, lval); @@ -879,7 +905,7 @@ auto AndExpr::ieval(Context ctx, const Value& val, const ResultFn& res) -> tl::e if (!v->template as()) return res(ctx, std::move(lval)); - return right_->eval(ctx, val, LambdaResultFn([&res](const Context& ctx, Value rval) { + return right_->eval(ctx, val, LambdaResultFn([&res](const Context& ctx, Value&& rval) { return res(ctx, std::move(rval)); })); })); @@ -917,7 +943,7 @@ auto OrExpr::ieval(Context ctx, const Value& val, const ResultFn& res) -> tl::ex { /* Operator or behaves like in lua: * 'a or b' returns a if 'a?' else b is returned */ - return left_->eval(ctx, val, LambdaResultFn([this, &res, &val](Context ctx, Value lval) -> tl::expected { + return left_->eval(ctx, val, LambdaResultFn([this, &res, &val](Context ctx, Value&& lval) -> tl::expected { if (lval.isa(ValueType::Undef)) return res(ctx, lval); @@ -932,7 +958,7 @@ auto OrExpr::ieval(Context ctx, const Value& val, const ResultFn& res) -> tl::ex return res(ctx, std::move(lval)); ++rightEvaluations_; - return right_->eval(ctx, val, LambdaResultFn([&](Context ctx, Value rval) { + return right_->eval(ctx, val, LambdaResultFn([&](Context ctx, Value&& rval) { return res(ctx, std::move(rval)); })); })); diff --git a/src/expressions.h b/src/expressions.h index 552c9c94..c375feb5 100644 --- a/src/expressions.h +++ b/src/expressions.h @@ -105,6 +105,7 @@ class FieldExpr : public Expr auto type() const -> Type override; auto ieval(Context ctx, const Value& val, const ResultFn& res) -> tl::expected override; + auto ieval(Context ctx, Value&& val, const ResultFn& res) -> tl::expected override; auto clone() const -> ExprPtr override; void accept(ExprVisitor& v) override; auto toString() const -> std::string override; @@ -178,6 +179,7 @@ class SubExpr : public Expr auto type() const -> Type override; auto ieval(Context ctx, const Value& val, const ResultFn& ores) -> tl::expected override; + auto ieval(Context ctx, Value&& val, const ResultFn& ores) -> tl::expected override; auto toString() const -> std::string override; auto clone() const -> ExprPtr override; void accept(ExprVisitor& v) override; @@ -228,6 +230,7 @@ class CallExpression : public Expr auto type() const -> Type override; auto ieval(Context ctx, const Value& val, const ResultFn& res) -> tl::expected override; + auto ieval(Context ctx, Value&& val, const ResultFn& res) -> tl::expected override; auto clone() const -> ExprPtr override; void accept(ExprVisitor& v) override; auto toString() const -> std::string override; @@ -245,6 +248,7 @@ class PathExpr : public Expr auto type() const -> Type override; auto ieval(Context ctx, const Value& val, const ResultFn& ores) -> tl::expected override; + auto ieval(Context ctx, Value&& val, const ResultFn& ores) -> tl::expected override; auto clone() const -> ExprPtr override; void accept(ExprVisitor& v) override; auto toString() const -> std::string override; diff --git a/src/function.cpp b/src/function.cpp index abcc8204..2146dc9d 100644 --- a/src/function.cpp +++ b/src/function.cpp @@ -47,7 +47,7 @@ struct ArgParser } auto subctx = ctx; - auto res = args[idx]->eval(subctx, value, LambdaResultFn([&, n = 0](Context, Value vv) mutable -> tl::expected { + auto res = args[idx]->eval(subctx, value, LambdaResultFn([&, n = 0](Context, Value&& vv) mutable -> tl::expected { if (++n > 1) [[unlikely]] { return tl::unexpected(Error::ExpectedSingleValue, fmt::format("expeted single argument value for argument {} for function {}", name, functionName)); @@ -84,7 +84,7 @@ struct ArgParser } auto subctx = ctx; - auto res = args[idx]->eval(subctx, value, LambdaResultFn([&, n = 0](Context, Value vv) mutable -> tl::expected { + auto res = args[idx]->eval(subctx, value, LambdaResultFn([&, n = 0](Context, Value&& vv) mutable -> tl::expected { if (++n > 1) { return tl::unexpected(Error::ExpectedSingleValue, fmt::format("{}: argument {} must return a single value", functionName, name)); @@ -291,7 +291,7 @@ auto ReFn::eval(Context ctx, Value val, const std::vector& args, const fmt::format("'re' expects 1 argument, got {}", args.size())); auto subctx = ctx; - return args[0]->eval(subctx, val, LambdaResultFn([&](Context, Value vv) -> tl::expected { + return args[0]->eval(subctx, val, LambdaResultFn([&](Context, Value&& vv) -> tl::expected { if (vv.isa(ValueType::Undef)) return res(ctx, Value::undef()); @@ -328,7 +328,7 @@ auto ArrFn::eval(Context ctx, Value val, const std::vector& args, const return res(ctx, Value::null()); for (const auto& arg : args) { - if (auto r = arg->eval(ctx, val, LambdaResultFn([&res](Context ctx, Value vv) { + if (auto r = arg->eval(ctx, val, LambdaResultFn([&res](Context ctx, Value&& vv) { return res(ctx, std::move(vv)); })); r && *r == Result::Stop) return Result::Stop; @@ -460,7 +460,7 @@ auto SelectFn::eval(Context ctx, Value val, const std::vector& args, co if (icnt <= 0) icnt = std::numeric_limits::max(); - auto result = args[0]->eval(ctx, val, LambdaResultFn([&, n = -1](Context ctx, Value vv) mutable -> tl::expected { + auto result = args[0]->eval(ctx, val, LambdaResultFn([&, n = -1](Context ctx, Value&& vv) mutable -> tl::expected { ++n; if (ctx.phase == Context::Phase::Compilation) if (vv.isa(ValueType::Undef)) @@ -499,12 +499,12 @@ auto SumFn::eval(Context ctx, Value val, const std::vector& args, const Expr* subexpr = args.size() >= 2 ? args[1].get() : nullptr; Expr* initval = args.size() == 3 ? args[2].get() : nullptr; if (initval) - (void)initval->eval(ctx, val, LambdaResultFn([&](Context ctx, Value vv) { + (void)initval->eval(ctx, val, LambdaResultFn([&](Context ctx, Value&& vv) { sum = std::move(vv); return Result::Continue; })); - (void)args[0]->eval(ctx, val, LambdaResultFn([&, n = 0](Context ctx, Value vv) mutable -> tl::expected { + (void)args[0]->eval(ctx, val, LambdaResultFn([&, n = 0](Context ctx, Value&& vv) mutable -> tl::expected { if (subexpr) { auto ov = model_ptr::make(vv); ov->set(StringPool::OverlaySum, sum); @@ -512,7 +512,7 @@ auto SumFn::eval(Context ctx, Value val, const std::vector& args, const ov->set(StringPool::OverlayIndex, Value::make(static_cast(n))); n += 1; - subexpr->eval(ctx, Value::field(ov), LambdaResultFn([&ov, &sum](auto ctx, auto vv) { + subexpr->eval(ctx, Value::field(ov), LambdaResultFn([&ov, &sum](auto ctx, Value&& vv) { ov->set(StringPool::OverlaySum, vv); sum = vv; return Result::Continue; diff --git a/src/simfil.cpp b/src/simfil.cpp index 02490a94..5c4e3e6f 100644 --- a/src/simfil.cpp +++ b/src/simfil.cpp @@ -157,7 +157,7 @@ static auto simplifyOrForward(Environment* env, expected expr) - std::deque values; auto stub = Context(env, Context::Phase::Compilation); - auto res = (*expr)->eval(stub, Value::undef(), LambdaResultFn([&, n = 0](Context ctx, Value vv) mutable { + auto res = (*expr)->eval(stub, Value::undef(), LambdaResultFn([&, n = 0](Context ctx, Value&& vv) mutable { n += 1; if ((n <= MultiConstExpr::Limit) && (!vv.isa(ValueType::Undef) || vv.nodePtr())) { values.push_back(std::move(vv)); @@ -875,7 +875,7 @@ auto eval(Environment& env, const AST& ast, const ModelNode& node, Diagnostics* auto mutableAST = ast.expr().clone(); std::vector values; - auto res = mutableAST->eval(ctx, Value::field(node), LambdaResultFn([&values](Context, Value value) { + auto res = mutableAST->eval(ctx, Value::field(node), LambdaResultFn([&values](Context, Value&& value) { values.push_back(std::move(value)); return Result::Continue; })); From 09df80c011c6dba9058c5480c8564a9c17ed6f12 Mon Sep 17 00:00:00 2001 From: Johannes Wolf Date: Thu, 25 Sep 2025 14:44:55 +0200 Subject: [PATCH 09/50] function: Take values by reference --- include/simfil/function.h | 67 +++++++-------------------------------- include/simfil/result.h | 4 --- src/function.cpp | 20 ++++++------ test/common.hpp | 2 +- 4 files changed, 23 insertions(+), 70 deletions(-) diff --git a/include/simfil/function.h b/include/simfil/function.h index 9a84e575..19265b1d 100644 --- a/include/simfil/function.h +++ b/include/simfil/function.h @@ -14,49 +14,6 @@ namespace simfil { struct Context; -struct ArgumentCountError : std::exception -{ - const Function* fn; - size_t min; - size_t max; - size_t have; - - ArgumentCountError(const Function& fn, size_t min, size_t max, size_t have) - : fn(&fn), min(min), max(max), have(have) - {} - - mutable std::string msg; - auto what() const noexcept -> const char* override; -}; - -struct ArgumentTypeError : std::exception -{ - const Function* fn; - size_t index; - std::string want; - std::string have; - - ArgumentTypeError(const Function& fn, size_t index, std::string want, std::string have) - : fn(&fn), index(index), want(std::move(want)), have(std::move(have)) - {} - - mutable std::string msg; - auto what() const noexcept -> const char* override; -}; - -struct ArgumentValueCountError : std::exception -{ - const Function* fn; - size_t index; - - ArgumentValueCountError(const Function& fn, size_t index) - : fn(&fn), index(index) - {} - - mutable std::string msg; - auto what() const noexcept -> const char* override; -}; - /** * Function info. */ @@ -76,7 +33,7 @@ class Function virtual ~Function() = default; virtual auto ident() const -> const FnInfo& = 0; - virtual auto eval(Context, Value, const std::vector&, const ResultFn&) const -> tl::expected = 0; + virtual auto eval(Context, const Value&, const std::vector&, const ResultFn&) const -> tl::expected = 0; }; class CountFn : public Function @@ -87,7 +44,7 @@ class CountFn : public Function CountFn(); auto ident() const -> const FnInfo& override; - auto eval(Context, Value, const std::vector&, const ResultFn&) const -> tl::expected override; + auto eval(Context, const Value&, const std::vector&, const ResultFn&) const -> tl::expected override; }; class TraceFn : public Function @@ -98,7 +55,7 @@ class TraceFn : public Function TraceFn(); auto ident() const -> const FnInfo& override; - auto eval(Context, Value, const std::vector&, const ResultFn&) const -> tl::expected override; + auto eval(Context, const Value&, const std::vector&, const ResultFn&) const -> tl::expected override; }; class RangeFn : public Function @@ -109,7 +66,7 @@ class RangeFn : public Function RangeFn(); auto ident() const -> const FnInfo& override; - auto eval(Context, Value, const std::vector&, const ResultFn&) const -> tl::expected override; + auto eval(Context, const Value&, const std::vector&, const ResultFn&) const -> tl::expected override; }; class ReFn : public Function @@ -120,7 +77,7 @@ class ReFn : public Function ReFn(); auto ident() const -> const FnInfo& override; - auto eval(Context, Value, const std::vector&, const ResultFn&) const -> tl::expected override; + auto eval(Context, const Value&, const std::vector&, const ResultFn&) const -> tl::expected override; }; class ArrFn : public Function @@ -131,7 +88,7 @@ class ArrFn : public Function ArrFn(); auto ident() const -> const FnInfo& override; - auto eval(Context, Value, const std::vector&, const ResultFn&) const -> tl::expected override; + auto eval(Context, const Value&, const std::vector&, const ResultFn&) const -> tl::expected override; }; class SplitFn : public Function @@ -142,7 +99,7 @@ class SplitFn : public Function SplitFn(); auto ident() const -> const FnInfo& override; - auto eval(Context, Value, const std::vector&, const ResultFn&) const -> tl::expected override; + auto eval(Context, const Value&, const std::vector&, const ResultFn&) const -> tl::expected override; }; class SelectFn : public Function @@ -153,7 +110,7 @@ class SelectFn : public Function SelectFn(); auto ident() const -> const FnInfo& override; - auto eval(Context, Value, const std::vector&, const ResultFn&) const -> tl::expected override; + auto eval(Context, const Value&, const std::vector&, const ResultFn&) const -> tl::expected override; }; class SumFn : public Function @@ -164,7 +121,7 @@ class SumFn : public Function SumFn(); auto ident() const -> const FnInfo& override; - auto eval(Context, Value, const std::vector&, const ResultFn&) const -> tl::expected override; + auto eval(Context, const Value&, const std::vector&, const ResultFn&) const -> tl::expected override; }; class KeysFn : public Function @@ -175,14 +132,14 @@ class KeysFn : public Function KeysFn(); auto ident() const -> const FnInfo& override; - auto eval(Context, Value, const std::vector&, const ResultFn&) const -> tl::expected override; + auto eval(Context, const Value&, const std::vector&, const ResultFn&) const -> tl::expected override; }; /** Utility functions for working with arguments*/ namespace util { -inline auto evalArg1Any(Context ctx, Value val, const ExprPtr& expr) -> std::tuple +inline auto evalArg1Any(Context ctx, const Value& val, const ExprPtr& expr) -> std::tuple { if (!expr) return {false, Value::undef()}; @@ -198,7 +155,7 @@ inline auto evalArg1Any(Context ctx, Value val, const ExprPtr& expr) -> std::tup } template -auto evalArg1(Context ctx, Value val, const ExprPtr& expr, CType fallback = {}) -> std::tuple +auto evalArg1(Context ctx, const Value& val, const ExprPtr& expr, CType fallback = {}) -> std::tuple { auto&& [ok, value] = evalArg1Any(ctx, val, expr); if (ok) diff --git a/include/simfil/result.h b/include/simfil/result.h index d5055b74..ce4e9891 100644 --- a/include/simfil/result.h +++ b/include/simfil/result.h @@ -8,10 +8,6 @@ namespace simfil { -/** - * Result value callback. - * Return `false` to stop evaluation. - */ enum Result { Continue = 1, Stop = 0 }; struct ResultFn diff --git a/src/function.cpp b/src/function.cpp index 2146dc9d..2a21f1dd 100644 --- a/src/function.cpp +++ b/src/function.cpp @@ -42,7 +42,7 @@ struct ArgParser auto arg(const char* name, ValueType type, Value& outValue) -> ArgParser& { if (args.size() <= idx) { - error = Error(Error::InvalidArguments, fmt::format("missing argumnt {} for function {}", name, this->functionName)); + error = Error(Error::InvalidArguments, fmt::format("missing argument {} for function {}", name, this->functionName)); return *this; } @@ -142,7 +142,7 @@ auto CountFn::ident() const -> const FnInfo& return info; } -auto CountFn::eval(Context ctx, Value val, const std::vector& args, const ResultFn& res) const -> tl::expected +auto CountFn::eval(Context ctx, const Value& val, const std::vector& args, const ResultFn& res) const -> tl::expected { if (args.empty()) return tl::unexpected(Error::InvalidArguments, @@ -186,7 +186,7 @@ auto TraceFn::ident() const -> const FnInfo& return info; } -auto TraceFn::eval(Context ctx, Value val, const std::vector& args, const ResultFn& res) const -> tl::expected +auto TraceFn::eval(Context ctx, const Value& val, const std::vector& args, const ResultFn& res) const -> tl::expected { Value name = Value::undef(); Value limit = Value::undef(); @@ -248,7 +248,7 @@ auto RangeFn::ident() const -> const FnInfo& return info; } -auto RangeFn::eval(Context ctx, Value val, const std::vector& args, const ResultFn& res) const -> tl::expected +auto RangeFn::eval(Context ctx, const Value& val, const std::vector& args, const ResultFn& res) const -> tl::expected { if (args.size() != 2) return tl::unexpected(Error::InvalidArguments, @@ -284,7 +284,7 @@ auto ReFn::ident() const -> const FnInfo& return info; } -auto ReFn::eval(Context ctx, Value val, const std::vector& args, const ResultFn& res) const -> tl::expected +auto ReFn::eval(Context ctx, const Value& val, const std::vector& args, const ResultFn& res) const -> tl::expected { if (args.size() != 1) return tl::unexpected(Error::InvalidArguments, @@ -322,7 +322,7 @@ auto ArrFn::ident() const -> const FnInfo& } -auto ArrFn::eval(Context ctx, Value val, const std::vector& args, const ResultFn& res) const -> tl::expected +auto ArrFn::eval(Context ctx, const Value& val, const std::vector& args, const ResultFn& res) const -> tl::expected { if (args.empty()) return res(ctx, Value::null()); @@ -397,7 +397,7 @@ ContainerType split(std::string_view what, } } -auto SplitFn::eval(Context ctx, Value val, const std::vector& args, const ResultFn& res) const -> tl::expected +auto SplitFn::eval(Context ctx, const Value& val, const std::vector& args, const ResultFn& res) const -> tl::expected { Value str = Value::undef(); Value sep = Value::undef(); @@ -439,7 +439,7 @@ auto SelectFn::ident() const -> const FnInfo& return info; } -auto SelectFn::eval(Context ctx, Value val, const std::vector& args, const ResultFn& res) const -> tl::expected +auto SelectFn::eval(Context ctx, const Value& val, const std::vector& args, const ResultFn& res) const -> tl::expected { Value idx = Value::undef(); Value cnt = Value::undef(); @@ -488,7 +488,7 @@ auto SumFn::ident() const -> const FnInfo& return info; } -auto SumFn::eval(Context ctx, Value val, const std::vector& args, const ResultFn& res) const -> tl::expected +auto SumFn::eval(Context ctx, const Value& val, const std::vector& args, const ResultFn& res) const -> tl::expected { if (args.empty() || args.size() > 3) return tl::unexpected(Error::InvalidArguments, @@ -548,7 +548,7 @@ auto KeysFn::ident() const -> const FnInfo& return info; } -auto KeysFn::eval(Context ctx, Value val, const std::vector& args, const ResultFn& res) const -> tl::expected +auto KeysFn::eval(Context ctx, const Value& val, const std::vector& args, const ResultFn& res) const -> tl::expected { if (args.size() != 1) return tl::unexpected(Error::InvalidArguments, diff --git a/test/common.hpp b/test/common.hpp index 585b52c1..deeff9b0 100644 --- a/test/common.hpp +++ b/test/common.hpp @@ -70,7 +70,7 @@ class PanicFn : public simfil::Function return info; } - auto eval(Context ctx, Value, const std::vector&, const ResultFn& res) const -> tl::expected override + auto eval(Context ctx, const Value&, const std::vector&, const ResultFn& res) const -> tl::expected override { if (ctx.phase != Context::Phase::Compilation) return tl::unexpected(Error::RuntimeError, "Panic!"); From 92565c24902537834bf02891e3288c87403f2245 Mon Sep 17 00:00:00 2001 From: Johannes Wolf Date: Thu, 25 Sep 2025 14:48:16 +0200 Subject: [PATCH 10/50] tests: Add benchmark that calls a function --- test/performance.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/performance.cpp b/test/performance.cpp index 5f9261df..1c8d7d35 100644 --- a/test/performance.cpp +++ b/test/performance.cpp @@ -155,6 +155,10 @@ TEST_CASE("Big model query performance", "[perf.big-model-benchmark]") { BENCHMARK("Query field id") { return result(model, "count(*.id == 25)"); }; + + BENCHMARK("Query field keys recursive") { + return result(model, "count(keys(**))"); + }; } #define REQUIRE_RESULT(query, result) \ From e7f18b07f838426829ab2074924328d6d158b478 Mon Sep 17 00:00:00 2001 From: Johannes Wolf Date: Thu, 25 Sep 2025 15:04:15 +0200 Subject: [PATCH 11/50] operator: Use string_view if possible --- include/simfil/operator.h | 68 +++++++++++++++++++-------------------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/include/simfil/operator.h b/include/simfil/operator.h index 041c93e9..c98a7e6e 100644 --- a/include/simfil/operator.h +++ b/include/simfil/operator.h @@ -11,12 +11,12 @@ #include #include #include -#include namespace simfil { using namespace std::string_literals; +using namespace std::string_view_literals; /** * Special return type for operator type mismatch. @@ -102,8 +102,8 @@ struct OperatorNot { NAME("not") - template - auto operator()(const _Type& value) const + template + auto operator()(const Type_& value) const { return !OperatorBool()(value); } @@ -140,46 +140,46 @@ struct OperatorTypeof { NAME("typeof") - auto operator()(NullType) const -> const std::string& + auto operator()(NullType) const -> std::string_view { - static auto n = "null"s; + static auto n = "null"sv; return n; } - auto operator()(bool) const -> const std::string& + auto operator()(bool) const -> std::string_view { - static auto n = "bool"s; + static auto n = "bool"sv; return n; } - auto operator()(int64_t) const -> const std::string& + auto operator()(int64_t) const -> std::string_view { - static auto n = "int"s; + static auto n = "int"sv; return n; } - auto operator()(double) const -> const std::string& + auto operator()(double) const -> std::string_view { - static auto n = "float"s; + static auto n = "float"sv; return n; } - auto operator()(const std::string&) const -> const std::string& + auto operator()(const std::string&) -> std::string_view { - static auto n = "string"s; + static auto n = "string"sv; return n; } - auto operator()(const ModelNode& v) const -> const std::string& + auto operator()(const ModelNode& v) -> std::string_view { - static auto n = "model"s; + static auto n = "model"sv; return n; } - auto operator()(const TransientObject& v) const + auto operator()(const TransientObject& v) -> std::string_view { // Handled by MetaType::unaryOp - return ""s; + return ""sv; } }; @@ -431,21 +431,21 @@ struct OperatorDiv { if (r == 0) return tl::unexpected(Error::DivisionByZero, "Division by zero"); - return static_cast(l / r); + return static_cast(l) / r; } auto operator()(double l, int64_t r) const -> tl::expected { if (r == 0) return tl::unexpected(Error::DivisionByZero, "Division by zero"); - return static_cast(l / r); + return l / static_cast(r); } auto operator()(double l, double r) const -> tl::expected { if (r == 0) return tl::unexpected(Error::DivisionByZero, "Division by zero"); - return static_cast(l / r); + return l / r; } }; @@ -617,42 +617,42 @@ struct OperatorShr namespace impl { -template -inline auto makeOperatorResult(_CType&& value) -> tl::expected +template +inline auto makeOperatorResult(CType_&& value) -> tl::expected { - return Value::make(std::forward<_CType>(value)); + return Value::make(std::forward(value)); } -template +template inline auto makeOperatorResult(Value value) -> tl::expected { return value; } -template -inline auto makeOperatorResult(tl::expected<_CType, Error> value) -> tl::expected +template +inline auto makeOperatorResult(tl::expected value) -> tl::expected { if (!value) return tl::unexpected(std::move(value.error())); - return Value::make(std::forward<_CType>(std::move(value.value()))); + return Value::make(std::forward(std::move(value.value()))); } -template +template inline auto makeOperatorResult(InvalidOperands) -> tl::expected { - return tl::unexpected(Error::InvalidOperands, fmt::format("Invalid operands for operator '{}'", _Operator::name())); + return tl::unexpected(Error::InvalidOperands, fmt::format("Invalid operands for operator '{}'", Operator_::name())); } } -template +template struct UnaryOperatorDispatcher { static auto dispatch(const Value& value) -> tl::expected { if (value.isa(ValueType::TransientObject)) { const auto& obj = value.as(); - return obj.meta->unaryOp(_Operator::name(), obj); + return obj.meta->unaryOp(Operator_::name(), obj); } return value.visit(UnaryOperatorDispatcher()); @@ -666,11 +666,11 @@ struct UnaryOperatorDispatcher template auto operator()(const Value_& rhs) -> tl::expected { - return impl::makeOperatorResult<_Operator>(_Operator()(rhs)); + return impl::makeOperatorResult(Operator_()(rhs)); } }; -template +template struct BinaryOperatorDispatcherRHS { const Left& lhs; @@ -686,7 +686,7 @@ struct BinaryOperatorDispatcherRHS template auto operator()(const Right& rhs) -> tl::expected { - return impl::makeOperatorResult(Operator()(lhs, rhs)); + return impl::makeOperatorResult(Operator_()(lhs, rhs)); } }; From a903a4987c0e7b28c0d9c16ec89a30f64118f249 Mon Sep 17 00:00:00 2001 From: Johannes Wolf Date: Thu, 25 Sep 2025 15:07:29 +0200 Subject: [PATCH 12/50] model: Remove bad raise The tree contains noexcept function, so we replace the raise with an assert(). --- include/simfil/model/nodes.h | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/include/simfil/model/nodes.h b/include/simfil/model/nodes.h index 0516af64..36a2c1c9 100644 --- a/include/simfil/model/nodes.h +++ b/include/simfil/model/nodes.h @@ -109,29 +109,23 @@ struct model_ptr return model_ptr(std::in_place, std::forward(args)...); } - inline void ensureModelIsNotNull() const { - if (!data_.model_ || !data_.addr_) { - raise("Attempt to dereference null model_ptr!"); - } - } - inline T& operator* () { - ensureModelIsNotNull(); + assert(data_.model_ && data_.addr_); return data_; } inline T* operator-> () { - ensureModelIsNotNull(); + assert(data_.model_ && data_.addr_); return &data_; } inline T const& operator* () const { - ensureModelIsNotNull(); + assert(data_.model_ && data_.addr_); return data_; } inline T const* operator-> () const { - ensureModelIsNotNull(); + assert(data_.model_ && data_.addr_); return &data_; } From 2edfd74bfd428b499cc9c3c2614ca97c640c205c Mon Sep 17 00:00:00 2001 From: Johannes Wolf Date: Thu, 25 Sep 2025 20:14:24 +0200 Subject: [PATCH 13/50] Remove std::function from scoped helper --- src/simfil.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/simfil.cpp b/src/simfil.cpp index 5c4e3e6f..3f108b9f 100644 --- a/src/simfil.cpp +++ b/src/simfil.cpp @@ -120,14 +120,15 @@ static auto isSymbolWord(std::string_view sv) -> bool /** * RIIA Helper for calling function at destruction. */ +template struct scoped { - std::function f; + Fun f; - explicit scoped(std::function f) : f(std::move(f)) {} + explicit scoped(Fun f) : f(std::move(f)) {} scoped(scoped&& s) noexcept : f(std::move(s.f)) { s.f = nullptr; } scoped(const scoped& s) = delete; ~scoped() { - try { if (f) { f(); } } catch (...) {} + f(); } }; From 95730f2ede0c82cfcac1d458f2d477f61b62650d Mon Sep 17 00:00:00 2001 From: Johannes Wolf Date: Mon, 29 Sep 2025 16:12:16 +0200 Subject: [PATCH 14/50] tree: Forward ignored errors --- include/simfil/model/arena.h | 6 ++++-- src/expressions.cpp | 9 +++++---- src/function.cpp | 4 +++- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/include/simfil/model/arena.h b/include/simfil/model/arena.h index 9a18d70f..92ed45f6 100644 --- a/include/simfil/model/arena.h +++ b/include/simfil/model/arena.h @@ -211,9 +211,11 @@ class ArrayArena ArrayIterator(ArrayArenaRef arena, ArrayIndex array_index, size_t elem_index) : arena_(arena), array_index_(array_index), elem_index_(elem_index) {} - ElementRef operator*() { + ElementRef operator*() noexcept { + auto res = arena_.at(array_index_, elem_index_); + assert(res); // Unchecked access! - return *arena_.at(array_index_, elem_index_); + return *res; } ArrayIterator& operator++() { diff --git a/src/expressions.cpp b/src/expressions.cpp index 4ccff9e6..7b2920fb 100644 --- a/src/expressions.cpp +++ b/src/expressions.cpp @@ -6,6 +6,7 @@ #include "simfil/function.h" #include "fmt/core.h" +#include "src/expected.h" namespace simfil { @@ -473,7 +474,7 @@ auto AnyExpr::ieval(Context ctx, const Value& val, const ResultFn& res) -> tl::e auto undef = false; /* At least one value is undef */ for (const auto& arg : args_) { - arg->eval(ctx, val, LambdaResultFn([&](Context, const Value& vv) { + auto res = arg->eval(ctx, val, LambdaResultFn([&](Context, const Value& vv) { if (ctx.phase == Context::Phase::Compilation) { if (vv.isa(ValueType::Undef)) { undef = true; @@ -484,7 +485,7 @@ auto AnyExpr::ieval(Context ctx, const Value& val, const ResultFn& res) -> tl::e result = result || boolify(vv); return result ? Result::Stop : Result::Continue; })); - + TRY_EXPECTED(res); if (result || undef) break; } @@ -543,7 +544,7 @@ auto EachExpr::ieval(Context ctx, const Value& val, const ResultFn& res) -> tl:: auto undef = false; /* At least one value is undef */ for (const auto& arg : args_) { - arg->eval(ctx, val, LambdaResultFn([&](Context, const Value& vv) { + auto res = arg->eval(ctx, val, LambdaResultFn([&](Context, const Value& vv) { if (ctx.phase == Context::Phase::Compilation) { if (vv.isa(ValueType::Undef)) { undef = true; @@ -553,7 +554,7 @@ auto EachExpr::ieval(Context ctx, const Value& val, const ResultFn& res) -> tl:: result = result && boolify(vv); return result ? Result::Continue : Result::Stop; })); - + TRY_EXPECTED(res); if (!result || undef) break; } diff --git a/src/function.cpp b/src/function.cpp index 2a21f1dd..5923fe6b 100644 --- a/src/function.cpp +++ b/src/function.cpp @@ -9,6 +9,7 @@ #include "simfil/overlay.h" #include "fmt/core.h" #include "tl/expected.hpp" +#include "expected.h" #include @@ -153,7 +154,7 @@ auto CountFn::eval(Context ctx, const Value& val, const std::vector& ar int64_t count = 0; for (const auto& arg : args) { - arg->eval(ctx, val, LambdaResultFn([&](Context, const Value& vv) { + auto res = arg->eval(ctx, val, LambdaResultFn([&](Context, const Value& vv) { if (ctx.phase == Context::Phase::Compilation) { if (vv.isa(ValueType::Undef)) { undef = true; @@ -163,6 +164,7 @@ auto CountFn::eval(Context ctx, const Value& val, const std::vector& ar count += boolify(vv) ? 1 : 0; return Result::Continue; })); + TRY_EXPECTED(res); if (undef) break; From ef9c17d871e0f5ac4a2e92991919556f1e666475 Mon Sep 17 00:00:00 2001 From: Johannes Wolf Date: Mon, 6 Oct 2025 19:30:29 +0200 Subject: [PATCH 15/50] completion: Fix SEGFAULT --- src/completion.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/completion.cpp b/src/completion.cpp index 2e2bddb5..7da478c5 100644 --- a/src/completion.cpp +++ b/src/completion.cpp @@ -144,10 +144,14 @@ auto CompletionFieldOrWordExpr::ieval(Context ctx, const Value& val, const Resul if (val.isa(ValueType::Undef)) return res(ctx, val); + const auto node = val.node(); + if (!node) + return Result::Stop; + const auto caseSensitive = comp_->options.smartCase && containsUppercaseCharacter(prefix_); // First we try to complete fields - for (StringId id : val.node()->fieldNames()) { + for (StringId id : node->fieldNames()) { if (comp_->size() >= comp_->limit) return Result::Stop; From 6ac2d4cb54f8820ba4879bd3b0e9488665af37d6 Mon Sep 17 00:00:00 2001 From: Johannes Wolf Date: Mon, 6 Oct 2025 20:43:20 +0200 Subject: [PATCH 16/50] irange: Fix invalid expected access --- src/types.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/types.cpp b/src/types.cpp index 24d41cf1..ca217100 100644 --- a/src/types.cpp +++ b/src/types.cpp @@ -44,9 +44,9 @@ auto IRangeType::binaryOp(std::string_view op, const IRange& l, const Value& r) /* Range ==/!= operator checks if the other operand is _in_ the range (for int/float) */ if (op == OperatorNeq::name()) { auto res = binaryOp(OperatorEq::name(), l, r); - if (res->isa(ValueType::Bool)) + if (res && res->isa(ValueType::Bool)) return Value::make(!res->template as()); - assert(0); + return res; } if (op == OperatorEq::name()) { From e6997bf7cee335383987dba62ea2755b15713108 Mon Sep 17 00:00:00 2001 From: Johannes Wolf Date: Mon, 6 Oct 2025 22:08:04 +0200 Subject: [PATCH 17/50] operator: Fix bitshift operator --- src/token.cpp | 2 +- test/simfil.cpp | 46 +++++++++++++++++++++++++++++++++++++++++++++- test/token.cpp | 1 + 3 files changed, 47 insertions(+), 2 deletions(-) diff --git a/src/token.cpp b/src/token.cpp index 32bfe0ee..cefbdc0a 100644 --- a/src/token.cpp +++ b/src/token.cpp @@ -435,7 +435,7 @@ std::optional scanSyntax(Scanner& s) {"/", Token::OP_DIV}, {"%", Token::OP_MOD}, {"<<",Token::OP_LSHIFT}, - {">>",Token::OP_LSHIFT}, + {">>",Token::OP_RSHIFT}, {"~", Token::OP_BITINV}, {"|", Token::OP_BITOR}, {"&", Token::OP_BITAND}, diff --git a/test/simfil.cpp b/test/simfil.cpp index 768d66a3..c9b8ef7d 100644 --- a/test/simfil.cpp +++ b/test/simfil.cpp @@ -44,10 +44,14 @@ TEST_CASE("Wildcard", "[ast.wildcard]") { TEST_CASE("OperatorConst", "[ast.operator]") { /* Arithmetic */ REQUIRE_AST("-1", "-1"); + REQUIRE_AST("-1.1","-1.100000"); REQUIRE_AST("1+2", "3"); - REQUIRE_AST("1-2", "-1"); + REQUIRE_AST("1.5+2", "3.500000"); + REQUIRE_AST("1.5-2", "-0.500000"); REQUIRE_AST("2*2", "4"); + REQUIRE_AST("2.5*2", "5.000000"); REQUIRE_AST("8/2", "4"); + REQUIRE_AST("3/2.0", "1.500000"); REQUIRE_AST("-a", "(- a)"); REQUIRE_AST("a+2", "(+ a 2)"); REQUIRE_AST("2+a", "(+ 2 a)"); @@ -60,6 +64,8 @@ TEST_CASE("OperatorConst", "[ast.operator]") { REQUIRE_AST("null-1", "null"); REQUIRE_AST("1/null", "null"); REQUIRE_AST("null/1", "null"); + REQUIRE_AST("null%null", "null"); + REQUIRE_AST("null/null", "null"); auto GetError = [&](std::string_view query) -> std::string { Environment env(Environment::WithNewStringCache); @@ -84,11 +90,20 @@ TEST_CASE("OperatorConst", "[ast.operator]") { REQUIRE_AST("1!=1", "false"); REQUIRE_AST("2>1", "true"); + REQUIRE_AST("1>=1", "true"); + REQUIRE_AST("1.0>=1", "true"); + REQUIRE_AST("1>=1.0", "true"); REQUIRE_AST("1<2", "true"); REQUIRE_AST("2<1", "false"); REQUIRE_AST("2<=2", "true"); REQUIRE_AST("2<=1", "false"); REQUIRE_AST("1<=1.1", "true"); + REQUIRE_AST("1.0<=1", "true"); + REQUIRE_AST("1.0<=1.0", "true"); + REQUIRE_AST("'a'<'b'", "true"); + REQUIRE_AST("'a'<='b'", "true"); + REQUIRE_AST("'b'>'a'", "true"); + REQUIRE_AST("'b'>='b'", "true"); /* Null behaviour */ REQUIRE_AST("1> 2", "32"); + REQUIRE_AST("32 << 2", "128"); + REQUIRE_AST("0xf0 & 0x80", "128"); + REQUIRE_AST("0xf0 | 0x01", "241"); } TEST_CASE("CompareIncompatibleTypes", "[ast.compare-incompatible]") { @@ -223,6 +257,16 @@ TEST_CASE("CompareIncompatibleTypesFields", "[ast.compare-incompatible-types-fie REQUIRE(test("all(*.another=1)") == false); } +TEST_CASE("OperatorNegate", "[ast.operator-negate]") { + REQUIRE_AST("-(1)", "-1"); + REQUIRE_AST("-(1.0)", "-1.000000"); +} + +TEST_CASE("OperatorNot", "[ast.operator-not]") { + REQUIRE_AST("not true", "false"); + REQUIRE_AST("not false", "true"); +} + TEST_CASE("OperatorAndOr", "[ast.operator-and-or]") { /* Or */ REQUIRE_AST("null or 1", "1"); diff --git a/test/token.cpp b/test/token.cpp index 19a294cf..e2b7b842 100644 --- a/test/token.cpp +++ b/test/token.cpp @@ -130,6 +130,7 @@ TEST_CASE("Tokenize symbols", "[token.symbol]") { REQUIRE(getFirstType("<") == Token::Type::OP_LT); REQUIRE(getFirstType("< <") == Token::Type::OP_LT); REQUIRE(getFirstType("<<") == Token::Type::OP_LSHIFT); + REQUIRE(getFirstType(">>") == Token::Type::OP_RSHIFT); REQUIRE(getFirstType("typeof") == Token::Type::OP_TYPEOF); } From d659fcc9ef4fe383a13c6d2c2ba7229b30ad784c Mon Sep 17 00:00:00 2001 From: Johannes Wolf Date: Mon, 6 Oct 2025 22:14:55 +0200 Subject: [PATCH 18/50] tests: Add more tests --- test/completion.cpp | 4 ++++ test/simfil.cpp | 16 ++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/test/completion.cpp b/test/completion.cpp index 013d777a..a5fa1dfa 100644 --- a/test/completion.cpp +++ b/test/completion.cpp @@ -77,6 +77,10 @@ TEST_CASE("CompleteString", "[completion.string-const]") { EXPECT_COMPLETION("1 > C", {}, "CONSTANT_1"); } +TEST_CASE("CompleteFunction", "[completion.function]") { + EXPECT_COMPLETION("spl", {}, "split", Type::FUNCTION); +} + TEST_CASE("CompleteFieldOrString") { // Complete both the field and the constants EXPECT_COMPLETION("cons", {}, "constant", Type::FIELD); diff --git a/test/simfil.cpp b/test/simfil.cpp index c9b8ef7d..e44ed972 100644 --- a/test/simfil.cpp +++ b/test/simfil.cpp @@ -24,6 +24,22 @@ static constexpr auto StaticTestKey = StringPool::NextStaticId; #define REQUIRE_AST_AUTOWILDCARD(input, output) \ REQUIRE(Compile(input, true)->expr().toString() == (output)); +TEST_CASE("Int", "[ast.integer]") { + REQUIRE_AST("1", "1"); + REQUIRE_AST("123", "123"); + REQUIRE_AST("-1", "-1"); + REQUIRE_AST("0xff", "255"); + REQUIRE_AST("-0xff", "-255"); +} + +TEST_CASE("Float", "[ast.float]") { + REQUIRE_AST("1.0", "1.000000"); + REQUIRE_AST("1.5", "1.500000"); + REQUIRE_AST("-1.0", "-1.000000"); + REQUIRE_AST("1e2", "100.000000"); + REQUIRE_AST("1e-2", "0.010000"); +} + TEST_CASE("Path", "[ast.path]") { REQUIRE_AST("a", "a"); REQUIRE_AST("a.b", "(. a b)"); From 24faaa1fe4bb0c7aa21ad7e3513f4bdeea353e4b Mon Sep 17 00:00:00 2001 From: Johannes Wolf Date: Mon, 6 Oct 2025 22:28:46 +0200 Subject: [PATCH 19/50] tests: More operator tests --- include/simfil/operator.h | 1 + test/common.cpp | 11 +++++++++++ test/common.hpp | 1 + test/simfil.cpp | 33 +++++++++++++++++++++++++++++++++ 4 files changed, 46 insertions(+) diff --git a/include/simfil/operator.h b/include/simfil/operator.h index c98a7e6e..b6598192 100644 --- a/include/simfil/operator.h +++ b/include/simfil/operator.h @@ -71,6 +71,7 @@ struct InvalidOperands { struct OperatorNegate { NAME("-") + NULL_AS_NULL() DENY_OTHER() DECL_OPERATION(int64_t, -) DECL_OPERATION(double, -) diff --git a/test/common.cpp b/test/common.cpp index c1263b28..1d05a0ac 100644 --- a/test/common.cpp +++ b/test/common.cpp @@ -5,6 +5,17 @@ static const PanicFn panicFn{}; +auto CompileError(std::string_view query, bool autoWildcard) -> Error +{ + Environment env(Environment::WithNewStringCache); + env.constants.emplace("a_number", simfil::Value::make((int64_t)123)); + + auto ast = compile(env, query, false, autoWildcard); + REQUIRE(!ast.has_value()); + + return std::move(ast.error()); +} + auto Compile(std::string_view query, bool autoWildcard) -> ASTPtr { Environment env(Environment::WithNewStringCache); diff --git a/test/common.hpp b/test/common.hpp index deeff9b0..eaa50480 100644 --- a/test/common.hpp +++ b/test/common.hpp @@ -80,6 +80,7 @@ class PanicFn : public simfil::Function }; auto Compile(std::string_view query, bool autoWildcard = false) -> ASTPtr; +auto CompileError(std::string_view query, bool autoWildcard = false) -> Error; auto JoinedResult(std::string_view query, std::optional json = {}) -> std::string; auto CompleteQuery(std::string_view query, size_t point, std::optional json = {}) -> std::vector; auto GetDiagnosticMessages(std::string_view query) -> std::vector; diff --git a/test/simfil.cpp b/test/simfil.cpp index e44ed972..28416a53 100644 --- a/test/simfil.cpp +++ b/test/simfil.cpp @@ -21,6 +21,9 @@ static constexpr auto StaticTestKey = StringPool::NextStaticId; #define REQUIRE_AST(input, output) \ REQUIRE(Compile(input, false)->expr().toString() == (output)); +#define REQUIRE_ERROR(input) \ + REQUIRE(CompileError(input, false).message != ""); + #define REQUIRE_AST_AUTOWILDCARD(input, output) \ REQUIRE(Compile(input, true)->expr().toString() == (output)); @@ -201,6 +204,14 @@ TEST_CASE("OperatorConst", "[ast.operator]") { REQUIRE_AST("0xf0 | 0x01", "241"); } +TEST_CASE("OperatorLength", "[ast.operator-length]") { + REQUIRE_ERROR("#0"); + REQUIRE_ERROR("#0.0"); + REQUIRE_ERROR("#true"); + REQUIRE_AST("#null", "null"); + REQUIRE_AST("#'abc'", "3"); +} + TEST_CASE("CompareIncompatibleTypes", "[ast.compare-incompatible]") { REQUIRE_AST("1=\"A\"", "false"); REQUIRE_AST("1!=\"A\"", "true"); @@ -274,25 +285,47 @@ TEST_CASE("CompareIncompatibleTypesFields", "[ast.compare-incompatible-types-fie } TEST_CASE("OperatorNegate", "[ast.operator-negate]") { + REQUIRE_ERROR("-('abc')"); + REQUIRE_ERROR("-(true)"); REQUIRE_AST("-(1)", "-1"); REQUIRE_AST("-(1.0)", "-1.000000"); + REQUIRE_AST("-(null)", "null"); +} + +TEST_CASE("OperatorSubstract", "[ast.operator-substract]") { + REQUIRE_AST("1-2", "-1"); + REQUIRE_AST("1-2.0", "-1.000000"); + REQUIRE_AST("1-null", "null"); + REQUIRE_AST("1.0-2", "-1.000000"); + REQUIRE_AST("1.0-2.0", "-1.000000"); + REQUIRE_AST("1.0-null", "null"); + REQUIRE_ERROR("1-true"); + REQUIRE_ERROR("true-1"); + REQUIRE_ERROR("true-false"); } TEST_CASE("OperatorNot", "[ast.operator-not]") { REQUIRE_AST("not true", "false"); REQUIRE_AST("not false", "true"); + REQUIRE_AST("not 'abc'", "false"); + REQUIRE_AST("not 1", "false"); + REQUIRE_AST("not 1.0", "false"); + REQUIRE_AST("not null", "true"); } TEST_CASE("OperatorAndOr", "[ast.operator-and-or]") { /* Or */ REQUIRE_AST("null or 1", "1"); + REQUIRE_AST("false or 1", "1"); REQUIRE_AST("1 or null", "1"); REQUIRE_AST("1 or 2", "1"); REQUIRE_AST("a or b", "(or a b)"); /* And */ REQUIRE_AST("null and 1", "null"); + REQUIRE_AST("false and 1", "false"); REQUIRE_AST("1 and null", "null"); + REQUIRE_AST("true and 2", "2"); REQUIRE_AST("1 and 2", "2"); REQUIRE_AST("a and b", "(and a b)"); } From 056751f310f3304beb37a256fac4c921282f259b Mon Sep 17 00:00:00 2001 From: Johannes Wolf Date: Mon, 6 Oct 2025 20:36:00 +0200 Subject: [PATCH 20/50] completion: Show a completion hint for ** mode --- include/simfil/environment.h | 1 + src/completion.cpp | 5 +++++ src/completion.h | 1 + src/simfil.cpp | 11 +++++++++++ 4 files changed, 18 insertions(+) diff --git a/include/simfil/environment.h b/include/simfil/environment.h index 1c1e7878..a12b6f04 100644 --- a/include/simfil/environment.h +++ b/include/simfil/environment.h @@ -200,6 +200,7 @@ struct CompletionCandidate CONSTANT = 1, FIELD = 2, FUNCTION = 3, + HINT = 4, }; std::string text; // Text to insert diff --git a/src/completion.cpp b/src/completion.cpp index 7da478c5..7e304d55 100644 --- a/src/completion.cpp +++ b/src/completion.cpp @@ -356,6 +356,11 @@ auto CompletionWordExpr::type() const -> Type return Type::VALUE; } +auto CompletionWordExpr::constant() const -> bool +{ + return true; +} + auto CompletionWordExpr::ieval(Context ctx, const Value& val, const ResultFn& res) -> tl::expected { if (ctx.phase == Context::Phase::Compilation) diff --git a/src/completion.h b/src/completion.h index e620efb3..f94fb83d 100644 --- a/src/completion.h +++ b/src/completion.h @@ -92,6 +92,7 @@ class CompletionWordExpr : public Expr CompletionWordExpr(std::string prefix, Completion* comp, const Token& token); auto type() const -> Type override; + auto constant() const -> bool override; auto ieval(Context ctx, const Value& value, const ResultFn& result) -> tl::expected override; auto clone() const -> std::unique_ptr override; auto accept(ExprVisitor& v) -> void override; diff --git a/src/simfil.cpp b/src/simfil.cpp index 3f108b9f..b8825fbe 100644 --- a/src/simfil.cpp +++ b/src/simfil.cpp @@ -1,6 +1,7 @@ #include "simfil/simfil.h" #include "simfil/model/nodes.h" #include "simfil/model/string-pool.h" +#include "simfil/sourcelocation.h" #include "simfil/token.h" #include "simfil/operator.h" #include "simfil/value.h" @@ -844,9 +845,12 @@ auto complete(Environment& env, std::string_view query, size_t point, const Mode auto ast = std::move(*astResult); /* Expand a single value to `** == ` */ + auto showWildcardHint = false; if (options.autoWildcard && ast && ast->constant()) { ast = std::make_unique>( std::make_unique(), std::move(ast)); + + showWildcardHint = true; } Context ctx(&env); @@ -863,6 +867,13 @@ auto complete(Environment& env, std::string_view query, size_t point, const Mode return left.text < right.text; }); + /* Show a hint to prepend `** =` */ + if (showWildcardHint) + candidates.insert(candidates.begin(), {fmt::format("** = {}", query), + SourceLocation(0, query.size()), + CompletionCandidate::Type::HINT, + "Expand to wildcard query"}); + return candidates; } From 2252b7e68a1bc20df584b1210e93bf74ac39f3b8 Mon Sep 17 00:00:00 2001 From: Johannes Wolf Date: Mon, 6 Oct 2025 20:40:38 +0200 Subject: [PATCH 21/50] tests: Add ** completion test --- test/common.cpp | 1 + test/completion.cpp | 8 ++++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/test/common.cpp b/test/common.cpp index 1d05a0ac..9630cf67 100644 --- a/test/common.cpp +++ b/test/common.cpp @@ -68,6 +68,7 @@ auto CompleteQuery(std::string_view query, size_t point, std::optionalstrings()); CompletionOptions opts; + opts.autoWildcard = true; auto root = model.value()->root(0); return complete(env, query, point, **root, opts).value_or(std::vector()); } diff --git a/test/completion.cpp b/test/completion.cpp index a5fa1dfa..71b9623c 100644 --- a/test/completion.cpp +++ b/test/completion.cpp @@ -87,8 +87,12 @@ TEST_CASE("CompleteFieldOrString") { EXPECT_COMPLETION("cons", {}, "CONSTANT_1", Type::CONSTANT); // Do not complete the field - EXPECT_COMPLETION("CONS", {}, "CONSTANT_1", Type::CONSTANT, 2); - EXPECT_COMPLETION("CONS", {}, "CONSTANT_2", Type::CONSTANT, 2); + EXPECT_COMPLETION("CONS", {}, "CONSTANT_1", Type::CONSTANT, 3); // 3 entries bc. of `** =` + EXPECT_COMPLETION("CONS", {}, "CONSTANT_2", Type::CONSTANT, 3); +} + +TEST_CASE("CompleteWildcardEquals") { + EXPECT_COMPLETION("A_CONST", {}, "** = A_CONST", Type::HINT); } TEST_CASE("CompleteSorted") { From 02a20fa9bc51647f4292b2c2db2e62708968ad76 Mon Sep 17 00:00:00 2001 From: Johannes Wolf Date: Mon, 6 Oct 2025 20:40:38 +0200 Subject: [PATCH 22/50] tests: Add ** completion test --- src/simfil.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/simfil.cpp b/src/simfil.cpp index b8825fbe..aff35e2a 100644 --- a/src/simfil.cpp +++ b/src/simfil.cpp @@ -869,10 +869,10 @@ auto complete(Environment& env, std::string_view query, size_t point, const Mode /* Show a hint to prepend `** =` */ if (showWildcardHint) - candidates.insert(candidates.begin(), {fmt::format("** = {}", query), - SourceLocation(0, query.size()), - CompletionCandidate::Type::HINT, - "Expand to wildcard query"}); + candidates.emplace_back(fmt::format("** = {}", query), + SourceLocation(0, query.size()), + CompletionCandidate::Type::HINT, + "Expand to wildcard query"); return candidates; } From faafa15193e716483d080fbbfecb6623a4463902 Mon Sep 17 00:00:00 2001 From: Johannes Wolf Date: Sun, 12 Oct 2025 14:53:26 +0200 Subject: [PATCH 23/50] parser: Make parselets static (stack) objects --- include/simfil/expression.h | 2 +- include/simfil/parser.h | 4 +- src/parser.cpp | 4 +- src/simfil.cpp | 142 ++++++++++++++++++++++++------------ 4 files changed, 99 insertions(+), 53 deletions(-) diff --git a/include/simfil/expression.h b/include/simfil/expression.h index 696d82f2..01fb0ad0 100644 --- a/include/simfil/expression.h +++ b/include/simfil/expression.h @@ -133,7 +133,7 @@ class AST /* The original query string of the AST */ std::string queryString_; - /* The actuall AST */ + /* The actual AST */ ExprPtr expr_; }; diff --git a/include/simfil/parser.h b/include/simfil/parser.h index 083493a3..43fe4c2b 100644 --- a/include/simfil/parser.h +++ b/include/simfil/parser.h @@ -105,8 +105,8 @@ class Parser Context ctx; Environment* const env; - std::unordered_map> prefixParsers; - std::unordered_map> infixParsers; + std::unordered_map prefixParsers; + std::unordered_map infixParsers; private: auto findPrefixParser(const Token& t) const -> const PrefixParselet*; diff --git a/src/parser.cpp b/src/parser.cpp index e03361c3..ab9fd132 100644 --- a/src/parser.cpp +++ b/src/parser.cpp @@ -237,14 +237,14 @@ auto Parser::parseList(Token::Type stop) -> expected, Error auto Parser::findPrefixParser(const Token& t) const -> const PrefixParselet* { if (auto iter = prefixParsers.find(t.type); iter != prefixParsers.end()) - return iter->second.get(); + return iter->second; return nullptr; } auto Parser::findInfixParser(const Token& t) const -> const InfixParselet* { if (auto iter = infixParsers.find(t.type); iter != infixParsers.end()) - return iter->second.get(); + return iter->second; return nullptr; } } diff --git a/src/simfil.cpp b/src/simfil.cpp index aff35e2a..314d26a6 100644 --- a/src/simfil.cpp +++ b/src/simfil.cpp @@ -719,71 +719,114 @@ class CompletionPathParser : public PathParser Completion* comp_; }; +namespace +{ +// Static stateles parselets re-used by all parser instances +const ScalarParser intParser; +const ScalarParser floatParser; +const ScalarParser stringParser; +const RegExpParser regexpParser; +const UnaryOpParser negateParser; +const UnaryOpParser bitInvParser; +const UnaryOpParser notParser; +const UnaryOpParser lenParser; +const UnaryOpParser typeofParser; +const UnaryPostOpParser boolParser; +const BinaryOpParser addParser; +const BinaryOpParser subParser; +const BinaryOpParser mulParser; +const BinaryOpParser divParser; +const BinaryOpParser modParser; +const BinaryOpParser bitAndParser; +const BinaryOpParser bitOrParser; +const BinaryOpParser bitXorParser; +const BinaryOpParser shlParser; +const BinaryOpParser shrParser; +const BinaryOpParser eqParser; +const BinaryOpParser neqParser; +const BinaryOpParser ltParser; +const BinaryOpParser lteqParser; +const BinaryOpParser gtParser; +const BinaryOpParser gteqParser; +const AndOrParser andOrParser; +const CastParser castParser; +const ParenParser parenParser; +const SubSelectParser subSelectParser; +const SubscriptParser subscriptParser; +const WordParser wordParser; +const PathParser pathParser; +const UnpackOpParser unpackParser; +const WordOpParser wordOpParser; +const ConstParser trueParser{Value::t()}; +const ConstParser falseParser{Value::f()}; +const ConstParser nullParser{Value::null()}; +} + static auto setupParser(Parser& p) { /* Scalars */ - p.prefixParsers[Token::C_TRUE] = std::make_unique(Value::t()); - p.prefixParsers[Token::C_FALSE] = std::make_unique(Value::f()); - p.prefixParsers[Token::C_NULL] = std::make_unique(Value::null()); - p.prefixParsers[Token::INT] = std::make_unique>(); - p.prefixParsers[Token::FLOAT] = std::make_unique>(); - p.prefixParsers[Token::STRING] = std::make_unique>(); - p.prefixParsers[Token::REGEXP] = std::make_unique(); + p.prefixParsers[Token::C_TRUE] = &trueParser; + p.prefixParsers[Token::C_FALSE] = &falseParser; + p.prefixParsers[Token::C_NULL] = &nullParser; + p.prefixParsers[Token::INT] = &intParser; + p.prefixParsers[Token::FLOAT] = &floatParser; + p.prefixParsers[Token::STRING] = &stringParser; + p.prefixParsers[Token::REGEXP] = ®expParser; /* Unary Operators */ - p.prefixParsers[Token::OP_SUB] = std::make_unique>(); - p.prefixParsers[Token::OP_BITINV] = std::make_unique>(); - p.prefixParsers[Token::OP_NOT] = std::make_unique>(); - p.prefixParsers[Token::OP_LEN] = std::make_unique>(); - p.infixParsers[Token::OP_BOOL] = std::make_unique>(); - p.prefixParsers[Token::OP_TYPEOF] = std::make_unique>(); - p.infixParsers[Token::OP_UNPACK] = std::make_unique(); - p.infixParsers[Token::WORD] = std::make_unique(); + p.prefixParsers[Token::OP_SUB] = &negateParser; + p.prefixParsers[Token::OP_BITINV] = &bitInvParser; + p.prefixParsers[Token::OP_NOT] = ¬Parser; + p.prefixParsers[Token::OP_LEN] = &lenParser; + p.infixParsers[Token::OP_BOOL] = &boolParser; + p.prefixParsers[Token::OP_TYPEOF] = &typeofParser; + p.infixParsers[Token::OP_UNPACK] = &unpackParser; + p.infixParsers[Token::WORD] = &wordOpParser; /* Binary Operators */ - p.infixParsers[Token::OP_ADD] = std::make_unique>(); - p.infixParsers[Token::OP_SUB] = std::make_unique>(); - p.infixParsers[Token::OP_TIMES] = std::make_unique>(); - p.infixParsers[Token::OP_DIV] = std::make_unique>(); - p.infixParsers[Token::OP_MOD] = std::make_unique>(); + p.infixParsers[Token::OP_ADD] = &addParser; + p.infixParsers[Token::OP_SUB] = &subParser; + p.infixParsers[Token::OP_TIMES] = &mulParser; + p.infixParsers[Token::OP_DIV] = &divParser; + p.infixParsers[Token::OP_MOD] = &modParser; /* Bit Operators */ - p.infixParsers[Token::OP_BITAND] = std::make_unique>(); - p.infixParsers[Token::OP_BITOR] = std::make_unique>(); - p.infixParsers[Token::OP_BITXOR] = std::make_unique>(); - p.infixParsers[Token::OP_LSHIFT] = std::make_unique>(); - p.infixParsers[Token::OP_RSHIFT] = std::make_unique>(); + p.infixParsers[Token::OP_BITAND] = &bitAndParser; + p.infixParsers[Token::OP_BITOR] = &bitOrParser; + p.infixParsers[Token::OP_BITXOR] = &bitXorParser; + p.infixParsers[Token::OP_LSHIFT] = &shlParser; + p.infixParsers[Token::OP_RSHIFT] = &shrParser; /* Comparison/Test */ - p.infixParsers[Token::OP_EQ] = std::make_unique>(); - p.infixParsers[Token::OP_NOT_EQ] = std::make_unique>(); - p.infixParsers[Token::OP_LT] = std::make_unique>(); - p.infixParsers[Token::OP_LTEQ] = std::make_unique>(); - p.infixParsers[Token::OP_GT] = std::make_unique>(); - p.infixParsers[Token::OP_GTEQ] = std::make_unique>(); - p.infixParsers[Token::OP_AND] = std::make_unique(); - p.infixParsers[Token::OP_OR] = std::make_unique(); + p.infixParsers[Token::OP_EQ] = &eqParser; + p.infixParsers[Token::OP_NOT_EQ] = &neqParser; + p.infixParsers[Token::OP_LT] = <Parser; + p.infixParsers[Token::OP_LTEQ] = <eqParser; + p.infixParsers[Token::OP_GT] = >Parser; + p.infixParsers[Token::OP_GTEQ] = >eqParser; + p.infixParsers[Token::OP_AND] = &andOrParser; + p.infixParsers[Token::OP_OR] = &andOrParser; /* Cast */ - p.infixParsers[Token::OP_CAST] = std::make_unique(); + p.infixParsers[Token::OP_CAST] = &castParser; /* Subexpressions/Subscript */ - p.prefixParsers[Token::LPAREN] = std::make_unique(); /* (...) */ - p.prefixParsers[Token::LBRACE] = std::make_unique(); /* {...} */ - p.infixParsers[Token::LBRACE] = std::make_unique(); - p.prefixParsers[Token::LBRACK] = std::make_unique(); /* [...] */ - p.infixParsers[Token::LBRACK] = std::make_unique(); + p.prefixParsers[Token::LPAREN] = &parenParser; /* (...) */ + p.prefixParsers[Token::LBRACE] = &subSelectParser; /* {...} */ + p.infixParsers[Token::LBRACE] = &subSelectParser; + p.prefixParsers[Token::LBRACK] = &subscriptParser; /* [...] */ + p.infixParsers[Token::LBRACK] = &subscriptParser; /* Ident/Function */ - p.prefixParsers[Token::WORD] = std::make_unique(); - p.prefixParsers[Token::SELF] = std::make_unique(); + p.prefixParsers[Token::WORD] = &wordParser; + p.prefixParsers[Token::SELF] = &wordParser; /* Wildcards */ - p.prefixParsers[Token::WILDCARD] = std::make_unique(); - p.prefixParsers[Token::OP_TIMES] = std::make_unique(); + p.prefixParsers[Token::WILDCARD] = &wordParser; + p.prefixParsers[Token::OP_TIMES] = &wordParser; /* Paths */ - p.infixParsers[Token::DOT] = std::make_unique(); + p.infixParsers[Token::DOT] = &pathParser; } auto compile(Environment& env, std::string_view query, bool any, bool autoWildcard) -> expected @@ -835,10 +878,13 @@ auto complete(Environment& env, std::string_view query, size_t point, const Mode if (options.limit > 0) comp.limit = options.limit; - p.prefixParsers[Token::WORD] = std::make_unique(&comp); - p.infixParsers[Token::DOT] = std::make_unique(&comp); - p.infixParsers[Token::OP_AND] = std::make_unique(&comp); - p.infixParsers[Token::OP_OR] = std::make_unique(&comp); + CompletionWordParser wordCompletionParser(&comp); + CompletionPathParser pathCompletionParser(&comp); + CompletionAndOrParser andOrCompletionParser(&comp); + p.prefixParsers[Token::WORD] = &wordCompletionParser; + p.infixParsers[Token::DOT] = &pathCompletionParser; + p.infixParsers[Token::OP_AND] = &andOrCompletionParser; + p.infixParsers[Token::OP_OR] = &andOrCompletionParser; auto astResult = p.parse(); TRY_EXPECTED(astResult); From ffa1793010e54634fd902474fddb5bf036f1c50e Mon Sep 17 00:00:00 2001 From: Johannes Wolf Date: Sun, 12 Oct 2025 18:03:13 +0200 Subject: [PATCH 24/50] completion: Generate hints for prepending **. --- include/simfil/environment.h | 3 ++ src/expression-patterns.h | 78 ++++++++++++++++++++++++++++++++++++ src/expressions.cpp | 23 +++++++++++ src/expressions.h | 13 ++++++ src/simfil.cpp | 31 +++++++++++--- test/completion.cpp | 11 ++++- 6 files changed, 152 insertions(+), 7 deletions(-) create mode 100644 src/expression-patterns.h diff --git a/include/simfil/environment.h b/include/simfil/environment.h index a12b6f04..d1fe1ba8 100644 --- a/include/simfil/environment.h +++ b/include/simfil/environment.h @@ -178,6 +178,9 @@ struct CompletionOptions // Auto insert a wildcard if the first token is a field name. bool autoWildcard = false; + // Show hints about completing certain queries to a wildcard query. + bool showWildcardHints = true; + // Limit of candidates. size_t limit = 15; diff --git a/src/expression-patterns.h b/src/expression-patterns.h new file mode 100644 index 00000000..10fb8b30 --- /dev/null +++ b/src/expression-patterns.h @@ -0,0 +1,78 @@ +#pragma once + +#include "expressions.h" +#include +#include + +namespace simfil +{ + +/** + * Checks if the root expression is a single (constant) value. + */ +inline bool isSingleValueExpression(const Expr* expr) { + if (!expr) + return false; + + if (dynamic_cast(expr)) { + return true; + } + + return false; +} + +/** + * Checks if the root expression is a comparison of the following form: + * ` ` + */ +inline bool isSimpleFieldComparison(const Expr* expr) { + if (!expr) + return false; + + auto checkComparison = [](const auto* compExpr) -> bool { + if (!compExpr) + return false; + + auto leftIsFieldOrEnum = false; + if (const auto* left = dynamic_cast(compExpr->left_.get())) { + leftIsFieldOrEnum = true; + } else if (const auto* left = dynamic_cast(compExpr->left_.get())) { + // Test if the value is a WORD. + auto value = left->value(); + if (value.isa(ValueType::String)) { + auto str = value.as(); + auto loc = std::locale(); + leftIsFieldOrEnum = std::all_of(str.begin(), str.end(), [&](auto c) { + return c == '_' || std::isupper(c, loc); + }) && str.size() > 0; + } + } + + auto rightIsConstant = compExpr->right_->constant(); + + return leftIsFieldOrEnum && rightIsConstant; + }; + + if (auto* eqExpr = dynamic_cast*>(expr)) { + return checkComparison(eqExpr); + } + if (auto* neqExpr = dynamic_cast*>(expr)) { + return checkComparison(neqExpr); + } + if (auto* ltExpr = dynamic_cast*>(expr)) { + return checkComparison(ltExpr); + } + if (auto* lteqExpr = dynamic_cast*>(expr)) { + return checkComparison(lteqExpr); + } + if (auto* gtExpr = dynamic_cast*>(expr)) { + return checkComparison(gtExpr); + } + if (auto* gteqExpr = dynamic_cast*>(expr)) { + return checkComparison(gteqExpr); + } + + return false; +} + +} diff --git a/src/expressions.cpp b/src/expressions.cpp index 7b2920fb..b3bfc637 100644 --- a/src/expressions.cpp +++ b/src/expressions.cpp @@ -7,6 +7,7 @@ #include "fmt/core.h" #include "src/expected.h" +#include namespace simfil { @@ -335,6 +336,28 @@ auto ConstExpr::toString() const -> std::string return value_.toString(); } +auto ConstExpr::value() const -> const Value& +{ + return value_; +} + +auto CompletionConstExpr::constant() const -> bool +{ + return false; +} + +auto CompletionConstExpr::clone() const -> ExprPtr +{ + return std::make_unique(value_); +} + +auto CompletionConstExpr::ieval(Context ctx, const Value&, const ResultFn& res) -> tl::expected +{ + if (ctx.phase == Context::Compilation) + return res(ctx, Value::undef()); + return res(ctx, value_); +} + SubscriptExpr::SubscriptExpr(ExprPtr left, ExprPtr index) : left_(std::move(left)) , index_(std::move(index)) diff --git a/src/expressions.h b/src/expressions.h index c375feb5..18e8684a 100644 --- a/src/expressions.h +++ b/src/expressions.h @@ -152,10 +152,23 @@ class ConstExpr : public Expr void accept(ExprVisitor& v) override; auto toString() const -> std::string override; + auto value() const -> const Value&; + protected: const Value value_; }; +/** A special expression to prevent constant value evaluation during completion. */ +class CompletionConstExpr : public ConstExpr +{ +public: + using ConstExpr::ConstExpr; + + auto constant() const -> bool override; + auto clone() const -> ExprPtr override; + auto ieval(Context ctx, const Value&, const ResultFn& res) -> tl::expected override; +}; + class SubscriptExpr : public Expr { public: diff --git a/src/simfil.cpp b/src/simfil.cpp index 314d26a6..85971c57 100644 --- a/src/simfil.cpp +++ b/src/simfil.cpp @@ -15,6 +15,7 @@ #include "fmt/core.h" #include "expressions.h" +#include "expression-patterns.h" #include "completion.h" #include "expected.h" @@ -640,7 +641,7 @@ class CompletionWordParser : public WordParser if (t.containsPoint(comp_->point)) { return std::make_unique(word.substr(0, comp_->point - t.begin), comp_, t); } - return std::make_unique(Value::make(std::move(word))); + return std::make_unique(Value::make(std::move(word))); } /* Constant */ else if (auto constant = p.env->findConstant(word)) { @@ -890,13 +891,25 @@ auto complete(Environment& env, std::string_view query, size_t point, const Mode TRY_EXPECTED(astResult); auto ast = std::move(*astResult); - /* Expand a single value to `** == ` */ - auto showWildcardHint = false; + // Determine which hints to show. + auto showConstantWildcardHint = false; + auto showComparisonWildcardHint = false; + + // Check for simple constant expressions. if (options.autoWildcard && ast && ast->constant()) { ast = std::make_unique>( std::make_unique(), std::move(ast)); + showConstantWildcardHint = true; + } + + // Test the query for patterns and hint for converting it + // to a wildcard query by prepending `**.` to the query. + if (options.showWildcardHints) { + if (isSingleValueExpression(ast.get())) + showConstantWildcardHint = true; - showWildcardHint = true; + if (isSimpleFieldComparison(ast.get())) + showComparisonWildcardHint = true; } Context ctx(&env); @@ -913,12 +926,18 @@ auto complete(Environment& env, std::string_view query, size_t point, const Mode return left.text < right.text; }); - /* Show a hint to prepend `** =` */ - if (showWildcardHint) + // Show special hints for wildcard expansion. + if (showConstantWildcardHint) candidates.emplace_back(fmt::format("** = {}", query), SourceLocation(0, query.size()), CompletionCandidate::Type::HINT, "Expand to wildcard query"); + + if (showComparisonWildcardHint) + candidates.emplace_back(fmt::format("**.{}", query), + SourceLocation(0, query.size()), + CompletionCandidate::Type::HINT, + "Expand to wildcard query"); return candidates; } diff --git a/test/completion.cpp b/test/completion.cpp index 71b9623c..f72af9e5 100644 --- a/test/completion.cpp +++ b/test/completion.cpp @@ -37,7 +37,6 @@ auto FindCompletion(std::string_view query, std::optional point, std::st REQUIRE(comp.size() > 0); for (const auto& item : comp) { - INFO(item.text); if (item.text == what && (!type || item.type == *type)) return true; } @@ -46,6 +45,7 @@ auto FindCompletion(std::string_view query, std::optional point, std::st auto EXPECT_COMPLETION(std::string_view query, std::optional point, std::string_view what, std::optional type = {}, size_t count = 0) { + INFO("Query: " << query); REQUIRE(FindCompletion(query, point, what, type, count) == true); } @@ -95,6 +95,15 @@ TEST_CASE("CompleteWildcardEquals") { EXPECT_COMPLETION("A_CONST", {}, "** = A_CONST", Type::HINT); } +TEST_CASE("CompleteWildcardComparison") { + EXPECT_COMPLETION("field == 123", {}, "**.field == 123", Type::HINT); + EXPECT_COMPLETION("name != \"test\"", {}, "**.name != \"test\"", Type::HINT); + EXPECT_COMPLETION("count < 10", {}, "**.count < 10", Type::HINT); + EXPECT_COMPLETION("value >= 5.0", {}, "**.value >= 5.0", Type::HINT); + EXPECT_COMPLETION("STATUS_ACTIVE == \"active\"", {}, "**.STATUS_ACTIVE == \"active\"", Type::HINT); + EXPECT_COMPLETION("TYPE_ENUM != 42", {}, "**.TYPE_ENUM != 42", Type::HINT); +} + TEST_CASE("CompleteSorted") { auto comp = GetCompletion("f", {}); REQUIRE(std::is_sorted(comp.begin(), comp.end(), [](const auto& l, const auto& r) { From b407a445119978c5fdfa1264ec57a62df6732e9e Mon Sep 17 00:00:00 2001 From: Johannes Wolf Date: Sun, 12 Oct 2025 19:55:02 +0200 Subject: [PATCH 25/50] completion: Replace copy by const ref --- src/expression-patterns.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/expression-patterns.h b/src/expression-patterns.h index 10fb8b30..3637579b 100644 --- a/src/expression-patterns.h +++ b/src/expression-patterns.h @@ -38,7 +38,7 @@ inline bool isSimpleFieldComparison(const Expr* expr) { leftIsFieldOrEnum = true; } else if (const auto* left = dynamic_cast(compExpr->left_.get())) { // Test if the value is a WORD. - auto value = left->value(); + const auto& value = left->value(); if (value.isa(ValueType::String)) { auto str = value.as(); auto loc = std::locale(); From ae1a3b1f8681799b3dce142d654175f7a896540f Mon Sep 17 00:00:00 2001 From: Johannes Wolf Date: Sun, 12 Oct 2025 19:55:21 +0200 Subject: [PATCH 26/50] completion: Provide wildcard hints --- include/simfil/environment.h | 3 -- repl/repl.cpp | 1 - src/completion.cpp | 19 ++++++++++- src/completion.h | 18 ++++++++-- src/expression-patterns.h | 64 +++++++++++++++++++++++++++--------- src/expressions.cpp | 19 +---------- src/expressions.h | 11 ------- src/simfil.cpp | 34 ++++++++++--------- test/common.cpp | 7 ++-- test/common.hpp | 2 +- test/completion.cpp | 52 ++++++++++++++++------------- 11 files changed, 137 insertions(+), 93 deletions(-) diff --git a/include/simfil/environment.h b/include/simfil/environment.h index d1fe1ba8..2a83b39a 100644 --- a/include/simfil/environment.h +++ b/include/simfil/environment.h @@ -175,9 +175,6 @@ struct Debug */ struct CompletionOptions { - // Auto insert a wildcard if the first token is a field name. - bool autoWildcard = false; - // Show hints about completing certain queries to a wildcard query. bool showWildcardHints = true; diff --git a/repl/repl.cpp b/repl/repl.cpp index 7fe29c59..6239a212 100644 --- a/repl/repl.cpp +++ b/repl/repl.cpp @@ -79,7 +79,6 @@ char* command_generator(const char* text, int state) if (current_env) { simfil::CompletionOptions opts; opts.limit = 25; - opts.autoWildcard = options.auto_wildcard; auto root = model->root(0); if (!root) diff --git a/src/completion.cpp b/src/completion.cpp index 7e304d55..6515f0ba 100644 --- a/src/completion.cpp +++ b/src/completion.cpp @@ -133,7 +133,7 @@ CompletionFieldOrWordExpr::CompletionFieldOrWordExpr(std::string prefix, Complet auto CompletionFieldOrWordExpr::type() const -> Type { - return Type::PATH; + return Type::FIELD; } auto CompletionFieldOrWordExpr::ieval(Context ctx, const Value& val, const ResultFn& res) -> tl::expected @@ -387,4 +387,21 @@ auto CompletionWordExpr::accept(ExprVisitor& v) -> void v.visit(*this); } +auto CompletionConstExpr::constant() const -> bool +{ + return false; +} + +auto CompletionConstExpr::clone() const -> ExprPtr +{ + return std::make_unique(value_); +} + +auto CompletionConstExpr::ieval(Context ctx, const Value&, const ResultFn& res) -> tl::expected +{ + if (ctx.phase == Context::Compilation) + return res(ctx, Value::undef()); + return res(ctx, value_); +} + } diff --git a/src/completion.h b/src/completion.h index f94fb83d..17eca844 100644 --- a/src/completion.h +++ b/src/completion.h @@ -1,10 +1,10 @@ #pragma once -#include "simfil/expression.h" #include "simfil/token.h" #include "simfil/environment.h" -#include +#include "expressions.h" + #include #include #include @@ -102,4 +102,18 @@ class CompletionWordExpr : public Expr Completion* comp_; }; +/** + * A special expression to prevent constant value + * evaluation during completion. + */ +class CompletionConstExpr : public ConstExpr +{ +public: + using ConstExpr::ConstExpr; + + auto constant() const -> bool override; + auto clone() const -> ExprPtr override; + auto ieval(Context ctx, const Value&, const ResultFn& res) -> tl::expected override; +}; + } diff --git a/src/expression-patterns.h b/src/expression-patterns.h index 3637579b..1e1d507c 100644 --- a/src/expression-patterns.h +++ b/src/expression-patterns.h @@ -1,21 +1,52 @@ #pragma once #include "expressions.h" +#include "simfil/expression.h" +#include "simfil/model/nodes.h" +#include "src/completion.h" + #include #include namespace simfil { +inline auto isSingleValueExpression(const Expr* expr) -> bool { + if (!expr) + return false; + + if (dynamic_cast(expr)) + return true; + + if (auto* v = dynamic_cast(expr)) + return true; + + return false; +} + /** - * Checks if the root expression is a single (constant) value. + * Checks if the root expression is a single (constant) value + * or a field. */ -inline bool isSingleValueExpression(const Expr* expr) { +inline auto isSingleValueOrFieldExpression(const Expr* expr) -> bool { if (!expr) return false; - if (dynamic_cast(expr)) { + if (expr->type() == Expr::Type::FIELD) return true; + + if (dynamic_cast(expr)) + return true; + + if (auto* v = dynamic_cast(expr)) { + const auto value = v->value(); + if (value.isa(ValueType::String)) { + auto str = value.as(); + auto loc = std::locale(); + return std::all_of(str.begin(), str.end(), [&](auto c) { + return c == '_' || std::isupper(c, loc); + }) && !str.empty(); + } } return false; @@ -25,7 +56,7 @@ inline bool isSingleValueExpression(const Expr* expr) { * Checks if the root expression is a comparison of the following form: * ` ` */ -inline bool isSimpleFieldComparison(const Expr* expr) { +inline auto isFieldComparison(const Expr* expr) -> bool { if (!expr) return false; @@ -38,6 +69,7 @@ inline bool isSimpleFieldComparison(const Expr* expr) { leftIsFieldOrEnum = true; } else if (const auto* left = dynamic_cast(compExpr->left_.get())) { // Test if the value is a WORD. + // This is not optimal const auto& value = left->value(); if (value.isa(ValueType::String)) { auto str = value.as(); @@ -53,23 +85,23 @@ inline bool isSimpleFieldComparison(const Expr* expr) { return leftIsFieldOrEnum && rightIsConstant; }; - if (auto* eqExpr = dynamic_cast*>(expr)) { - return checkComparison(eqExpr); + if (auto* e = dynamic_cast*>(expr)) { + return checkComparison(e); } - if (auto* neqExpr = dynamic_cast*>(expr)) { - return checkComparison(neqExpr); + if (auto* e = dynamic_cast*>(expr)) { + return checkComparison(e); } - if (auto* ltExpr = dynamic_cast*>(expr)) { - return checkComparison(ltExpr); + if (auto* e = dynamic_cast*>(expr)) { + return checkComparison(e); } - if (auto* lteqExpr = dynamic_cast*>(expr)) { - return checkComparison(lteqExpr); + if (auto* e = dynamic_cast*>(expr)) { + return checkComparison(e); } - if (auto* gtExpr = dynamic_cast*>(expr)) { - return checkComparison(gtExpr); + if (auto* e = dynamic_cast*>(expr)) { + return checkComparison(e); } - if (auto* gteqExpr = dynamic_cast*>(expr)) { - return checkComparison(gteqExpr); + if (auto* e = dynamic_cast*>(expr)) { + return checkComparison(e); } return false; diff --git a/src/expressions.cpp b/src/expressions.cpp index b3bfc637..973f87a5 100644 --- a/src/expressions.cpp +++ b/src/expressions.cpp @@ -193,7 +193,7 @@ FieldExpr::FieldExpr(std::string name, const Token& token) auto FieldExpr::type() const -> Type { - return Type::PATH; + return Type::FIELD; } auto FieldExpr::ieval(Context ctx, const Value& val, const ResultFn& res) -> tl::expected @@ -341,23 +341,6 @@ auto ConstExpr::value() const -> const Value& return value_; } -auto CompletionConstExpr::constant() const -> bool -{ - return false; -} - -auto CompletionConstExpr::clone() const -> ExprPtr -{ - return std::make_unique(value_); -} - -auto CompletionConstExpr::ieval(Context ctx, const Value&, const ResultFn& res) -> tl::expected -{ - if (ctx.phase == Context::Compilation) - return res(ctx, Value::undef()); - return res(ctx, value_); -} - SubscriptExpr::SubscriptExpr(ExprPtr left, ExprPtr index) : left_(std::move(left)) , index_(std::move(index)) diff --git a/src/expressions.h b/src/expressions.h index 18e8684a..97c14be2 100644 --- a/src/expressions.h +++ b/src/expressions.h @@ -158,17 +158,6 @@ class ConstExpr : public Expr const Value value_; }; -/** A special expression to prevent constant value evaluation during completion. */ -class CompletionConstExpr : public ConstExpr -{ -public: - using ConstExpr::ConstExpr; - - auto constant() const -> bool override; - auto clone() const -> ExprPtr override; - auto ieval(Context ctx, const Value&, const ResultFn& res) -> tl::expected override; -}; - class SubscriptExpr : public Expr { public: diff --git a/src/simfil.cpp b/src/simfil.cpp index 85971c57..3c86daa0 100644 --- a/src/simfil.cpp +++ b/src/simfil.cpp @@ -482,7 +482,7 @@ class SubscriptParser : public PrefixParselet, public InfixParselet auto parse(Parser& p, ExprPtr left, Token t) const -> expected override { - if (auto err = expect(left, Expr::Type::PATH, Expr::Type::VALUE, Expr::Type::SUBEXPR, Expr::Type::SUBSCRIPT)) + if (auto err = expect(left, Expr::Type::FIELD, Expr::Type::PATH, Expr::Type::VALUE, Expr::Type::SUBEXPR, Expr::Type::SUBSCRIPT)) return *err; auto _ = scopedNotInPath(p); @@ -523,7 +523,7 @@ class SubSelectParser : public PrefixParselet, public InfixParselet auto parse(Parser& p, ExprPtr left, Token t) const -> expected override { - if (auto err = expect(left, Expr::Type::PATH, Expr::Type::VALUE, Expr::Type::SUBEXPR, Expr::Type::SUBSCRIPT)) + if (auto err = expect(left, Expr::Type::FIELD, Expr::Type::PATH, Expr::Type::VALUE, Expr::Type::SUBEXPR, Expr::Type::SUBSCRIPT)) return *err; auto _ = scopedNotInPath(p); @@ -893,22 +893,18 @@ auto complete(Environment& env, std::string_view query, size_t point, const Mode // Determine which hints to show. auto showConstantWildcardHint = false; + auto showFieldWildcardHint = false; auto showComparisonWildcardHint = false; - - // Check for simple constant expressions. - if (options.autoWildcard && ast && ast->constant()) { - ast = std::make_unique>( - std::make_unique(), std::move(ast)); - showConstantWildcardHint = true; - } - - // Test the query for patterns and hint for converting it - // to a wildcard query by prepending `**.` to the query. if (options.showWildcardHints) { + // Test the query for patterns and hint for converting it + // to a wildcard query by prepending `**.` to the query. if (isSingleValueExpression(ast.get())) showConstantWildcardHint = true; - if (isSimpleFieldComparison(ast.get())) + if (isSingleValueOrFieldExpression(ast.get())) + showFieldWildcardHint = true; + + if (isFieldComparison(ast.get())) showComparisonWildcardHint = true; } @@ -927,17 +923,23 @@ auto complete(Environment& env, std::string_view query, size_t point, const Mode }); // Show special hints for wildcard expansion. + if (showFieldWildcardHint) + candidates.emplace_back(fmt::format("**.{}", query), + SourceLocation(0, query.size()), + CompletionCandidate::Type::HINT, + fmt::format("Query field '{}' recursive", query)); + if (showConstantWildcardHint) candidates.emplace_back(fmt::format("** = {}", query), SourceLocation(0, query.size()), CompletionCandidate::Type::HINT, - "Expand to wildcard query"); - + fmt::format("Query fields matching '{}' recursive", query)); + if (showComparisonWildcardHint) candidates.emplace_back(fmt::format("**.{}", query), SourceLocation(0, query.size()), CompletionCandidate::Type::HINT, - "Expand to wildcard query"); + "Expand to recursive query"); return candidates; } diff --git a/test/common.cpp b/test/common.cpp index 9630cf67..2a2b956b 100644 --- a/test/common.cpp +++ b/test/common.cpp @@ -61,14 +61,17 @@ auto JoinedResult(std::string_view query, std::optional json) -> st return vals; } -auto CompleteQuery(std::string_view query, size_t point, std::optional json) -> std::vector +auto CompleteQuery(std::string_view query, size_t point, std::optional json, const CompletionOptions* options) -> std::vector { auto model = simfil::json::parse(json.value_or(TestModel)); REQUIRE(model); Environment env(model.value()->strings()); CompletionOptions opts; - opts.autoWildcard = true; + opts.showWildcardHints = true; + if (options) + opts = *options; + auto root = model.value()->root(0); return complete(env, query, point, **root, opts).value_or(std::vector()); } diff --git a/test/common.hpp b/test/common.hpp index eaa50480..54fa3106 100644 --- a/test/common.hpp +++ b/test/common.hpp @@ -82,5 +82,5 @@ class PanicFn : public simfil::Function auto Compile(std::string_view query, bool autoWildcard = false) -> ASTPtr; auto CompileError(std::string_view query, bool autoWildcard = false) -> Error; auto JoinedResult(std::string_view query, std::optional json = {}) -> std::string; -auto CompleteQuery(std::string_view query, size_t point, std::optional json = {}) -> std::vector; +auto CompleteQuery(std::string_view query, size_t point, std::optional json = {}, const CompletionOptions* = nullptr) -> std::vector; auto GetDiagnosticMessages(std::string_view query) -> std::vector; diff --git a/test/completion.cpp b/test/completion.cpp index f72af9e5..03a6d4d5 100644 --- a/test/completion.cpp +++ b/test/completion.cpp @@ -1,8 +1,12 @@ +#include "src/completion.h" #include #include #include +#include #include "common.hpp" +#include "simfil/environment.h" +#include "src/expected.h" const auto model = R"json( { @@ -23,30 +27,29 @@ const auto model = R"json( using Type = simfil::CompletionCandidate::Type; -auto GetCompletion(std::string_view query, std::optional point) +auto GetCompletion(std::string_view query, std::optional point, const CompletionOptions* options = nullptr) { - return CompleteQuery(query, point.value_or(query.size()), model); + return CompleteQuery(query, point.value_or(query.size()), model, options); } -auto FindCompletion(std::string_view query, std::optional point, std::string_view what, std::optional type, size_t count) +auto EXPECT_COMPLETION(std::string_view query, std::optional point, std::string_view what, std::optional type = {}, size_t count = 0) { - auto comp = GetCompletion(query, point); - if (count != 0) - REQUIRE(comp.size() == count); - else - REQUIRE(comp.size() > 0); + INFO("Query: " << query); + INFO("Expected completion: " << what); + auto found = false; + auto comp = GetCompletion(query, point); for (const auto& item : comp) { - if (item.text == what && (!type || item.type == *type)) - return true; + INFO(" Item: " << item.text); + if (item.text == what && (!type || item.type == *type)) { + found = true; + } } - return false; -} -auto EXPECT_COMPLETION(std::string_view query, std::optional point, std::string_view what, std::optional type = {}, size_t count = 0) -{ - INFO("Query: " << query); - REQUIRE(FindCompletion(query, point, what, type, count) == true); + REQUIRE(found); + if (count > 0) { + REQUIRE(comp.size() == count); + } } TEST_CASE("CompleteField", "[completion.field.incompleteQuery]") { @@ -87,25 +90,30 @@ TEST_CASE("CompleteFieldOrString") { EXPECT_COMPLETION("cons", {}, "CONSTANT_1", Type::CONSTANT); // Do not complete the field - EXPECT_COMPLETION("CONS", {}, "CONSTANT_1", Type::CONSTANT, 3); // 3 entries bc. of `** =` - EXPECT_COMPLETION("CONS", {}, "CONSTANT_2", Type::CONSTANT, 3); + EXPECT_COMPLETION("CONS", {}, "CONSTANT_1", Type::CONSTANT, 4); // 3 entries bc. of `** =` + EXPECT_COMPLETION("CONS", {}, "CONSTANT_2", Type::CONSTANT); } TEST_CASE("CompleteWildcardEquals") { EXPECT_COMPLETION("A_CONST", {}, "** = A_CONST", Type::HINT); + EXPECT_COMPLETION("A_CONST", {}, "**.A_CONST", Type::HINT); + EXPECT_COMPLETION("field", {}, "**.field", Type::HINT); } TEST_CASE("CompleteWildcardComparison") { EXPECT_COMPLETION("field == 123", {}, "**.field == 123", Type::HINT); - EXPECT_COMPLETION("name != \"test\"", {}, "**.name != \"test\"", Type::HINT); + EXPECT_COMPLETION("name != \"Mulldrifter\"", {}, "**.name != \"Mulldrifter\"", Type::HINT); EXPECT_COMPLETION("count < 10", {}, "**.count < 10", Type::HINT); EXPECT_COMPLETION("value >= 5.0", {}, "**.value >= 5.0", Type::HINT); - EXPECT_COMPLETION("STATUS_ACTIVE == \"active\"", {}, "**.STATUS_ACTIVE == \"active\"", Type::HINT); - EXPECT_COMPLETION("TYPE_ENUM != 42", {}, "**.TYPE_ENUM != 42", Type::HINT); + EXPECT_COMPLETION("A_FIELD == \"Value\"", {}, "**.A_FIELD == \"Value\"", Type::HINT); + EXPECT_COMPLETION("A_CONST != 42", {}, "**.A_CONST != 42", Type::HINT); } TEST_CASE("CompleteSorted") { - auto comp = GetCompletion("f", {}); + CompletionOptions opts; + opts.showWildcardHints = false; + + auto comp = GetCompletion("f", {}, &opts); REQUIRE(std::is_sorted(comp.begin(), comp.end(), [](const auto& l, const auto& r) { return l.text < r.text; })); From 12777475fb6b13461de128b0c179d8cdc68cc768 Mon Sep 17 00:00:00 2001 From: Johannes Wolf Date: Sun, 19 Oct 2025 15:44:27 +0200 Subject: [PATCH 27/50] completion: Fix recursive field completion --- src/completion.cpp | 11 ++++++++--- src/expressions.cpp | 22 ++++++++++++++++------ 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/src/completion.cpp b/src/completion.cpp index 6515f0ba..21e46e68 100644 --- a/src/completion.cpp +++ b/src/completion.cpp @@ -1,6 +1,7 @@ #include "completion.h" #include "expressions.h" +#include "simfil/model/string-pool.h" #include "simfil/result.h" #include "simfil/simfil.h" #include "simfil/function.h" @@ -146,21 +147,25 @@ auto CompletionFieldOrWordExpr::ieval(Context ctx, const Value& val, const Resul const auto node = val.node(); if (!node) - return Result::Stop; + return res(ctx, val); const auto caseSensitive = comp_->options.smartCase && containsUppercaseCharacter(prefix_); // First we try to complete fields for (StringId id : node->fieldNames()) { - if (comp_->size() >= comp_->limit) + if (comp_->size() >= comp_->limit) { return Result::Stop; + } + + if (id == StringPool::Empty) + continue; auto keyPtr = ctx.env->strings()->resolve(id); if (!keyPtr || keyPtr->empty()) continue; const auto& key = *keyPtr; - if (key.size() >= prefix_.size() && startsWith(key, prefix_, caseSensitive)) { + if (startsWith(key, prefix_, caseSensitive)) { if (needsEscaping(key)) { comp_->add(escapeKey(key), sourceLocation(), CompletionCandidate::Type::FIELD); } else { diff --git a/src/expressions.cpp b/src/expressions.cpp index 973f87a5..c937a927 100644 --- a/src/expressions.cpp +++ b/src/expressions.cpp @@ -92,21 +92,31 @@ auto WildcardExpr::ieval(Context ctx, const Value& val, const ResultFn& ores) -> Context& ctx; ResultFn& res; - [[nodiscard]] auto iterate(ModelNode const& val) noexcept -> Result + [[nodiscard]] auto iterate(ModelNode const& val) noexcept -> tl::expected { if (val.type() == ValueType::Null) [[unlikely]] return Result::Continue; auto result = res(ctx, Value::field(val)); - if (!result || *result == Result::Stop) [[unlikely]] - return result ? *result : Result::Stop; + if (!result) + return tl::unexpected(result.error()); - Result finalResult = Result::Continue; + if (*result == Result::Stop) [[unlikely]] + return *result; + + tl::expected finalResult = Result::Continue; val.iterate(ModelNode::IterLambda([&, this](const auto& subNode) -> bool { - if (iterate(subNode) == Result::Stop) { + auto subResult = iterate(subNode); + if (!subResult) { + finalResult = std::move(subResult); + return false; + } + + if (*subResult == Result::Stop) { finalResult = Result::Stop; return false; } + return true; })); @@ -114,7 +124,7 @@ auto WildcardExpr::ieval(Context ctx, const Value& val, const ResultFn& ores) -> } }; - auto r = val.nodePtr() ? Iterate{ctx, res}.iterate(**val.nodePtr()) : Result::Continue; + auto r = val.nodePtr() ? Iterate{ctx, res}.iterate(**val.nodePtr()) : tl::expected(Result::Continue); res.ensureCall(); return r; } From 69fbd03645afc7a07b3a764bd805ea0ed9a45254 Mon Sep 17 00:00:00 2001 From: Johannes Wolf Date: Sun, 19 Oct 2025 17:51:00 +0200 Subject: [PATCH 28/50] tests: Add erro + operator tests --- test/CMakeLists.txt | 4 +- test/error.cpp | 57 +++++++++++ test/operator.cpp | 224 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 284 insertions(+), 1 deletion(-) create mode 100644 test/error.cpp create mode 100644 test/operator.cpp diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index ea758562..4ec71298 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -11,7 +11,9 @@ add_executable(test.simfil completion.cpp complex.cpp performance.cpp - arena.cpp) + arena.cpp + error.cpp + operator.cpp) target_link_libraries(test.simfil PUBLIC diff --git a/test/error.cpp b/test/error.cpp new file mode 100644 index 00000000..43c464c4 --- /dev/null +++ b/test/error.cpp @@ -0,0 +1,57 @@ +#include + +#include "simfil/error.h" +#include "simfil/token.h" + + +using namespace simfil; + +TEST_CASE("Error constructors", "[error]") { + SECTION("Constructor with type only") { + Error error(Error::Type::ParserError); + REQUIRE(error.type == Error::Type::ParserError); + REQUIRE(error.message.empty()); + REQUIRE(error.location.offset == 0); + REQUIRE(error.location.size == 0); + } + + SECTION("Constructor with type and message") { + Error error(Error::Type::RuntimeError, "Test error message"); + REQUIRE(error.type == Error::Type::RuntimeError); + REQUIRE(error.message == "Test error message"); + REQUIRE(error.location.offset == 0); + REQUIRE(error.location.size == 0); + } + + SECTION("Constructor with type, message and source location") { + SourceLocation loc(10, 5); + Error error(Error::Type::InvalidExpression, "Invalid expr", loc); + REQUIRE(error.type == Error::Type::InvalidExpression); + REQUIRE(error.message == "Invalid expr"); + REQUIRE(error.location.offset == 10); + REQUIRE(error.location.size == 5); + } + + SECTION("Constructor with type, message and token") { + Token token(Token::Type::WORD, 20, 25); + Error error(Error::Type::UnknownFunction, "MyFunction", token); + REQUIRE(error.type == Error::Type::UnknownFunction); + REQUIRE(error.message == "MyFunction"); + REQUIRE(error.location.offset == 20); + REQUIRE(error.location.size == 5); + } +} + +TEST_CASE("Error equality operator", "[error]") { + SECTION("Equal errors") { + Error a(Error::Type::DivisionByZero, "Division by zero"); + Error b(Error::Type::DivisionByZero, "Division by zero"); + REQUIRE(a == b); + } + + SECTION("Different types") { + Error a(Error::Type::DivisionByZero, "Error"); + Error b(Error::Type::RuntimeError, "Error"); + REQUIRE(!(a == b)); + } +} diff --git a/test/operator.cpp b/test/operator.cpp new file mode 100644 index 00000000..39415fff --- /dev/null +++ b/test/operator.cpp @@ -0,0 +1,224 @@ +#include +#include +#include +#include + +#include "simfil/operator.h" +#include "simfil/model/model.h" +#include "simfil/value.h" + +using namespace simfil; +using namespace std::string_literals; + +#define REQUIRE_INVALID_OPERANDS(v) \ + static_assert(std::is_same_v); + +TEST_CASE("Unary operators", "[operator.unary]") { + SECTION("OperatorNegate") { + OperatorNegate op; + REQUIRE(op.name() == std::string("-")); + + REQUIRE(op(int64_t(5)) == int64_t(-5)); + REQUIRE(op(int64_t(-5)) == int64_t(5)); + REQUIRE(op(0.0) == Catch::Approx(-0.0)); + REQUIRE(op(3.14) == Catch::Approx(-3.14)); + REQUIRE(op(-3.14) == Catch::Approx(3.14)); + + REQUIRE_INVALID_OPERANDS(op("string"s)); + REQUIRE_INVALID_OPERANDS(op(true)); + } + + SECTION("OperatorBool") { + OperatorBool op; + REQUIRE(op.name() == std::string("?")); + + REQUIRE(op(NullType{}) == false); + REQUIRE(op(false) == false); + REQUIRE(op(true) == true); + REQUIRE(op(int64_t(0)) == true); + REQUIRE(op(int64_t(1)) == true); + REQUIRE(op(0.0) == true); + REQUIRE(op(1.0) == true); + REQUIRE(op("") == true); + REQUIRE(op("hello") == true); + } + + SECTION("OperatorNot") { + OperatorNot op; + REQUIRE(op.name() == std::string("not")); + + REQUIRE(op(NullType{}) == true); + REQUIRE(op(false) == true); + REQUIRE(op(true) == false); + REQUIRE(op(int64_t(0)) == false); + REQUIRE(op(int64_t(1)) == false); + REQUIRE(op(0.0) == false); + REQUIRE(op(1.0) == false); + REQUIRE(op("") == false); + REQUIRE(op("hello") == false); + } + + SECTION("OperatorBitInv") { + OperatorBitInv op; + REQUIRE(op.name() == std::string("~")); + + REQUIRE(op(uint64_t(0)) == ~uint64_t(0)); + REQUIRE(op(uint64_t(0xFF)) == ~uint64_t(0xFF)); + REQUIRE(op(int64_t(0)) == int64_t(~uint64_t(0))); + REQUIRE(op(int64_t(-1)) == int64_t(0)); + + REQUIRE_INVALID_OPERANDS(op("string"s)); + REQUIRE_INVALID_OPERANDS(op(3.14)); + REQUIRE_INVALID_OPERANDS(op(true)); + } + + SECTION("OperatorLen") { + OperatorLen op; + REQUIRE(op.name() == std::string("#")); + + REQUIRE(op(""s) == int64_t(0)); + REQUIRE(op("hello"s) == int64_t(5)); + REQUIRE(op("hello world"s) == int64_t(11)); + + REQUIRE_INVALID_OPERANDS(op(int64_t(123))); + REQUIRE_INVALID_OPERANDS(op(3.14)); + REQUIRE_INVALID_OPERANDS(op(true)); + } + + SECTION("OperatorTypeof") { + OperatorTypeof op; + REQUIRE(op.name() == std::string("typeof")); + + REQUIRE(op(NullType{}) == "null"); + REQUIRE(op(true) == "bool"); + REQUIRE(op(false) == "bool"); + REQUIRE(op(int64_t(42)) == "int"); + REQUIRE(op(3.14) == "float"); + REQUIRE(op("hello"s) == "string"); + } +} + +TEST_CASE("Type conversion operators", "[operator.conversion]") { + SECTION("OperatorAsInt") { + OperatorAsInt op; + REQUIRE(op.name() == std::string("int")); + + REQUIRE(op(true) == int64_t(1)); + REQUIRE(op(false) == int64_t(0)); + REQUIRE(op(int64_t(42)) == int64_t(42)); + REQUIRE(op(3.14) == int64_t(3)); + REQUIRE(op(-3.99) == int64_t(-3)); + REQUIRE(op("123"s) == int64_t(123)); + REQUIRE(op("-456"s) == int64_t(-456)); + REQUIRE(op("not a number"s) == int64_t(0)); + REQUIRE(op(""s) == int64_t(0)); + REQUIRE(op(NullType{}) == int64_t(0)); + } + + SECTION("OperatorAsFloat") { + OperatorAsFloat op; + REQUIRE(op.name() == std::string("float")); + + REQUIRE(op(true) == 1.0); + REQUIRE(op(false) == 0.0); + REQUIRE(op(int64_t(42)) == 42.0); + REQUIRE(op(3.14) == 3.14); + REQUIRE(op("123.45"s) == 123.45); + REQUIRE(op("-67.89"s) == -67.89); + REQUIRE(op("not a number"s) == 0.0); + REQUIRE(op(""s) == 0.0); + REQUIRE(op(NullType{}) == 0.0); + } +} + +TEST_CASE("Binary arithmetic operators", "[operator.binary.arithmetic]") { + SECTION("OperatorAdd") { + OperatorAdd op; + REQUIRE(op.name() == std::string("+")); + + REQUIRE(op(int64_t(5), int64_t(3)) == int64_t(8)); + REQUIRE(op(int64_t(-5), int64_t(3)) == int64_t(-2)); + REQUIRE(op(3.14, 2.86) == Catch::Approx(6.0)); + REQUIRE(op(-1.5, 2.5) == Catch::Approx(1.0)); + REQUIRE(op(int64_t(5), 3.5) == Catch::Approx(8.5)); + REQUIRE(op(3.5, int64_t(5)) == Catch::Approx(8.5)); + REQUIRE(op("hello"s, " world"s) == "hello world"); + + REQUIRE_INVALID_OPERANDS(op(true, int64_t(123))); + } + + SECTION("OperatorSub") { + OperatorSub op; + REQUIRE(op.name() == std::string("-")); + + REQUIRE(op(int64_t(5), int64_t(3)) == int64_t(2)); + REQUIRE(op(int64_t(3), int64_t(5)) == int64_t(-2)); + REQUIRE(op(5.5, 2.5) == Catch::Approx(3.0)); + REQUIRE(op(2.5, 5.5) == Catch::Approx(-3.0)); + REQUIRE(op(int64_t(10), 3.5) == Catch::Approx(6.5)); + REQUIRE(op(3.5, int64_t(10)) == Catch::Approx(-6.5)); + + REQUIRE_INVALID_OPERANDS(op("test"s, "test"s)); + } + + SECTION("OperatorMul") { + OperatorMul op; + REQUIRE(op.name() == std::string("*")); + + REQUIRE(op(int64_t(5), int64_t(3)) == int64_t(15)); + REQUIRE(op(int64_t(-5), int64_t(3)) == int64_t(-15)); + REQUIRE(op(2.5, 4.0) == Catch::Approx(10.0)); + REQUIRE(op(-2.5, 4.0) == Catch::Approx(-10.0)); + REQUIRE(op(int64_t(5), 2.5) == Catch::Approx(12.5)); + REQUIRE(op(2.5, int64_t(5)) == Catch::Approx(12.5)); + + REQUIRE_INVALID_OPERANDS(op("test"s, "test"s)); + REQUIRE_INVALID_OPERANDS(op(true, 123)); + } + + SECTION("OperatorDiv") { + OperatorDiv op; + REQUIRE(op.name() == std::string("/")); + + REQUIRE(op(int64_t(15), int64_t(3)) == int64_t(5)); + REQUIRE(op(int64_t(16), int64_t(3)) == int64_t(5)); + REQUIRE(op(10.0, 4.0) == Catch::Approx(2.5)); + REQUIRE(op(-10.0, 4.0) == Catch::Approx(-2.5)); + REQUIRE(op(int64_t(10), 4.0) == Catch::Approx(2.5)); + REQUIRE(op(10.0, int64_t(4)) == Catch::Approx(2.5)); + } + + SECTION("OperatorMod") { + OperatorMod op; + REQUIRE(op.name() == std::string("%")); + + REQUIRE(op(int64_t(17), int64_t(5)) == int64_t(2)); + REQUIRE(op(int64_t(-17), int64_t(5)) == int64_t(-2)); + REQUIRE(op(int64_t(17), int64_t(-5)) == int64_t(2)); + + REQUIRE_INVALID_OPERANDS(op(3.14, 2.0)); + REQUIRE_INVALID_OPERANDS(op("test"s, "test"s)); + } +} + +TEST_CASE("Binary comparison operators", "[operator.binary.comparison]") { + SECTION("OperatorEq") { + OperatorEq op; + REQUIRE(op.name() == std::string("==")); + + REQUIRE(op(int64_t(5), int64_t(5)) == true); + REQUIRE(op(int64_t(5), int64_t(6)) == false); + REQUIRE(op(3.14, 3.14) == true); + REQUIRE(op(3.14, 3.15) == false); + REQUIRE(op("hello"s, "hello"s) == true); + REQUIRE(op("hello"s, "world"s) == false); + REQUIRE(op(true, true) == true); + REQUIRE(op(false, false) == true); + REQUIRE(op(true, false) == false); + REQUIRE(op(NullType{}, NullType{}) == true); + REQUIRE(op(NullType{}, int64_t(0)) == false); + REQUIRE(op(int64_t(5), 5.0) == true); + REQUIRE(op(5.0, int64_t(5)) == true); + REQUIRE(op(int64_t(5), 5.1) == false); + } +} From 7aafd96e4d2d88b0c438da3290c8154114f6742d Mon Sep 17 00:00:00 2001 From: Johannes Wolf Date: Sun, 19 Oct 2025 18:28:53 +0200 Subject: [PATCH 29/50] tests: Add more completion tests --- test/completion.cpp | 47 +++++++++++++++++++++++++++++++++++++-------- 1 file changed, 39 insertions(+), 8 deletions(-) diff --git a/test/completion.cpp b/test/completion.cpp index 03a6d4d5..a9eb75f3 100644 --- a/test/completion.cpp +++ b/test/completion.cpp @@ -21,7 +21,8 @@ const auto model = R"json( "this needs escaping": 6, "sub": { "child": 7 - } + }, + "with a space": 1 } )json"; @@ -80,13 +81,28 @@ TEST_CASE("CompleteString", "[completion.string-const]") { EXPECT_COMPLETION("1 > C", {}, "CONSTANT_1"); } -TEST_CASE("CompleteFunction", "[completion.function]") { - EXPECT_COMPLETION("spl", {}, "split", Type::FUNCTION); +TEST_CASE("Complete Function", "[completion.function]") { + EXPECT_COMPLETION("cou", {}, "count"); + EXPECT_COMPLETION("su", {}, "sum"); } -TEST_CASE("CompleteFieldOrString") { +TEST_CASE("Completion Limit", "[completion.option-limit]") { + CompletionOptions opts; + opts.limit = 3; + + auto comp = GetCompletion("", {}, &opts); + REQUIRE(comp.size() <= opts.limit); +} + +TEST_CASE("Complete in Expression", "[completion.complete-mid-expression]") { + EXPECT_COMPLETION("(field + oth)", 12, "other"); + EXPECT_COMPLETION("count(fie", 9, "field"); + EXPECT_COMPLETION("sub.ch > 123", 6, "child"); +} + +TEST_CASE("Complete SmartCas", "[completion.smart-case]") { // Complete both the field and the constants - EXPECT_COMPLETION("cons", {}, "constant", Type::FIELD); + EXPECT_COMPLETION("cons", {}, "constant", Type::FIELD, 4); EXPECT_COMPLETION("cons", {}, "CONSTANT_1", Type::CONSTANT); // Do not complete the field @@ -94,13 +110,28 @@ TEST_CASE("CompleteFieldOrString") { EXPECT_COMPLETION("CONS", {}, "CONSTANT_2", Type::CONSTANT); } -TEST_CASE("CompleteWildcardEquals") { +TEST_CASE("Complete Field with Special Characters", "[copletion.escape-field]") { + EXPECT_COMPLETION("with", {}, "[\"with a space\"]", Type::FIELD); +} + +TEST_CASE("Complete And/Or", "[copletion.and-or]") { + EXPECT_COMPLETION("true and f", {}, "field"); + EXPECT_COMPLETION("f and true", 1, "field"); + EXPECT_COMPLETION("false and f", {}, "field"); + EXPECT_COMPLETION("f and false", 1, "field"); + EXPECT_COMPLETION("true or f", {}, "field"); + EXPECT_COMPLETION("f or true", 1, "field"); + EXPECT_COMPLETION("false or f", {}, "field"); + EXPECT_COMPLETION("f or false", 1, "field"); +} + +TEST_CASE("Complete Wildcard Hint", "[completion.generate-eq-value-hint]") { EXPECT_COMPLETION("A_CONST", {}, "** = A_CONST", Type::HINT); EXPECT_COMPLETION("A_CONST", {}, "**.A_CONST", Type::HINT); EXPECT_COMPLETION("field", {}, "**.field", Type::HINT); } -TEST_CASE("CompleteWildcardComparison") { +TEST_CASE("Complete Wildcard Comparison", "[completion.generate-compare-recursive-hint]") { EXPECT_COMPLETION("field == 123", {}, "**.field == 123", Type::HINT); EXPECT_COMPLETION("name != \"Mulldrifter\"", {}, "**.name != \"Mulldrifter\"", Type::HINT); EXPECT_COMPLETION("count < 10", {}, "**.count < 10", Type::HINT); @@ -109,7 +140,7 @@ TEST_CASE("CompleteWildcardComparison") { EXPECT_COMPLETION("A_CONST != 42", {}, "**.A_CONST != 42", Type::HINT); } -TEST_CASE("CompleteSorted") { +TEST_CASE("Sort Completion", "[completion.sorted]") { CompletionOptions opts; opts.showWildcardHints = false; From fa2c7b0015644b28ecb5249ac6cbeac448e46811 Mon Sep 17 00:00:00 2001 From: Johannes Wolf Date: Sun, 19 Oct 2025 18:35:43 +0200 Subject: [PATCH 30/50] cmake: Set C++ standard to 20 for Catch2 --- test/CMakeLists.txt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 4ec71298..db283d37 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -1,6 +1,8 @@ project(test.simfil) -set(CMAKE_CXX_STANDARD 17) # Required for catch2 +set(CMAKE_CXX_STANDARD 20) # Required for catch2 +target_compile_features(Catch2 PRIVATE cxx_std_20) +target_compile_features(Catch2WithMain PRIVATE cxx_std_20) add_executable(test.simfil common.hpp From 5ebe148f37d142dc17e4b7a2385d2fbe12c31a1a Mon Sep 17 00:00:00 2001 From: Johannes Wolf Date: Sun, 19 Oct 2025 19:08:20 +0200 Subject: [PATCH 31/50] perf: Replace string concatenation with fmt --- src/expressions.cpp | 68 ++++++++++++++++++++++----------------------- src/simfil.cpp | 44 ----------------------------- 2 files changed, 34 insertions(+), 78 deletions(-) diff --git a/src/expressions.cpp b/src/expressions.cpp index c937a927..df442248 100644 --- a/src/expressions.cpp +++ b/src/expressions.cpp @@ -1,13 +1,16 @@ #include "expressions.h" +#include "fmt/format.h" #include "simfil/environment.h" #include "simfil/result.h" #include "simfil/value.h" #include "simfil/function.h" #include "fmt/core.h" +#include "fmt/ranges.h" #include "src/expected.h" #include +#include namespace simfil { @@ -141,7 +144,7 @@ void WildcardExpr::accept(ExprVisitor& v) auto WildcardExpr::toString() const -> std::string { - return "**"; + return "**"s; } AnyChildExpr::AnyChildExpr() = default; @@ -183,7 +186,7 @@ void AnyChildExpr::accept(ExprVisitor& v) auto AnyChildExpr::toString() const -> std::string { - return "*"; + return "*"s; } FieldExpr::FieldExpr(std::string name) @@ -300,14 +303,11 @@ void MultiConstExpr::accept(ExprVisitor& v) auto MultiConstExpr::toString() const -> std::string { - auto list = ""s; - for (const auto& v : values_) { - if (!list.empty()) - list += " "; - list += v.toString(); - } + auto items = values_ | std::views::transform([](const auto& arg) { + return arg.toString(); + }); - return "{"s + list + "}"s; + return fmt::format("{{{}}}", fmt::join(items, " ")); } ConstExpr::ConstExpr(Value value) @@ -342,7 +342,7 @@ void ConstExpr::accept(ExprVisitor& v) auto ConstExpr::toString() const -> std::string { if (value_.isa(ValueType::String)) - return "\""s + value_.toString() + "\""s; + return fmt::format("\"{}\"", value_.toString()); return value_.toString(); } @@ -412,7 +412,7 @@ void SubscriptExpr::accept(ExprVisitor& v) auto SubscriptExpr::toString() const -> std::string { - return "(index "s + left_->toString() + " "s + index_->toString() + ")"s; + return fmt::format("(index {} {})", left_->toString(), index_->toString()); } SubExpr::SubExpr(ExprPtr sub) @@ -461,7 +461,7 @@ auto SubExpr::ieval(Context ctx, Value&& val, const ResultFn& ores) -> tl::expec auto SubExpr::toString() const -> std::string { - return "(sub "s + left_->toString() + " "s + sub_->toString() + ")"s; + return fmt::format("(sub {} {})", left_->toString(), sub_->toString()); } auto SubExpr::clone() const -> ExprPtr @@ -537,11 +537,11 @@ auto AnyExpr::toString() const -> std::string if (args_.empty()) return "any()"s; - auto s = "(any"s; - for (const auto& arg : args_) { - s += " "s + arg->toString(); - } - return s + ")"s; + auto items = args_ | std::views::transform([](const auto& arg) { + return arg->toString(); + }); + + return fmt::format("(any {})", fmt::join(items, " ")); } EachExpr::EachExpr(std::vector args) @@ -606,11 +606,11 @@ auto EachExpr::toString() const -> std::string if (args_.empty()) return "each()"s; - auto s = "(each"s; - for (const auto& arg : args_) { - s += " "s + arg->toString(); - } - return s + ")"s; + auto items = args_ | std::views::transform([](const auto& arg) { + return arg->toString(); + }); + + return fmt::format("(each {})", fmt::join(items, " ")); } CallExpression::CallExpression(std::string name, std::vector args) @@ -668,13 +668,13 @@ void CallExpression::accept(ExprVisitor& v) auto CallExpression::toString() const -> std::string { if (args_.empty()) - return "("s + name_ + ")"s; + return fmt::format("({})", name_); - std::string s = "("s + name_; - for (const auto& arg : args_) { - s += " "s + arg->toString(); - } - return s + ")"s; + auto items = args_ | std::views::transform([](const auto& arg) { + return arg->toString(); + }); + + return fmt::format("({} {})", name_, fmt::join(items, " ")); } PathExpr::PathExpr(ExprPtr right) @@ -739,7 +739,7 @@ void PathExpr::accept(ExprVisitor& v) auto PathExpr::toString() const -> std::string { - return "(. "s + left_->toString() + " "s + right_->toString() + ")"s; + return fmt::format("(. {} {})", left_->toString(), right_->toString()); } UnpackExpr::UnpackExpr(ExprPtr sub) @@ -790,7 +790,7 @@ void UnpackExpr::accept(ExprVisitor& v) auto UnpackExpr::toString() const -> std::string { - return "(... "s + sub_->toString() + ")"s; + return fmt::format("(... {})", sub_->toString()); } UnaryWordOpExpr::UnaryWordOpExpr(std::string ident, ExprPtr left) @@ -834,7 +834,7 @@ auto UnaryWordOpExpr::clone() const -> ExprPtr auto UnaryWordOpExpr::toString() const -> std::string { - return "("s + ident_ + " "s + left_->toString() + ")"s; + return fmt::format("({} {})", ident_, left_->toString()); } BinaryWordOpExpr::BinaryWordOpExpr(std::string ident, ExprPtr left, ExprPtr right) @@ -890,7 +890,7 @@ auto BinaryWordOpExpr::clone() const -> ExprPtr auto BinaryWordOpExpr::toString() const -> std::string { - return "("s + ident_ + " "s + left_->toString() + " "s + right_->toString() + ")"s; + return fmt::format("({} {} {})", ident_, left_->toString(), right_->toString()); } AndExpr::AndExpr(ExprPtr left, ExprPtr right) @@ -940,7 +940,7 @@ auto AndExpr::clone() const -> ExprPtr auto AndExpr::toString() const -> std::string { - return "(and "s + left_->toString() + " "s + right_->toString() + ")"s; + return fmt::format("(and {} {})", left_->toString(), right_->toString()); } OrExpr::OrExpr(ExprPtr left, ExprPtr right) @@ -994,7 +994,7 @@ auto OrExpr::clone() const -> ExprPtr auto OrExpr::toString() const -> std::string { - return "(or "s + left_->toString() + " "s + right_->toString() + ")"s; + return fmt::format("(or {} {})", left_->toString(), right_->toString()); } void ExprVisitor::visit(Expr& e) diff --git a/src/simfil.cpp b/src/simfil.cpp index 3c86daa0..343d3b8a 100644 --- a/src/simfil.cpp +++ b/src/simfil.cpp @@ -65,44 +65,6 @@ enum Precedence { LOGIC = 1, // and, or }; -/** - * - */ -template -static auto expect(const ExprPtr& e, Type... types) -> std::optional> -{ - const auto type2str = [](Expr::Type t) { - switch (t) { - case Expr::Type::FIELD: return "field"s; - case Expr::Type::PATH: return "path"s; - case Expr::Type::SUBEXPR: return "subexpression"s; - case Expr::Type::SUBSCRIPT: return "subscript"s; - case Expr::Type::VALUE: return "value"s; - } - return "error"s; - }; - - if (!e) - return unexpected(Error::InvalidExpression, "Expected expression"); - - if constexpr (sizeof...(types) >= 1) { - Expr::Type list[] = {types...}; - for (auto i = 0; i < sizeof...(types); ++i) { - if (e->type() == list[i]) - return {}; - } - - std::string typeNames; - for (auto i = 0; i < sizeof...(types); ++i) { - if (!typeNames.empty()) - typeNames += " or "; - typeNames += type2str(list[i]); - } - - return unexpected(Error::InvalidExpression, fmt::format("Expected {} got {}", typeNames, type2str(e->type()))); - } -} - /** * Returns if a word should be parsed as a symbol (string). * This is true for all UPPER_CASE words. @@ -482,9 +444,6 @@ class SubscriptParser : public PrefixParselet, public InfixParselet auto parse(Parser& p, ExprPtr left, Token t) const -> expected override { - if (auto err = expect(left, Expr::Type::FIELD, Expr::Type::PATH, Expr::Type::VALUE, Expr::Type::SUBEXPR, Expr::Type::SUBSCRIPT)) - return *err; - auto _ = scopedNotInPath(p); auto body = p.parseTo(Token::RBRACK); if (!body) @@ -523,9 +482,6 @@ class SubSelectParser : public PrefixParselet, public InfixParselet auto parse(Parser& p, ExprPtr left, Token t) const -> expected override { - if (auto err = expect(left, Expr::Type::FIELD, Expr::Type::PATH, Expr::Type::VALUE, Expr::Type::SUBEXPR, Expr::Type::SUBSCRIPT)) - return *err; - auto _ = scopedNotInPath(p); auto body = p.parseTo(Token::RBRACE); if (!body) From 0c7bb654b32e51931c50d376b9544d8cd65bb483 Mon Sep 17 00:00:00 2001 From: Johannes Wolf Date: Fri, 24 Oct 2025 23:26:05 +0200 Subject: [PATCH 32/50] expr: Fix some error forwarding --- src/expressions.cpp | 10 +++++++--- test/performance.cpp | 15 +++++++++++++++ 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/src/expressions.cpp b/src/expressions.cpp index df442248..aa99ec64 100644 --- a/src/expressions.cpp +++ b/src/expressions.cpp @@ -284,8 +284,10 @@ auto MultiConstExpr::constant() const -> bool auto MultiConstExpr::ieval(Context ctx, const Value&, const ResultFn& res) -> tl::expected { for (const auto& v : values_) { - if (res(ctx, v) == Result::Stop) + if (auto r = res(ctx, v); r && *r == Result::Stop) return Result::Stop; + else if (!r) + return tl::unexpected(std::move(r.error())); } return Result::Continue; @@ -754,7 +756,7 @@ auto UnpackExpr::type() const -> Type auto UnpackExpr::ieval(Context ctx, const Value& val, const ResultFn& res) -> tl::expected { auto anyval = false; - auto r = sub_->eval(ctx, val, LambdaResultFn([&res, &anyval](Context ctx, Value&& v) { + auto r = sub_->eval(ctx, val, LambdaResultFn([&res, &anyval](Context ctx, Value&& v) -> tl::expected { if (v.isa(ValueType::TransientObject)) { const auto& obj = v.as(); auto r = Result::Continue; @@ -767,8 +769,10 @@ auto UnpackExpr::ieval(Context ctx, const Value& val, const ResultFn& res) -> tl return Result::Stop; } else { anyval = true; - if (res(ctx, std::move(v)) == Result::Stop) + if (auto r = res(ctx, std::move(v)); r && *r == Result::Stop) return Result::Stop; + else if (!r) + return tl::unexpected(std::move(r.error())); } return Result::Continue; })); diff --git a/test/performance.cpp b/test/performance.cpp index 1c8d7d35..c1e8de01 100644 --- a/test/performance.cpp +++ b/test/performance.cpp @@ -173,3 +173,18 @@ TEST_CASE("Big model queries", "[perf.big-model-queries]") { REQUIRE_RESULT("count(*.id == 250)", "1"); CALLGRIND_STOP_INSTRUMENTATION; } + +TEST_CASE("Slow Compile Time Query", "[perf.slow-compile-time-query]") { + if (RUNNING_ON_VALGRIND) { + SKIP("Skipping benchmarks when running under valgrind"); + } + + const auto model = generate_model(1); + + BENCHMARK("Find Primes") { + auto value = result(model, "count(range(1,1000)...{count((_ % range(1,_)...) == 0) == 2})"); + REQUIRE(value); + REQUIRE(value->front().template as() == 168); + return value; + }; +} From f4566b898d7cbfd589f5e0e94ef55b4e56d6c4f1 Mon Sep 17 00:00:00 2001 From: Johannes Wolf Date: Sat, 25 Oct 2025 00:25:44 +0200 Subject: [PATCH 33/50] expr: Fix function error forwarding, add tests --- src/function.cpp | 6 ++++-- test/common.cpp | 2 ++ test/common.hpp | 2 +- test/completion.cpp | 1 - test/simfil.cpp | 15 +++++++++++++++ 5 files changed, 22 insertions(+), 4 deletions(-) diff --git a/src/function.cpp b/src/function.cpp index 5923fe6b..9e1fc28c 100644 --- a/src/function.cpp +++ b/src/function.cpp @@ -506,7 +506,7 @@ auto SumFn::eval(Context ctx, const Value& val, const std::vector& args return Result::Continue; })); - (void)args[0]->eval(ctx, val, LambdaResultFn([&, n = 0](Context ctx, Value&& vv) mutable -> tl::expected { + auto argRes = args[0]->eval(ctx, val, LambdaResultFn([&, n = 0](Context ctx, Value&& vv) mutable -> tl::expected { if (subexpr) { auto ov = model_ptr::make(vv); ov->set(StringPool::OverlaySum, sum); @@ -514,11 +514,12 @@ auto SumFn::eval(Context ctx, const Value& val, const std::vector& args ov->set(StringPool::OverlayIndex, Value::make(static_cast(n))); n += 1; - subexpr->eval(ctx, Value::field(ov), LambdaResultFn([&ov, &sum](auto ctx, Value&& vv) { + auto subRes = subexpr->eval(ctx, Value::field(ov), LambdaResultFn([&ov, &sum](auto ctx, Value&& vv) { ov->set(StringPool::OverlaySum, vv); sum = vv; return Result::Continue; })); + TRY_EXPECTED(subRes); } else { if (sum.isa(ValueType::Null)) { sum = std::move(vv); @@ -533,6 +534,7 @@ auto SumFn::eval(Context ctx, const Value& val, const std::vector& args return Result::Continue; })); + TRY_EXPECTED(argRes); return res(ctx, sum); } diff --git a/test/common.cpp b/test/common.cpp index 2a2b956b..cb72b3f1 100644 --- a/test/common.cpp +++ b/test/common.cpp @@ -46,6 +46,8 @@ auto JoinedResult(std::string_view query, std::optional json) -> st INFO("AST: " << (*ast)->expr().toString()); auto root = model.value()->root(0); + REQUIRE(root); + auto res = eval(env, **ast, **root, nullptr); if (!res) { INFO("ERROR: " << res.error().message); diff --git a/test/common.hpp b/test/common.hpp index 54fa3106..3291a10f 100644 --- a/test/common.hpp +++ b/test/common.hpp @@ -63,7 +63,7 @@ class PanicFn : public simfil::Function { static const FnInfo info{ "panic", - "Thrown an error", + "Raise an error", "panic()" }; diff --git a/test/completion.cpp b/test/completion.cpp index a9eb75f3..cc5ae355 100644 --- a/test/completion.cpp +++ b/test/completion.cpp @@ -2,7 +2,6 @@ #include #include #include -#include #include "common.hpp" #include "simfil/environment.h" diff --git a/test/simfil.cpp b/test/simfil.cpp index 28416a53..2a1f2b42 100644 --- a/test/simfil.cpp +++ b/test/simfil.cpp @@ -472,28 +472,43 @@ TEST_CASE("Model Functions", "[yaml.mode-functions]") { } SECTION("Test arr(... )") { REQUIRE_RESULT("arr(2,3,5,7,'ok')", "2|3|5|7|ok"); + + REQUIRE_ERROR("arr(0,panic(),2)"); } SECTION("Test split(... )") { REQUIRE_RESULT("split('hello.this.is.a.test.', '.')", "hello|this|is|a|test|"); REQUIRE_RESULT("split('hello.this.is.a.test.', '.', false)", "hello|this|is|a|test"); + + REQUIRE_ERROR("split(panic(), '.')"); + REQUIRE_ERROR("split('a.b.c', panic())"); } SECTION("Test select(... )") { REQUIRE_RESULT("select(split('a.b.c.d', '.'), a)", "b"); REQUIRE_RESULT("select(split('a.b.c.d', '.'), 0)", "a"); REQUIRE_RESULT("select(split('a.b.c.d', '.'), 1, 2)", "b|c"); REQUIRE_RESULT("select(split('a.b.c.d', '.'), 1, 0)", "b|c|d"); + + REQUIRE_ERROR("select(panic(), 0)"); + REQUIRE_ERROR("select(0, panic())"); } SECTION("Test sum(... )") { REQUIRE_RESULT("sum(range(1, 10)...)", "55"); REQUIRE_RESULT("sum(range(1, 10)..., $sum + $val)", "55"); REQUIRE_RESULT("sum(range(1, 10)..., $sum + $val, 10)", "65"); REQUIRE_RESULT("sum(range(1, 10)..., $sum * $val, 1)", "3628800"); + + // Test error forwarding + REQUIRE_ERROR("sum(panic(), 0)"); + REQUIRE_ERROR("sum(range(1, 10)..., panic())"); + REQUIRE_ERROR("sum(range(1, 10)..., 0, panic())"); } SECTION("Count non-false values generated by arr(...)") { REQUIRE_RESULT("count(arr(null, null))", "0"); REQUIRE_RESULT("count(arr(true, null))", "1"); REQUIRE_RESULT("count(arr(null, true))", "1"); REQUIRE_RESULT("count(arr(true, true))", "2"); + + REQUIRE_ERROR("count(panic())"); } } From ea1568c4b15ccfb987741d64e98439667dba9ab8 Mon Sep 17 00:00:00 2001 From: Johannes Wolf Date: Sun, 26 Oct 2025 20:54:13 +0100 Subject: [PATCH 34/50] test: Add test cases for simfil::Value --- test/CMakeLists.txt | 1 + test/value.cpp | 403 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 404 insertions(+) create mode 100644 test/value.cpp diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index db283d37..53b8792b 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -15,6 +15,7 @@ add_executable(test.simfil performance.cpp arena.cpp error.cpp + value.cpp operator.cpp) target_link_libraries(test.simfil diff --git a/test/value.cpp b/test/value.cpp new file mode 100644 index 00000000..e6a66231 --- /dev/null +++ b/test/value.cpp @@ -0,0 +1,403 @@ +#include +#include +#include + +#include "simfil/value.h" +#include "simfil/model/model.h" +#include "simfil/transient.h" + +using namespace simfil; +using namespace std::string_literals; + +TEST_CASE("Value Constructors", "[value.value-constructor]") { + SECTION("Boolean") { + auto trueVal = Value::t(); + REQUIRE(trueVal.isa(ValueType::Bool)); + REQUIRE(trueVal.asBool() == true); + + auto falseVal = Value::f(); + REQUIRE(falseVal.isa(ValueType::Bool)); + REQUIRE(falseVal.asBool() == false); + } + + SECTION("Null") { + auto nullVal = Value::null(); + REQUIRE(nullVal.isa(ValueType::Null)); + } + + SECTION("Undef") { + auto undefVal = Value::undef(); + REQUIRE(undefVal.isa(ValueType::Undef)); + } + + SECTION("Model") { + auto modelVal = Value::model(); + REQUIRE(modelVal.isa(ValueType::Object)); + } + + SECTION("StrRef") { + auto strVal = Value::strref("test"); + REQUIRE(strVal.isa(ValueType::String)); + REQUIRE(strVal.as() == "test"); + } + + SECTION("Make Bool") { + auto val = Value::make(true); + REQUIRE(val.isa(ValueType::Bool)); + REQUIRE(val.asBool() == true); + + auto val2 = Value::make(false); + REQUIRE(val2.isa(ValueType::Bool)); + REQUIRE(val2.asBool() == false); + } + + SECTION("Make int64_t") { + auto val = Value::make(int64_t(42)); + REQUIRE(val.isa(ValueType::Int)); + REQUIRE(val.as() == 42); + } + + SECTION("Make double") { + auto val = Value::make(3.14); + REQUIRE(val.isa(ValueType::Float)); + REQUIRE(val.as() == Catch::Approx(3.14)); + } + + SECTION("Make string") { + auto val = Value::make("hello"s); + REQUIRE(val.isa(ValueType::String)); + REQUIRE(val.as() == "hello"); + } + + SECTION("Make string_view") { + std::string_view sv = "world"; + auto val = Value::make(sv); + REQUIRE(val.isa(ValueType::String)); + REQUIRE(val.as() == "world"); + } + + SECTION("Type constructor") { + Value val(ValueType::Null); + REQUIRE(val.isa(ValueType::Null)); + } + + SECTION("Type and value constructor") { + Value val(ValueType::Int, int64_t(123)); + REQUIRE(val.isa(ValueType::Int)); + REQUIRE(val.as() == 123); + } + + SECTION("ScalarValueType constructor") { + ScalarValueType scalar = (int64_t)123; + Value val(std::move(scalar)); + REQUIRE(val.isa(ValueType::Int)); + REQUIRE(val.as() == 123); + + ScalarValueType scalar2 = "Brainstorm"s; + Value val2(std::move(scalar2)); + REQUIRE(val2.isa(ValueType::String)); + REQUIRE(val2.as() == "Brainstorm"); + } + + SECTION("Copy & Move constructor") { + Value original = Value::make(123); + + Value copied = original; + REQUIRE(copied.isa(ValueType::Int)); + REQUIRE(copied.as() == 123); + REQUIRE(original.isa(ValueType::Int)); + REQUIRE(original.as() == 123); + + Value moved = std::move(original); + REQUIRE(moved.isa(ValueType::Int)); + REQUIRE(moved.as() == 123); + } +} + +TEST_CASE("Value field() methods", "[value.field]") { + auto model = std::make_shared(); + + SECTION("field() from scalar ModelNode") { + auto intNode = model->newValue(int64_t(42)); + auto val = Value::field(*intNode); + REQUIRE(val.isa(ValueType::Int)); + REQUIRE(val.as() == 42); + + auto strNode = model->newValue("test"s); + auto val2 = Value::field(*strNode); + REQUIRE(val2.isa(ValueType::String)); + REQUIRE(val2.as() == "test"); + } + + SECTION("field() from object ModelNode") { + auto objNode = model->newObject(1); + objNode->addField("key", model->newValue(int64_t(123))); + + auto val = Value::field(*objNode); + REQUIRE(val.isa(ValueType::Object)); + REQUIRE(val.node() != nullptr); + } + + SECTION("field() from array ModelNode") { + auto arrNode = model->newArray(2); + arrNode->append(model->newValue(int64_t(1))); + arrNode->append(model->newValue(int64_t(2))); + + auto val = Value::field(*arrNode); + REQUIRE(val.isa(ValueType::Array)); + REQUIRE(val.node() != nullptr); + } + + SECTION("field() from model_ptr") { + auto nodePtr = model->newValue((int64_t)88); + auto val = Value::field(nodePtr); + REQUIRE(val.isa(ValueType::Int)); + REQUIRE(val.as() == 88); + } +} + +TEST_CASE("Value As", "[value.as]") { + SECTION("as()") { + auto val = Value::make(int64_t(42)); + REQUIRE(val.as() == 42); + } + + SECTION("as()") { + auto val = Value::make(3.14); + REQUIRE(val.as() == Catch::Approx(3.14)); + } + + SECTION("as() from string") { + auto val = Value::make("hello"s); + REQUIRE(val.as() == "hello"); + } + + SECTION("as() from string_view") { + auto val = Value::strref("world"); + REQUIRE(val.as() == "world"); + } + + SECTION("as()") { + auto model = std::make_shared(); + auto objNode = model->newObject(0); + auto val = Value::field(objNode); + auto ptr = val.as(); + REQUIRE(!!ptr); + } + + SECTION("as()") { + auto model = std::make_shared(); + auto arrNode = model->newArray(0); + auto val = Value::field(arrNode); + auto ptr = val.as(); + REQUIRE(!!ptr); + } +} + +TEST_CASE("Value visit() method", "[value.visit]") { + SECTION("Visit UndefinedType") { + auto val = Value::undef(); + auto result = val.visit([](auto&& v) -> std::string { + using T = std::decay_t; + if constexpr (std::is_same_v) { + return "undefined"; + } + return "other"; + }); + REQUIRE(result == "undefined") +; + } + + SECTION("Visit NullType") { + auto val = Value::null(); + auto result = val.visit([](auto&& v) -> std::string { + using T = std::decay_t; + if constexpr (std::is_same_v) { + return "null"; + } + return "other"; + }); + REQUIRE(result == "null"); + } + + SECTION("Visit bool") { + auto val = Value::t() +; + auto result = val.visit([](auto&& v) -> bool { + using T = std::decay_t; + if constexpr (std::is_same_v) { + return v; + } + return false; + }); + REQUIRE(result == true); + } + + SECTION("Visit int64_t") { + auto val = Value::make(int64_t(42)); + auto result = val.visit([](auto&& v) -> int64_t { + using T = std::decay_t; + if constexpr (std::is_same_v) { + return v; + } + return 0; + }); + REQUIRE(result == 42); + } + + SECTION("Visit double") { + auto val = Value::make(3.14); + auto result = val.visit([](auto&& v) -> double { + using T = std::decay_t; + if constexpr (std::is_same_v) { + return v; + } + return 0.0; + }); + REQUIRE(result == Catch::Approx(3.14)); + } + + SECTION("Visit string") { + auto val = Value::make("Preordain"s); + auto result = val.visit([](auto&& v) -> std::string { + using T = std::decay_t; + if constexpr (std::is_same_v) { + return v; + } + return ""; + }); + REQUIRE(result == "Preordain"); + } + + SECTION("Visit ModelNode with valid pointer") { + auto model = std::make_shared(); + auto objNode = model->newObject(0); + auto val = Value::field(objNode); + + auto result = val.visit([](auto&& v) -> bool { + using T = std::decay_t; + if constexpr (std::is_same_v) { + return true; + } else if constexpr (std::is_same_v) { + return false; + } + return false; + }); + REQUIRE(result); + } + + SECTION("Visit ModelNode with null pointer") { + Value val(ValueType::Object); + auto result = val.visit([](auto&& v) -> bool { + using T = std::decay_t; + if constexpr (std::is_same_v) { + return true; + } + return false; + }); + REQUIRE(result); + } +} + +TEST_CASE("Value toString() method", "[value.toString]") { + REQUIRE(Value::undef().toString() == "undef"); + REQUIRE(Value::null().toString() == "null"); + REQUIRE(Value::t().toString() == "true"); + REQUIRE(Value::f().toString() == "false"); + REQUIRE(Value::make(int64_t(123)).toString() == "123"); + REQUIRE(Value::make(int64_t(-123)).toString() == "-123"); + REQUIRE(Value::make(double(3.14)).toString().find("3.14") == 0); + REQUIRE(Value::make("Ponder"s).toString() == "Ponder"); +} + +TEST_CASE("Value utility methods", "[value.utilities]") { + SECTION("getScalar() method") { + auto intVal = Value::make(int64_t(123)); + auto scalar = intVal.getScalar(); + REQUIRE(std::holds_alternative(scalar)); + REQUIRE(std::get(scalar) == 123); + + auto strVal = Value::make("test"s); + auto scalar2 = strVal.getScalar(); + REQUIRE(std::holds_alternative(scalar2)); + REQUIRE(std::get(scalar2) == "test"); + } + + SECTION("stringViewValue() method") { + auto strVal = Value::strref("Snap"); + auto svPtr = strVal.stringViewValue(); + REQUIRE(svPtr != nullptr); + REQUIRE(*svPtr == "Snap"); + + auto intVal = Value::make(123); + auto svPtr2 = intVal.stringViewValue(); + REQUIRE(svPtr2 == nullptr); // No string_view pointer + } + + SECTION("nodePtr() method") { + auto model = std::make_shared(); + auto objNode = model->newObject(0); + auto val = Value::field(objNode); + REQUIRE(val.nodePtr() != nullptr); + + REQUIRE(Value::make(123).nodePtr() == nullptr); + } + + SECTION("node() method") { + auto model = std::make_shared(); + auto objNode = model->newObject(0); + auto val = Value::field(objNode); + auto node = val.node(); + REQUIRE(val.node() != nullptr); + + REQUIRE(Value::make(42).node() == nullptr); + } +} + +TEST_CASE("valueType2String() function", "[value.type2string]") { + REQUIRE(valueType2String(ValueType::Undef) == "undef"s); + REQUIRE(valueType2String(ValueType::Null) == "null"s); + REQUIRE(valueType2String(ValueType::Bool) == "bool"s); + REQUIRE(valueType2String(ValueType::Int) == "int"s); + REQUIRE(valueType2String(ValueType::Float) == "float"s); + REQUIRE(valueType2String(ValueType::String) == "string"s); + REQUIRE(valueType2String(ValueType::TransientObject) == "transient"s); + REQUIRE(valueType2String(ValueType::Object) == "object"s); + REQUIRE(valueType2String(ValueType::Array) == "array"s); +} + +TEST_CASE("getNumeric() function", "[value.numeric]") { + SECTION("getNumeric from ith int") { + auto val = Value::make(int64_t(123)); + auto [ok, result] = getNumeric(val); + REQUIRE(ok); + REQUIRE(result == Catch::Approx(123.0)); + } + + SECTION("getNumeric from float") { + auto val = Value::make(3.14); + auto [ok, result] = getNumeric(val); + REQUIRE(ok); + REQUIRE(result == Catch::Approx(3.14)); + } + + SECTION("getNumeric from non-numeric") { + auto val = Value::make("Cloudpost"s); + auto [ok, result] = getNumeric(val); + REQUIRE_FALSE(ok); + } + + SECTION("getNumeric from double") { + ScalarValueType scalar = 2.718; + auto [ok, result] = getNumeric(scalar); + REQUIRE(ok); + REQUIRE(result == Catch::Approx(2.718f)); + } + + SECTION("getNumeric from int64_t") { + ScalarValueType scalar = int64_t(99); + auto [ok, result] = getNumeric(scalar); + REQUIRE(ok); + REQUIRE(result == 99); + } +} From 95a7fd84088e0cf495cff5200a7e6718d8d7768b Mon Sep 17 00:00:00 2001 From: Johannes Wolf Date: Sun, 26 Oct 2025 21:13:53 +0100 Subject: [PATCH 35/50] test: Make test macros require semicolon --- test/simfil.cpp | 35 ++++++++++++++++++----------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/test/simfil.cpp b/test/simfil.cpp index 2a1f2b42..05c7ba04 100644 --- a/test/simfil.cpp +++ b/test/simfil.cpp @@ -19,13 +19,14 @@ static constexpr auto StaticTestKey = StringPool::NextStaticId; REQUIRE(JoinedResult((query)) == (result)) #define REQUIRE_AST(input, output) \ - REQUIRE(Compile(input, false)->expr().toString() == (output)); + REQUIRE(Compile(input, false)->expr().toString() == (output)) #define REQUIRE_ERROR(input) \ - REQUIRE(CompileError(input, false).message != ""); + REQUIRE(CompileError(input, false).message != "") #define REQUIRE_AST_AUTOWILDCARD(input, output) \ - REQUIRE(Compile(input, true)->expr().toString() == (output)); + REQUIRE(Compile(input, true)->expr().toString() == (output)) + TEST_CASE("Int", "[ast.integer]") { REQUIRE_AST("1", "1"); @@ -62,11 +63,8 @@ TEST_CASE("Wildcard", "[ast.wildcard]") { TEST_CASE("OperatorConst", "[ast.operator]") { /* Arithmetic */ - REQUIRE_AST("-1", "-1"); - REQUIRE_AST("-1.1","-1.100000"); REQUIRE_AST("1+2", "3"); REQUIRE_AST("1.5+2", "3.500000"); - REQUIRE_AST("1.5-2", "-0.500000"); REQUIRE_AST("2*2", "4"); REQUIRE_AST("2.5*2", "5.000000"); REQUIRE_AST("8/2", "4"); @@ -75,16 +73,8 @@ TEST_CASE("OperatorConst", "[ast.operator]") { REQUIRE_AST("a+2", "(+ a 2)"); REQUIRE_AST("2+a", "(+ 2 a)"); REQUIRE_AST("a+b", "(+ a b)"); - REQUIRE_AST("1+null", "null"); - REQUIRE_AST("null+1", "null"); - REQUIRE_AST("1*null", "null"); - REQUIRE_AST("null*1", "null"); - REQUIRE_AST("1-null", "null"); - REQUIRE_AST("null-1", "null"); - REQUIRE_AST("1/null", "null"); - REQUIRE_AST("null/1", "null"); - REQUIRE_AST("null%null", "null"); - REQUIRE_AST("null/null", "null"); + REQUIRE_ERROR("1+panic()"); + REQUIRE_ERROR("panic()+1"); auto GetError = [&](std::string_view query) -> std::string { Environment env(Environment::WithNewStringCache); @@ -132,6 +122,16 @@ TEST_CASE("OperatorConst", "[ast.operator]") { REQUIRE_AST("nullnull", "false"); REQUIRE_AST("null==null", "true"); + REQUIRE_AST("1+null", "null"); + REQUIRE_AST("null+1", "null"); + REQUIRE_AST("1*null", "null"); + REQUIRE_AST("null*1", "null"); + REQUIRE_AST("1-null", "null"); + REQUIRE_AST("null-1", "null"); + REQUIRE_AST("1/null", "null"); + REQUIRE_AST("null/1", "null"); + REQUIRE_AST("null%null", "null"); + REQUIRE_AST("null/null", "null"); /* Typeof */ REQUIRE_AST("typeof 'abc'", "\"string\""); @@ -287,8 +287,9 @@ TEST_CASE("CompareIncompatibleTypesFields", "[ast.compare-incompatible-types-fie TEST_CASE("OperatorNegate", "[ast.operator-negate]") { REQUIRE_ERROR("-('abc')"); REQUIRE_ERROR("-(true)"); + REQUIRE_ERROR("-panic()"); REQUIRE_AST("-(1)", "-1"); - REQUIRE_AST("-(1.0)", "-1.000000"); + REQUIRE_AST("-(1.1)", "-1.100000"); REQUIRE_AST("-(null)", "null"); } From 6dc842cf1a663f3d094e264ab72bcd3cd9e63024 Mon Sep 17 00:00:00 2001 From: Johannes Wolf Date: Sun, 26 Oct 2025 21:56:43 +0100 Subject: [PATCH 36/50] test: Test range error forwarding --- test/simfil.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/simfil.cpp b/test/simfil.cpp index 05c7ba04..2bd02f0a 100644 --- a/test/simfil.cpp +++ b/test/simfil.cpp @@ -358,7 +358,9 @@ TEST_CASE("ModeSetter", "[ast.mode-setter]") { } TEST_CASE("UtilityFns", "[ast.functions]") { - REQUIRE_AST("range(a,b)", "(range a b)"); /* Can not optimize */ + REQUIRE_ERROR("range(panic(), 5)"); + REQUIRE_ERROR("range(1, panic())"); + REQUIRE_AST("range(a,b)", "(range a b)"); /* Ca not optimize */ REQUIRE_AST("range(1,5)", "1..5"); REQUIRE_AST("range(1,5)==0", "false"); REQUIRE_AST("range(1,5)==3", "true"); From 1bebe288034b455c5c92c26f6000087cc59561d8 Mon Sep 17 00:00:00 2001 From: Johannes Wolf Date: Sun, 26 Oct 2025 22:05:34 +0100 Subject: [PATCH 37/50] sonar: Add sonar-project settings file --- sonar-project.properties | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 sonar-project.properties diff --git a/sonar-project.properties b/sonar-project.properties new file mode 100644 index 00000000..8e27eddf --- /dev/null +++ b/sonar-project.properties @@ -0,0 +1,2 @@ +# Exclude from duplicate lines/blocks check +sonar.cpd.exclusions=test/simfil.cpp From a6bba7bd051b7e8a07e7aaf591a9bcfd5185cb4f Mon Sep 17 00:00:00 2001 From: Johannes Wolf Date: Sun, 26 Oct 2025 22:11:54 +0100 Subject: [PATCH 38/50] tree: Fix some SonarQube findings --- include/simfil/operator.h | 6 +++--- src/expression-patterns.h | 6 +++--- src/expressions.cpp | 2 +- src/function.cpp | 6 +++--- test/common.cpp | 4 ++-- test/operator.cpp | 2 +- test/value.cpp | 25 ++++++++++++------------- 7 files changed, 25 insertions(+), 26 deletions(-) diff --git a/include/simfil/operator.h b/include/simfil/operator.h index b6598192..351bca9f 100644 --- a/include/simfil/operator.h +++ b/include/simfil/operator.h @@ -165,19 +165,19 @@ struct OperatorTypeof return n; } - auto operator()(const std::string&) -> std::string_view + auto operator()(const std::string&) const -> std::string_view { static auto n = "string"sv; return n; } - auto operator()(const ModelNode& v) -> std::string_view + auto operator()(const ModelNode&) const -> std::string_view { static auto n = "model"sv; return n; } - auto operator()(const TransientObject& v) -> std::string_view + auto operator()(const TransientObject&) const -> std::string_view { // Handled by MetaType::unaryOp return ""sv; diff --git a/src/expression-patterns.h b/src/expression-patterns.h index 1e1d507c..58fdd698 100644 --- a/src/expression-patterns.h +++ b/src/expression-patterns.h @@ -39,11 +39,11 @@ inline auto isSingleValueOrFieldExpression(const Expr* expr) -> bool { return true; if (auto* v = dynamic_cast(expr)) { - const auto value = v->value(); + const auto& value = v->value(); if (value.isa(ValueType::String)) { auto str = value.as(); auto loc = std::locale(); - return std::all_of(str.begin(), str.end(), [&](auto c) { + return std::ranges::all_of(str, [&](auto c) { return c == '_' || std::isupper(c, loc); }) && !str.empty(); } @@ -74,7 +74,7 @@ inline auto isFieldComparison(const Expr* expr) -> bool { if (value.isa(ValueType::String)) { auto str = value.as(); auto loc = std::locale(); - leftIsFieldOrEnum = std::all_of(str.begin(), str.end(), [&](auto c) { + leftIsFieldOrEnum = std::ranges::all_of(str, [&](auto c) { return c == '_' || std::isupper(c, loc); }) && str.size() > 0; } diff --git a/src/expressions.cpp b/src/expressions.cpp index aa99ec64..73fd794d 100644 --- a/src/expressions.cpp +++ b/src/expressions.cpp @@ -108,7 +108,7 @@ auto WildcardExpr::ieval(Context ctx, const Value& val, const ResultFn& ores) -> return *result; tl::expected finalResult = Result::Continue; - val.iterate(ModelNode::IterLambda([&, this](const auto& subNode) -> bool { + val.iterate(ModelNode::IterLambda([&, this](const auto& subNode) { auto subResult = iterate(subNode); if (!subResult) { finalResult = std::move(subResult); diff --git a/src/function.cpp b/src/function.cpp index 9e1fc28c..6ef6e6a4 100644 --- a/src/function.cpp +++ b/src/function.cpp @@ -154,7 +154,7 @@ auto CountFn::eval(Context ctx, const Value& val, const std::vector& ar int64_t count = 0; for (const auto& arg : args) { - auto res = arg->eval(ctx, val, LambdaResultFn([&](Context, const Value& vv) { + auto evalRes = arg->eval(ctx, val, LambdaResultFn([&](Context, const Value& vv) { if (ctx.phase == Context::Phase::Compilation) { if (vv.isa(ValueType::Undef)) { undef = true; @@ -164,7 +164,7 @@ auto CountFn::eval(Context ctx, const Value& val, const std::vector& ar count += boolify(vv) ? 1 : 0; return Result::Continue; })); - TRY_EXPECTED(res); + TRY_EXPECTED(evalRes); if (undef) break; @@ -501,7 +501,7 @@ auto SumFn::eval(Context ctx, const Value& val, const std::vector& args Expr* subexpr = args.size() >= 2 ? args[1].get() : nullptr; Expr* initval = args.size() == 3 ? args[2].get() : nullptr; if (initval) - (void)initval->eval(ctx, val, LambdaResultFn([&](Context ctx, Value&& vv) { + (void)initval->eval(ctx, val, LambdaResultFn([&sum](Context, Value&& vv) { sum = std::move(vv); return Result::Continue; })); diff --git a/test/common.cpp b/test/common.cpp index cb72b3f1..310598d6 100644 --- a/test/common.cpp +++ b/test/common.cpp @@ -8,7 +8,7 @@ static const PanicFn panicFn{}; auto CompileError(std::string_view query, bool autoWildcard) -> Error { Environment env(Environment::WithNewStringCache); - env.constants.emplace("a_number", simfil::Value::make((int64_t)123)); + env.constants.try_emplace("a_number", simfil::Value::make((int64_t)123)); auto ast = compile(env, query, false, autoWildcard); REQUIRE(!ast.has_value()); @@ -19,7 +19,7 @@ auto CompileError(std::string_view query, bool autoWildcard) -> Error auto Compile(std::string_view query, bool autoWildcard) -> ASTPtr { Environment env(Environment::WithNewStringCache); - env.constants.emplace("a_number", simfil::Value::make((int64_t)123)); + env.constants.try_emplace("a_number", simfil::Value::make((int64_t)123)); auto ast = compile(env, query, false, autoWildcard); if (!ast) diff --git a/test/operator.cpp b/test/operator.cpp index 39415fff..7e78ed03 100644 --- a/test/operator.cpp +++ b/test/operator.cpp @@ -11,7 +11,7 @@ using namespace simfil; using namespace std::string_literals; #define REQUIRE_INVALID_OPERANDS(v) \ - static_assert(std::is_same_v); + static_assert(std::is_same_v) TEST_CASE("Unary operators", "[operator.unary]") { SECTION("OperatorNegate") { diff --git a/test/value.cpp b/test/value.cpp index e6a66231..bc4b0885 100644 --- a/test/value.cpp +++ b/test/value.cpp @@ -197,7 +197,7 @@ TEST_CASE("Value As", "[value.as]") { TEST_CASE("Value visit() method", "[value.visit]") { SECTION("Visit UndefinedType") { auto val = Value::undef(); - auto result = val.visit([](auto&& v) -> std::string { + auto result = val.visit([](const auto& v) -> std::string { using T = std::decay_t; if constexpr (std::is_same_v) { return "undefined"; @@ -210,7 +210,7 @@ TEST_CASE("Value visit() method", "[value.visit]") { SECTION("Visit NullType") { auto val = Value::null(); - auto result = val.visit([](auto&& v) -> std::string { + auto result = val.visit([](const auto& v) -> std::string { using T = std::decay_t; if constexpr (std::is_same_v) { return "null"; @@ -223,7 +223,7 @@ TEST_CASE("Value visit() method", "[value.visit]") { SECTION("Visit bool") { auto val = Value::t() ; - auto result = val.visit([](auto&& v) -> bool { + auto result = val.visit([](const auto& v) -> bool { using T = std::decay_t; if constexpr (std::is_same_v) { return v; @@ -235,7 +235,7 @@ TEST_CASE("Value visit() method", "[value.visit]") { SECTION("Visit int64_t") { auto val = Value::make(int64_t(42)); - auto result = val.visit([](auto&& v) -> int64_t { + auto result = val.visit([](const auto& v) -> int64_t { using T = std::decay_t; if constexpr (std::is_same_v) { return v; @@ -247,7 +247,7 @@ TEST_CASE("Value visit() method", "[value.visit]") { SECTION("Visit double") { auto val = Value::make(3.14); - auto result = val.visit([](auto&& v) -> double { + auto result = val.visit([](const auto& v) -> double { using T = std::decay_t; if constexpr (std::is_same_v) { return v; @@ -259,7 +259,7 @@ TEST_CASE("Value visit() method", "[value.visit]") { SECTION("Visit string") { auto val = Value::make("Preordain"s); - auto result = val.visit([](auto&& v) -> std::string { + auto result = val.visit([](const auto& v) -> std::string { using T = std::decay_t; if constexpr (std::is_same_v) { return v; @@ -274,7 +274,7 @@ TEST_CASE("Value visit() method", "[value.visit]") { auto objNode = model->newObject(0); auto val = Value::field(objNode); - auto result = val.visit([](auto&& v) -> bool { + auto result = val.visit([](const auto& v) -> bool { using T = std::decay_t; if constexpr (std::is_same_v) { return true; @@ -288,7 +288,7 @@ TEST_CASE("Value visit() method", "[value.visit]") { SECTION("Visit ModelNode with null pointer") { Value val(ValueType::Object); - auto result = val.visit([](auto&& v) -> bool { + auto result = val.visit([](const auto& v) -> bool { using T = std::decay_t; if constexpr (std::is_same_v) { return true; @@ -347,7 +347,6 @@ TEST_CASE("Value utility methods", "[value.utilities]") { auto model = std::make_shared(); auto objNode = model->newObject(0); auto val = Value::field(objNode); - auto node = val.node(); REQUIRE(val.node() != nullptr); REQUIRE(Value::make(42).node() == nullptr); @@ -388,16 +387,16 @@ TEST_CASE("getNumeric() function", "[value.numeric]") { } SECTION("getNumeric from double") { - ScalarValueType scalar = 2.718; + ScalarValueType scalar = 1.5; auto [ok, result] = getNumeric(scalar); REQUIRE(ok); - REQUIRE(result == Catch::Approx(2.718f)); + REQUIRE(result == Catch::Approx(1.5f)); } SECTION("getNumeric from int64_t") { - ScalarValueType scalar = int64_t(99); + ScalarValueType scalar = int64_t(123); auto [ok, result] = getNumeric(scalar); REQUIRE(ok); - REQUIRE(result == 99); + REQUIRE(result == 123); } } From 0977c1f2b8a39da655c89562b634d75839009e9f Mon Sep 17 00:00:00 2001 From: Johannes Wolf Date: Sun, 26 Oct 2025 23:35:18 +0100 Subject: [PATCH 39/50] function: Fix error forwarding + panic tests --- include/simfil/operator.h | 8 +++--- src/function.cpp | 15 +++++++--- src/simfil.cpp | 2 +- test/common.cpp | 2 ++ test/simfil.cpp | 59 ++++++++++++++++++++++----------------- 5 files changed, 52 insertions(+), 34 deletions(-) diff --git a/include/simfil/operator.h b/include/simfil/operator.h index 351bca9f..a6f57f89 100644 --- a/include/simfil/operator.h +++ b/include/simfil/operator.h @@ -625,13 +625,13 @@ inline auto makeOperatorResult(CType_&& value) -> tl::expected } template -inline auto makeOperatorResult(Value value) -> tl::expected +inline auto makeOperatorResult(Value&& value) -> tl::expected { - return value; + return std::forward(value); } template -inline auto makeOperatorResult(tl::expected value) -> tl::expected +inline auto makeOperatorResult(tl::expected&& value) -> tl::expected { if (!value) return tl::unexpected(std::move(value.error())); @@ -722,7 +722,7 @@ struct BinaryOperatorDispatcher if (!result) { // Try to find the operand types - auto& error = result.error(); + const auto& error = result.error(); if (error.type == Error::InvalidOperands) { auto ltype = UnaryOperatorDispatcher::dispatch(lhs).value_or(Value::strref("unknown")).toString(); auto rtype = UnaryOperatorDispatcher::dispatch(rhs).value_or(Value::strref("unknown")).toString(); diff --git a/src/function.cpp b/src/function.cpp index 6ef6e6a4..15977101 100644 --- a/src/function.cpp +++ b/src/function.cpp @@ -335,7 +335,7 @@ auto ArrFn::eval(Context ctx, const Value& val, const std::vector& args })); r && *r == Result::Stop) return Result::Stop; else if (!r) - return tl::unexpected(r.error()); + return tl::unexpected(std::move(r.error())); } return Result::Continue; @@ -500,12 +500,17 @@ auto SumFn::eval(Context ctx, const Value& val, const std::vector& args Expr* subexpr = args.size() >= 2 ? args[1].get() : nullptr; Expr* initval = args.size() == 3 ? args[2].get() : nullptr; - if (initval) - (void)initval->eval(ctx, val, LambdaResultFn([&sum](Context, Value&& vv) { + if (initval) { + auto initRes = initval->eval(ctx, val, LambdaResultFn([&sum](Context, Value&& vv) { sum = std::move(vv); return Result::Continue; })); + TRY_EXPECTED(initRes); + if (sum.isa(ValueType::Undef)) + return res(ctx, sum); + } + auto argRes = args[0]->eval(ctx, val, LambdaResultFn([&, n = 0](Context ctx, Value&& vv) mutable -> tl::expected { if (subexpr) { auto ov = model_ptr::make(vv); @@ -566,8 +571,10 @@ auto KeysFn::eval(Context ctx, const Value& val, const std::vector& arg if (vv.nodePtr()) for (auto&& fieldName : vv.node()->fieldNames()) { if (auto key = ctx.env->stringPool->resolve(fieldName)) { - if (res(ctx, Value::strref(*key)) == Result::Stop) + if (auto r = res(ctx, Value::strref(*key)); r && *r == Result::Stop) return Result::Stop; + else if (!r) + return tl::unexpected(std::move(r.error())); } } return Result::Continue; diff --git a/src/simfil.cpp b/src/simfil.cpp index 343d3b8a..7440273d 100644 --- a/src/simfil.cpp +++ b/src/simfil.cpp @@ -138,7 +138,7 @@ static auto simplifyOrForward(Environment* env, expected expr) - if (!values.empty() && std::ranges::all_of(values.begin(), values.end(), [](const Value& v) { return v.isa(ValueType::Null); })) - env->warn("Expression is alway null"s, (*expr)->toString()); + env->warn("Expression is always null"s, (*expr)->toString()); if (!values.empty() && values[0].isa(ValueType::Bool) && std::ranges::all_of(values.begin(), values.end(), [&](const Value& v) { return v.isBool(values[0].as()); diff --git a/test/common.cpp b/test/common.cpp index 310598d6..2cc522b4 100644 --- a/test/common.cpp +++ b/test/common.cpp @@ -9,6 +9,7 @@ auto CompileError(std::string_view query, bool autoWildcard) -> Error { Environment env(Environment::WithNewStringCache); env.constants.try_emplace("a_number", simfil::Value::make((int64_t)123)); + env.functions["panic"] = &panicFn; auto ast = compile(env, query, false, autoWildcard); REQUIRE(!ast.has_value()); @@ -20,6 +21,7 @@ auto Compile(std::string_view query, bool autoWildcard) -> ASTPtr { Environment env(Environment::WithNewStringCache); env.constants.try_emplace("a_number", simfil::Value::make((int64_t)123)); + env.functions["panic"] = &panicFn; auto ast = compile(env, query, false, autoWildcard); if (!ast) diff --git a/test/simfil.cpp b/test/simfil.cpp index 2bd02f0a..0b5c2305 100644 --- a/test/simfil.cpp +++ b/test/simfil.cpp @@ -21,11 +21,14 @@ static constexpr auto StaticTestKey = StringPool::NextStaticId; #define REQUIRE_AST(input, output) \ REQUIRE(Compile(input, false)->expr().toString() == (output)) +#define REQUIRE_AST_AUTOWILDCARD(input, output) \ + REQUIRE(Compile(input, true)->expr().toString() == (output)) + #define REQUIRE_ERROR(input) \ REQUIRE(CompileError(input, false).message != "") -#define REQUIRE_AST_AUTOWILDCARD(input, output) \ - REQUIRE(Compile(input, true)->expr().toString() == (output)) +#define REQUIRE_PANIC(input) \ + REQUIRE_RESULT((input), "ERROR: Panic!") TEST_CASE("Int", "[ast.integer]") { @@ -73,8 +76,8 @@ TEST_CASE("OperatorConst", "[ast.operator]") { REQUIRE_AST("a+2", "(+ a 2)"); REQUIRE_AST("2+a", "(+ 2 a)"); REQUIRE_AST("a+b", "(+ a b)"); - REQUIRE_ERROR("1+panic()"); - REQUIRE_ERROR("panic()+1"); + REQUIRE_PANIC("1+panic()"); + REQUIRE_PANIC("panic()+1"); auto GetError = [&](std::string_view query) -> std::string { Environment env(Environment::WithNewStringCache); @@ -287,7 +290,7 @@ TEST_CASE("CompareIncompatibleTypesFields", "[ast.compare-incompatible-types-fie TEST_CASE("OperatorNegate", "[ast.operator-negate]") { REQUIRE_ERROR("-('abc')"); REQUIRE_ERROR("-(true)"); - REQUIRE_ERROR("-panic()"); + REQUIRE_PANIC("-panic()"); REQUIRE_AST("-(1)", "-1"); REQUIRE_AST("-(1.1)", "-1.100000"); REQUIRE_AST("-(null)", "null"); @@ -358,8 +361,8 @@ TEST_CASE("ModeSetter", "[ast.mode-setter]") { } TEST_CASE("UtilityFns", "[ast.functions]") { - REQUIRE_ERROR("range(panic(), 5)"); - REQUIRE_ERROR("range(1, panic())"); + REQUIRE_PANIC("range(panic(), 5)"); + REQUIRE_PANIC("range(1, panic())"); REQUIRE_AST("range(a,b)", "(range a b)"); /* Ca not optimize */ REQUIRE_AST("range(1,5)", "1..5"); REQUIRE_AST("range(1,5)==0", "false"); @@ -460,58 +463,64 @@ TEST_CASE("Single Values", "[yaml.single-values]") { REQUIRE_RESULT("sub.sub.a", "sub sub a"); } -TEST_CASE("Model Functions", "[yaml.mode-functions]") { - SECTION("Test any(... ) with values generated by arr(...)") { +TEST_CASE("Model Functions", "[yaml.model-functions]") { + SECTION("Test any(...)") { REQUIRE_RESULT("any(arr(null, null))", "false"); REQUIRE_RESULT("any(arr(true, null))", "true"); REQUIRE_RESULT("any(arr(null, true))", "true"); REQUIRE_RESULT("any(arr(true, true))", "true"); } - SECTION("Test each(... ) with values generated by arr(...)") { + SECTION("Test each(...)") { REQUIRE_RESULT("each(arr(null, null))", "false"); REQUIRE_RESULT("each(arr(true, null))", "false"); REQUIRE_RESULT("each(arr(null, true))", "false"); REQUIRE_RESULT("each(arr(true, true))", "true"); } - SECTION("Test arr(... )") { + SECTION("Test arr(...)") { REQUIRE_RESULT("arr(2,3,5,7,'ok')", "2|3|5|7|ok"); - REQUIRE_ERROR("arr(0,panic(),2)"); + REQUIRE_PANIC("arr(0,panic(),2)"); } - SECTION("Test split(... )") { + SECTION("Test split(...)") { REQUIRE_RESULT("split('hello.this.is.a.test.', '.')", "hello|this|is|a|test|"); REQUIRE_RESULT("split('hello.this.is.a.test.', '.', false)", "hello|this|is|a|test"); - REQUIRE_ERROR("split(panic(), '.')"); - REQUIRE_ERROR("split('a.b.c', panic())"); + REQUIRE_PANIC("split(panic(), '.')"); + REQUIRE_PANIC("split('a.b.c', panic())"); } - SECTION("Test select(... )") { + SECTION("Test select(...)") { REQUIRE_RESULT("select(split('a.b.c.d', '.'), a)", "b"); REQUIRE_RESULT("select(split('a.b.c.d', '.'), 0)", "a"); REQUIRE_RESULT("select(split('a.b.c.d', '.'), 1, 2)", "b|c"); REQUIRE_RESULT("select(split('a.b.c.d', '.'), 1, 0)", "b|c|d"); - REQUIRE_ERROR("select(panic(), 0)"); - REQUIRE_ERROR("select(0, panic())"); + REQUIRE_PANIC("select(panic(), 0)"); + REQUIRE_PANIC("select(0, panic())"); } - SECTION("Test sum(... )") { + SECTION("Test sum(...)") { REQUIRE_RESULT("sum(range(1, 10)...)", "55"); REQUIRE_RESULT("sum(range(1, 10)..., $sum + $val)", "55"); REQUIRE_RESULT("sum(range(1, 10)..., $sum + $val, 10)", "65"); REQUIRE_RESULT("sum(range(1, 10)..., $sum * $val, 1)", "3628800"); - // Test error forwarding - REQUIRE_ERROR("sum(panic(), 0)"); - REQUIRE_ERROR("sum(range(1, 10)..., panic())"); - REQUIRE_ERROR("sum(range(1, 10)..., 0, panic())"); + REQUIRE_PANIC("sum(panic())"); + REQUIRE_PANIC("sum(range(1, 10)..., panic())"); + REQUIRE_PANIC("sum(range(1, 10)..., 0, panic())"); } - SECTION("Count non-false values generated by arr(...)") { + SECTION("Count non-false values of arr(...)") { REQUIRE_RESULT("count(arr(null, null))", "0"); REQUIRE_RESULT("count(arr(true, null))", "1"); REQUIRE_RESULT("count(arr(null, true))", "1"); REQUIRE_RESULT("count(arr(true, true))", "2"); - REQUIRE_ERROR("count(panic())"); + REQUIRE_PANIC("count(panic())"); + } + SECTION("Count model keys") { + REQUIRE_RESULT("count(keys(**))", "50"); + + REQUIRE_PANIC("keys(panic())"); + REQUIRE_PANIC("keys(**.{panic()})"); + REQUIRE_PANIC("panic(keys(**))"); } } From e39cb9d4f8bf2f42928e4ec15cc69e96a3fd301a Mon Sep 17 00:00:00 2001 From: Johannes Wolf Date: Mon, 27 Oct 2025 00:39:11 +0100 Subject: [PATCH 40/50] expr: Fix error forwarding for AnyChild expr --- include/simfil/operator.h | 4 ++-- src/expressions.cpp | 19 ++++++++++++------- src/expressions.h | 3 +-- test/common.cpp | 2 ++ test/diagnostics.cpp | 2 +- 5 files changed, 18 insertions(+), 12 deletions(-) diff --git a/include/simfil/operator.h b/include/simfil/operator.h index a6f57f89..6e874cd9 100644 --- a/include/simfil/operator.h +++ b/include/simfil/operator.h @@ -625,9 +625,9 @@ inline auto makeOperatorResult(CType_&& value) -> tl::expected } template -inline auto makeOperatorResult(Value&& value) -> tl::expected +inline auto makeOperatorResult(Value value) -> tl::expected { - return std::forward(value); + return value; } template diff --git a/src/expressions.cpp b/src/expressions.cpp index 73fd794d..a2ac877a 100644 --- a/src/expressions.cpp +++ b/src/expressions.cpp @@ -162,16 +162,20 @@ auto AnyChildExpr::ieval(Context ctx, const Value& val, const ResultFn& res) -> if (!val.node() || !val.node()->size()) return res(ctx, Value::null()); - val.node()->iterate(ModelNode::IterLambda([&](auto subNode) -> bool { + std::optional error; + val.node()->iterate(ModelNode::IterLambda([&error, &ctx, &res](auto subNode) -> bool { auto result = res(ctx, Value::field(std::move(subNode))); - if (!result || *result == Result::Stop) { + if (!result) { + error = std::move(result.error()); return false; } + if (*result == Result::Stop) + return false; return true; })); - - return Result::Continue; - //return result; + if (error) + return tl::unexpected(std::move(*error)); + return Result::Continue; } auto AnyChildExpr::clone() const -> ExprPtr @@ -390,14 +394,14 @@ auto SubscriptExpr::ieval(Context ctx, const Value& val, const ResultFn& ores) - ctx.env->warn("Invalid subscript index type "s + valueType2String(ival.type), this->toString()); } else { auto v = BinaryOperatorDispatcher::dispatch(lval, ival); - if (!v) - return tl::unexpected(std::move(v.error())); + TRY_EXPECTED(v); return res(ctx, std::move(v.value())); } return Result::Continue; })); })); + TRY_EXPECTED(r); res.ensureCall(); return r; } @@ -776,6 +780,7 @@ auto UnpackExpr::ieval(Context ctx, const Value& val, const ResultFn& res) -> tl } return Result::Continue; })); + TRY_EXPECTED(r); if (!anyval) r = res(ctx, Value::null()); diff --git a/src/expressions.h b/src/expressions.h index 97c14be2..96b3ea2e 100644 --- a/src/expressions.h +++ b/src/expressions.h @@ -444,8 +444,7 @@ class ComparisonExpr : public ComparisonExprBase ++falseResults_; } - return res(ctx, std::move(operatorResult.value()) -); + return res(ctx, std::move(operatorResult.value())); })); })); } diff --git a/test/common.cpp b/test/common.cpp index 2cc522b4..f2fc341d 100644 --- a/test/common.cpp +++ b/test/common.cpp @@ -77,6 +77,7 @@ auto CompleteQuery(std::string_view query, size_t point, std::optionalroot(0); + REQUIRE(root); return complete(env, query, point, **root, opts).value_or(std::vector()); } @@ -97,6 +98,7 @@ auto GetDiagnosticMessages(std::string_view query) -> std::vectorroot(0); + REQUIRE(root); auto res = eval(env, **ast, **root, &diag); if (!res) INFO(res.error().message); diff --git a/test/diagnostics.cpp b/test/diagnostics.cpp index 65f8005f..ec2e2580 100644 --- a/test/diagnostics.cpp +++ b/test/diagnostics.cpp @@ -28,7 +28,7 @@ TEST_CASE("UnknownField", "[diag.unknown-field]") { } TEST_CASE("ComparatorTypeMissmatch", "[diag.comparator-type-missmatch]") { - EXPECT_DIAGNOSTIC_MESSAGE_CONTAINING("*['string'] > 123", "All values compared to"); + EXPECT_DIAGNOSTIC_MESSAGE_CONTAINING("['string'] > 123", "All values compared to true. Left hand types are string"); } TEST_CASE("AnyUnknownField", "[diag.any-suppress-if-any]") { From 62600befcf3246aab7f1d6afda532a6450bbfa9e Mon Sep 17 00:00:00 2001 From: Johannes Wolf Date: Mon, 27 Oct 2025 00:45:38 +0100 Subject: [PATCH 41/50] value: Add ModelNode::Ptr visitor to getScalar --- include/simfil/value.h | 1 + 1 file changed, 1 insertion(+) diff --git a/include/simfil/value.h b/include/simfil/value.h index fb36f768..dd0d70fe 100644 --- a/include/simfil/value.h +++ b/include/simfil/value.h @@ -431,6 +431,7 @@ class Value void operator() (std::string const& v) {result = v;} void operator() (std::string_view const& v) {result = v;} void operator() (TransientObject const&) {} + void operator() (ModelNode::Ptr const&) {} ScalarValueType result; } scalarVisitor; std::visit(scalarVisitor, value); From 78dc75696c3fa09f508b8c60202f0098dfb068cb Mon Sep 17 00:00:00 2001 From: Johannes Wolf Date: Mon, 27 Oct 2025 01:18:08 +0100 Subject: [PATCH 42/50] ci: Ignore low line coverage for now --- .github/workflows/coverage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 80f1f7b0..12952239 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -63,7 +63,7 @@ jobs: with: filename: coverage.xml badge: false - fail_below_min: true + fail_below_min: false format: markdown hide_branch_rate: false hide_complexity: true From 1d2ddae341335b34064667ae0087febc9e2fe144 Mon Sep 17 00:00:00 2001 From: Johannes Wolf Date: Mon, 27 Oct 2025 09:04:04 +0100 Subject: [PATCH 43/50] util: Fix null-function call --- src/simfil.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/simfil.cpp b/src/simfil.cpp index 7440273d..f077afc2 100644 --- a/src/simfil.cpp +++ b/src/simfil.cpp @@ -87,12 +87,14 @@ static auto isSymbolWord(std::string_view sv) -> bool template struct scoped { Fun f; + bool call = true; explicit scoped(Fun f) : f(std::move(f)) {} - scoped(scoped&& s) noexcept : f(std::move(s.f)) { s.f = nullptr; } + scoped(scoped&& s) noexcept : f(std::move(s.f)) { s.call = false; } scoped(const scoped& s) = delete; ~scoped() { - f(); + if (call) + f(); } }; From 06fb09a342ee8c09776cffbdeaddf4464fabbf0c Mon Sep 17 00:00:00 2001 From: Johannes Wolf Date: Mon, 27 Oct 2025 09:14:34 +0100 Subject: [PATCH 44/50] value: Fix field constructor with string_view --- include/simfil/value.h | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/include/simfil/value.h b/include/simfil/value.h index dd0d70fe..75f5900c 100644 --- a/include/simfil/value.h +++ b/include/simfil/value.h @@ -310,6 +310,11 @@ class Value const auto type = node.type(); if (type == ValueType::Object || type == ValueType::Array) { return Value{type, model_ptr(node)}; + } else if (type == ValueType::String) { + auto value = node.value(); + if (auto view = std::get_if(&value)) + return Value::make(std::string(*view)); + return Value{std::move(value)}; } else { return Value{node.value()}; } @@ -320,6 +325,11 @@ class Value const auto type = node.type(); if (type == ValueType::Object || type == ValueType::Array) { return {type, model_ptr(std::move(node))}; + } else if (type == ValueType::String) { + auto value = node.value(); + if (auto view = std::get_if(&value)) + return Value::make(std::string(*view)); + return Value{std::move(value)}; } else { return Value{node.value()}; } From 8d8f561bbb998dfa42aa31183dee7d269ab30794 Mon Sep 17 00:00:00 2001 From: Johannes Wolf Date: Mon, 27 Oct 2025 09:16:43 +0100 Subject: [PATCH 45/50] model: Fix addFieldInternal return value --- include/simfil/model/nodes.impl.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/simfil/model/nodes.impl.h b/include/simfil/model/nodes.impl.h index 9b302760..4f1fa48b 100644 --- a/include/simfil/model/nodes.impl.h +++ b/include/simfil/model/nodes.impl.h @@ -162,7 +162,7 @@ tl::expected>, Error if (!fieldId) return tl::unexpected(std::move(fieldId.error())); storage_->emplace_back(members_, *fieldId, value->addr()); - return *this; + return std::ref(*this); } } // namespace simfil From fc76ae9bf970b1c6b71d5a4e2cd146925e103c66 Mon Sep 17 00:00:00 2001 From: Johannes Wolf Date: Mon, 27 Oct 2025 09:32:04 +0100 Subject: [PATCH 46/50] cursor: Fix false-positive shadowing bug warning --- src/expressions.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/expressions.cpp b/src/expressions.cpp index a2ac877a..fbeef7dd 100644 --- a/src/expressions.cpp +++ b/src/expressions.cpp @@ -566,7 +566,7 @@ auto EachExpr::ieval(Context ctx, const Value& val, const ResultFn& res) -> tl:: auto undef = false; /* At least one value is undef */ for (const auto& arg : args_) { - auto res = arg->eval(ctx, val, LambdaResultFn([&](Context, const Value& vv) { + auto argRes = arg->eval(ctx, val, LambdaResultFn([&](Context, const Value& vv) { if (ctx.phase == Context::Phase::Compilation) { if (vv.isa(ValueType::Undef)) { undef = true; @@ -576,7 +576,7 @@ auto EachExpr::ieval(Context ctx, const Value& val, const ResultFn& res) -> tl:: result = result && boolify(vv); return result ? Result::Continue : Result::Stop; })); - TRY_EXPECTED(res); + TRY_EXPECTED(argRes); if (!result || undef) break; } From d1e076c50bdf127a8f91930116a0496db00c612a Mon Sep 17 00:00:00 2001 From: Johannes Wolf Date: Wed, 29 Oct 2025 17:31:02 +0100 Subject: [PATCH 47/50] ci: GCOVR ignore TRY_EXPECTED macro --- .github/workflows/coverage.yml | 2 ++ src/expressions.cpp | 37 +++++++++++-------------------- src/function.cpp | 40 +++++++++++++++------------------- src/model/model.cpp | 5 +++-- src/model/nodes.cpp | 14 +++++------- src/simfil.cpp | 8 ++----- src/token.cpp | 2 +- 7 files changed, 44 insertions(+), 64 deletions(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 12952239..e847d892 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -35,8 +35,10 @@ jobs: run: | mkdir coverage gcovr --html-details coverage/coverage.html \ + --exclude-lines-by-pattern 'TRY_EXPECTED' \ --filter src/ --filter include/ gcovr --cobertura coverage.xml \ + --exclude-lines-by-pattern 'TRY_EXPECTED' \ --html coverage.html \ --filter src/ --filter include/ less coverage.xml diff --git a/src/expressions.cpp b/src/expressions.cpp index fbeef7dd..ce3f03f8 100644 --- a/src/expressions.cpp +++ b/src/expressions.cpp @@ -101,9 +101,7 @@ auto WildcardExpr::ieval(Context ctx, const Value& val, const ResultFn& ores) -> return Result::Continue; auto result = res(ctx, Value::field(val)); - if (!result) - return tl::unexpected(result.error()); - + TRY_EXPECTED(result); if (*result == Result::Stop) [[unlikely]] return *result; @@ -288,10 +286,10 @@ auto MultiConstExpr::constant() const -> bool auto MultiConstExpr::ieval(Context ctx, const Value&, const ResultFn& res) -> tl::expected { for (const auto& v : values_) { - if (auto r = res(ctx, v); r && *r == Result::Stop) + auto r = res(ctx, v); + TRY_EXPECTED(r); + if (*r == Result::Stop) return Result::Stop; - else if (!r) - return tl::unexpected(std::move(r.error())); } return Result::Continue; @@ -449,9 +447,7 @@ auto SubExpr::ieval(Context ctx, Value&& val, const ResultFn& ores) -> tl::expec auto r = left_->eval(ctx, val, LambdaResultFn([this, &res](Context ctx, const Value& lv) -> tl::expected { return sub_->eval(ctx, lv, LambdaResultFn([&res, &lv](const Context& ctx, const Value& vv) -> tl::expected { auto bv = UnaryOperatorDispatcher::dispatch(vv); - if (!bv) [[unlikely]] - return tl::unexpected(std::move(bv.error())); - + TRY_EXPECTED(bv); if (bv->isa(ValueType::Undef)) return Result::Continue; @@ -773,10 +769,10 @@ auto UnpackExpr::ieval(Context ctx, const Value& val, const ResultFn& res) -> tl return Result::Stop; } else { anyval = true; - if (auto r = res(ctx, std::move(v)); r && *r == Result::Stop) + auto r = res(ctx, std::move(v)); + TRY_EXPECTED(r); + if (*r == Result::Stop) return Result::Stop; - else if (!r) - return tl::unexpected(std::move(r.error())); } return Result::Continue; })); @@ -821,8 +817,7 @@ auto UnaryWordOpExpr::ieval(Context ctx, const Value& val, const ResultFn& res) if (val.isa(ValueType::TransientObject)) { const auto& obj = val.as(); auto v = obj.meta->unaryOp(ident_, obj); - if (!v) - return tl::unexpected(std::move(v.error())); + TRY_EXPECTED(v); return res(ctx, std::move(v.value())); } @@ -867,16 +862,14 @@ auto BinaryWordOpExpr::ieval(Context ctx, const Value& val, const ResultFn& res) if (lval.isa(ValueType::TransientObject)) { const auto& obj = lval.as(); auto v = obj.meta->binaryOp(ident_, obj, rval); - if (!v) - return tl::unexpected(std::move(v.error())); + TRY_EXPECTED(v); return res(ctx, std::move(v.value())); } if (rval.isa(ValueType::TransientObject)) { const auto& obj = rval.as(); auto v = obj.meta->binaryOp(ident_, lval, obj); - if (!v) - return tl::unexpected(std::move(v.error())); + TRY_EXPECTED(v); return res(ctx, std::move(v.value())); } @@ -924,9 +917,7 @@ auto AndExpr::ieval(Context ctx, const Value& val, const ResultFn& res) -> tl::e return res(ctx, lval); auto v = UnaryOperatorDispatcher::dispatch(lval); - if (!v) - return tl::unexpected(std::move(v.error())); - + TRY_EXPECTED(v); if (v->isa(ValueType::Bool)) if (!v->template as()) return res(ctx, std::move(lval)); @@ -976,9 +967,7 @@ auto OrExpr::ieval(Context ctx, const Value& val, const ResultFn& res) -> tl::ex ++leftEvaluations_; auto v = UnaryOperatorDispatcher::dispatch(lval); - if (!v) - return tl::unexpected(std::move(v.error())); - + TRY_EXPECTED(v); if (v->isa(ValueType::Bool)) if (v->template as()) return res(ctx, std::move(lval)); diff --git a/src/function.cpp b/src/function.cpp index 15977101..9a57fd41 100644 --- a/src/function.cpp +++ b/src/function.cpp @@ -198,8 +198,7 @@ auto TraceFn::eval(Context ctx, const Value& val, const std::vector& ar .opt("limit", ValueType::Int, limit, Value::make(static_cast(-1))) .opt("name", ValueType::String, name, Value::make(args[0]->toString())) .ok(); - if (!ok) - return tl::unexpected(std::move(ok.error())); + TRY_EXPECTED(ok); /* Never run in compilation phase */ if (ctx.phase == Context::Phase::Compilation) @@ -263,8 +262,7 @@ auto RangeFn::eval(Context ctx, const Value& val, const std::vector& ar .arg("begin", ValueType::Int, begin) .arg("end", ValueType::Int, end) .ok(); - if (!ok) - return tl::unexpected(std::move(ok.error())); + TRY_EXPECTED(ok); if (!ok.value()) [[unlikely]] return res(ctx, Value::undef()); @@ -330,12 +328,12 @@ auto ArrFn::eval(Context ctx, const Value& val, const std::vector& args return res(ctx, Value::null()); for (const auto& arg : args) { - if (auto r = arg->eval(ctx, val, LambdaResultFn([&res](Context ctx, Value&& vv) { + auto r = arg->eval(ctx, val, LambdaResultFn([&res](Context ctx, Value &&vv) { return res(ctx, std::move(vv)); - })); r && *r == Result::Stop) + })); + TRY_EXPECTED(r); + if (*r == Result::Stop) return Result::Stop; - else if (!r) - return tl::unexpected(std::move(r.error())); } return Result::Continue; @@ -410,8 +408,7 @@ auto SplitFn::eval(Context ctx, const Value& val, const std::vector& ar .arg("separator", ValueType::String, sep) .opt("keepEmpty", ValueType::Bool, keepEmpty, Value::t()) .ok(); - if (!ok) - return tl::unexpected(std::move(ok.error())); + TRY_EXPECTED(ok); auto subctx = ctx; if (!ok.value()) [[unlikely]] @@ -419,10 +416,10 @@ auto SplitFn::eval(Context ctx, const Value& val, const std::vector& ar auto items = split(str.as(), sep.as(), !keepEmpty.as()); for (auto&& item : items) { - if (auto r = res(subctx, Value::make(std::move(item))); r && *r == Result::Stop) + auto r = res(subctx, Value::make(std::move(item))); + TRY_EXPECTED(r); + if (*r == Result::Stop) break; - else if (!r) - return tl::unexpected(r.error()); } return Result::Continue; @@ -451,8 +448,7 @@ auto SelectFn::eval(Context ctx, const Value& val, const std::vector& a .arg("index", ValueType::Int, idx) .opt("limit", ValueType::Int, cnt, Value::make(static_cast(1))) .ok(); - if (!ok) - return tl::unexpected(std::move(ok.error())); + TRY_EXPECTED(ok); if (!ok.value()) [[unlikely]] return res(ctx, Value::undef()); @@ -529,11 +525,9 @@ auto SumFn::eval(Context ctx, const Value& val, const std::vector& args if (sum.isa(ValueType::Null)) { sum = std::move(vv); } else { - if (auto newSum = BinaryOperatorDispatcher::dispatch(sum, vv)) { - sum = std::move(newSum.value()); - } else { - return tl::unexpected(std::move(newSum.error())); - } + auto newSum = BinaryOperatorDispatcher::dispatch(sum, vv); + TRY_EXPECTED(newSum); + sum = std::move(newSum.value()); } } @@ -571,10 +565,10 @@ auto KeysFn::eval(Context ctx, const Value& val, const std::vector& arg if (vv.nodePtr()) for (auto&& fieldName : vv.node()->fieldNames()) { if (auto key = ctx.env->stringPool->resolve(fieldName)) { - if (auto r = res(ctx, Value::strref(*key)); r && *r == Result::Stop) + auto r = res(ctx, Value::strref(*key)); + TRY_EXPECTED(r); + if (*r == Result::Stop) return Result::Stop; - else if (!r) - return tl::unexpected(std::move(r.error())); } } return Result::Continue; diff --git a/src/model/model.cpp b/src/model/model.cpp index b805cb04..8e0cdf23 100644 --- a/src/model/model.cpp +++ b/src/model/model.cpp @@ -16,6 +16,8 @@ #include #include +#include "../expected.h" + namespace simfil { @@ -363,8 +365,7 @@ auto ModelPool::setStrings(std::shared_ptr const& strings) -> tl::ex for (auto& member : memberArray) { if (auto resolvedName = oldStrings->resolve(member.name_)) { auto stringId = strings->emplace(*resolvedName); - if (!stringId) - return tl::unexpected(std::move(stringId.error())); + TRY_EXPECTED(stringId); member.name_ = *stringId; } } diff --git a/src/model/nodes.cpp b/src/model/nodes.cpp index 2a4ccc6f..458ff040 100644 --- a/src/model/nodes.cpp +++ b/src/model/nodes.cpp @@ -246,10 +246,9 @@ Array& Array::append(std::string_view const& value) {storage_->push_back(members tl::expected Array::extend(model_ptr const& other) { auto otherSize = other->size(); for (auto i = 0u; i < otherSize; ++i) { - if (auto value = storage_->at(other->members_, i)) - storage_->push_back(members_, *value); - else - return tl::unexpected(std::move(value.error())); + auto value = storage_->at(other->members_, i); + TRY_EXPECTED(value); + storage_->push_back(members_, *value); } return {}; } @@ -311,10 +310,9 @@ tl::expected Object::extend(model_ptr const& other) { auto otherSize = other->size(); for (auto i = 0u; i < otherSize; ++i) { - if (auto value = storage_->at(other->members_, i)) - storage_->push_back(members_, *value); - else - return tl::unexpected(std::move(value.error())); + auto value = storage_->at(other->members_, i); + TRY_EXPECTED(value); + storage_->push_back(members_, *value); } return {}; } diff --git a/src/simfil.cpp b/src/simfil.cpp index f077afc2..aab512bd 100644 --- a/src/simfil.cpp +++ b/src/simfil.cpp @@ -475,9 +475,7 @@ class SubSelectParser : public PrefixParselet, public InfixParselet /* Prefix sub-selects are transformed to a right side path expression, * with the current node on the left. As "standalone" sub-selects are not useful. */ auto body = p.parseTo(Token::RBRACE); - if (!body) - return unexpected(std::move(body.error())); - + TRY_EXPECTED(body); return simplifyOrForward(p.env, std::make_unique(std::make_unique("_"), std::move(*body))); } @@ -486,9 +484,7 @@ class SubSelectParser : public PrefixParselet, public InfixParselet { auto _ = scopedNotInPath(p); auto body = p.parseTo(Token::RBRACE); - if (!body) - return unexpected(std::move(body.error())); - + TRY_EXPECTED(body); return simplifyOrForward(p.env, std::make_unique(std::move(left), std::move(*body))); } diff --git a/src/token.cpp b/src/token.cpp index cefbdc0a..094a99da 100644 --- a/src/token.cpp +++ b/src/token.cpp @@ -480,7 +480,7 @@ auto tokenize(std::string_view expr) -> expected, Error> } if (s.hasError()) - return unexpected(s.error()); + return unexpected(std::move(s.error())); } tokens.emplace_back(Token::NIL, expr.size(), expr.size()); From f597714bde5b2fb410e3a06a13f78736b33d1aef Mon Sep 17 00:00:00 2001 From: Johannes Wolf Date: Wed, 29 Oct 2025 18:05:46 +0100 Subject: [PATCH 48/50] test: Add more unit-tests --- include/simfil/exception-handler.h | 2 ++ include/simfil/value.h | 4 +--- test/completion.cpp | 6 ++++++ test/token.cpp | 31 ++++++++++++++++++++++++++++++ 4 files changed, 40 insertions(+), 3 deletions(-) diff --git a/include/simfil/exception-handler.h b/include/simfil/exception-handler.h index bde4f525..79c4244c 100644 --- a/include/simfil/exception-handler.h +++ b/include/simfil/exception-handler.h @@ -46,6 +46,7 @@ class ThrowHandler template [[noreturn]] void raise(Args&&... args) { + // GCOVR_EXCL_START ExceptionType exceptionInstance(std::forward(args)...); if (auto const& excHandler = ThrowHandler::instance().get()) { @@ -55,6 +56,7 @@ template errorMessage = exceptionInstance.what(); excHandler(typeName, errorMessage); } + // GCOVR_EXCL_STOP throw std::move(exceptionInstance); } diff --git a/include/simfil/value.h b/include/simfil/value.h index 75f5900c..5c871c39 100644 --- a/include/simfil/value.h +++ b/include/simfil/value.h @@ -87,10 +87,8 @@ inline auto valueType2String(ValueType t) -> const char* case ValueType::TransientObject: return "transient"; case ValueType::Object: return "object"; case ValueType::Array: return "array"; - default: - assert(0 && "unreachable"); - return "unknown"; } + assert(0 && "unreachable"); return "unknown"; // GCOVR_EXCL_LINE } diff --git a/test/completion.cpp b/test/completion.cpp index cc5ae355..d45762e9 100644 --- a/test/completion.cpp +++ b/test/completion.cpp @@ -99,6 +99,12 @@ TEST_CASE("Complete in Expression", "[completion.complete-mid-expression]") { EXPECT_COMPLETION("sub.ch > 123", 6, "child"); } +TEST_CASE("Complete in unclosed expression", "[completion.complete-in-unclosed-expr]") { + EXPECT_COMPLETION("(field + oth", {}, "other"); + EXPECT_COMPLETION("_{field + oth", {}, "other"); + EXPECT_COMPLETION("*[field + oth", {}, "other"); +} + TEST_CASE("Complete SmartCas", "[completion.smart-case]") { // Complete both the field and the constants EXPECT_COMPLETION("cons", {}, "constant", Type::FIELD, 4); diff --git a/test/token.cpp b/test/token.cpp index e2b7b842..c68b3733 100644 --- a/test/token.cpp +++ b/test/token.cpp @@ -158,3 +158,34 @@ TEST_CASE("Token location", "[token.location]") { REQUIRE(tokens[3].begin == pos); REQUIRE(tokens[3].end == pos+3); pos+=4; /* and */ REQUIRE(tokens[4].begin == pos); REQUIRE(tokens[4].end == pos+4); pos+=4; /* true */ } + +TEST_CASE("Token to string", "[token.to-string]") { + auto tokens = tokenize("1 1.5 'Æthervial' re'.* Familiar' abc ()[]{},:._ * ** " + "null true false + - * / % << >> & | ^ ~ not and or " + "== != < <= > >= ? # typeof as ..."); + REQUIRE(tokens); + + auto i = 0; + #define REQUIRE_STR(expr) do { REQUIRE((*tokens)[i].toString() == (expr)); ++i; } while (false) + + REQUIRE_STR("1"); + REQUIRE_STR("1.500000"); + REQUIRE_STR("'Æthervial'"); + REQUIRE_STR("re'.* Familiar'"); + REQUIRE_STR("abc"); + REQUIRE_STR("("); REQUIRE_STR(")"); + REQUIRE_STR("["); REQUIRE_STR("]"); + REQUIRE_STR("{"); REQUIRE_STR("}"); + REQUIRE_STR(","); REQUIRE_STR(":"); + REQUIRE_STR("."); REQUIRE_STR("_"); + REQUIRE_STR("*"); REQUIRE_STR("**"); + REQUIRE_STR("null"); REQUIRE_STR("true"); REQUIRE_STR("false"); + REQUIRE_STR("+"); REQUIRE_STR("-"); REQUIRE_STR("*"); REQUIRE_STR("/"); + REQUIRE_STR("%"); + REQUIRE_STR("<<"); REQUIRE_STR(">>"); REQUIRE_STR("&"); REQUIRE_STR("|"); REQUIRE_STR("^"); REQUIRE_STR("~"); + REQUIRE_STR("not"); REQUIRE_STR("and"); REQUIRE_STR("or"); + REQUIRE_STR("=="); REQUIRE_STR("!="); REQUIRE_STR("<"); + REQUIRE_STR("<="); REQUIRE_STR(">"); REQUIRE_STR(">="); + REQUIRE_STR("?"); REQUIRE_STR("#"); REQUIRE_STR("typeof"); + REQUIRE_STR("as"); REQUIRE_STR("..."); +} From 8518daea3385013dff4568310cad43fb8832b11e Mon Sep 17 00:00:00 2001 From: Johannes Wolf Date: Thu, 30 Oct 2025 22:34:44 +0100 Subject: [PATCH 49/50] completion: Require single expression --- src/simfil.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/simfil.cpp b/src/simfil.cpp index aab512bd..6f69da53 100644 --- a/src/simfil.cpp +++ b/src/simfil.cpp @@ -842,6 +842,9 @@ auto complete(Environment& env, std::string_view query, size_t point, const Mode p.infixParsers[Token::OP_OR] = &andOrCompletionParser; auto astResult = p.parse(); + if (!p.match(Token::Type::NIL)) + return unexpected(Error::ExpectedEOF, "Expected end-of-input; got "s + p.current().toString()); + TRY_EXPECTED(astResult); auto ast = std::move(*astResult); From 90615c16d5909ebace68fa71bbc2541ddbda76da Mon Sep 17 00:00:00 2001 From: Johannes Wolf Date: Thu, 30 Oct 2025 23:22:43 +0100 Subject: [PATCH 50/50] =?UTF-8?q?diagnostics:=20Remove=20=E2=80=9Cdid=20yo?= =?UTF-8?q?u=20mean=3F=E2=80=9D=20Diagnostics?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit They never help but confuse. --- README.md | 2 -- src/diagnostics.cpp | 40 +++------------------------------------- src/levenshtein.h | 38 -------------------------------------- 3 files changed, 3 insertions(+), 77 deletions(-) delete mode 100644 src/levenshtein.h diff --git a/README.md b/README.md index 8e08c4eb..66be7190 100644 --- a/README.md +++ b/README.md @@ -101,8 +101,6 @@ The query language can be extended by additional functions and addititonal types ## Diagnostics Simfil can output query diagnostics messages that can include a fixed query string. -Currently, it supports the following types of messages: -- "No matches for field '...'. Did you mean '...'?" ## Using the Library ### CMake FetchContent diff --git a/src/diagnostics.cpp b/src/diagnostics.cpp index 3f1620a6..2929bb87 100644 --- a/src/diagnostics.cpp +++ b/src/diagnostics.cpp @@ -2,9 +2,10 @@ #include "simfil/diagnostics.h" -#include "levenshtein.h" #include "expressions.h" +#include +#include #include #include #include @@ -111,31 +112,6 @@ auto Diagnostics::read(std::istream& stream) -> tl::expected return {}; } - -static auto findSimilarString(std::string_view source, const StringPool& pool) -> std::string -{ - std::string_view best; - auto bestScore = std::numeric_limits::max(); - - const auto isDollar = source[0] == '$'; - for (const auto& target : pool.strings()) { - const auto targetIsDollar = target[0] == '$'; - if (isDollar != targetIsDollar) - continue; - if (target == source) - continue; - - const auto score = levenshtein(source, target); - if (score < bestScore) { - bestScore = score; - best = target; - } - } - - return std::string(best); -} - - auto Diagnostics::buildMessages(Environment& env, const AST& ast) const -> std::vector { struct Visitor : ExprVisitor @@ -164,17 +140,7 @@ auto Diagnostics::buildMessages(Environment& env, const AST& ast) const -> std:: if (iter->second > 0) return; - // Generate "did you mean ...?" messages for missing fields - auto guess = findSimilarString(e.name_, *env.strings()); - if (!guess.empty()) { - std::string fix = ast.query(); - if (auto loc = e.sourceLocation(); loc.size > 0) - fix.replace(loc.offset, loc.size, guess); - - addMessage(fmt::format("No matches for field '{}'. Did you mean '{}'?", e.name_, guess), e, fix); - } else { - addMessage(fmt::format("No matches for field '{}'.", e.name_, guess), e, {}); - } + addMessage(fmt::format("No matches for field '{}'.", e.name_), e, {}); } void visitComparisonOperator(ComparisonExprBase& e, bool expectedResult) diff --git a/src/levenshtein.h b/src/levenshtein.h deleted file mode 100644 index 0b1e54ca..00000000 --- a/src/levenshtein.h +++ /dev/null @@ -1,38 +0,0 @@ -#pragma once - -#include -#include -#include - -namespace simfil -{ - -inline auto levenshtein(std::string_view a, std::string_view b) -> int -{ - if (a.size() > b.size()) - return levenshtein(b, a); - - auto lo = a.size(); - auto hi = b.size(); - - std::vector dist; - dist.resize(lo + 1); - std::ranges::generate(dist.begin(), dist.end(), [i = 0]() mutable { return i++; }); - - for (auto j = 1; j <= hi; ++j) { - auto p = dist[0]++; - - for (auto i = 1; i <= lo; ++i) { - auto s = dist[i]; - if (a[i - 1] == b[j - 1]) - dist[i] = p; - else - dist[i] = std::min(std::min(dist[i - 1], dist[i]), p) + 1; - p = s; - } - } - - return dist[lo]; -} - -}