Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
191 changes: 189 additions & 2 deletions include/simfil/model/schema.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#include "simfil/model/string-pool.h"
#include <algorithm>
#include <cassert>
#include <compare>
#include <concepts>
#include <cstdint>
#include <functional>
Expand All @@ -21,6 +22,28 @@
constexpr SchemaId NoSchemaId = SchemaId{0};
constexpr SchemaId MaxSchemaId = SchemaId{std::numeric_limits<SchemaId>::max()};

/**
* One segment in a schema-derived query path.
*
* Field segments address object members. Array-element segments represent the
* non-recursive `*` operator needed to traverse array elements precisely.
*/
struct SchemaPathSegment
{
enum class Kind {
Field,
ArrayElement,
};

Kind kind = Kind::Field;
StringId field = 0;

auto operator<=>(const SchemaPathSegment&) const = default;
};

/** Sequence of schema path segments from a root schema to a reachable value. */
using SchemaPath = std::vector<SchemaPathSegment>;

/**
* Concept defining a callback to query a Schema* by SchemaId.
*/
Expand Down Expand Up @@ -53,6 +76,8 @@
Clean,
};

using SchemaIdStack = sfl::small_vector<SchemaId, 8>;

virtual ~Schema() = default;

/**
Expand Down Expand Up @@ -107,6 +132,47 @@
return {};
}

/**
* Return enum-like string symbols accepted directly by this schema node.
*
* Unlike nestedEnumSymbols(), this does not include descendants and is used
* to derive precise schema paths for auto-wildcard rewrites.
*/
virtual auto directEnumSymbols() const & -> std::span<const StringId>
{
return {};
}

/**
* Enumerate precise paths to all fields with the requested name.
*/
static auto fieldPaths(SchemaId root,
const std::function<const Schema*(SchemaId)>& queryFn,
StringId field) -> std::vector<SchemaPath>
{
std::vector<SchemaPath> paths;
SchemaIdStack visited;
SchemaPath current;
collectFieldPaths(root, queryFn, field, visited, current, paths);
sortUniquePaths(paths);
return paths;
}

/**
* Enumerate precise paths to all values that can hold the enum-like symbol.
*/
static auto enumSymbolPaths(SchemaId root,
const std::function<const Schema*(SchemaId)>& queryFn,
StringId symbol) -> std::vector<SchemaPath>
{
std::vector<SchemaPath> paths;
SchemaIdStack visited;
SchemaPath current;
collectEnumSymbolPaths(root, queryFn, symbol, visited, current, paths);
sortUniquePaths(paths);
return paths;
}

/**
* Return true once `canHaveField` is backed by finalized field caches.
*/
Expand All @@ -124,8 +190,6 @@
}

protected:
using SchemaIdStack = sfl::small_vector<SchemaId, 8>;

/**
* Append all fields reachable from this schema without relying on cached
* finalization state. This lets cyclic schema graphs still produce an exact
Expand All @@ -142,9 +206,114 @@
virtual auto collectNestedEnumSymbols(const std::function<Schema*(SchemaId)>& queryFn,
SchemaIdStack& visited,
std::vector<StringId>& symbols) const -> void
{
}

Check failure on line 210 in include/simfil/model/schema.h

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Add a nested comment explaining why this method is empty, or complete the implementation.

See more on https://sonarcloud.io/project/issues?id=Klebert-Engineering_simfil&issues=AZ5QilC-FWZNvFTrJNIo&open=AZ5QilC-FWZNvFTrJNIo&pullRequest=145

/**
* Visit fields declared directly by this schema and their possible child
* schemas. The default is empty for scalar schemas.
*/
virtual auto forEachDirectField(
const std::function<void(StringId, std::span<const SchemaId>)>&) const -> void
{
}

Check failure on line 219 in include/simfil/model/schema.h

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Add a nested comment explaining why this method is empty, or complete the implementation.

