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
97 changes: 91 additions & 6 deletions include/simfil/model/model.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@

#include <memory>
#include <string_view>
#include <type_traits>
#include <vector>
#include <utility>
#include <cassert>
#include <istream>
#include <ostream>

Expand All @@ -19,6 +22,38 @@
namespace simfil
{

namespace res
{
// Tag type for ADL-based resolve hooks implemented by model libraries.
template<typename Target>
struct tag {};
}

namespace detail
{
template<class T>
concept HasModelType = requires { typename T::ModelType; };

template<HasModelType T>
using ModelTypeOf = typename T::ModelType;
}

/**
* ADL customization point for typed node resolution.
* Libraries define resolveInternal(tag, model, node) in their namespace.
*/
template<typename Target, typename ModelType>
model_ptr<Target> resolveInternal(res::tag<Target>, ModelType const&, ModelNode const&) = delete;

class ModelPool;

// Built-in resolve hooks for core node types. Declared here so ADL sees them
// across translation units without relying on friend injection.
template<>
model_ptr<Object> resolveInternal(res::tag<Object>, ModelPool const&, ModelNode const&);
template<>
model_ptr<Array> resolveInternal(res::tag<Array>, ModelPool const&, ModelNode const&);

/**
* Basic node model which only resolves trivial node types.
*/
Expand Down Expand Up @@ -58,6 +93,60 @@ class Model : public std::enable_shared_from_this<Model>
*/
virtual tl::expected<void, Error> resolve(ModelNode const& n, ResolveFn const& cb) const;

/**
* Resolve a node to a specific ModelNode subtype using ADL hooks.
* This provides a clean cast API without exposing model internals.
*/
template<typename Target = ModelNode>
model_ptr<Target> resolve(ModelNodeAddress const& address) const
{
if constexpr (std::is_same_v<Target, ModelNode>) {
return ModelNode::Ptr::make(shared_from_this(), address);
}
return resolve<Target>(*ModelNode::Ptr::make(shared_from_this(), address));
}

template<typename Target = ModelNode>
model_ptr<Target> resolve(ModelNodeAddress const& address, ScalarValueType data) const
{
if constexpr (std::is_same_v<Target, ModelNode>) {
return ModelNode::Ptr::make(shared_from_this(), address, std::move(data));
}
return resolve<Target>(*ModelNode::Ptr::make(shared_from_this(), address, std::move(data)));
}

template<typename Target>
model_ptr<Target> resolve(ModelNode::Ptr const& node) const
{
return resolve<Target>(*node);
}

template<typename Target>
model_ptr<Target> resolve(ModelNode const& node) const
{
if constexpr (std::is_same_v<Target, ModelNode>) {
return model_ptr<ModelNode>(node);
}
else {
if constexpr (!detail::HasModelType<Target>) {
static_assert(detail::HasModelType<Target>, "Target must provide a ModelType alias.");
return {};
}
else {
using ModelType = detail::ModelTypeOf<Target>;
#if !defined(NDEBUG)
// In debug builds, validate the model type to catch misuse early.
auto typedModel = dynamic_cast<ModelType const*>(this);
assert(typedModel && "resolve<T> called on incompatible model type.");
return resolveInternal(res::tag<Target>{}, *typedModel, node);
#else
// In release builds, avoid RTTI overhead on this hot path.
return resolveInternal(res::tag<Target>{}, *static_cast<ModelType const*>(this), node);
#endif
}
}
}

/** Add a small scalar value and get its model node view */
ModelNode::Ptr newSmallValue(bool value);
ModelNode::Ptr newSmallValue(int16_t value);
Expand Down Expand Up @@ -88,6 +177,8 @@ class ModelPool : public Model
template<typename, typename> friend struct BaseArray;

public:
// Keep Model::resolve<T> overloads visible alongside the virtual resolve override.
using Model::resolve;
/**
* The pool consists of multiple ModelNode columns,
* each for a different data type. Each column
Expand Down Expand Up @@ -156,12 +247,6 @@ class ModelPool : public Model
ModelNode::Ptr newValue(std::string_view const& value);
ModelNode::Ptr newValue(StringId handle);

/** Node-type-specific resolve-functions */
[[nodiscard]]
model_ptr<Object> resolveObject(ModelNode::Ptr const& n) const;
[[nodiscard]]
model_ptr<Array> resolveArray(ModelNode::Ptr const& n) const;

/** Access the field name storage */
[[nodiscard]]
std::shared_ptr<StringPool> strings() const;
Expand Down
72 changes: 40 additions & 32 deletions include/simfil/model/nodes.h
Original file line number Diff line number Diff line change
Expand Up @@ -67,18 +67,18 @@ using ScalarValueType = std::variant<

namespace detail
{
// Passkey for ModelNode construction: ModelNode types take this in their constructors so only
// model_ptr (and ModelPool via a shared key instance) can default/in-place construct them.
// This avoids per-node friendship and keeps IDEs happy across library boundaries.
struct mp_key
{
mp_key() = delete;
private:
explicit mp_key(int) {}
template<typename> friend struct ::simfil::model_ptr;
friend class ::simfil::Model;
friend class ::simfil::ModelPool;
};
// Passkey for ModelNode construction: ModelNode types take this in their constructors so only
// model_ptr (and ModelPool via a shared key instance) can default/in-place construct them.
// This avoids per-node friendship and keeps IDEs happy across library boundaries.
struct mp_key
{
mp_key() = delete;
private:
explicit mp_key(int) {}
template<typename> friend struct ::simfil::model_ptr;
friend class ::simfil::Model;
friend class ::simfil::ModelPool;
};
}

/**
Expand Down Expand Up @@ -206,6 +206,25 @@ struct ModelNodeAddress
}
};

namespace detail
{
// Shared storage entry for object fields across all BaseObject instantiations.
// Keeps the underlying ArrayArena type identical regardless of ModelType.
struct ObjectField
{
ObjectField() = default;
ObjectField(StringId name, ModelNodeAddress a) : name_(name), node_(a) {}
StringId name_ = StringPool::Empty;
ModelNodeAddress node_;

template<typename S>
void serialize(S& s) {
s.value2b(name_);
s.object(node_);
}
};
}

/** Semantic view onto a particular node in a ModelPool. */
struct ModelNode
{
Expand Down Expand Up @@ -413,13 +432,15 @@ struct ValueNode final : public ModelNodeBase
* a reference to a Model-derived pool type.
* @tparam ModelType Model-derived type.
*/
template<class ModelType>
template<class ModelTypeT>
struct MandatoryDerivedModelNodeBase : public ModelNodeBase
{
inline ModelType& model() const {return *modelPtr<ModelType>();} // NOLINT
using ModelType = ModelTypeT;

inline ModelTypeT& model() const {return *modelPtr<ModelTypeT>();} // NOLINT

protected:
template<class ModelType_ = ModelType>
template<class ModelType_ = ModelTypeT>
inline ModelType_* modelPtr() const {
return static_cast<ModelType_*>(const_cast<Model*>(model_.get()));
}
Expand Down Expand Up @@ -570,24 +591,9 @@ struct BaseObject : public MandatoryDerivedModelNodeBase<ModelType>

protected:
/**
* Object field - a name and a tree node address.
* These are stored in the ModelPools Field array arena.
* Object fields are stored in the model's shared object-field arena.
*/
struct Field
{
Field() = default;
Field(StringId name, ModelNodeAddress a) : name_(name), node_(a) {}
StringId name_ = StringPool::Empty;
ModelNodeAddress node_;

template<typename S>
void serialize(S& s) {
s.value2b(name_);
s.object(node_);
}
};

using Storage = ArrayArena<Field, detail::ColumnPageSize*2>;
using Storage = ArrayArena<detail::ObjectField, detail::ColumnPageSize*2>;
using ModelNode::model_;
using MandatoryDerivedModelNodeBase<ModelType>::model;

Expand Down Expand Up @@ -637,6 +643,8 @@ template<uint16_t MaxProceduralFields, class LambdaThisType=Object, class ModelP
class ProceduralObject : public Object
{
public:
using ModelType = ModelPoolDerivedModel;

[[nodiscard]] ModelNode::Ptr at(int64_t i) const override {
if (i < fields_.size())
return fields_[i].second(static_cast<LambdaThisType const&>(*this));
Expand Down
16 changes: 10 additions & 6 deletions src/model/model.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -394,17 +394,21 @@ ModelNode::Ptr ModelPool::newValue(StringId handle) {
ModelNodeAddress{PooledString, static_cast<uint32_t>(handle)});
}

model_ptr<Object> ModelPool::resolveObject(const ModelNode::Ptr& n) const {
if (n->addr_.column() != Objects)
// Core ADL resolve hooks for base Object/Array nodes.
template<>
model_ptr<Object> resolveInternal(res::tag<Object>, ModelPool const& model, ModelNode const& node)
{
if (node.addr().column() != ModelPool::Objects)
raise<std::runtime_error>("Cannot cast this node to an object.");
return model_ptr<Object>::make(shared_from_this(), n->addr_);
return model_ptr<Object>::make(model.shared_from_this(), node.addr());
}

model_ptr<Array> ModelPool::resolveArray(ModelNode::Ptr const& n) const
template<>
model_ptr<Array> resolveInternal(res::tag<Array>, ModelPool const& model, ModelNode const& node)
{
if (n->addr_.column() != Arrays)
if (node.addr().column() != ModelPool::Arrays)
raise<std::runtime_error>("Cannot cast this node to an array.");
return model_ptr<Array>::make(shared_from_this(), n->addr_);
return model_ptr<Array>::make(model.shared_from_this(), node.addr());
}

std::shared_ptr<StringPool> ModelPool::strings() const
Expand Down
Loading