See more on https://sonarcloud.io/project/issues?id=Klebert-Engineering_simfil&issues=AZ50mxig0m21dqBYC0U_&open=AZ50mxig0m21dqBYC0U_&pullRequest=145

/**
* Visit possible array element schemas. The default is empty for non-arrays.
*/
virtual auto forEachElementSchema(const std::function<void(SchemaId)>&) const -> void
{
}

Check failure on line 226 in include/simfil/model/schema.h

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Add a nested comment explaining why this method is empty, or complete the implementation.

See more on https://sonarcloud.io/project/issues?id=Klebert-Engineering_simfil&issues=AZ50mxig0m21dqBYC0VA&open=AZ50mxig0m21dqBYC0VA&pullRequest=145

/**
* Recursively collect schema paths to matching fields.
*/
static auto collectFieldPaths(SchemaId schemaId,
const std::function<const Schema*(SchemaId)>& queryFn,
StringId field,
SchemaIdStack& visited,
SchemaPath& current,
std::vector<SchemaPath>& paths) -> void
{
if (schemaId == NoSchemaId || std::ranges::find(visited, schemaId) != visited.end())
return;

auto const* schema = queryFn(schemaId);
if (!schema)
return;

visited.push_back(schemaId);

schema->forEachDirectField([&](StringId directField, std::span<const SchemaId> childSchemas) {
current.push_back({SchemaPathSegment::Kind::Field, directField});
if (directField == field)
paths.push_back(current);
for (auto childSchemaId : childSchemas)
collectFieldPaths(childSchemaId, queryFn, field, visited, current, paths);
current.pop_back();
});

schema->forEachElementSchema([&](SchemaId elementSchemaId) {
current.push_back({SchemaPathSegment::Kind::ArrayElement, 0});
collectFieldPaths(elementSchemaId, queryFn, field, visited, current, paths);
current.pop_back();
});

visited.pop_back();
}

/**
* Recursively collect schema paths to values accepting a matching enum-like
* string symbol.
*/
static auto collectEnumSymbolPaths(SchemaId schemaId,
const std::function<const Schema*(SchemaId)>& queryFn,
StringId symbol,
SchemaIdStack& visited,
SchemaPath& current,
std::vector<SchemaPath>& paths) -> void
{
if (schemaId == NoSchemaId || std::ranges::find(visited, schemaId) != visited.end())
return;

auto const* schema = queryFn(schemaId);
if (!schema)
return;

visited.push_back(schemaId);

for (auto directSymbol : schema->directEnumSymbols()) {
if (directSymbol == symbol)
paths.push_back(current);
}

schema->forEachDirectField([&](StringId directField, std::span<const SchemaId> childSchemas) {
current.push_back({SchemaPathSegment::Kind::Field, directField});
for (auto childSchemaId : childSchemas)
collectEnumSymbolPaths(childSchemaId, queryFn, symbol, visited, current, paths);
current.pop_back();
});

schema->forEachElementSchema([&](SchemaId elementSchemaId) {
current.push_back({SchemaPathSegment::Kind::ArrayElement, 0});
collectEnumSymbolPaths(elementSchemaId, queryFn, symbol, visited, current, paths);
current.pop_back();
});

visited.pop_back();
}

/**
* Keep path rewrites deterministic and avoid duplicate paths from combined
* schemas or shared references.
*/
static auto sortUniquePaths(std::vector<SchemaPath>& paths) -> void
{
std::ranges::sort(paths);
auto duplicates = std::ranges::unique(paths);
paths.erase(duplicates.begin(), duplicates.end());
}

/**
* Append reachable values through a schema id, using finalized child
* caches when possible and falling back to raw graph traversal for cycles.
Expand Down Expand Up @@ -232,7 +401,7 @@
std::vector<StringId>& flatEnumSymbols,
const std::function<Schema*(SchemaId)>& queryFn,
const Schema& schema) -> State
{

Check warning on line 404 in include/simfil/model/schema.h

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Reduce verbosity with "using enum" for "simfil::Schema::State".

See more on https://sonarcloud.io/project/issues?id=Klebert-Engineering_simfil&issues=AZ5KJX_qh7s663YLdn7H&open=AZ5KJX_qh7s663YLdn7H&pullRequest=145
if (state == State::Clean || state == State::Finalizing)
return state;

Expand Down Expand Up @@ -327,6 +496,13 @@
return {fields_.begin(), fields_.end()};
}

auto forEachDirectField(
const std::function<void(StringId, std::span<const SchemaId>)>& fn) const -> void override
{
for (auto const& field : fields_)
fn(field.field, {field.schemas.begin(), field.schemas.end()});
}

auto nestedFields() const & -> std::span<const StringId> override
{
return {flatFields_.cbegin(), flatFields_.cend()};
Expand Down Expand Up @@ -418,7 +594,7 @@
}

auto finalize(const std::function<Schema*(SchemaId)>&) -> State override
{

Check warning on line 597 in include/simfil/model/schema.h

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Reduce verbosity with "using enum" for "simfil::Schema::State".

See more on https://sonarcloud.io/project/issues?id=Klebert-Engineering_simfil&issues=AZ5QilC-FWZNvFTrJNIp&open=AZ5QilC-FWZNvFTrJNIp&pullRequest=145
if (state_ == State::Clean || state_ == State::Finalizing)
return state_;

Expand All @@ -438,6 +614,11 @@
return {enumSymbols_.cbegin(), enumSymbols_.cend()};
}

auto directEnumSymbols() const & -> std::span<const StringId> override
{
return {enumSymbols_.cbegin(), enumSymbols_.cend()};
}

auto finalized() const -> bool override
{
return state_ == State::Clean;
Expand All @@ -452,8 +633,8 @@
auto collectNestedFields(const std::function<Schema*(SchemaId)>&,
SchemaIdStack&,
std::vector<StringId>&) const -> void override
{
}

Check failure on line 637 in include/simfil/model/schema.h

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Add a nested comment explaining why this method is empty, or complete the implementation.

See more on https://sonarcloud.io/project/issues?id=Klebert-Engineering_simfil&issues=AZ5QilC-FWZNvFTrJNIq&open=AZ5QilC-FWZNvFTrJNIq&pullRequest=145

auto collectNestedEnumSymbols(const std::function<Schema*(SchemaId)>&,
SchemaIdStack&,
Expand Down Expand Up @@ -535,6 +716,12 @@
return {schemas_.begin(), schemas_.end()};
}

auto forEachElementSchema(const std::function<void(SchemaId)>& fn) const -> void override
{
for (auto schemaId : schemas_)
fn(schemaId);
}

private:
auto collectNestedFields(const std::function<Schema*(SchemaId)>& lookup,
SchemaIdStack& visited,
Expand Down
57 changes: 57 additions & 0 deletions include/simfil/simfil.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,52 @@
#include "simfil/diagnostics.h"
#include "simfil/value.h"
#include "simfil/error.h"
#include "simfil/model/schema.h"

namespace simfil
{

struct ModelNode;

/**
* Options used while parsing and rewriting a query.
*/
struct CompileOptions
{
bool any = true;
bool autoWildcard = false;
SchemaId rootSchema = NoSchemaId;
};

/**
* One schema path referenced by a compiled expression.
*
* The path is expressed relative to the root schema supplied to
* `referencedSchemaPaths`. If `viaWildcard` is set, the path came from a
* recursive wildcard-field lookup such as `**.foo`.
*/
struct ReferencedSchemaPath
{
SchemaPath path;
SourceLocation location;
bool viaWildcard = false;
};

/**
* Schema references discovered by static AST inspection.
*
* The flags make the result conservative: callers can reject automatic scope
* decisions when the query contains broad wildcards or field access that cannot
* be tied to concrete schema paths.
*/
struct ReferencedSchemaPaths
{
std::vector<ReferencedSchemaPath> paths;
bool hasDynamicAccess = false;
bool hasUnresolvedAccess = false;
bool hasBroadWildcardAccess = false;
};

/**
* Compile expression `src`.
* Param:
Expand All @@ -30,6 +70,23 @@ struct ModelNode;
*/
auto compile(Environment& env, std::string_view query, bool any = true, bool autoWildcard = false) -> tl::expected<ASTPtr, Error>;

/**
* Compile expression `src` with explicit options.
*
* If rootSchema is set and autoWildcard is enabled, single field/enum queries
* are classified through the schema instead of legacy casing heuristics.
*/
auto compile(Environment& env, std::string_view query, CompileOptions options) -> tl::expected<ASTPtr, Error>;

/**
* Collect schema paths that are referenced by a compiled query.
*
* This is static analysis over the AST, not runtime evaluation: both sides of
* `and`/`or` are inspected, and schema-aware auto-wildcard rewrites are resolved
* to the exact paths they can touch.
*/
auto referencedSchemaPaths(Environment& env, const AST& ast, SchemaId rootSchema) -> tl::expected<ReferencedSchemaPaths, Error>;

/**
* Evaluate compiled expression.
* Param:
Expand Down
14 changes: 14 additions & 0 deletions src/expressions.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,11 @@
: value_(std::move(value))
{}

ConstExpr::ConstExpr(Value value, const Token& token)
: Expr(token)
, value_(std::move(value))
{}

auto ConstExpr::type() const -> Type
{
return Type::VALUE;
Expand Down Expand Up @@ -708,6 +713,15 @@
assert(right_.get());
}

PathExpr::PathExpr(ExprPtr left, ExprPtr right, SourceLocation location)
: Expr(location)
, left_(std::move(left))
, right_(std::move(right))
{
assert(left_.get());
assert(right_.get());
}

auto PathExpr::type() const -> Type
{
return Type::PATH;
Expand Down Expand Up @@ -1179,7 +1193,7 @@
schemaPlans_.resize(planIndex + 1);
auto plan = buildSchemaPlan(ctx, schema);
schemaPlans_[planIndex] = std::make_unique<CachedSchemaPlan>(
CachedSchemaPlan{schemaId, &schema, schemaRevision, std::move(plan)});

Check warning on line 1196 in src/expressions.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Directly pass the arguments for object creation to this function.

See more on https://sonarcloud.io/project/issues?id=Klebert-Engineering_simfil&issues=AZ5KJX9Sh7s663YLdn7A&open=AZ5KJX9Sh7s663YLdn7A&pullRequest=145

return &schemaPlans_[planIndex]->plan;
}
Expand Down
9 changes: 9 additions & 0 deletions src/expressions.h
Original file line number Diff line number Diff line change
Expand Up @@ -129,12 +129,20 @@ class ConstExpr : public Expr
: value_(Value::make(std::forward<CType_>(value)))
{}

template <class CType_>
requires (!std::is_base_of_v<ConstExpr, std::remove_cvref_t<CType_>>)
ConstExpr(CType_&& value, const Token& token)
: Expr(token)
, value_(Value::make(std::forward<CType_>(value)))
{}

ConstExpr(const ConstExpr&) = delete;
ConstExpr(ConstExpr&&) = delete;
auto operator=(const ConstExpr&) -> ConstExpr& = delete;
auto operator=(ConstExpr&&) -> ConstExpr& = delete;

explicit ConstExpr(Value value);
ConstExpr(Value value, const Token& token);

auto type() const -> Type override;
auto constant() const -> bool override;
Expand Down Expand Up @@ -237,6 +245,7 @@ class PathExpr : public Expr
{
public:
PathExpr(ExprPtr left, ExprPtr right);
PathExpr(ExprPtr left, ExprPtr right, SourceLocation location);

auto type() const -> Type override;
auto ieval(Context ctx, const Value& val, const ResultFn& ores) const -> tl::expected<Result, Error> override;
Expand Down
Loading
Loading