From 59ba170957a3a580b8aa08e057036dbb358022ac Mon Sep 17 00:00:00 2001 From: Kenneth Weiss Date: Tue, 30 Jun 2026 09:50:40 -0700 Subject: [PATCH 01/52] Klee: Adds callback functionality to lua-based Klee input Converted some SLIC_ERROR() calls to throwing exceptions so we can provide better error handling. --- src/axom/inlet/Container.cpp | 29 +- src/axom/inlet/LuaReader.cpp | 97 +++- src/axom/inlet/tests/inlet_function.cpp | 15 + .../klee/docs/sphinx/specifying_shapes.rst | 52 +++ src/axom/klee/io/GeometryOperatorsIO.cpp | 430 +++++++++++++++--- src/axom/klee/io/GeometryOperatorsIO.hpp | 24 +- src/axom/klee/io/IO.cpp | 35 +- src/axom/klee/tests/klee_io.cpp | 206 +++++++++ 8 files changed, 794 insertions(+), 94 deletions(-) diff --git a/src/axom/inlet/Container.cpp b/src/axom/inlet/Container.cpp index 7fc8f8d623..8addd4c5d6 100644 --- a/src/axom/inlet/Container.cpp +++ b/src/axom/inlet/Container.cpp @@ -905,9 +905,32 @@ Verifiable& Container::addFunction(const std::string& name, const bool is_nested = transformFromNestedElements( std::back_inserter(funcs), name, - [&name, &ret_type, &arg_types, &description](Container& subcontainer, - const std::string& path) -> Verifiable& { - return subcontainer.addFunction(name, ret_type, arg_types, description, path); + [&name, &ret_type, &arg_types, &description, &pathOverride]( + Container& subcontainer, + const std::string& path) -> Verifiable& { + std::string nestedPathOverride = path; + if(!pathOverride.empty()) + { + // Function aliases can keep an internal schema name while reading from + // a public input path. For struct arrays, apply the override relative + // to each concrete element path found by transformFromNestedElements(). + if(path.empty()) + { + if(subcontainer.isStructCollection() || !subcontainer.m_nested_aggregates.empty()) + { + nestedPathOverride = pathOverride; + } + else + { + nestedPathOverride = Path::join({Path(subcontainer.name()), Path(pathOverride)}); + } + } + else + { + nestedPathOverride = Path::join({Path(path).parent(), Path(pathOverride)}); + } + } + return subcontainer.addFunction(name, ret_type, arg_types, description, nestedPathOverride); }); if(is_nested) { diff --git a/src/axom/inlet/LuaReader.cpp b/src/axom/inlet/LuaReader.cpp index 9c6fd9e2e8..f63193de3a 100644 --- a/src/axom/inlet/LuaReader.cpp +++ b/src/axom/inlet/LuaReader.cpp @@ -13,6 +13,7 @@ */ #include +#include #include "axom/inlet/LuaReader.hpp" @@ -380,9 +381,15 @@ template axom::sol::protected_function_result callWith(const axom::sol::protected_function& func, Args&&... args) { + // Lua functions are exposed to clients as std::functions that can be invoked + // after schema verification. Use a catchable failure here so those clients can + // add context; SLIC errors may abort or only log and continue. auto tentative_result = func(std::forward(args)...); - SLIC_ERROR_IF(!tentative_result.valid(), - "[Inlet] Lua function call failed, argument types possibly incorrect"); + if(!tentative_result.valid()) + { + axom::sol::error err = tentative_result; + throw std::runtime_error(fmt::format("[Inlet] Lua function call failed: {0}", err.what())); + } return tentative_result; } @@ -401,7 +408,12 @@ template Ret extractResult(axom::sol::protected_function_result&& res) { axom::sol::optional option = res; - SLIC_ERROR_IF(!option, "[Inlet] Lua function call failed, return types possibly incorrect"); + if(!option) + { + // A failed result conversion is a runtime input error for this function + // call. Throwing avoids dereferencing an empty optional after a SLIC log. + throw std::runtime_error("[Inlet] Lua function call failed, return types possibly incorrect"); + } return option.value(); } @@ -409,6 +421,56 @@ template <> FunctionType::Void extractResult(axom::sol::protected_function_result&&) { } +template <> +FunctionType::Vector extractResult(axom::sol::protected_function_result&& res) +{ + // Keep Vector.new(...) returns supported, but also accept raw numeric Lua + // tables so input decks can write idiomatic vector callbacks such as + // function() return {1.0, 2.0, 3.0} end. + axom::sol::optional vector_option = res; + if(vector_option) + { + return vector_option.value(); + } + + axom::sol::optional table_option = res; + if(table_option) + { + axom::sol::table table = table_option.value(); + const auto size = table.size(); + if(size < 1 || size > 3) + { + throw std::runtime_error( + fmt::format("[Inlet] Lua vector function returned a table with {0} entries; expected 1 to " + "3 numeric entries", + size)); + } + + std::vector values; + values.reserve(size); + for(std::size_t i = 1; i <= size; ++i) + { + axom::sol::optional value = table[i]; + if(!value) + { + throw std::runtime_error(fmt::format( + "[Inlet] Lua vector function returned a table with a non-numeric entry at index {0}", + i)); + } + values.push_back(value.value()); + } + return FunctionType::Vector {values.data(), static_cast(values.size())}; + } + + axom::sol::optional scalar_option = res; + if(scalar_option) + { + return FunctionType::Vector {scalar_option.value()}; + } + + throw std::runtime_error("[Inlet] Lua function call failed, return types possibly incorrect"); +} + /*! ***************************************************************************** * \brief Creates a std::function given a Lua function and template parameters @@ -540,10 +602,17 @@ ReaderResult LuaReader::getValue(const std::string& id, T& value) { std::vector tokens = axom::utilities::string::split(id, SCOPE_DELIMITER); + // A schema may register a function alias with the same input path as a + // concrete field. Treat a Lua function as absent for value readers so the + // function schema entry can claim it instead of failing as a wrong type. if(tokens.size() == 1) { if((*m_lua)[tokens[0]].valid()) { + if((*m_lua)[tokens[0]].get_type() == axom::sol::type::function) + { + return ReaderResult::NotFound; + } return detail::checkedGet((*m_lua)[tokens[0]], value); } return ReaderResult::NotFound; @@ -555,6 +624,10 @@ ReaderResult LuaReader::getValue(const std::string& id, T& value) { if(t[tokens.back()].valid()) { + if(t[tokens.back()].get_type() == axom::sol::type::function) + { + return ReaderResult::NotFound; + } return detail::checkedGet(t[tokens.back()], value); } } @@ -577,6 +650,24 @@ ReaderResult LuaReader::getMap(const std::string& id, values.clear(); std::vector tokens = axom::utilities::string::split(id, SCOPE_DELIMITER); + // As with scalar value reads, a function at this path belongs to a function + // schema alias rather than to the map reader. + if(tokens.size() == 1 && (*m_lua)[tokens[0]].valid() && + (*m_lua)[tokens[0]].get_type() == axom::sol::type::function) + { + return ReaderResult::NotFound; + } + + if(tokens.size() > 1) + { + axom::sol::table parent; + if(traverseToTable(tokens.begin(), tokens.end() - 1, parent) && parent[tokens.back()].valid() && + parent[tokens.back()].get_type() == axom::sol::type::function) + { + return ReaderResult::NotFound; + } + } + axom::sol::table t; if(tokens.empty() || !traverseToTable(tokens.begin(), tokens.end(), t)) { diff --git a/src/axom/inlet/tests/inlet_function.cpp b/src/axom/inlet/tests/inlet_function.cpp index c8dd19562a..4e04994eb9 100644 --- a/src/axom/inlet/tests/inlet_function.cpp +++ b/src/axom/inlet/tests/inlet_function.cpp @@ -66,6 +66,21 @@ TEST(inlet_function, simple_vec3_to_vec3_raw) EXPECT_FLOAT_EQ(result[2], 6); } +TEST(inlet_function, simple_vec3_to_vec3_raw_table_return) +{ + std::string testString = "function foo (v) return {v.x + 1, v.y + 2, v.z + 3} end"; + auto inlet = createBasicInlet(testString); + + auto func = inlet.reader().getFunction("foo", FunctionTag::Vector, {FunctionTag::Vector}); + + EXPECT_TRUE(func); + auto result = func.call(FunctionType::Vector {1, 2, 3}); + EXPECT_EQ(result.dim, 3); + EXPECT_FLOAT_EQ(result[0], 2); + EXPECT_FLOAT_EQ(result[1], 4); + EXPECT_FLOAT_EQ(result[2], 6); +} + TEST(inlet_function, simple_vec3_to_vec3_raw_partial_init) { std::string testString = "function foo (v) return 2*v end"; diff --git a/src/axom/klee/docs/sphinx/specifying_shapes.rst b/src/axom/klee/docs/sphinx/specifying_shapes.rst index 81613793cd..212460e458 100644 --- a/src/axom/klee/docs/sphinx/specifying_shapes.rst +++ b/src/axom/klee/docs/sphinx/specifying_shapes.rst @@ -128,6 +128,51 @@ global namespace contains only the Klee schema fields that Inlet should read. For Lua input, a one-value scale is written as a one-entry table, for example :code:`{ scale = {2.0} }`. +Selected operator fields may also be written as zero-argument Lua callbacks. +Klee evaluates each callback exactly once while reading the deck; the resulting +shape still contains ordinary affine or slice operators, not runtime Lua +functions. Callbacks should be pure functions of local deck variables. + +.. code-block:: lua + + local dim = 2 + local r = 4.0 + local z = 8.0 + local x = 1.0 + local y = 2.0 + + dimensions = dim + + shapes = { + { + name = "part", + material = "steel", + geometry = { + format = "stl", + path = "part.stl", + units = "cm", + operators = { + { + translate = function() + if dim == 2 then + return {r, z} + end + return {x, y, z} + end + } + } + } + } + } + +Vector-valued callbacks return raw numeric Lua tables such as :code:`{x, y}` or +:code:`{x, y, z}`. The typed :code:`Vector.new(...)` object is also accepted. +Scalar-valued callbacks return a number. Supported callback fields are +:code:`translate`, :code:`axis`, :code:`center`, :code:`scale`, +:code:`slice.origin`, :code:`slice.normal`, :code:`slice.up`, :code:`rotate`, +:code:`slice.x`, :code:`slice.y`, and :code:`slice.z`. For :code:`scale`, a +number means uniform scaling and a table means per-axis scaling. + Common Lua input errors are reported as Klee parsing errors. A Lua input file read without Lua support reports: @@ -153,6 +198,13 @@ or inspect :code:`getErrors()` when multiple verification errors are available. Klee may still throw standard exceptions such as :code:`std::logic_error` or :code:`std::invalid_argument` for programming errors or inconsistent manually constructed objects. +Callback failures include the field, shape name when available, and operator +location, for example: + +.. code-block:: text + + Error evaluating callback for 'translate' in shape 'part' operator 1: [Inlet] Lua function call failed: ... + Paths ***** The paths specified in shapes are specified either as absolute paths diff --git a/src/axom/klee/io/GeometryOperatorsIO.cpp b/src/axom/klee/io/GeometryOperatorsIO.cpp index 19b4baca26..38d51f225d 100644 --- a/src/axom/klee/io/GeometryOperatorsIO.cpp +++ b/src/axom/klee/io/GeometryOperatorsIO.cpp @@ -19,6 +19,7 @@ #include #include #include +#include namespace axom { @@ -30,7 +31,7 @@ namespace { using OpPtr = CompositeOperator::OpPtr; using OperatorParser = - std::function; + std::function; using internal::toDoubleVector; using primal::Point3D; using primal::Vector3D; @@ -46,6 +47,198 @@ std::string childName(const inlet::Container& container, const std::string& name return result; } +// Callback schema entries are internal aliases: the public input path remains +// the ordinary operator field name, while Inlet stores the function separately. +constexpr char const *LUA_CALLBACK_SUFFIX = "__klee_lua_callback"; + +std::string callbackName(char const *fieldName) +{ + return std::string(fieldName) + LUA_CALLBACK_SUFFIX; +} + +std::string publicNameForCallback(const std::string &childName) +{ + const std::string suffix = LUA_CALLBACK_SUFFIX; + if(childName.size() > suffix.size() && + childName.compare(childName.size() - suffix.size(), suffix.size(), suffix) == 0) + { + return childName.substr(0, childName.size() - suffix.size()); + } + return childName; +} + +bool hasCallback(const inlet::Container &container, char const *fieldName) +{ + const auto name = callbackName(fieldName); + return container.contains(name); +} + +bool containsFieldOrCallback(const inlet::Container &container, char const *fieldName) +{ + return container.contains(fieldName) || hasCallback(container, fieldName); +} + +Path fieldPath(const inlet::Container &container, char const *fieldName) +{ + return Path::join({Path {container.name()}, Path {std::string {fieldName}}}); +} + +std::string callbackContext(const inlet::Container &container, + char const *fieldName, + const std::string &shapeName) +{ + Path path {container.name()}; + std::string operatorIndex = path.baseName(); + if(operatorIndex == "slice") + { + operatorIndex = path.parent().baseName(); + } + + const auto operatorLabel = operatorIndex.empty() ? std::string {"operator at "} + container.name() + : std::string {"operator "} + operatorIndex; + if(shapeName.empty()) + { + return axom::fmt::format("Error evaluating callback for '{}' in {}", fieldName, operatorLabel); + } + return axom::fmt::format("Error evaluating callback for '{}' in shape '{}' {}", + fieldName, + shapeName, + operatorLabel); +} + +template +Result wrapCallbackErrors(const inlet::Container &container, + char const *fieldName, + const std::string &shapeName, + Func &&func) +{ + // Convert generic Inlet/Lua callback failures into Klee diagnostics at the + // boundary where the shape, operator, and field context are all available. + try + { + return func(); + } + catch(const KleeError &) + { + throw; + } + catch(const std::exception &ex) + { + throw KleeError( + {fieldPath(container, fieldName), + axom::fmt::format("{}: {}", callbackContext(container, fieldName, shapeName), ex.what())}); + } +} + +double getScalar(const inlet::Container &container, + char const *fieldName, + const std::string &shapeName) +{ + if(hasCallback(container, fieldName)) + { + return wrapCallbackErrors(container, fieldName, shapeName, [&]() { + return container[callbackName(fieldName)].call(); + }); + } + return container[fieldName].get(); +} + +std::vector callbackVectorToDoubleVector(const inlet::FunctionType::Vector &value) +{ + std::vector result; + result.reserve(value.dim); + for(int i = 0; i < value.dim; ++i) + { + result.push_back(value.vec[i]); + } + return result; +} + +std::vector getDoubleVector(const inlet::Container &container, + char const *fieldName, + Dimensions expectedDims, + const std::string &shapeName) +{ + if(hasCallback(container, fieldName)) + { + auto values = wrapCallbackErrors>(container, fieldName, shapeName, [&]() { + return callbackVectorToDoubleVector( + container[callbackName(fieldName)].call()); + }); + auto actualSize = values.size(); + auto expectedSize = static_cast(expectedDims); + if(actualSize != expectedSize) + { + throw KleeError({fieldPath(container, fieldName), + fmt::format("{}: Wrong size for {}. Expected {}. Got {}.", + callbackContext(container, fieldName, shapeName), + fieldName, + expectedSize, + actualSize)}); + } + return values; + } + return toDoubleVector(container[fieldName], expectedDims, fieldName); +} + +template +T toArrayLike(const inlet::Container &parent, + char const *fieldName, + Dimensions expectedDims, + const std::string &shapeName) +{ + auto values = getDoubleVector(parent, fieldName, expectedDims, shapeName); + return T {values.data(), static_cast(expectedDims)}; +} + +template +T toArrayLike(const inlet::Container &parent, + char const *fieldName, + Dimensions expectedDims, + const T &defaultValue, + const std::string &shapeName) +{ + if(containsFieldOrCallback(parent, fieldName)) + { + return toArrayLike(parent, fieldName, expectedDims, shapeName); + } + return defaultValue; +} + +Point3D getPoint(const inlet::Container &parent, + char const *fieldName, + Dimensions expectedDims, + const std::string &shapeName) +{ + return toArrayLike(parent, fieldName, expectedDims, shapeName); +} + +Point3D getPoint(const inlet::Container &parent, + char const *fieldName, + Dimensions expectedDims, + const Point3D &defaultValue, + const std::string &shapeName) +{ + return toArrayLike(parent, fieldName, expectedDims, defaultValue, shapeName); +} + +Vector3D getVector(const inlet::Container &parent, + char const *fieldName, + Dimensions expectedDims, + const std::string &shapeName) +{ + return toArrayLike(parent, fieldName, expectedDims, shapeName); +} + +Vector3D getVector(const inlet::Container &parent, + char const *fieldName, + Dimensions expectedDims, + const Vector3D &defaultValue, + const std::string &shapeName) +{ + return toArrayLike(parent, fieldName, expectedDims, defaultValue, shapeName); +} + /** * Get the names of all the children in the given container. * @@ -79,7 +272,7 @@ std::unordered_set getChildNames(const inlet::Container& container) { if(*child.second) { - allChildren.insert(childName(container, child.first)); + allChildren.insert(publicNameForCallback(childName(container, child.first))); } } @@ -125,7 +318,7 @@ void verifyObjectFields(const inlet::Container& containerToTest, for(auto& requiredField : requiredFields) { - if(!containerToTest.contains(requiredField)) + if(!containsFieldOrCallback(containerToTest, requiredField.c_str())) { throw KleeError( {containerToTest.name(), @@ -157,11 +350,13 @@ void verifyObjectFields(const inlet::Container& containerToTest, * \return the created operator * \throws KleeError if the operator fields or vector dimensions are invalid */ -OpPtr parseTranslate(const inlet::Container& opContainer, - const TransformableGeometryProperties& startProperties) +OpPtr parseTranslate(const SingleOperatorData &data, + const TransformableGeometryProperties &startProperties) { + const auto &opContainer = *data.m_container; verifyObjectFields(opContainer, "translate", FieldSet {}, FieldSet {}); - return std::make_shared(toVector(opContainer, "translate", startProperties.dimensions), + return std::make_shared( + getVector(opContainer, "translate", startProperties.dimensions, data.m_shapeName), startProperties); } @@ -173,9 +368,10 @@ OpPtr parseTranslate(const inlet::Container& opContainer, * \return the created operator * \throws KleeError if the rotation is invalid for the start dimensions or operator fields */ -OpPtr parseRotate(const inlet::Container& opContainer, - const TransformableGeometryProperties& startProperties) +OpPtr parseRotate(const SingleOperatorData &data, + const TransformableGeometryProperties &startProperties) { + const auto &opContainer = *data.m_container; switch(startProperties.dimensions) { case Dimensions::Two: @@ -183,8 +379,8 @@ OpPtr parseRotate(const inlet::Container& opContainer, verifyObjectFields(opContainer, "rotate", FieldSet {}, {"center"}); Vector3D axis {0, 0, 1}; return std::make_shared( - opContainer["rotate"].get(), - toPoint(opContainer, "center", Dimensions::Two, Point3D {0, 0, 0}), + getScalar(opContainer, "rotate", data.m_shapeName), + getPoint(opContainer, "center", Dimensions::Two, Point3D {0, 0, 0}, data.m_shapeName), axis, startProperties); } @@ -193,9 +389,9 @@ OpPtr parseRotate(const inlet::Container& opContainer, { verifyObjectFields(opContainer, "rotate", {"axis"}, {"center"}); return std::make_shared( - opContainer["rotate"].get(), - toPoint(opContainer, "center", Dimensions::Three, Point3D {0, 0, 0}), - toVector(opContainer, "axis", Dimensions::Three), + getScalar(opContainer, "rotate", data.m_shapeName), + getPoint(opContainer, "center", Dimensions::Three, Point3D {0, 0, 0}, data.m_shapeName), + getVector(opContainer, "axis", Dimensions::Three, data.m_shapeName), startProperties); } break; @@ -242,11 +438,12 @@ OpPtr makeCheckedSlice(Point3D origin, * \return the point to use as the origin * \throws KleeError if the specified origin is not on the slice plane */ -primal::Point3D getPerpendicularSliceOrigin(const inlet::Container& sliceContainer, - char const* planeName, - const primal::Vector3D& defaultNormal) +primal::Point3D getPerpendicularSliceOrigin(const inlet::Container &sliceContainer, + char const *planeName, + const primal::Vector3D &defaultNormal, + const std::string &shapeName) { - double axisIntercept = sliceContainer[planeName]; + double axisIntercept = getScalar(sliceContainer, planeName, shapeName); primal::Point3D defaultOrigin; int nonZeroIndex = -1; @@ -259,12 +456,12 @@ primal::Point3D getPerpendicularSliceOrigin(const inlet::Container& sliceContain } } - if(!sliceContainer.contains("origin")) + if(!containsFieldOrCallback(sliceContainer, "origin")) { return defaultOrigin; } - primal::Point3D givenOrigin = toPoint(sliceContainer, "origin", Dimensions::Three); + primal::Point3D givenOrigin = getPoint(sliceContainer, "origin", Dimensions::Three, shapeName); if(givenOrigin[nonZeroIndex] != axisIntercept) { throw KleeError({sliceContainer["origin"].name(), "The origin must be on the slice plane"}); @@ -280,15 +477,16 @@ primal::Point3D getPerpendicularSliceOrigin(const inlet::Container& sliceContain * \return the vector to use as the normal * \throws KleeError if the specified normal is not parallel to the slice plane normal */ -primal::Vector3D getPerpendicularSliceNormal(const inlet::Container& sliceContainer, - const primal::Vector3D& defaultNormal) +primal::Vector3D getPerpendicularSliceNormal(const inlet::Container &sliceContainer, + const primal::Vector3D &defaultNormal, + const std::string &shapeName) { - if(!sliceContainer.contains("normal")) + if(!containsFieldOrCallback(sliceContainer, "normal")) { return defaultNormal; } - primal::Vector3D givenNormal = toVector(sliceContainer, "normal", Dimensions::Three); + primal::Vector3D givenNormal = getVector(sliceContainer, "normal", Dimensions::Three, shapeName); auto cross = primal::Vector3D::cross_product(givenNormal, defaultNormal); bool parallel = cross.is_zero(); if(!parallel) @@ -309,18 +507,19 @@ primal::Vector3D getPerpendicularSliceNormal(const inlet::Container& sliceContai * \return the parsed plane * \throws KleeError if the slice fields or values are invalid */ -OpPtr readPerpendicularSlice(const inlet::Container& sliceContainer, - char const* planeName, - Vector3D const& defaultNormal, - Vector3D const& defaultUp, - const TransformableGeometryProperties& startProperties) +OpPtr readPerpendicularSlice(const inlet::Container &sliceContainer, + char const *planeName, + Vector3D const &defaultNormal, + Vector3D const &defaultUp, + const TransformableGeometryProperties &startProperties, + const std::string &shapeName) { verifyObjectFields(sliceContainer, planeName, FieldSet {}, {"origin", "normal", "up"}); const primal::Vector3D defaultNormalVec {defaultNormal.data()}; - auto origin = getPerpendicularSliceOrigin(sliceContainer, planeName, defaultNormalVec); - auto normal = getPerpendicularSliceNormal(sliceContainer, defaultNormalVec); - auto up = toVector(sliceContainer, "up", Dimensions::Three, defaultUp); + auto origin = getPerpendicularSliceOrigin(sliceContainer, planeName, defaultNormalVec, shapeName); + auto normal = getPerpendicularSliceNormal(sliceContainer, defaultNormalVec, shapeName); + auto up = getVector(sliceContainer, "up", Dimensions::Three, defaultUp, shapeName); return makeCheckedSlice(origin, normal, up, startProperties, sliceContainer.name()); } @@ -333,33 +532,49 @@ OpPtr readPerpendicularSlice(const inlet::Container& sliceContainer, * \return the created operator * \throws KleeError if the slice fields or values are invalid */ -OpPtr parseSlice(const inlet::Container& opContainer, - const TransformableGeometryProperties& startProperties) +OpPtr parseSlice(const SingleOperatorData &data, + const TransformableGeometryProperties &startProperties) { + const auto &opContainer = *data.m_container; if(startProperties.dimensions != Dimensions::Three) { throw KleeError({opContainer.name(), "Cannot do a slice from 2D"}); } verifyObjectFields(opContainer, "slice", FieldSet {}, FieldSet {}); - auto& sliceContainer = *opContainer.getChildContainers().at(opContainer.name() + "/slice").get(); - if(sliceContainer.contains("x")) + auto &sliceContainer = *opContainer.getChildContainers().at(opContainer.name() + "/slice").get(); + if(containsFieldOrCallback(sliceContainer, "x")) { - return readPerpendicularSlice(sliceContainer, "x", {1, 0, 0}, {0, 0, 1}, startProperties); + return readPerpendicularSlice(sliceContainer, + "x", + {1, 0, 0}, + {0, 0, 1}, + startProperties, + data.m_shapeName); } - else if(sliceContainer.contains("y")) + else if(containsFieldOrCallback(sliceContainer, "y")) { - return readPerpendicularSlice(sliceContainer, "y", {0, 1, 0}, {1, 0, 0}, startProperties); + return readPerpendicularSlice(sliceContainer, + "y", + {0, 1, 0}, + {1, 0, 0}, + startProperties, + data.m_shapeName); } - else if(sliceContainer.contains("z")) + else if(containsFieldOrCallback(sliceContainer, "z")) { - return readPerpendicularSlice(sliceContainer, "z", {0, 0, 1}, {0, 1, 0}, startProperties); + return readPerpendicularSlice(sliceContainer, + "z", + {0, 0, 1}, + {0, 1, 0}, + startProperties, + data.m_shapeName); } verifyObjectFields(sliceContainer, "origin", {"normal", "up"}, FieldSet {}); - return makeCheckedSlice(toPoint(sliceContainer, "origin", Dimensions::Three), - toVector(sliceContainer, "normal", Dimensions::Three), - toVector(sliceContainer, "up", Dimensions::Three), + return makeCheckedSlice(getPoint(sliceContainer, "origin", Dimensions::Three, data.m_shapeName), + getVector(sliceContainer, "normal", Dimensions::Three, data.m_shapeName), + getVector(sliceContainer, "up", Dimensions::Three, data.m_shapeName), startProperties, sliceContainer.name()); } @@ -372,24 +587,51 @@ OpPtr parseSlice(const inlet::Container& opContainer, * \return the created operator * \throws KleeError if the scale fields or vector dimensions are invalid */ -OpPtr parseScale(const inlet::Container& opContainer, - const TransformableGeometryProperties& startProperties) +OpPtr parseScale(const SingleOperatorData &data, + const TransformableGeometryProperties &startProperties) { + const auto &opContainer = *data.m_container; verifyObjectFields(opContainer, "scale", FieldSet {}, FieldSet {"center"}); - auto factors = opContainer["scale"].get>(); + auto factors = hasCallback(opContainer, "scale") + ? wrapCallbackErrors>( + opContainer, + "scale", + data.m_shapeName, + [&]() { + return callbackVectorToDoubleVector( + opContainer[callbackName("scale")].call()); + }) + : opContainer["scale"].get>(); if(factors.size() == 1) { return std::make_shared(factors[0], factors[0], factors[0], startProperties); } - factors = toDoubleVector(opContainer["scale"], startProperties.dimensions, "scale"); + if(hasCallback(opContainer, "scale")) + { + auto actualSize = factors.size(); + auto expectedSize = static_cast(startProperties.dimensions); + if(actualSize != expectedSize) + { + throw KleeError({fieldPath(opContainer, "scale"), + fmt::format("{}: Wrong size for scale. Expected {}. Got {}.", + callbackContext(opContainer, "scale", data.m_shapeName), + expectedSize, + actualSize)}); + } + } + else + { + factors = toDoubleVector(opContainer["scale"], startProperties.dimensions, "scale"); + } if(startProperties.dimensions == Dimensions::Two) { factors.emplace_back(1.0); } Point3D center {0., 0., 0.}; - if(opContainer.contains("center")) + if(containsFieldOrCallback(opContainer, "center")) { - center = toPoint(opContainer, "center", startProperties.dimensions, Point3D {0, 0, 0}); + center = + getPoint(opContainer, "center", startProperties.dimensions, Point3D {0, 0, 0}, data.m_shapeName); } return std::make_shared(factors[0], factors[1], factors[2], center, startProperties); @@ -403,9 +645,10 @@ OpPtr parseScale(const inlet::Container& opContainer, * \return the created operator * \throws KleeError if the unit string or operator fields are invalid */ -OpPtr parseConvertUnits(const inlet::Container& opContainer, - const TransformableGeometryProperties& startProperties) +OpPtr parseConvertUnits(const SingleOperatorData &data, + const TransformableGeometryProperties &startProperties) { + const auto &opContainer = *data.m_container; verifyObjectFields(opContainer, "convert_units_to", FieldSet {}, FieldSet {}); auto endUnits = internal::parseLengthUnits(opContainer["convert_units_to"]); return std::make_shared(endUnits, startProperties); @@ -420,10 +663,11 @@ OpPtr parseConvertUnits(const inlet::Container& opContainer, * \return the created operator * \throws KleeError if the reference is missing or the operator fields are invalid */ -OpPtr parseRef(const inlet::Container& opContainer, - const TransformableGeometryProperties& startProperties, - const NamedOperatorMap& namedOperators) +OpPtr parseRef(const SingleOperatorData &data, + const TransformableGeometryProperties &startProperties, + const NamedOperatorMap &namedOperators) { + const auto &opContainer = *data.m_container; verifyObjectFields(opContainer, "ref", FieldSet {}, FieldSet {}); std::string const& operatorName = opContainer["ref"]; auto opIter = namedOperators.find(operatorName); @@ -479,21 +723,32 @@ OpPtr convertOperator(SingleOperatorData const& data, {"scale", parseScale}, {"convert_units_to", parseConvertUnits}, {"ref", - [&namedOperators](const inlet::Container& opNode, - const TransformableGeometryProperties& startProperties) { - return parseRef(opNode, startProperties, namedOperators); + [&namedOperators](const SingleOperatorData &opData, + const TransformableGeometryProperties &startProperties) { + return parseRef(opData, startProperties, namedOperators); }}, }; for(auto& entry : parsers) { - if(data.m_container->contains(entry.first)) + if(containsFieldOrCallback(*data.m_container, entry.first.c_str())) { - return entry.second(*data.m_container, startProperties); + return entry.second(data, startProperties); } } - throw KleeError({data.m_container->name(), "Invalid transformation"}); + auto childNames = getChildNames(*data.m_container); + std::string message = axom::fmt::format("Invalid transformation at {}", data.m_container->name()); + if(!childNames.empty()) + { + message += ". Found parameters:"; + for(const auto &name : childNames) + { + message += " "; + message += name; + } + } + throw KleeError({data.m_container->name(), message}); } } // namespace @@ -509,9 +764,19 @@ GeometryOperatorData::GeometryOperatorData(const Path& path, , m_singleOperatorData {singleOperatorData} { } -inlet::Container& GeometryOperatorData::defineSchema(inlet::Container& parent, - const std::string& fieldName, - const std::string& description) +void GeometryOperatorData::setShapeName(std::string shapeName) +{ + m_shapeName = std::move(shapeName); + for(auto &data : m_singleOperatorData) + { + data.m_shapeName = m_shapeName; + } +} + +inlet::Container &GeometryOperatorData::defineSchema(inlet::Container &parent, + const std::string &fieldName, + const std::string &description, + bool enableLuaCallbacks) { auto& opContainer = parent.addStructArray(fieldName, description).strict(); @@ -534,6 +799,27 @@ inlet::Container& GeometryOperatorData::defineSchema(inlet::Container& parent, slice.addDoubleArray("up"); opContainer.addString("ref"); + if(enableLuaCallbacks) + { + // These Lua-only function alternatives read from the public field paths via + // pathOverride, leaving YAML and concrete Lua field parsing unchanged. + opContainer.addFunction(callbackName("translate"), + inlet::FunctionTag::Vector, + {}, + "", + "translate"); + opContainer.addFunction(callbackName("rotate"), inlet::FunctionTag::Double, {}, "", "rotate"); + opContainer.addFunction(callbackName("center"), inlet::FunctionTag::Vector, {}, "", "center"); + opContainer.addFunction(callbackName("axis"), inlet::FunctionTag::Vector, {}, "", "axis"); + opContainer.addFunction(callbackName("scale"), inlet::FunctionTag::Vector, {}, "", "scale"); + + slice.addFunction(callbackName("x"), inlet::FunctionTag::Double, {}, "", "x"); + slice.addFunction(callbackName("y"), inlet::FunctionTag::Double, {}, "", "y"); + slice.addFunction(callbackName("z"), inlet::FunctionTag::Double, {}, "", "z"); + slice.addFunction(callbackName("origin"), inlet::FunctionTag::Vector, {}, "", "origin"); + slice.addFunction(callbackName("normal"), inlet::FunctionTag::Vector, {}, "", "normal"); + slice.addFunction(callbackName("up"), inlet::FunctionTag::Vector, {}, "", "up"); + } return opContainer; } @@ -558,7 +844,7 @@ std::shared_ptr GeometryOperatorData::makeOperator( return composite; } -void NamedOperatorData::defineSchema(inlet::Container& container) +void NamedOperatorData::defineSchema(inlet::Container &container, bool enableLuaCallbacks) { container.addString("name").required(); defineDimensionsField(container, "start_dimensions", "The initial dimensions of the operator"); @@ -566,18 +852,22 @@ void NamedOperatorData::defineSchema(inlet::Container& container) "The units (both start and end) of the operator", "The start units of the operator", "The end units of the operator"); - GeometryOperatorData::defineSchema(container, "value", - "The operation to apply"); //.required(); + GeometryOperatorData::defineSchema(container, + "value", + "The operation to apply", + enableLuaCallbacks); //.required(); } NamedOperatorMapData::NamedOperatorMapData(std::vector&& operatorData) : m_operatorData {operatorData} { } -void NamedOperatorMapData::defineSchema(inlet::Container& parent, const std::string& name) +void NamedOperatorMapData::defineSchema(inlet::Container &parent, + const std::string &name, + bool enableLuaCallbacks) { - auto& container = parent.addStructArray(name); - NamedOperatorData::defineSchema(container); + auto &container = parent.addStructArray(name); + NamedOperatorData::defineSchema(container, enableLuaCallbacks); } NamedOperatorMap NamedOperatorMapData::makeNamedOperatorMap(Dimensions fileDimensions) const @@ -616,7 +906,7 @@ struct FromInlet { axom::klee::internal::SingleOperatorData operator()(const axom::inlet::Container& base) { - return axom::klee::internal::SingleOperatorData {&base}; + return axom::klee::internal::SingleOperatorData {&base, ""}; } }; diff --git a/src/axom/klee/io/GeometryOperatorsIO.hpp b/src/axom/klee/io/GeometryOperatorsIO.hpp index 7686ff752e..222adf651b 100644 --- a/src/axom/klee/io/GeometryOperatorsIO.hpp +++ b/src/axom/klee/io/GeometryOperatorsIO.hpp @@ -28,7 +28,8 @@ using NamedOperatorMap = std::unordered_map m_singleOperatorData; + std::string m_shapeName; }; /// Data for a named operator. @@ -100,7 +110,7 @@ struct NamedOperatorData * * @param container the container in which to describe a single named operator */ - static void defineSchema(inlet::Container& container); + static void defineSchema(inlet::Container &container, bool enableLuaCallbacks = false); }; /// Data for all a collection of named operators @@ -132,7 +142,9 @@ struct NamedOperatorMapData * @param parent the parent object in which to define the operator map * @param name the name of the map */ - static void defineSchema(inlet::Container& parent, const std::string& name); + static void defineSchema(inlet::Container &parent, + const std::string &name, + bool enableLuaCallbacks = false); private: std::vector m_operatorData; diff --git a/src/axom/klee/io/IO.cpp b/src/axom/klee/io/IO.cpp index a00a0bccab..318ecad4f9 100644 --- a/src/axom/klee/io/IO.cpp +++ b/src/axom/klee/io/IO.cpp @@ -67,11 +67,13 @@ struct FromInlet { axom::klee::ShapeData operator()(const axom::inlet::Container& base) { - return axom::klee::ShapeData {base.get("name"), + axom::klee::ShapeData data {base.get("name"), base.get("material"), base["replaces"].get>(), base["does_not_replace"].get>(), base.get("geometry")}; + data.geometry.operatorData.setShapeName(data.name); + return data; } }; @@ -114,7 +116,7 @@ namespace * * @param geometry the Container representing a "geometry" object. */ -void defineGeometry(inlet::Container& geometry) +void defineGeometry(inlet::Container &geometry, bool enableLuaCallbacks) { geometry.addString("format", "The format of the input file").required(); geometry.addString("path", @@ -135,7 +137,8 @@ void defineGeometry(inlet::Container& geometry) "The end units of the shape"); internal::GeometryOperatorData::defineSchema(geometry, "operators", - "Operators to apply to this object"); + "Operators to apply to this object", + enableLuaCallbacks); } /** @@ -143,7 +146,7 @@ void defineGeometry(inlet::Container& geometry) * * @param document the Inlet document for which to define the schema */ -void defineShapeList(inlet::Inlet& document) +void defineShapeList(inlet::Inlet &document, bool enableLuaCallbacks) { inlet::Container& shapeList = document.addStructArray("shapes", "The list of shapes"); @@ -154,7 +157,7 @@ void defineShapeList(inlet::Inlet& document) auto& geometry = shapeList.addStruct("geometry", "Contains information about the shape's geometry"); - defineGeometry(geometry); + defineGeometry(geometry, enableLuaCallbacks); // Verify syntax here, semantics later!!! shapeList.registerVerifier( @@ -191,11 +194,13 @@ void defineShapeList(inlet::Inlet& document) * * @param document the Inlet document for which to define the schema */ -void defineKleeSchema(inlet::Inlet& document) +void defineKleeSchema(inlet::Inlet &document, bool enableLuaCallbacks) { internal::defineDimensionsField(document.getGlobalContainer(), "dimensions").required(); - defineShapeList(document); - internal::NamedOperatorMapData::defineSchema(document.getGlobalContainer(), "named_operators"); + defineShapeList(document, enableLuaCallbacks); + internal::NamedOperatorMapData::defineSchema(document.getGlobalContainer(), + "named_operators", + enableLuaCallbacks); } /** @@ -438,14 +443,17 @@ void appendUnexpectedGlobalErrors(const inlet::Inlet& doc, * * \param reader the parsed Inlet reader * \param rejectUnexpectedGlobals true if unexpected top-level Lua globals should be rejected + * \param enableLuaCallbacks true if Lua callbacks should be enabled * \return the parsed and verified ShapeSet * \throws KleeError if schema verification or semantic validation fails */ -ShapeSet readShapeSetFromReader(std::unique_ptr reader, bool rejectUnexpectedGlobals) +ShapeSet readShapeSetFromReader(std::unique_ptr reader, + bool rejectUnexpectedGlobals, + bool enableLuaCallbacks) { sidre::DataStore dataStore; inlet::Inlet doc(std::move(reader), dataStore.getRoot()); - defineKleeSchema(doc); + defineKleeSchema(doc, enableLuaCallbacks); std::vector errors; bool verified = doc.verify(&errors); if(rejectUnexpectedGlobals) @@ -484,7 +492,9 @@ ShapeSet readShapeSet(std::istream& stream, InputFormat format) format, Path {""}, "from stream"); - return readShapeSetFromReader(std::move(reader), format == InputFormat::Lua); + return readShapeSetFromReader(std::move(reader), + format == InputFormat::Lua, + format == InputFormat::Lua); } ShapeSet readShapeSet(const std::string& filePath) @@ -499,7 +509,8 @@ ShapeSet readShapeSet(const std::string& filePath, InputFormat format) format, Path {filePath}, axom::fmt::format("from file '{}'", filePath)); - auto shapeSet = readShapeSetFromReader(std::move(reader), format == InputFormat::Lua); + auto shapeSet = + readShapeSetFromReader(std::move(reader), format == InputFormat::Lua, format == InputFormat::Lua); shapeSet.setPath(filePath); return shapeSet; } diff --git a/src/axom/klee/tests/klee_io.cpp b/src/axom/klee/tests/klee_io.cpp index 9643cf1165..e355364900 100644 --- a/src/axom/klee/tests/klee_io.cpp +++ b/src/axom/klee/tests/klee_io.cpp @@ -770,6 +770,212 @@ TEST(IOTest, readShapeSet_luaGeneratedOrdinaryTableValues) EXPECT_THAT(translation->getOffset(), AlmostEqVector(Vector3D {4, 8, 0})); } +TEST(IOTest, readShapeSet_luaOperatorCallbacks) +{ + auto shapeSet = readShapeSetFromString(R"( + local dim = 3 + + dimensions = dim + + shapes = { + { + name = "callbacks", + material = "steel", + geometry = { + format = "stl", + path = "callbacks.stl", + units = "cm", + operators = { + { + rotate = function() return 45 end, + axis = function() return {0, 0, 1} end, + center = function() return Vector.new(1, 2, 3) end + }, + { translate = function() return {4, 5, 6} end }, + { scale = function() return 2.0 end }, + { + scale = function() return {1.5, 2.5, 3.5} end, + center = function() return {1, 1, 1} end + } + } + } + }, + { + name = "slice_callbacks", + material = "glass", + geometry = { + format = "stl", + path = "slice_callbacks.stl", + start_dimensions = 3, + dimensions = 2, + units = "cm", + operators = { + { + slice = { + origin = function() return {1, 2, 3} end, + normal = function() return {0, 0, 1} end, + up = function() return {0, 1, 0} end + } + } + } + } + } + } + )", + InputFormat::Lua); + + ASSERT_EQ(2u, shapeSet.getShapes().size()); + auto composite = std::dynamic_pointer_cast( + shapeSet.getShapes()[0].getGeometry().getGeometryOperator()); + ASSERT_TRUE(composite); + ASSERT_EQ(4u, composite->getOperators().size()); + + auto rotation = dynamic_cast(composite->getOperators()[0].get()); + ASSERT_NE(rotation, nullptr); + EXPECT_DOUBLE_EQ(45.0, rotation->getAngle()); + EXPECT_THAT(rotation->getAxis(), AlmostEqVector(Vector3D {0, 0, 1})); + EXPECT_THAT(rotation->getCenter(), AlmostEqPoint(Point3D {1, 2, 3})); + + auto translation = dynamic_cast(composite->getOperators()[1].get()); + ASSERT_NE(translation, nullptr); + EXPECT_THAT(translation->getOffset(), AlmostEqVector(Vector3D {4, 5, 6})); + + auto uniformScale = dynamic_cast(composite->getOperators()[2].get()); + ASSERT_NE(uniformScale, nullptr); + EXPECT_DOUBLE_EQ(2.0, uniformScale->getXFactor()); + EXPECT_DOUBLE_EQ(2.0, uniformScale->getYFactor()); + EXPECT_DOUBLE_EQ(2.0, uniformScale->getZFactor()); + + auto vectorScale = dynamic_cast(composite->getOperators()[3].get()); + ASSERT_NE(vectorScale, nullptr); + EXPECT_DOUBLE_EQ(1.5, vectorScale->getXFactor()); + EXPECT_DOUBLE_EQ(2.5, vectorScale->getYFactor()); + EXPECT_DOUBLE_EQ(3.5, vectorScale->getZFactor()); + EXPECT_THAT(vectorScale->getCenter(), AlmostEqPoint(Point3D {1, 1, 1})); + + auto sliceComposite = std::dynamic_pointer_cast( + shapeSet.getShapes()[1].getGeometry().getGeometryOperator()); + ASSERT_TRUE(sliceComposite); + ASSERT_EQ(1u, sliceComposite->getOperators().size()); + auto slice = std::dynamic_pointer_cast(sliceComposite->getOperators()[0]); + ASSERT_TRUE(slice); + EXPECT_THAT(slice->getOrigin(), AlmostEqPoint(Point3D {1, 2, 3})); + EXPECT_THAT(slice->getNormal(), AlmostEqVector(Vector3D {0, 0, 1})); + EXPECT_THAT(slice->getUp(), AlmostEqVector(Vector3D {0, 1, 0})); +} + +TEST(IOTest, readShapeSet_luaDimensionDependentCallback) +{ + auto shapeSet = readShapeSetFromString(R"( + local dim = 2 + local r = 4.0 + local z = 8.0 + local x = 1.0 + local y = 2.0 + + dimensions = dim + + shapes = { + { + name = "part", + material = "steel", + geometry = { + format = "stl", + path = "part.stl", + units = "cm", + operators = { + { + translate = function() + if dim == 2 then + return {r, z} + end + return {x, y, z} + end + } + } + } + } + } + )", + InputFormat::Lua); + + auto composite = std::dynamic_pointer_cast( + shapeSet.getShapes()[0].getGeometry().getGeometryOperator()); + ASSERT_TRUE(composite); + auto translation = dynamic_cast(composite->getOperators()[0].get()); + ASSERT_NE(translation, nullptr); + EXPECT_THAT(translation->getOffset(), AlmostEqVector(Vector3D {4, 8, 0})); +} + +TEST(IOTest, readShapeSet_luaCallbackErrorIncludesContext) +{ + try + { + readShapeSetFromString(R"( + dimensions = 2 + shapes = { + { + name = "bad_shape", + material = "steel", + geometry = { + format = "stl", + path = "bad.stl", + units = "cm", + operators = { + { + translate = function() + error("callback boom") + end + } + } + } + } + } + )", + InputFormat::Lua); + FAIL() << "Should have thrown"; + } + catch(const KleeError& err) + { + EXPECT_THAT(err.what(), HasSubstr("translate")); + EXPECT_THAT(err.what(), HasSubstr("bad_shape")); + EXPECT_THAT(err.what(), HasSubstr("operator")); + EXPECT_THAT(err.what(), HasSubstr("callback boom")); + } +} + +TEST(IOTest, readShapeSet_luaCallbackWrongVectorDimensionIncludesContext) +{ + try + { + readShapeSetFromString(R"( + dimensions = 2 + shapes = { + { + name = "wrong_dim", + material = "steel", + geometry = { + format = "stl", + path = "wrong_dim.stl", + units = "cm", + operators = { + { translate = function() return {1, 2, 3} end } + } + } + } + } + )", + InputFormat::Lua); + FAIL() << "Should have thrown"; + } + catch(const KleeError& err) + { + EXPECT_THAT(err.what(), HasSubstr("translate")); + EXPECT_THAT(err.what(), HasSubstr("wrong_dim")); + EXPECT_THAT(err.what(), HasSubstr("Wrong size")); + } +} + TEST(IOTest, readShapeSet_luaUnexpectedGlobalDiagnostic) { try From 2b84e46377fb915fbfa87f1d347933e16f254ec7 Mon Sep 17 00:00:00 2001 From: Kenneth Weiss Date: Tue, 30 Jun 2026 15:42:23 -0700 Subject: [PATCH 02/52] Klee: Adds ability to load variables into klee lua --- .../klee/docs/sphinx/specifying_shapes.rst | 38 +++++ src/axom/klee/io/IO.cpp | 132 ++++++++++++++++-- src/axom/klee/io/IO.hpp | 31 ++++ src/axom/klee/tests/klee_io.cpp | 90 ++++++++++++ 4 files changed, 279 insertions(+), 12 deletions(-) diff --git a/src/axom/klee/docs/sphinx/specifying_shapes.rst b/src/axom/klee/docs/sphinx/specifying_shapes.rst index 212460e458..e0a4e4b9ce 100644 --- a/src/axom/klee/docs/sphinx/specifying_shapes.rst +++ b/src/axom/klee/docs/sphinx/specifying_shapes.rst @@ -123,6 +123,44 @@ ordinary table values can be generated programmatically: } } +Caller-provided input variables can also be injected into a Lua deck before it +is evaluated. This is useful when an application wants one deck to select between +2D and 3D geometry, dimensions, or transforms at run time: + +.. code-block:: c++ + + axom::klee::InputVariables vars { + {"dimensions", axom::klee::InputVariableValue {2}}, + {"shape_suffix", axom::klee::InputVariableValue {std::string {"2d"}}} + }; + auto shapeSet = axom::klee::readShapeSet("shape.lua", vars); + +.. code-block:: lua + + local function shape_path() + return "part_" .. shape_suffix .. ".stl" + end + + shapes = { + { + name = "part", + material = "steel", + geometry = { + format = "stl", + path = shape_path(), + units = "cm", + operators = { + { translate = (dimensions == 2) and {1.0, 2.0} or {1.0, 2.0, 3.0} } + } + } + } + } + +Input variables are Lua-only and may be booleans, integers, doubles, or strings. +Their names must be Lua identifiers. They are globals by construction and are +allowed by Klee's unexpected-global check; other helper values in the deck should +still be declared :code:`local`. + Use :code:`local` helper functions and constants for intermediate values so the global namespace contains only the Klee schema fields that Inlet should read. For Lua input, a one-value scale is written as a one-entry table, for example diff --git a/src/axom/klee/io/IO.cpp b/src/axom/klee/io/IO.cpp index 318ecad4f9..b180c1e7ae 100644 --- a/src/axom/klee/io/IO.cpp +++ b/src/axom/klee/io/IO.cpp @@ -17,14 +17,18 @@ #include "axom/inlet.hpp" #ifdef AXOM_USE_LUA #include "axom/inlet/LuaReader.hpp" + #include "axom/sol.hpp" #endif +#include +#include #include #include #include #include #include #include +#include namespace axom { @@ -32,6 +36,18 @@ namespace klee { namespace { +#ifdef AXOM_USE_LUA +class KleeLuaReader : public inlet::LuaReader +{ +public: + void setInputVariable(const std::string &name, const InputVariableValue &value) + { + auto lua = solState(); + std::visit([&](const auto &typedValue) { (*lua)[name] = typedValue; }, value); + } +}; +#endif + // Because we can't have context-aware validation when extracting the // data from Inlet, we need a set of structs that parallels the real // classes. These are used to do some basic validation, and then we convert @@ -347,22 +363,81 @@ InputFormat inferInputFormat(const std::string& filePath) extension)}); } +bool isLuaIdentifier(const std::string &name) +{ + if(name.empty()) + { + return false; + } + + auto isNameStart = [](unsigned char ch) { return std::isalpha(ch) || ch == '_'; }; + auto isNameChar = [](unsigned char ch) { return std::isalnum(ch) || ch == '_'; }; + + if(!isNameStart(static_cast(name.front()))) + { + return false; + } + return std::all_of(name.begin() + 1, name.end(), [&](char ch) { + return isNameChar(static_cast(ch)); + }); +} + +std::unordered_set inputVariableNames(const InputVariables &variables) +{ + std::unordered_set names; + for(const auto &entry : variables) + { + names.insert(entry.first); + } + return names; +} + +void validateInputVariables(const InputVariables &variables) +{ + for(const auto &entry : variables) + { + if(!isLuaIdentifier(entry.first)) + { + throw KleeError({Path {entry.first.empty() ? "" : entry.first}, + axom::fmt::format("Invalid Klee Lua input variable name '{}'. Input " + "variable names must be Lua identifiers.", + entry.first)}); + } + } +} + /** * Create an Inlet reader for a Klee input format. * * \param format the input file format to read + * \param variables primitive values to inject before Lua input evaluation * \return a reader for \a format - * \throws KleeError if \a format is unsupported or Lua support was not enabled + * \throws KleeError if \a format is unsupported, Lua support was not enabled, + * or the input variables are invalid for the selected format */ -std::unique_ptr createReader(InputFormat format) +std::unique_ptr createReader(InputFormat format, const InputVariables &variables) { + if(format != InputFormat::Lua && !variables.empty()) + { + throw KleeError( + {Path {""}, "Klee input variables are only supported for Lua input decks."}); + } + validateInputVariables(variables); + switch(format) { case InputFormat::YAML: return std::make_unique(); case InputFormat::Lua: #ifdef AXOM_USE_LUA - return std::make_unique(); + { + auto reader = std::make_unique(); + for(const auto &entry : variables) + { + reader->setInputVariable(entry.first, entry.second); + } + return reader; + } #else throw KleeError( {Path {""}, @@ -423,13 +498,18 @@ void parseOrThrow(Parse&& parse, } } -void appendUnexpectedGlobalErrors(const inlet::Inlet& doc, - std::vector& errors) +void appendUnexpectedGlobalErrors(const inlet::Inlet &doc, + std::vector &errors, + const std::unordered_set &allowedGlobals) { for(const auto& name : doc.unexpectedNames()) { if(name.find('/') == std::string::npos) { + if(allowedGlobals.find(name) != allowedGlobals.end()) + { + continue; + } errors.push_back({Path {name}, axom::fmt::format("Unexpected global variable '{}' in Lua input file. " "Use 'local' for helper values and functions.", @@ -449,7 +529,8 @@ void appendUnexpectedGlobalErrors(const inlet::Inlet& doc, */ ShapeSet readShapeSetFromReader(std::unique_ptr reader, bool rejectUnexpectedGlobals, - bool enableLuaCallbacks) + bool enableLuaCallbacks, + const std::unordered_set &allowedGlobals) { sidre::DataStore dataStore; inlet::Inlet doc(std::move(reader), dataStore.getRoot()); @@ -458,7 +539,7 @@ ShapeSet readShapeSetFromReader(std::unique_ptr reader, bool verified = doc.verify(&errors); if(rejectUnexpectedGlobals) { - appendUnexpectedGlobalErrors(doc, errors); + appendUnexpectedGlobalErrors(doc, errors, allowedGlobals); verified = verified && errors.empty(); } if(!verified) @@ -484,17 +565,25 @@ ShapeSet readShapeSetFromReader(std::unique_ptr reader, ShapeSet readShapeSet(std::istream& stream) { return readShapeSet(stream, InputFormat::YAML); } ShapeSet readShapeSet(std::istream& stream, InputFormat format) +{ + return readShapeSet(stream, format, InputVariables {}); +} + +ShapeSet readShapeSet(std::istream &stream, + InputFormat format, + const InputVariables &variables) { std::string contents {std::istreambuf_iterator(stream), {}}; - auto reader = createReader(format); + auto reader = createReader(format, variables); parseOrThrow([&]() { return reader->parseString(contents); }, format, Path {""}, "from stream"); return readShapeSetFromReader(std::move(reader), format == InputFormat::Lua, - format == InputFormat::Lua); + format == InputFormat::Lua, + inputVariableNames(variables)); } ShapeSet readShapeSet(const std::string& filePath) @@ -504,13 +593,32 @@ ShapeSet readShapeSet(const std::string& filePath) ShapeSet readShapeSet(const std::string& filePath, InputFormat format) { - auto reader = createReader(format); + const InputVariables variables; + auto reader = createReader(format, variables); + parseOrThrow([&]() { return reader->parseFile(filePath); }, + format, + Path {filePath}, + axom::fmt::format("from file '{}'", filePath)); + auto shapeSet = readShapeSetFromReader(std::move(reader), + format == InputFormat::Lua, + format == InputFormat::Lua, + inputVariableNames(variables)); + shapeSet.setPath(filePath); + return shapeSet; +} + +ShapeSet readShapeSet(const std::string &filePath, const InputVariables &variables) +{ + const auto format = inferInputFormat(filePath); + auto reader = createReader(format, variables); parseOrThrow([&]() { return reader->parseFile(filePath); }, format, Path {filePath}, axom::fmt::format("from file '{}'", filePath)); - auto shapeSet = - readShapeSetFromReader(std::move(reader), format == InputFormat::Lua, format == InputFormat::Lua); + auto shapeSet = readShapeSetFromReader(std::move(reader), + format == InputFormat::Lua, + format == InputFormat::Lua, + inputVariableNames(variables)); shapeSet.setPath(filePath); return shapeSet; } diff --git a/src/axom/klee/io/IO.hpp b/src/axom/klee/io/IO.hpp index 2cd6d5e373..bd65523cd8 100644 --- a/src/axom/klee/io/IO.hpp +++ b/src/axom/klee/io/IO.hpp @@ -10,6 +10,8 @@ #include #include +#include +#include namespace axom { @@ -22,6 +24,12 @@ enum class InputFormat Lua }; +/// Primitive value types that may be injected into a Lua input deck. +using InputVariableValue = std::variant; + +/// Variables to make available to a Lua input deck before it is evaluated. +using InputVariables = std::unordered_map; + /** * Read a ShapeSet from an input stream. * @@ -41,6 +49,17 @@ ShapeSet readShapeSet(std::istream& stream); */ ShapeSet readShapeSet(std::istream& stream, InputFormat format); +/** + * Read a ShapeSet from an input stream with caller-provided input variables. + * + * \param stream the stream from which to read the ShapeSet + * \param format the input deck format to use + * \param variables primitive values to inject into Lua before deck evaluation + * \note Input variables are supported only for Lua input decks. + * \throws runtime_error if the input is invalid + */ +ShapeSet readShapeSet(std::istream &stream, InputFormat format, const InputVariables &variables); + /** * Read a ShapeSet from a specified file * @@ -64,5 +83,17 @@ ShapeSet readShapeSet(const std::string& filePath); */ ShapeSet readShapeSet(const std::string& filePath, InputFormat format); +/** + * Read a ShapeSet from a specified file with caller-provided input variables. + * + * \param filePath the file from which to read the ShapeSet + * \param variables primitive values to inject into Lua before deck evaluation + * \note The input format is inferred from the file extension. Input variables + * are supported only for Lua input decks. + * \return the ShapeSet read from the file + * \throws runtime_error if the input is invalid + */ +ShapeSet readShapeSet(const std::string &filePath, const InputVariables &variables); + } // namespace klee } // namespace axom diff --git a/src/axom/klee/tests/klee_io.cpp b/src/axom/klee/tests/klee_io.cpp index e355364900..e4a8435d24 100644 --- a/src/axom/klee/tests/klee_io.cpp +++ b/src/axom/klee/tests/klee_io.cpp @@ -27,6 +27,7 @@ namespace primal = axom::primal; using klee::CompositeOperator; using klee::Dimensions; using klee::InputFormat; +using klee::InputVariables; using klee::KleeError; using klee::LengthUnit; using klee::Rotation; @@ -56,6 +57,14 @@ ShapeSet readShapeSetFromString(const std::string& input, InputFormat format) std::istringstream istream(input); return klee::readShapeSet(istream, format); } + +ShapeSet readShapeSetFromString(const std::string& input, + InputFormat format, + const InputVariables& variables) +{ + std::istringstream istream(input); + return klee::readShapeSet(istream, format, variables); +} } // end namespace TEST(IOTest, readShapeSet_noShapes) @@ -423,6 +432,25 @@ TEST(IOTest, readShapeSet_streamDefaultsToYaml) } } +TEST(IOTest, readShapeSet_yamlRejectsInputVariables) +{ + try + { + readShapeSetFromString(R"( + dimensions: 2 + shapes: [] + )", + InputFormat::YAML, + {{"dimensions", klee::InputVariableValue {2}}}); + FAIL() << "Should have thrown"; + } + catch(const KleeError& err) + { + EXPECT_THAT(err.what(), HasSubstr("input variables")); + EXPECT_THAT(err.what(), HasSubstr("Lua")); + } +} + #ifndef AXOM_USE_LUA TEST(IOTest, readShapeSet_luaUnavailableDiagnostic) { @@ -499,6 +527,68 @@ TEST(IOTest, readShapeSet_luaStreamMinimalShapeList) EXPECT_EQ(Dimensions::Two, shapeSet.getDimensions()); } +TEST(IOTest, readShapeSet_luaInputVariablesProvideInitialDimensionAndOperator) +{ + InputVariables variables { + {"dimensions", klee::InputVariableValue {2}}, + {"shape_suffix", klee::InputVariableValue {std::string {"2d"}}}, + {"lift", klee::InputVariableValue {3.0}}, + }; + + auto shapeSet = readShapeSetFromString(R"( + local function shape_path() + return "part_" .. shape_suffix .. ".stl" + end + + shapes = { + { + name = "controlled", + material = "steel", + geometry = { + format = "stl", + path = shape_path(), + units = "cm", + operators = { + { translate = (dimensions == 2) and {1.0, lift} or {1.0, 0.0, lift} } + } + } + } + } + )", + InputFormat::Lua, + variables); + + ASSERT_EQ(Dimensions::Two, shapeSet.getDimensions()); + ASSERT_EQ(1u, shapeSet.getShapes().size()); + const auto& geometry = shapeSet.getShapes()[0].getGeometry(); + EXPECT_EQ("part_2d.stl", geometry.getPath()); + auto composite = std::dynamic_pointer_cast(geometry.getGeometryOperator()); + ASSERT_TRUE(composite); + ASSERT_EQ(1u, composite->getOperators().size()); + auto translation = std::dynamic_pointer_cast(composite->getOperators()[0]); + ASSERT_TRUE(translation); + EXPECT_THAT(translation->getOffset(), AlmostEqVector(Vector3D {1.0, 3.0, 0.0})); +} + +TEST(IOTest, readShapeSet_luaInputVariableRejectsInvalidName) +{ + try + { + readShapeSetFromString(R"( + dimensions = 2 + shapes = {} + )", + InputFormat::Lua, + {{"shape-dim", klee::InputVariableValue {2}}}); + FAIL() << "Should have thrown"; + } + catch(const KleeError& err) + { + EXPECT_THAT(err.what(), HasSubstr("Invalid Klee Lua input variable name")); + EXPECT_THAT(err.what(), HasSubstr("Lua identifiers")); + } +} + TEST(IOTest, readShapeSet_luaFileExtension) { std::string fileName = "testFile.lua"; From a76830fc4535361f4c1633de0b62570e97911d6b Mon Sep 17 00:00:00 2001 From: Kenneth Weiss Date: Tue, 30 Jun 2026 20:28:49 -0700 Subject: [PATCH 03/52] Klee: Adds LuaBindingChunk as a way of passing state to the lua Klee deck Users can pass in a lua table containing variables and functions. --- .../klee/docs/sphinx/specifying_shapes.rst | 52 ++++ src/axom/klee/io/IO.cpp | 268 +++++++++++++++-- src/axom/klee/io/IO.hpp | 64 ++++ src/axom/klee/tests/klee_io.cpp | 277 ++++++++++++++++++ src/examples/CMakeLists.txt | 6 + src/examples/shaping_tutorial/CMakeLists.txt | 6 + .../shaping_tutorial/lesson_03/README.md | 11 + .../lesson_03/ice_cream_bindings.lua | 51 ++++ .../lesson_03/ice_cream_runtime_bindings.lua | 32 ++ .../klee_operators_and_validation.cpp | 21 +- 10 files changed, 764 insertions(+), 24 deletions(-) create mode 100644 src/examples/shaping_tutorial/lesson_03/ice_cream_bindings.lua create mode 100644 src/examples/shaping_tutorial/lesson_03/ice_cream_runtime_bindings.lua diff --git a/src/axom/klee/docs/sphinx/specifying_shapes.rst b/src/axom/klee/docs/sphinx/specifying_shapes.rst index e0a4e4b9ce..8f8b77a948 100644 --- a/src/axom/klee/docs/sphinx/specifying_shapes.rst +++ b/src/axom/klee/docs/sphinx/specifying_shapes.rst @@ -161,6 +161,58 @@ Their names must be Lua identifiers. They are globals by construction and are allowed by Klee's unexpected-global check; other helper values in the deck should still be declared :code:`local`. +Applications that need richer runtime customization can also provide a Lua +bindings chunk. Klee evaluates the chunk before parsing the deck, expects it to +return a table of exported bindings, and then makes those exported names +available as globals while still rejecting unrelated unexpected globals in the +deck. This allows a host code to pass user-supplied helper functions and local +closures at run time without recompiling the C++ application: + +.. code-block:: c++ + + axom::klee::LuaBindingsChunk bindings { + R"( + local dim = 2 + local lift = 3.0 + + local function offset(y) + return function() + return {0.0, y} + end + end + + return { + dimensions = dim, + lift = lift, + offset = offset + } + )", + "runtime_bindings" + }; + auto shapeSet = axom::klee::readShapeSet("shape.lua", bindings); + +.. code-block:: lua + + shapes = { + { + name = "part", + material = "steel", + geometry = { + format = "stl", + path = "part.stl", + units = "cm", + operators = { + { translate = offset(lift) } + } + } + } + } + +Bindings chunks must return a table whose exported keys are Lua identifiers. +Exported values may be booleans, numbers, strings, tables, or functions. Like +input variables, exported bindings are ordinary Lua globals; deck code can +reassign exported names and can mutate exported tables. + Use :code:`local` helper functions and constants for intermediate values so the global namespace contains only the Klee schema fields that Inlet should read. For Lua input, a one-value scale is written as a one-entry table, for example diff --git a/src/axom/klee/io/IO.cpp b/src/axom/klee/io/IO.cpp index b180c1e7ae..c34691c48b 100644 --- a/src/axom/klee/io/IO.cpp +++ b/src/axom/klee/io/IO.cpp @@ -36,15 +36,172 @@ namespace klee { namespace { +bool isLuaIdentifier(const std::string &name); + #ifdef AXOM_USE_LUA class KleeLuaReader : public inlet::LuaReader { public: + std::unordered_set topLevelGlobalNames() const + { + std::unordered_set names; + auto lua = const_cast(this)->solState(); + for(const auto &entry : lua->globals()) + { + if(entry.first.get_type() == axom::sol::type::string) + { + names.insert(entry.first.as()); + } + } + return names; + } + void setInputVariable(const std::string &name, const InputVariableValue &value) { auto lua = solState(); std::visit([&](const auto &typedValue) { (*lua)[name] = typedValue; }, value); } + + std::unordered_set applyBindingsChunk( + const LuaBindingsChunk &bindings, + const std::unordered_set &reservedNames, + const std::unordered_set &existingExternalNames) + { + auto lua = solState(); + auto chunkPath = Path {bindings.label.empty() ? "" : bindings.label}; + if(bindings.source.empty()) + { + throw KleeError({chunkPath, "Klee Lua bindings chunk is empty."}); + } + + try + { + auto result = lua->script(bindings.source); + if(!result.valid()) + { + axom::sol::error err = result; + throw KleeError({chunkPath, + axom::fmt::format("Failed to evaluate Klee Lua bindings chunk '{}': {}", + static_cast(chunkPath), + err.what())}); + } + + axom::sol::optional tableOption = result; + if(!tableOption) + { + throw KleeError({chunkPath, + axom::fmt::format("Klee Lua bindings chunk '{}' must return a table of " + "exported bindings.", + static_cast(chunkPath))}); + } + + std::unordered_set exportedNames; + auto exportPath = [&](const std::string &name) { + return Path::join({chunkPath, Path {name}}); + }; + auto typeName = [](axom::sol::type type) { + switch(type) + { + case axom::sol::type::boolean: + return "boolean"; + case axom::sol::type::number: + return "number"; + case axom::sol::type::string: + return "string"; + case axom::sol::type::table: + return "table"; + case axom::sol::type::function: + return "function"; + case axom::sol::type::nil: + return "nil"; + default: + return "unsupported"; + } + }; + + for(const auto &entry : tableOption.value()) + { + if(entry.first.get_type() != axom::sol::type::string) + { + throw KleeError({chunkPath, + axom::fmt::format("Klee Lua bindings chunk '{}' must return a table " + "with string keys.", + static_cast(chunkPath))}); + } + + const std::string name = entry.first.as(); + if(!isLuaIdentifier(name)) + { + throw KleeError( + {exportPath(name), + axom::fmt::format("Invalid Klee Lua binding name '{}'. Binding names must be Lua " + "identifiers.", + name)}); + } + if(reservedNames.find(name) != reservedNames.end()) + { + throw KleeError( + {exportPath(name), + axom::fmt::format("Klee Lua binding name '{}' conflicts with an existing Lua global.", + name)}); + } + if(existingExternalNames.find(name) != existingExternalNames.end()) + { + throw KleeError({exportPath(name), + axom::fmt::format( + "Klee Lua binding name '{}' duplicates another external Lua binding.", + name)}); + } + if(!exportedNames.insert(name).second) + { + throw KleeError( + {exportPath(name), + axom::fmt::format("Klee Lua bindings chunk '{}' exports '{}' more than once.", + static_cast(chunkPath), + name)}); + } + + switch(entry.second.get_type()) + { + case axom::sol::type::boolean: + (*lua)[name] = entry.second.as(); + break; + case axom::sol::type::number: + (*lua)[name] = entry.second.as(); + break; + case axom::sol::type::string: + (*lua)[name] = entry.second.as(); + break; + case axom::sol::type::function: + (*lua)[name] = entry.second.as(); + break; + case axom::sol::type::table: + (*lua)[name] = entry.second.as(); + break; + default: + throw KleeError({exportPath(name), + axom::fmt::format("Klee Lua binding '{}' has unsupported value type " + "'{}'. Supported exported binding value types are " + "booleans, numbers, strings, tables, and functions.", + name, + typeName(entry.second.get_type()))}); + } + } + + return exportedNames; + } + catch(const KleeError &) + { + throw; + } + catch(const std::exception &ex) + { + throw KleeError({chunkPath, + axom::fmt::format("Failed to evaluate Klee Lua bindings chunk '{}': {}", + static_cast(chunkPath), + ex.what())}); + } + } }; #endif @@ -382,16 +539,6 @@ bool isLuaIdentifier(const std::string &name) }); } -std::unordered_set inputVariableNames(const InputVariables &variables) -{ - std::unordered_set names; - for(const auto &entry : variables) - { - names.insert(entry.first); - } - return names; -} - void validateInputVariables(const InputVariables &variables) { for(const auto &entry : variables) @@ -411,18 +558,25 @@ void validateInputVariables(const InputVariables &variables) * * \param format the input file format to read * \param variables primitive values to inject before Lua input evaluation + * \param bindings optional Lua chunk to evaluate before input evaluation + * \param allowedGlobals receives external names permitted in the input * \return a reader for \a format * \throws KleeError if \a format is unsupported, Lua support was not enabled, - * or the input variables are invalid for the selected format + * or the external Lua bindings are invalid for the selected format */ -std::unique_ptr createReader(InputFormat format, const InputVariables &variables) +std::unique_ptr createReader(InputFormat format, + const InputVariables &variables, + const LuaBindingsChunk *bindings, + std::unordered_set &allowedGlobals) { - if(format != InputFormat::Lua && !variables.empty()) + allowedGlobals.clear(); + if(format != InputFormat::Lua && (!variables.empty() || bindings != nullptr)) { - throw KleeError( - {Path {""}, "Klee input variables are only supported for Lua input decks."}); + throw KleeError({Path {""}, + bindings != nullptr + ? "Klee Lua bindings are only supported for Lua input decks." + : "Klee input variables are only supported for Lua input decks."}); } - validateInputVariables(variables); switch(format) { @@ -432,9 +586,25 @@ std::unique_ptr createReader(InputFormat format, const InputVaria #ifdef AXOM_USE_LUA { auto reader = std::make_unique(); + const auto reservedGlobals = reader->topLevelGlobalNames(); + validateInputVariables(variables); for(const auto &entry : variables) { + if(reservedGlobals.find(entry.first) != reservedGlobals.end()) + { + throw KleeError( + {Path {entry.first}, + axom::fmt::format("Klee Lua input variable name '{}' conflicts with an existing Lua " + "global.", + entry.first)}); + } reader->setInputVariable(entry.first, entry.second); + allowedGlobals.insert(entry.first); + } + if(bindings != nullptr) + { + auto exportedNames = reader->applyBindingsChunk(*bindings, reservedGlobals, allowedGlobals); + allowedGlobals.insert(exportedNames.begin(), exportedNames.end()); } return reader; } @@ -524,6 +694,7 @@ void appendUnexpectedGlobalErrors(const inlet::Inlet &doc, * \param reader the parsed Inlet reader * \param rejectUnexpectedGlobals true if unexpected top-level Lua globals should be rejected * \param enableLuaCallbacks true if Lua callbacks should be enabled + * \param allowedGlobals external Lua globals permitted in the input * \return the parsed and verified ShapeSet * \throws KleeError if schema verification or semantic validation fails */ @@ -575,7 +746,34 @@ ShapeSet readShapeSet(std::istream &stream, { std::string contents {std::istreambuf_iterator(stream), {}}; - auto reader = createReader(format, variables); + std::unordered_set allowedGlobals; + auto reader = createReader(format, variables, nullptr, allowedGlobals); + parseOrThrow([&]() { return reader->parseString(contents); }, + format, + Path {""}, + "from stream"); + return readShapeSetFromReader(std::move(reader), + format == InputFormat::Lua, + format == InputFormat::Lua, + allowedGlobals); +} + +ShapeSet readShapeSet(std::istream &stream, + InputFormat format, + const LuaBindingsChunk &bindings) +{ + return readShapeSet(stream, format, InputVariables {}, bindings); +} + +ShapeSet readShapeSet(std::istream &stream, + InputFormat format, + const InputVariables &variables, + const LuaBindingsChunk &bindings) +{ + std::string contents {std::istreambuf_iterator(stream), {}}; + + std::unordered_set allowedGlobals; + auto reader = createReader(format, variables, &bindings, allowedGlobals); parseOrThrow([&]() { return reader->parseString(contents); }, format, Path {""}, @@ -583,7 +781,7 @@ ShapeSet readShapeSet(std::istream &stream, return readShapeSetFromReader(std::move(reader), format == InputFormat::Lua, format == InputFormat::Lua, - inputVariableNames(variables)); + allowedGlobals); } ShapeSet readShapeSet(const std::string& filePath) @@ -594,7 +792,8 @@ ShapeSet readShapeSet(const std::string& filePath) ShapeSet readShapeSet(const std::string& filePath, InputFormat format) { const InputVariables variables; - auto reader = createReader(format, variables); + std::unordered_set allowedGlobals; + auto reader = createReader(format, variables, nullptr, allowedGlobals); parseOrThrow([&]() { return reader->parseFile(filePath); }, format, Path {filePath}, @@ -602,7 +801,7 @@ ShapeSet readShapeSet(const std::string& filePath, InputFormat format) auto shapeSet = readShapeSetFromReader(std::move(reader), format == InputFormat::Lua, format == InputFormat::Lua, - inputVariableNames(variables)); + allowedGlobals); shapeSet.setPath(filePath); return shapeSet; } @@ -610,7 +809,32 @@ ShapeSet readShapeSet(const std::string& filePath, InputFormat format) ShapeSet readShapeSet(const std::string &filePath, const InputVariables &variables) { const auto format = inferInputFormat(filePath); - auto reader = createReader(format, variables); + std::unordered_set allowedGlobals; + auto reader = createReader(format, variables, nullptr, allowedGlobals); + parseOrThrow([&]() { return reader->parseFile(filePath); }, + format, + Path {filePath}, + axom::fmt::format("from file '{}'", filePath)); + auto shapeSet = readShapeSetFromReader(std::move(reader), + format == InputFormat::Lua, + format == InputFormat::Lua, + allowedGlobals); + shapeSet.setPath(filePath); + return shapeSet; +} + +ShapeSet readShapeSet(const std::string &filePath, const LuaBindingsChunk &bindings) +{ + return readShapeSet(filePath, InputVariables {}, bindings); +} + +ShapeSet readShapeSet(const std::string &filePath, + const InputVariables &variables, + const LuaBindingsChunk &bindings) +{ + const auto format = inferInputFormat(filePath); + std::unordered_set allowedGlobals; + auto reader = createReader(format, variables, &bindings, allowedGlobals); parseOrThrow([&]() { return reader->parseFile(filePath); }, format, Path {filePath}, @@ -618,7 +842,7 @@ ShapeSet readShapeSet(const std::string &filePath, const InputVariables &variabl auto shapeSet = readShapeSetFromReader(std::move(reader), format == InputFormat::Lua, format == InputFormat::Lua, - inputVariableNames(variables)); + allowedGlobals); shapeSet.setPath(filePath); return shapeSet; } diff --git a/src/axom/klee/io/IO.hpp b/src/axom/klee/io/IO.hpp index bd65523cd8..09a86d3575 100644 --- a/src/axom/klee/io/IO.hpp +++ b/src/axom/klee/io/IO.hpp @@ -24,6 +24,13 @@ enum class InputFormat Lua }; +/// Runtime Lua chunk evaluated before deck parsing; it must return a table of exported bindings. +struct LuaBindingsChunk +{ + std::string source; + std::string label {""}; +}; + /// Primitive value types that may be injected into a Lua input deck. using InputVariableValue = std::variant; @@ -60,6 +67,34 @@ ShapeSet readShapeSet(std::istream& stream, InputFormat format); */ ShapeSet readShapeSet(std::istream &stream, InputFormat format, const InputVariables &variables); +/** + * Read a ShapeSet from an input stream with caller-provided Lua bindings. + * + * \param stream the stream from which to read the ShapeSet + * \param format the input deck format to use + * \param bindings Lua chunk evaluated before deck parsing; must return a table + * of exported bindings + * \note Lua bindings are supported only for Lua input decks. + * \throws runtime_error if the input is invalid + */ +ShapeSet readShapeSet(std::istream &stream, InputFormat format, const LuaBindingsChunk &bindings); + +/** + * Read a ShapeSet from an input stream with caller-provided input variables and Lua bindings. + * + * \param stream the stream from which to read the ShapeSet + * \param format the input deck format to use + * \param variables primitive values to inject into Lua before deck evaluation + * \param bindings Lua chunk evaluated before deck parsing; must return a table + * of exported bindings + * \note Input variables and Lua bindings are supported only for Lua input decks. + * \throws runtime_error if the input is invalid + */ +ShapeSet readShapeSet(std::istream &stream, + InputFormat format, + const InputVariables &variables, + const LuaBindingsChunk &bindings); + /** * Read a ShapeSet from a specified file * @@ -95,5 +130,34 @@ ShapeSet readShapeSet(const std::string& filePath, InputFormat format); */ ShapeSet readShapeSet(const std::string &filePath, const InputVariables &variables); +/** + * Read a ShapeSet from a specified file with caller-provided Lua bindings. + * + * \param filePath the file from which to read the ShapeSet + * \param bindings Lua chunk evaluated before deck parsing; must return a table + * of exported bindings + * \note The input format is inferred from the file extension. Lua bindings are + * supported only for Lua input decks. + * \return the ShapeSet read from the file + * \throws runtime_error if the input is invalid + */ +ShapeSet readShapeSet(const std::string &filePath, const LuaBindingsChunk &bindings); + +/** + * Read a ShapeSet from a specified file with caller-provided input variables and Lua bindings. + * + * \param filePath the file from which to read the ShapeSet + * \param variables primitive values to inject into Lua before deck evaluation + * \param bindings Lua chunk evaluated before deck parsing; must return a table + * of exported bindings + * \note The input format is inferred from the file extension. Input variables + * and Lua bindings are supported only for Lua input decks. + * \return the ShapeSet read from the file + * \throws runtime_error if the input is invalid + */ +ShapeSet readShapeSet(const std::string &filePath, + const InputVariables &variables, + const LuaBindingsChunk &bindings); + } // namespace klee } // namespace axom diff --git a/src/axom/klee/tests/klee_io.cpp b/src/axom/klee/tests/klee_io.cpp index e4a8435d24..c1ebe3134f 100644 --- a/src/axom/klee/tests/klee_io.cpp +++ b/src/axom/klee/tests/klee_io.cpp @@ -30,6 +30,7 @@ using klee::InputFormat; using klee::InputVariables; using klee::KleeError; using klee::LengthUnit; +using klee::LuaBindingsChunk; using klee::Rotation; using klee::Scale; using klee::ShapeSet; @@ -65,6 +66,23 @@ ShapeSet readShapeSetFromString(const std::string& input, std::istringstream istream(input); return klee::readShapeSet(istream, format, variables); } + +ShapeSet readShapeSetFromString(const std::string& input, + InputFormat format, + const LuaBindingsChunk& bindings) +{ + std::istringstream istream(input); + return klee::readShapeSet(istream, format, bindings); +} + +ShapeSet readShapeSetFromString(const std::string& input, + InputFormat format, + const InputVariables& variables, + const LuaBindingsChunk& bindings) +{ + std::istringstream istream(input); + return klee::readShapeSet(istream, format, variables, bindings); +} } // end namespace TEST(IOTest, readShapeSet_noShapes) @@ -451,6 +469,30 @@ TEST(IOTest, readShapeSet_yamlRejectsInputVariables) } } +TEST(IOTest, readShapeSet_yamlRejectsLuaBindings) +{ + try + { + readShapeSetFromString(R"( + dimensions: 2 + shapes: [] + )", + InputFormat::YAML, + LuaBindingsChunk {R"( + return { + dimensions = 2 + } + )", + "runtime_bindings"}); + FAIL() << "Should have thrown"; + } + catch(const KleeError& err) + { + EXPECT_THAT(err.what(), HasSubstr("Lua bindings")); + EXPECT_THAT(err.what(), HasSubstr("Lua input decks")); + } +} + #ifndef AXOM_USE_LUA TEST(IOTest, readShapeSet_luaUnavailableDiagnostic) { @@ -570,6 +612,152 @@ TEST(IOTest, readShapeSet_luaInputVariablesProvideInitialDimensionAndOperator) EXPECT_THAT(translation->getOffset(), AlmostEqVector(Vector3D {1.0, 3.0, 0.0})); } +TEST(IOTest, readShapeSet_luaBindingsChunkProvidesInitialDimensionAndOperator) +{ + LuaBindingsChunk bindings {R"( + local dim = 2 + local lift = 3.0 + + return { + dimensions = dim, + shape_suffix = "2d", + lift = lift + } + )", + "runtime_bindings"}; + + auto shapeSet = readShapeSetFromString(R"( + local function shape_path() + return "part_" .. shape_suffix .. ".stl" + end + + shapes = { + { + name = "controlled", + material = "steel", + geometry = { + format = "stl", + path = shape_path(), + units = "cm", + operators = { + { translate = (dimensions == 2) and {1.0, lift} or {1.0, 0.0, lift} } + } + } + } + } + )", + InputFormat::Lua, + bindings); + + ASSERT_EQ(Dimensions::Two, shapeSet.getDimensions()); + ASSERT_EQ(1u, shapeSet.getShapes().size()); + const auto& geometry = shapeSet.getShapes()[0].getGeometry(); + EXPECT_EQ("part_2d.stl", geometry.getPath()); + auto composite = std::dynamic_pointer_cast(geometry.getGeometryOperator()); + ASSERT_TRUE(composite); + ASSERT_EQ(1u, composite->getOperators().size()); + auto translation = std::dynamic_pointer_cast(composite->getOperators()[0]); + ASSERT_TRUE(translation); + EXPECT_THAT(translation->getOffset(), AlmostEqVector(Vector3D {1.0, 3.0, 0.0})); +} + +TEST(IOTest, readShapeSet_luaBindingsChunkProvidesInitialMutableGlobals) +{ + LuaBindingsChunk bindings {R"( + return { + dimensions = 2, + settings = { + lift = 3.0 + } + } + )", + "runtime_bindings"}; + + auto shapeSet = readShapeSetFromString(R"( + dimensions = 3 + settings.lift = 7.0 + + shapes = { + { + name = "overridden", + material = "steel", + geometry = { + format = "stl", + path = "part.stl", + units = "cm", + operators = { + { translate = {1.0, 2.0, settings.lift} } + } + } + } + } + )", + InputFormat::Lua, + bindings); + + ASSERT_EQ(Dimensions::Three, shapeSet.getDimensions()); + ASSERT_EQ(1u, shapeSet.getShapes().size()); + const auto& geometry = shapeSet.getShapes()[0].getGeometry(); + auto composite = std::dynamic_pointer_cast(geometry.getGeometryOperator()); + ASSERT_TRUE(composite); + ASSERT_EQ(1u, composite->getOperators().size()); + auto translation = std::dynamic_pointer_cast(composite->getOperators()[0]); + ASSERT_TRUE(translation); + EXPECT_THAT(translation->getOffset(), AlmostEqVector(Vector3D {1.0, 2.0, 7.0})); +} + +TEST(IOTest, readShapeSet_luaBindingsChunkAndInputVariables) +{ + LuaBindingsChunk bindings {R"( + local lift = 3.0 + + return { + lift = lift + } + )", + "runtime_bindings"}; + + InputVariables variables { + {"dimensions", klee::InputVariableValue {2}}, + {"shape_suffix", klee::InputVariableValue {std::string {"2d"}}}, + }; + + auto shapeSet = readShapeSetFromString(R"( + local function shape_path() + return "part_" .. shape_suffix .. ".stl" + end + + shapes = { + { + name = "controlled", + material = "steel", + geometry = { + format = "stl", + path = shape_path(), + units = "cm", + operators = { + { translate = (dimensions == 2) and {1.0, lift} or {1.0, 0.0, lift} } + } + } + } + } + )", + InputFormat::Lua, + variables, + bindings); + + ASSERT_EQ(Dimensions::Two, shapeSet.getDimensions()); + ASSERT_EQ(1u, shapeSet.getShapes().size()); + const auto& geometry = shapeSet.getShapes()[0].getGeometry(); + EXPECT_EQ("part_2d.stl", geometry.getPath()); + auto composite = std::dynamic_pointer_cast(geometry.getGeometryOperator()); + ASSERT_TRUE(composite); + ASSERT_EQ(1u, composite->getOperators().size()); + auto translation = std::dynamic_pointer_cast(composite->getOperators()[0]); + ASSERT_TRUE(translation); + EXPECT_THAT(translation->getOffset(), AlmostEqVector(Vector3D {1.0, 3.0, 0.0})); +} + TEST(IOTest, readShapeSet_luaInputVariableRejectsInvalidName) { try @@ -589,6 +777,95 @@ TEST(IOTest, readShapeSet_luaInputVariableRejectsInvalidName) } } +TEST(IOTest, readShapeSet_luaBindingsChunkRejectsInvalidExportName) +{ + try + { + readShapeSetFromString(R"( + shapes = {} + )", + InputFormat::Lua, + LuaBindingsChunk {R"( + return { + ["shape-dim"] = 2 + } + )", + "runtime_bindings"}); + FAIL() << "Should have thrown"; + } + catch(const KleeError& err) + { + EXPECT_THAT(err.what(), HasSubstr("Invalid Klee Lua binding name")); + EXPECT_THAT(err.what(), HasSubstr("Lua identifiers")); + } +} + +TEST(IOTest, readShapeSet_luaBindingsChunkRejectsReservedGlobalName) +{ + try + { + readShapeSetFromString(R"( + shapes = {} + )", + InputFormat::Lua, + LuaBindingsChunk {R"( + return { + math = 2 + } + )", + "runtime_bindings"}); + FAIL() << "Should have thrown"; + } + catch(const KleeError& err) + { + EXPECT_THAT(err.what(), HasSubstr("conflicts with an existing Lua global")); + EXPECT_THAT(err.what(), HasSubstr("math")); + } +} + +TEST(IOTest, readShapeSet_luaBindingsChunkRejectsDuplicateInputVariableName) +{ + try + { + readShapeSetFromString(R"( + shapes = {} + )", + InputFormat::Lua, + {{"dimensions", klee::InputVariableValue {2}}}, + LuaBindingsChunk {R"( + return { + dimensions = 3 + } + )", + "runtime_bindings"}); + FAIL() << "Should have thrown"; + } + catch(const KleeError& err) + { + EXPECT_THAT(err.what(), HasSubstr("duplicates another external Lua binding")); + EXPECT_THAT(err.what(), HasSubstr("dimensions")); + } +} + +TEST(IOTest, readShapeSet_luaBindingsChunkRequiresTableReturn) +{ + try + { + readShapeSetFromString(R"( + dimensions = 2 + shapes = {} + )", + InputFormat::Lua, + LuaBindingsChunk {"return 2", "runtime_bindings"}); + FAIL() << "Should have thrown"; + } + catch(const KleeError& err) + { + EXPECT_THAT(err.what(), HasSubstr("must return a table")); + EXPECT_THAT(err.what(), HasSubstr("runtime_bindings")); + } +} + TEST(IOTest, readShapeSet_luaFileExtension) { std::string fileName = "testFile.lua"; diff --git a/src/examples/CMakeLists.txt b/src/examples/CMakeLists.txt index a77b7e06a4..d3d3cb6cc8 100644 --- a/src/examples/CMakeLists.txt +++ b/src/examples/CMakeLists.txt @@ -202,6 +202,12 @@ if(AXOM_ENABLE_TUTORIALS AND AXOM_ENABLE_KLEE AND AXOM_ENABLE_QUEST) blt_add_test(NAME shaping_tutorial_lesson_03_klee_operators_and_validation_lua COMMAND shaping_tutorial_lesson_03_klee_operators_and_validation ${CMAKE_CURRENT_SOURCE_DIR}/shaping_tutorial/lesson_03/ice_cream.lua) + + blt_add_test(NAME shaping_tutorial_lesson_03_klee_operators_and_validation_lua_bindings + COMMAND shaping_tutorial_lesson_03_klee_operators_and_validation + ${CMAKE_CURRENT_SOURCE_DIR}/shaping_tutorial/lesson_03/ice_cream_bindings.lua + --bindings-file + ${CMAKE_CURRENT_SOURCE_DIR}/shaping_tutorial/lesson_03/ice_cream_runtime_bindings.lua) endif() endif() diff --git a/src/examples/shaping_tutorial/CMakeLists.txt b/src/examples/shaping_tutorial/CMakeLists.txt index f990e82d89..175de18b14 100644 --- a/src/examples/shaping_tutorial/CMakeLists.txt +++ b/src/examples/shaping_tutorial/CMakeLists.txt @@ -107,6 +107,12 @@ if(ENABLE_TESTS) if(AXOM_USE_LUA) blt_add_test(NAME lesson_03_klee_operators_and_validation_lua COMMAND lesson_03_klee_operators_and_validation ../lesson_03/ice_cream.lua) + + blt_add_test(NAME lesson_03_klee_operators_and_validation_lua_bindings + COMMAND lesson_03_klee_operators_and_validation + ../lesson_03/ice_cream_bindings.lua + --bindings-file + ../lesson_03/ice_cream_runtime_bindings.lua) endif() if(AXOM_USE_LUA AND AXOM_USE_MFEM) diff --git a/src/examples/shaping_tutorial/lesson_03/README.md b/src/examples/shaping_tutorial/lesson_03/README.md index 89011fc8b1..e830d8fd09 100644 --- a/src/examples/shaping_tutorial/lesson_03/README.md +++ b/src/examples/shaping_tutorial/lesson_03/README.md @@ -439,6 +439,17 @@ catch(axom::klee::KleeError& error) } ``` +The validator example also accepts an optional `--bindings-file` argument for +Lua decks. The bindings file is a Lua chunk that returns a table of exported +variables and helper functions, which lets an application provide runtime Lua +customization without rebuilding the executable: + +```bash +./bin/lesson_03_klee_operators_and_validation \ + ../lesson_03/ice_cream_bindings.lua \ + --bindings-file ../lesson_03/ice_cream_runtime_bindings.lua +``` + Next, we loop through the shapes and print out information about each shape. We're using an `fmt::memory_buffer` (similar to a `std::stringstream`) to write everything in a single log statement: ```cpp axom::fmt::memory_buffer buffer; diff --git a/src/examples/shaping_tutorial/lesson_03/ice_cream_bindings.lua b/src/examples/shaping_tutorial/lesson_03/ice_cream_bindings.lua new file mode 100644 index 0000000000..75c90ceaa7 --- /dev/null +++ b/src/examples/shaping_tutorial/lesson_03/ice_cream_bindings.lua @@ -0,0 +1,51 @@ +shapes = { + { + name = "background", + material = "background", + geometry = { + format = "none" + } + }, + { + name = "vanilla_scoop", + material = "ice_cream", + geometry = { + format = "mfem", + path = "ice_cream_scoop.mesh", + units = "cm", + operators = { + { scale = scoop_scale }, + { rotate = 5 }, + { translate = offset(scoop_lift) } + } + } + }, + { + name = "colorful_sprinkles", + material = "sprinkles", + geometry = { + format = "mfem", + path = "ice_cream_sprinkles.mesh", + units = "cm", + operators = { + { rotate = 15 }, + { translate = offset(sprinkle_lift) } + } + }, + replaces = {"ice_cream"} + }, + { + name = "cone", + material = "batter", + geometry = { + format = "mfem", + path = "ice_cream_cone.mesh", + units = "cm", + operators = { + { rotate = -5 }, + { translate = offset(cone_lift) } + } + }, + does_not_replace = {"ice_cream", "sprinkles"} + } +} diff --git a/src/examples/shaping_tutorial/lesson_03/ice_cream_runtime_bindings.lua b/src/examples/shaping_tutorial/lesson_03/ice_cream_runtime_bindings.lua new file mode 100644 index 0000000000..60332a6367 --- /dev/null +++ b/src/examples/shaping_tutorial/lesson_03/ice_cream_runtime_bindings.lua @@ -0,0 +1,32 @@ +local dim = 2 + +local scoop_radius = 1.1 +local scoop_lift = 2.0 +local sprinkle_lift = 3.0 +local cone_lift = -2.0 + +local function point(x, y, z) + if dim == 2 then + return {x, y} + end + return {x, y, z or 0.0} +end + +local function offset(y) + return function() + return point(0.0, y) + end +end + +local function scoop_scale() + return {scoop_radius} +end + +return { + dimensions = dim, + scoop_lift = scoop_lift, + sprinkle_lift = sprinkle_lift, + cone_lift = cone_lift, + offset = offset, + scoop_scale = scoop_scale +} diff --git a/src/examples/shaping_tutorial/lesson_03/klee_operators_and_validation.cpp b/src/examples/shaping_tutorial/lesson_03/klee_operators_and_validation.cpp index de8c08b170..f6b5b24e44 100644 --- a/src/examples/shaping_tutorial/lesson_03/klee_operators_and_validation.cpp +++ b/src/examples/shaping_tutorial/lesson_03/klee_operators_and_validation.cpp @@ -142,17 +142,34 @@ int main(int argc, char** argv) // CLI axom::CLI::App app {"Klee Input Validator and Summary"}; std::string inputFilename; + std::string bindingsFilename; app.add_option("input", inputFilename) ->description("Klee input file") ->required() ->check(axom::CLI::ExistingFile); + app.add_option("--bindings-file", bindingsFilename) + ->description("Optional Lua chunk that returns a table of runtime bindings") + ->check(axom::CLI::ExistingFile); CLI11_PARSE(app, argc, argv); + auto loadShapeSet = [&]() { + if(bindingsFilename.empty()) + { + return axom::klee::readShapeSet(inputFilename); + } + + std::ifstream bindingsStream {bindingsFilename}; + std::string bindingsSource {std::istreambuf_iterator(bindingsStream), {}}; + axom::klee::LuaBindingsChunk bindings {bindingsSource, bindingsFilename}; + return axom::klee::readShapeSet(inputFilename, bindings); + }; + // Load the klee shape file and extract some information try { - auto shapeSet = axom::klee::readShapeSet(inputFilename); + auto shapeSet = loadShapeSet(); + AXOM_UNUSED_VAR(shapeSet); } catch(axom::klee::KleeError& error) { @@ -170,7 +187,7 @@ int main(int argc, char** argv) exit(1); } - auto shapeSet = axom::klee::readShapeSet(inputFilename); + auto shapeSet = loadShapeSet(); printShapeSetInfo(shapeSet); return 0; From 55d43a21223e0f0c9df5f530046cf1c2861ad478 Mon Sep 17 00:00:00 2001 From: Kenneth Weiss Date: Wed, 8 Jul 2026 22:16:24 -0700 Subject: [PATCH 04/52] Improves how we override inlet checks for functions Adds inlet::KleeReader::shouldTreatFunctionAsNotFound() as a way to override default behavior. This defaults to false, but is overriden by klee to allow functions for scalars/maps. --- src/axom/inlet/LuaReader.cpp | 19 +++++----- src/axom/inlet/LuaReader.hpp | 17 +++++++++ src/axom/inlet/tests/inlet_Reader.cpp | 16 +++++++++ src/axom/klee/io/GeometryOperatorsIO.cpp | 3 ++ src/axom/klee/io/IO.cpp | 44 ++++++++++++++++++++++++ src/axom/klee/tests/klee_io.cpp | 28 +++++++++++++++ 6 files changed, 118 insertions(+), 9 deletions(-) diff --git a/src/axom/inlet/LuaReader.cpp b/src/axom/inlet/LuaReader.cpp index f63193de3a..60eb429de2 100644 --- a/src/axom/inlet/LuaReader.cpp +++ b/src/axom/inlet/LuaReader.cpp @@ -241,6 +241,8 @@ bool LuaReader::parseString(const std::string& luaString) return true; } +bool LuaReader::shouldTreatFunctionAsNotFound(const std::string&) const { return false; } + // TODO allow alternate delimiter at sidre level #define SCOPE_DELIMITER '/' @@ -602,16 +604,15 @@ ReaderResult LuaReader::getValue(const std::string& id, T& value) { std::vector tokens = axom::utilities::string::split(id, SCOPE_DELIMITER); - // A schema may register a function alias with the same input path as a - // concrete field. Treat a Lua function as absent for value readers so the - // function schema entry can claim it instead of failing as a wrong type. + // If we find a function at a value path, treat it as WrongType + // unless a derived reader has an explicit alternate function schema for this path. if(tokens.size() == 1) { if((*m_lua)[tokens[0]].valid()) { if((*m_lua)[tokens[0]].get_type() == axom::sol::type::function) { - return ReaderResult::NotFound; + return shouldTreatFunctionAsNotFound(id) ? ReaderResult::NotFound : ReaderResult::WrongType; } return detail::checkedGet((*m_lua)[tokens[0]], value); } @@ -626,7 +627,7 @@ ReaderResult LuaReader::getValue(const std::string& id, T& value) { if(t[tokens.back()].get_type() == axom::sol::type::function) { - return ReaderResult::NotFound; + return shouldTreatFunctionAsNotFound(id) ? ReaderResult::NotFound : ReaderResult::WrongType; } return detail::checkedGet(t[tokens.back()], value); } @@ -650,12 +651,12 @@ ReaderResult LuaReader::getMap(const std::string& id, values.clear(); std::vector tokens = axom::utilities::string::split(id, SCOPE_DELIMITER); - // As with scalar value reads, a function at this path belongs to a function - // schema alias rather than to the map reader. + // Same policy as scalar values: functions are WrongType for maps unless a + // derived reader opts this path into a parallel function schema. if(tokens.size() == 1 && (*m_lua)[tokens[0]].valid() && (*m_lua)[tokens[0]].get_type() == axom::sol::type::function) { - return ReaderResult::NotFound; + return shouldTreatFunctionAsNotFound(id) ? ReaderResult::NotFound : ReaderResult::WrongType; } if(tokens.size() > 1) @@ -664,7 +665,7 @@ ReaderResult LuaReader::getMap(const std::string& id, if(traverseToTable(tokens.begin(), tokens.end() - 1, parent) && parent[tokens.back()].valid() && parent[tokens.back()].get_type() == axom::sol::type::function) { - return ReaderResult::NotFound; + return shouldTreatFunctionAsNotFound(id) ? ReaderResult::NotFound : ReaderResult::WrongType; } } diff --git a/src/axom/inlet/LuaReader.hpp b/src/axom/inlet/LuaReader.hpp index a9f88418d9..79861336ff 100644 --- a/src/axom/inlet/LuaReader.hpp +++ b/src/axom/inlet/LuaReader.hpp @@ -110,6 +110,23 @@ class LuaReader : public Reader */ std::shared_ptr solState() { return m_lua; } + /*! + ***************************************************************************** + * \brief Should a function at a scalar/map path be treated as absent? + * + * Inlet normally accepts Lua functions only through getFunction(), + * i.e. for schema entries created with Container::addFunction(). + * If a scalar or map reader sees a function, the input path exists + * but has the wrong kind of value, so the default is ReaderResult::WrongType. + * + * Derived readers may override this for specific paths that intentionally + * have both a concrete field schema entry and an alternate function schema entry. + * Returning ReaderResult::NotFound lets the concrete field stay absent + * so the function schema entry can claim the same public input path. + ***************************************************************************** + */ + virtual bool shouldTreatFunctionAsNotFound(const std::string& id) const; + private: // Expect this to be called for only Inlet-supported types. template diff --git a/src/axom/inlet/tests/inlet_Reader.cpp b/src/axom/inlet/tests/inlet_Reader.cpp index 6dbc99e4da..3cfa0b7222 100644 --- a/src/axom/inlet/tests/inlet_Reader.cpp +++ b/src/axom/inlet/tests/inlet_Reader.cpp @@ -468,6 +468,22 @@ TEST(inlet_Reader_lua, getDiscontiguousMap) std::unordered_map expectedStrs {{33, "hello"}, {200, "bye"}}; EXPECT_EQ(expectedStrs, strs); } + +TEST(inlet_Reader_lua, functionValueIsWrongTypeForFieldsAndMaps) +{ + axom::inlet::LuaReader reader; + reader.parseString( + "foo = function() return 1 end\n" + "bar = { baz = function() return {1, 2, 3} end }"); + + double scalar = 0.0; + EXPECT_EQ(ReaderResult::WrongType, reader.getDouble("foo", scalar)); + EXPECT_EQ(ReaderResult::WrongType, reader.getDouble("bar/baz", scalar)); + + std::unordered_map values; + EXPECT_EQ(ReaderResult::WrongType, reader.getDoubleMap("foo", values)); + EXPECT_EQ(ReaderResult::WrongType, reader.getDoubleMap("bar/baz", values)); +} #endif //------------------------------------------------------------------------------ diff --git a/src/axom/klee/io/GeometryOperatorsIO.cpp b/src/axom/klee/io/GeometryOperatorsIO.cpp index 38d51f225d..7d32897a3f 100644 --- a/src/axom/klee/io/GeometryOperatorsIO.cpp +++ b/src/axom/klee/io/GeometryOperatorsIO.cpp @@ -803,6 +803,9 @@ inlet::Container &GeometryOperatorData::defineSchema(inlet::Container &parent, { // These Lua-only function alternatives read from the public field paths via // pathOverride, leaving YAML and concrete Lua field parsing unchanged. + // KleeLuaReader::shouldTreatFunctionAsNotFound() is what lets a function at + // one of these paths bypass the concrete field and be claimed by the alias. + // Keep this list in sync with isKleeLuaCallbackPath() in IO.cpp. opContainer.addFunction(callbackName("translate"), inlet::FunctionTag::Vector, {}, diff --git a/src/axom/klee/io/IO.cpp b/src/axom/klee/io/IO.cpp index c34691c48b..62ec9a5b74 100644 --- a/src/axom/klee/io/IO.cpp +++ b/src/axom/klee/io/IO.cpp @@ -29,6 +29,7 @@ #include #include #include +#include namespace axom { @@ -39,6 +40,43 @@ namespace bool isLuaIdentifier(const std::string &name); #ifdef AXOM_USE_LUA +// Klee Lua callbacks are parse-time sugar for ordinary Klee operator fields. +// The schema registers both the concrete field and a hidden function alias at +// the same public input path. Only those callback-capable paths should make the +// concrete field reader return NotFound for a Lua function; everywhere else, a +// function at a Klee field path is still WrongType. +bool isKleeLuaCallbackPath(const std::string& id) +{ + const auto tokens = axom::utilities::string::split(id, '/'); + if(tokens.empty()) + { + return false; + } + + static const std::unordered_set operatorFields { + "translate", + "rotate", + "center", + "axis", + "scale", + }; + if(operatorFields.find(tokens.back()) != operatorFields.end()) + { + return true; + } + + static const std::unordered_set sliceFields { + "x", + "y", + "z", + "origin", + "normal", + "up", + }; + return tokens.size() >= 2 && tokens[tokens.size() - 2] == "slice" && + sliceFields.find(tokens.back()) != sliceFields.end(); +} + class KleeLuaReader : public inlet::LuaReader { public: @@ -202,6 +240,12 @@ class KleeLuaReader : public inlet::LuaReader ex.what())}); } } + +protected: + bool shouldTreatFunctionAsNotFound(const std::string& id) const override + { + return isKleeLuaCallbackPath(id); + } }; #endif diff --git a/src/axom/klee/tests/klee_io.cpp b/src/axom/klee/tests/klee_io.cpp index c1ebe3134f..7999794013 100644 --- a/src/axom/klee/tests/klee_io.cpp +++ b/src/axom/klee/tests/klee_io.cpp @@ -1343,6 +1343,34 @@ TEST(IOTest, readShapeSet_luaCallbackWrongVectorDimensionIncludesContext) } } +TEST(IOTest, readShapeSet_luaFunctionValueWrongTypeOutsideCallbackFields) +{ + try + { + readShapeSetFromString(R"( + dimensions = 2 + shapes = { + { + name = "bad_units", + material = "steel", + geometry = { + format = "stl", + path = "part.stl", + units = function() return "cm" end + } + } + } + )", + InputFormat::Lua); + FAIL() << "Should have thrown"; + } + catch(const KleeError& err) + { + EXPECT_THAT(err.what(), HasSubstr("units")); + EXPECT_THAT(err.what(), HasSubstr("wrong type")); + } +} + TEST(IOTest, readShapeSet_luaUnexpectedGlobalDiagnostic) { try From e99d25f6d34a915b940008bf2c6564a1e633e6c4 Mon Sep 17 00:00:00 2001 From: Kenneth Weiss Date: Wed, 8 Jul 2026 22:33:18 -0700 Subject: [PATCH 05/52] Only load the shapeset once in the tutorial example --- .../lesson_03/klee_operators_and_validation.cpp | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/examples/shaping_tutorial/lesson_03/klee_operators_and_validation.cpp b/src/examples/shaping_tutorial/lesson_03/klee_operators_and_validation.cpp index f6b5b24e44..7565ec8e00 100644 --- a/src/examples/shaping_tutorial/lesson_03/klee_operators_and_validation.cpp +++ b/src/examples/shaping_tutorial/lesson_03/klee_operators_and_validation.cpp @@ -169,7 +169,7 @@ int main(int argc, char** argv) try { auto shapeSet = loadShapeSet(); - AXOM_UNUSED_VAR(shapeSet); + printShapeSetInfo(shapeSet); } catch(axom::klee::KleeError& error) { @@ -187,8 +187,5 @@ int main(int argc, char** argv) exit(1); } - auto shapeSet = loadShapeSet(); - printShapeSetInfo(shapeSet); - return 0; } From 0459b14ae379fbae59037e21637630c6530ed544 Mon Sep 17 00:00:00 2001 From: Kenneth Weiss Date: Thu, 9 Jul 2026 00:28:08 -0700 Subject: [PATCH 06/52] inlet: keep Lua state alive for returned functions --- src/axom/inlet/LuaReader.cpp | 38 ++++++++++++++++++++++++------------ 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/src/axom/inlet/LuaReader.cpp b/src/axom/inlet/LuaReader.cpp index 60eb429de2..75cad0cd1a 100644 --- a/src/axom/inlet/LuaReader.cpp +++ b/src/axom/inlet/LuaReader.cpp @@ -13,6 +13,7 @@ */ #include +#include #include #include "axom/inlet/LuaReader.hpp" @@ -490,10 +491,13 @@ FunctionType::Vector extractResult(axom::sol::protected_fu */ template std::function::type...)> buildStdFunction( - axom::sol::protected_function&& func) + axom::sol::protected_function&& func, + std::shared_ptr lua_state) { - // Generalized lambda capture needed to move into lambda - return [func(std::move(func))](typename detail::inlet_function_arg_type::type... args) { + // Keep the Lua state alive for the lifetime of callbacks returned to callers. + return [lua_state(std::move(lua_state)), + func(std::move(func))](typename detail::inlet_function_arg_type::type... args) { + SLIC_ASSERT(lua_state); return extractResult(callWith(func, args...)); }; } @@ -518,7 +522,8 @@ std::function::type...)> buil template typename std::enable_if<(I > MAX_NUM_ARGS), FunctionVariant>::type bindArgType( axom::sol::protected_function&&, - const std::vector&) + const std::vector&, + std::shared_ptr) { SLIC_ERROR("[Inlet] Maximum number of function arguments exceeded: " << I); return {}; @@ -527,22 +532,29 @@ typename std::enable_if<(I > MAX_NUM_ARGS), FunctionVariant>::type bindArgType( template typename std::enable_if::type bindArgType( axom::sol::protected_function&& func, - const std::vector& arg_types) + const std::vector& arg_types, + std::shared_ptr lua_state) { if(arg_types.size() == I) { - return buildStdFunction(std::move(func)); + return buildStdFunction(std::move(func), std::move(lua_state)); } else { switch(arg_types[I]) { case FunctionTag::Vector: - return bindArgType(std::move(func), arg_types); + return bindArgType(std::move(func), + arg_types, + std::move(lua_state)); case FunctionTag::Double: - return bindArgType(std::move(func), arg_types); + return bindArgType(std::move(func), + arg_types, + std::move(lua_state)); case FunctionTag::String: - return bindArgType(std::move(func), arg_types); + return bindArgType(std::move(func), + arg_types, + std::move(lua_state)); default: SLIC_ERROR("[Inlet] Unexpected function argument type"); } @@ -585,13 +597,13 @@ FunctionVariant LuaReader::getFunction(const std::string& id, switch(ret_type) { case FunctionTag::Vector: - return detail::bindArgType<0u, FunctionType::Vector>(std::move(lua_func), arg_types); + return detail::bindArgType<0u, FunctionType::Vector>(std::move(lua_func), arg_types, m_lua); case FunctionTag::Double: - return detail::bindArgType<0u, double>(std::move(lua_func), arg_types); + return detail::bindArgType<0u, double>(std::move(lua_func), arg_types, m_lua); case FunctionTag::Void: - return detail::bindArgType<0u, void>(std::move(lua_func), arg_types); + return detail::bindArgType<0u, void>(std::move(lua_func), arg_types, m_lua); case FunctionTag::String: - return detail::bindArgType<0u, std::string>(std::move(lua_func), arg_types); + return detail::bindArgType<0u, std::string>(std::move(lua_func), arg_types, m_lua); default: SLIC_ERROR("[Inlet] Unexpected function return type"); } From 2d427b053aa628a523fb85f95a71e01fdb59a02d Mon Sep 17 00:00:00 2001 From: Kenneth Weiss Date: Wed, 8 Jul 2026 22:53:18 -0700 Subject: [PATCH 07/52] klee: Improves docs and tests about input lua variables to klee deck Specifically, the variables can be modified within the deck. --- .../klee/docs/sphinx/specifying_shapes.rst | 22 +++++----- src/axom/klee/io/IO.cpp | 3 ++ src/axom/klee/io/IO.hpp | 22 +++++----- src/axom/klee/tests/klee_io.cpp | 40 +++++++++++++++++++ .../shaping_tutorial/lesson_03/README.md | 6 ++- 5 files changed, 70 insertions(+), 23 deletions(-) diff --git a/src/axom/klee/docs/sphinx/specifying_shapes.rst b/src/axom/klee/docs/sphinx/specifying_shapes.rst index 8f8b77a948..8c3c9ff6ed 100644 --- a/src/axom/klee/docs/sphinx/specifying_shapes.rst +++ b/src/axom/klee/docs/sphinx/specifying_shapes.rst @@ -123,9 +123,9 @@ ordinary table values can be generated programmatically: } } -Caller-provided input variables can also be injected into a Lua deck before it -is evaluated. This is useful when an application wants one deck to select between -2D and 3D geometry, dimensions, or transforms at run time: +Caller-provided input variables can also be installed as initial Lua globals before a deck is evaluated. +This is useful when an application wants one deck to select between 2D and 3D geometry, +dimensions, or operator values at run time: .. code-block:: c++ @@ -157,16 +157,18 @@ is evaluated. This is useful when an application wants one deck to select betwee } Input variables are Lua-only and may be booleans, integers, doubles, or strings. -Their names must be Lua identifiers. They are globals by construction and are -allowed by Klee's unexpected-global check; other helper values in the deck should -still be declared :code:`local`. +Their names must be Lua identifiers. They are ordinary mutable globals by +construction and are allowed by Klee's unexpected-global check. Deck code can +reassign these names, so applications should treat them as initial values rather +than read-only controls. Other helper values in the deck should still be declared +:code:`local`. Applications that need richer runtime customization can also provide a Lua bindings chunk. Klee evaluates the chunk before parsing the deck, expects it to -return a table of exported bindings, and then makes those exported names -available as globals while still rejecting unrelated unexpected globals in the -deck. This allows a host code to pass user-supplied helper functions and local -closures at run time without recompiling the C++ application: +return a table of exported bindings, and then installs those exported names as +initial globals while still rejecting unrelated unexpected globals in the deck. +This allows a host code to pass user-supplied helper functions and local closures +at run time without recompiling the C++ application: .. code-block:: c++ diff --git a/src/axom/klee/io/IO.cpp b/src/axom/klee/io/IO.cpp index 62ec9a5b74..56790e76a2 100644 --- a/src/axom/klee/io/IO.cpp +++ b/src/axom/klee/io/IO.cpp @@ -632,6 +632,9 @@ std::unique_ptr createReader(InputFormat format, auto reader = std::make_unique(); const auto reservedGlobals = reader->topLevelGlobalNames(); validateInputVariables(variables); + // External inputs are ordinary Lua globals installed before deck parsing. + // allowedGlobals only prevents Klee's unexpected-global check from rejecting + // those names; it does not make them read-only inside the deck. for(const auto &entry : variables) { if(reservedGlobals.find(entry.first) != reservedGlobals.end()) diff --git a/src/axom/klee/io/IO.hpp b/src/axom/klee/io/IO.hpp index 09a86d3575..6995a682de 100644 --- a/src/axom/klee/io/IO.hpp +++ b/src/axom/klee/io/IO.hpp @@ -24,17 +24,17 @@ enum class InputFormat Lua }; -/// Runtime Lua chunk evaluated before deck parsing; it must return a table of exported bindings. +/// Runtime Lua chunk evaluated before deck parsing; exported bindings become initial Lua globals. struct LuaBindingsChunk { std::string source; std::string label {""}; }; -/// Primitive value types that may be injected into a Lua input deck. +/// Primitive value types that may be set as initial Lua globals. using InputVariableValue = std::variant; -/// Variables to make available to a Lua input deck before it is evaluated. +/// Variables to set as ordinary mutable globals before a Lua input deck is evaluated. using InputVariables = std::unordered_map; /** @@ -61,7 +61,7 @@ ShapeSet readShapeSet(std::istream& stream, InputFormat format); * * \param stream the stream from which to read the ShapeSet * \param format the input deck format to use - * \param variables primitive values to inject into Lua before deck evaluation + * \param variables primitive values to set as initial mutable Lua globals * \note Input variables are supported only for Lua input decks. * \throws runtime_error if the input is invalid */ @@ -73,7 +73,7 @@ ShapeSet readShapeSet(std::istream &stream, InputFormat format, const InputVaria * \param stream the stream from which to read the ShapeSet * \param format the input deck format to use * \param bindings Lua chunk evaluated before deck parsing; must return a table - * of exported bindings + * of exported bindings, which become initial mutable Lua globals * \note Lua bindings are supported only for Lua input decks. * \throws runtime_error if the input is invalid */ @@ -84,9 +84,9 @@ ShapeSet readShapeSet(std::istream &stream, InputFormat format, const LuaBinding * * \param stream the stream from which to read the ShapeSet * \param format the input deck format to use - * \param variables primitive values to inject into Lua before deck evaluation + * \param variables primitive values to set as initial mutable Lua globals * \param bindings Lua chunk evaluated before deck parsing; must return a table - * of exported bindings + * of exported bindings, which become initial mutable Lua globals * \note Input variables and Lua bindings are supported only for Lua input decks. * \throws runtime_error if the input is invalid */ @@ -122,7 +122,7 @@ ShapeSet readShapeSet(const std::string& filePath, InputFormat format); * Read a ShapeSet from a specified file with caller-provided input variables. * * \param filePath the file from which to read the ShapeSet - * \param variables primitive values to inject into Lua before deck evaluation + * \param variables primitive values to set as initial mutable Lua globals * \note The input format is inferred from the file extension. Input variables * are supported only for Lua input decks. * \return the ShapeSet read from the file @@ -135,7 +135,7 @@ ShapeSet readShapeSet(const std::string &filePath, const InputVariables &variabl * * \param filePath the file from which to read the ShapeSet * \param bindings Lua chunk evaluated before deck parsing; must return a table - * of exported bindings + * of exported bindings, which become initial mutable Lua globals * \note The input format is inferred from the file extension. Lua bindings are * supported only for Lua input decks. * \return the ShapeSet read from the file @@ -147,9 +147,9 @@ ShapeSet readShapeSet(const std::string &filePath, const LuaBindingsChunk &bindi * Read a ShapeSet from a specified file with caller-provided input variables and Lua bindings. * * \param filePath the file from which to read the ShapeSet - * \param variables primitive values to inject into Lua before deck evaluation + * \param variables primitive values to set as initial mutable Lua globals * \param bindings Lua chunk evaluated before deck parsing; must return a table - * of exported bindings + * of exported bindings, which become initial mutable Lua globals * \note The input format is inferred from the file extension. Input variables * and Lua bindings are supported only for Lua input decks. * \return the ShapeSet read from the file diff --git a/src/axom/klee/tests/klee_io.cpp b/src/axom/klee/tests/klee_io.cpp index 7999794013..3910787747 100644 --- a/src/axom/klee/tests/klee_io.cpp +++ b/src/axom/klee/tests/klee_io.cpp @@ -612,6 +612,46 @@ TEST(IOTest, readShapeSet_luaInputVariablesProvideInitialDimensionAndOperator) EXPECT_THAT(translation->getOffset(), AlmostEqVector(Vector3D {1.0, 3.0, 0.0})); } +TEST(IOTest, readShapeSet_luaInputVariablesAreInitialMutableGlobals) +{ + InputVariables variables { + {"dimensions", klee::InputVariableValue {2}}, + {"lift", klee::InputVariableValue {3.0}}, + }; + + auto shapeSet = readShapeSetFromString(R"( + dimensions = 3 + lift = 7.0 + + shapes = { + { + name = "overridden", + material = "steel", + geometry = { + format = "stl", + path = "part.stl", + units = "cm", + operators = { + { translate = {1.0, 2.0, lift} } + } + } + } + } + )", + InputFormat::Lua, + variables); + + ASSERT_EQ(Dimensions::Three, shapeSet.getDimensions()); + ASSERT_EQ(1u, shapeSet.getShapes().size()); + const auto& geometry = shapeSet.getShapes()[0].getGeometry(); + auto composite = std::dynamic_pointer_cast(geometry.getGeometryOperator()); + ASSERT_TRUE(composite); + ASSERT_EQ(1u, composite->getOperators().size()); + auto translation = std::dynamic_pointer_cast(composite->getOperators()[0]); + ASSERT_TRUE(translation); + EXPECT_THAT(translation->getOffset(), AlmostEqVector(Vector3D {1.0, 2.0, 7.0})); +} + TEST(IOTest, readShapeSet_luaBindingsChunkProvidesInitialDimensionAndOperator) { LuaBindingsChunk bindings {R"( diff --git a/src/examples/shaping_tutorial/lesson_03/README.md b/src/examples/shaping_tutorial/lesson_03/README.md index e830d8fd09..ce915e48cb 100644 --- a/src/examples/shaping_tutorial/lesson_03/README.md +++ b/src/examples/shaping_tutorial/lesson_03/README.md @@ -441,8 +441,10 @@ catch(axom::klee::KleeError& error) The validator example also accepts an optional `--bindings-file` argument for Lua decks. The bindings file is a Lua chunk that returns a table of exported -variables and helper functions, which lets an application provide runtime Lua -customization without rebuilding the executable: +variables and helper functions, which are installed as initial mutable globals +before the deck is evaluated. This lets an application provide runtime Lua +customization without rebuilding the executable, while still allowing the deck +to reassign those globals if it chooses: ```bash ./bin/lesson_03_klee_operators_and_validation \ From 987b8c20deedf7881a9cc1007de0083fb0ba35b8 Mon Sep 17 00:00:00 2001 From: Kenneth Weiss Date: Wed, 8 Jul 2026 23:10:19 -0700 Subject: [PATCH 08/52] klee: Improves test that compares lua and yaml parsing --- src/axom/klee/tests/klee_io.cpp | 158 ++++++++++++++++++++++++++++++++ 1 file changed, 158 insertions(+) diff --git a/src/axom/klee/tests/klee_io.cpp b/src/axom/klee/tests/klee_io.cpp index 3910787747..0fe3be2207 100644 --- a/src/axom/klee/tests/klee_io.cpp +++ b/src/axom/klee/tests/klee_io.cpp @@ -1457,6 +1457,164 @@ TEST(IOTest, readShapeSet_luaNestedUnexpectedFieldsMatchYamlValidation) ASSERT_EQ(1u, shapeSet.getShapes().size()); EXPECT_EQ("wheel", shapeSet.getShapes()[0].getName()); } + +TEST(IOTest, readShapeSet_luaIntegratedWorkflowSmoke) +{ + auto shapeSet = readShapeSetFromString(R"( + local dim = 2 + local scale_factor = 1.25 + local lift = 3.0 + + local function point(x, y, z) + if dim == 2 then + return {x, y} + end + return {x, y, z or 0.0} + end + + local function lift_by(amount) + return function() + return point(0.0, amount) + end + end + + local function scale_callback() + return {scale_factor} + end + + dimensions = dim + shapes = { + { + name = "generated", + material = "steel", + geometry = { + format = "mfem", + path = "generated.mesh", + units = "cm", + operators = { + { scale = scale_callback }, + { translate = lift_by(lift) } + } + } + } + } + )", + InputFormat::Lua); + + ASSERT_EQ(1u, shapeSet.getShapes().size()); + const auto &geometry = shapeSet.getShapes()[0].getGeometry(); + auto composite = std::dynamic_pointer_cast(geometry.getGeometryOperator()); + ASSERT_TRUE(composite); + ASSERT_EQ(2u, composite->getOperators().size()); + + auto scale = std::dynamic_pointer_cast(composite->getOperators()[0]); + ASSERT_TRUE(scale); + EXPECT_DOUBLE_EQ(1.25, scale->getXFactor()); + EXPECT_DOUBLE_EQ(1.25, scale->getYFactor()); + EXPECT_DOUBLE_EQ(1.25, scale->getZFactor()); + + auto translation = std::dynamic_pointer_cast(composite->getOperators()[1]); + ASSERT_TRUE(translation); + EXPECT_THAT(translation->getOffset(), AlmostEqVector(Vector3D {0.0, 3.0, 0.0})); +} + +TEST(IOTest, readShapeSet_luaParseSmokeMatchesYaml) +{ + const std::string yaml = R"( + dimensions: 3 + shapes: + - name: one + material: steel + geometry: + format: stl + path: one.stl + units: cm + operators: + - rotate: 20 + axis: [0, 0, 1] + center: [1, 2, 3] + - translate: [4, 5, 6] + - name: two + material: glass + replaces: [steel] + geometry: + format: stl + path: two.stl + units: cm + operators: + - scale: [1.5, 2.0, 2.5] + center: [0, 0, 0] + )"; + + const std::string lua = R"( + local angle = 20 + local axis = {0, 0, 1} + local center = {1, 2, 3} + dimensions = 3 + shapes = { + { + name = "one", + material = "steel", + geometry = { + format = "stl", + path = "one.stl", + units = "cm", + operators = { + { rotate = angle, axis = axis, center = center }, + { translate = {4, 5, 6} } + } + } + }, + { + name = "two", + material = "glass", + replaces = {"steel"}, + geometry = { + format = "stl", + path = "two.stl", + units = "cm", + operators = { + { scale = {1.5, 2.0, 2.5}, center = {0, 0, 0} } + } + } + } + } + )"; + + auto yamlShapeSet = readShapeSetFromString(yaml, InputFormat::YAML); + auto luaShapeSet = readShapeSetFromString(lua, InputFormat::Lua); + + ASSERT_EQ(Dimensions::Three, yamlShapeSet.getDimensions()); + ASSERT_EQ(yamlShapeSet.getDimensions(), luaShapeSet.getDimensions()); + ASSERT_EQ(2u, yamlShapeSet.getShapes().size()); + ASSERT_EQ(yamlShapeSet.getShapes().size(), luaShapeSet.getShapes().size()); + + const auto &yamlFirstShape = yamlShapeSet.getShapes()[0]; + const auto &luaFirstShape = luaShapeSet.getShapes()[0]; + EXPECT_EQ(yamlFirstShape.getName(), luaFirstShape.getName()); + EXPECT_EQ(yamlFirstShape.getMaterial(), luaFirstShape.getMaterial()); + EXPECT_EQ(yamlFirstShape.getGeometry().getPath(), luaFirstShape.getGeometry().getPath()); + + const auto &yamlFirstOperator = yamlFirstShape.getGeometry().getGeometryOperator(); + const auto &luaFirstOperator = luaFirstShape.getGeometry().getGeometryOperator(); + auto yamlFirstComposite = std::dynamic_pointer_cast(yamlFirstOperator); + auto luaFirstComposite = std::dynamic_pointer_cast(luaFirstOperator); + ASSERT_TRUE(yamlFirstComposite); + ASSERT_TRUE(luaFirstComposite); + ASSERT_EQ(yamlFirstComposite->getOperators().size(), luaFirstComposite->getOperators().size()); + + const auto &yamlSecondShape = yamlShapeSet.getShapes()[1]; + const auto &luaSecondShape = luaShapeSet.getShapes()[1]; + EXPECT_EQ(yamlSecondShape.getName(), luaSecondShape.getName()); + EXPECT_EQ(yamlSecondShape.getMaterial(), luaSecondShape.getMaterial()); + EXPECT_TRUE(luaSecondShape.replaces("steel")); + + const auto &luaSecondOperator = luaSecondShape.getGeometry().getGeometryOperator(); + auto luaSecondComposite = std::dynamic_pointer_cast(luaSecondOperator); + ASSERT_TRUE(luaSecondComposite); + ASSERT_EQ(1u, luaSecondComposite->getOperators().size()); + EXPECT_TRUE(std::dynamic_pointer_cast(luaSecondComposite->getOperators()[0])); +} #endif TEST(IOTest, readShapeSet_shapeWithReplacesAndDoesNotReplaceLists) From f0a303562e6cebeb9caedc567f286e3382088396 Mon Sep 17 00:00:00 2001 From: Kenneth Weiss Date: Thu, 9 Jul 2026 00:32:29 -0700 Subject: [PATCH 09/52] docs: demonstrate callback-based Klee Lua tutorial deck --- .../shaping_tutorial/lesson_03/README.md | 34 ++++--------------- .../shaping_tutorial/lesson_03/ice_cream.lua | 26 ++++++++++---- 2 files changed, 26 insertions(+), 34 deletions(-) diff --git a/src/examples/shaping_tutorial/lesson_03/README.md b/src/examples/shaping_tutorial/lesson_03/README.md index ce915e48cb..1e59b9cc8e 100644 --- a/src/examples/shaping_tutorial/lesson_03/README.md +++ b/src/examples/shaping_tutorial/lesson_03/README.md @@ -281,37 +281,17 @@ shapes:
Figure: Example Klee inputs showing `scale`, `translate`, `rotate` and unit conversion operators.
-#### Lua decks +#### Lua decks for generated geometry setup Klee can also read Lua decks when Axom is configured with Lua support. -Lua decks use the same shape schema as YAML, but Lua is evaluated before -Inlet reads the resulting global tables. The lesson's `ice_cream.lua` deck -mirrors the YAML setup while using local Lua variables for shared values: +Lua decks use the same shape schema as YAML, but Lua is evaluated first, +which lets you keep helper constants and functions local to the deck: -```lua -local dim = 2 -local scoop_radius = 1.1 -local scoop_offset = {0.0, 2.0} +- ordinary Klee fields can be generated from local variables, +- selected affine operator fields can be zero-argument callbacks evaluated once during parsing. -dimensions = dim - -shapes = { - { - name = "vanilla_scoop", - material = "ice_cream", - geometry = { - format = "mfem", - path = "ice_cream_scoop.mesh", - units = "cm", - operators = { - { scale = {scoop_radius} }, - { rotate = 5 }, - { translate = scoop_offset } - } - } - } -} -``` +The lesson's `ice_cream.lua` deck mirrors the YAML ice-cream setup while using these Lua features in one small workflow. +Helper functions generate dimensional points, and callbacks compute scale and translation fields. ### Replacement Rules Replacement rules give users some extra control in how shapes get overlaid. By default, a new shape of a given material will replace all other shapes. diff --git a/src/examples/shaping_tutorial/lesson_03/ice_cream.lua b/src/examples/shaping_tutorial/lesson_03/ice_cream.lua index e1f6d5bece..77c76b4c5b 100644 --- a/src/examples/shaping_tutorial/lesson_03/ice_cream.lua +++ b/src/examples/shaping_tutorial/lesson_03/ice_cream.lua @@ -5,10 +5,22 @@ local scoop_lift = 2.0 local sprinkle_lift = 3.0 local cone_lift = -2.0 -local scoop_scale = {scoop_radius} -local scoop_offset = {0.0, scoop_lift} -local sprinkle_offset = {0.0, sprinkle_lift} -local cone_offset = {0.0, cone_lift} +local function point(x, y, z) + if dim == 2 then + return {x, y} + end + return {x, y, z or 0.0} +end + +local function offset(y) + return function() + return point(0.0, y) + end +end + +local function scoop_scale() + return {scoop_radius} +end dimensions = dim @@ -30,7 +42,7 @@ shapes = { operators = { { scale = scoop_scale }, { rotate = 5 }, - { translate = scoop_offset } + { translate = offset(scoop_lift) } } } }, @@ -43,7 +55,7 @@ shapes = { units = "cm", operators = { { rotate = 15 }, - { translate = sprinkle_offset } + { translate = offset(sprinkle_lift) } } }, replaces = {"ice_cream"} @@ -57,7 +69,7 @@ shapes = { units = "cm", operators = { { rotate = -5 }, - { translate = cone_offset } + { translate = offset(cone_lift) } } }, does_not_replace = {"ice_cream", "sprinkles"} From be06403ccf1dd924c2f389d092619fadcc61aa89 Mon Sep 17 00:00:00 2001 From: Kenneth Weiss Date: Wed, 29 Jul 2026 00:45:50 -0700 Subject: [PATCH 10/52] Klee: several cleanups to callback support * Removed scalar->vector coersion. Need to return a table with one entry rather than a scalar * Stop storing name for operators. They were not used * Removed unreachable duplicate export extraction --- src/axom/inlet/LuaReader.cpp | 10 +- src/axom/inlet/tests/inlet_function.cpp | 10 ++ .../klee/docs/sphinx/specifying_shapes.rst | 11 ++- src/axom/klee/io/GeometryOperatorsIO.cpp | 93 ++++++++++--------- src/axom/klee/io/GeometryOperatorsIO.hpp | 15 +-- src/axom/klee/io/IO.cpp | 21 ++--- .../klee/tests/klee_geometry_operators_io.cpp | 2 +- src/axom/klee/tests/klee_io.cpp | 2 +- 8 files changed, 80 insertions(+), 84 deletions(-) diff --git a/src/axom/inlet/LuaReader.cpp b/src/axom/inlet/LuaReader.cpp index 75cad0cd1a..46687787c1 100644 --- a/src/axom/inlet/LuaReader.cpp +++ b/src/axom/inlet/LuaReader.cpp @@ -444,8 +444,8 @@ FunctionType::Vector extractResult(axom::sol::protected_fu if(size < 1 || size > 3) { throw std::runtime_error( - fmt::format("[Inlet] Lua vector function returned a table with {0} entries; expected 1 to " - "3 numeric entries", + fmt::format("[Inlet] Lua vector function returned a table with {0} entries; " + "expected 1 to 3 numeric entries", size)); } @@ -465,12 +465,6 @@ FunctionType::Vector extractResult(axom::sol::protected_fu return FunctionType::Vector {values.data(), static_cast(values.size())}; } - axom::sol::optional scalar_option = res; - if(scalar_option) - { - return FunctionType::Vector {scalar_option.value()}; - } - throw std::runtime_error("[Inlet] Lua function call failed, return types possibly incorrect"); } diff --git a/src/axom/inlet/tests/inlet_function.cpp b/src/axom/inlet/tests/inlet_function.cpp index 4e04994eb9..0f98791f21 100644 --- a/src/axom/inlet/tests/inlet_function.cpp +++ b/src/axom/inlet/tests/inlet_function.cpp @@ -13,6 +13,7 @@ #include "gtest/gtest.h" #include +#include #include #include #include @@ -81,6 +82,15 @@ TEST(inlet_function, simple_vec3_to_vec3_raw_table_return) EXPECT_FLOAT_EQ(result[2], 6); } +TEST(inlet_function, vector_function_rejects_scalar_return) +{ + auto inlet = createBasicInlet("function foo () return 2.0 end"); + auto func = inlet.reader().getFunction("foo", FunctionTag::Vector, {}); + + EXPECT_TRUE(func); + EXPECT_THROW(func.call(), std::runtime_error); +} + TEST(inlet_function, simple_vec3_to_vec3_raw_partial_init) { std::string testString = "function foo (v) return 2*v end"; diff --git a/src/axom/klee/docs/sphinx/specifying_shapes.rst b/src/axom/klee/docs/sphinx/specifying_shapes.rst index 8c3c9ff6ed..8b9d5b51fb 100644 --- a/src/axom/klee/docs/sphinx/specifying_shapes.rst +++ b/src/axom/klee/docs/sphinx/specifying_shapes.rst @@ -257,13 +257,14 @@ functions. Callbacks should be pure functions of local deck variables. } } -Vector-valued callbacks return raw numeric Lua tables such as :code:`{x, y}` or -:code:`{x, y, z}`. The typed :code:`Vector.new(...)` object is also accepted. -Scalar-valued callbacks return a number. Supported callback fields are +Vector-valued callbacks return raw numeric Lua tables such as :code:`{x, y}` or :code:`{x, y, z}`. +The typed :code:`Vector.new(...)` object is also accepted. +Scalar-valued callbacks return a number. +Supported callback fields are :code:`translate`, :code:`axis`, :code:`center`, :code:`scale`, :code:`slice.origin`, :code:`slice.normal`, :code:`slice.up`, :code:`rotate`, -:code:`slice.x`, :code:`slice.y`, and :code:`slice.z`. For :code:`scale`, a -number means uniform scaling and a table means per-axis scaling. +:code:`slice.x`, :code:`slice.y`, and :code:`slice.z`. +For :code:`scale`, a one-entry table means uniform scaling and a multi-entry table means per-axis scaling. Common Lua input errors are reported as Klee parsing errors. A Lua input file read without Lua support reports: diff --git a/src/axom/klee/io/GeometryOperatorsIO.cpp b/src/axom/klee/io/GeometryOperatorsIO.cpp index 7d32897a3f..0d84146020 100644 --- a/src/axom/klee/io/GeometryOperatorsIO.cpp +++ b/src/axom/klee/io/GeometryOperatorsIO.cpp @@ -31,7 +31,9 @@ namespace { using OpPtr = CompositeOperator::OpPtr; using OperatorParser = - std::function; + std::function; using internal::toDoubleVector; using primal::Point3D; using primal::Vector3D; @@ -351,13 +353,14 @@ void verifyObjectFields(const inlet::Container& containerToTest, * \throws KleeError if the operator fields or vector dimensions are invalid */ OpPtr parseTranslate(const SingleOperatorData &data, - const TransformableGeometryProperties &startProperties) + const TransformableGeometryProperties &startProperties, + const std::string &shapeName) { const auto &opContainer = *data.m_container; verifyObjectFields(opContainer, "translate", FieldSet {}, FieldSet {}); return std::make_shared( - getVector(opContainer, "translate", startProperties.dimensions, data.m_shapeName), - startProperties); + getVector(opContainer, "translate", startProperties.dimensions, shapeName), + startProperties); } /** @@ -369,7 +372,8 @@ OpPtr parseTranslate(const SingleOperatorData &data, * \throws KleeError if the rotation is invalid for the start dimensions or operator fields */ OpPtr parseRotate(const SingleOperatorData &data, - const TransformableGeometryProperties &startProperties) + const TransformableGeometryProperties &startProperties, + const std::string &shapeName) { const auto &opContainer = *data.m_container; switch(startProperties.dimensions) @@ -379,8 +383,8 @@ OpPtr parseRotate(const SingleOperatorData &data, verifyObjectFields(opContainer, "rotate", FieldSet {}, {"center"}); Vector3D axis {0, 0, 1}; return std::make_shared( - getScalar(opContainer, "rotate", data.m_shapeName), - getPoint(opContainer, "center", Dimensions::Two, Point3D {0, 0, 0}, data.m_shapeName), + getScalar(opContainer, "rotate", shapeName), + getPoint(opContainer, "center", Dimensions::Two, Point3D {0, 0, 0}, shapeName), axis, startProperties); } @@ -389,9 +393,9 @@ OpPtr parseRotate(const SingleOperatorData &data, { verifyObjectFields(opContainer, "rotate", {"axis"}, {"center"}); return std::make_shared( - getScalar(opContainer, "rotate", data.m_shapeName), - getPoint(opContainer, "center", Dimensions::Three, Point3D {0, 0, 0}, data.m_shapeName), - getVector(opContainer, "axis", Dimensions::Three, data.m_shapeName), + getScalar(opContainer, "rotate", shapeName), + getPoint(opContainer, "center", Dimensions::Three, Point3D {0, 0, 0}, shapeName), + getVector(opContainer, "axis", Dimensions::Three, shapeName), startProperties); } break; @@ -533,7 +537,8 @@ OpPtr readPerpendicularSlice(const inlet::Container &sliceContainer, * \throws KleeError if the slice fields or values are invalid */ OpPtr parseSlice(const SingleOperatorData &data, - const TransformableGeometryProperties &startProperties) + const TransformableGeometryProperties &startProperties, + const std::string &shapeName) { const auto &opContainer = *data.m_container; if(startProperties.dimensions != Dimensions::Three) @@ -549,7 +554,7 @@ OpPtr parseSlice(const SingleOperatorData &data, {1, 0, 0}, {0, 0, 1}, startProperties, - data.m_shapeName); + shapeName); } else if(containsFieldOrCallback(sliceContainer, "y")) { @@ -558,7 +563,7 @@ OpPtr parseSlice(const SingleOperatorData &data, {0, 1, 0}, {1, 0, 0}, startProperties, - data.m_shapeName); + shapeName); } else if(containsFieldOrCallback(sliceContainer, "z")) { @@ -567,14 +572,14 @@ OpPtr parseSlice(const SingleOperatorData &data, {0, 0, 1}, {0, 1, 0}, startProperties, - data.m_shapeName); + shapeName); } verifyObjectFields(sliceContainer, "origin", {"normal", "up"}, FieldSet {}); - return makeCheckedSlice(getPoint(sliceContainer, "origin", Dimensions::Three, data.m_shapeName), - getVector(sliceContainer, "normal", Dimensions::Three, data.m_shapeName), - getVector(sliceContainer, "up", Dimensions::Three, data.m_shapeName), + return makeCheckedSlice(getPoint(sliceContainer, "origin", Dimensions::Three, shapeName), + getVector(sliceContainer, "normal", Dimensions::Three, shapeName), + getVector(sliceContainer, "up", Dimensions::Three, shapeName), startProperties, sliceContainer.name()); } @@ -588,7 +593,8 @@ OpPtr parseSlice(const SingleOperatorData &data, * \throws KleeError if the scale fields or vector dimensions are invalid */ OpPtr parseScale(const SingleOperatorData &data, - const TransformableGeometryProperties &startProperties) + const TransformableGeometryProperties &startProperties, + const std::string &shapeName) { const auto &opContainer = *data.m_container; verifyObjectFields(opContainer, "scale", FieldSet {}, FieldSet {"center"}); @@ -596,7 +602,7 @@ OpPtr parseScale(const SingleOperatorData &data, ? wrapCallbackErrors>( opContainer, "scale", - data.m_shapeName, + shapeName, [&]() { return callbackVectorToDoubleVector( opContainer[callbackName("scale")].call()); @@ -613,10 +619,10 @@ OpPtr parseScale(const SingleOperatorData &data, if(actualSize != expectedSize) { throw KleeError({fieldPath(opContainer, "scale"), - fmt::format("{}: Wrong size for scale. Expected {}. Got {}.", - callbackContext(opContainer, "scale", data.m_shapeName), - expectedSize, - actualSize)}); + fmt::format("{}: Wrong size for scale. Expected {}. Got {}.", + callbackContext(opContainer, "scale", shapeName), + expectedSize, + actualSize)}); } } else @@ -630,8 +636,11 @@ OpPtr parseScale(const SingleOperatorData &data, Point3D center {0., 0., 0.}; if(containsFieldOrCallback(opContainer, "center")) { - center = - getPoint(opContainer, "center", startProperties.dimensions, Point3D {0, 0, 0}, data.m_shapeName); + center = getPoint(opContainer, + "center", + startProperties.dimensions, + Point3D {0, 0, 0}, + shapeName); } return std::make_shared(factors[0], factors[1], factors[2], center, startProperties); @@ -646,7 +655,8 @@ OpPtr parseScale(const SingleOperatorData &data, * \throws KleeError if the unit string or operator fields are invalid */ OpPtr parseConvertUnits(const SingleOperatorData &data, - const TransformableGeometryProperties &startProperties) + const TransformableGeometryProperties &startProperties, + const std::string &) { const auto &opContainer = *data.m_container; verifyObjectFields(opContainer, "convert_units_to", FieldSet {}, FieldSet {}); @@ -714,7 +724,8 @@ OpPtr parseRef(const SingleOperatorData &data, */ OpPtr convertOperator(SingleOperatorData const& data, TransformableGeometryProperties startProperties, - const NamedOperatorMap& namedOperators) + const NamedOperatorMap &namedOperators, + const std::string &shapeName) { std::unordered_map parsers { {"translate", parseTranslate}, @@ -724,7 +735,8 @@ OpPtr convertOperator(SingleOperatorData const& data, {"convert_units_to", parseConvertUnits}, {"ref", [&namedOperators](const SingleOperatorData &opData, - const TransformableGeometryProperties &startProperties) { + const TransformableGeometryProperties &startProperties, + const std::string &) { return parseRef(opData, startProperties, namedOperators); }}, }; @@ -733,7 +745,7 @@ OpPtr convertOperator(SingleOperatorData const& data, { if(containsFieldOrCallback(*data.m_container, entry.first.c_str())) { - return entry.second(data, startProperties); + return entry.second(data, startProperties, shapeName); } } @@ -761,18 +773,9 @@ GeometryOperatorData::GeometryOperatorData(const Path& path) GeometryOperatorData::GeometryOperatorData(const Path& path, std::vector&& singleOperatorData) : m_path {path} - , m_singleOperatorData {singleOperatorData} + , m_singleOperatorData {std::move(singleOperatorData)} { } -void GeometryOperatorData::setShapeName(std::string shapeName) -{ - m_shapeName = std::move(shapeName); - for(auto &data : m_singleOperatorData) - { - data.m_shapeName = m_shapeName; - } -} - inlet::Container &GeometryOperatorData::defineSchema(inlet::Container &parent, const std::string &fieldName, const std::string &description, @@ -827,8 +830,9 @@ inlet::Container &GeometryOperatorData::defineSchema(inlet::Container &parent, } std::shared_ptr GeometryOperatorData::makeOperator( - const TransformableGeometryProperties& startProperties, - const NamedOperatorMap& namedOperators) const + const TransformableGeometryProperties &startProperties, + const NamedOperatorMap &namedOperators, + const std::string &shapeName) const { if(m_singleOperatorData.empty()) { @@ -842,7 +846,8 @@ std::shared_ptr GeometryOperatorData::makeOperator( auto composite = std::make_shared(startProperties); for(auto& data : m_singleOperatorData) { - composite->addOperator(convertOperator(data, composite->getEndProperties(), namedOperators)); + composite->addOperator( + convertOperator(data, composite->getEndProperties(), namedOperators, shapeName)); } return composite; } @@ -889,7 +894,7 @@ NamedOperatorMap NamedOperatorMapData::makeNamedOperatorMap(Dimensions fileDimen dimensions, opData.startUnits, }; - auto op = opData.value.makeOperator(startProperties, namedOperators); + auto op = opData.value.makeOperator(startProperties, namedOperators, ""); if(op->getEndProperties().units != opData.endUnits) { @@ -909,7 +914,7 @@ struct FromInlet { axom::klee::internal::SingleOperatorData operator()(const axom::inlet::Container& base) { - return axom::klee::internal::SingleOperatorData {&base, ""}; + return axom::klee::internal::SingleOperatorData {&base}; } }; diff --git a/src/axom/klee/io/GeometryOperatorsIO.hpp b/src/axom/klee/io/GeometryOperatorsIO.hpp index 222adf651b..e17e12027b 100644 --- a/src/axom/klee/io/GeometryOperatorsIO.hpp +++ b/src/axom/klee/io/GeometryOperatorsIO.hpp @@ -29,7 +29,6 @@ using NamedOperatorMap = std::unordered_map makeOperator(const TransformableGeometryProperties& startProperties, - const NamedOperatorMap& namedOperators) const; + std::shared_ptr makeOperator(const TransformableGeometryProperties &startProperties, + const NamedOperatorMap &namedOperators, + const std::string &shapeName) const; /** * Get the path of this operator in the source document @@ -82,17 +83,9 @@ class GeometryOperatorData */ const Path& getPath() const { return m_path; } - /** - * Set the name of the shape that owns these operators, when known. - * - * @param shapeName the owning shape name - */ - void setShapeName(std::string shapeName); - private: Path m_path; std::vector m_singleOperatorData; - std::string m_shapeName; }; /// Data for a named operator. diff --git a/src/axom/klee/io/IO.cpp b/src/axom/klee/io/IO.cpp index 56790e76a2..e830957c91 100644 --- a/src/axom/klee/io/IO.cpp +++ b/src/axom/klee/io/IO.cpp @@ -80,10 +80,10 @@ bool isKleeLuaCallbackPath(const std::string& id) class KleeLuaReader : public inlet::LuaReader { public: - std::unordered_set topLevelGlobalNames() const + std::unordered_set topLevelGlobalNames() { std::unordered_set names; - auto lua = const_cast(this)->solState(); + auto lua = solState(); for(const auto &entry : lua->globals()) { if(entry.first.get_type() == axom::sol::type::string) @@ -190,14 +190,7 @@ class KleeLuaReader : public inlet::LuaReader "Klee Lua binding name '{}' duplicates another external Lua binding.", name)}); } - if(!exportedNames.insert(name).second) - { - throw KleeError( - {exportPath(name), - axom::fmt::format("Klee Lua bindings chunk '{}' exports '{}' more than once.", - static_cast(chunkPath), - name)}); - } + exportedNames.insert(name); switch(entry.second.get_type()) { @@ -289,7 +282,6 @@ struct FromInlet base["replaces"].get>(), base["does_not_replace"].get>(), base.get("geometry")}; - data.geometry.operatorData.setShapeName(data.name); return data; } }; @@ -431,7 +423,8 @@ void defineKleeSchema(inlet::Inlet &document, bool enableLuaCallbacks) */ Geometry convert(GeometryData const& data, Dimensions fileDimensions, - internal::NamedOperatorMap const& namedOperators) + internal::NamedOperatorMap const &namedOperators, + const std::string &shapeName) { const bool has_start_dims = data.startDimensions != Dimensions::Unspecified; const bool has_explicit_dims = data.explicitDimensions != Dimensions::Unspecified; @@ -454,7 +447,7 @@ Geometry convert(GeometryData const& data, Geometry geometry {startProperties, data.format, data.path, - data.operatorData.makeOperator(startProperties, namedOperators)}; + data.operatorData.makeOperator(startProperties, namedOperators, shapeName)}; const auto computed_end_dims = geometry.getEndProperties().dimensions; const auto expected_end_dims = has_explicit_dims ? data.explicitDimensions : fileDimensions; @@ -488,7 +481,7 @@ Shape convert(ShapeData const& data, data.material, data.materialsReplaced, data.materialsNotReplaced, - convert(data.geometry, fileDimensions, namedOperators)}; + convert(data.geometry, fileDimensions, namedOperators, data.name)}; } /** diff --git a/src/axom/klee/tests/klee_geometry_operators_io.cpp b/src/axom/klee/tests/klee_geometry_operators_io.cpp index 7c19ad909d..60daa5088a 100644 --- a/src/axom/klee/tests/klee_geometry_operators_io.cpp +++ b/src/axom/klee/tests/klee_geometry_operators_io.cpp @@ -80,7 +80,7 @@ OperatorPointer readOperators(const TransformableGeometryProperties& startProper throw KleeError(errors); } auto opData = doc["test_list"].get(); - return opData.makeOperator(startProperties, namedOperators); + return opData.makeOperator(startProperties, namedOperators, ""); } /** diff --git a/src/axom/klee/tests/klee_io.cpp b/src/axom/klee/tests/klee_io.cpp index 0fe3be2207..1c4c11b264 100644 --- a/src/axom/klee/tests/klee_io.cpp +++ b/src/axom/klee/tests/klee_io.cpp @@ -1199,7 +1199,7 @@ TEST(IOTest, readShapeSet_luaOperatorCallbacks) center = function() return Vector.new(1, 2, 3) end }, { translate = function() return {4, 5, 6} end }, - { scale = function() return 2.0 end }, + { scale = function() return {2.0} end }, { scale = function() return {1.5, 2.5, 3.5} end, center = function() return {1, 1, 1} end From 725ed5cbef55994ba8fc0f98eb3167e6d03dc66b Mon Sep 17 00:00:00 2001 From: Kenneth Weiss Date: Wed, 29 Jul 2026 01:20:19 -0700 Subject: [PATCH 11/52] Klee: Isolates Lua bindings environment Bindings operate in isolated environment so they don't inherit or affect the global state. --- .../klee/docs/sphinx/specifying_shapes.rst | 7 ++ src/axom/klee/io/IO.cpp | 7 +- src/axom/klee/tests/klee_io.cpp | 108 ++++++++++++++++++ 3 files changed, 121 insertions(+), 1 deletion(-) diff --git a/src/axom/klee/docs/sphinx/specifying_shapes.rst b/src/axom/klee/docs/sphinx/specifying_shapes.rst index 8b9d5b51fb..a1276f724a 100644 --- a/src/axom/klee/docs/sphinx/specifying_shapes.rst +++ b/src/axom/klee/docs/sphinx/specifying_shapes.rst @@ -215,6 +215,13 @@ Exported values may be booleans, numbers, strings, tables, or functions. Like input variables, exported bindings are ordinary Lua globals; deck code can reassign exported names and can mutate exported tables. +Klee evaluates the chunk in an isolated Lua environment. Preloaded Lua +libraries and caller-provided input variables remain visible, but globals +assigned by the chunk do not leak into the input deck unless they are returned +in the export table. Exported functions retain access to the chunk's private +environment. This name isolation is not a security sandbox; applications +should execute only trusted Lua bindings code. + Use :code:`local` helper functions and constants for intermediate values so the global namespace contains only the Klee schema fields that Inlet should read. For Lua input, a one-value scale is written as a one-entry table, for example diff --git a/src/axom/klee/io/IO.cpp b/src/axom/klee/io/IO.cpp index e830957c91..75fa25a7ca 100644 --- a/src/axom/klee/io/IO.cpp +++ b/src/axom/klee/io/IO.cpp @@ -114,7 +114,12 @@ class KleeLuaReader : public inlet::LuaReader try { - auto result = lua->script(bindings.source); + // Evaluate bindings in their own environment so assignments made by the + // chunk do not mutate the input file's globals. The fallback keeps + // preloaded libraries and caller-provided input variables visible. + axom::sol::environment bindingsEnvironment {*lua, axom::sol::create, lua->globals()}; + bindingsEnvironment["_G"] = bindingsEnvironment; + auto result = lua->script(bindings.source, bindingsEnvironment); if(!result.valid()) { axom::sol::error err = result; diff --git a/src/axom/klee/tests/klee_io.cpp b/src/axom/klee/tests/klee_io.cpp index 1c4c11b264..cd7a6cdfb9 100644 --- a/src/axom/klee/tests/klee_io.cpp +++ b/src/axom/klee/tests/klee_io.cpp @@ -798,6 +798,114 @@ TEST(IOTest, readShapeSet_luaBindingsChunkAndInputVariables) EXPECT_THAT(translation->getOffset(), AlmostEqVector(Vector3D {1.0, 3.0, 0.0})); } +TEST(IOTest, readShapeSet_luaBindingsChunkIsolatesUnexportedGlobals) +{ + LuaBindingsChunk bindings {R"( + dimensions = 3 + unexported_value = "bindings" + math = { + sqrt = function() return -1 end + } + _G.also_unexported = "bindings" + + return { + exported_lift = 4.0 + } + )", + "runtime_bindings"}; + + auto shapeSet = readShapeSetFromString(R"( + dimensions = 2 + local isolation_ok = + unexported_value == nil and + also_unexported == nil and + math.sqrt(9.0) == 3.0 + + shapes = { + { + name = "isolated", + material = "steel", + geometry = { + format = "stl", + path = isolation_ok and "isolated.stl" or "leaked.stl", + units = "cm", + operators = { + { translate = {math.sqrt(4.0), exported_lift} } + } + } + } + } + )", + InputFormat::Lua, + bindings); + + ASSERT_EQ(Dimensions::Two, shapeSet.getDimensions()); + ASSERT_EQ(1u, shapeSet.getShapes().size()); + const auto& geometry = shapeSet.getShapes()[0].getGeometry(); + EXPECT_EQ("isolated.stl", geometry.getPath()); + auto composite = std::dynamic_pointer_cast(geometry.getGeometryOperator()); + ASSERT_TRUE(composite); + auto translation = std::dynamic_pointer_cast(composite->getOperators()[0]); + ASSERT_TRUE(translation); + EXPECT_THAT(translation->getOffset(), AlmostEqVector(Vector3D {2.0, 4.0, 0.0})); +} + +TEST(IOTest, readShapeSet_luaBindingsChunkCannotSetSchemaGlobalsWithoutExporting) +{ + for(const std::string& source : + {"dimensions = 2; return {}", "_G.dimensions = 2; return {}"}) + { + LuaBindingsChunk bindings {source, "runtime_bindings"}; + EXPECT_THROW(readShapeSetFromString("shapes = {}", InputFormat::Lua, bindings), KleeError); + } +} + +TEST(IOTest, readShapeSet_luaBindingsClosureRetainsIsolatedEnvironment) +{ + InputVariables variables { + {"dimensions", klee::InputVariableValue {2}}, + {"base_offset", klee::InputVariableValue {1.5}}, + }; + LuaBindingsChunk bindings {R"( + private_offset = 3.5 + + return { + offset = function() + return {base_offset, private_offset} + end + } + )", + "runtime_bindings"}; + + auto shapeSet = readShapeSetFromString(R"( + shapes = { + { + name = "closure", + material = "steel", + geometry = { + format = "stl", + path = "closure.stl", + units = "cm", + operators = { + { translate = offset } + } + } + } + } + )", + InputFormat::Lua, + variables, + bindings); + + ASSERT_EQ(1u, shapeSet.getShapes().size()); + const auto& geometry = shapeSet.getShapes()[0].getGeometry(); + auto composite = std::dynamic_pointer_cast(geometry.getGeometryOperator()); + ASSERT_TRUE(composite); + auto translation = std::dynamic_pointer_cast(composite->getOperators()[0]); + ASSERT_TRUE(translation); + EXPECT_THAT(translation->getOffset(), AlmostEqVector(Vector3D {1.5, 3.5, 0.0})); +} + TEST(IOTest, readShapeSet_luaInputVariableRejectsInvalidName) { try From 016c46cf949cc403b748630109a6992499ae6ad4 Mon Sep 17 00:00:00 2001 From: Kenneth Weiss Date: Wed, 29 Jul 2026 12:16:13 -0700 Subject: [PATCH 12/52] Klee: Streamlines variables/bindings for Lua input into single struct This avoids extra calls for variables vs. tabular Lua input. --- .../klee/docs/sphinx/specifying_shapes.rst | 10 +- src/axom/klee/io/IO.cpp | 93 +++--------- src/axom/klee/io/IO.hpp | 89 ++++------- src/axom/klee/tests/klee_io.cpp | 141 ++++++++++-------- .../klee_operators_and_validation.cpp | 5 +- 5 files changed, 139 insertions(+), 199 deletions(-) diff --git a/src/axom/klee/docs/sphinx/specifying_shapes.rst b/src/axom/klee/docs/sphinx/specifying_shapes.rst index a1276f724a..bc18e945b3 100644 --- a/src/axom/klee/docs/sphinx/specifying_shapes.rst +++ b/src/axom/klee/docs/sphinx/specifying_shapes.rst @@ -129,11 +129,12 @@ dimensions, or operator values at run time: .. code-block:: c++ - axom::klee::InputVariables vars { + axom::klee::LuaInputOptions options; + options.variables = { {"dimensions", axom::klee::InputVariableValue {2}}, {"shape_suffix", axom::klee::InputVariableValue {std::string {"2d"}}} }; - auto shapeSet = axom::klee::readShapeSet("shape.lua", vars); + auto shapeSet = axom::klee::readShapeSet("shape.lua", options); .. code-block:: lua @@ -172,7 +173,8 @@ at run time without recompiling the C++ application: .. code-block:: c++ - axom::klee::LuaBindingsChunk bindings { + axom::klee::LuaInputOptions options; + options.bindings = axom::klee::LuaBindingsChunk { R"( local dim = 2 local lift = 3.0 @@ -191,7 +193,7 @@ at run time without recompiling the C++ application: )", "runtime_bindings" }; - auto shapeSet = axom::klee::readShapeSet("shape.lua", bindings); + auto shapeSet = axom::klee::readShapeSet("shape.lua", options); .. code-block:: lua diff --git a/src/axom/klee/io/IO.cpp b/src/axom/klee/io/IO.cpp index 75fa25a7ca..049b441717 100644 --- a/src/axom/klee/io/IO.cpp +++ b/src/axom/klee/io/IO.cpp @@ -599,23 +599,21 @@ void validateInputVariables(const InputVariables &variables) * Create an Inlet reader for a Klee input format. * * \param format the input file format to read - * \param variables primitive values to inject before Lua input evaluation - * \param bindings optional Lua chunk to evaluate before input evaluation + * \param options optional variables and bindings for Lua input evaluation * \param allowedGlobals receives external names permitted in the input * \return a reader for \a format * \throws KleeError if \a format is unsupported, Lua support was not enabled, * or the external Lua bindings are invalid for the selected format */ std::unique_ptr createReader(InputFormat format, - const InputVariables &variables, - const LuaBindingsChunk *bindings, + const LuaInputOptions &options, std::unordered_set &allowedGlobals) { allowedGlobals.clear(); - if(format != InputFormat::Lua && (!variables.empty() || bindings != nullptr)) + if(format != InputFormat::Lua && (!options.variables.empty() || options.bindings)) { throw KleeError({Path {""}, - bindings != nullptr + options.bindings ? "Klee Lua bindings are only supported for Lua input decks." : "Klee input variables are only supported for Lua input decks."}); } @@ -629,11 +627,11 @@ std::unique_ptr createReader(InputFormat format, { auto reader = std::make_unique(); const auto reservedGlobals = reader->topLevelGlobalNames(); - validateInputVariables(variables); + validateInputVariables(options.variables); // External inputs are ordinary Lua globals installed before deck parsing. // allowedGlobals only prevents Klee's unexpected-global check from rejecting // those names; it does not make them read-only inside the deck. - for(const auto &entry : variables) + for(const auto &entry : options.variables) { if(reservedGlobals.find(entry.first) != reservedGlobals.end()) { @@ -646,9 +644,10 @@ std::unique_ptr createReader(InputFormat format, reader->setInputVariable(entry.first, entry.second); allowedGlobals.insert(entry.first); } - if(bindings != nullptr) + if(options.bindings) { - auto exportedNames = reader->applyBindingsChunk(*bindings, reservedGlobals, allowedGlobals); + auto exportedNames = + reader->applyBindingsChunk(*options.bindings, reservedGlobals, allowedGlobals); allowedGlobals.insert(exportedNames.begin(), exportedNames.end()); } return reader; @@ -782,43 +781,17 @@ ShapeSet readShapeSet(std::istream& stream) { return readShapeSet(stream, InputF ShapeSet readShapeSet(std::istream& stream, InputFormat format) { - return readShapeSet(stream, format, InputVariables {}); + return readShapeSet(stream, format, LuaInputOptions {}); } ShapeSet readShapeSet(std::istream &stream, InputFormat format, - const InputVariables &variables) + const LuaInputOptions &options) { std::string contents {std::istreambuf_iterator(stream), {}}; std::unordered_set allowedGlobals; - auto reader = createReader(format, variables, nullptr, allowedGlobals); - parseOrThrow([&]() { return reader->parseString(contents); }, - format, - Path {""}, - "from stream"); - return readShapeSetFromReader(std::move(reader), - format == InputFormat::Lua, - format == InputFormat::Lua, - allowedGlobals); -} - -ShapeSet readShapeSet(std::istream &stream, - InputFormat format, - const LuaBindingsChunk &bindings) -{ - return readShapeSet(stream, format, InputVariables {}, bindings); -} - -ShapeSet readShapeSet(std::istream &stream, - InputFormat format, - const InputVariables &variables, - const LuaBindingsChunk &bindings) -{ - std::string contents {std::istreambuf_iterator(stream), {}}; - - std::unordered_set allowedGlobals; - auto reader = createReader(format, variables, &bindings, allowedGlobals); + auto reader = createReader(format, options, allowedGlobals); parseOrThrow([&]() { return reader->parseString(contents); }, format, Path {""}, @@ -836,50 +809,20 @@ ShapeSet readShapeSet(const std::string& filePath) ShapeSet readShapeSet(const std::string& filePath, InputFormat format) { - const InputVariables variables; - std::unordered_set allowedGlobals; - auto reader = createReader(format, variables, nullptr, allowedGlobals); - parseOrThrow([&]() { return reader->parseFile(filePath); }, - format, - Path {filePath}, - axom::fmt::format("from file '{}'", filePath)); - auto shapeSet = readShapeSetFromReader(std::move(reader), - format == InputFormat::Lua, - format == InputFormat::Lua, - allowedGlobals); - shapeSet.setPath(filePath); - return shapeSet; + return readShapeSet(filePath, format, LuaInputOptions {}); } -ShapeSet readShapeSet(const std::string &filePath, const InputVariables &variables) +ShapeSet readShapeSet(const std::string &filePath, const LuaInputOptions &options) { - const auto format = inferInputFormat(filePath); - std::unordered_set allowedGlobals; - auto reader = createReader(format, variables, nullptr, allowedGlobals); - parseOrThrow([&]() { return reader->parseFile(filePath); }, - format, - Path {filePath}, - axom::fmt::format("from file '{}'", filePath)); - auto shapeSet = readShapeSetFromReader(std::move(reader), - format == InputFormat::Lua, - format == InputFormat::Lua, - allowedGlobals); - shapeSet.setPath(filePath); - return shapeSet; -} - -ShapeSet readShapeSet(const std::string &filePath, const LuaBindingsChunk &bindings) -{ - return readShapeSet(filePath, InputVariables {}, bindings); + return readShapeSet(filePath, inferInputFormat(filePath), options); } ShapeSet readShapeSet(const std::string &filePath, - const InputVariables &variables, - const LuaBindingsChunk &bindings) + InputFormat format, + const LuaInputOptions &options) { - const auto format = inferInputFormat(filePath); std::unordered_set allowedGlobals; - auto reader = createReader(format, variables, &bindings, allowedGlobals); + auto reader = createReader(format, options, allowedGlobals); parseOrThrow([&]() { return reader->parseFile(filePath); }, format, Path {filePath}, diff --git a/src/axom/klee/io/IO.hpp b/src/axom/klee/io/IO.hpp index 6995a682de..6eac2b9a80 100644 --- a/src/axom/klee/io/IO.hpp +++ b/src/axom/klee/io/IO.hpp @@ -10,6 +10,7 @@ #include #include +#include #include #include @@ -37,6 +38,16 @@ using InputVariableValue = std::variant; /// Variables to set as ordinary mutable globals before a Lua input deck is evaluated. using InputVariables = std::unordered_map; +/// Optional caller-provided values and bindings for a Lua input deck. +struct LuaInputOptions +{ + /// Primitive values to install as initial mutable Lua globals. + InputVariables variables; + + /// Chunk to evaluate before the input deck; returned entries become mutable Lua globals. + std::optional bindings; +}; + /** * Read a ShapeSet from an input stream. * @@ -57,43 +68,17 @@ ShapeSet readShapeSet(std::istream& stream); ShapeSet readShapeSet(std::istream& stream, InputFormat format); /** - * Read a ShapeSet from an input stream with caller-provided input variables. - * - * \param stream the stream from which to read the ShapeSet - * \param format the input deck format to use - * \param variables primitive values to set as initial mutable Lua globals - * \note Input variables are supported only for Lua input decks. - * \throws runtime_error if the input is invalid - */ -ShapeSet readShapeSet(std::istream &stream, InputFormat format, const InputVariables &variables); - -/** - * Read a ShapeSet from an input stream with caller-provided Lua bindings. - * - * \param stream the stream from which to read the ShapeSet - * \param format the input deck format to use - * \param bindings Lua chunk evaluated before deck parsing; must return a table - * of exported bindings, which become initial mutable Lua globals - * \note Lua bindings are supported only for Lua input decks. - * \throws runtime_error if the input is invalid - */ -ShapeSet readShapeSet(std::istream &stream, InputFormat format, const LuaBindingsChunk &bindings); - -/** - * Read a ShapeSet from an input stream with caller-provided input variables and Lua bindings. + * Read a ShapeSet from an input stream with caller-provided Lua inputs. * * \param stream the stream from which to read the ShapeSet * \param format the input deck format to use - * \param variables primitive values to set as initial mutable Lua globals - * \param bindings Lua chunk evaluated before deck parsing; must return a table - * of exported bindings, which become initial mutable Lua globals - * \note Input variables and Lua bindings are supported only for Lua input decks. - * \throws runtime_error if the input is invalid + * \param options optional variables and bindings for a Lua input deck + * \note Non-empty Lua input options are supported only for Lua input decks. + * \throws KleeError if the input or Lua input options are invalid */ ShapeSet readShapeSet(std::istream &stream, InputFormat format, - const InputVariables &variables, - const LuaBindingsChunk &bindings); + const LuaInputOptions &options); /** * Read a ShapeSet from a specified file @@ -119,45 +104,31 @@ ShapeSet readShapeSet(const std::string& filePath); ShapeSet readShapeSet(const std::string& filePath, InputFormat format); /** - * Read a ShapeSet from a specified file with caller-provided input variables. - * - * \param filePath the file from which to read the ShapeSet - * \param variables primitive values to set as initial mutable Lua globals - * \note The input format is inferred from the file extension. Input variables - * are supported only for Lua input decks. - * \return the ShapeSet read from the file - * \throws runtime_error if the input is invalid - */ -ShapeSet readShapeSet(const std::string &filePath, const InputVariables &variables); - -/** - * Read a ShapeSet from a specified file with caller-provided Lua bindings. + * Read a ShapeSet from a specified file with caller-provided Lua inputs. * * \param filePath the file from which to read the ShapeSet - * \param bindings Lua chunk evaluated before deck parsing; must return a table - * of exported bindings, which become initial mutable Lua globals - * \note The input format is inferred from the file extension. Lua bindings are - * supported only for Lua input decks. + * \param options optional variables and bindings for a Lua input deck + * \note The input format is inferred from the file extension. Non-empty Lua + * input options are supported only for Lua input decks. * \return the ShapeSet read from the file - * \throws runtime_error if the input is invalid + * \throws KleeError if the input or Lua input options are invalid */ -ShapeSet readShapeSet(const std::string &filePath, const LuaBindingsChunk &bindings); +ShapeSet readShapeSet(const std::string &filePath, const LuaInputOptions &options); /** - * Read a ShapeSet from a specified file with caller-provided input variables and Lua bindings. + * Read a ShapeSet from a specified file using an explicit format and + * caller-provided Lua inputs. * * \param filePath the file from which to read the ShapeSet - * \param variables primitive values to set as initial mutable Lua globals - * \param bindings Lua chunk evaluated before deck parsing; must return a table - * of exported bindings, which become initial mutable Lua globals - * \note The input format is inferred from the file extension. Input variables - * and Lua bindings are supported only for Lua input decks. + * \param format the input file format to use, regardless of the file extension + * \param options optional variables and bindings for a Lua input deck + * \note Non-empty Lua input options are supported only for Lua input decks. * \return the ShapeSet read from the file - * \throws runtime_error if the input is invalid + * \throws KleeError if the input or Lua input options are invalid */ ShapeSet readShapeSet(const std::string &filePath, - const InputVariables &variables, - const LuaBindingsChunk &bindings); + InputFormat format, + const LuaInputOptions &options); } // namespace klee } // namespace axom diff --git a/src/axom/klee/tests/klee_io.cpp b/src/axom/klee/tests/klee_io.cpp index cd7a6cdfb9..873341ce76 100644 --- a/src/axom/klee/tests/klee_io.cpp +++ b/src/axom/klee/tests/klee_io.cpp @@ -31,6 +31,7 @@ using klee::InputVariables; using klee::KleeError; using klee::LengthUnit; using klee::LuaBindingsChunk; +using klee::LuaInputOptions; using klee::Rotation; using klee::Scale; using klee::ShapeSet; @@ -61,27 +62,10 @@ ShapeSet readShapeSetFromString(const std::string& input, InputFormat format) ShapeSet readShapeSetFromString(const std::string& input, InputFormat format, - const InputVariables& variables) + const LuaInputOptions& options) { std::istringstream istream(input); - return klee::readShapeSet(istream, format, variables); -} - -ShapeSet readShapeSetFromString(const std::string& input, - InputFormat format, - const LuaBindingsChunk& bindings) -{ - std::istringstream istream(input); - return klee::readShapeSet(istream, format, bindings); -} - -ShapeSet readShapeSetFromString(const std::string& input, - InputFormat format, - const InputVariables& variables, - const LuaBindingsChunk& bindings) -{ - std::istringstream istream(input); - return klee::readShapeSet(istream, format, variables, bindings); + return klee::readShapeSet(istream, format, options); } } // end namespace @@ -452,6 +436,9 @@ TEST(IOTest, readShapeSet_streamDefaultsToYaml) TEST(IOTest, readShapeSet_yamlRejectsInputVariables) { + LuaInputOptions options; + options.variables = {{"dimensions", klee::InputVariableValue {2}}}; + try { readShapeSetFromString(R"( @@ -459,7 +446,7 @@ TEST(IOTest, readShapeSet_yamlRejectsInputVariables) shapes: [] )", InputFormat::YAML, - {{"dimensions", klee::InputVariableValue {2}}}); + options); FAIL() << "Should have thrown"; } catch(const KleeError& err) @@ -471,6 +458,14 @@ TEST(IOTest, readShapeSet_yamlRejectsInputVariables) TEST(IOTest, readShapeSet_yamlRejectsLuaBindings) { + LuaInputOptions options; + options.bindings = LuaBindingsChunk {R"( + return { + dimensions = 2 + } + )", + "runtime_bindings"}; + try { readShapeSetFromString(R"( @@ -478,12 +473,7 @@ TEST(IOTest, readShapeSet_yamlRejectsLuaBindings) shapes: [] )", InputFormat::YAML, - LuaBindingsChunk {R"( - return { - dimensions = 2 - } - )", - "runtime_bindings"}); + options); FAIL() << "Should have thrown"; } catch(const KleeError& err) @@ -520,10 +510,11 @@ TEST(IOTest, readShapeSet_explicitLuaOverridesFileExtension) { axom::utilities::filesystem::TempFile input {"explicitLua", "yaml"}; input.write(R"( - dimensions = 2 shapes = {})"); - auto shapeSet = klee::readShapeSet(input.getPath(), InputFormat::Lua); + LuaInputOptions options; + options.variables = {{"dimensions", klee::InputVariableValue {2}}}; + auto shapeSet = klee::readShapeSet(input.getPath(), InputFormat::Lua, options); EXPECT_EQ(Dimensions::Two, shapeSet.getDimensions()); EXPECT_EQ(input.getPath(), shapeSet.getPath()); } @@ -576,6 +567,8 @@ TEST(IOTest, readShapeSet_luaInputVariablesProvideInitialDimensionAndOperator) {"shape_suffix", klee::InputVariableValue {std::string {"2d"}}}, {"lift", klee::InputVariableValue {3.0}}, }; + LuaInputOptions options; + options.variables = variables; auto shapeSet = readShapeSetFromString(R"( local function shape_path() @@ -598,7 +591,7 @@ TEST(IOTest, readShapeSet_luaInputVariablesProvideInitialDimensionAndOperator) } )", InputFormat::Lua, - variables); + options); ASSERT_EQ(Dimensions::Two, shapeSet.getDimensions()); ASSERT_EQ(1u, shapeSet.getShapes().size()); @@ -618,6 +611,8 @@ TEST(IOTest, readShapeSet_luaInputVariablesAreInitialMutableGlobals) {"dimensions", klee::InputVariableValue {2}}, {"lift", klee::InputVariableValue {3.0}}, }; + LuaInputOptions options; + options.variables = variables; auto shapeSet = readShapeSetFromString(R"( dimensions = 3 @@ -639,7 +634,7 @@ TEST(IOTest, readShapeSet_luaInputVariablesAreInitialMutableGlobals) } )", InputFormat::Lua, - variables); + options); ASSERT_EQ(Dimensions::Three, shapeSet.getDimensions()); ASSERT_EQ(1u, shapeSet.getShapes().size()); @@ -665,6 +660,8 @@ TEST(IOTest, readShapeSet_luaBindingsChunkProvidesInitialDimensionAndOperator) } )", "runtime_bindings"}; + LuaInputOptions options; + options.bindings = bindings; auto shapeSet = readShapeSetFromString(R"( local function shape_path() @@ -687,7 +684,7 @@ TEST(IOTest, readShapeSet_luaBindingsChunkProvidesInitialDimensionAndOperator) } )", InputFormat::Lua, - bindings); + options); ASSERT_EQ(Dimensions::Two, shapeSet.getDimensions()); ASSERT_EQ(1u, shapeSet.getShapes().size()); @@ -712,6 +709,8 @@ TEST(IOTest, readShapeSet_luaBindingsChunkProvidesInitialMutableGlobals) } )", "runtime_bindings"}; + LuaInputOptions options; + options.bindings = bindings; auto shapeSet = readShapeSetFromString(R"( dimensions = 3 @@ -733,7 +732,7 @@ TEST(IOTest, readShapeSet_luaBindingsChunkProvidesInitialMutableGlobals) } )", InputFormat::Lua, - bindings); + options); ASSERT_EQ(Dimensions::Three, shapeSet.getDimensions()); ASSERT_EQ(1u, shapeSet.getShapes().size()); @@ -761,6 +760,9 @@ TEST(IOTest, readShapeSet_luaBindingsChunkAndInputVariables) {"dimensions", klee::InputVariableValue {2}}, {"shape_suffix", klee::InputVariableValue {std::string {"2d"}}}, }; + LuaInputOptions options; + options.variables = variables; + options.bindings = bindings; auto shapeSet = readShapeSetFromString(R"( local function shape_path() @@ -783,8 +785,7 @@ TEST(IOTest, readShapeSet_luaBindingsChunkAndInputVariables) } )", InputFormat::Lua, - variables, - bindings); + options); ASSERT_EQ(Dimensions::Two, shapeSet.getDimensions()); ASSERT_EQ(1u, shapeSet.getShapes().size()); @@ -813,6 +814,8 @@ TEST(IOTest, readShapeSet_luaBindingsChunkIsolatesUnexportedGlobals) } )", "runtime_bindings"}; + LuaInputOptions options; + options.bindings = bindings; auto shapeSet = readShapeSetFromString(R"( dimensions = 2 @@ -837,7 +840,7 @@ TEST(IOTest, readShapeSet_luaBindingsChunkIsolatesUnexportedGlobals) } )", InputFormat::Lua, - bindings); + options); ASSERT_EQ(Dimensions::Two, shapeSet.getDimensions()); ASSERT_EQ(1u, shapeSet.getShapes().size()); @@ -856,7 +859,10 @@ TEST(IOTest, readShapeSet_luaBindingsChunkCannotSetSchemaGlobalsWithoutExporting {"dimensions = 2; return {}", "_G.dimensions = 2; return {}"}) { LuaBindingsChunk bindings {source, "runtime_bindings"}; - EXPECT_THROW(readShapeSetFromString("shapes = {}", InputFormat::Lua, bindings), KleeError); + LuaInputOptions options; + options.bindings = bindings; + EXPECT_THROW(readShapeSetFromString("shapes = {}", InputFormat::Lua, options), + KleeError); } } @@ -876,6 +882,9 @@ TEST(IOTest, readShapeSet_luaBindingsClosureRetainsIsolatedEnvironment) } )", "runtime_bindings"}; + LuaInputOptions options; + options.variables = variables; + options.bindings = bindings; auto shapeSet = readShapeSetFromString(R"( shapes = { @@ -894,8 +903,7 @@ TEST(IOTest, readShapeSet_luaBindingsClosureRetainsIsolatedEnvironment) } )", InputFormat::Lua, - variables, - bindings); + options); ASSERT_EQ(1u, shapeSet.getShapes().size()); const auto& geometry = shapeSet.getShapes()[0].getGeometry(); @@ -908,6 +916,9 @@ TEST(IOTest, readShapeSet_luaBindingsClosureRetainsIsolatedEnvironment) TEST(IOTest, readShapeSet_luaInputVariableRejectsInvalidName) { + LuaInputOptions options; + options.variables = {{"shape-dim", klee::InputVariableValue {2}}}; + try { readShapeSetFromString(R"( @@ -915,7 +926,7 @@ TEST(IOTest, readShapeSet_luaInputVariableRejectsInvalidName) shapes = {} )", InputFormat::Lua, - {{"shape-dim", klee::InputVariableValue {2}}}); + options); FAIL() << "Should have thrown"; } catch(const KleeError& err) @@ -927,18 +938,21 @@ TEST(IOTest, readShapeSet_luaInputVariableRejectsInvalidName) TEST(IOTest, readShapeSet_luaBindingsChunkRejectsInvalidExportName) { + LuaInputOptions options; + options.bindings = LuaBindingsChunk {R"( + return { + ["shape-dim"] = 2 + } + )", + "runtime_bindings"}; + try { readShapeSetFromString(R"( shapes = {} )", InputFormat::Lua, - LuaBindingsChunk {R"( - return { - ["shape-dim"] = 2 - } - )", - "runtime_bindings"}); + options); FAIL() << "Should have thrown"; } catch(const KleeError& err) @@ -950,18 +964,21 @@ TEST(IOTest, readShapeSet_luaBindingsChunkRejectsInvalidExportName) TEST(IOTest, readShapeSet_luaBindingsChunkRejectsReservedGlobalName) { + LuaInputOptions options; + options.bindings = LuaBindingsChunk {R"( + return { + math = 2 + } + )", + "runtime_bindings"}; + try { readShapeSetFromString(R"( shapes = {} )", InputFormat::Lua, - LuaBindingsChunk {R"( - return { - math = 2 - } - )", - "runtime_bindings"}); + options); FAIL() << "Should have thrown"; } catch(const KleeError& err) @@ -973,19 +990,22 @@ TEST(IOTest, readShapeSet_luaBindingsChunkRejectsReservedGlobalName) TEST(IOTest, readShapeSet_luaBindingsChunkRejectsDuplicateInputVariableName) { + LuaInputOptions options; + options.variables = {{"dimensions", klee::InputVariableValue {2}}}; + options.bindings = LuaBindingsChunk {R"( + return { + dimensions = 3 + } + )", + "runtime_bindings"}; + try { readShapeSetFromString(R"( shapes = {} )", InputFormat::Lua, - {{"dimensions", klee::InputVariableValue {2}}}, - LuaBindingsChunk {R"( - return { - dimensions = 3 - } - )", - "runtime_bindings"}); + options); FAIL() << "Should have thrown"; } catch(const KleeError& err) @@ -997,6 +1017,9 @@ TEST(IOTest, readShapeSet_luaBindingsChunkRejectsDuplicateInputVariableName) TEST(IOTest, readShapeSet_luaBindingsChunkRequiresTableReturn) { + LuaInputOptions options; + options.bindings = LuaBindingsChunk {"return 2", "runtime_bindings"}; + try { readShapeSetFromString(R"( @@ -1004,7 +1027,7 @@ TEST(IOTest, readShapeSet_luaBindingsChunkRequiresTableReturn) shapes = {} )", InputFormat::Lua, - LuaBindingsChunk {"return 2", "runtime_bindings"}); + options); FAIL() << "Should have thrown"; } catch(const KleeError& err) diff --git a/src/examples/shaping_tutorial/lesson_03/klee_operators_and_validation.cpp b/src/examples/shaping_tutorial/lesson_03/klee_operators_and_validation.cpp index 7565ec8e00..d950e11bd9 100644 --- a/src/examples/shaping_tutorial/lesson_03/klee_operators_and_validation.cpp +++ b/src/examples/shaping_tutorial/lesson_03/klee_operators_and_validation.cpp @@ -161,8 +161,9 @@ int main(int argc, char** argv) std::ifstream bindingsStream {bindingsFilename}; std::string bindingsSource {std::istreambuf_iterator(bindingsStream), {}}; - axom::klee::LuaBindingsChunk bindings {bindingsSource, bindingsFilename}; - return axom::klee::readShapeSet(inputFilename, bindings); + axom::klee::LuaInputOptions options; + options.bindings = axom::klee::LuaBindingsChunk {bindingsSource, bindingsFilename}; + return axom::klee::readShapeSet(inputFilename, options); }; // Load the klee shape file and extract some information From 404fbf3680ab8daf64f6bc4a50ff981ea8614762 Mon Sep 17 00:00:00 2001 From: Kenneth Weiss Date: Wed, 29 Jul 2026 13:58:01 -0700 Subject: [PATCH 13/52] Klee: Improved validation of user input -- checks for lua keywords --- .../klee/docs/sphinx/specifying_shapes.rst | 4 +- src/axom/klee/io/IO.cpp | 37 ++++++++++++------ src/axom/klee/tests/klee_io.cpp | 39 +++++++++++++++++++ 3 files changed, 67 insertions(+), 13 deletions(-) diff --git a/src/axom/klee/docs/sphinx/specifying_shapes.rst b/src/axom/klee/docs/sphinx/specifying_shapes.rst index bc18e945b3..b2c99e89bb 100644 --- a/src/axom/klee/docs/sphinx/specifying_shapes.rst +++ b/src/axom/klee/docs/sphinx/specifying_shapes.rst @@ -158,7 +158,7 @@ dimensions, or operator values at run time: } Input variables are Lua-only and may be booleans, integers, doubles, or strings. -Their names must be Lua identifiers. They are ordinary mutable globals by +Their names must be non-keyword Lua identifiers. They are ordinary mutable globals by construction and are allowed by Klee's unexpected-global check. Deck code can reassign these names, so applications should treat them as initial values rather than read-only controls. Other helper values in the deck should still be declared @@ -212,7 +212,7 @@ at run time without recompiling the C++ application: } } -Bindings chunks must return a table whose exported keys are Lua identifiers. +Bindings chunks must return a table whose exported keys are non-keyword Lua identifiers. Exported values may be booleans, numbers, strings, tables, or functions. Like input variables, exported bindings are ordinary Lua globals; deck code can reassign exported names and can mutate exported tables. diff --git a/src/axom/klee/io/IO.cpp b/src/axom/klee/io/IO.cpp index 049b441717..7fa14f0231 100644 --- a/src/axom/klee/io/IO.cpp +++ b/src/axom/klee/io/IO.cpp @@ -37,6 +37,7 @@ namespace klee { namespace { +bool isLuaKeyword(const std::string &name); bool isLuaIdentifier(const std::string &name); #ifdef AXOM_USE_LUA @@ -175,11 +176,11 @@ class KleeLuaReader : public inlet::LuaReader const std::string name = entry.first.as(); if(!isLuaIdentifier(name)) { - throw KleeError( - {exportPath(name), - axom::fmt::format("Invalid Klee Lua binding name '{}'. Binding names must be Lua " - "identifiers.", - name)}); + const auto reason = isLuaKeyword(name) + ? "Reserved Lua keywords cannot be used as binding names." + : "Binding names must be Lua identifiers."; + throw KleeError({exportPath(name), + axom::fmt::format("Invalid Klee Lua binding name '{}'. {}", name, reason)}); } if(reservedNames.find(name) != reservedNames.end()) { @@ -562,6 +563,17 @@ InputFormat inferInputFormat(const std::string& filePath) extension)}); } +bool isLuaKeyword(const std::string &name) +{ + static const std::unordered_set keywords { + "and", "break", "do", "else", "elseif", "end", "false", + "for", "function", "goto", "if", "in", "local", "nil", + "not", "or", "repeat", "return", "then", "true", "until", + "while", + }; + return keywords.find(name) != keywords.end(); +} + bool isLuaIdentifier(const std::string &name) { if(name.empty()) @@ -577,8 +589,9 @@ bool isLuaIdentifier(const std::string &name) return false; } return std::all_of(name.begin() + 1, name.end(), [&](char ch) { - return isNameChar(static_cast(ch)); - }); + return isNameChar(static_cast(ch)); + }) && + !isLuaKeyword(name); } void validateInputVariables(const InputVariables &variables) @@ -587,10 +600,12 @@ void validateInputVariables(const InputVariables &variables) { if(!isLuaIdentifier(entry.first)) { - throw KleeError({Path {entry.first.empty() ? "" : entry.first}, - axom::fmt::format("Invalid Klee Lua input variable name '{}'. Input " - "variable names must be Lua identifiers.", - entry.first)}); + const auto reason = isLuaKeyword(entry.first) + ? "Reserved Lua keywords cannot be used as input variable names." + : "Input variable names must be Lua identifiers."; + throw KleeError( + {Path {entry.first.empty() ? "" : entry.first}, + axom::fmt::format("Invalid Klee Lua input variable name '{}'. {}", entry.first, reason)}); } } } diff --git a/src/axom/klee/tests/klee_io.cpp b/src/axom/klee/tests/klee_io.cpp index 873341ce76..e75f5c5e35 100644 --- a/src/axom/klee/tests/klee_io.cpp +++ b/src/axom/klee/tests/klee_io.cpp @@ -936,6 +936,23 @@ TEST(IOTest, readShapeSet_luaInputVariableRejectsInvalidName) } } +TEST(IOTest, readShapeSet_luaInputVariableRejectsKeyword) +{ + LuaInputOptions options; + options.variables = {{"end", klee::InputVariableValue {2}}}; + + try + { + readShapeSetFromString("dimensions = 2; shapes = {}", InputFormat::Lua, options); + FAIL() << "Should have thrown"; + } + catch(const KleeError& err) + { + EXPECT_THAT(err.what(), HasSubstr("Reserved Lua keywords")); + EXPECT_THAT(err.what(), HasSubstr("end")); + } +} + TEST(IOTest, readShapeSet_luaBindingsChunkRejectsInvalidExportName) { LuaInputOptions options; @@ -962,6 +979,28 @@ TEST(IOTest, readShapeSet_luaBindingsChunkRejectsInvalidExportName) } } +TEST(IOTest, readShapeSet_luaBindingsChunkRejectsKeywordExport) +{ + LuaInputOptions options; + options.bindings = LuaBindingsChunk {R"( + return { + ["function"] = 2 + } + )", + "runtime_bindings"}; + + try + { + readShapeSetFromString("dimensions = 2; shapes = {}", InputFormat::Lua, options); + FAIL() << "Should have thrown"; + } + catch(const KleeError& err) + { + EXPECT_THAT(err.what(), HasSubstr("Reserved Lua keywords")); + EXPECT_THAT(err.what(), HasSubstr("function")); + } +} + TEST(IOTest, readShapeSet_luaBindingsChunkRejectsReservedGlobalName) { LuaInputOptions options; From 0f81df2fae956b41ed456e7b36aec75ee2e95e9c Mon Sep 17 00:00:00 2001 From: Kenneth Weiss Date: Wed, 29 Jul 2026 15:36:39 -0700 Subject: [PATCH 14/52] Reworks callback registration in Inlet/Klee We have to explicitly register which paths can contain callbacks with inlet. --- src/axom/inlet/LuaReader.cpp | 46 +++++++++++++++------- src/axom/inlet/LuaReader.hpp | 23 +++-------- src/axom/inlet/tests/inlet_Reader.cpp | 23 +++++++++++ src/axom/klee/io/GeometryOperatorsIO.cpp | 49 ++++++++++++------------ src/axom/klee/io/IO.cpp | 43 --------------------- 5 files changed, 85 insertions(+), 99 deletions(-) diff --git a/src/axom/inlet/LuaReader.cpp b/src/axom/inlet/LuaReader.cpp index 46687787c1..fe25d96e68 100644 --- a/src/axom/inlet/LuaReader.cpp +++ b/src/axom/inlet/LuaReader.cpp @@ -217,6 +217,7 @@ LuaReader::LuaReader() bool LuaReader::parseFile(const std::string& filePath) { + m_function_paths.clear(); if(!axom::utilities::filesystem::pathExists(filePath)) { SLIC_WARNING(fmt::format("Inlet: Given Lua input file does not exist: {0}", filePath)); @@ -233,6 +234,7 @@ bool LuaReader::parseFile(const std::string& filePath) bool LuaReader::parseString(const std::string& luaString) { + m_function_paths.clear(); if(luaString.empty()) { SLIC_WARNING("Inlet: Given an empty Lua string to parse."); @@ -242,8 +244,6 @@ bool LuaReader::parseString(const std::string& luaString) return true; } -bool LuaReader::shouldTreatFunctionAsNotFound(const std::string&) const { return false; } - // TODO allow alternate delimiter at sidre level #define SCOPE_DELIMITER '/' @@ -588,19 +588,33 @@ FunctionVariant LuaReader::getFunction(const std::string& id, auto lua_func = getFunctionInternal(id); if(lua_func) { + FunctionVariant function; switch(ret_type) { case FunctionTag::Vector: - return detail::bindArgType<0u, FunctionType::Vector>(std::move(lua_func), arg_types, m_lua); + function = + detail::bindArgType<0u, FunctionType::Vector>(std::move(lua_func), arg_types, m_lua); + break; case FunctionTag::Double: - return detail::bindArgType<0u, double>(std::move(lua_func), arg_types, m_lua); + function = detail::bindArgType<0u, double>(std::move(lua_func), arg_types, m_lua); + break; case FunctionTag::Void: - return detail::bindArgType<0u, void>(std::move(lua_func), arg_types, m_lua); + function = detail::bindArgType<0u, void>(std::move(lua_func), arg_types, m_lua); + break; case FunctionTag::String: - return detail::bindArgType<0u, std::string>(std::move(lua_func), arg_types, m_lua); + function = detail::bindArgType<0u, std::string>(std::move(lua_func), arg_types, m_lua); + break; default: SLIC_ERROR("[Inlet] Unexpected function return type"); } + if(function) + { + // A successful function lookup marks this exact path as a schema-supported function. + // A later scalar/map lookup at the same path may therefore treat the function + // as an absent concrete value instead of a type error. + m_function_paths.insert(id); + } + return function; } return {}; // Return an empty function to indicate that the function was not found } @@ -610,15 +624,16 @@ ReaderResult LuaReader::getValue(const std::string& id, T& value) { std::vector tokens = axom::utilities::string::split(id, SCOPE_DELIMITER); - // If we find a function at a value path, treat it as WrongType - // unless a derived reader has an explicit alternate function schema for this path. + // Functions are concrete-value type errors unless an earlier successful getFunction() + // registered the same path as a schema-supported alternative. if(tokens.size() == 1) { if((*m_lua)[tokens[0]].valid()) { if((*m_lua)[tokens[0]].get_type() == axom::sol::type::function) { - return shouldTreatFunctionAsNotFound(id) ? ReaderResult::NotFound : ReaderResult::WrongType; + return m_function_paths.find(id) != m_function_paths.end() ? ReaderResult::NotFound + : ReaderResult::WrongType; } return detail::checkedGet((*m_lua)[tokens[0]], value); } @@ -633,7 +648,8 @@ ReaderResult LuaReader::getValue(const std::string& id, T& value) { if(t[tokens.back()].get_type() == axom::sol::type::function) { - return shouldTreatFunctionAsNotFound(id) ? ReaderResult::NotFound : ReaderResult::WrongType; + return m_function_paths.find(id) != m_function_paths.end() ? ReaderResult::NotFound + : ReaderResult::WrongType; } return detail::checkedGet(t[tokens.back()], value); } @@ -657,12 +673,13 @@ ReaderResult LuaReader::getMap(const std::string& id, values.clear(); std::vector tokens = axom::utilities::string::split(id, SCOPE_DELIMITER); - // Same policy as scalar values: functions are WrongType for maps unless a - // derived reader opts this path into a parallel function schema. + // Same policy as scalar values: only a preceding successful function lookup + // can make a function count as an alternative to this concrete map. if(tokens.size() == 1 && (*m_lua)[tokens[0]].valid() && (*m_lua)[tokens[0]].get_type() == axom::sol::type::function) { - return shouldTreatFunctionAsNotFound(id) ? ReaderResult::NotFound : ReaderResult::WrongType; + return m_function_paths.find(id) != m_function_paths.end() ? ReaderResult::NotFound + : ReaderResult::WrongType; } if(tokens.size() > 1) @@ -671,7 +688,8 @@ ReaderResult LuaReader::getMap(const std::string& id, if(traverseToTable(tokens.begin(), tokens.end() - 1, parent) && parent[tokens.back()].valid() && parent[tokens.back()].get_type() == axom::sol::type::function) { - return shouldTreatFunctionAsNotFound(id) ? ReaderResult::NotFound : ReaderResult::WrongType; + return m_function_paths.find(id) != m_function_paths.end() ? ReaderResult::NotFound + : ReaderResult::WrongType; } } diff --git a/src/axom/inlet/LuaReader.hpp b/src/axom/inlet/LuaReader.hpp index 79861336ff..d37992d5f3 100644 --- a/src/axom/inlet/LuaReader.hpp +++ b/src/axom/inlet/LuaReader.hpp @@ -17,6 +17,8 @@ #include "axom/inlet/Reader.hpp" #include "axom/sol_forward.hpp" +#include + namespace axom { // Forward declarations to avoid having to include "sol.hpp" in everything @@ -110,23 +112,6 @@ class LuaReader : public Reader */ std::shared_ptr solState() { return m_lua; } - /*! - ***************************************************************************** - * \brief Should a function at a scalar/map path be treated as absent? - * - * Inlet normally accepts Lua functions only through getFunction(), - * i.e. for schema entries created with Container::addFunction(). - * If a scalar or map reader sees a function, the input path exists - * but has the wrong kind of value, so the default is ReaderResult::WrongType. - * - * Derived readers may override this for specific paths that intentionally - * have both a concrete field schema entry and an alternate function schema entry. - * Returning ReaderResult::NotFound lets the concrete field stay absent - * so the function schema entry can claim the same public input path. - ***************************************************************************** - */ - virtual bool shouldTreatFunctionAsNotFound(const std::string& id) const; - private: // Expect this to be called for only Inlet-supported types. template @@ -182,6 +167,10 @@ class LuaReader : public Reader // The elements in the global table preloaded by Sol/Lua, these are ignored // to ensure that name retrieval only includes user-provided paths std::vector m_preloaded_globals; + + // Paths successfully retrieved through getFunction(). These may serve as + // schema-supported alternatives to concrete scalar or map fields. + std::unordered_set m_function_paths; }; } // end namespace inlet diff --git a/src/axom/inlet/tests/inlet_Reader.cpp b/src/axom/inlet/tests/inlet_Reader.cpp index 3cfa0b7222..bc6e452848 100644 --- a/src/axom/inlet/tests/inlet_Reader.cpp +++ b/src/axom/inlet/tests/inlet_Reader.cpp @@ -484,6 +484,29 @@ TEST(inlet_Reader_lua, functionValueIsWrongTypeForFieldsAndMaps) EXPECT_EQ(ReaderResult::WrongType, reader.getDoubleMap("foo", values)); EXPECT_EQ(ReaderResult::WrongType, reader.getDoubleMap("bar/baz", values)); } + +TEST(inlet_Reader_lua, registeredFunctionPathIsAbsentForConcreteFieldsAndMaps) +{ + axom::inlet::LuaReader reader; + reader.parseString( + "foo = function() return 1 end\n" + "bar = { baz = function() return {1, 2, 3} end }"); + + auto scalarFunction = + reader.getFunction("foo", axom::inlet::FunctionTag::Double, {}); + auto vectorFunction = + reader.getFunction("bar/baz", axom::inlet::FunctionTag::Vector, {}); + ASSERT_TRUE(scalarFunction); + ASSERT_TRUE(vectorFunction); + + double scalar = 0.0; + EXPECT_EQ(ReaderResult::NotFound, reader.getDouble("foo", scalar)); + EXPECT_EQ(ReaderResult::NotFound, reader.getDouble("bar/baz", scalar)); + + std::unordered_map values; + EXPECT_EQ(ReaderResult::NotFound, reader.getDoubleMap("foo", values)); + EXPECT_EQ(ReaderResult::NotFound, reader.getDoubleMap("bar/baz", values)); +} #endif //------------------------------------------------------------------------------ diff --git a/src/axom/klee/io/GeometryOperatorsIO.cpp b/src/axom/klee/io/GeometryOperatorsIO.cpp index 0d84146020..023179f3af 100644 --- a/src/axom/klee/io/GeometryOperatorsIO.cpp +++ b/src/axom/klee/io/GeometryOperatorsIO.cpp @@ -781,34 +781,14 @@ inlet::Container &GeometryOperatorData::defineSchema(inlet::Container &parent, const std::string &description, bool enableLuaCallbacks) { - auto& opContainer = parent.addStructArray(fieldName, description).strict(); + auto &opContainer = parent.addStructArray(fieldName, description).strict(); + auto &slice = opContainer.addStruct("slice"); - opContainer.addDoubleArray("translate"); - - opContainer.addDouble("rotate"); - opContainer.addDoubleArray("center"); - opContainer.addDoubleArray("axis"); - - opContainer.addDoubleArray("scale"); - - opContainer.addString("convert_units_to"); - - auto& slice = opContainer.addStruct("slice"); - slice.addDouble("x"); - slice.addDouble("y"); - slice.addDouble("z"); - slice.addDoubleArray("origin"); - slice.addDoubleArray("normal"); - slice.addDoubleArray("up"); - - opContainer.addString("ref"); if(enableLuaCallbacks) { - // These Lua-only function alternatives read from the public field paths via - // pathOverride, leaving YAML and concrete Lua field parsing unchanged. - // KleeLuaReader::shouldTreatFunctionAsNotFound() is what lets a function at - // one of these paths bypass the concrete field and be claimed by the alias. - // Keep this list in sync with isKleeLuaCallbackPath() in IO.cpp. + // Register the function alternatives before reading the corresponding concrete fields. + // LuaReader then knows that a function at one of these exact public paths is supported + // by the schema rather than a type error. opContainer.addFunction(callbackName("translate"), inlet::FunctionTag::Vector, {}, @@ -826,6 +806,25 @@ inlet::Container &GeometryOperatorData::defineSchema(inlet::Container &parent, slice.addFunction(callbackName("normal"), inlet::FunctionTag::Vector, {}, "", "normal"); slice.addFunction(callbackName("up"), inlet::FunctionTag::Vector, {}, "", "up"); } + + opContainer.addDoubleArray("translate"); + + opContainer.addDouble("rotate"); + opContainer.addDoubleArray("center"); + opContainer.addDoubleArray("axis"); + + opContainer.addDoubleArray("scale"); + + opContainer.addString("convert_units_to"); + + slice.addDouble("x"); + slice.addDouble("y"); + slice.addDouble("z"); + slice.addDoubleArray("origin"); + slice.addDoubleArray("normal"); + slice.addDoubleArray("up"); + + opContainer.addString("ref"); return opContainer; } diff --git a/src/axom/klee/io/IO.cpp b/src/axom/klee/io/IO.cpp index 7fa14f0231..4e5d71328c 100644 --- a/src/axom/klee/io/IO.cpp +++ b/src/axom/klee/io/IO.cpp @@ -41,43 +41,6 @@ bool isLuaKeyword(const std::string &name); bool isLuaIdentifier(const std::string &name); #ifdef AXOM_USE_LUA -// Klee Lua callbacks are parse-time sugar for ordinary Klee operator fields. -// The schema registers both the concrete field and a hidden function alias at -// the same public input path. Only those callback-capable paths should make the -// concrete field reader return NotFound for a Lua function; everywhere else, a -// function at a Klee field path is still WrongType. -bool isKleeLuaCallbackPath(const std::string& id) -{ - const auto tokens = axom::utilities::string::split(id, '/'); - if(tokens.empty()) - { - return false; - } - - static const std::unordered_set operatorFields { - "translate", - "rotate", - "center", - "axis", - "scale", - }; - if(operatorFields.find(tokens.back()) != operatorFields.end()) - { - return true; - } - - static const std::unordered_set sliceFields { - "x", - "y", - "z", - "origin", - "normal", - "up", - }; - return tokens.size() >= 2 && tokens[tokens.size() - 2] == "slice" && - sliceFields.find(tokens.back()) != sliceFields.end(); -} - class KleeLuaReader : public inlet::LuaReader { public: @@ -239,12 +202,6 @@ class KleeLuaReader : public inlet::LuaReader ex.what())}); } } - -protected: - bool shouldTreatFunctionAsNotFound(const std::string& id) const override - { - return isKleeLuaCallbackPath(id); - } }; #endif From ed6a381480fae76e6024ae13d7cd8137069df045 Mon Sep 17 00:00:00 2001 From: Kenneth Weiss Date: Wed, 29 Jul 2026 16:24:44 -0700 Subject: [PATCH 15/52] Klee: Improves internal plumbing of Lua callbacks --- src/axom/klee/io/IO.cpp | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/src/axom/klee/io/IO.cpp b/src/axom/klee/io/IO.cpp index 4e5d71328c..9596b6d777 100644 --- a/src/axom/klee/io/IO.cpp +++ b/src/axom/klee/io/IO.cpp @@ -708,23 +708,22 @@ void appendUnexpectedGlobalErrors(const inlet::Inlet &doc, * Read a ShapeSet from a reader that has already parsed an input file. * * \param reader the parsed Inlet reader - * \param rejectUnexpectedGlobals true if unexpected top-level Lua globals should be rejected - * \param enableLuaCallbacks true if Lua callbacks should be enabled + * \param format the input format used by the reader * \param allowedGlobals external Lua globals permitted in the input * \return the parsed and verified ShapeSet * \throws KleeError if schema verification or semantic validation fails */ ShapeSet readShapeSetFromReader(std::unique_ptr reader, - bool rejectUnexpectedGlobals, - bool enableLuaCallbacks, + InputFormat format, const std::unordered_set &allowedGlobals) { + const bool isLuaInput = format == InputFormat::Lua; sidre::DataStore dataStore; inlet::Inlet doc(std::move(reader), dataStore.getRoot()); - defineKleeSchema(doc, enableLuaCallbacks); + defineKleeSchema(doc, isLuaInput); std::vector errors; bool verified = doc.verify(&errors); - if(rejectUnexpectedGlobals) + if(isLuaInput) { appendUnexpectedGlobalErrors(doc, errors, allowedGlobals); verified = verified && errors.empty(); @@ -769,8 +768,7 @@ ShapeSet readShapeSet(std::istream &stream, Path {""}, "from stream"); return readShapeSetFromReader(std::move(reader), - format == InputFormat::Lua, - format == InputFormat::Lua, + format, allowedGlobals); } @@ -800,8 +798,7 @@ ShapeSet readShapeSet(const std::string &filePath, Path {filePath}, axom::fmt::format("from file '{}'", filePath)); auto shapeSet = readShapeSetFromReader(std::move(reader), - format == InputFormat::Lua, - format == InputFormat::Lua, + format, allowedGlobals); shapeSet.setPath(filePath); return shapeSet; From f845e3f3abccabd85593ae00a1d464b09d638dea Mon Sep 17 00:00:00 2001 From: Kenneth Weiss Date: Wed, 29 Jul 2026 17:25:44 -0700 Subject: [PATCH 16/52] Inlet: Improves path-finding for nested callback aliases --- src/axom/inlet/Container.cpp | 69 +++++++++++++------------ src/axom/inlet/Container.hpp | 28 +++++++--- src/axom/inlet/tests/inlet_function.cpp | 58 +++++++++++++++++++++ 3 files changed, 115 insertions(+), 40 deletions(-) diff --git a/src/axom/inlet/Container.cpp b/src/axom/inlet/Container.cpp index 8addd4c5d6..70ab3ae3f2 100644 --- a/src/axom/inlet/Container.cpp +++ b/src/axom/inlet/Container.cpp @@ -310,16 +310,16 @@ Field& Container::addField(axom::sidre::Group* sidreGroup, return *(emplace_result.first->second); } -Function& Container::addFunctionInternal(axom::sidre::Group* sidreGroup, - FunctionVariant&& func, - const std::string& fullName, - const std::string& name) +Function& Container::storeFunction(axom::sidre::Group* sidreGroup, + FunctionVariant&& func, + const std::string& fullName, + const std::string& name) { const size_t found = name.find_last_of("/"); auto currContainer = this; if(found != std::string::npos) { - // This will add any intermediate Containers (if not present) before adding the field + // This will add any intermediate Containers (if not present) before storing the function currContainer = &addContainer(name.substr(0, found)); } const auto& emplace_result = currContainer->m_functionChildren.emplace( @@ -896,6 +896,17 @@ Verifiable& Container::addFunction(const std::string& name, const std::vector& arg_types, const std::string& description, const std::string& pathOverride) +{ + return addFunctionWithInputPath(name, ret_type, arg_types, description, pathOverride, false); +} + +Verifiable& Container::addFunctionWithInputPath( + const std::string& name, + const FunctionTag ret_type, + const std::vector& arg_types, + const std::string& description, + const std::string& inputPath, + const bool inputPathIsRelative) { // If it has indices, we're adding a function to an array // of structs, so we need to iterate over the subcontainers @@ -905,32 +916,16 @@ Verifiable& Container::addFunction(const std::string& name, const bool is_nested = transformFromNestedElements( std::back_inserter(funcs), name, - [&name, &ret_type, &arg_types, &description, &pathOverride]( + [&name, &ret_type, &arg_types, &description, &inputPath]( Container& subcontainer, const std::string& path) -> Verifiable& { - std::string nestedPathOverride = path; - if(!pathOverride.empty()) - { - // Function aliases can keep an internal schema name while reading from - // a public input path. For struct arrays, apply the override relative - // to each concrete element path found by transformFromNestedElements(). - if(path.empty()) - { - if(subcontainer.isStructCollection() || !subcontainer.m_nested_aggregates.empty()) - { - nestedPathOverride = pathOverride; - } - else - { - nestedPathOverride = Path::join({Path(subcontainer.name()), Path(pathOverride)}); - } - } - else - { - nestedPathOverride = Path::join({Path(path).parent(), Path(pathOverride)}); - } - } - return subcontainer.addFunction(name, ret_type, arg_types, description, nestedPathOverride); + const bool hasPathOverride = !inputPath.empty(); + return subcontainer.addFunctionWithInputPath(name, + ret_type, + arg_types, + description, + hasPathOverride ? inputPath : path, + hasPathOverride); }); if(is_nested) { @@ -955,14 +950,22 @@ Verifiable& Container::addFunction(const std::string& name, SLIC_ERROR_IF(sidreGroup == nullptr, fmt::format("Failed to create Sidre group with name '{0}'", fullName)); detail::addSignatureToGroup(ret_type, arg_types, sidreGroup); - // If a pathOverride is specified, needed when Inlet-internal groups - // are part of fullName - std::string lookupPath = (pathOverride.empty()) ? fullName : pathOverride; + // A caller-provided override becomes relative when a schema is expanded across + // a struct collection. Exact paths supplied by the expansion itself remain unchanged. + std::string lookupPath = inputPath; + if(lookupPath.empty()) + { + lookupPath = fullName; + } + else if(inputPathIsRelative) + { + lookupPath = Path::join({Path(m_name), Path(inputPath)}); + } lookupPath = utilities::string::removeAllInstances(lookupPath, detail::COLLECTION_GROUP_NAME + "/"); detail::updateUnexpectedNames(lookupPath, m_unexpectedNames); auto func = m_reader.getFunction(lookupPath, ret_type, arg_types); - return addFunctionInternal(sidreGroup, std::move(func), fullName, name); + return storeFunction(sidreGroup, std::move(func), fullName, name); } } diff --git a/src/axom/inlet/Container.hpp b/src/axom/inlet/Container.hpp index d2387e40c4..49e86305d1 100644 --- a/src/axom/inlet/Container.hpp +++ b/src/axom/inlet/Container.hpp @@ -734,8 +734,9 @@ class Container : public Verifiable * \param [in] ret_type The return type of the function * \param [in] arg_types The argument types of the function * \param [in] description Description of the function - * \param [in] pathOverride The path within the input file to read from, if - * different than the structure of the Sidre datastore + * \param [in] pathOverride The path within the input file to read from, + * if different than the structure of the Sidre datastore. When adding to a + * struct collection, this is resolved relative to each concrete element. * * \return Reference to the created Function ***************************************************************************** @@ -1251,7 +1252,7 @@ class Container : public Verifiable /*! ***************************************************************************** - * \brief Adds the Function. + * \brief Stores an already-read Function in this Container's schema. * * \param [in] The Sidre Group corresponding to the Function that will be added. * \param [in] func The actual callable to store @@ -1262,10 +1263,23 @@ class Container : public Verifiable * \return The child Function matching the target name. ***************************************************************************** */ - Function& addFunctionInternal(axom::sidre::Group* sidreGroup, - FunctionVariant&& func, - const std::string& fullName, - const std::string& name); + Function& storeFunction(axom::sidre::Group* sidreGroup, + FunctionVariant&& func, + const std::string& fullName, + const std::string& name); + + /*! + ***************************************************************************** + * \brief Adds a function using an input path that may be relative to each + * concrete nested container. + ***************************************************************************** + */ + Verifiable& addFunctionWithInputPath(const std::string& name, + FunctionTag ret_type, + const std::vector& arg_types, + const std::string& description, + const std::string& inputPath, + bool inputPathIsRelative); axom::sidre::View* baseGet(const std::string& name) const; diff --git a/src/axom/inlet/tests/inlet_function.cpp b/src/axom/inlet/tests/inlet_function.cpp index 0f98791f21..9091e7fd28 100644 --- a/src/axom/inlet/tests/inlet_function.cpp +++ b/src/axom/inlet/tests/inlet_function.cpp @@ -150,6 +150,21 @@ TEST(inlet_function, simple_double_to_double_through_container) EXPECT_FLOAT_EQ(result, (arg * 3.4) + 9.64); } +TEST(inlet_function, function_path_override) +{ + auto inlet = createBasicInlet("function public_name (x) return x + 2 end"); + + auto& schema = inlet.addStruct("internal_group"); + schema.addFunction("internal_name", + FunctionTag::Double, + {FunctionTag::Double}, + "", + "public_name"); + + auto callback = inlet["internal_group/internal_name"].get>(); + EXPECT_DOUBLE_EQ(callback(3.0), 5.0); +} + TEST(inlet_function, simple_void_to_double_through_container) { std::string testString = "function foo () return 9.64 end"; @@ -384,6 +399,29 @@ TEST(inlet_function, simple_vec3_to_vec3_array_of_struct) EXPECT_FLOAT_EQ(second_result[2], 18); } +TEST(inlet_function, function_path_override_in_array_of_struct) +{ + std::string testString = + "foo = { [7] = { bar = true, " + " callback = function (v) return 2*v end }, " + " [12] = { bar = false, " + " callback = function (v) return 3*v end } " + "}"; + auto inlet = createBasicInlet(testString); + + auto& arr_container = inlet.addStructArray("foo"); + arr_container.addBool("bar"); + arr_container.addFunction("baz", + FunctionTag::Vector, + {FunctionTag::Vector}, + "", + "callback"); + + auto foos = inlet["foo"].get>(); + EXPECT_FLOAT_EQ(foos[7].baz({1, 2, 3})[0], 2); + EXPECT_FLOAT_EQ(foos[12].baz({1, 2, 3})[0], 3); +} + TEST(inlet_function, dimension_dependent_result) { std::string testString = @@ -455,6 +493,26 @@ TEST(inlet_function, nested_function_in_struct) EXPECT_DOUBLE_EQ(second_func(4.0), 7.0); } +TEST(inlet_function, function_path_override_in_nested_struct) +{ + std::string testString = + "quux = { [0] = { foo = { callback = function (x) return x + 1 end } }, " + " [1] = { foo = { callback = function (x) return x + 3 end } } }"; + auto inlet = createBasicInlet(testString); + + auto& quux_schema = inlet.addStructArray("quux"); + auto& foo_schema = quux_schema.addStruct("foo"); + foo_schema.addFunction("bar", + FunctionTag::Double, + {FunctionTag::Double}, + "", + "callback"); + + auto foos = inlet["quux"].get>(); + EXPECT_DOUBLE_EQ(foos[0].bar(4.0), 5.0); + EXPECT_DOUBLE_EQ(foos[1].bar(4.0), 7.0); +} + template Ret checkedCall(const axom::sol::protected_function& func, Args&&... args) { From 17b5e9da0fd0f3ce63e2143edb4aadc9fc16dc4b Mon Sep 17 00:00:00 2001 From: Kenneth Weiss Date: Wed, 29 Jul 2026 19:02:07 -0700 Subject: [PATCH 17/52] Inlet: Adds addFunctionAsValueAlternative to support cases that benefit from a function instead of a value Also fixes order dependency in LuaReader and cleans up how objects and tables and extracted in LuaReader. Applies these to operators in Klee. --- src/axom/inlet/Container.cpp | 93 +++++++++- src/axom/inlet/Container.hpp | 58 +++++- src/axom/inlet/Inlet.hpp | 24 +++ src/axom/inlet/LuaReader.cpp | 226 +++++++++++------------ src/axom/inlet/LuaReader.hpp | 17 +- src/axom/inlet/docs/sphinx/functions.rst | 34 +++- src/axom/inlet/tests/inlet_Reader.cpp | 10 +- src/axom/inlet/tests/inlet_function.cpp | 113 ++++++++++++ src/axom/klee/io/GeometryOperatorsIO.cpp | 65 ++++--- src/axom/klee/tests/klee_io.cpp | 12 +- 10 files changed, 484 insertions(+), 168 deletions(-) diff --git a/src/axom/inlet/Container.cpp b/src/axom/inlet/Container.cpp index 70ab3ae3f2..3cb98af85a 100644 --- a/src/axom/inlet/Container.cpp +++ b/src/axom/inlet/Container.cpp @@ -329,6 +329,43 @@ Function& Container::storeFunction(axom::sidre::Group* sidreGroup, return *(emplace_result.first->second); } +ReaderResult Container::adjustForFunctionAlternative(const std::string& inputPath, + ReaderResult result) const +{ + if(result == ReaderResult::WrongType && + m_functionAlternativePaths.find(inputPath) != m_functionAlternativePaths.end()) + { + return ReaderResult::NotFound; + } + return result; +} + +void Container::registerValueInputPath(const std::string& inputPath, axom::sidre::Group* group) +{ + m_valueInputPathGroups.emplace(inputPath, group); +} + +void Container::registerFunctionAlternativePath(const std::string& inputPath) +{ + m_functionAlternativePaths.insert(inputPath); + + const auto groups = m_valueInputPathGroups.equal_range(inputPath); + for(auto iter = groups.first; iter != groups.second; ++iter) + { + auto* group = iter->second; + if(group->hasView("retrieval_status")) + { + auto* statusView = group->getView("retrieval_status"); + const auto status = + static_cast(static_cast(statusView->getData())); + if(status == ReaderResult::WrongType) + { + statusView->setScalar(static_cast(ReaderResult::NotFound)); + } + } + } +} + template VerifiableScalar& Container::addPrimitive(const std::string& name, const std::string& description, @@ -376,6 +413,7 @@ VerifiableScalar& Container::addPrimitive(const std::string& name, lookupPath = utilities::string::removeAllInstances(lookupPath, detail::COLLECTION_GROUP_NAME + "/"); detail::updateUnexpectedNames(lookupPath, m_unexpectedNames); + registerValueInputPath(lookupPath, sidreGroup); auto typeId = addPrimitiveHelper(sidreGroup, lookupPath, forArray, val); return addField(sidreGroup, typeId, fullName, name); } @@ -387,7 +425,7 @@ axom::sidre::DataTypeId Container::addPrimitiveHelper(axom::sidre::Group* bool forArray, bool val) { - const auto result = m_reader.getBool(lookupPath, val); + const auto result = adjustForFunctionAlternative(lookupPath, m_reader.getBool(lookupPath, val)); if(forArray || result == ReaderResult::Success) { sidreGroup->createViewScalar("value", val ? std::int8_t(1) : std::int8_t(0)); @@ -405,7 +443,7 @@ axom::sidre::DataTypeId Container::addPrimitiveHelper(axom::sidre::Group* s bool forArray, int val) { - const auto result = m_reader.getInt(lookupPath, val); + const auto result = adjustForFunctionAlternative(lookupPath, m_reader.getInt(lookupPath, val)); if(forArray || result == ReaderResult::Success) { sidreGroup->createViewScalar("value", val); @@ -423,7 +461,7 @@ axom::sidre::DataTypeId Container::addPrimitiveHelper(axom::sidre::Group bool forArray, double val) { - const auto result = m_reader.getDouble(lookupPath, val); + const auto result = adjustForFunctionAlternative(lookupPath, m_reader.getDouble(lookupPath, val)); if(forArray || result == ReaderResult::Success) { sidreGroup->createViewScalar("value", val); @@ -441,7 +479,7 @@ axom::sidre::DataTypeId Container::addPrimitiveHelper(axom::sidre:: bool forArray, std::string val) { - const auto result = m_reader.getString(lookupPath, val); + const auto result = adjustForFunctionAlternative(lookupPath, m_reader.getString(lookupPath, val)); if(forArray || result == ReaderResult::Success) { sidreGroup->createViewString("value", val); @@ -853,8 +891,13 @@ Verifiable& Container::addPrimitiveArray(const std::string& name, lookupPath = utilities::string::removeAllInstances(lookupPath, detail::COLLECTION_GROUP_NAME + "/"); detail::updateUnexpectedNames(lookupPath, m_unexpectedNames); + registerValueInputPath(lookupPath, container.sidreGroup()); std::vector indices; - if(isDict) + if(m_functionAlternativePaths.find(lookupPath) != m_functionAlternativePaths.end()) + { + markRetrievalStatus(*container.sidreGroup(), ReaderResult::NotFound); + } + else if(isDict) { indices = detail::PrimitiveArrayHelper::add(container, m_reader, lookupPath); } @@ -897,7 +940,33 @@ Verifiable& Container::addFunction(const std::string& name, const std::string& description, const std::string& pathOverride) { - return addFunctionWithInputPath(name, ret_type, arg_types, description, pathOverride, false); + return addFunctionWithInputPath( + name, + ret_type, + arg_types, + description, + pathOverride, + false, + false); +} + +Verifiable& Container::addFunctionAsValueAlternative( + const std::string& name, + const FunctionTag ret_type, + const std::vector& arg_types, + const std::string& inputPath, + const std::string& description) +{ + SLIC_ERROR_IF(inputPath.empty(), + "[Inlet] A function value alternative requires a non-empty input path"); + return addFunctionWithInputPath( + name, + ret_type, + arg_types, + description, + inputPath, + false, + true); } Verifiable& Container::addFunctionWithInputPath( @@ -906,7 +975,8 @@ Verifiable& Container::addFunctionWithInputPath( const std::vector& arg_types, const std::string& description, const std::string& inputPath, - const bool inputPathIsRelative) + const bool inputPathIsRelative, + const bool isValueAlternative) { // If it has indices, we're adding a function to an array // of structs, so we need to iterate over the subcontainers @@ -916,7 +986,7 @@ Verifiable& Container::addFunctionWithInputPath( const bool is_nested = transformFromNestedElements( std::back_inserter(funcs), name, - [&name, &ret_type, &arg_types, &description, &inputPath]( + [&name, &ret_type, &arg_types, &description, &inputPath, isValueAlternative]( Container& subcontainer, const std::string& path) -> Verifiable& { const bool hasPathOverride = !inputPath.empty(); @@ -925,7 +995,8 @@ Verifiable& Container::addFunctionWithInputPath( arg_types, description, hasPathOverride ? inputPath : path, - hasPathOverride); + hasPathOverride, + isValueAlternative); }); if(is_nested) { @@ -965,6 +1036,10 @@ Verifiable& Container::addFunctionWithInputPath( utilities::string::removeAllInstances(lookupPath, detail::COLLECTION_GROUP_NAME + "/"); detail::updateUnexpectedNames(lookupPath, m_unexpectedNames); auto func = m_reader.getFunction(lookupPath, ret_type, arg_types); + if(isValueAlternative && func) + { + registerFunctionAlternativePath(lookupPath); + } return storeFunction(sidreGroup, std::move(func), fullName, name); } } diff --git a/src/axom/inlet/Container.hpp b/src/axom/inlet/Container.hpp index 49e86305d1..b1bfac5eb2 100644 --- a/src/axom/inlet/Container.hpp +++ b/src/axom/inlet/Container.hpp @@ -18,9 +18,10 @@ #include #include #include +#include #include #include -#include +#include #include #include @@ -747,6 +748,32 @@ class Container : public Verifiable const std::string& description = "", const std::string& pathOverride = ""); + /*! + ***************************************************************************** + * \brief Get a function that is an alternative representation of a primitive + * value or collection in the input deck. + * + * The function is stored in the Inlet schema under \a name, but is read from + * \a inputPath. If a function exists there, a primitive field or collection + * that reads the same input path is treated as absent rather than as having + * the wrong type. The function and concrete value may be added in either order. + * + * \param [in] name Name under which to store the function + * \param [in] ret_type The return type of the function + * \param [in] arg_types The argument types of the function + * \param [in] inputPath Path of the function in the input deck + * \param [in] description Description of the function + * + * \return Reference to the created Function + ***************************************************************************** + */ + Verifiable& addFunctionAsValueAlternative( + const std::string& name, + FunctionTag ret_type, + const std::vector& arg_types, + const std::string& inputPath, + const std::string& description = ""); + /*! ******************************************************************************* * \brief Returns a stored value of primitive type. @@ -1279,7 +1306,32 @@ class Container : public Verifiable const std::vector& arg_types, const std::string& description, const std::string& inputPath, - bool inputPathIsRelative); + bool inputPathIsRelative, + bool isValueAlternative); + + /*! + ***************************************************************************** + * \brief Adjust a Reader result when a function satisfies a declared value + * alternative at the same input path. + ***************************************************************************** + */ + ReaderResult adjustForFunctionAlternative(const std::string& inputPath, + ReaderResult result) const; + + /*! + ***************************************************************************** + * \brief Record the Sidre group populated from an input value path. + ***************************************************************************** + */ + void registerValueInputPath(const std::string& inputPath, axom::sidre::Group* group); + + /*! + ***************************************************************************** + * \brief Record a successfully read function alternative and update any + * value schema entry that was added first. + ***************************************************************************** + */ + void registerFunctionAlternativePath(const std::string& inputPath); axom::sidre::View* baseGet(const std::string& name) const; @@ -1471,6 +1523,8 @@ class Container : public Verifiable std::unordered_map> m_containerChildren; std::unordered_map> m_fieldChildren; std::unordered_map> m_functionChildren; + std::unordered_set m_functionAlternativePaths; + std::unordered_multimap m_valueInputPathGroups; Verifier m_verifier; // Used for ownership only - need to take ownership of these so children diff --git a/src/axom/inlet/Inlet.hpp b/src/axom/inlet/Inlet.hpp index 7168e9c177..18545fa0ed 100644 --- a/src/axom/inlet/Inlet.hpp +++ b/src/axom/inlet/Inlet.hpp @@ -430,6 +430,30 @@ class Inlet { return m_globalContainer.addFunction(name, ret_type, arg_types, description); } + + /*! + ***************************************************************************** + * \brief Get a function that is an alternative representation of a primitive + * value or collection in the input deck. + * + * \see Container::addFunctionAsValueAlternative + ***************************************************************************** + */ + Verifiable& addFunctionAsValueAlternative( + const std::string& name, + FunctionTag ret_type, + const std::vector& arg_types, + const std::string& inputPath, + const std::string& description = "") + { + return m_globalContainer.addFunctionAsValueAlternative( + name, + ret_type, + arg_types, + inputPath, + description); + } + /*! ***************************************************************************** * \brief Add a dictionary of Boolean Fields to the input file schema. diff --git a/src/axom/inlet/LuaReader.cpp b/src/axom/inlet/LuaReader.cpp index fe25d96e68..a4ee720820 100644 --- a/src/axom/inlet/LuaReader.cpp +++ b/src/axom/inlet/LuaReader.cpp @@ -12,6 +12,7 @@ ******************************************************************************* */ +#include #include #include #include @@ -217,7 +218,6 @@ LuaReader::LuaReader() bool LuaReader::parseFile(const std::string& filePath) { - m_function_paths.clear(); if(!axom::utilities::filesystem::pathExists(filePath)) { SLIC_WARNING(fmt::format("Inlet: Given Lua input file does not exist: {0}", filePath)); @@ -234,7 +234,6 @@ bool LuaReader::parseFile(const std::string& filePath) bool LuaReader::parseString(const std::string& luaString) { - m_function_paths.clear(); if(luaString.empty()) { SLIC_WARNING("Inlet: Given an empty Lua string to parse."); @@ -325,13 +324,14 @@ bool LuaReader::traverseToTable(Iter begin, Iter end, axom::sol::table& table) return true; } - if(!(*m_lua)[*begin].valid()) + axom::sol::object object = (*m_lua)[*begin]; + if(!object.valid() || object.get_type() != axom::sol::type::table) { return false; } // Use the first one to index into the global lua state - table = (*m_lua)[*begin]; + table = object.as(); ++begin; // Then use the remaining keys to walk down to the requested table @@ -339,23 +339,65 @@ bool LuaReader::traverseToTable(Iter begin, Iter end, axom::sol::table& table) { auto key = *curr; bool is_int = conduit::utils::string_is_integer(key); - int key_as_int = conduit::utils::string_to_value(key); - if(is_int && table[key_as_int].valid()) + axom::sol::object child; + if(is_int) { - table = table[key_as_int]; + const int key_as_int = conduit::utils::string_to_value(key); + if(table[key_as_int].valid()) + { + child = table[key_as_int]; + } } - else if(table[key].valid()) + if(!child.valid() && table[key].valid()) { - table = table[key]; + child = table[key]; } - else + if(!child.valid()) + { + return false; + } + + if(child.get_type() != axom::sol::type::table) { return false; } + table = child.as(); } return true; } +axom::sol::object LuaReader::getObject(const std::string& id) +{ + const auto tokens = axom::utilities::string::split(id, SCOPE_DELIMITER); + if(tokens.empty()) + { + return {}; + } + + if(tokens.size() == 1) + { + return (*m_lua)[tokens.front()]; + } + + axom::sol::table parent; + if(!traverseToTable(tokens.begin(), tokens.end() - 1, parent)) + { + return {}; + } + + const auto& key = tokens.back(); + const bool is_int = conduit::utils::string_is_integer(key); + if(is_int) + { + const int key_as_int = conduit::utils::string_to_value(key); + if(parent[key_as_int].valid()) + { + return parent[key_as_int]; + } + } + return parent[key]; +} + ReaderResult LuaReader::getIndices(const std::string& id, std::vector& indices) { return getIndicesInternal(id, indices); @@ -440,29 +482,53 @@ FunctionType::Vector extractResult(axom::sol::protected_fu if(table_option) { axom::sol::table table = table_option.value(); - const auto size = table.size(); - if(size < 1 || size > 3) + std::array values {{0., 0., 0.}}; + std::array seen {{false, false, false}}; + int count = 0; + + for(const auto& entry : table) { - throw std::runtime_error( - fmt::format("[Inlet] Lua vector function returned a table with {0} entries; " - "expected 1 to 3 numeric entries", - size)); + if(entry.first.get_type() != axom::sol::type::number) + { + throw std::runtime_error( + "[Inlet] Lua vector function return must only contain numeric indices"); + } + + const double numeric_index = entry.first.as(); + const int index = entry.first.as(); + if(static_cast(index) != numeric_index || index < 1 || index > 3) + { + throw std::runtime_error( + "[Inlet] Lua vector function return indices must be integers between 1 and 3"); + } + if(entry.second.get_type() != axom::sol::type::number) + { + throw std::runtime_error( + "[Inlet] Lua vector function return components must be numeric"); + } + + values[index - 1] = entry.second.as(); + seen[index - 1] = true; + ++count; } - std::vector values; - values.reserve(size); - for(std::size_t i = 1; i <= size; ++i) + if(count < 1 || count > 3) { - axom::sol::optional value = table[i]; - if(!value) + throw std::runtime_error(fmt::format( + "[Inlet] Lua vector function returned a table with {0} entries; " + "expected 1 to 3 numeric entries", + count)); + } + for(int i = 0; i < count; ++i) + { + if(!seen[i]) { - throw std::runtime_error(fmt::format( - "[Inlet] Lua vector function returned a table with a non-numeric entry at index {0}", - i)); + throw std::runtime_error( + "[Inlet] Lua vector function return indices must be contiguous starting at 1"); } - values.push_back(value.value()); } - return FunctionType::Vector {values.data(), static_cast(values.size())}; + + return FunctionType::Vector {values.data(), count}; } throw std::runtime_error("[Inlet] Lua function call failed, return types possibly incorrect"); @@ -570,10 +636,9 @@ typename std::enable_if::type bindArgType( template ReaderResult checkedGet(const Proxy& proxy, Value& val) { - axom::sol::optional option = proxy; - if(option) + if(proxy.template is()) { - val = option.value(); + val = proxy.template as(); return ReaderResult::Success; } return ReaderResult::WrongType; @@ -607,13 +672,6 @@ FunctionVariant LuaReader::getFunction(const std::string& id, default: SLIC_ERROR("[Inlet] Unexpected function return type"); } - if(function) - { - // A successful function lookup marks this exact path as a schema-supported function. - // A later scalar/map lookup at the same path may therefore treat the function - // as an absent concrete value instead of a type error. - m_function_paths.insert(id); - } return function; } return {}; // Return an empty function to indicate that the function was not found @@ -622,40 +680,13 @@ FunctionVariant LuaReader::getFunction(const std::string& id, template ReaderResult LuaReader::getValue(const std::string& id, T& value) { - std::vector tokens = axom::utilities::string::split(id, SCOPE_DELIMITER); - - // Functions are concrete-value type errors unless an earlier successful getFunction() - // registered the same path as a schema-supported alternative. - if(tokens.size() == 1) + const auto object = getObject(id); + if(!object.valid()) { - if((*m_lua)[tokens[0]].valid()) - { - if((*m_lua)[tokens[0]].get_type() == axom::sol::type::function) - { - return m_function_paths.find(id) != m_function_paths.end() ? ReaderResult::NotFound - : ReaderResult::WrongType; - } - return detail::checkedGet((*m_lua)[tokens[0]], value); - } return ReaderResult::NotFound; } - axom::sol::table t; - // Don't traverse through the last token as it doesn't contain a table - if(traverseToTable(tokens.begin(), tokens.end() - 1, t)) - { - if(t[tokens.back()].valid()) - { - if(t[tokens.back()].get_type() == axom::sol::type::function) - { - return m_function_paths.find(id) != m_function_paths.end() ? ReaderResult::NotFound - : ReaderResult::WrongType; - } - return detail::checkedGet(t[tokens.back()], value); - } - } - - return ReaderResult::NotFound; + return detail::checkedGet(object, value); } std::vector LuaReader::getAllNames() @@ -671,33 +702,17 @@ ReaderResult LuaReader::getMap(const std::string& id, axom::sol::type type) { values.clear(); - std::vector tokens = axom::utilities::string::split(id, SCOPE_DELIMITER); - - // Same policy as scalar values: only a preceding successful function lookup - // can make a function count as an alternative to this concrete map. - if(tokens.size() == 1 && (*m_lua)[tokens[0]].valid() && - (*m_lua)[tokens[0]].get_type() == axom::sol::type::function) + const auto object = getObject(id); + if(!object.valid()) { - return m_function_paths.find(id) != m_function_paths.end() ? ReaderResult::NotFound - : ReaderResult::WrongType; + return ReaderResult::NotFound; } - - if(tokens.size() > 1) + if(object.get_type() != axom::sol::type::table) { - axom::sol::table parent; - if(traverseToTable(tokens.begin(), tokens.end() - 1, parent) && parent[tokens.back()].valid() && - parent[tokens.back()].get_type() == axom::sol::type::function) - { - return m_function_paths.find(id) != m_function_paths.end() ? ReaderResult::NotFound - : ReaderResult::WrongType; - } + return ReaderResult::WrongType; } - axom::sol::table t; - if(tokens.empty() || !traverseToTable(tokens.begin(), tokens.end(), t)) - { - return ReaderResult::NotFound; - } + const auto table = object.as(); // Allows for filtering out keys of incorrect type const auto is_correct_key_type = [](const axom::sol::type type) { @@ -714,7 +729,7 @@ ReaderResult LuaReader::getMap(const std::string& id, } }; bool contains_other_type = false; - for(const auto& entry : t) + for(const auto& entry : table) { // Gets only indexed items in the table. if(is_correct_key_type(entry.first.get_type()) && entry.second.get_type() == type) @@ -736,8 +751,8 @@ ReaderResult LuaReader::getVariantMapInternal(const std::string& id, values.clear(); std::vector tokens = axom::utilities::string::split(id, SCOPE_DELIMITER); - axom::sol::table t; - if(tokens.empty() || !traverseToTable(tokens.begin(), tokens.end(), t)) + axom::sol::table table; + if(tokens.empty() || !traverseToTable(tokens.begin(), tokens.end(), table)) { return ReaderResult::NotFound; } @@ -755,7 +770,7 @@ ReaderResult LuaReader::getVariantMapInternal(const std::string& id, }; bool contains_other_type = false; - for(const auto& entry : t) + for(const auto& entry : table) { VariantValue value; if(is_correct_key_type(entry.first.get_type()) && detail::extractVariantValue(entry.second, value)) @@ -775,9 +790,8 @@ ReaderResult LuaReader::getIndicesInternal(const std::string& id, std::vector { std::vector tokens = axom::utilities::string::split(id, SCOPE_DELIMITER); - axom::sol::table t; - - if(tokens.empty() || !traverseToTable(tokens.begin(), tokens.end(), t)) + axom::sol::table table; + if(tokens.empty() || !traverseToTable(tokens.begin(), tokens.end(), table)) { return ReaderResult::NotFound; } @@ -785,7 +799,7 @@ ReaderResult LuaReader::getIndicesInternal(const std::string& id, std::vector indices.clear(); // std::transform ends up being messier here - for(const auto& entry : t) + for(const auto& entry : table) { indices.push_back(detail::extractAs(entry.first)); } @@ -794,25 +808,11 @@ ReaderResult LuaReader::getIndicesInternal(const std::string& id, std::vector axom::sol::protected_function LuaReader::getFunctionInternal(const std::string& id) { - std::vector tokens = axom::utilities::string::split(id, SCOPE_DELIMITER); axom::sol::protected_function lua_func; - - if(tokens.size() == 1) + const auto object = getObject(id); + if(object.valid()) { - if((*m_lua)[tokens[0]].valid()) - { - lua_func = (*m_lua)[tokens[0]]; - detail::checkedGet((*m_lua)[tokens[0]], lua_func); - } - } - else - { - axom::sol::table t; - // Don't traverse through the last token as it doesn't contain a table - if(traverseToTable(tokens.begin(), tokens.end() - 1, t) && t[tokens.back()].valid()) - { - detail::checkedGet(t[tokens.back()], lua_func); - } + detail::checkedGet(object, lua_func); } return lua_func; } diff --git a/src/axom/inlet/LuaReader.hpp b/src/axom/inlet/LuaReader.hpp index d37992d5f3..7dc8006269 100644 --- a/src/axom/inlet/LuaReader.hpp +++ b/src/axom/inlet/LuaReader.hpp @@ -17,8 +17,6 @@ #include "axom/inlet/Reader.hpp" #include "axom/sol_forward.hpp" -#include - namespace axom { // Forward declarations to avoid having to include "sol.hpp" in everything @@ -130,6 +128,17 @@ class LuaReader : public Reader template ReaderResult getIndicesInternal(const std::string& id, std::vector& indices); + /*! + ***************************************************************************** + * \brief Resolve an input path to its Lua object. + * + * \param [in] id The path to resolve + * + * \return The object at \a id, or an invalid object if the path was not found + ***************************************************************************** + */ + axom::sol::object getObject(const std::string& id); + /*! ***************************************************************************** * \brief Obtains the Lua table reached by successive indexing through the @@ -167,10 +176,6 @@ class LuaReader : public Reader // The elements in the global table preloaded by Sol/Lua, these are ignored // to ensure that name retrieval only includes user-provided paths std::vector m_preloaded_globals; - - // Paths successfully retrieved through getFunction(). These may serve as - // schema-supported alternatives to concrete scalar or map fields. - std::unordered_set m_function_paths; }; } // end namespace inlet diff --git a/src/axom/inlet/docs/sphinx/functions.rst b/src/axom/inlet/docs/sphinx/functions.rst index f250667b09..639cefdfb8 100644 --- a/src/axom/inlet/docs/sphinx/functions.rst +++ b/src/axom/inlet/docs/sphinx/functions.rst @@ -37,12 +37,17 @@ The return type and argument types are described with the ``inlet::FunctionTag`` * ``Void`` - corresponds to C++ ``void``, should only be used for functions that don't return a value Note that a single type tag is passed for the return type, while a vector of tags is passed -for the argument types. Currently a maximum of two arguments are supported. +for the argument types. Currently a maximum of two arguments are supported. To declare a function with no arguments, simply leave the list of argument types empty. .. note:: The ``InletVector`` type (and its Lua representation) are statically-sized vectors with a maximum dimension of three. That is, they can also be used to represent two-dimensional vectors. +A Lua callback declared with a ``Vector`` return type may return either a ``Vector.new(...)`` +value or an ordinary Lua table containing one to three numeric components. Ordinary table +returns must use contiguous integer indices starting at one; sparse tables, named entries, +and non-numeric components are rejected when the callback is called. + In Lua, the following operations on the ``Vector`` type are supported (for ``Vector`` s ``u``, ``v``, and ``w``): 1. Construction of a 3D vector: ``u = Vector.new(1, 2, 3)`` @@ -58,6 +63,27 @@ In Lua, the following operations on the ``Vector`` type are supported (for ``Vec #. Dimension retrieval: ``d = u.dim`` #. Component retrieval: ``d = u.x``, ``d = u.y``, ``d = u.z`` +Functions as value alternatives +------------------------------- + +Some schemas accept either a concrete value or a function that computes that value. Use +``addFunctionAsValueAlternative`` to declare this relationship explicitly. The callback +has its own schema name but reads from the same input path as the concrete field: + +.. code-block:: C++ + + inlet.addFunctionAsValueAlternative( + "scale_callback", + axom::inlet::FunctionTag::Vector, + {}, + "scale"); + inlet.addDoubleArray("scale"); + +With this schema, ``scale = {2.0, 3.0, 4.0}`` populates ``scale``, while +``scale = function() return {2.0, 3.0, 4.0} end`` populates ``scale_callback``. +The two schema entries may be added in either order. A function encountered at a normal +field path remains a type error unless this alternative has been declared. + Accessing --------- @@ -83,6 +109,10 @@ by calling it directly: double result = inlet["coef"].call(axom::inlet::FunctionType::Vector{3, 5, 7}); .. note:: Using ``call(ArgType1, ArgType2, ...)`` requires both that the return type - be explicitly specified and that argument types be passed with the exact type as used in the + be explicitly specified and that argument types be passed with the exact type as used in the signature defined as part of the schema. This is because the arguments do not participate in overload resolution. + +Callbacks copied out of Inlet keep their Lua state alive and remain callable after the Inlet +object is destroyed. Lua execution errors and invalid callback return values are reported as +``std::runtime_error`` at the call site. diff --git a/src/axom/inlet/tests/inlet_Reader.cpp b/src/axom/inlet/tests/inlet_Reader.cpp index bc6e452848..ab04b30732 100644 --- a/src/axom/inlet/tests/inlet_Reader.cpp +++ b/src/axom/inlet/tests/inlet_Reader.cpp @@ -485,7 +485,7 @@ TEST(inlet_Reader_lua, functionValueIsWrongTypeForFieldsAndMaps) EXPECT_EQ(ReaderResult::WrongType, reader.getDoubleMap("bar/baz", values)); } -TEST(inlet_Reader_lua, registeredFunctionPathIsAbsentForConcreteFieldsAndMaps) +TEST(inlet_Reader_lua, functionLookupDoesNotChangeFieldAndMapResults) { axom::inlet::LuaReader reader; reader.parseString( @@ -500,12 +500,12 @@ TEST(inlet_Reader_lua, registeredFunctionPathIsAbsentForConcreteFieldsAndMaps) ASSERT_TRUE(vectorFunction); double scalar = 0.0; - EXPECT_EQ(ReaderResult::NotFound, reader.getDouble("foo", scalar)); - EXPECT_EQ(ReaderResult::NotFound, reader.getDouble("bar/baz", scalar)); + EXPECT_EQ(ReaderResult::WrongType, reader.getDouble("foo", scalar)); + EXPECT_EQ(ReaderResult::WrongType, reader.getDouble("bar/baz", scalar)); std::unordered_map values; - EXPECT_EQ(ReaderResult::NotFound, reader.getDoubleMap("foo", values)); - EXPECT_EQ(ReaderResult::NotFound, reader.getDoubleMap("bar/baz", values)); + EXPECT_EQ(ReaderResult::WrongType, reader.getDoubleMap("foo", values)); + EXPECT_EQ(ReaderResult::WrongType, reader.getDoubleMap("bar/baz", values)); } #endif diff --git a/src/axom/inlet/tests/inlet_function.cpp b/src/axom/inlet/tests/inlet_function.cpp index 9091e7fd28..82d0e78e64 100644 --- a/src/axom/inlet/tests/inlet_function.cpp +++ b/src/axom/inlet/tests/inlet_function.cpp @@ -91,6 +91,25 @@ TEST(inlet_function, vector_function_rejects_scalar_return) EXPECT_THROW(func.call(), std::runtime_error); } +TEST(inlet_function, vector_function_rejects_malformed_table_returns) +{ + const std::array inputs {{ + "function foo () return {} end", + "function foo () return {1, 2, 3, 4} end", + "function foo () return {[1] = 1, [3] = 3} end", + "function foo () return {1, 'two'} end", + "function foo () return {1, 2, label = 3} end", + }}; + + for(const auto& input : inputs) + { + auto inlet = createBasicInlet(input); + auto func = inlet.reader().getFunction("foo", FunctionTag::Vector, {}); + ASSERT_TRUE(func); + EXPECT_THROW(func.call(), std::runtime_error); + } +} + TEST(inlet_function, simple_vec3_to_vec3_raw_partial_init) { std::string testString = "function foo (v) return 2*v end"; @@ -165,6 +184,65 @@ TEST(inlet_function, function_path_override) EXPECT_DOUBLE_EQ(callback(3.0), 5.0); } +TEST(inlet_function, function_value_alternative_is_schema_order_independent) +{ + const auto addSchema = [](Inlet& inlet, bool functionFirst) { + if(!functionFirst) + { + inlet.addDouble("foo"); + } + inlet.addFunctionAsValueAlternative( + "foo_callback", + FunctionTag::Double, + {}, + "foo"); + if(functionFirst) + { + inlet.addDouble("foo"); + } + }; + + for(const bool functionFirst : {true, false}) + { + auto inlet = createBasicInlet("function foo () return 2.0 end"); + addSchema(inlet, functionFirst); + + EXPECT_TRUE(inlet.verify()); + EXPECT_FALSE(inlet.contains("foo")); + ASSERT_TRUE(inlet.contains("foo_callback")); + EXPECT_DOUBLE_EQ(inlet["foo_callback"].call(), 2.0); + } +} + +TEST(inlet_function, function_value_alternative_preserves_concrete_value) +{ + auto inlet = createBasicInlet("foo = 4.0"); + inlet.addFunctionAsValueAlternative( + "foo_callback", + FunctionTag::Double, + {}, + "foo"); + inlet.addDouble("foo"); + + EXPECT_TRUE(inlet.verify()); + EXPECT_FALSE(inlet.contains("foo_callback")); + ASSERT_TRUE(inlet.contains("foo")); + EXPECT_DOUBLE_EQ(inlet["foo"].get(), 4.0); +} + +TEST(inlet_function, returned_function_keeps_lua_state_alive) +{ + std::function callback; + { + auto inlet = createBasicInlet( + "offset = 3.0; function foo (value) return value + offset end"); + inlet.addFunction("foo", FunctionTag::Double, {FunctionTag::Double}); + callback = inlet["foo"].get>(); + } + + EXPECT_DOUBLE_EQ(callback(4.0), 7.0); +} + TEST(inlet_function, simple_void_to_double_through_container) { std::string testString = "function foo () return 9.64 end"; @@ -351,6 +429,20 @@ struct FromInlet } }; +struct FooWithValueAlternative +{ + std::function bar; +}; + +template <> +struct FromInlet +{ + FooWithValueAlternative operator()(const axom::inlet::Container& base) + { + return {base["bar_callback"]}; + } +}; + TEST(inlet_function, simple_vec3_to_vec3_struct) { std::string testString = "foo = { bar = true; baz = function (v) return 2*v end }"; @@ -422,6 +514,27 @@ TEST(inlet_function, function_path_override_in_array_of_struct) EXPECT_FLOAT_EQ(foos[12].baz({1, 2, 3})[0], 3); } +TEST(inlet_function, function_value_alternative_in_array_of_struct) +{ + auto inlet = createBasicInlet( + "foo = { [7] = { bar = function () return 2 end }, " + " [12] = { bar = function () return 3 end } }"); + + auto& arr_container = inlet.addStructArray("foo"); + arr_container.addDouble("bar"); + arr_container.addFunctionAsValueAlternative( + "bar_callback", + FunctionTag::Double, + {}, + "bar"); + + EXPECT_TRUE(inlet.verify()); + auto foos = + inlet["foo"].get>(); + EXPECT_DOUBLE_EQ(foos[7].bar(), 2.0); + EXPECT_DOUBLE_EQ(foos[12].bar(), 3.0); +} + TEST(inlet_function, dimension_dependent_result) { std::string testString = diff --git a/src/axom/klee/io/GeometryOperatorsIO.cpp b/src/axom/klee/io/GeometryOperatorsIO.cpp index 023179f3af..a62e0594e2 100644 --- a/src/axom/klee/io/GeometryOperatorsIO.cpp +++ b/src/axom/klee/io/GeometryOperatorsIO.cpp @@ -608,11 +608,9 @@ OpPtr parseScale(const SingleOperatorData &data, opContainer[callbackName("scale")].call()); }) : opContainer["scale"].get>(); - if(factors.size() == 1) - { - return std::make_shared(factors[0], factors[0], factors[0], startProperties); - } - if(hasCallback(opContainer, "scale")) + + const bool isUniform = factors.size() == 1; + if(!isUniform && hasCallback(opContainer, "scale")) { auto actualSize = factors.size(); auto expectedSize = static_cast(startProperties.dimensions); @@ -625,14 +623,15 @@ OpPtr parseScale(const SingleOperatorData &data, actualSize)}); } } - else + else if(!isUniform) { factors = toDoubleVector(opContainer["scale"], startProperties.dimensions, "scale"); } - if(startProperties.dimensions == Dimensions::Two) + if(!isUniform && startProperties.dimensions == Dimensions::Two) { factors.emplace_back(1.0); } + Point3D center {0., 0., 0.}; if(containsFieldOrCallback(opContainer, "center")) { @@ -640,7 +639,17 @@ OpPtr parseScale(const SingleOperatorData &data, "center", startProperties.dimensions, Point3D {0, 0, 0}, - shapeName); + shapeName); + } + + if(isUniform) + { + return std::make_shared( + factors[0], + factors[0], + factors[0], + center, + startProperties); } return std::make_shared(factors[0], factors[1], factors[2], center, startProperties); @@ -786,25 +795,27 @@ inlet::Container &GeometryOperatorData::defineSchema(inlet::Container &parent, if(enableLuaCallbacks) { - // Register the function alternatives before reading the corresponding concrete fields. - // LuaReader then knows that a function at one of these exact public paths is supported - // by the schema rather than a type error. - opContainer.addFunction(callbackName("translate"), - inlet::FunctionTag::Vector, - {}, - "", - "translate"); - opContainer.addFunction(callbackName("rotate"), inlet::FunctionTag::Double, {}, "", "rotate"); - opContainer.addFunction(callbackName("center"), inlet::FunctionTag::Vector, {}, "", "center"); - opContainer.addFunction(callbackName("axis"), inlet::FunctionTag::Vector, {}, "", "axis"); - opContainer.addFunction(callbackName("scale"), inlet::FunctionTag::Vector, {}, "", "scale"); - - slice.addFunction(callbackName("x"), inlet::FunctionTag::Double, {}, "", "x"); - slice.addFunction(callbackName("y"), inlet::FunctionTag::Double, {}, "", "y"); - slice.addFunction(callbackName("z"), inlet::FunctionTag::Double, {}, "", "z"); - slice.addFunction(callbackName("origin"), inlet::FunctionTag::Vector, {}, "", "origin"); - slice.addFunction(callbackName("normal"), inlet::FunctionTag::Vector, {}, "", "normal"); - slice.addFunction(callbackName("up"), inlet::FunctionTag::Vector, {}, "", "up"); + const auto addCallbackAlternative = + [](inlet::Container &container, const char *fieldName, inlet::FunctionTag returnType) { + container.addFunctionAsValueAlternative( + callbackName(fieldName), + returnType, + {}, + fieldName); + }; + + addCallbackAlternative(opContainer, "translate", inlet::FunctionTag::Vector); + addCallbackAlternative(opContainer, "rotate", inlet::FunctionTag::Double); + addCallbackAlternative(opContainer, "center", inlet::FunctionTag::Vector); + addCallbackAlternative(opContainer, "axis", inlet::FunctionTag::Vector); + addCallbackAlternative(opContainer, "scale", inlet::FunctionTag::Vector); + + addCallbackAlternative(slice, "x", inlet::FunctionTag::Double); + addCallbackAlternative(slice, "y", inlet::FunctionTag::Double); + addCallbackAlternative(slice, "z", inlet::FunctionTag::Double); + addCallbackAlternative(slice, "origin", inlet::FunctionTag::Vector); + addCallbackAlternative(slice, "normal", inlet::FunctionTag::Vector); + addCallbackAlternative(slice, "up", inlet::FunctionTag::Vector); } opContainer.addDoubleArray("translate"); diff --git a/src/axom/klee/tests/klee_io.cpp b/src/axom/klee/tests/klee_io.cpp index e75f5c5e35..0df789c6f1 100644 --- a/src/axom/klee/tests/klee_io.cpp +++ b/src/axom/klee/tests/klee_io.cpp @@ -1369,7 +1369,10 @@ TEST(IOTest, readShapeSet_luaOperatorCallbacks) center = function() return Vector.new(1, 2, 3) end }, { translate = function() return {4, 5, 6} end }, - { scale = function() return {2.0} end }, + { + scale = function() return {2.0} end, + center = function() return {3, 4, 5} end + }, { scale = function() return {1.5, 2.5, 3.5} end, center = function() return {1, 1, 1} end @@ -1422,6 +1425,7 @@ TEST(IOTest, readShapeSet_luaOperatorCallbacks) EXPECT_DOUBLE_EQ(2.0, uniformScale->getXFactor()); EXPECT_DOUBLE_EQ(2.0, uniformScale->getYFactor()); EXPECT_DOUBLE_EQ(2.0, uniformScale->getZFactor()); + EXPECT_THAT(uniformScale->getCenter(), AlmostEqPoint(Point3D {3, 4, 5})); auto vectorScale = dynamic_cast(composite->getOperators()[3].get()); ASSERT_NE(vectorScale, nullptr); @@ -1861,7 +1865,7 @@ TEST(IOTest, readShapeSet_geometryOperators_scaleWithCenter) path: path/to/file.format units: m operators: - - scale: [1.5, 2.5] + - scale: [1.5] center: [10, 20] )"); auto& shapes = shapeSet.getShapes(); @@ -1876,8 +1880,8 @@ TEST(IOTest, readShapeSet_geometryOperators_scaleWithCenter) auto scale = dynamic_cast(composite->getOperators()[0].get()); ASSERT_NE(scale, nullptr); EXPECT_DOUBLE_EQ(1.5, scale->getXFactor()); - EXPECT_DOUBLE_EQ(2.5, scale->getYFactor()); - EXPECT_DOUBLE_EQ(1.0, scale->getZFactor()); + EXPECT_DOUBLE_EQ(1.5, scale->getYFactor()); + EXPECT_DOUBLE_EQ(1.5, scale->getZFactor()); EXPECT_THAT(scale->getCenter(), AlmostEqPoint(Point3D {10, 20, 0})); } From fb2757681a8252d931492d35babb748218c92a8d Mon Sep 17 00:00:00 2001 From: Kenneth Weiss Date: Wed, 29 Jul 2026 21:46:29 -0700 Subject: [PATCH 18/52] Klee: Stabilized Lua runtime input contract * Renames some initialization functions to better convey intent * Preserves Lua integers (we no longer convert to double and then back to integer) * Improves initialization diagnostics * Expanded test coverage --- .../klee/docs/sphinx/specifying_shapes.rst | 60 ++-- src/axom/klee/io/IO.cpp | 166 +++++---- src/axom/klee/io/IO.hpp | 26 +- src/axom/klee/tests/klee_io.cpp | 321 +++++++++++++----- src/examples/CMakeLists.txt | 8 +- src/examples/shaping_tutorial/CMakeLists.txt | 8 +- .../shaping_tutorial/lesson_03/README.md | 18 +- ...dings.lua => ice_cream_initialization.lua} | 0 ...bindings.lua => ice_cream_initialized.lua} | 0 .../klee_operators_and_validation.cpp | 18 +- 10 files changed, 397 insertions(+), 228 deletions(-) rename src/examples/shaping_tutorial/lesson_03/{ice_cream_runtime_bindings.lua => ice_cream_initialization.lua} (100%) rename src/examples/shaping_tutorial/lesson_03/{ice_cream_bindings.lua => ice_cream_initialized.lua} (100%) diff --git a/src/axom/klee/docs/sphinx/specifying_shapes.rst b/src/axom/klee/docs/sphinx/specifying_shapes.rst index b2c99e89bb..0ea6951d85 100644 --- a/src/axom/klee/docs/sphinx/specifying_shapes.rst +++ b/src/axom/klee/docs/sphinx/specifying_shapes.rst @@ -123,16 +123,16 @@ ordinary table values can be generated programmatically: } } -Caller-provided input variables can also be installed as initial Lua globals before a deck is evaluated. -This is useful when an application wants one deck to select between 2D and 3D geometry, -dimensions, or operator values at run time: +Caller-provided primitive values can be installed as initial Lua globals before a deck is +evaluated. This is useful when an application wants one deck to select between 2D and 3D +geometry, dimensions, or operator values at run time: .. code-block:: c++ axom::klee::LuaInputOptions options; - options.variables = { - {"dimensions", axom::klee::InputVariableValue {2}}, - {"shape_suffix", axom::klee::InputVariableValue {std::string {"2d"}}} + options.initialGlobals = { + {"dimensions", axom::klee::LuaGlobalValue {2}}, + {"shape_suffix", axom::klee::LuaGlobalValue {std::string {"2d"}}} }; auto shapeSet = axom::klee::readShapeSet("shape.lua", options); @@ -157,24 +157,25 @@ dimensions, or operator values at run time: } } -Input variables are Lua-only and may be booleans, integers, doubles, or strings. -Their names must be non-keyword Lua identifiers. They are ordinary mutable globals by -construction and are allowed by Klee's unexpected-global check. Deck code can -reassign these names, so applications should treat them as initial values rather -than read-only controls. Other helper values in the deck should still be declared -:code:`local`. +Initial globals are Lua-only and may be booleans, integers, doubles, or strings. +Their names must be non-keyword ASCII Lua identifiers. They are ordinary mutable +globals—not a read-only context—and are allowed by Klee's unexpected-global check. +Deck code can reassign or delete them, so applications should treat them as initial +values rather than controls. Other helper values in the deck should still be +declared :code:`local`. Initial globals may not replace standard Lua globals such +as :code:`math` or :code:`package`. Applications that need richer runtime customization can also provide a Lua -bindings chunk. Klee evaluates the chunk before parsing the deck, expects it to -return a table of exported bindings, and then installs those exported names as -initial globals while still rejecting unrelated unexpected globals in the deck. -This allows a host code to pass user-supplied helper functions and local closures -at run time without recompiling the C++ application: +initialization chunk. Klee evaluates the chunk after installing +:code:`initialGlobals` and before parsing the deck. The chunk must return a table; +those table entries are then installed as initial globals while unrelated +unexpected globals in the deck remain errors. This allows host code to provide +helper functions, tables, and local closures without recompiling the application: .. code-block:: c++ axom::klee::LuaInputOptions options; - options.bindings = axom::klee::LuaBindingsChunk { + options.initialization = axom::klee::LuaInitializationChunk { R"( local dim = 2 local lift = 3.0 @@ -191,7 +192,7 @@ at run time without recompiling the C++ application: offset = offset } )", - "runtime_bindings" + "runtime_initialization" }; auto shapeSet = axom::klee::readShapeSet("shape.lua", options); @@ -212,17 +213,24 @@ at run time without recompiling the C++ application: } } -Bindings chunks must return a table whose exported keys are non-keyword Lua identifiers. -Exported values may be booleans, numbers, strings, tables, or functions. Like -input variables, exported bindings are ordinary Lua globals; deck code can -reassign exported names and can mutate exported tables. +Initialization chunks must return a table whose exported keys are non-keyword +ASCII Lua identifiers. Exported values may be booleans, numbers, strings, tables, +or functions and retain their original Lua representation. An exported Lua integer, +for example, is not converted through a C++ floating-point value. Export names may +not collide with standard Lua globals or :code:`initialGlobals`. Klee evaluates the chunk in an isolated Lua environment. Preloaded Lua -libraries and caller-provided input variables remain visible, but globals +libraries and caller-provided initial globals remain visible, but global names assigned by the chunk do not leak into the input deck unless they are returned in the export table. Exported functions retain access to the chunk's private -environment. This name isolation is not a security sandbox; applications -should execute only trusted Lua bindings code. +environment. Exported globals are mutable: deck code can replace or delete them +and can mutate exported tables. + +The environment isolation is shallow. Inherited objects such as :code:`math` and +:code:`package` are shared, so mutating a member of an inherited table is visible +to the deck. Initialization chunks and decks are trusted code: this mechanism is +not a security sandbox, :code:`package` may load additional code, and Klee imposes +no CPU, memory, or recursion limits. Use :code:`local` helper functions and constants for intermediate values so the global namespace contains only the Klee schema fields that Inlet should read. diff --git a/src/axom/klee/io/IO.cpp b/src/axom/klee/io/IO.cpp index 9596b6d777..51fbaa7408 100644 --- a/src/axom/klee/io/IO.cpp +++ b/src/axom/klee/io/IO.cpp @@ -21,7 +21,6 @@ #endif #include -#include #include #include #include @@ -58,48 +57,57 @@ class KleeLuaReader : public inlet::LuaReader return names; } - void setInputVariable(const std::string &name, const InputVariableValue &value) + void setInitialGlobal(const std::string &name, const LuaGlobalValue &value) { auto lua = solState(); std::visit([&](const auto &typedValue) { (*lua)[name] = typedValue; }, value); } - std::unordered_set applyBindingsChunk( - const LuaBindingsChunk &bindings, + std::unordered_set applyInitializationChunk( + const LuaInitializationChunk &initialization, const std::unordered_set &reservedNames, const std::unordered_set &existingExternalNames) { auto lua = solState(); - auto chunkPath = Path {bindings.label.empty() ? "" : bindings.label}; - if(bindings.source.empty()) + const std::string chunkName = + initialization.label.empty() ? "" : initialization.label; + const auto chunkPath = Path {chunkName}; + const auto chunkMessage = [&](const std::string &message) { + return axom::fmt::format( + "Klee Lua initialization chunk '{}': {}", + chunkName, + message); + }; + if(initialization.source.empty()) { - throw KleeError({chunkPath, "Klee Lua bindings chunk is empty."}); + throw KleeError({chunkPath, chunkMessage("Chunk is empty.")}); } try { - // Evaluate bindings in their own environment so assignments made by the + // Evaluate initialization in its own environment so assignments made by the // chunk do not mutate the input file's globals. The fallback keeps - // preloaded libraries and caller-provided input variables visible. - axom::sol::environment bindingsEnvironment {*lua, axom::sol::create, lua->globals()}; - bindingsEnvironment["_G"] = bindingsEnvironment; - auto result = lua->script(bindings.source, bindingsEnvironment); + // preloaded libraries and caller-provided initial globals visible. + axom::sol::environment initializationEnvironment { + *lua, + axom::sol::create, + lua->globals()}; + initializationEnvironment["_G"] = initializationEnvironment; + auto result = lua->script(initialization.source, initializationEnvironment); if(!result.valid()) { axom::sol::error err = result; - throw KleeError({chunkPath, - axom::fmt::format("Failed to evaluate Klee Lua bindings chunk '{}': {}", - static_cast(chunkPath), - err.what())}); + throw KleeError( + {chunkPath, + chunkMessage(axom::fmt::format("Failed to evaluate chunk: {}", err.what()))}); } axom::sol::optional tableOption = result; if(!tableOption) { - throw KleeError({chunkPath, - axom::fmt::format("Klee Lua bindings chunk '{}' must return a table of " - "exported bindings.", - static_cast(chunkPath))}); + throw KleeError( + {chunkPath, + chunkMessage("Chunk must return a table of exported globals.")}); } std::unordered_set exportedNames; @@ -130,62 +138,64 @@ class KleeLuaReader : public inlet::LuaReader { if(entry.first.get_type() != axom::sol::type::string) { - throw KleeError({chunkPath, - axom::fmt::format("Klee Lua bindings chunk '{}' must return a table " - "with string keys.", - static_cast(chunkPath))}); + throw KleeError( + {chunkPath, + chunkMessage("Export table must contain only string keys.")}); } const std::string name = entry.first.as(); if(!isLuaIdentifier(name)) { const auto reason = isLuaKeyword(name) - ? "Reserved Lua keywords cannot be used as binding names." - : "Binding names must be Lua identifiers."; - throw KleeError({exportPath(name), - axom::fmt::format("Invalid Klee Lua binding name '{}'. {}", name, reason)}); + ? "Reserved Lua keywords cannot be used as exported global names." + : "Exported global names must be Lua identifiers."; + throw KleeError( + {exportPath(name), + chunkMessage(axom::fmt::format( + "Invalid exported Lua global name '{}'. {}", + name, + reason))}); } if(reservedNames.find(name) != reservedNames.end()) { throw KleeError( {exportPath(name), - axom::fmt::format("Klee Lua binding name '{}' conflicts with an existing Lua global.", - name)}); + chunkMessage(axom::fmt::format( + "Exported Lua global name '{}' conflicts with an existing Lua global.", + name))}); } if(existingExternalNames.find(name) != existingExternalNames.end()) { - throw KleeError({exportPath(name), - axom::fmt::format( - "Klee Lua binding name '{}' duplicates another external Lua binding.", - name)}); + throw KleeError( + {exportPath(name), + chunkMessage(axom::fmt::format( + "Exported Lua global name '{}' duplicates an initial Lua global.", + name))}); } - exportedNames.insert(name); switch(entry.second.get_type()) { case axom::sol::type::boolean: - (*lua)[name] = entry.second.as(); - break; case axom::sol::type::number: - (*lua)[name] = entry.second.as(); - break; case axom::sol::type::string: - (*lua)[name] = entry.second.as(); - break; case axom::sol::type::function: - (*lua)[name] = entry.second.as(); - break; case axom::sol::type::table: - (*lua)[name] = entry.second.as(); break; default: - throw KleeError({exportPath(name), - axom::fmt::format("Klee Lua binding '{}' has unsupported value type " - "'{}'. Supported exported binding value types are " - "booleans, numbers, strings, tables, and functions.", - name, - typeName(entry.second.get_type()))}); + throw KleeError( + {exportPath(name), + chunkMessage(axom::fmt::format( + "Exported Lua global '{}' has unsupported value type '{}'. " + "Supported exported global value types are booleans, numbers, " + "strings, tables, and functions.", + name, + typeName(entry.second.get_type())))}); } + + // Preserve the original Lua representation. In particular, copying a + // Lua integer through a C++ double can silently lose precision. + (*lua)[name] = entry.second; + exportedNames.insert(name); } return exportedNames; @@ -196,10 +206,9 @@ class KleeLuaReader : public inlet::LuaReader } catch(const std::exception &ex) { - throw KleeError({chunkPath, - axom::fmt::format("Failed to evaluate Klee Lua bindings chunk '{}': {}", - static_cast(chunkPath), - ex.what())}); + throw KleeError( + {chunkPath, + chunkMessage(axom::fmt::format("Failed to evaluate chunk: {}", ex.what()))}); } } }; @@ -538,8 +547,14 @@ bool isLuaIdentifier(const std::string &name) return false; } - auto isNameStart = [](unsigned char ch) { return std::isalpha(ch) || ch == '_'; }; - auto isNameChar = [](unsigned char ch) { return std::isalnum(ch) || ch == '_'; }; + const auto isAsciiLetter = [](unsigned char ch) { + return (ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z'); + }; + const auto isAsciiDigit = [](unsigned char ch) { return ch >= '0' && ch <= '9'; }; + const auto isNameStart = [&](unsigned char ch) { return isAsciiLetter(ch) || ch == '_'; }; + const auto isNameChar = [&](unsigned char ch) { + return isAsciiLetter(ch) || isAsciiDigit(ch) || ch == '_'; + }; if(!isNameStart(static_cast(name.front()))) { @@ -551,18 +566,18 @@ bool isLuaIdentifier(const std::string &name) !isLuaKeyword(name); } -void validateInputVariables(const InputVariables &variables) +void validateInitialGlobals(const LuaInitialGlobals &initialGlobals) { - for(const auto &entry : variables) + for(const auto &entry : initialGlobals) { if(!isLuaIdentifier(entry.first)) { const auto reason = isLuaKeyword(entry.first) - ? "Reserved Lua keywords cannot be used as input variable names." - : "Input variable names must be Lua identifiers."; + ? "Reserved Lua keywords cannot be used as initial global names." + : "Initial global names must be Lua identifiers."; throw KleeError( {Path {entry.first.empty() ? "" : entry.first}, - axom::fmt::format("Invalid Klee Lua input variable name '{}'. {}", entry.first, reason)}); + axom::fmt::format("Invalid initial Lua global name '{}'. {}", entry.first, reason)}); } } } @@ -571,23 +586,24 @@ void validateInputVariables(const InputVariables &variables) * Create an Inlet reader for a Klee input format. * * \param format the input file format to read - * \param options optional variables and bindings for Lua input evaluation + * \param options optional globals and initialization for Lua input evaluation * \param allowedGlobals receives external names permitted in the input * \return a reader for \a format * \throws KleeError if \a format is unsupported, Lua support was not enabled, - * or the external Lua bindings are invalid for the selected format + * or the external Lua initialization is invalid for the selected format */ std::unique_ptr createReader(InputFormat format, const LuaInputOptions &options, std::unordered_set &allowedGlobals) { allowedGlobals.clear(); - if(format != InputFormat::Lua && (!options.variables.empty() || options.bindings)) + if(format != InputFormat::Lua && + (!options.initialGlobals.empty() || options.initialization)) { throw KleeError({Path {""}, - options.bindings - ? "Klee Lua bindings are only supported for Lua input decks." - : "Klee input variables are only supported for Lua input decks."}); + options.initialization + ? "Klee Lua initialization is only supported for Lua input decks." + : "Klee initial Lua globals are only supported for Lua input decks."}); } switch(format) @@ -599,27 +615,29 @@ std::unique_ptr createReader(InputFormat format, { auto reader = std::make_unique(); const auto reservedGlobals = reader->topLevelGlobalNames(); - validateInputVariables(options.variables); + validateInitialGlobals(options.initialGlobals); // External inputs are ordinary Lua globals installed before deck parsing. // allowedGlobals only prevents Klee's unexpected-global check from rejecting // those names; it does not make them read-only inside the deck. - for(const auto &entry : options.variables) + for(const auto &entry : options.initialGlobals) { if(reservedGlobals.find(entry.first) != reservedGlobals.end()) { throw KleeError( {Path {entry.first}, - axom::fmt::format("Klee Lua input variable name '{}' conflicts with an existing Lua " - "global.", + axom::fmt::format("Initial Lua global name '{}' conflicts with an existing Lua global.", entry.first)}); } - reader->setInputVariable(entry.first, entry.second); + reader->setInitialGlobal(entry.first, entry.second); allowedGlobals.insert(entry.first); } - if(options.bindings) + if(options.initialization) { auto exportedNames = - reader->applyBindingsChunk(*options.bindings, reservedGlobals, allowedGlobals); + reader->applyInitializationChunk( + *options.initialization, + reservedGlobals, + allowedGlobals); allowedGlobals.insert(exportedNames.begin(), exportedNames.end()); } return reader; diff --git a/src/axom/klee/io/IO.hpp b/src/axom/klee/io/IO.hpp index 6eac2b9a80..33da0e9645 100644 --- a/src/axom/klee/io/IO.hpp +++ b/src/axom/klee/io/IO.hpp @@ -25,27 +25,27 @@ enum class InputFormat Lua }; -/// Runtime Lua chunk evaluated before deck parsing; exported bindings become initial Lua globals. -struct LuaBindingsChunk +/// Lua initialization chunk evaluated before deck parsing in an isolated environment. +struct LuaInitializationChunk { std::string source; - std::string label {""}; + std::string label {""}; }; /// Primitive value types that may be set as initial Lua globals. -using InputVariableValue = std::variant; +using LuaGlobalValue = std::variant; -/// Variables to set as ordinary mutable globals before a Lua input deck is evaluated. -using InputVariables = std::unordered_map; +/// Ordinary mutable globals to install before a Lua input deck is evaluated. +using LuaInitialGlobals = std::unordered_map; -/// Optional caller-provided values and bindings for a Lua input deck. +/// Optional caller-provided initialization for a Lua input deck. struct LuaInputOptions { /// Primitive values to install as initial mutable Lua globals. - InputVariables variables; + LuaInitialGlobals initialGlobals; - /// Chunk to evaluate before the input deck; returned entries become mutable Lua globals. - std::optional bindings; + /// Isolated chunk whose returned table entries become initial mutable Lua globals. + std::optional initialization; }; /** @@ -72,7 +72,7 @@ ShapeSet readShapeSet(std::istream& stream, InputFormat format); * * \param stream the stream from which to read the ShapeSet * \param format the input deck format to use - * \param options optional variables and bindings for a Lua input deck + * \param options optional initial globals and initialization for a Lua input deck * \note Non-empty Lua input options are supported only for Lua input decks. * \throws KleeError if the input or Lua input options are invalid */ @@ -107,7 +107,7 @@ ShapeSet readShapeSet(const std::string& filePath, InputFormat format); * Read a ShapeSet from a specified file with caller-provided Lua inputs. * * \param filePath the file from which to read the ShapeSet - * \param options optional variables and bindings for a Lua input deck + * \param options optional initial globals and initialization for a Lua input deck * \note The input format is inferred from the file extension. Non-empty Lua * input options are supported only for Lua input decks. * \return the ShapeSet read from the file @@ -121,7 +121,7 @@ ShapeSet readShapeSet(const std::string &filePath, const LuaInputOptions &option * * \param filePath the file from which to read the ShapeSet * \param format the input file format to use, regardless of the file extension - * \param options optional variables and bindings for a Lua input deck + * \param options optional initial globals and initialization for a Lua input deck * \note Non-empty Lua input options are supported only for Lua input decks. * \return the ShapeSet read from the file * \throws KleeError if the input or Lua input options are invalid diff --git a/src/axom/klee/tests/klee_io.cpp b/src/axom/klee/tests/klee_io.cpp index 0df789c6f1..a5f3ef5839 100644 --- a/src/axom/klee/tests/klee_io.cpp +++ b/src/axom/klee/tests/klee_io.cpp @@ -16,6 +16,7 @@ #include "gtest/gtest.h" +#include #include #include @@ -27,10 +28,10 @@ namespace primal = axom::primal; using klee::CompositeOperator; using klee::Dimensions; using klee::InputFormat; -using klee::InputVariables; using klee::KleeError; using klee::LengthUnit; -using klee::LuaBindingsChunk; +using klee::LuaInitialGlobals; +using klee::LuaInitializationChunk; using klee::LuaInputOptions; using klee::Rotation; using klee::Scale; @@ -434,10 +435,10 @@ TEST(IOTest, readShapeSet_streamDefaultsToYaml) } } -TEST(IOTest, readShapeSet_yamlRejectsInputVariables) +TEST(IOTest, readShapeSet_yamlRejectsInitialLuaGlobals) { LuaInputOptions options; - options.variables = {{"dimensions", klee::InputVariableValue {2}}}; + options.initialGlobals = {{"dimensions", klee::LuaGlobalValue {2}}}; try { @@ -451,20 +452,20 @@ TEST(IOTest, readShapeSet_yamlRejectsInputVariables) } catch(const KleeError& err) { - EXPECT_THAT(err.what(), HasSubstr("input variables")); + EXPECT_THAT(err.what(), HasSubstr("initial Lua globals")); EXPECT_THAT(err.what(), HasSubstr("Lua")); } } -TEST(IOTest, readShapeSet_yamlRejectsLuaBindings) +TEST(IOTest, readShapeSet_yamlRejectsLuaInitialization) { LuaInputOptions options; - options.bindings = LuaBindingsChunk {R"( + options.initialization = LuaInitializationChunk {R"( return { dimensions = 2 } )", - "runtime_bindings"}; + "runtime_initialization"}; try { @@ -478,7 +479,7 @@ TEST(IOTest, readShapeSet_yamlRejectsLuaBindings) } catch(const KleeError& err) { - EXPECT_THAT(err.what(), HasSubstr("Lua bindings")); + EXPECT_THAT(err.what(), HasSubstr("Lua initialization")); EXPECT_THAT(err.what(), HasSubstr("Lua input decks")); } } @@ -513,12 +514,24 @@ TEST(IOTest, readShapeSet_explicitLuaOverridesFileExtension) shapes = {})"); LuaInputOptions options; - options.variables = {{"dimensions", klee::InputVariableValue {2}}}; + options.initialGlobals = {{"dimensions", klee::LuaGlobalValue {2}}}; auto shapeSet = klee::readShapeSet(input.getPath(), InputFormat::Lua, options); EXPECT_EQ(Dimensions::Two, shapeSet.getDimensions()); EXPECT_EQ(input.getPath(), shapeSet.getPath()); } +TEST(IOTest, readShapeSet_inferredLuaAcceptsInitializationOptions) +{ + axom::utilities::filesystem::TempFile input {"inferredLua", "lua"}; + input.write("shapes = {}"); + + LuaInputOptions options; + options.initialGlobals = {{"dimensions", klee::LuaGlobalValue {2}}}; + auto shapeSet = klee::readShapeSet(input.getPath(), options); + EXPECT_EQ(Dimensions::Two, shapeSet.getDimensions()); + EXPECT_EQ(input.getPath(), shapeSet.getPath()); +} + TEST(IOTest, readShapeSet_malformedLuaReportsParseFailure) { try @@ -560,19 +573,20 @@ TEST(IOTest, readShapeSet_luaStreamMinimalShapeList) EXPECT_EQ(Dimensions::Two, shapeSet.getDimensions()); } -TEST(IOTest, readShapeSet_luaInputVariablesProvideInitialDimensionAndOperator) +TEST(IOTest, readShapeSet_luaInitialGlobalsProvideDimensionAndOperator) { - InputVariables variables { - {"dimensions", klee::InputVariableValue {2}}, - {"shape_suffix", klee::InputVariableValue {std::string {"2d"}}}, - {"lift", klee::InputVariableValue {3.0}}, + LuaInitialGlobals initialGlobals { + {"dimensions", klee::LuaGlobalValue {2}}, + {"shape_suffix", klee::LuaGlobalValue {std::string {"2d"}}}, + {"lift", klee::LuaGlobalValue {3.0}}, + {"use_suffix", klee::LuaGlobalValue {true}}, }; LuaInputOptions options; - options.variables = variables; + options.initialGlobals = initialGlobals; auto shapeSet = readShapeSetFromString(R"( local function shape_path() - return "part_" .. shape_suffix .. ".stl" + return use_suffix and ("part_" .. shape_suffix .. ".stl") or "part.stl" end shapes = { @@ -605,18 +619,19 @@ TEST(IOTest, readShapeSet_luaInputVariablesProvideInitialDimensionAndOperator) EXPECT_THAT(translation->getOffset(), AlmostEqVector(Vector3D {1.0, 3.0, 0.0})); } -TEST(IOTest, readShapeSet_luaInputVariablesAreInitialMutableGlobals) +TEST(IOTest, readShapeSet_luaInitialGlobalsAreMutable) { - InputVariables variables { - {"dimensions", klee::InputVariableValue {2}}, - {"lift", klee::InputVariableValue {3.0}}, + LuaInitialGlobals initialGlobals { + {"dimensions", klee::LuaGlobalValue {2}}, + {"lift", klee::LuaGlobalValue {3.0}}, }; LuaInputOptions options; - options.variables = variables; + options.initialGlobals = initialGlobals; auto shapeSet = readShapeSetFromString(R"( dimensions = 3 - lift = 7.0 + lift = nil + local resolved_lift = lift or 7.0 shapes = { { @@ -627,7 +642,7 @@ TEST(IOTest, readShapeSet_luaInputVariablesAreInitialMutableGlobals) path = "part.stl", units = "cm", operators = { - { translate = {1.0, 2.0, lift} } + { translate = {1.0, 2.0, resolved_lift} } } } } @@ -647,21 +662,22 @@ TEST(IOTest, readShapeSet_luaInputVariablesAreInitialMutableGlobals) EXPECT_THAT(translation->getOffset(), AlmostEqVector(Vector3D {1.0, 2.0, 7.0})); } -TEST(IOTest, readShapeSet_luaBindingsChunkProvidesInitialDimensionAndOperator) +TEST(IOTest, readShapeSet_luaInitializationProvidesDimensionAndOperator) { - LuaBindingsChunk bindings {R"( + LuaInitializationChunk initialization {R"( local dim = 2 local lift = 3.0 return { dimensions = dim, shape_suffix = "2d", - lift = lift + lift = lift, + enabled = true } )", - "runtime_bindings"}; + "runtime_initialization"}; LuaInputOptions options; - options.bindings = bindings; + options.initialization = initialization; auto shapeSet = readShapeSetFromString(R"( local function shape_path() @@ -674,7 +690,7 @@ TEST(IOTest, readShapeSet_luaBindingsChunkProvidesInitialDimensionAndOperator) material = "steel", geometry = { format = "stl", - path = shape_path(), + path = enabled and shape_path() or "disabled.stl", units = "cm", operators = { { translate = (dimensions == 2) and {1.0, lift} or {1.0, 0.0, lift} } @@ -698,9 +714,9 @@ TEST(IOTest, readShapeSet_luaBindingsChunkProvidesInitialDimensionAndOperator) EXPECT_THAT(translation->getOffset(), AlmostEqVector(Vector3D {1.0, 3.0, 0.0})); } -TEST(IOTest, readShapeSet_luaBindingsChunkProvidesInitialMutableGlobals) +TEST(IOTest, readShapeSet_luaInitializationExportsMutableGlobals) { - LuaBindingsChunk bindings {R"( + LuaInitializationChunk initialization {R"( return { dimensions = 2, settings = { @@ -708,9 +724,9 @@ TEST(IOTest, readShapeSet_luaBindingsChunkProvidesInitialMutableGlobals) } } )", - "runtime_bindings"}; + "runtime_initialization"}; LuaInputOptions options; - options.bindings = bindings; + options.initialization = initialization; auto shapeSet = readShapeSetFromString(R"( dimensions = 3 @@ -745,24 +761,24 @@ TEST(IOTest, readShapeSet_luaBindingsChunkProvidesInitialMutableGlobals) EXPECT_THAT(translation->getOffset(), AlmostEqVector(Vector3D {1.0, 2.0, 7.0})); } -TEST(IOTest, readShapeSet_luaBindingsChunkAndInputVariables) +TEST(IOTest, readShapeSet_luaInitializationCanUseInitialGlobals) { - LuaBindingsChunk bindings {R"( + LuaInitializationChunk initialization {R"( local lift = 3.0 return { lift = lift } )", - "runtime_bindings"}; + "runtime_initialization"}; - InputVariables variables { - {"dimensions", klee::InputVariableValue {2}}, - {"shape_suffix", klee::InputVariableValue {std::string {"2d"}}}, + LuaInitialGlobals initialGlobals { + {"dimensions", klee::LuaGlobalValue {2}}, + {"shape_suffix", klee::LuaGlobalValue {std::string {"2d"}}}, }; LuaInputOptions options; - options.variables = variables; - options.bindings = bindings; + options.initialGlobals = initialGlobals; + options.initialization = initialization; auto shapeSet = readShapeSetFromString(R"( local function shape_path() @@ -799,23 +815,23 @@ TEST(IOTest, readShapeSet_luaBindingsChunkAndInputVariables) EXPECT_THAT(translation->getOffset(), AlmostEqVector(Vector3D {1.0, 3.0, 0.0})); } -TEST(IOTest, readShapeSet_luaBindingsChunkIsolatesUnexportedGlobals) +TEST(IOTest, readShapeSet_luaInitializationIsolatesUnexportedGlobals) { - LuaBindingsChunk bindings {R"( + LuaInitializationChunk initialization {R"( dimensions = 3 - unexported_value = "bindings" + unexported_value = "private" math = { sqrt = function() return -1 end } - _G.also_unexported = "bindings" + _G.also_unexported = "private" return { exported_lift = 4.0 } )", - "runtime_bindings"}; + "runtime_initialization"}; LuaInputOptions options; - options.bindings = bindings; + options.initialization = initialization; auto shapeSet = readShapeSetFromString(R"( dimensions = 2 @@ -853,26 +869,26 @@ TEST(IOTest, readShapeSet_luaBindingsChunkIsolatesUnexportedGlobals) EXPECT_THAT(translation->getOffset(), AlmostEqVector(Vector3D {2.0, 4.0, 0.0})); } -TEST(IOTest, readShapeSet_luaBindingsChunkCannotSetSchemaGlobalsWithoutExporting) +TEST(IOTest, readShapeSet_luaInitializationCannotSetSchemaGlobalsWithoutExporting) { for(const std::string& source : {"dimensions = 2; return {}", "_G.dimensions = 2; return {}"}) { - LuaBindingsChunk bindings {source, "runtime_bindings"}; + LuaInitializationChunk initialization {source, "runtime_initialization"}; LuaInputOptions options; - options.bindings = bindings; + options.initialization = initialization; EXPECT_THROW(readShapeSetFromString("shapes = {}", InputFormat::Lua, options), KleeError); } } -TEST(IOTest, readShapeSet_luaBindingsClosureRetainsIsolatedEnvironment) +TEST(IOTest, readShapeSet_luaInitializationClosureRetainsEnvironment) { - InputVariables variables { - {"dimensions", klee::InputVariableValue {2}}, - {"base_offset", klee::InputVariableValue {1.5}}, + LuaInitialGlobals initialGlobals { + {"dimensions", klee::LuaGlobalValue {2}}, + {"base_offset", klee::LuaGlobalValue {1.5}}, }; - LuaBindingsChunk bindings {R"( + LuaInitializationChunk initialization {R"( private_offset = 3.5 return { @@ -881,10 +897,10 @@ TEST(IOTest, readShapeSet_luaBindingsClosureRetainsIsolatedEnvironment) end } )", - "runtime_bindings"}; + "runtime_initialization"}; LuaInputOptions options; - options.variables = variables; - options.bindings = bindings; + options.initialGlobals = initialGlobals; + options.initialization = initialization; auto shapeSet = readShapeSetFromString(R"( shapes = { @@ -914,32 +930,154 @@ TEST(IOTest, readShapeSet_luaBindingsClosureRetainsIsolatedEnvironment) EXPECT_THAT(translation->getOffset(), AlmostEqVector(Vector3D {1.5, 3.5, 0.0})); } -TEST(IOTest, readShapeSet_luaInputVariableRejectsInvalidName) +TEST(IOTest, readShapeSet_luaInitializationPreservesLuaInteger) +{ + LuaInputOptions options; + options.initialization = LuaInitializationChunk {R"( + return { + exact_integer = 9007199254740993 + } + )", + "integer_initialization"}; + + auto shapeSet = readShapeSetFromString(R"( + dimensions = 2 + local integer_is_exact = exact_integer == 9007199254740993 + shapes = { + { + name = "integer", + material = "steel", + geometry = { + format = "stl", + path = integer_is_exact and "exact.stl" or "rounded.stl", + units = "cm" + } + } + } + )", + InputFormat::Lua, + options); + + ASSERT_EQ(1u, shapeSet.getShapes().size()); + EXPECT_EQ("exact.stl", shapeSet.getShapes()[0].getGeometry().getPath()); +} + +TEST(IOTest, readShapeSet_luaInitializationIsolationIsShallow) +{ + LuaInputOptions options; + options.initialization = LuaInitializationChunk {R"( + math.initialization_value = 4.0 + return {} + )", + "shallow_initialization"}; + + auto shapeSet = readShapeSetFromString(R"( + dimensions = 2 + shapes = { + { + name = "shared_table", + material = "steel", + geometry = { + format = "stl", + path = math.initialization_value == 4.0 and "shared.stl" or "isolated.stl", + units = "cm" + } + } + } + )", + InputFormat::Lua, + options); + + ASSERT_EQ(1u, shapeSet.getShapes().size()); + EXPECT_EQ("shared.stl", shapeSet.getShapes()[0].getGeometry().getPath()); +} + +TEST(IOTest, readShapeSet_luaInitializationRejectsInvalidChunks) +{ + struct InvalidInitialization + { + std::string source; + std::string expectedMessage; + }; + + const std::array invalidInitializations {{ + {"", "empty"}, + {"return {", "Failed to evaluate"}, + {"error('initialization boom')", "initialization boom"}, + {"local value = 2", "must return a table"}, + {"return {[1] = 2}", "string keys"}, + {"return {bad = Vector.new(1, 2)}", "unsupported value type"}, + }}; + + for(const auto& invalid : invalidInitializations) + { + LuaInputOptions options; + options.initialization = + LuaInitializationChunk {invalid.source, "invalid_initialization"}; + + try + { + readShapeSetFromString( + "dimensions = 2; shapes = {}", + InputFormat::Lua, + options); + FAIL() << "Should have thrown"; + } + catch(const KleeError& err) + { + EXPECT_THAT(err.what(), HasSubstr("invalid_initialization")); + EXPECT_THAT(err.what(), HasSubstr(invalid.expectedMessage)); + } + } +} + +TEST(IOTest, readShapeSet_luaInitialGlobalRejectsInvalidName) +{ + const std::array invalidNames {{"", "shape-dim", "\xC3\xA9"}}; + for(const auto& name : invalidNames) + { + LuaInputOptions options; + options.initialGlobals = {{name, klee::LuaGlobalValue {2}}}; + + try + { + readShapeSetFromString(R"( + dimensions = 2 + shapes = {} + )", + InputFormat::Lua, + options); + FAIL() << "Should have thrown"; + } + catch(const KleeError& err) + { + EXPECT_THAT(err.what(), HasSubstr("Invalid initial Lua global name")); + EXPECT_THAT(err.what(), HasSubstr("Lua identifiers")); + } + } +} + +TEST(IOTest, readShapeSet_luaInitialGlobalRejectsKeyword) { LuaInputOptions options; - options.variables = {{"shape-dim", klee::InputVariableValue {2}}}; + options.initialGlobals = {{"end", klee::LuaGlobalValue {2}}}; try { - readShapeSetFromString(R"( - dimensions = 2 - shapes = {} - )", - InputFormat::Lua, - options); + readShapeSetFromString("dimensions = 2; shapes = {}", InputFormat::Lua, options); FAIL() << "Should have thrown"; } catch(const KleeError& err) { - EXPECT_THAT(err.what(), HasSubstr("Invalid Klee Lua input variable name")); - EXPECT_THAT(err.what(), HasSubstr("Lua identifiers")); + EXPECT_THAT(err.what(), HasSubstr("Reserved Lua keywords")); + EXPECT_THAT(err.what(), HasSubstr("end")); } } -TEST(IOTest, readShapeSet_luaInputVariableRejectsKeyword) +TEST(IOTest, readShapeSet_luaInitialGlobalRejectsReservedGlobalName) { LuaInputOptions options; - options.variables = {{"end", klee::InputVariableValue {2}}}; + options.initialGlobals = {{"math", klee::LuaGlobalValue {2}}}; try { @@ -948,20 +1086,20 @@ TEST(IOTest, readShapeSet_luaInputVariableRejectsKeyword) } catch(const KleeError& err) { - EXPECT_THAT(err.what(), HasSubstr("Reserved Lua keywords")); - EXPECT_THAT(err.what(), HasSubstr("end")); + EXPECT_THAT(err.what(), HasSubstr("conflicts with an existing Lua global")); + EXPECT_THAT(err.what(), HasSubstr("math")); } } -TEST(IOTest, readShapeSet_luaBindingsChunkRejectsInvalidExportName) +TEST(IOTest, readShapeSet_luaInitializationRejectsInvalidExportName) { LuaInputOptions options; - options.bindings = LuaBindingsChunk {R"( + options.initialization = LuaInitializationChunk {R"( return { ["shape-dim"] = 2 } )", - "runtime_bindings"}; + "runtime_initialization"}; try { @@ -974,20 +1112,20 @@ TEST(IOTest, readShapeSet_luaBindingsChunkRejectsInvalidExportName) } catch(const KleeError& err) { - EXPECT_THAT(err.what(), HasSubstr("Invalid Klee Lua binding name")); + EXPECT_THAT(err.what(), HasSubstr("Invalid exported Lua global name")); EXPECT_THAT(err.what(), HasSubstr("Lua identifiers")); } } -TEST(IOTest, readShapeSet_luaBindingsChunkRejectsKeywordExport) +TEST(IOTest, readShapeSet_luaInitializationRejectsKeywordExport) { LuaInputOptions options; - options.bindings = LuaBindingsChunk {R"( + options.initialization = LuaInitializationChunk {R"( return { ["function"] = 2 } )", - "runtime_bindings"}; + "runtime_initialization"}; try { @@ -1001,15 +1139,15 @@ TEST(IOTest, readShapeSet_luaBindingsChunkRejectsKeywordExport) } } -TEST(IOTest, readShapeSet_luaBindingsChunkRejectsReservedGlobalName) +TEST(IOTest, readShapeSet_luaInitializationRejectsReservedGlobalName) { LuaInputOptions options; - options.bindings = LuaBindingsChunk {R"( + options.initialization = LuaInitializationChunk {R"( return { math = 2 } )", - "runtime_bindings"}; + "runtime_initialization"}; try { @@ -1027,16 +1165,16 @@ TEST(IOTest, readShapeSet_luaBindingsChunkRejectsReservedGlobalName) } } -TEST(IOTest, readShapeSet_luaBindingsChunkRejectsDuplicateInputVariableName) +TEST(IOTest, readShapeSet_luaInitializationRejectsDuplicateInitialGlobal) { LuaInputOptions options; - options.variables = {{"dimensions", klee::InputVariableValue {2}}}; - options.bindings = LuaBindingsChunk {R"( + options.initialGlobals = {{"dimensions", klee::LuaGlobalValue {2}}}; + options.initialization = LuaInitializationChunk {R"( return { dimensions = 3 } )", - "runtime_bindings"}; + "runtime_initialization"}; try { @@ -1049,15 +1187,16 @@ TEST(IOTest, readShapeSet_luaBindingsChunkRejectsDuplicateInputVariableName) } catch(const KleeError& err) { - EXPECT_THAT(err.what(), HasSubstr("duplicates another external Lua binding")); + EXPECT_THAT(err.what(), HasSubstr("duplicates an initial Lua global")); EXPECT_THAT(err.what(), HasSubstr("dimensions")); } } -TEST(IOTest, readShapeSet_luaBindingsChunkRequiresTableReturn) +TEST(IOTest, readShapeSet_luaInitializationRequiresTableReturn) { LuaInputOptions options; - options.bindings = LuaBindingsChunk {"return 2", "runtime_bindings"}; + options.initialization = + LuaInitializationChunk {"return 2", "runtime_initialization"}; try { @@ -1072,7 +1211,7 @@ TEST(IOTest, readShapeSet_luaBindingsChunkRequiresTableReturn) catch(const KleeError& err) { EXPECT_THAT(err.what(), HasSubstr("must return a table")); - EXPECT_THAT(err.what(), HasSubstr("runtime_bindings")); + EXPECT_THAT(err.what(), HasSubstr("runtime_initialization")); } } diff --git a/src/examples/CMakeLists.txt b/src/examples/CMakeLists.txt index d3d3cb6cc8..a7ec083dc5 100644 --- a/src/examples/CMakeLists.txt +++ b/src/examples/CMakeLists.txt @@ -203,11 +203,11 @@ if(AXOM_ENABLE_TUTORIALS AND AXOM_ENABLE_KLEE AND AXOM_ENABLE_QUEST) COMMAND shaping_tutorial_lesson_03_klee_operators_and_validation ${CMAKE_CURRENT_SOURCE_DIR}/shaping_tutorial/lesson_03/ice_cream.lua) - blt_add_test(NAME shaping_tutorial_lesson_03_klee_operators_and_validation_lua_bindings + blt_add_test(NAME shaping_tutorial_lesson_03_klee_operators_and_validation_lua_initialization COMMAND shaping_tutorial_lesson_03_klee_operators_and_validation - ${CMAKE_CURRENT_SOURCE_DIR}/shaping_tutorial/lesson_03/ice_cream_bindings.lua - --bindings-file - ${CMAKE_CURRENT_SOURCE_DIR}/shaping_tutorial/lesson_03/ice_cream_runtime_bindings.lua) + ${CMAKE_CURRENT_SOURCE_DIR}/shaping_tutorial/lesson_03/ice_cream_initialized.lua + --initialization-file + ${CMAKE_CURRENT_SOURCE_DIR}/shaping_tutorial/lesson_03/ice_cream_initialization.lua) endif() endif() diff --git a/src/examples/shaping_tutorial/CMakeLists.txt b/src/examples/shaping_tutorial/CMakeLists.txt index 175de18b14..30abe5ed22 100644 --- a/src/examples/shaping_tutorial/CMakeLists.txt +++ b/src/examples/shaping_tutorial/CMakeLists.txt @@ -108,11 +108,11 @@ if(ENABLE_TESTS) blt_add_test(NAME lesson_03_klee_operators_and_validation_lua COMMAND lesson_03_klee_operators_and_validation ../lesson_03/ice_cream.lua) - blt_add_test(NAME lesson_03_klee_operators_and_validation_lua_bindings + blt_add_test(NAME lesson_03_klee_operators_and_validation_lua_initialization COMMAND lesson_03_klee_operators_and_validation - ../lesson_03/ice_cream_bindings.lua - --bindings-file - ../lesson_03/ice_cream_runtime_bindings.lua) + ../lesson_03/ice_cream_initialized.lua + --initialization-file + ../lesson_03/ice_cream_initialization.lua) endif() if(AXOM_USE_LUA AND AXOM_USE_MFEM) diff --git a/src/examples/shaping_tutorial/lesson_03/README.md b/src/examples/shaping_tutorial/lesson_03/README.md index 1e59b9cc8e..6f3a9989f2 100644 --- a/src/examples/shaping_tutorial/lesson_03/README.md +++ b/src/examples/shaping_tutorial/lesson_03/README.md @@ -398,7 +398,7 @@ The code example for this lesson loads a Klee file, performs some validation and ### Load and validate the Klee input ```cpp -axom::klee::ShapeSet shapeset +axom::klee::ShapeSet shapeSet; try { shapeSet = axom::klee::readShapeSet(inputFilename); @@ -419,17 +419,17 @@ catch(axom::klee::KleeError& error) } ``` -The validator example also accepts an optional `--bindings-file` argument for -Lua decks. The bindings file is a Lua chunk that returns a table of exported -variables and helper functions, which are installed as initial mutable globals -before the deck is evaluated. This lets an application provide runtime Lua -customization without rebuilding the executable, while still allowing the deck -to reassign those globals if it chooses: +The validator example also accepts an optional `--initialization-file` argument +for Lua decks. The initialization file is a Lua chunk that returns a table of +exported variables and helper functions, which are installed as initial mutable +globals before the deck is evaluated. This lets an application provide runtime +Lua customization without rebuilding the executable, while still allowing the +deck to reassign those globals if it chooses: ```bash ./bin/lesson_03_klee_operators_and_validation \ - ../lesson_03/ice_cream_bindings.lua \ - --bindings-file ../lesson_03/ice_cream_runtime_bindings.lua + ../lesson_03/ice_cream_initialized.lua \ + --initialization-file ../lesson_03/ice_cream_initialization.lua ``` Next, we loop through the shapes and print out information about each shape. We're using an `fmt::memory_buffer` (similar to a `std::stringstream`) to write everything in a single log statement: diff --git a/src/examples/shaping_tutorial/lesson_03/ice_cream_runtime_bindings.lua b/src/examples/shaping_tutorial/lesson_03/ice_cream_initialization.lua similarity index 100% rename from src/examples/shaping_tutorial/lesson_03/ice_cream_runtime_bindings.lua rename to src/examples/shaping_tutorial/lesson_03/ice_cream_initialization.lua diff --git a/src/examples/shaping_tutorial/lesson_03/ice_cream_bindings.lua b/src/examples/shaping_tutorial/lesson_03/ice_cream_initialized.lua similarity index 100% rename from src/examples/shaping_tutorial/lesson_03/ice_cream_bindings.lua rename to src/examples/shaping_tutorial/lesson_03/ice_cream_initialized.lua diff --git a/src/examples/shaping_tutorial/lesson_03/klee_operators_and_validation.cpp b/src/examples/shaping_tutorial/lesson_03/klee_operators_and_validation.cpp index d950e11bd9..7b7f5340d6 100644 --- a/src/examples/shaping_tutorial/lesson_03/klee_operators_and_validation.cpp +++ b/src/examples/shaping_tutorial/lesson_03/klee_operators_and_validation.cpp @@ -142,27 +142,31 @@ int main(int argc, char** argv) // CLI axom::CLI::App app {"Klee Input Validator and Summary"}; std::string inputFilename; - std::string bindingsFilename; + std::string initializationFilename; app.add_option("input", inputFilename) ->description("Klee input file") ->required() ->check(axom::CLI::ExistingFile); - app.add_option("--bindings-file", bindingsFilename) - ->description("Optional Lua chunk that returns a table of runtime bindings") + app.add_option("--initialization-file", initializationFilename) + ->description("Optional Lua chunk that returns a table of initial globals") ->check(axom::CLI::ExistingFile); CLI11_PARSE(app, argc, argv); auto loadShapeSet = [&]() { - if(bindingsFilename.empty()) + if(initializationFilename.empty()) { return axom::klee::readShapeSet(inputFilename); } - std::ifstream bindingsStream {bindingsFilename}; - std::string bindingsSource {std::istreambuf_iterator(bindingsStream), {}}; + std::ifstream initializationStream {initializationFilename}; + std::string initializationSource { + std::istreambuf_iterator(initializationStream), + {}}; axom::klee::LuaInputOptions options; - options.bindings = axom::klee::LuaBindingsChunk {bindingsSource, bindingsFilename}; + options.initialization = axom::klee::LuaInitializationChunk { + initializationSource, + initializationFilename}; return axom::klee::readShapeSet(inputFilename, options); }; From 70c8fc5c5061a1a930af99e8a94ef453dd1d7285 Mon Sep 17 00:00:00 2001 From: Kenneth Weiss Date: Wed, 29 Jul 2026 22:26:38 -0700 Subject: [PATCH 19/52] Inlet: simplifies LuaReader's variant map and index retrieval And improves test coverage. --- src/axom/inlet/LuaReader.cpp | 26 ++++++---- src/axom/inlet/tests/inlet_Reader.cpp | 71 +++++++++++++++++++++++++++ 2 files changed, 87 insertions(+), 10 deletions(-) diff --git a/src/axom/inlet/LuaReader.cpp b/src/axom/inlet/LuaReader.cpp index a4ee720820..3e13b50f90 100644 --- a/src/axom/inlet/LuaReader.cpp +++ b/src/axom/inlet/LuaReader.cpp @@ -749,13 +749,17 @@ ReaderResult LuaReader::getVariantMapInternal(const std::string& id, std::unordered_map& values) { values.clear(); - std::vector tokens = axom::utilities::string::split(id, SCOPE_DELIMITER); - - axom::sol::table table; - if(tokens.empty() || !traverseToTable(tokens.begin(), tokens.end(), table)) + const auto object = getObject(id); + if(!object.valid()) { return ReaderResult::NotFound; } + if(object.get_type() != axom::sol::type::table) + { + return ReaderResult::WrongType; + } + + const auto table = object.as(); const auto is_correct_key_type = [](const axom::sol::type type) { const bool is_number = type == axom::sol::type::number; @@ -788,16 +792,18 @@ ReaderResult LuaReader::getVariantMapInternal(const std::string& id, template ReaderResult LuaReader::getIndicesInternal(const std::string& id, std::vector& indices) { - std::vector tokens = axom::utilities::string::split(id, SCOPE_DELIMITER); - - axom::sol::table table; - if(tokens.empty() || !traverseToTable(tokens.begin(), tokens.end(), table)) + indices.clear(); + const auto object = getObject(id); + if(!object.valid()) { return ReaderResult::NotFound; } + if(object.get_type() != axom::sol::type::table) + { + return ReaderResult::WrongType; + } - indices.clear(); - + const auto table = object.as(); // std::transform ends up being messier here for(const auto& entry : table) { diff --git a/src/axom/inlet/tests/inlet_Reader.cpp b/src/axom/inlet/tests/inlet_Reader.cpp index ab04b30732..9edf4162d6 100644 --- a/src/axom/inlet/tests/inlet_Reader.cpp +++ b/src/axom/inlet/tests/inlet_Reader.cpp @@ -11,6 +11,7 @@ #include "gtest/gtest.h" +#include #include #include #include @@ -507,6 +508,76 @@ TEST(inlet_Reader_lua, functionLookupDoesNotChangeFieldAndMapResults) EXPECT_EQ(ReaderResult::WrongType, reader.getDoubleMap("foo", values)); EXPECT_EQ(ReaderResult::WrongType, reader.getDoubleMap("bar/baz", values)); } + +TEST(inlet_Reader_lua, variantMapsAndIndicesUseConsistentObjectLookup) +{ + axom::inlet::LuaReader reader; + reader.parseString(R"( + callback = function() return {1, 2} end + nested = { + [7] = { + values = {[2] = 42, [5] = "five"}, + dictionary = {[2] = 42, label = true} + } + } + )"); + + auto callback = + reader.getFunction("callback", axom::inlet::FunctionTag::Vector, {}); + ASSERT_TRUE(callback); + + std::unordered_map values { + {99, axom::inlet::VariantValue {99}}}; + EXPECT_EQ(ReaderResult::WrongType, + reader.getVariantMap("callback", values)); + EXPECT_TRUE(values.empty()); + EXPECT_EQ(ReaderResult::NotFound, + reader.getVariantMap("missing", values)); + EXPECT_TRUE(values.empty()); + + EXPECT_EQ(ReaderResult::Success, + reader.getVariantMap("nested/7/values", values)); + const std::unordered_map expectedValues { + {2, axom::inlet::VariantValue {42}}, + {5, axom::inlet::VariantValue {std::string {"five"}}}}; + EXPECT_EQ(expectedValues, values); + + std::vector indices {99}; + EXPECT_EQ(ReaderResult::WrongType, + reader.getIndices("callback", indices)); + EXPECT_TRUE(indices.empty()); + EXPECT_EQ(ReaderResult::NotFound, + reader.getIndices("missing", indices)); + EXPECT_TRUE(indices.empty()); + + EXPECT_EQ(ReaderResult::Success, + reader.getIndices("nested/7/values", indices)); + std::sort(indices.begin(), indices.end()); + EXPECT_EQ((std::vector {2, 5}), indices); + + std::unordered_map + dictionary; + EXPECT_EQ(ReaderResult::Success, + reader.getVariantMap("nested/7/dictionary", dictionary)); + EXPECT_EQ(2u, dictionary.size()); + EXPECT_EQ(axom::inlet::VariantValue {42}, + dictionary[axom::inlet::VariantKey {2}]); + EXPECT_EQ(axom::inlet::VariantValue {true}, + dictionary[axom::inlet::VariantKey {"label"}]); + + std::vector dictionaryIndices; + EXPECT_EQ(ReaderResult::Success, + reader.getIndices("nested/7/dictionary", dictionaryIndices)); + EXPECT_EQ(2u, dictionaryIndices.size()); + EXPECT_NE(dictionaryIndices.end(), + std::find(dictionaryIndices.begin(), + dictionaryIndices.end(), + axom::inlet::VariantKey {2})); + EXPECT_NE(dictionaryIndices.end(), + std::find(dictionaryIndices.begin(), + dictionaryIndices.end(), + axom::inlet::VariantKey {"label"})); +} #endif //------------------------------------------------------------------------------ From 4adf1e914ca536740a802db23529167faaa0ec3a Mon Sep 17 00:00:00 2001 From: Kenneth Weiss Date: Wed, 29 Jul 2026 23:04:22 -0700 Subject: [PATCH 20/52] Inlet: Adds InputPath descriptor for a path to indicate if it is absolute or relative This clarifies semantics when dealing with arrays of collections. --- src/axom/inlet/Container.cpp | 65 +++++++++++++++---- src/axom/inlet/Container.hpp | 73 ++++++++++++++++++++- src/axom/inlet/Inlet.hpp | 39 ++++++++++++ src/axom/inlet/docs/sphinx/functions.rst | 36 +++++++++++ src/axom/inlet/tests/inlet_function.cpp | 81 ++++++++++++++++++++++-- 5 files changed, 274 insertions(+), 20 deletions(-) diff --git a/src/axom/inlet/Container.cpp b/src/axom/inlet/Container.cpp index 3cb98af85a..e415c90104 100644 --- a/src/axom/inlet/Container.cpp +++ b/src/axom/inlet/Container.cpp @@ -940,13 +940,32 @@ Verifiable& Container::addFunction(const std::string& name, const std::string& description, const std::string& pathOverride) { + const auto pathMode = (isStructCollection() || !m_nested_aggregates.empty()) + ? InputPathMode::RelativeToCollectionElement + : InputPathMode::Exact; return addFunctionWithInputPath( name, ret_type, arg_types, description, - pathOverride, - false, + InputPath {pathOverride, pathMode}, + false); +} + +Verifiable& Container::addFunction(const std::string& name, + const FunctionTag ret_type, + const std::vector& arg_types, + const InputPath& inputPath, + const std::string& description) +{ + SLIC_ERROR_IF(inputPath.value.empty(), + "[Inlet] An explicit function input path must be non-empty"); + return addFunctionWithInputPath( + name, + ret_type, + arg_types, + description, + inputPath, false); } @@ -959,13 +978,33 @@ Verifiable& Container::addFunctionAsValueAlternative( { SLIC_ERROR_IF(inputPath.empty(), "[Inlet] A function value alternative requires a non-empty input path"); + const auto pathMode = (isStructCollection() || !m_nested_aggregates.empty()) + ? InputPathMode::RelativeToCollectionElement + : InputPathMode::Exact; + return addFunctionWithInputPath( + name, + ret_type, + arg_types, + description, + InputPath {inputPath, pathMode}, + true); +} + +Verifiable& Container::addFunctionAsValueAlternative( + const std::string& name, + const FunctionTag ret_type, + const std::vector& arg_types, + const InputPath& inputPath, + const std::string& description) +{ + SLIC_ERROR_IF(inputPath.value.empty(), + "[Inlet] A function value alternative requires a non-empty input path"); return addFunctionWithInputPath( name, ret_type, arg_types, description, inputPath, - false, true); } @@ -974,8 +1013,7 @@ Verifiable& Container::addFunctionWithInputPath( const FunctionTag ret_type, const std::vector& arg_types, const std::string& description, - const std::string& inputPath, - const bool inputPathIsRelative, + const InputPath& inputPath, const bool isValueAlternative) { // If it has indices, we're adding a function to an array @@ -989,13 +1027,16 @@ Verifiable& Container::addFunctionWithInputPath( [&name, &ret_type, &arg_types, &description, &inputPath, isValueAlternative]( Container& subcontainer, const std::string& path) -> Verifiable& { - const bool hasPathOverride = !inputPath.empty(); + InputPath nestedInputPath = inputPath; + if(nestedInputPath.value.empty()) + { + nestedInputPath = InputPath::exact(path); + } return subcontainer.addFunctionWithInputPath(name, ret_type, arg_types, description, - hasPathOverride ? inputPath : path, - hasPathOverride, + nestedInputPath, isValueAlternative); }); if(is_nested) @@ -1021,16 +1062,14 @@ Verifiable& Container::addFunctionWithInputPath( SLIC_ERROR_IF(sidreGroup == nullptr, fmt::format("Failed to create Sidre group with name '{0}'", fullName)); detail::addSignatureToGroup(ret_type, arg_types, sidreGroup); - // A caller-provided override becomes relative when a schema is expanded across - // a struct collection. Exact paths supplied by the expansion itself remain unchanged. - std::string lookupPath = inputPath; + std::string lookupPath = inputPath.value; if(lookupPath.empty()) { lookupPath = fullName; } - else if(inputPathIsRelative) + else if(inputPath.mode == InputPathMode::RelativeToCollectionElement) { - lookupPath = Path::join({Path(m_name), Path(inputPath)}); + lookupPath = Path::join({Path(m_name), Path(inputPath.value)}); } lookupPath = utilities::string::removeAllInstances(lookupPath, detail::COLLECTION_GROUP_NAME + "/"); diff --git a/src/axom/inlet/Container.hpp b/src/axom/inlet/Container.hpp index b1bfac5eb2..b559de2de9 100644 --- a/src/axom/inlet/Container.hpp +++ b/src/axom/inlet/Container.hpp @@ -23,6 +23,7 @@ #include #include #include +#include #include #include "axom/fmt.hpp" @@ -67,6 +68,44 @@ class Container; template class VariantStructCollection; +/*! + ***************************************************************************** + * \brief Controls how an aliased input path is resolved. + ***************************************************************************** + */ +enum class InputPathMode +{ + Exact, + RelativeToCollectionElement +}; + +/*! + ***************************************************************************** + * \brief Describes an input path whose resolution semantics must remain + * explicit when a schema is expanded across a struct collection. + ***************************************************************************** + */ +struct InputPath +{ + InputPath(std::string path, InputPathMode pathMode) + : value(std::move(path)) + , mode(pathMode) + { } + + static InputPath exact(std::string path) + { + return InputPath(std::move(path), InputPathMode::Exact); + } + + static InputPath relativeToCollectionElement(std::string path) + { + return InputPath(std::move(path), InputPathMode::RelativeToCollectionElement); + } + + std::string value; + InputPathMode mode; +}; + namespace detail { struct VariantStructFactoryBase @@ -748,6 +787,25 @@ class Container : public Verifiable const std::string& description = "", const std::string& pathOverride = ""); + /*! + ***************************************************************************** + * \brief Get a function from an explicitly resolved input path. + * + * \param [in] name Name of the function in the schema + * \param [in] ret_type The return type of the function + * \param [in] arg_types The argument types of the function + * \param [in] inputPath Explicit input path and resolution mode + * \param [in] description Description of the function + * + * \return Reference to the created Function + ***************************************************************************** + */ + Verifiable& addFunction(const std::string& name, + FunctionTag ret_type, + const std::vector& arg_types, + const InputPath& inputPath, + const std::string& description = ""); + /*! ***************************************************************************** * \brief Get a function that is an alternative representation of a primitive @@ -774,6 +832,18 @@ class Container : public Verifiable const std::string& inputPath, const std::string& description = ""); + /*! + ********************************************************************************* + * \brief Get a function value alternative from an explicitly resolved input path. + ********************************************************************************* + */ + Verifiable& addFunctionAsValueAlternative( + const std::string& name, + FunctionTag ret_type, + const std::vector& arg_types, + const InputPath& inputPath, + const std::string& description = ""); + /*! ******************************************************************************* * \brief Returns a stored value of primitive type. @@ -1305,8 +1375,7 @@ class Container : public Verifiable FunctionTag ret_type, const std::vector& arg_types, const std::string& description, - const std::string& inputPath, - bool inputPathIsRelative, + const InputPath& inputPath, bool isValueAlternative); /*! diff --git a/src/axom/inlet/Inlet.hpp b/src/axom/inlet/Inlet.hpp index 18545fa0ed..4f7e983531 100644 --- a/src/axom/inlet/Inlet.hpp +++ b/src/axom/inlet/Inlet.hpp @@ -431,6 +431,22 @@ class Inlet return m_globalContainer.addFunction(name, ret_type, arg_types, description); } + /*! + ***************************************************************************** + * \brief Get a function from an explicitly resolved input path. + * + * \see Container::addFunction + ***************************************************************************** + */ + Verifiable& addFunction(const std::string& name, + const FunctionTag ret_type, + const std::vector& arg_types, + const InputPath& inputPath, + const std::string& description = "") + { + return m_globalContainer.addFunction(name, ret_type, arg_types, inputPath, description); + } + /*! ***************************************************************************** * \brief Get a function that is an alternative representation of a primitive @@ -454,6 +470,29 @@ class Inlet description); } + /*! + ***************************************************************************** + * \brief Get a function value alternative from an explicitly resolved input + * path. + * + * \see Container::addFunctionAsValueAlternative + ***************************************************************************** + */ + Verifiable& addFunctionAsValueAlternative( + const std::string& name, + FunctionTag ret_type, + const std::vector& arg_types, + const InputPath& inputPath, + const std::string& description = "") + { + return m_globalContainer.addFunctionAsValueAlternative( + name, + ret_type, + arg_types, + inputPath, + description); + } + /*! ***************************************************************************** * \brief Add a dictionary of Boolean Fields to the input file schema. diff --git a/src/axom/inlet/docs/sphinx/functions.rst b/src/axom/inlet/docs/sphinx/functions.rst index 639cefdfb8..8104e67327 100644 --- a/src/axom/inlet/docs/sphinx/functions.rst +++ b/src/axom/inlet/docs/sphinx/functions.rst @@ -63,6 +63,40 @@ In Lua, the following operations on the ``Vector`` type are supported (for ``Vec #. Dimension retrieval: ``d = u.dim`` #. Component retrieval: ``d = u.x``, ``d = u.y``, ``d = u.z`` +Aliased input paths +------------------- + +A function's schema name and its path in the input do not need to match. +Inlet provides an ``InputPath`` descriptor to allow a path to retain the same meaning +when a schema is expanded across a struct array or dictionary: + +.. code-block:: C++ + + using axom::inlet::InputPath; + + // Every element reads the same root-level callback. + shapes.addFunction( + "transform_callback", + axom::inlet::FunctionTag::Vector, + {axom::inlet::FunctionTag::Vector}, + InputPath::exact("shared_transform")); + + // Every element reads its own "transform" callback. + shapes.addFunction( + "transform_callback", + axom::inlet::FunctionTag::Vector, + {axom::inlet::FunctionTag::Vector}, + InputPath::relativeToCollectionElement("transform")); + +An exact path is used unchanged, regardless of where the function is stored in the schema. +A collection-relative path is joined to each concrete collection element. +Outside a collection, it is relative to the current ``Container``. + +The ``addFunction(name, returnType, argumentTypes, description, pathOverride)`` overload +remains available. Its string override retains the rule that +it is exact outside a struct collection and relative to each element inside one. +Prefer ``InputPath`` when adding new aliases so this behavior is explicit at the call site. + Functions as value alternatives ------------------------------- @@ -83,6 +117,8 @@ With this schema, ``scale = {2.0, 3.0, 4.0}`` populates ``scale``, while ``scale = function() return {2.0, 3.0, 4.0} end`` populates ``scale_callback``. The two schema entries may be added in either order. A function encountered at a normal field path remains a type error unless this alternative has been declared. +``addFunctionAsValueAlternative`` also accepts an ``InputPath`` descriptor when exact +or collection-relative resolution needs to be explicit. Accessing --------- diff --git a/src/axom/inlet/tests/inlet_function.cpp b/src/axom/inlet/tests/inlet_function.cpp index 82d0e78e64..d31f95af0a 100644 --- a/src/axom/inlet/tests/inlet_function.cpp +++ b/src/axom/inlet/tests/inlet_function.cpp @@ -23,6 +23,7 @@ using axom::inlet::FunctionTag; using axom::inlet::FunctionType; using axom::inlet::Inlet; using axom::inlet::InletType; +using axom::inlet::InputPath; using axom::inlet::LuaReader; using axom::inlet::VerificationError; @@ -184,6 +185,19 @@ TEST(inlet_function, function_path_override) EXPECT_DOUBLE_EQ(callback(3.0), 5.0); } +TEST(inlet_function, explicit_exact_function_input_path) +{ + auto inlet = createBasicInlet("function public_name (x) return x + 2 end"); + + inlet.addFunction("internal_name", + FunctionTag::Double, + {FunctionTag::Double}, + InputPath::exact("public_name")); + + auto callback = inlet["internal_name"].get>(); + EXPECT_DOUBLE_EQ(callback(3.0), 5.0); +} + TEST(inlet_function, function_value_alternative_is_schema_order_independent) { const auto addSchema = [](Inlet& inlet, bool functionFirst) { @@ -429,6 +443,20 @@ struct FromInlet } }; +struct FooDictionary +{ + std::unordered_map values; +}; + +template <> +struct FromInlet +{ + FooDictionary operator()(const axom::inlet::Container& base) + { + return {base["foo"].get>()}; + } +}; + struct FooWithValueAlternative { std::function bar; @@ -514,7 +542,51 @@ TEST(inlet_function, function_path_override_in_array_of_struct) EXPECT_FLOAT_EQ(foos[12].baz({1, 2, 3})[0], 3); } -TEST(inlet_function, function_value_alternative_in_array_of_struct) +TEST(inlet_function, explicit_exact_function_input_path_in_array_of_struct) +{ + auto inlet = createBasicInlet( + "shared_callback = function (v) return 4*v end; " + "foo = { [7] = { bar = true }, [12] = { bar = false } }"); + + auto& arr_container = inlet.addStructArray("foo"); + arr_container.addBool("bar"); + arr_container.addFunction("baz", + FunctionTag::Vector, + {FunctionTag::Vector}, + InputPath::exact("shared_callback")); + + auto foos = inlet["foo"].get>(); + EXPECT_FLOAT_EQ(foos[7].baz({1, 2, 3})[0], 4); + EXPECT_FLOAT_EQ(foos[12].baz({1, 2, 3})[0], 4); +} + +TEST(inlet_function, explicit_relative_function_input_path_in_nested_dictionary_of_struct) +{ + auto inlet = createBasicInlet( + "groups = { " + " [0] = { foo = { first = { bar = true, " + " callback = function (v) return 2*v end }, " + " second = { bar = false, " + " callback = function (v) return 3*v end } } }, " + " [1] = { foo = { third = { bar = true, " + " callback = function (v) return 4*v end } } } }"); + + auto& group_container = inlet.addStructArray("groups"); + auto& dict_container = group_container.addStructDictionary("foo"); + dict_container.addBool("bar"); + dict_container.addFunction( + "baz", + FunctionTag::Vector, + {FunctionTag::Vector}, + InputPath::relativeToCollectionElement("callback")); + + auto groups = inlet["groups"].get>(); + EXPECT_FLOAT_EQ(groups[0].values["first"].baz({1, 2, 3})[0], 2); + EXPECT_FLOAT_EQ(groups[0].values["second"].baz({1, 2, 3})[0], 3); + EXPECT_FLOAT_EQ(groups[1].values["third"].baz({1, 2, 3})[0], 4); +} + +TEST(inlet_function, explicit_relative_function_value_alternative_in_array_of_struct) { auto inlet = createBasicInlet( "foo = { [7] = { bar = function () return 2 end }, " @@ -526,7 +598,7 @@ TEST(inlet_function, function_value_alternative_in_array_of_struct) "bar_callback", FunctionTag::Double, {}, - "bar"); + InputPath::relativeToCollectionElement("bar")); EXPECT_TRUE(inlet.verify()); auto foos = @@ -606,7 +678,7 @@ TEST(inlet_function, nested_function_in_struct) EXPECT_DOUBLE_EQ(second_func(4.0), 7.0); } -TEST(inlet_function, function_path_override_in_nested_struct) +TEST(inlet_function, explicit_relative_function_input_path_in_nested_struct) { std::string testString = "quux = { [0] = { foo = { callback = function (x) return x + 1 end } }, " @@ -618,8 +690,7 @@ TEST(inlet_function, function_path_override_in_nested_struct) foo_schema.addFunction("bar", FunctionTag::Double, {FunctionTag::Double}, - "", - "callback"); + InputPath::relativeToCollectionElement("callback")); auto foos = inlet["quux"].get>(); EXPECT_DOUBLE_EQ(foos[0].bar(4.0), 5.0); From 57f1f67d0f67a9c84ce773fc7298778fc59320f7 Mon Sep 17 00:00:00 2001 From: Kenneth Weiss Date: Wed, 29 Jul 2026 23:22:35 -0700 Subject: [PATCH 21/52] Inlet: Improves tests coverage and docs for addFunctionAsValueAlternative Misc: Adds some clarifying comments to tests. --- src/axom/inlet/docs/sphinx/functions.rst | 21 +++- src/axom/inlet/tests/inlet_Reader.cpp | 2 + src/axom/inlet/tests/inlet_function.cpp | 152 +++++++++++++++++++++++ 3 files changed, 170 insertions(+), 5 deletions(-) diff --git a/src/axom/inlet/docs/sphinx/functions.rst b/src/axom/inlet/docs/sphinx/functions.rst index 8104e67327..34bb8f2349 100644 --- a/src/axom/inlet/docs/sphinx/functions.rst +++ b/src/axom/inlet/docs/sphinx/functions.rst @@ -100,9 +100,10 @@ Prefer ``InputPath`` when adding new aliases so this behavior is explicit at the Functions as value alternatives ------------------------------- -Some schemas accept either a concrete value or a function that computes that value. Use -``addFunctionAsValueAlternative`` to declare this relationship explicitly. The callback -has its own schema name but reads from the same input path as the concrete field: +Some schemas accept either a concrete value or a function that computes that value. +Use ``addFunctionAsValueAlternative`` to declare this relationship explicitly. +The callback has its own schema name but reads from the same input path +as the concrete field: .. code-block:: C++ @@ -113,13 +114,23 @@ has its own schema name but reads from the same input path as the concrete field "scale"); inlet.addDoubleArray("scale"); -With this schema, ``scale = {2.0, 3.0, 4.0}`` populates ``scale``, while -``scale = function() return {2.0, 3.0, 4.0} end`` populates ``scale_callback``. +With this schema, ``scale = {2.0, 3.0, 4.0}`` populates ``scale``, +while ``scale = function() return {2.0, 3.0, 4.0} end`` populates ``scale_callback``. The two schema entries may be added in either order. A function encountered at a normal field path remains a type error unless this alternative has been declared. ``addFunctionAsValueAlternative`` also accepts an ``InputPath`` descriptor when exact or collection-relative resolution needs to be explicit. +Only the selected schema entry exists: ``contains`` reports the concrete field when a +value was supplied and the function entry when a callback was supplied. A value with an +unrelated type matches neither entry and fails verification. The shared input path is +recognized by strict containers and is not reported as unexpected. + +The returned function and the concrete field remain independently verifiable schema entries. +Consequently, ``required()`` and registered verifiers apply to the entry on +which they are configured. The narrow value-alternative API does not currently provide +a group-level annotation meaning "either representation is required." + Accessing --------- diff --git a/src/axom/inlet/tests/inlet_Reader.cpp b/src/axom/inlet/tests/inlet_Reader.cpp index 9edf4162d6..55948f1cac 100644 --- a/src/axom/inlet/tests/inlet_Reader.cpp +++ b/src/axom/inlet/tests/inlet_Reader.cpp @@ -493,6 +493,7 @@ TEST(inlet_Reader_lua, functionLookupDoesNotChangeFieldAndMapResults) "foo = function() return 1 end\n" "bar = { baz = function() return {1, 2, 3} end }"); + // Function lookup must not cache a coercion that changes later typed reads. auto scalarFunction = reader.getFunction("foo", axom::inlet::FunctionTag::Double, {}); auto vectorFunction = @@ -526,6 +527,7 @@ TEST(inlet_Reader_lua, variantMapsAndIndicesUseConsistentObjectLookup) reader.getFunction("callback", axom::inlet::FunctionTag::Vector, {}); ASSERT_TRUE(callback); + // Map and index queries should agree on missing, non-table, and nested objects. std::unordered_map values { {99, axom::inlet::VariantValue {99}}}; EXPECT_EQ(ReaderResult::WrongType, diff --git a/src/axom/inlet/tests/inlet_function.cpp b/src/axom/inlet/tests/inlet_function.cpp index d31f95af0a..3316b17400 100644 --- a/src/axom/inlet/tests/inlet_function.cpp +++ b/src/axom/inlet/tests/inlet_function.cpp @@ -83,6 +83,26 @@ TEST(inlet_function, simple_vec3_to_vec3_raw_table_return) EXPECT_FLOAT_EQ(result[2], 6); } +TEST(inlet_function, vector_function_accepts_one_and_two_entry_table_returns) +{ + auto inlet = createBasicInlet( + "function one () return {4.0} end\n" + "function two () return {4.0, 5.0} end"); + + auto one = inlet.reader().getFunction("one", FunctionTag::Vector, {}); + ASSERT_TRUE(one); + const auto oneResult = one.call(); + EXPECT_EQ(oneResult.dim, 1); + EXPECT_FLOAT_EQ(oneResult[0], 4.0); + + auto two = inlet.reader().getFunction("two", FunctionTag::Vector, {}); + ASSERT_TRUE(two); + const auto twoResult = two.call(); + EXPECT_EQ(twoResult.dim, 2); + EXPECT_FLOAT_EQ(twoResult[0], 4.0); + EXPECT_FLOAT_EQ(twoResult[1], 5.0); +} + TEST(inlet_function, vector_function_rejects_scalar_return) { auto inlet = createBasicInlet("function foo () return 2.0 end"); @@ -94,6 +114,7 @@ TEST(inlet_function, vector_function_rejects_scalar_return) TEST(inlet_function, vector_function_rejects_malformed_table_returns) { + // Lua vectors must be dense numeric sequences with a supported dimension. const std::array inputs {{ "function foo () return {} end", "function foo () return {1, 2, 3, 4} end", @@ -111,6 +132,39 @@ TEST(inlet_function, vector_function_rejects_malformed_table_returns) } } +TEST(inlet_function, scalar_and_string_functions_reject_wrong_return_types) +{ + auto inlet = createBasicInlet( + "function scalar () return 'not a number' end\n" + "function string () return {} end"); + + auto scalar = inlet.reader().getFunction("scalar", FunctionTag::Double, {}); + ASSERT_TRUE(scalar); + EXPECT_THROW(scalar.call(), std::runtime_error); + + auto string = inlet.reader().getFunction("string", FunctionTag::String, {}); + ASSERT_TRUE(string); + EXPECT_THROW(string.call(), std::runtime_error); +} + +TEST(inlet_function, lua_callback_runtime_error_is_catchable) +{ + auto inlet = createBasicInlet("function foo () error('callback failed') end"); + auto func = inlet.reader().getFunction("foo", FunctionTag::Double, {}); + ASSERT_TRUE(func); + + try + { + func.call(); + FAIL() << "Expected the Lua callback to throw"; + } + catch(const std::runtime_error& error) + { + EXPECT_NE(std::string(error.what()).find("callback failed"), + std::string::npos); + } +} + TEST(inlet_function, simple_vec3_to_vec3_raw_partial_init) { std::string testString = "function foo (v) return 2*v end"; @@ -200,6 +254,7 @@ TEST(inlet_function, explicit_exact_function_input_path) TEST(inlet_function, function_value_alternative_is_schema_order_independent) { + // Both schema entries inspect "foo"; declaration order must not select one. const auto addSchema = [](Inlet& inlet, bool functionFirst) { if(!functionFirst) { @@ -244,8 +299,84 @@ TEST(inlet_function, function_value_alternative_preserves_concrete_value) EXPECT_DOUBLE_EQ(inlet["foo"].get(), 4.0); } +TEST(inlet_function, required_function_value_alternative_missing) +{ + auto inlet = createBasicInlet(""); + inlet.addDouble("foo"); + inlet + .addFunctionAsValueAlternative( + "foo_callback", + FunctionTag::Double, + {}, + "foo") + .required(); + + std::vector errors; + EXPECT_FALSE(inlet.verify(&errors)); + EXPECT_FALSE(errors.empty()); + EXPECT_FALSE(inlet.contains("foo")); + EXPECT_FALSE(inlet.contains("foo_callback")); + EXPECT_FALSE(inlet.getGlobalContainer().exists()); +} + +TEST(inlet_function, function_value_alternative_rejects_unrelated_wrong_type) +{ + const auto addSchema = [](Inlet& inlet, bool functionFirst) { + if(!functionFirst) + { + inlet.addDouble("foo"); + } + inlet.addFunctionAsValueAlternative( + "foo_callback", + FunctionTag::Double, + {}, + "foo"); + if(functionFirst) + { + inlet.addDouble("foo"); + } + }; + + for(const bool functionFirst : {true, false}) + { + auto inlet = createBasicInlet("foo = 'not a number or function'"); + addSchema(inlet, functionFirst); + + EXPECT_FALSE(inlet.verify()); + EXPECT_FALSE(inlet.contains("foo")); + EXPECT_FALSE(inlet.contains("foo_callback")); + // The input exists even though neither schema entry accepts its type. + EXPECT_TRUE(inlet.isUserProvided("foo")); + EXPECT_FALSE(inlet.isUserProvided("foo_callback")); + EXPECT_FALSE(inlet.getGlobalContainer().exists()); + } +} + +TEST(inlet_function, function_value_alternative_is_valid_in_strict_container) +{ + for(const bool useFunction : {true, false}) + { + auto inlet = + createBasicInlet(useFunction ? "function foo () return 2.0 end" : "foo = 4.0"); + inlet.getGlobalContainer().strict(); + inlet.addFunctionAsValueAlternative( + "foo_callback", + FunctionTag::Double, + {}, + "foo"); + inlet.addDouble("foo"); + + EXPECT_TRUE(inlet.verify()); + EXPECT_TRUE(inlet.unexpectedNames().empty()); + EXPECT_EQ(inlet.contains("foo_callback"), useFunction); + EXPECT_EQ(inlet.contains("foo"), !useFunction); + EXPECT_TRUE(inlet.getGlobalContainer().exists()); + } +} + TEST(inlet_function, returned_function_keeps_lua_state_alive) { + // An extracted callback must retain its Lua state after Inlet is destroyed. std::function callback; { auto inlet = createBasicInlet( @@ -257,6 +388,26 @@ TEST(inlet_function, returned_function_keeps_lua_state_alive) EXPECT_DOUBLE_EQ(callback(4.0), 7.0); } +TEST(inlet_function, returned_functions_share_their_lua_state) +{ + std::function increment; + std::function current; + { + auto inlet = createBasicInlet( + "value = 0\n" + "function increment () value = value + 1; return value end\n" + "function current () return value end"); + inlet.addFunction("increment", FunctionTag::Double, {}); + inlet.addFunction("current", FunctionTag::Double, {}); + increment = inlet["increment"].get>(); + current = inlet["current"].get>(); + } + + EXPECT_DOUBLE_EQ(current(), 0.0); + EXPECT_DOUBLE_EQ(increment(), 1.0); + EXPECT_DOUBLE_EQ(current(), 1.0); +} + TEST(inlet_function, simple_void_to_double_through_container) { std::string testString = "function foo () return 9.64 end"; @@ -574,6 +725,7 @@ TEST(inlet_function, explicit_relative_function_input_path_in_nested_dictionary_ auto& group_container = inlet.addStructArray("groups"); auto& dict_container = group_container.addStructDictionary("foo"); dict_container.addBool("bar"); + // Resolve "callback" from each dictionary value, not the enclosing schema. dict_container.addFunction( "baz", FunctionTag::Vector, From e2655c54533a5acf3fa1b98c68b39751ec409db4 Mon Sep 17 00:00:00 2001 From: Kenneth Weiss Date: Thu, 30 Jul 2026 01:23:31 -0700 Subject: [PATCH 22/52] Inlet: Adds automatically named functions to inlet and container Uses this in Klee to replace custom functionality for automatically named functions. --- src/axom/inlet/Container.cpp | 78 ++++++++++++++++++- src/axom/inlet/Container.hpp | 53 +++++++++++++ src/axom/inlet/Inlet.hpp | 41 ++++++++++ src/axom/inlet/docs/sphinx/functions.rst | 30 ++++--- src/axom/inlet/tests/inlet_function.cpp | 35 +++++++++ .../klee/docs/sphinx/specifying_shapes.rst | 2 +- src/axom/klee/io/GeometryOperatorsIO.cpp | 37 ++------- 7 files changed, 231 insertions(+), 45 deletions(-) diff --git a/src/axom/inlet/Container.cpp b/src/axom/inlet/Container.cpp index e415c90104..6310158219 100644 --- a/src/axom/inlet/Container.cpp +++ b/src/axom/inlet/Container.cpp @@ -1008,6 +1008,47 @@ Verifiable& Container::addFunctionAsValueAlternative( true); } +Verifiable& Container::addFunctionAsValueAlternative( + const FunctionTag ret_type, + const std::vector& arg_types, + const std::string& inputPath, + const std::string& description) +{ + return addFunctionAsValueAlternative( + nextFunctionValueAlternativeName(), + ret_type, + arg_types, + inputPath, + description); +} + +Verifiable& Container::addFunctionAsValueAlternative( + const FunctionTag ret_type, + const std::vector& arg_types, + const InputPath& inputPath, + const std::string& description) +{ + return addFunctionAsValueAlternative( + nextFunctionValueAlternativeName(), + ret_type, + arg_types, + inputPath, + description); +} + +std::string Container::nextFunctionValueAlternativeName() +{ + std::string name; + std::string fullName; + do + { + name = + axom::fmt::format("__inlet_function_value_alternative_{}", m_nextFunctionValueAlternativeId++); + fullName = utilities::string::appendPrefix(m_name, name); + } while(m_sidreRootGroup->hasGroup(fullName)); + return name; +} + Verifiable& Container::addFunctionWithInputPath( const std::string& name, const FunctionTag ret_type, @@ -1079,7 +1120,12 @@ Verifiable& Container::addFunctionWithInputPath( { registerFunctionAlternativePath(lookupPath); } - return storeFunction(sidreGroup, std::move(func), fullName, name); + auto& storedFunction = storeFunction(sidreGroup, std::move(func), fullName, name); + if(isValueAlternative) + { + m_functionValueAlternatives[inputPath.value] = &storedFunction; + } + return storedFunction; } } @@ -1476,5 +1522,35 @@ const std::unordered_map>& Container::get return m_functionChildren; } +bool Container::containsFunctionValueAlternative(const std::string& inputPath) const +{ + const auto iter = m_functionValueAlternatives.find(inputPath); + return iter != m_functionValueAlternatives.end() && + static_cast(*iter->second); +} + +Function& Container::getFunctionValueAlternative(const std::string& inputPath) const +{ + const auto iter = m_functionValueAlternatives.find(inputPath); + SLIC_ERROR_IF( + iter == m_functionValueAlternatives.end(), + axom::fmt::format("[Inlet] Function value alternative not found for input path: {0}", inputPath)); + + return *iter->second; +} + +std::vector Container::getFunctionValueAlternativeNames() const +{ + std::vector result; + for(const auto& entry : m_functionValueAlternatives) + { + if(static_cast(*entry.second)) + { + result.push_back(entry.first); + } + } + return result; +} + } // namespace inlet } // namespace axom diff --git a/src/axom/inlet/Container.hpp b/src/axom/inlet/Container.hpp index b559de2de9..4fd56cde04 100644 --- a/src/axom/inlet/Container.hpp +++ b/src/axom/inlet/Container.hpp @@ -844,6 +844,32 @@ class Container : public Verifiable const InputPath& inputPath, const std::string& description = ""); + /*! + ********************************************************************************* + * \brief Add an automatically named function value alternative. + * + * The function remains accessible through \a inputPath using + * getFunctionValueAlternative(), without exposing its internal storage name. + ********************************************************************************* + */ + Verifiable& addFunctionAsValueAlternative( + FunctionTag ret_type, + const std::vector& arg_types, + const std::string& inputPath, + const std::string& description = ""); + + /*! + ********************************************************************************* + * \brief Add an automatically named function value alternative from an + * explicitly resolved input path. + ********************************************************************************* + */ + Verifiable& addFunctionAsValueAlternative( + FunctionTag ret_type, + const std::vector& arg_types, + const InputPath& inputPath, + const std::string& description = ""); + /*! ******************************************************************************* * \brief Returns a stored value of primitive type. @@ -1125,6 +1151,29 @@ class Container : public Verifiable */ const std::unordered_map>& getChildFunctions() const; + /*! + ***************************************************************************** + * \brief Return whether a function value alternative was supplied at the + * given input path. + ***************************************************************************** + */ + bool containsFunctionValueAlternative(const std::string& inputPath) const; + + /*! + ***************************************************************************** + * \brief Retrieve the function value alternative associated with an input + * path. + ***************************************************************************** + */ + Function& getFunctionValueAlternative(const std::string& inputPath) const; + + /*! + ***************************************************************************** + * \brief Return the input paths of supplied function value alternatives. + ***************************************************************************** + */ + std::vector getFunctionValueAlternativeNames() const; + /*! ***************************************************************************** * \return The full name of this Container. @@ -1378,6 +1427,8 @@ class Container : public Verifiable const InputPath& inputPath, bool isValueAlternative); + std::string nextFunctionValueAlternativeName(); + /*! ***************************************************************************** * \brief Adjust a Reader result when a function satisfies a declared value @@ -1593,7 +1644,9 @@ class Container : public Verifiable std::unordered_map> m_fieldChildren; std::unordered_map> m_functionChildren; std::unordered_set m_functionAlternativePaths; + std::unordered_map m_functionValueAlternatives; std::unordered_multimap m_valueInputPathGroups; + std::size_t m_nextFunctionValueAlternativeId {0}; Verifier m_verifier; // Used for ownership only - need to take ownership of these so children diff --git a/src/axom/inlet/Inlet.hpp b/src/axom/inlet/Inlet.hpp index 4f7e983531..934d2c2953 100644 --- a/src/axom/inlet/Inlet.hpp +++ b/src/axom/inlet/Inlet.hpp @@ -493,6 +493,47 @@ class Inlet description); } + /*! + ***************************************************************************** + * \brief Add an automatically named function value alternative. + * + * \see Container::addFunctionAsValueAlternative + ***************************************************************************** + */ + Verifiable& addFunctionAsValueAlternative( + FunctionTag ret_type, + const std::vector& arg_types, + const std::string& inputPath, + const std::string& description = "") + { + return m_globalContainer.addFunctionAsValueAlternative( + ret_type, + arg_types, + inputPath, + description); + } + + /*! + ***************************************************************************** + * \brief Add an automatically named function value alternative from an + * explicitly resolved input path. + * + * \see Container::addFunctionAsValueAlternative + ***************************************************************************** + */ + Verifiable& addFunctionAsValueAlternative( + FunctionTag ret_type, + const std::vector& arg_types, + const InputPath& inputPath, + const std::string& description = "") + { + return m_globalContainer.addFunctionAsValueAlternative( + ret_type, + arg_types, + inputPath, + description); + } + /*! ***************************************************************************** * \brief Add a dictionary of Boolean Fields to the input file schema. diff --git a/src/axom/inlet/docs/sphinx/functions.rst b/src/axom/inlet/docs/sphinx/functions.rst index 34bb8f2349..dd3204d191 100644 --- a/src/axom/inlet/docs/sphinx/functions.rst +++ b/src/axom/inlet/docs/sphinx/functions.rst @@ -93,7 +93,7 @@ A collection-relative path is joined to each concrete collection element. Outside a collection, it is relative to the current ``Container``. The ``addFunction(name, returnType, argumentTypes, description, pathOverride)`` overload -remains available. Its string override retains the rule that +remains available. Its string override retains the rule that it is exact outside a struct collection and relative to each element inside one. Prefer ``InputPath`` when adding new aliases so this behavior is explicit at the call site. @@ -101,30 +101,36 @@ Functions as value alternatives ------------------------------- Some schemas accept either a concrete value or a function that computes that value. -Use ``addFunctionAsValueAlternative`` to declare this relationship explicitly. -The callback has its own schema name but reads from the same input path -as the concrete field: +Use ``addFunctionAsValueAlternative`` to declare this relationship explicitly. +The recommended overload lets Inlet own the callback's internal storage name and +associates it with the concrete field's input path: .. code-block:: C++ inlet.addFunctionAsValueAlternative( - "scale_callback", axom::inlet::FunctionTag::Vector, {}, "scale"); inlet.addDoubleArray("scale"); With this schema, ``scale = {2.0, 3.0, 4.0}`` populates ``scale``, -while ``scale = function() return {2.0, 3.0, 4.0} end`` populates ``scale_callback``. +while ``scale = function() return {2.0, 3.0, 4.0} end`` supplies the function +alternative associated with ``scale``. Use ``containsFunctionValueAlternative("scale")`` +and ``getFunctionValueAlternative("scale")`` on the containing ``Container`` to +query and retrieve it without depending on an internal schema name. + +An overload that takes a schema name first remains available when the callback needs +an independently addressable schema entry. Both forms accept an ``InputPath`` descriptor +when exact or collection-relative resolution needs to be explicit. + The two schema entries may be added in either order. A function encountered at a normal field path remains a type error unless this alternative has been declared. -``addFunctionAsValueAlternative`` also accepts an ``InputPath`` descriptor when exact -or collection-relative resolution needs to be explicit. -Only the selected schema entry exists: ``contains`` reports the concrete field when a -value was supplied and the function entry when a callback was supplied. A value with an -unrelated type matches neither entry and fails verification. The shared input path is -recognized by strict containers and is not reported as unexpected. +Only the selected representation exists: ``contains`` reports the concrete field when a +value was supplied, and ``containsFunctionValueAlternative`` reports the callback when a +function was supplied. A value with an unrelated type matches neither representation and +fails verification. The shared input path is recognized by strict containers and is not +reported as unexpected. The returned function and the concrete field remain independently verifiable schema entries. Consequently, ``required()`` and registered verifiers apply to the entry on diff --git a/src/axom/inlet/tests/inlet_function.cpp b/src/axom/inlet/tests/inlet_function.cpp index 3316b17400..f0b4c6cb18 100644 --- a/src/axom/inlet/tests/inlet_function.cpp +++ b/src/axom/inlet/tests/inlet_function.cpp @@ -299,6 +299,41 @@ TEST(inlet_function, function_value_alternative_preserves_concrete_value) EXPECT_DOUBLE_EQ(inlet["foo"].get(), 4.0); } +TEST(inlet_function, auto_named_function_value_alternative_uses_input_path) +{ + // set and access function in alternative + { + auto inlet = createBasicInlet("function foo () return 2.0 end"); + inlet.addFunctionAsValueAlternative(FunctionTag::Double, {}, "foo"); + inlet.addDouble("foo"); + + EXPECT_TRUE(inlet.verify()); + auto& container = inlet.getGlobalContainer(); + EXPECT_TRUE(container.containsFunctionValueAlternative("foo")); + + const auto names = container.getFunctionValueAlternativeNames(); + ASSERT_EQ(names.size(), 1u); + EXPECT_EQ(names[0], "foo"); + EXPECT_DOUBLE_EQ( + container.getFunctionValueAlternative("foo").call(), + 2.0); + } + + // set and access value in alternative + { + auto concreteInlet = createBasicInlet("foo = 4.0"); + concreteInlet.addFunctionAsValueAlternative(FunctionTag::Double, {}, "foo"); + concreteInlet.addDouble("foo"); + + EXPECT_TRUE(concreteInlet.verify()); + auto& concreteContainer = concreteInlet.getGlobalContainer(); + EXPECT_FALSE(concreteContainer.containsFunctionValueAlternative("foo")); + + EXPECT_TRUE(concreteContainer.getFunctionValueAlternativeNames().empty()); + EXPECT_DOUBLE_EQ(concreteInlet["foo"].get(), 4.0); + } +} + TEST(inlet_function, required_function_value_alternative_missing) { auto inlet = createBasicInlet(""); diff --git a/src/axom/klee/docs/sphinx/specifying_shapes.rst b/src/axom/klee/docs/sphinx/specifying_shapes.rst index 0ea6951d85..530e085694 100644 --- a/src/axom/klee/docs/sphinx/specifying_shapes.rst +++ b/src/axom/klee/docs/sphinx/specifying_shapes.rst @@ -276,7 +276,7 @@ functions. Callbacks should be pure functions of local deck variables. Vector-valued callbacks return raw numeric Lua tables such as :code:`{x, y}` or :code:`{x, y, z}`. The typed :code:`Vector.new(...)` object is also accepted. -Scalar-valued callbacks return a number. +Scalar-valued callbacks return a number. Supported callback fields are :code:`translate`, :code:`axis`, :code:`center`, :code:`scale`, :code:`slice.origin`, :code:`slice.normal`, :code:`slice.up`, :code:`rotate`, diff --git a/src/axom/klee/io/GeometryOperatorsIO.cpp b/src/axom/klee/io/GeometryOperatorsIO.cpp index a62e0594e2..1ce5d2ffae 100644 --- a/src/axom/klee/io/GeometryOperatorsIO.cpp +++ b/src/axom/klee/io/GeometryOperatorsIO.cpp @@ -49,30 +49,9 @@ std::string childName(const inlet::Container& container, const std::string& name return result; } -// Callback schema entries are internal aliases: the public input path remains -// the ordinary operator field name, while Inlet stores the function separately. -constexpr char const *LUA_CALLBACK_SUFFIX = "__klee_lua_callback"; - -std::string callbackName(char const *fieldName) -{ - return std::string(fieldName) + LUA_CALLBACK_SUFFIX; -} - -std::string publicNameForCallback(const std::string &childName) -{ - const std::string suffix = LUA_CALLBACK_SUFFIX; - if(childName.size() > suffix.size() && - childName.compare(childName.size() - suffix.size(), suffix.size(), suffix) == 0) - { - return childName.substr(0, childName.size() - suffix.size()); - } - return childName; -} - bool hasCallback(const inlet::Container &container, char const *fieldName) { - const auto name = callbackName(fieldName); - return container.contains(name); + return container.containsFunctionValueAlternative(fieldName); } bool containsFieldOrCallback(const inlet::Container &container, char const *fieldName) @@ -139,7 +118,7 @@ double getScalar(const inlet::Container &container, if(hasCallback(container, fieldName)) { return wrapCallbackErrors(container, fieldName, shapeName, [&]() { - return container[callbackName(fieldName)].call(); + return container.getFunctionValueAlternative(fieldName).call(); }); } return container[fieldName].get(); @@ -165,7 +144,7 @@ std::vector getDoubleVector(const inlet::Container &container, { auto values = wrapCallbackErrors>(container, fieldName, shapeName, [&]() { return callbackVectorToDoubleVector( - container[callbackName(fieldName)].call()); + container.getFunctionValueAlternative(fieldName).call()); }); auto actualSize = values.size(); auto expectedSize = static_cast(expectedDims); @@ -270,12 +249,9 @@ std::unordered_set getChildNames(const inlet::Container& container) } } - for(auto& child : container.getChildFunctions()) + for(const auto &name : container.getFunctionValueAlternativeNames()) { - if(*child.second) - { - allChildren.insert(publicNameForCallback(childName(container, child.first))); - } + allChildren.insert(name); } return allChildren; @@ -605,7 +581,7 @@ OpPtr parseScale(const SingleOperatorData &data, shapeName, [&]() { return callbackVectorToDoubleVector( - opContainer[callbackName("scale")].call()); + opContainer.getFunctionValueAlternative("scale").call()); }) : opContainer["scale"].get>(); @@ -798,7 +774,6 @@ inlet::Container &GeometryOperatorData::defineSchema(inlet::Container &parent, const auto addCallbackAlternative = [](inlet::Container &container, const char *fieldName, inlet::FunctionTag returnType) { container.addFunctionAsValueAlternative( - callbackName(fieldName), returnType, {}, fieldName); From 3f32257db54e1ed55186bcc21c626444428bcfb6 Mon Sep 17 00:00:00 2001 From: Kenneth Weiss Date: Thu, 30 Jul 2026 02:09:42 -0700 Subject: [PATCH 23/52] Klee: Ensure callbacks work for named operators --- src/axom/inlet/Container.cpp | 2 +- .../klee/docs/sphinx/specifying_shapes.rst | 13 +- src/axom/klee/io/GeometryOperatorsIO.cpp | 118 ++++++++--------- src/axom/klee/io/GeometryOperatorsIO.hpp | 4 +- src/axom/klee/io/IO.cpp | 4 +- src/axom/klee/tests/klee_io.cpp | 119 ++++++++++++++++++ 6 files changed, 196 insertions(+), 64 deletions(-) diff --git a/src/axom/inlet/Container.cpp b/src/axom/inlet/Container.cpp index 6310158219..4529f3bc1f 100644 --- a/src/axom/inlet/Container.cpp +++ b/src/axom/inlet/Container.cpp @@ -1535,7 +1535,7 @@ Function& Container::getFunctionValueAlternative(const std::string& inputPath) c SLIC_ERROR_IF( iter == m_functionValueAlternatives.end(), axom::fmt::format("[Inlet] Function value alternative not found for input path: {0}", inputPath)); - + return *iter->second; } diff --git a/src/axom/klee/docs/sphinx/specifying_shapes.rst b/src/axom/klee/docs/sphinx/specifying_shapes.rst index 530e085694..437cf1cd80 100644 --- a/src/axom/klee/docs/sphinx/specifying_shapes.rst +++ b/src/axom/klee/docs/sphinx/specifying_shapes.rst @@ -241,6 +241,9 @@ Selected operator fields may also be written as zero-argument Lua callbacks. Klee evaluates each callback exactly once while reading the deck; the resulting shape still contains ordinary affine or slice operators, not runtime Lua functions. Callbacks should be pure functions of local deck variables. +Callbacks in a named operator are evaluated when that named operator is +constructed. Each :code:`ref` reuses the resulting concrete operator rather +than evaluating its callbacks again for the referring shape. .. code-block:: lua @@ -308,8 +311,8 @@ or inspect :code:`getErrors()` when multiple verification errors are available. Klee may still throw standard exceptions such as :code:`std::logic_error` or :code:`std::invalid_argument` for programming errors or inconsistent manually constructed objects. -Callback failures include the field, shape name when available, and operator -location, for example: +Callback failures include the field, owning shape or named operator, +and operator location, for example: .. code-block:: text @@ -642,6 +645,12 @@ object. This is a list where each entry has the following values: last operator is specified. It is an error if the units aren't properly converted to `end_units` after applying all operations. +For Lua input, named-operator values support the same callback-capable fields +as shape operators. Klee constructs named operators before shapes, evaluates +each of their callbacks once, and shares that concrete result through every :code:`ref`. +A named-operator callback therefore cannot depend on the identity of a shape +that later refers to it. + The example below demonstrates how to create and then use a named operator. Notice how we can use multiple :code:`ref` entries in the list of operators and we can intermix these with other operators as needed. diff --git a/src/axom/klee/io/GeometryOperatorsIO.cpp b/src/axom/klee/io/GeometryOperatorsIO.cpp index 1ce5d2ffae..1207def133 100644 --- a/src/axom/klee/io/GeometryOperatorsIO.cpp +++ b/src/axom/klee/io/GeometryOperatorsIO.cpp @@ -66,7 +66,7 @@ Path fieldPath(const inlet::Container &container, char const *fieldName) std::string callbackContext(const inlet::Container &container, char const *fieldName, - const std::string &shapeName) + const std::string &ownerLabel) { Path path {container.name()}; std::string operatorIndex = path.baseName(); @@ -77,20 +77,21 @@ std::string callbackContext(const inlet::Container &container, const auto operatorLabel = operatorIndex.empty() ? std::string {"operator at "} + container.name() : std::string {"operator "} + operatorIndex; - if(shapeName.empty()) + if(ownerLabel.empty()) { return axom::fmt::format("Error evaluating callback for '{}' in {}", fieldName, operatorLabel); } - return axom::fmt::format("Error evaluating callback for '{}' in shape '{}' {}", - fieldName, - shapeName, - operatorLabel); + return axom::fmt::format( + "Error evaluating callback for '{}' in {} {}", + fieldName, + ownerLabel, + operatorLabel); } template Result wrapCallbackErrors(const inlet::Container &container, char const *fieldName, - const std::string &shapeName, + const std::string &ownerLabel, Func &&func) { // Convert generic Inlet/Lua callback failures into Klee diagnostics at the @@ -107,17 +108,17 @@ Result wrapCallbackErrors(const inlet::Container &container, { throw KleeError( {fieldPath(container, fieldName), - axom::fmt::format("{}: {}", callbackContext(container, fieldName, shapeName), ex.what())}); + axom::fmt::format("{}: {}", callbackContext(container, fieldName, ownerLabel), ex.what())}); } } double getScalar(const inlet::Container &container, char const *fieldName, - const std::string &shapeName) + const std::string &ownerLabel) { if(hasCallback(container, fieldName)) { - return wrapCallbackErrors(container, fieldName, shapeName, [&]() { + return wrapCallbackErrors(container, fieldName, ownerLabel, [&]() { return container.getFunctionValueAlternative(fieldName).call(); }); } @@ -138,11 +139,11 @@ std::vector callbackVectorToDoubleVector(const inlet::FunctionType::Vect std::vector getDoubleVector(const inlet::Container &container, char const *fieldName, Dimensions expectedDims, - const std::string &shapeName) + const std::string &ownerLabel) { if(hasCallback(container, fieldName)) { - auto values = wrapCallbackErrors>(container, fieldName, shapeName, [&]() { + auto values = wrapCallbackErrors>(container, fieldName, ownerLabel, [&]() { return callbackVectorToDoubleVector( container.getFunctionValueAlternative(fieldName).call()); }); @@ -152,7 +153,7 @@ std::vector getDoubleVector(const inlet::Container &container, { throw KleeError({fieldPath(container, fieldName), fmt::format("{}: Wrong size for {}. Expected {}. Got {}.", - callbackContext(container, fieldName, shapeName), + callbackContext(container, fieldName, ownerLabel), fieldName, expectedSize, actualSize)}); @@ -166,9 +167,9 @@ template T toArrayLike(const inlet::Container &parent, char const *fieldName, Dimensions expectedDims, - const std::string &shapeName) + const std::string &ownerLabel) { - auto values = getDoubleVector(parent, fieldName, expectedDims, shapeName); + auto values = getDoubleVector(parent, fieldName, expectedDims, ownerLabel); return T {values.data(), static_cast(expectedDims)}; } @@ -177,11 +178,11 @@ T toArrayLike(const inlet::Container &parent, char const *fieldName, Dimensions expectedDims, const T &defaultValue, - const std::string &shapeName) + const std::string &ownerLabel) { if(containsFieldOrCallback(parent, fieldName)) { - return toArrayLike(parent, fieldName, expectedDims, shapeName); + return toArrayLike(parent, fieldName, expectedDims, ownerLabel); } return defaultValue; } @@ -189,35 +190,35 @@ T toArrayLike(const inlet::Container &parent, Point3D getPoint(const inlet::Container &parent, char const *fieldName, Dimensions expectedDims, - const std::string &shapeName) + const std::string &ownerLabel) { - return toArrayLike(parent, fieldName, expectedDims, shapeName); + return toArrayLike(parent, fieldName, expectedDims, ownerLabel); } Point3D getPoint(const inlet::Container &parent, char const *fieldName, Dimensions expectedDims, const Point3D &defaultValue, - const std::string &shapeName) + const std::string &ownerLabel) { - return toArrayLike(parent, fieldName, expectedDims, defaultValue, shapeName); + return toArrayLike(parent, fieldName, expectedDims, defaultValue, ownerLabel); } Vector3D getVector(const inlet::Container &parent, char const *fieldName, Dimensions expectedDims, - const std::string &shapeName) + const std::string &ownerLabel) { - return toArrayLike(parent, fieldName, expectedDims, shapeName); + return toArrayLike(parent, fieldName, expectedDims, ownerLabel); } Vector3D getVector(const inlet::Container &parent, char const *fieldName, Dimensions expectedDims, const Vector3D &defaultValue, - const std::string &shapeName) + const std::string &ownerLabel) { - return toArrayLike(parent, fieldName, expectedDims, defaultValue, shapeName); + return toArrayLike(parent, fieldName, expectedDims, defaultValue, ownerLabel); } /** @@ -330,12 +331,12 @@ void verifyObjectFields(const inlet::Container& containerToTest, */ OpPtr parseTranslate(const SingleOperatorData &data, const TransformableGeometryProperties &startProperties, - const std::string &shapeName) + const std::string &ownerLabel) { const auto &opContainer = *data.m_container; verifyObjectFields(opContainer, "translate", FieldSet {}, FieldSet {}); return std::make_shared( - getVector(opContainer, "translate", startProperties.dimensions, shapeName), + getVector(opContainer, "translate", startProperties.dimensions, ownerLabel), startProperties); } @@ -349,7 +350,7 @@ OpPtr parseTranslate(const SingleOperatorData &data, */ OpPtr parseRotate(const SingleOperatorData &data, const TransformableGeometryProperties &startProperties, - const std::string &shapeName) + const std::string &ownerLabel) { const auto &opContainer = *data.m_container; switch(startProperties.dimensions) @@ -359,8 +360,8 @@ OpPtr parseRotate(const SingleOperatorData &data, verifyObjectFields(opContainer, "rotate", FieldSet {}, {"center"}); Vector3D axis {0, 0, 1}; return std::make_shared( - getScalar(opContainer, "rotate", shapeName), - getPoint(opContainer, "center", Dimensions::Two, Point3D {0, 0, 0}, shapeName), + getScalar(opContainer, "rotate", ownerLabel), + getPoint(opContainer, "center", Dimensions::Two, Point3D {0, 0, 0}, ownerLabel), axis, startProperties); } @@ -369,9 +370,9 @@ OpPtr parseRotate(const SingleOperatorData &data, { verifyObjectFields(opContainer, "rotate", {"axis"}, {"center"}); return std::make_shared( - getScalar(opContainer, "rotate", shapeName), - getPoint(opContainer, "center", Dimensions::Three, Point3D {0, 0, 0}, shapeName), - getVector(opContainer, "axis", Dimensions::Three, shapeName), + getScalar(opContainer, "rotate", ownerLabel), + getPoint(opContainer, "center", Dimensions::Three, Point3D {0, 0, 0}, ownerLabel), + getVector(opContainer, "axis", Dimensions::Three, ownerLabel), startProperties); } break; @@ -421,9 +422,9 @@ OpPtr makeCheckedSlice(Point3D origin, primal::Point3D getPerpendicularSliceOrigin(const inlet::Container &sliceContainer, char const *planeName, const primal::Vector3D &defaultNormal, - const std::string &shapeName) + const std::string &ownerLabel) { - double axisIntercept = getScalar(sliceContainer, planeName, shapeName); + double axisIntercept = getScalar(sliceContainer, planeName, ownerLabel); primal::Point3D defaultOrigin; int nonZeroIndex = -1; @@ -441,7 +442,7 @@ primal::Point3D getPerpendicularSliceOrigin(const inlet::Container &sliceContain return defaultOrigin; } - primal::Point3D givenOrigin = getPoint(sliceContainer, "origin", Dimensions::Three, shapeName); + primal::Point3D givenOrigin = getPoint(sliceContainer, "origin", Dimensions::Three, ownerLabel); if(givenOrigin[nonZeroIndex] != axisIntercept) { throw KleeError({sliceContainer["origin"].name(), "The origin must be on the slice plane"}); @@ -459,14 +460,14 @@ primal::Point3D getPerpendicularSliceOrigin(const inlet::Container &sliceContain */ primal::Vector3D getPerpendicularSliceNormal(const inlet::Container &sliceContainer, const primal::Vector3D &defaultNormal, - const std::string &shapeName) + const std::string &ownerLabel) { if(!containsFieldOrCallback(sliceContainer, "normal")) { return defaultNormal; } - primal::Vector3D givenNormal = getVector(sliceContainer, "normal", Dimensions::Three, shapeName); + primal::Vector3D givenNormal = getVector(sliceContainer, "normal", Dimensions::Three, ownerLabel); auto cross = primal::Vector3D::cross_product(givenNormal, defaultNormal); bool parallel = cross.is_zero(); if(!parallel) @@ -492,14 +493,14 @@ OpPtr readPerpendicularSlice(const inlet::Container &sliceContainer, Vector3D const &defaultNormal, Vector3D const &defaultUp, const TransformableGeometryProperties &startProperties, - const std::string &shapeName) + const std::string &ownerLabel) { verifyObjectFields(sliceContainer, planeName, FieldSet {}, {"origin", "normal", "up"}); const primal::Vector3D defaultNormalVec {defaultNormal.data()}; - auto origin = getPerpendicularSliceOrigin(sliceContainer, planeName, defaultNormalVec, shapeName); - auto normal = getPerpendicularSliceNormal(sliceContainer, defaultNormalVec, shapeName); - auto up = getVector(sliceContainer, "up", Dimensions::Three, defaultUp, shapeName); + auto origin = getPerpendicularSliceOrigin(sliceContainer, planeName, defaultNormalVec, ownerLabel); + auto normal = getPerpendicularSliceNormal(sliceContainer, defaultNormalVec, ownerLabel); + auto up = getVector(sliceContainer, "up", Dimensions::Three, defaultUp, ownerLabel); return makeCheckedSlice(origin, normal, up, startProperties, sliceContainer.name()); } @@ -514,7 +515,7 @@ OpPtr readPerpendicularSlice(const inlet::Container &sliceContainer, */ OpPtr parseSlice(const SingleOperatorData &data, const TransformableGeometryProperties &startProperties, - const std::string &shapeName) + const std::string &ownerLabel) { const auto &opContainer = *data.m_container; if(startProperties.dimensions != Dimensions::Three) @@ -530,7 +531,7 @@ OpPtr parseSlice(const SingleOperatorData &data, {1, 0, 0}, {0, 0, 1}, startProperties, - shapeName); + ownerLabel); } else if(containsFieldOrCallback(sliceContainer, "y")) { @@ -539,7 +540,7 @@ OpPtr parseSlice(const SingleOperatorData &data, {0, 1, 0}, {1, 0, 0}, startProperties, - shapeName); + ownerLabel); } else if(containsFieldOrCallback(sliceContainer, "z")) { @@ -548,14 +549,14 @@ OpPtr parseSlice(const SingleOperatorData &data, {0, 0, 1}, {0, 1, 0}, startProperties, - shapeName); + ownerLabel); } verifyObjectFields(sliceContainer, "origin", {"normal", "up"}, FieldSet {}); - return makeCheckedSlice(getPoint(sliceContainer, "origin", Dimensions::Three, shapeName), - getVector(sliceContainer, "normal", Dimensions::Three, shapeName), - getVector(sliceContainer, "up", Dimensions::Three, shapeName), + return makeCheckedSlice(getPoint(sliceContainer, "origin", Dimensions::Three, ownerLabel), + getVector(sliceContainer, "normal", Dimensions::Three, ownerLabel), + getVector(sliceContainer, "up", Dimensions::Three, ownerLabel), startProperties, sliceContainer.name()); } @@ -570,7 +571,7 @@ OpPtr parseSlice(const SingleOperatorData &data, */ OpPtr parseScale(const SingleOperatorData &data, const TransformableGeometryProperties &startProperties, - const std::string &shapeName) + const std::string &ownerLabel) { const auto &opContainer = *data.m_container; verifyObjectFields(opContainer, "scale", FieldSet {}, FieldSet {"center"}); @@ -578,7 +579,7 @@ OpPtr parseScale(const SingleOperatorData &data, ? wrapCallbackErrors>( opContainer, "scale", - shapeName, + ownerLabel, [&]() { return callbackVectorToDoubleVector( opContainer.getFunctionValueAlternative("scale").call()); @@ -594,7 +595,7 @@ OpPtr parseScale(const SingleOperatorData &data, { throw KleeError({fieldPath(opContainer, "scale"), fmt::format("{}: Wrong size for scale. Expected {}. Got {}.", - callbackContext(opContainer, "scale", shapeName), + callbackContext(opContainer, "scale", ownerLabel), expectedSize, actualSize)}); } @@ -615,7 +616,7 @@ OpPtr parseScale(const SingleOperatorData &data, "center", startProperties.dimensions, Point3D {0, 0, 0}, - shapeName); + ownerLabel); } if(isUniform) @@ -710,7 +711,7 @@ OpPtr parseRef(const SingleOperatorData &data, OpPtr convertOperator(SingleOperatorData const& data, TransformableGeometryProperties startProperties, const NamedOperatorMap &namedOperators, - const std::string &shapeName) + const std::string &ownerLabel) { std::unordered_map parsers { {"translate", parseTranslate}, @@ -730,7 +731,7 @@ OpPtr convertOperator(SingleOperatorData const& data, { if(containsFieldOrCallback(*data.m_container, entry.first.c_str())) { - return entry.second(data, startProperties, shapeName); + return entry.second(data, startProperties, ownerLabel); } } @@ -817,7 +818,7 @@ inlet::Container &GeometryOperatorData::defineSchema(inlet::Container &parent, std::shared_ptr GeometryOperatorData::makeOperator( const TransformableGeometryProperties &startProperties, const NamedOperatorMap &namedOperators, - const std::string &shapeName) const + const std::string &ownerLabel) const { if(m_singleOperatorData.empty()) { @@ -832,7 +833,7 @@ std::shared_ptr GeometryOperatorData::makeOperator( for(auto& data : m_singleOperatorData) { composite->addOperator( - convertOperator(data, composite->getEndProperties(), namedOperators, shapeName)); + convertOperator(data, composite->getEndProperties(), namedOperators, ownerLabel)); } return composite; } @@ -879,7 +880,8 @@ NamedOperatorMap NamedOperatorMapData::makeNamedOperatorMap(Dimensions fileDimen dimensions, opData.startUnits, }; - auto op = opData.value.makeOperator(startProperties, namedOperators, ""); + const auto ownerLabel = axom::fmt::format("named operator '{}'", opData.name); + auto op = opData.value.makeOperator(startProperties, namedOperators, ownerLabel); if(op->getEndProperties().units != opData.endUnits) { diff --git a/src/axom/klee/io/GeometryOperatorsIO.hpp b/src/axom/klee/io/GeometryOperatorsIO.hpp index e17e12027b..bdd03fbdb5 100644 --- a/src/axom/klee/io/GeometryOperatorsIO.hpp +++ b/src/axom/klee/io/GeometryOperatorsIO.hpp @@ -69,13 +69,13 @@ class GeometryOperatorData * * @param startProperties properties of the geometry before the first operator * @param namedOperators a map of any named operators - * @param shapeName the owning shape name, or empty for named operators + * @param ownerLabel a description of the owning shape or named operator, for callback errors * @return the (possibly null) operator * @throws KleeError if the operator data is invalid for the given properties */ std::shared_ptr makeOperator(const TransformableGeometryProperties &startProperties, const NamedOperatorMap &namedOperators, - const std::string &shapeName) const; + const std::string &ownerLabel) const; /** * Get the path of this operator in the source document diff --git a/src/axom/klee/io/IO.cpp b/src/axom/klee/io/IO.cpp index 51fbaa7408..eddad1ef7f 100644 --- a/src/axom/klee/io/IO.cpp +++ b/src/axom/klee/io/IO.cpp @@ -419,7 +419,9 @@ Geometry convert(GeometryData const& data, Geometry geometry {startProperties, data.format, data.path, - data.operatorData.makeOperator(startProperties, namedOperators, shapeName)}; + data.operatorData.makeOperator(startProperties, + namedOperators, + axom::fmt::format("shape '{}'", shapeName))}; const auto computed_end_dims = geometry.getEndProperties().dimensions; const auto expected_end_dims = has_explicit_dims ? data.explicitDimensions : fileDimensions; diff --git a/src/axom/klee/tests/klee_io.cpp b/src/axom/klee/tests/klee_io.cpp index a5f3ef5839..4f0d6bd2e4 100644 --- a/src/axom/klee/tests/klee_io.cpp +++ b/src/axom/klee/tests/klee_io.cpp @@ -1408,6 +1408,125 @@ TEST(IOTest, readShapeSet_luaNamedGeometryOperatorsWithNestedRef) EXPECT_THAT(translation->getOffset(), AlmostEqVector(Vector3D {10, 20, 0})); } +TEST(IOTest, readShapeSet_luaNamedOperatorCallbackIsEvaluatedOnceAndReused) +{ + auto shapeSet = readShapeSetFromString(R"( + local callback_calls = 0 + + dimensions = 2 + + named_operators = { + { + name = "shared_callback", + units = "cm", + value = { + { + translate = function() + callback_calls = callback_calls + 1 + return {callback_calls, 2} + end + } + } + } + } + + shapes = { + { + name = "first", + material = "steel", + geometry = { + format = "stl", + path = "first.stl", + units = "cm", + operators = { + { ref = "shared_callback" } + } + } + }, + { + name = "second", + material = "steel", + geometry = { + format = "stl", + path = "second.stl", + units = "cm", + operators = { + { ref = "shared_callback" } + } + } + } + } + )", + InputFormat::Lua); + + ASSERT_EQ(2u, shapeSet.getShapes().size()); + auto firstComposite = std::dynamic_pointer_cast( + shapeSet.getShapes()[0].getGeometry().getGeometryOperator()); + auto secondComposite = std::dynamic_pointer_cast( + shapeSet.getShapes()[1].getGeometry().getGeometryOperator()); + ASSERT_TRUE(firstComposite); + ASSERT_TRUE(secondComposite); + ASSERT_EQ(1u, firstComposite->getOperators().size()); + ASSERT_EQ(1u, secondComposite->getOperators().size()); + + // Both refs reuse the concrete named operator built before either shape. + EXPECT_EQ(firstComposite->getOperators()[0], secondComposite->getOperators()[0]); + auto sharedOperator = + std::dynamic_pointer_cast(firstComposite->getOperators()[0]); + ASSERT_TRUE(sharedOperator); + ASSERT_EQ(1u, sharedOperator->getOperators().size()); + auto translation = + std::dynamic_pointer_cast(sharedOperator->getOperators()[0]); + ASSERT_TRUE(translation); + EXPECT_THAT(translation->getOffset(), AlmostEqVector(Vector3D {1, 2, 0})); +} + +TEST(IOTest, readShapeSet_luaNamedOperatorCallbackErrorIncludesContext) +{ + try + { + readShapeSetFromString(R"( + dimensions = 2 + + named_operators = { + { + name = "bad_named_operator", + units = "cm", + value = { + { + translate = function() + error("named callback boom") + end + } + } + } + } + + shapes = { + { + name = "placeholder", + material = "steel", + geometry = { + format = "stl", + path = "placeholder.stl", + units = "cm" + } + } + } + )", + InputFormat::Lua); + FAIL() << "Should have thrown"; + } + catch(const KleeError &err) + { + EXPECT_THAT(err.what(), HasSubstr("translate")); + EXPECT_THAT(err.what(), HasSubstr("named operator")); + EXPECT_THAT(err.what(), HasSubstr("bad_named_operator")); + EXPECT_THAT(err.what(), HasSubstr("operator 1")); + EXPECT_THAT(err.what(), HasSubstr("named callback boom")); + } +} + TEST(IOTest, readShapeSet_luaDifferentDimensions) { auto shapeSet = readShapeSetFromString(R"( From d47bf9693f26d1200734af2adfc314b5516df338 Mon Sep 17 00:00:00 2001 From: Kenneth Weiss Date: Thu, 30 Jul 2026 02:37:08 -0700 Subject: [PATCH 24/52] Klee: Adds tests for callbacks on operators --- src/axom/klee/Units.hpp | 11 + .../klee/docs/sphinx/specifying_shapes.rst | 23 +- src/axom/klee/io/GeometryOperatorsIO.cpp | 94 +++- .../klee/tests/klee_geometry_operators_io.cpp | 17 + src/axom/klee/tests/klee_io.cpp | 456 ++++++++++++++++++ 5 files changed, 574 insertions(+), 27 deletions(-) diff --git a/src/axom/klee/Units.hpp b/src/axom/klee/Units.hpp index fb4794ef30..1fccbb5916 100644 --- a/src/axom/klee/Units.hpp +++ b/src/axom/klee/Units.hpp @@ -24,6 +24,17 @@ using utilities::LengthUnit; namespace internal { +/*! + * \brief Parse a unit string, reporting an invalid value at the supplied input path. + * + * \param unitsAsString the unit string to parse + * \param path the input path to report on failure + * + * \return A LengthUnit containing the unit type. + * \throws KleeError if the unit string is invalid + */ +LengthUnit parseLengthUnits(const std::string &unitsAsString, const std::string &path); + /*! * \brief This function parses a string and returns a LengthUnit. It is a compatibility * function that throws a KleeError if the unit is invalid. diff --git a/src/axom/klee/docs/sphinx/specifying_shapes.rst b/src/axom/klee/docs/sphinx/specifying_shapes.rst index 437cf1cd80..4ffad45476 100644 --- a/src/axom/klee/docs/sphinx/specifying_shapes.rst +++ b/src/axom/klee/docs/sphinx/specifying_shapes.rst @@ -245,6 +245,17 @@ Callbacks in a named operator are evaluated when that named operator is constructed. Each :code:`ref` reuses the resulting concrete operator rather than evaluating its callbacks again for the referring shape. +Callback evaluation order is deterministic. Klee processes +:code:`named_operators` before :code:`shapes`, entries in each list in source +order, and each geometry's operators in source order. Within a multi-field +operator, fields are evaluated in this order: + +* :code:`rotate`, then :code:`center`, then :code:`axis` (when present) +* :code:`scale`, then :code:`center` (when present) +* perpendicular slice :code:`x`, :code:`y`, or :code:`z`, then + :code:`origin`, :code:`normal`, and :code:`up` (when present) +* arbitrary slice :code:`origin`, then :code:`normal`, then :code:`up` + .. code-block:: lua local dim = 2 @@ -280,10 +291,11 @@ than evaluating its callbacks again for the referring shape. Vector-valued callbacks return raw numeric Lua tables such as :code:`{x, y}` or :code:`{x, y, z}`. The typed :code:`Vector.new(...)` object is also accepted. Scalar-valued callbacks return a number. +String-valued callbacks return a string. Supported callback fields are -:code:`translate`, :code:`axis`, :code:`center`, :code:`scale`, -:code:`slice.origin`, :code:`slice.normal`, :code:`slice.up`, :code:`rotate`, -:code:`slice.x`, :code:`slice.y`, and :code:`slice.z`. +:code:`translate`, :code:`axis`, :code:`center`, :code:`scale`, :code:`slice.origin`, +:code:`slice.normal`, :code:`slice.up`, :code:`rotate`, :code:`slice.x`, +:code:`slice.y`, :code:`slice.z`, :code:`convert_units_to`, and :code:`ref`. For :code:`scale`, a one-entry table means uniform scaling and a multi-entry table means per-axis scaling. Common Lua input errors are reported as Klee parsing errors. @@ -523,7 +535,7 @@ Operators may also have additional required or optional parameters. :value: an angle, in degrees by which the shape will be rotated counterclockwise. :additional required parameters: - :axis: (3D only) the axis of rotation + :axis: (3D only) the nonzero axis of rotation :optional arguments: :center: a point specifying the center of rotation :example: @@ -716,7 +728,8 @@ the transformation was defined when you use it. In addition to using :code:`ref` in an individual shape's operators, you can also use it in other named operators. The only restriction is that it -be defined in the list before it is used. +be defined in the list before it is used. For Lua input, this restriction also +applies when a :code:`ref` callback returns the operator name. .. code-block:: yaml diff --git a/src/axom/klee/io/GeometryOperatorsIO.cpp b/src/axom/klee/io/GeometryOperatorsIO.cpp index 1207def133..30047d1350 100644 --- a/src/axom/klee/io/GeometryOperatorsIO.cpp +++ b/src/axom/klee/io/GeometryOperatorsIO.cpp @@ -125,6 +125,19 @@ double getScalar(const inlet::Container &container, return container[fieldName].get(); } +std::string getString(const inlet::Container &container, + char const *fieldName, + const std::string &ownerLabel) +{ + if(hasCallback(container, fieldName)) + { + return wrapCallbackErrors(container, fieldName, ownerLabel, [&]() { + return container.getFunctionValueAlternative(fieldName).call(); + }); + } + return container[fieldName].get(); +} + std::vector callbackVectorToDoubleVector(const inlet::FunctionType::Vector &value) { std::vector result; @@ -358,22 +371,32 @@ OpPtr parseRotate(const SingleOperatorData &data, case Dimensions::Two: { verifyObjectFields(opContainer, "rotate", FieldSet {}, {"center"}); + auto angle = getScalar(opContainer, "rotate", ownerLabel); + auto center = + getPoint(opContainer, "center", Dimensions::Two, Point3D {0, 0, 0}, ownerLabel); Vector3D axis {0, 0, 1}; - return std::make_shared( - getScalar(opContainer, "rotate", ownerLabel), - getPoint(opContainer, "center", Dimensions::Two, Point3D {0, 0, 0}, ownerLabel), - axis, - startProperties); + return std::make_shared(angle, center, axis, startProperties); } break; case Dimensions::Three: { verifyObjectFields(opContainer, "rotate", {"axis"}, {"center"}); - return std::make_shared( - getScalar(opContainer, "rotate", ownerLabel), - getPoint(opContainer, "center", Dimensions::Three, Point3D {0, 0, 0}, ownerLabel), - getVector(opContainer, "axis", Dimensions::Three, ownerLabel), - startProperties); + auto angle = getScalar(opContainer, "rotate", ownerLabel); + auto center = + getPoint(opContainer, "center", Dimensions::Three, Point3D {0, 0, 0}, ownerLabel); + auto axis = getVector(opContainer, "axis", Dimensions::Three, ownerLabel); + if(axis.is_zero()) + { + auto message = std::string {"The 'axis' vector must not be a zero vector"}; + if(hasCallback(opContainer, "axis")) + { + message = axom::fmt::format("{}: {}", + callbackContext(opContainer, "axis", ownerLabel), + message); + } + throw KleeError({fieldPath(opContainer, "axis"), message}); + } + return std::make_shared(angle, center, axis, startProperties); } break; default: @@ -554,11 +577,10 @@ OpPtr parseSlice(const SingleOperatorData &data, verifyObjectFields(sliceContainer, "origin", {"normal", "up"}, FieldSet {}); - return makeCheckedSlice(getPoint(sliceContainer, "origin", Dimensions::Three, ownerLabel), - getVector(sliceContainer, "normal", Dimensions::Three, ownerLabel), - getVector(sliceContainer, "up", Dimensions::Three, ownerLabel), - startProperties, - sliceContainer.name()); + auto origin = getPoint(sliceContainer, "origin", Dimensions::Three, ownerLabel); + auto normal = getVector(sliceContainer, "normal", Dimensions::Three, ownerLabel); + auto up = getVector(sliceContainer, "up", Dimensions::Three, ownerLabel); + return makeCheckedSlice(origin, normal, up, startProperties, sliceContainer.name()); } /** @@ -642,11 +664,30 @@ OpPtr parseScale(const SingleOperatorData &data, */ OpPtr parseConvertUnits(const SingleOperatorData &data, const TransformableGeometryProperties &startProperties, - const std::string &) + const std::string &ownerLabel) { const auto &opContainer = *data.m_container; verifyObjectFields(opContainer, "convert_units_to", FieldSet {}, FieldSet {}); - auto endUnits = internal::parseLengthUnits(opContainer["convert_units_to"]); + const auto unitName = getString(opContainer, "convert_units_to", ownerLabel); + const auto path = fieldPath(opContainer, "convert_units_to"); + LengthUnit endUnits; + try + { + endUnits = internal::parseLengthUnits(unitName, static_cast(path)); + } + catch(const KleeError &err) + { + if(!hasCallback(opContainer, "convert_units_to")) + { + throw; + } + throw KleeError( + {path, + axom::fmt::format( + "{}: {}", + callbackContext(opContainer, "convert_units_to", ownerLabel), + err.what())}); + } return std::make_shared(endUnits, startProperties); } @@ -661,18 +702,25 @@ OpPtr parseConvertUnits(const SingleOperatorData &data, */ OpPtr parseRef(const SingleOperatorData &data, const TransformableGeometryProperties &startProperties, - const NamedOperatorMap &namedOperators) + const NamedOperatorMap &namedOperators, + const std::string &ownerLabel) { const auto &opContainer = *data.m_container; verifyObjectFields(opContainer, "ref", FieldSet {}, FieldSet {}); - std::string const& operatorName = opContainer["ref"]; + const auto operatorName = getString(opContainer, "ref", ownerLabel); auto opIter = namedOperators.find(operatorName); if(opIter == namedOperators.end()) { std::string message = "No operator named '"; message += operatorName; message += '\''; - throw KleeError({opContainer["ref"].name(), message}); + if(hasCallback(opContainer, "ref")) + { + message = axom::fmt::format("{}: {}", + callbackContext(opContainer, "ref", ownerLabel), + message); + } + throw KleeError({fieldPath(opContainer, "ref"), message}); } auto referencedOperator = opIter->second; bool startUnitsMatch = startProperties.units == referencedOperator->getStartProperties().units; @@ -722,8 +770,8 @@ OpPtr convertOperator(SingleOperatorData const& data, {"ref", [&namedOperators](const SingleOperatorData &opData, const TransformableGeometryProperties &startProperties, - const std::string &) { - return parseRef(opData, startProperties, namedOperators); + const std::string &ownerLabel) { + return parseRef(opData, startProperties, namedOperators, ownerLabel); }}, }; @@ -785,6 +833,8 @@ inlet::Container &GeometryOperatorData::defineSchema(inlet::Container &parent, addCallbackAlternative(opContainer, "center", inlet::FunctionTag::Vector); addCallbackAlternative(opContainer, "axis", inlet::FunctionTag::Vector); addCallbackAlternative(opContainer, "scale", inlet::FunctionTag::Vector); + addCallbackAlternative(opContainer, "convert_units_to", inlet::FunctionTag::String); + addCallbackAlternative(opContainer, "ref", inlet::FunctionTag::String); addCallbackAlternative(slice, "x", inlet::FunctionTag::Double); addCallbackAlternative(slice, "y", inlet::FunctionTag::Double); diff --git a/src/axom/klee/tests/klee_geometry_operators_io.cpp b/src/axom/klee/tests/klee_geometry_operators_io.cpp index 60daa5088a..16d207dc2b 100644 --- a/src/axom/klee/tests/klee_geometry_operators_io.cpp +++ b/src/axom/klee/tests/klee_geometry_operators_io.cpp @@ -336,6 +336,23 @@ TEST(GeometryOperatorsIO, readRotation_3D_optionalFields) EXPECT_EQ(expectedProperties, rotation.getEndProperties()); } +TEST(GeometryOperatorsIO, readRotation_3D_zeroAxis) +{ + try + { + readSingleOperator({Dimensions::Three, LengthUnit::cm}, R"( + rotate: 45 + axis: [0, 0, 0] + )"); + FAIL() << "Should have rejected a zero rotation axis"; + } + catch(const KleeError &err) + { + EXPECT_THAT(err.what(), HasSubstr("axis")); + EXPECT_THAT(err.what(), HasSubstr("zero")); + } +} + TEST(GeometryOperatorsIO, readRotation_3D_axisMissing) { try diff --git a/src/axom/klee/tests/klee_io.cpp b/src/axom/klee/tests/klee_io.cpp index 4f0d6bd2e4..df3dd4c731 100644 --- a/src/axom/klee/tests/klee_io.cpp +++ b/src/axom/klee/tests/klee_io.cpp @@ -39,6 +39,7 @@ using klee::ShapeSet; using klee::SliceOperator; using klee::TransformableGeometryProperties; using klee::Translation; +using klee::UnitConverter; using primal::Point3D; using primal::Vector3D; using test::AlmostEqPoint; @@ -68,6 +69,100 @@ ShapeSet readShapeSetFromString(const std::string& input, std::istringstream istream(input); return klee::readShapeSet(istream, format, options); } + +std::string makeLuaSliceCallbackInput(const std::string &sliceFields) +{ + std::ostringstream input; + input << R"( + dimensions = 2 + shapes = { + { + name = "slice_callback", + material = "steel", + geometry = { + format = "stl", + path = "slice_callback.stl", + start_dimensions = 3, + dimensions = 2, + units = "cm", + operators = { + { + slice = { + )" + << sliceFields << R"( + } + } + } + } + } + } + )"; + return input.str(); +} + +std::string makeLuaRotationCallbackInput(const std::string &rotationFields) +{ + std::ostringstream input; + input << R"( + dimensions = 3 + shapes = { + { + name = "rotation_callback", + material = "steel", + geometry = { + format = "stl", + path = "rotation_callback.stl", + units = "cm", + operators = { + { + )" + << rotationFields << R"( + } + } + } + } + } + )"; + return input.str(); +} + +std::string makeLuaStringOperatorCallbackInput(const std::string &fieldName, + const std::string &returnExpression) +{ + std::ostringstream input; + input << R"( + dimensions = 2 + + named_operators = { + { + name = "known_operator", + units = "cm", + value = { + { translate = {1, 2} } + } + } + } + + shapes = { + { + name = "string_callback", + material = "steel", + geometry = { + format = "stl", + path = "string_callback.stl", + units = "cm", + operators = { + { + )" + << fieldName << " = function() return " << returnExpression << R"( end + } + } + } + } + } + )"; + return input.str(); +} } // end namespace TEST(IOTest, readShapeSet_noShapes) @@ -1481,6 +1576,113 @@ TEST(IOTest, readShapeSet_luaNamedOperatorCallbackIsEvaluatedOnceAndReused) EXPECT_THAT(translation->getOffset(), AlmostEqVector(Vector3D {1, 2, 0})); } +TEST(IOTest, readShapeSet_luaCallbacksHaveDeterministicEvaluationOrder) +{ + auto shapeSet = readShapeSetFromString(R"( + local callback_index = 0 + + local function ordered(expected_index, value) + return function() + callback_index = callback_index + 1 + if callback_index ~= expected_index then + error("expected callback " .. expected_index .. + ", got callback " .. callback_index) + end + return value + end + end + + dimensions = 3 + + named_operators = { + { + name = "rotate_and_scale", + units = "cm", + value = { + { + rotate = ordered(1, 30), + center = ordered(2, {1, 2, 3}), + axis = ordered(3, {0, 0, 1}) + }, + { + scale = ordered(4, {2}), + center = ordered(5, {4, 5, 6}) + } + } + }, + { + name = "shift", + units = "cm", + value = { + { translate = ordered(6, {7, 8, 9}) } + } + } + } + + shapes = { + { + name = "reference", + material = "steel", + geometry = { + format = "stl", + path = "reference.stl", + units = "cm", + operators = { + { translate = ordered(7, {1, 2, 3}) }, + { ref = ordered(8, "rotate_and_scale") } + } + } + }, + { + name = "arbitrary_slice", + material = "steel", + geometry = { + format = "stl", + path = "arbitrary_slice.stl", + start_dimensions = 3, + dimensions = 2, + units = "cm", + operators = { + { + slice = { + origin = ordered(9, {0, 0, 0}), + normal = ordered(10, {0, 0, 1}), + up = ordered(11, {0, 1, 0}) + } + } + } + } + }, + { + name = "perpendicular_slice", + material = "steel", + geometry = { + format = "stl", + path = "perpendicular_slice.stl", + start_dimensions = 3, + dimensions = 2, + units = "cm", + operators = { + { + slice = { + x = ordered(12, 3), + origin = ordered(13, {3, 0, 0}), + normal = ordered(14, {1, 0, 0}), + up = ordered(15, {0, 1, 0}) + } + } + } + } + } + } + )", + InputFormat::Lua); + + // Reaching the last callback proves ordering across top-level collections, + // operator lists, and the fields within each multi-field operator. + ASSERT_EQ(3u, shapeSet.getShapes().size()); +} + TEST(IOTest, readShapeSet_luaNamedOperatorCallbackErrorIncludesContext) { try @@ -1703,6 +1905,260 @@ TEST(IOTest, readShapeSet_luaOperatorCallbacks) EXPECT_THAT(slice->getUp(), AlmostEqVector(Vector3D {0, 1, 0})); } +TEST(IOTest, readShapeSet_luaStringOperatorCallbacks) +{ + auto shapeSet = readShapeSetFromString(R"( + local target_units = "cm" + local selected_operator = "shift" + + dimensions = 2 + + named_operators = { + { + name = "shift", + units = "cm", + value = { + { translate = {1, 2} } + } + } + } + + shapes = { + { + name = "string_callbacks", + material = "steel", + geometry = { + format = "stl", + path = "string_callbacks.stl", + units = "m", + operators = { + { convert_units_to = function() return target_units end }, + { ref = function() return selected_operator end } + } + } + } + } + )", + InputFormat::Lua); + + ASSERT_EQ(1u, shapeSet.getShapes().size()); + auto composite = std::dynamic_pointer_cast( + shapeSet.getShapes()[0].getGeometry().getGeometryOperator()); + ASSERT_TRUE(composite); + ASSERT_EQ(2u, composite->getOperators().size()); + + auto converter = std::dynamic_pointer_cast(composite->getOperators()[0]); + ASSERT_TRUE(converter); + EXPECT_EQ(LengthUnit::m, converter->getStartProperties().units); + EXPECT_EQ(LengthUnit::cm, converter->getEndProperties().units); + + auto referenced = + std::dynamic_pointer_cast(composite->getOperators()[1]); + ASSERT_TRUE(referenced); + ASSERT_EQ(1u, referenced->getOperators().size()); + auto translation = + std::dynamic_pointer_cast(referenced->getOperators()[0]); + ASSERT_TRUE(translation); + EXPECT_THAT(translation->getOffset(), AlmostEqVector(Vector3D {1, 2, 0})); +} + +TEST(IOTest, readShapeSet_luaStringOperatorCallbackErrorsIncludeContext) +{ + struct FailureCase + { + const char *field; + const char *returnExpression; + const char *expectedMessage; + }; + const std::array cases {{ + {"convert_units_to", "{}", "function call"}, + {"convert_units_to", "\"parsec\"", "Unrecognized units"}, + {"ref", "{}", "function call"}, + {"ref", "\"missing_operator\"", "No operator named"}, + }}; + + for(const auto &testCase : cases) + { + SCOPED_TRACE(testCase.expectedMessage); + try + { + readShapeSetFromString( + makeLuaStringOperatorCallbackInput(testCase.field, testCase.returnExpression), + InputFormat::Lua); + FAIL() << "Should have thrown"; + } + catch(const KleeError &err) + { + EXPECT_THAT(err.what(), + HasSubstr(std::string {"callback for '"} + testCase.field + "'")); + EXPECT_THAT(err.what(), HasSubstr("string_callback")); + EXPECT_THAT(err.what(), HasSubstr("operator 1")); + EXPECT_THAT(err.what(), HasSubstr(testCase.expectedMessage)); + } + } +} + +TEST(IOTest, readShapeSet_luaPerpendicularSliceCallbacks) +{ + auto shapeSet = readShapeSetFromString(R"( + dimensions = 2 + + shapes = { + { + name = "x_slice", + material = "steel", + geometry = { + format = "stl", + path = "x_slice.stl", + start_dimensions = 3, + dimensions = 2, + units = "cm", + operators = { + { slice = { x = function() return 10 end } } + } + } + }, + { + name = "y_slice", + material = "steel", + geometry = { + format = "stl", + path = "y_slice.stl", + start_dimensions = 3, + dimensions = 2, + units = "cm", + operators = { + { slice = { y = function() return 20 end } } + } + } + }, + { + name = "z_slice", + material = "steel", + geometry = { + format = "stl", + path = "z_slice.stl", + start_dimensions = 3, + dimensions = 2, + units = "cm", + operators = { + { slice = { z = function() return 30 end } } + } + } + } + } + )", + InputFormat::Lua); + + const std::array expectedOrigins {{{10, 0, 0}, {0, 20, 0}, {0, 0, 30}}}; + const std::array expectedNormals {{{1, 0, 0}, {0, 1, 0}, {0, 0, 1}}}; + const std::array expectedUp {{{0, 0, 1}, {1, 0, 0}, {0, 1, 0}}}; + + ASSERT_EQ(3u, shapeSet.getShapes().size()); + for(std::size_t i = 0; i < shapeSet.getShapes().size(); ++i) + { + SCOPED_TRACE(i); + auto composite = std::dynamic_pointer_cast( + shapeSet.getShapes()[i].getGeometry().getGeometryOperator()); + ASSERT_TRUE(composite); + ASSERT_EQ(1u, composite->getOperators().size()); + auto slice = std::dynamic_pointer_cast(composite->getOperators()[0]); + ASSERT_TRUE(slice); + EXPECT_THAT(slice->getOrigin(), AlmostEqPoint(expectedOrigins[i])); + EXPECT_THAT(slice->getNormal(), AlmostEqVector(expectedNormals[i])); + EXPECT_THAT(slice->getUp(), AlmostEqVector(expectedUp[i])); + } +} + +TEST(IOTest, readShapeSet_luaPerpendicularSliceCallbackWrongTypeIncludesContext) +{ + for(const char *axis : {"x", "y", "z"}) + { + SCOPED_TRACE(axis); + const auto sliceFields = std::string {axis} + " = function() return {1} end"; + try + { + readShapeSetFromString(makeLuaSliceCallbackInput(sliceFields), InputFormat::Lua); + FAIL() << "Should have thrown"; + } + catch(const KleeError &err) + { + EXPECT_THAT(err.what(), HasSubstr(std::string {"callback for '"} + axis + "'")); + EXPECT_THAT(err.what(), HasSubstr("slice_callback")); + EXPECT_THAT(err.what(), HasSubstr("operator 1")); + } + } +} + +TEST(IOTest, readShapeSet_luaPerpendicularSliceCallbacksAreValidated) +{ + struct ValidationCase + { + const char *field; + const char *returnValue; + const char *expectedMessage; + }; + const std::array cases {{ + {"origin", "{20, 0, 0}", "slice plane"}, + {"normal", "{1, 2, 3}", "Invalid normal"}, + {"up", "{1, 0, 0}", "perpendicular"}, + }}; + + for(const auto &testCase : cases) + { + SCOPED_TRACE(testCase.field); + const auto sliceFields = + std::string {"x = function() return 10 end, "} + testCase.field + + " = function() return " + testCase.returnValue + " end"; + try + { + readShapeSetFromString(makeLuaSliceCallbackInput(sliceFields), InputFormat::Lua); + FAIL() << "Should have thrown"; + } + catch(const KleeError &err) + { + EXPECT_THAT(err.what(), HasSubstr(testCase.field)); + EXPECT_THAT(err.what(), HasSubstr(testCase.expectedMessage)); + } + } +} + +TEST(IOTest, readShapeSet_luaRotationCallbacksAreValidated) +{ + struct ValidationCase + { + const char *fields; + const char *field; + const char *expectedMessage; + }; + const std::array cases {{ + {"rotate = function() return {45} end, axis = {0, 0, 1}", "rotate", "function call"}, + {"rotate = 45, axis = function() return {0, 1} end", "axis", "Wrong size"}, + {"rotate = 45, axis = {0, 0, 1}, center = function() return {1, 2} end", + "center", + "Wrong size"}, + {"rotate = 45, axis = function() return {0, 0, 0} end", "axis", "zero"}, + }}; + + for(const auto &testCase : cases) + { + SCOPED_TRACE(testCase.expectedMessage); + try + { + readShapeSetFromString(makeLuaRotationCallbackInput(testCase.fields), InputFormat::Lua); + FAIL() << "Should have thrown"; + } + catch(const KleeError &err) + { + EXPECT_THAT(err.what(), + HasSubstr(std::string {"callback for '"} + testCase.field + "'")); + EXPECT_THAT(err.what(), HasSubstr("rotation_callback")); + EXPECT_THAT(err.what(), HasSubstr("operator 1")); + EXPECT_THAT(err.what(), HasSubstr(testCase.expectedMessage)); + } + } +} + TEST(IOTest, readShapeSet_luaDimensionDependentCallback) { auto shapeSet = readShapeSetFromString(R"( From bfb0709f44c5e6d1e32415fc00e11b6dd207de0e Mon Sep 17 00:00:00 2001 From: Kenneth Weiss Date: Fri, 7 Aug 2026 17:15:57 -0700 Subject: [PATCH 25/52] Integrates Lua callbacks into quest shaping example and tutorial --- .../klee/docs/sphinx/specifying_shapes.rst | 4 ++ src/axom/quest/examples/CMakeLists.txt | 42 ++++++++++++++++ src/axom/quest/examples/shaping_driver.cpp | 32 ++++++++++++- src/examples/CMakeLists.txt | 21 ++++++++ src/examples/shaping_tutorial/CMakeLists.txt | 26 +++++++++- .../shaping_tutorial/lesson_04/README.md | 28 +++++++++++ .../shaping_tutorial/lesson_04/circles.lua | 48 +++++++++++++++++++ .../lesson_04/circles_initialization.lua | 4 ++ .../lesson_04/circles_initialized.lua | 45 +++++++++++++++++ .../lesson_04/quest_sampling_shaper.cpp | 30 ++++++++++-- 10 files changed, 271 insertions(+), 9 deletions(-) create mode 100644 src/examples/shaping_tutorial/lesson_04/circles.lua create mode 100644 src/examples/shaping_tutorial/lesson_04/circles_initialization.lua create mode 100644 src/examples/shaping_tutorial/lesson_04/circles_initialized.lua diff --git a/src/axom/klee/docs/sphinx/specifying_shapes.rst b/src/axom/klee/docs/sphinx/specifying_shapes.rst index 4ffad45476..b4aca019a6 100644 --- a/src/axom/klee/docs/sphinx/specifying_shapes.rst +++ b/src/axom/klee/docs/sphinx/specifying_shapes.rst @@ -256,6 +256,10 @@ operator, fields are evaluated in this order: :code:`origin`, :code:`normal`, and :code:`up` (when present) * arbitrary slice :code:`origin`, then :code:`normal`, then :code:`up` +Klee does not coordinate Lua evaluation across MPI ranks. If an application +calls :code:`readShapeSet` on every rank, each rank reads and evaluates the deck +and initialization chunk independently. + .. code-block:: lua local dim = 2 diff --git a/src/axom/quest/examples/CMakeLists.txt b/src/axom/quest/examples/CMakeLists.txt index e168c25f03..2322b60090 100644 --- a/src/axom/quest/examples/CMakeLists.txt +++ b/src/axom/quest/examples/CMakeLists.txt @@ -176,6 +176,7 @@ if(AXOM_HAS_MFEM_WITH_MPI AND AXOM_ENABLE_SIDRE AND AXOM_ENABLE_KLEE) if(AXOM_ENABLE_TESTS AND AXOM_DATA_DIR) # 2D shaping tests set(_nranks 1) + if(C2C_FOUND) set(_testname quest_shaping_driver_ex_sampling_circles) axom_add_test( @@ -350,6 +351,47 @@ if(AXOM_HAS_MFEM_WITH_MPI AND AXOM_ENABLE_SIDRE AND AXOM_ENABLE_KLEE) # 3D shaping tests set(_nranks 3) + + if(LUA_FOUND) + set(_testname quest_shaping_driver_ex_lua_callbacks) + axom_add_test( + NAME ${_testname} + COMMAND quest_shaping_driver_ex + -i ${shaping_data_dir}/sphere_callbacks.lua + --method sampling + inline_mesh --min -6 -6 -6 --max 6 6 6 --resolution 16 16 16 -d 3 + NUM_MPI_TASKS ${_nranks}) + set_tests_properties(${_testname} PROPERTIES + PASS_REGULAR_EXPRESSION "Volume of material 'steel' is 519.") + + set(_testname quest_shaping_driver_ex_lua_initialization) + axom_add_test( + NAME ${_testname} + COMMAND quest_shaping_driver_ex + -i ${shaping_data_dir}/sphere_callbacks.lua + --lua-init-file ${shaping_data_dir}/sphere_initialization.lua + --method sampling + inline_mesh --min -4.8 -4.8 -4.8 --max 4.8 4.8 4.8 + --resolution 16 16 16 -d 3 + NUM_MPI_TASKS ${_nranks}) + # Analytic volume for a sphere with radius 4 is ~268.08 + # the input surface is discretized and the computational mesh is coarse. + set_tests_properties(${_testname} PROPERTIES + PASS_REGULAR_EXPRESSION "Volume of material 'steel' is 26[56].") + + set(_testname quest_shaping_driver_ex_yaml_rejects_lua_initialization) + axom_add_test( + NAME ${_testname} + COMMAND quest_shaping_driver_ex + -i ${shaping_data_dir}/sphere.yaml + --lua-init-file ${shaping_data_dir}/sphere_initialization.lua + --method sampling + inline_mesh --min -6 -6 -6 --max 6 6 6 --resolution 16 16 16 -d 3 + NUM_MPI_TASKS ${_nranks}) + set_tests_properties(${_testname} PROPERTIES + WILL_FAIL TRUE) + endif() + set(_testname quest_shaping_driver_ex_sampling_sphere) axom_add_test( NAME ${_testname} diff --git a/src/axom/quest/examples/shaping_driver.cpp b/src/axom/quest/examples/shaping_driver.cpp index 19b0ff1aae..0282ffafb6 100644 --- a/src/axom/quest/examples/shaping_driver.cpp +++ b/src/axom/quest/examples/shaping_driver.cpp @@ -47,10 +47,12 @@ // C/C++ includes #include +#include +#include #include +#include #include #include -#include namespace klee = axom::klee; namespace primal = axom::primal; @@ -137,6 +139,7 @@ struct Input BlueprintMeshBacking blueprintMeshBacking {BlueprintMeshBacking::Sidre}; std::string shapeFile; + std::string luaInitializationFile; klee::ShapeSet shapeSet; ShapingMethod shapingMethod {ShapingMethod::Sampling}; @@ -339,6 +342,10 @@ struct Input ->check(axom::CLI::ExistingFile) ->required(); + app.add_option("--lua-init-file", luaInitializationFile) + ->description("Lua chunk that returns initial globals for a Lua shape file") + ->check(axom::CLI::ExistingFile); + app.add_flag("-v,--verbose,!--no-verbose", m_verboseOutput) ->description("Enable/disable verbose output") ->capture_default_str(); @@ -549,6 +556,27 @@ struct Input slic::setLoggingMsgLevel(m_verboseOutput ? slic::message::Debug : slic::message::Info); } + + klee::LuaInputOptions loadLuaInputOptions() const + { + klee::LuaInputOptions options; + if(luaInitializationFile.empty()) + { + return options; + } + + std::ifstream stream {luaInitializationFile}; + if(!stream) + { + throw klee::KleeError( + {axom::Path {luaInitializationFile}, "Could not read Lua initialization file"}); + } + + options.initialization = klee::LuaInitializationChunk { + std::string {std::istreambuf_iterator {stream}, std::istreambuf_iterator {}}, + luaInitializationFile}; + return options; + } }; /** @@ -704,7 +732,7 @@ int main(int argc, char** argv) try { AXOM_ANNOTATE_SCOPE("read Klee shape set"); - params.shapeSet = klee::readShapeSet(params.shapeFile); + params.shapeSet = klee::readShapeSet(params.shapeFile, params.loadLuaInputOptions()); slic::flushStreams(); } diff --git a/src/examples/CMakeLists.txt b/src/examples/CMakeLists.txt index a7ec083dc5..62641d2d03 100644 --- a/src/examples/CMakeLists.txt +++ b/src/examples/CMakeLists.txt @@ -208,6 +208,27 @@ if(AXOM_ENABLE_TUTORIALS AND AXOM_ENABLE_KLEE AND AXOM_ENABLE_QUEST) ${CMAKE_CURRENT_SOURCE_DIR}/shaping_tutorial/lesson_03/ice_cream_initialized.lua --initialization-file ${CMAKE_CURRENT_SOURCE_DIR}/shaping_tutorial/lesson_03/ice_cream_initialization.lua) + + if(MFEM_FOUND) + set(_lesson_04_dir + "${CMAKE_CURRENT_SOURCE_DIR}/shaping_tutorial/lesson_04") + + blt_add_test(NAME shaping_tutorial_lesson_04_quest_sampling_shaper_yaml + COMMAND shaping_tutorial_lesson_04_quest_sampling_shaper + -m ${_lesson_04_dir}/circle_input.lua + -k ${_lesson_04_dir}/circles.yaml) + + blt_add_test(NAME shaping_tutorial_lesson_04_quest_sampling_shaper_lua_callbacks + COMMAND shaping_tutorial_lesson_04_quest_sampling_shaper + -m ${_lesson_04_dir}/circle_input.lua + -k ${_lesson_04_dir}/circles.lua) + + blt_add_test(NAME shaping_tutorial_lesson_04_quest_sampling_shaper_lua_initialization + COMMAND shaping_tutorial_lesson_04_quest_sampling_shaper + -m ${_lesson_04_dir}/circle_input.lua + -k ${_lesson_04_dir}/circles_initialized.lua + --lua-init-file ${_lesson_04_dir}/circles_initialization.lua) + endif() endif() endif() diff --git a/src/examples/shaping_tutorial/CMakeLists.txt b/src/examples/shaping_tutorial/CMakeLists.txt index 30abe5ed22..7e4b24617b 100644 --- a/src/examples/shaping_tutorial/CMakeLists.txt +++ b/src/examples/shaping_tutorial/CMakeLists.txt @@ -117,8 +117,30 @@ if(ENABLE_TESTS) if(AXOM_USE_LUA AND AXOM_USE_MFEM) blt_add_test(NAME lesson_04_quest_sampling_shaper - COMMAND lesson_04_quest_sampling_shaper -m ../lesson_04/circle_input.lua - -k ../lesson_04/circles.yaml -v) + COMMAND lesson_04_quest_sampling_shaper -m ../lesson_04/circle_input.lua + -k ../lesson_04/circles.yaml -v) + + blt_add_test(NAME lesson_04_quest_sampling_shaper_lua_callbacks + COMMAND lesson_04_quest_sampling_shaper + -m ../lesson_04/circle_input.lua + -k ../lesson_04/circles.lua + -v) + + blt_add_test(NAME lesson_04_quest_sampling_shaper_lua_initialization + COMMAND lesson_04_quest_sampling_shaper + -m ../lesson_04/circle_input.lua + -k ../lesson_04/circles_initialized.lua + --lua-init-file ../lesson_04/circles_initialization.lua + -v) + + set(_testname + lesson_04_quest_sampling_shaper_yaml_rejects_lua_initialization) + blt_add_test(NAME ${_testname} + COMMAND lesson_04_quest_sampling_shaper + -m ../lesson_04/circle_input.lua + -k ../lesson_04/circles.yaml + --lua-init-file ../lesson_04/circles_initialization.lua) + set_tests_properties(${_testname} PROPERTIES WILL_FAIL TRUE) endif() endif() diff --git a/src/examples/shaping_tutorial/lesson_04/README.md b/src/examples/shaping_tutorial/lesson_04/README.md index a534e51bc4..b019367460 100644 --- a/src/examples/shaping_tutorial/lesson_04/README.md +++ b/src/examples/shaping_tutorial/lesson_04/README.md @@ -464,6 +464,34 @@ The replacement rules are implicit -- each new shape replaces all existing mater > -m ../src/examples/shaping_tutorial/lesson_04/circle_input.lua > ``` +The Klee shape file can also be Lua. In [`circles.lua`](circles.lua), the scale factors +are zero-argument callbacks. Klee evaluates them while reading the deck +and passes ordinary concrete operators to Quest: + +```bash +./bin/shaping_tutorial_lesson_04_quest_sampling_shaper \ + -k ../src/examples/shaping_tutorial/lesson_04/circles.lua \ + -m ../src/examples/shaping_tutorial/lesson_04/circle_input.lua +``` + +An application can provide runtime values through an initialization file +instead of embedding them in the deck. The `--lua-init-file` argument names a +Lua chunk that returns a table of initial globals: + +```bash +./bin/shaping_tutorial_lesson_04_quest_sampling_shaper \ + -k ../src/examples/shaping_tutorial/lesson_04/circles_initialized.lua \ + -m ../src/examples/shaping_tutorial/lesson_04/circle_input.lua \ + --lua-init-file ../src/examples/shaping_tutorial/lesson_04/circles_initialization.lua +``` + +This option is valid only with a Lua Klee deck and using it with YAML produces a +Klee validation error. In an MPI run, every rank reads the same deck and +initialization file and evaluates them independently. Consequently, callbacks +and initialization chunks must be deterministic and should not depend on rank, +random values, mutable external files, or unsynchronized side effects. +Klee does not currently parse on one rank and broadcast the resulting shape set. + ### Ice cream example revisited diff --git a/src/examples/shaping_tutorial/lesson_04/circles.lua b/src/examples/shaping_tutorial/lesson_04/circles.lua new file mode 100644 index 0000000000..5f5ed43901 --- /dev/null +++ b/src/examples/shaping_tutorial/lesson_04/circles.lua @@ -0,0 +1,48 @@ +local outer_radius = 5.0 +local inner_radius_mm = 25.0 + +dimensions = 2 + +shapes = { + { + name = "background", + material = "void", + geometry = { + format = "none" + } + }, + { + name = "outer_shell", + material = "steel", + geometry = { + format = "mfem", + path = "unit_circle.mesh", + units = "cm", + operators = { + { + scale = function() + return {outer_radius} + end + } + } + } + }, + { + name = "inner_ball", + material = "void", + geometry = { + format = "mfem", + path = "unit_circle.mesh", + start_units = "mm", + end_units = "cm", + operators = { + { + scale = function() + return {inner_radius_mm} + end + }, + { convert_units_to = "cm" } + } + } + } +} diff --git a/src/examples/shaping_tutorial/lesson_04/circles_initialization.lua b/src/examples/shaping_tutorial/lesson_04/circles_initialization.lua new file mode 100644 index 0000000000..8ce3542db7 --- /dev/null +++ b/src/examples/shaping_tutorial/lesson_04/circles_initialization.lua @@ -0,0 +1,4 @@ +return { + outer_radius = 4.0, + inner_radius_mm = 20.0 +} diff --git a/src/examples/shaping_tutorial/lesson_04/circles_initialized.lua b/src/examples/shaping_tutorial/lesson_04/circles_initialized.lua new file mode 100644 index 0000000000..7a0675bd49 --- /dev/null +++ b/src/examples/shaping_tutorial/lesson_04/circles_initialized.lua @@ -0,0 +1,45 @@ +dimensions = 2 + +shapes = { + { + name = "background", + material = "void", + geometry = { + format = "none" + } + }, + { + name = "outer_shell", + material = "steel", + geometry = { + format = "mfem", + path = "unit_circle.mesh", + units = "cm", + operators = { + { + scale = function() + return {outer_radius} + end + } + } + } + }, + { + name = "inner_ball", + material = "void", + geometry = { + format = "mfem", + path = "unit_circle.mesh", + start_units = "mm", + end_units = "cm", + operators = { + { + scale = function() + return {inner_radius_mm} + end + }, + { convert_units_to = "cm" } + } + } + } +} diff --git a/src/examples/shaping_tutorial/lesson_04/quest_sampling_shaper.cpp b/src/examples/shaping_tutorial/lesson_04/quest_sampling_shaper.cpp index 314339b4d3..d82db06c3f 100644 --- a/src/examples/shaping_tutorial/lesson_04/quest_sampling_shaper.cpp +++ b/src/examples/shaping_tutorial/lesson_04/quest_sampling_shaper.cpp @@ -12,14 +12,14 @@ * This example demonstrates how to use the Quest SamplingShaper to shape * a Klee input onto a computational mesh. The program: * 1. Creates a structured mesh from Inlet mesh metadata - * 2. Loads Klee shapes from a YAML file + * 2. Loads Klee shapes from a YAML or Lua file * 3. Generates volume fraction fields for each material based on the input shapes * 4. Outputs the generated mesh to disk * * This example supports both serial and MPI execution. * * Example run: - * > [srun -n8] ./quest_sampling_shaper -m mesh_metadata.lua -k shapes.yaml [-v] + * > [srun -n8] ./quest_sampling_shaper -m mesh_metadata.lua -k shapes.lua [-v] * */ //----------------------------------------------------------------------------- @@ -47,7 +47,9 @@ #error Shaping functionality requires Axom to be configured with Conduit and MFEM #endif +#include #include +#include #include #include #include @@ -306,7 +308,8 @@ int main(int argc, char** argv) // -------------------------------------------------------------------------- axom::CLI::App app {"Shaping pipeline using separate Inlet mesh metadata and Klee shapes"}; std::string inputFilename; // Mesh metadata Inlet Lua - std::string kleeFilename; // Klee shape set YAML + std::string kleeFilename; // Klee shape set YAML or Lua + std::string luaInitializationFilename; bool verbose = false; app.add_option("-m,--mesh_file", inputFilename) @@ -314,9 +317,12 @@ int main(int argc, char** argv) ->required() ->check(axom::CLI::ExistingFile); app.add_option("-k,--klee_file", kleeFilename) - ->description("Klee shape set YAML file") + ->description("Klee shape set YAML or Lua file") ->required() ->check(axom::CLI::ExistingFile); + app.add_option("--lua-init-file", luaInitializationFilename) + ->description("Lua chunk that returns initial globals for a Lua shape file") + ->check(axom::CLI::ExistingFile); app.add_flag("-v,--verbose", verbose)->description("Enable verbose (debug) logging"); try @@ -375,7 +381,21 @@ int main(int argc, char** argv) klee::ShapeSet shapeSet; try { - shapeSet = klee::readShapeSet(kleeFilename); + klee::LuaInputOptions options; + if(!luaInitializationFilename.empty()) + { + std::ifstream stream {luaInitializationFilename}; + if(!stream) + { + throw klee::KleeError( + {axom::Path {luaInitializationFilename}, "Could not read Lua initialization file"}); + } + + options.initialization = klee::LuaInitializationChunk { + std::string {std::istreambuf_iterator {stream}, std::istreambuf_iterator {}}, + luaInitializationFilename}; + } + shapeSet = klee::readShapeSet(kleeFilename, options); } catch(klee::KleeError& error) { From b20c6153b4dc4c3bf9f0814ed64470214a35a58d Mon Sep 17 00:00:00 2001 From: Kenneth Weiss Date: Fri, 7 Aug 2026 18:35:28 -0700 Subject: [PATCH 26/52] Updates data submodule with new shaping example --- data | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data b/data index 8ac544afdc..e0857e915d 160000 --- a/data +++ b/data @@ -1 +1 @@ -Subproject commit 8ac544afdc0d75e9cfe0681f9eaa8f2150534dea +Subproject commit e0857e915d695a936e16ec4742b928400855ced0 From 992cfd59cc7e34941b6c62398f22e3cfde2e4a72 Mon Sep 17 00:00:00 2001 From: Kenneth Weiss Date: Sun, 9 Aug 2026 13:43:35 -0700 Subject: [PATCH 27/52] Inlet: Defines restart behavior for function alternatives * Tags function alternatives as internal Sidre groups so restart reconstruction doesn't expose callback storage as ordinary fields * Reject multiple callback alternatives for the same input path and documents this --- src/axom/inlet/Container.cpp | 26 + src/axom/inlet/SphinxWriter.cpp | 654 ++++++++++++----------- src/axom/inlet/docs/sphinx/functions.rst | 15 + src/axom/inlet/inlet_utils.hpp | 7 +- src/axom/inlet/tests/inlet_function.cpp | 98 ++++ 5 files changed, 473 insertions(+), 327 deletions(-) diff --git a/src/axom/inlet/Container.cpp b/src/axom/inlet/Container.cpp index 4529f3bc1f..66d146233b 100644 --- a/src/axom/inlet/Container.cpp +++ b/src/axom/inlet/Container.cpp @@ -53,6 +53,13 @@ Container::Container(const std::string& name, { if(group.isUsingMap() && group.hasView("InletType")) { + // Lua callables cannot be reconstructed from Sidre. In particular, + // do not reconstruct their internal schema groups as ordinary Fields. + if(group.hasView(detail::FUNCTION_VALUE_ALTERNATIVE_FLAG)) + { + continue; + } + const std::string inletType = group.getView("InletType")->getString(); const std::string childName = utilities::string::appendPrefix(m_name, group.getName()); @@ -1099,6 +1106,20 @@ Verifiable& Container::addFunctionWithInputPath( { return *iter->second; } + + if(isValueAlternative) + { + const auto existingAlternative = m_functionValueAlternatives.find(inputPath.value); + if(existingAlternative != m_functionValueAlternatives.end()) + { + SLIC_ERROR(fmt::format("[Inlet] Input path '{0}' already has a function value " + "alternative in container '{1}'", + inputPath.value, + m_name)); + return *existingAlternative->second; + } + } + axom::sidre::Group* sidreGroup = createSidreGroup(fullName, description); SLIC_ERROR_IF(sidreGroup == nullptr, fmt::format("Failed to create Sidre group with name '{0}'", fullName)); @@ -1114,6 +1135,11 @@ Verifiable& Container::addFunctionWithInputPath( } lookupPath = utilities::string::removeAllInstances(lookupPath, detail::COLLECTION_GROUP_NAME + "/"); + if(isValueAlternative) + { + sidreGroup->createViewScalar(detail::FUNCTION_VALUE_ALTERNATIVE_FLAG, + static_cast(1)); + } detail::updateUnexpectedNames(lookupPath, m_unexpectedNames); auto func = m_reader.getFunction(lookupPath, ret_type, arg_types); if(isValueAlternative && func) diff --git a/src/axom/inlet/SphinxWriter.cpp b/src/axom/inlet/SphinxWriter.cpp index 298c172153..57dd266783 100644 --- a/src/axom/inlet/SphinxWriter.cpp +++ b/src/axom/inlet/SphinxWriter.cpp @@ -47,333 +47,339 @@ bool isTrivial(const Container& container) * \param [in] e The enumeration value to convert * This function should be removed once C++23 is available * \see https://en.cppreference.com/w/cpp/utility/to_underlying - */ -template -constexpr typename std::underlying_type::type to_underlying(const E e) -{ - return static_cast::type>(e); -} - -} // namespace detail - -SphinxWriter::SphinxWriter(const std::string& fileName) - : m_fieldColLabels( - {"Field Name", "Description", "Default Value", "Range/Valid Values", "Required"}) - , m_functionColLabels({"Function Name", "Description", "Signature", "Required"}) -{ - m_fileName = fileName; - m_oss << ".. |uncheck| unicode:: U+2610 .. UNCHECKED BOX\n"; - m_oss << ".. |check| unicode:: U+2611 .. CHECKED BOX\n\n"; - writeTitle("Input file Options"); -} - -void SphinxWriter::documentContainer(const Container& container) -{ - const auto sidreGroup = container.sidreGroup(); - const std::string pathName = sidreGroup->getPathName(); - std::string containerName = sidreGroup->getName(); - bool isSelectedElement = false; - - // If the container is empty, ignore it - if(detail::isTrivial(container)) - { - return; - } - - // Replace the "implementation-defined" name with something a bit more readable - if(isCollectionGroup(containerName)) - { - containerName = "Collection contents:"; - } - - // If we've gotten to this point and are an element of an array/dict, - // mark it as the selected element - if(sidreGroup->getParent()->getName() == detail::COLLECTION_GROUP_NAME) - { - // The collection that this Container is a part of - const std::string collectionName = sidreGroup->getParent()->getParent()->getPathName(); - isSelectedElement = true; - } - - m_inletContainerPathNames.push_back(pathName); - auto& currContainer = - m_rstTables.emplace(pathName, ContainerData {m_fieldColLabels, m_functionColLabels}).first->second; - currContainer.containerName = containerName; - currContainer.isSelectedElement = isSelectedElement; - if(containerName != "" && sidreGroup->hasView("description")) - { - currContainer.description = sidreGroup->getView("description")->getString(); - } - - for(const auto& field_entry : container.getChildFields()) - { - extractFieldMetadata(field_entry.second->sidreGroup(), currContainer); - } - + */ +template +constexpr typename std::underlying_type::type to_underlying(const E e) +{ + return static_cast::type>(e); +} + +} // namespace detail + +SphinxWriter::SphinxWriter(const std::string& fileName) + : m_fieldColLabels( + {"Field Name", "Description", "Default Value", "Range/Valid Values", "Required"}) + , m_functionColLabels({"Function Name", "Description", "Signature", "Required"}) +{ + m_fileName = fileName; + m_oss << ".. |uncheck| unicode:: U+2610 .. UNCHECKED BOX\n"; + m_oss << ".. |check| unicode:: U+2611 .. CHECKED BOX\n\n"; + writeTitle("Input file Options"); +} + +void SphinxWriter::documentContainer(const Container& container) +{ + const auto sidreGroup = container.sidreGroup(); + const std::string pathName = sidreGroup->getPathName(); + std::string containerName = sidreGroup->getName(); + bool isSelectedElement = false; + + // If the container is empty, ignore it + if(detail::isTrivial(container)) + { + return; + } + + // Replace the "implementation-defined" name with something a bit more readable + if(isCollectionGroup(containerName)) + { + containerName = "Collection contents:"; + } + + // If we've gotten to this point and are an element of an array/dict, + // mark it as the selected element + if(sidreGroup->getParent()->getName() == detail::COLLECTION_GROUP_NAME) + { + // The collection that this Container is a part of + const std::string collectionName = sidreGroup->getParent()->getParent()->getPathName(); + isSelectedElement = true; + } + + m_inletContainerPathNames.push_back(pathName); + auto& currContainer = + m_rstTables.emplace(pathName, ContainerData {m_fieldColLabels, m_functionColLabels}).first->second; + currContainer.containerName = containerName; + currContainer.isSelectedElement = isSelectedElement; + if(containerName != "" && sidreGroup->hasView("description")) + { + currContainer.description = sidreGroup->getView("description")->getString(); + } + + for(const auto& field_entry : container.getChildFields()) + { + extractFieldMetadata(field_entry.second->sidreGroup(), currContainer); + } + for(const auto& function_entry : container.getChildFunctions()) { - extractFunctionMetadata(function_entry.second->sidreGroup(), currContainer); - } -} - -void SphinxWriter::finalize() -{ - writeAllTables(); - m_outFile.open(m_fileName); - m_outFile << m_oss.str(); - m_outFile.close(); -} - -void SphinxWriter::writeTitle(const std::string& title) -{ - if(title != "") - { - std::string equals = std::string(title.length(), '='); - m_oss << equals << "\n" << title << "\n" << equals << "\n"; - } -} - -void SphinxWriter::writeSubtitle(const std::string& sub) -{ - if(sub != "") - { - std::string dashes = std::string(sub.length(), '-'); - m_oss << "\n" << dashes << "\n" << sub << "\n" << dashes << "\n\n"; - } -} - -void SphinxWriter::writeTable(const std::string& title, - const std::vector>& rstTable) -{ - SLIC_WARNING_IF(rstTable.size() <= 1, - "[Inlet] Vector for corresponding rst table must be nonempty"); - std::string result = ".. list-table:: " + title; - std::string widths = ":widths:"; - // This would be easier with an iterator adaptor like back_inserter but for - // concatenation - for(std::size_t i = 0u; i < rstTable.front().size(); i++) - { - widths += " 25"; - } - result += "\n " + widths + "\n"; - result += " :header-rows: 1\n :stub-columns: 1\n\n"; - for(unsigned int i = 0; i < rstTable.size(); ++i) - { - result += " * - "; - for(unsigned int j = 0; j < rstTable[i].size(); ++j) - { - if(j != 0) - { - result += " - "; - } - result += rstTable[i][j] + "\n"; - } - } - m_oss << result; -} - -void SphinxWriter::writeAllTables() -{ - for(std::string& pathName : m_inletContainerPathNames) - { - auto& currContainer = m_rstTables.at(pathName); - // If we're displaying a selected element, the title and description - // will already have been printed - if(currContainer.isSelectedElement) - { - m_oss << "The input schema defines a collection of this container.\n"; - m_oss << "For brevity, only one instance is displayed here.\n\n"; - } - else - { - writeSubtitle(currContainer.containerName); - if(currContainer.description != "") - { - m_oss << "Description: " << currContainer.description << "\n\n"; - } - } - if(currContainer.fieldTable.size() > 1) - { - writeTable("Fields", currContainer.fieldTable); - } - if(currContainer.functionTable.size() > 1) - { - writeTable("Functions", currContainer.functionTable); - } - } -} - -std::string SphinxWriter::getValueAsString(const axom::sidre::View* view) -{ - axom::sidre::TypeID type = view->getTypeID(); - if(type == axom::sidre::TypeID::INT8_ID) - { - std::int8_t val = view->getData(); - return val ? "True" : "False"; - } - else if(type == axom::sidre::TypeID::INT_ID) - { - int val = view->getData(); - return std::to_string(val); - } - else if(type == axom::sidre::TypeID::DOUBLE_ID) - { - double val = view->getData(); - return std::to_string(val); - } - return view->getString(); -} - -std::string SphinxWriter::getRangeAsString(const axom::sidre::View* view) -{ - std::ostringstream oss; - oss.precision(3); - oss << std::scientific; - - axom::sidre::TypeID type = view->getTypeID(); - if(type == axom::sidre::INT_ID) - { - const int* range = view->getData(); - oss << range[0] << " to " << range[1]; - } - else - { - const double* range = view->getData(); - oss << range[0] << " to " << range[1]; - } - return oss.str(); -} - -std::string SphinxWriter::getValidValuesAsString(const axom::sidre::View* view) -{ - const int* range = view->getData(); - size_t size = view->getBuffer()->getNumElements(); - std::string result = ""; - for(size_t i = 0; i < size; ++i) - { - if(i == size - 1) - { - result += std::to_string(range[i]); - } - else - { - result += std::to_string(range[i]) + ", "; - } - } - return result; -} - -std::string SphinxWriter::getValidStringValues(const axom::sidre::Group* sidreGroup) -{ - auto idx = sidreGroup->getFirstValidViewIndex(); - std::string validValues = ""; - while(axom::sidre::indexIsValid(idx)) - { - validValues += std::string(sidreGroup->getView(idx)->getString()); - idx = sidreGroup->getNextValidViewIndex(idx); - if(axom::sidre::indexIsValid(idx)) + const auto* functionGroup = function_entry.second->sidreGroup(); + const bool isValueAlternative = + functionGroup->hasView(detail::FUNCTION_VALUE_ALTERNATIVE_FLAG); + if(!isValueAlternative) { - validValues += ", "; + extractFunctionMetadata(functionGroup, currContainer); } } - return validValues; -} - -void SphinxWriter::extractFieldMetadata(const axom::sidre::Group* sidreGroup, - ContainerData& currentContainer) -{ - std::vector fieldAttributes(m_fieldColLabels.size()); - - fieldAttributes[0] = sidreGroup->getName(); - - if(sidreGroup->hasView("description")) - { - fieldAttributes[1] = std::string(sidreGroup->getView("description")->getString()); - } - - if(sidreGroup->hasView("defaultValue")) - { - fieldAttributes[2] = getValueAsString(sidreGroup->getView("defaultValue")); - } - - if(sidreGroup->hasView("range")) - { - fieldAttributes[3] = getRangeAsString(sidreGroup->getView("range")); - } - else if(sidreGroup->hasView("validValues")) - { - fieldAttributes[3] = getValidValuesAsString(sidreGroup->getView("validValues")); - } - else if(sidreGroup->hasGroup("validStringValues")) - { - fieldAttributes[3] = getValidStringValues(sidreGroup->getGroup("validStringValues")); - } - - if(sidreGroup->hasView("required")) - { - std::int8_t required = sidreGroup->getView("required")->getData(); - fieldAttributes[4] = required ? "|check|" : "|uncheck|"; - } - else - { - fieldAttributes[4] = "|uncheck|"; - } - - currentContainer.fieldTable.push_back(fieldAttributes); -} - -std::string SphinxWriter::getSignatureAsString(const axom::sidre::Group* sidreGroup) -{ - using underlying = std::underlying_type::type; - static const auto type_names = []() { - std::unordered_map result; - result[detail::to_underlying(FunctionTag::Vector)] = "Vector"; - result[detail::to_underlying(FunctionTag::Double)] = "Double"; - result[detail::to_underlying(FunctionTag::Void)] = "Void"; - result[detail::to_underlying(FunctionTag::String)] = "String"; - return result; - }(); - - // View::getData does not have a const version... - const auto ret_type = static_cast(sidreGroup->getView("return_type")->getData()); - - const auto args_view = sidreGroup->getView("function_arguments"); - const underlying* arg_tags = args_view->getData(); - const int num_args = args_view->getNumElements(); - std::vector arg_types(num_args); - for(int i = 0; i < num_args; i++) - { - arg_types[i] = type_names.at(arg_tags[i]); - } - return fmt::format("{0}({1})", type_names.at(ret_type), fmt::join(arg_types, ", ")); } - -void SphinxWriter::extractFunctionMetadata(const axom::sidre::Group* sidreGroup, - ContainerData& currentContainer) -{ - std::vector functionAttributes(m_functionColLabels.size()); - - functionAttributes[0] = sidreGroup->getName(); - - if(sidreGroup->hasView("description")) - { - functionAttributes[1] = std::string(sidreGroup->getView("description")->getString()); - } - - functionAttributes[2] = getSignatureAsString(sidreGroup); - - if(sidreGroup->hasView("required")) - { - std::int8_t required = sidreGroup->getView("required")->getData(); - functionAttributes[3] = required ? "|check|" : "|uncheck|"; - } - else - { - functionAttributes[3] = "|uncheck|"; - } - - currentContainer.functionTable.push_back(functionAttributes); -} - -} // namespace inlet -} // namespace axom + +void SphinxWriter::finalize() +{ + writeAllTables(); + m_outFile.open(m_fileName); + m_outFile << m_oss.str(); + m_outFile.close(); +} + +void SphinxWriter::writeTitle(const std::string& title) +{ + if(title != "") + { + std::string equals = std::string(title.length(), '='); + m_oss << equals << "\n" << title << "\n" << equals << "\n"; + } +} + +void SphinxWriter::writeSubtitle(const std::string& sub) +{ + if(sub != "") + { + std::string dashes = std::string(sub.length(), '-'); + m_oss << "\n" << dashes << "\n" << sub << "\n" << dashes << "\n\n"; + } +} + +void SphinxWriter::writeTable(const std::string& title, + const std::vector>& rstTable) +{ + SLIC_WARNING_IF(rstTable.size() <= 1, + "[Inlet] Vector for corresponding rst table must be nonempty"); + std::string result = ".. list-table:: " + title; + std::string widths = ":widths:"; + // This would be easier with an iterator adaptor like back_inserter but for + // concatenation + for(std::size_t i = 0u; i < rstTable.front().size(); i++) + { + widths += " 25"; + } + result += "\n " + widths + "\n"; + result += " :header-rows: 1\n :stub-columns: 1\n\n"; + for(unsigned int i = 0; i < rstTable.size(); ++i) + { + result += " * - "; + for(unsigned int j = 0; j < rstTable[i].size(); ++j) + { + if(j != 0) + { + result += " - "; + } + result += rstTable[i][j] + "\n"; + } + } + m_oss << result; +} + +void SphinxWriter::writeAllTables() +{ + for(std::string& pathName : m_inletContainerPathNames) + { + auto& currContainer = m_rstTables.at(pathName); + // If we're displaying a selected element, the title and description + // will already have been printed + if(currContainer.isSelectedElement) + { + m_oss << "The input schema defines a collection of this container.\n"; + m_oss << "For brevity, only one instance is displayed here.\n\n"; + } + else + { + writeSubtitle(currContainer.containerName); + if(currContainer.description != "") + { + m_oss << "Description: " << currContainer.description << "\n\n"; + } + } + if(currContainer.fieldTable.size() > 1) + { + writeTable("Fields", currContainer.fieldTable); + } + if(currContainer.functionTable.size() > 1) + { + writeTable("Functions", currContainer.functionTable); + } + } +} + +std::string SphinxWriter::getValueAsString(const axom::sidre::View* view) +{ + axom::sidre::TypeID type = view->getTypeID(); + if(type == axom::sidre::TypeID::INT8_ID) + { + std::int8_t val = view->getData(); + return val ? "True" : "False"; + } + else if(type == axom::sidre::TypeID::INT_ID) + { + int val = view->getData(); + return std::to_string(val); + } + else if(type == axom::sidre::TypeID::DOUBLE_ID) + { + double val = view->getData(); + return std::to_string(val); + } + return view->getString(); +} + +std::string SphinxWriter::getRangeAsString(const axom::sidre::View* view) +{ + std::ostringstream oss; + oss.precision(3); + oss << std::scientific; + + axom::sidre::TypeID type = view->getTypeID(); + if(type == axom::sidre::INT_ID) + { + const int* range = view->getData(); + oss << range[0] << " to " << range[1]; + } + else + { + const double* range = view->getData(); + oss << range[0] << " to " << range[1]; + } + return oss.str(); +} + +std::string SphinxWriter::getValidValuesAsString(const axom::sidre::View* view) +{ + const int* range = view->getData(); + size_t size = view->getBuffer()->getNumElements(); + std::string result = ""; + for(size_t i = 0; i < size; ++i) + { + if(i == size - 1) + { + result += std::to_string(range[i]); + } + else + { + result += std::to_string(range[i]) + ", "; + } + } + return result; +} + +std::string SphinxWriter::getValidStringValues(const axom::sidre::Group* sidreGroup) +{ + auto idx = sidreGroup->getFirstValidViewIndex(); + std::string validValues = ""; + while(axom::sidre::indexIsValid(idx)) + { + validValues += std::string(sidreGroup->getView(idx)->getString()); + idx = sidreGroup->getNextValidViewIndex(idx); + if(axom::sidre::indexIsValid(idx)) + { + validValues += ", "; + } + } + return validValues; +} + +void SphinxWriter::extractFieldMetadata(const axom::sidre::Group* sidreGroup, + ContainerData& currentContainer) +{ + std::vector fieldAttributes(m_fieldColLabels.size()); + + fieldAttributes[0] = sidreGroup->getName(); + + if(sidreGroup->hasView("description")) + { + fieldAttributes[1] = std::string(sidreGroup->getView("description")->getString()); + } + + if(sidreGroup->hasView("defaultValue")) + { + fieldAttributes[2] = getValueAsString(sidreGroup->getView("defaultValue")); + } + + if(sidreGroup->hasView("range")) + { + fieldAttributes[3] = getRangeAsString(sidreGroup->getView("range")); + } + else if(sidreGroup->hasView("validValues")) + { + fieldAttributes[3] = getValidValuesAsString(sidreGroup->getView("validValues")); + } + else if(sidreGroup->hasGroup("validStringValues")) + { + fieldAttributes[3] = getValidStringValues(sidreGroup->getGroup("validStringValues")); + } + + if(sidreGroup->hasView("required")) + { + std::int8_t required = sidreGroup->getView("required")->getData(); + fieldAttributes[4] = required ? "|check|" : "|uncheck|"; + } + else + { + fieldAttributes[4] = "|uncheck|"; + } + + currentContainer.fieldTable.push_back(fieldAttributes); +} + +std::string SphinxWriter::getSignatureAsString(const axom::sidre::Group* sidreGroup) +{ + using underlying = std::underlying_type::type; + static const auto type_names = []() { + std::unordered_map result; + result[detail::to_underlying(FunctionTag::Vector)] = "Vector"; + result[detail::to_underlying(FunctionTag::Double)] = "Double"; + result[detail::to_underlying(FunctionTag::Void)] = "Void"; + result[detail::to_underlying(FunctionTag::String)] = "String"; + return result; + }(); + + // View::getData does not have a const version... + const auto ret_type = static_cast(sidreGroup->getView("return_type")->getData()); + + const auto args_view = sidreGroup->getView("function_arguments"); + const underlying* arg_tags = args_view->getData(); + const int num_args = args_view->getNumElements(); + std::vector arg_types(num_args); + for(int i = 0; i < num_args; i++) + { + arg_types[i] = type_names.at(arg_tags[i]); + } + return fmt::format("{0}({1})", type_names.at(ret_type), fmt::join(arg_types, ", ")); +} + +void SphinxWriter::extractFunctionMetadata(const axom::sidre::Group* sidreGroup, + ContainerData& currentContainer) +{ + std::vector functionAttributes(m_functionColLabels.size()); + + functionAttributes[0] = sidreGroup->getName(); + + if(sidreGroup->hasView("description")) + { + functionAttributes[1] = std::string(sidreGroup->getView("description")->getString()); + } + + functionAttributes[2] = getSignatureAsString(sidreGroup); + + if(sidreGroup->hasView("required")) + { + std::int8_t required = sidreGroup->getView("required")->getData(); + functionAttributes[3] = required ? "|check|" : "|uncheck|"; + } + else + { + functionAttributes[3] = "|uncheck|"; + } + + currentContainer.functionTable.push_back(functionAttributes); +} + +} // namespace inlet +} // namespace axom diff --git a/src/axom/inlet/docs/sphinx/functions.rst b/src/axom/inlet/docs/sphinx/functions.rst index dd3204d191..acc0e2012e 100644 --- a/src/axom/inlet/docs/sphinx/functions.rst +++ b/src/axom/inlet/docs/sphinx/functions.rst @@ -125,6 +125,10 @@ when exact or collection-relative resolution needs to be explicit. The two schema entries may be added in either order. A function encountered at a normal field path remains a type error unless this alternative has been declared. +The narrow API permits one callback alternative for a public input path in a given container +and declaring a second callback alternative for that path is an error. +Since one Lua object cannot simultaneously be a concrete value and a function, +at most one of the two supported representations can match. Only the selected representation exists: ``contains`` reports the concrete field when a value was supplied, and ``containsFunctionValueAlternative`` reports the callback when a @@ -137,6 +141,17 @@ Consequently, ``required()`` and registered verifiers apply to the entry on which they are configured. The narrow value-alternative API does not currently provide a group-level annotation meaning "either representation is required." +In Sidre, the callback entry retains the same signature metadata as an ordinary Inlet function +and is tagged as an internal value alternative. The live Lua callable remains in memory. +Inlet restart reconstructs containers and fields, not functions, so the tag prevents +the internal callback group from being mistaken for a field. +A persisted concrete value is reconstructed normally, while a Lua callback is not. + +Generated Sphinx and JSON Schema documentation describe the concrete value form. +The internal callback entry is omitted: its storage name is not an input-file path, +and JSON inputs cannot provide a Lua function. Document the callback form separately when exposing +it as part of an application's Lua interface. + Accessing --------- diff --git a/src/axom/inlet/inlet_utils.hpp b/src/axom/inlet/inlet_utils.hpp index 83e510166f..c59be1b664 100644 --- a/src/axom/inlet/inlet_utils.hpp +++ b/src/axom/inlet/inlet_utils.hpp @@ -131,15 +131,16 @@ namespace detail * Names of the internal collection data and collection index groups/fields * used for managing arrays/dictionaries ******************************************************************************* - */ -const std::string COLLECTION_GROUP_NAME = "_inlet_collection"; + */ +const std::string COLLECTION_GROUP_NAME = "_inlet_collection"; const std::string COLLECTION_INDICES_NAME = "_inlet_collection_indices"; const std::string STRUCT_COLLECTION_FLAG = "_inlet_struct_collection"; const std::string VARIANT_STRUCT_COLLECTION_FLAG = "_inlet_variant_struct_collection"; +const std::string FUNCTION_VALUE_ALTERNATIVE_FLAG = "_inlet_function_value_alternative"; const std::string REQUIRED_FLAG = "required"; const std::string STRICT_FLAG = "strict"; } // namespace detail - + /*! ***************************************************************************** * \brief Determines whether a Container is a collection group diff --git a/src/axom/inlet/tests/inlet_function.cpp b/src/axom/inlet/tests/inlet_function.cpp index f0b4c6cb18..eb3f16787a 100644 --- a/src/axom/inlet/tests/inlet_function.cpp +++ b/src/axom/inlet/tests/inlet_function.cpp @@ -9,10 +9,15 @@ #include "axom/inlet/LuaReader.hpp" #include "axom/inlet/Inlet.hpp" +#include "axom/inlet/JSONSchemaWriter.hpp" +#include "axom/inlet/SphinxWriter.hpp" #include "gtest/gtest.h" #include +#include +#include +#include #include #include #include @@ -24,7 +29,9 @@ using axom::inlet::FunctionType; using axom::inlet::Inlet; using axom::inlet::InletType; using axom::inlet::InputPath; +using axom::inlet::JSONSchemaWriter; using axom::inlet::LuaReader; +using axom::inlet::SphinxWriter; using axom::inlet::VerificationError; #include "axom/sol.hpp" @@ -409,6 +416,97 @@ TEST(inlet_function, function_value_alternative_is_valid_in_strict_container) } } +TEST(inlet_function, function_value_alternative_rejects_duplicate_callback_alternative) +{ + auto inlet = createBasicInlet("function scale () return 2.0 end"); + auto& callback = inlet.addFunctionAsValueAlternative( + "scale_callback", + FunctionTag::Double, + {}, + "scale"); + auto& repeatedCallback = inlet.addFunctionAsValueAlternative( + "scale_callback", + FunctionTag::Double, + {}, + "scale"); + + // Like other Inlet schema entries, repeating the same name is idempotent. + EXPECT_EQ(&callback, &repeatedCallback); + + axom::slic::ScopedAbortToThrow abortGuard; + EXPECT_THROW(inlet.addFunctionAsValueAlternative( + "another_scale_callback", + FunctionTag::Double, + {}, + "scale"), + axom::slic::SlicAbortException); + EXPECT_EQ(inlet.getGlobalContainer().getChildFunctions().size(), 1u); +} + +TEST(inlet_function, function_value_alternative_sidre_and_generated_documentation) +{ + const std::string sphinxFile = "inlet_function_value_alternative.rst"; + const std::string jsonFile = "inlet_function_value_alternative.json"; + auto inlet = createBasicInlet("scale = 2.0"); + inlet.addFunctionAsValueAlternative( + "scale_callback", + FunctionTag::Double, + {}, + "scale"); + inlet.addDouble("scale", "Scale factor"); + + // Sidre marks the internal callback schema group without storing the callable. + const auto* callbackGroup = inlet.getGlobalContainer() + .getChildFunctions() + .at("scale_callback") + ->sidreGroup(); + ASSERT_TRUE(callbackGroup->hasView(axom::inlet::detail::FUNCTION_VALUE_ALTERNATIVE_FLAG)); + const std::int8_t alternativeFlag = + callbackGroup->getView(axom::inlet::detail::FUNCTION_VALUE_ALTERNATIVE_FLAG)->getScalar(); + EXPECT_EQ(alternativeFlag, 1); + + inlet.write(SphinxWriter(sphinxFile)); + inlet.write(JSONSchemaWriter(jsonFile)); + + const auto readFile = [](const std::string& path) { + std::ifstream stream(path); + return std::string(std::istreambuf_iterator(stream), + std::istreambuf_iterator()); + }; + const std::string sphinx = readFile(sphinxFile); + const std::string json = readFile(jsonFile); + std::remove(sphinxFile.c_str()); + std::remove(jsonFile.c_str()); + + EXPECT_NE(sphinx.find("scale"), std::string::npos); + EXPECT_EQ(sphinx.find("scale_callback"), std::string::npos); + EXPECT_NE(json.find("scale"), std::string::npos); + EXPECT_EQ(json.find("scale_callback"), std::string::npos); +} + +TEST(inlet_function, function_value_alternative_restart_does_not_create_internal_field) +{ + axom::sidre::DataStore datastore; + { + auto reader = std::make_unique(); + reader->parseString("scale = 2.0"); + Inlet inlet(std::move(reader), datastore.getRoot()); + inlet.addFunctionAsValueAlternative(FunctionTag::Double, {}, "scale"); + inlet.addDouble("scale"); + } + + { + auto reader = std::make_unique(); + Inlet restart(std::move(reader), datastore.getRoot(), true, true); + + EXPECT_TRUE(restart.contains("scale")); + EXPECT_DOUBLE_EQ(restart.get("scale"), 2.0); + EXPECT_EQ(restart.getGlobalContainer().getChildFields().size(), 1u); + EXPECT_TRUE(restart.getGlobalContainer().getChildFunctions().empty()); + EXPECT_FALSE(restart.getGlobalContainer().containsFunctionValueAlternative("scale")); + } +} + TEST(inlet_function, returned_function_keeps_lua_state_alive) { // An extracted callback must retain its Lua state after Inlet is destroyed. From 828936f719bfe0e3d7afae9601ac49d161fd823d Mon Sep 17 00:00:00 2001 From: Kenneth Weiss Date: Sun, 9 Aug 2026 14:00:08 -0700 Subject: [PATCH 28/52] Inlet: Improve docs and examples related to lua functions --- src/axom/inlet/docs/sphinx/functions.rst | 54 ++++++++++++++- src/axom/inlet/docs/sphinx/readers.rst | 13 +++- src/axom/inlet/examples/CMakeLists.txt | 4 ++ src/axom/inlet/examples/functions.cpp | 86 ++++++++++++++++++++++++ 4 files changed, 153 insertions(+), 4 deletions(-) create mode 100644 src/axom/inlet/examples/functions.cpp diff --git a/src/axom/inlet/docs/sphinx/functions.rst b/src/axom/inlet/docs/sphinx/functions.rst index acc0e2012e..96050d0d61 100644 --- a/src/axom/inlet/docs/sphinx/functions.rst +++ b/src/axom/inlet/docs/sphinx/functions.rst @@ -119,6 +119,40 @@ alternative associated with ``scale``. Use ``containsFunctionValueAlternative("s and ``getFunctionValueAlternative("scale")`` on the containing ``Container`` to query and retrieve it without depending on an internal schema name. +Inside a struct collection, use a collection-relative path when each element supplies +its own value or callback. For example, this Lua input supplies ``bar`` directly +for one ``foo`` element and computes it with a callback for another: + +.. code-block:: Lua + + foo = { + [7] = { bar = 2 }, + [12] = { bar = function() return 3 end } + } + +The corresponding schema is compiled as part of Inlet's function callback example: + +.. literalinclude:: ../../examples/functions.cpp + :start-after: _inlet_nested_callback_alternative_start + :end-before: _inlet_nested_callback_alternative_end + :language: C++ + :dedent: 2 + +After verification, each concrete element contains only the representation that was provided. +A ``FromInlet`` specialization can normalize both representations to a scalar: + +.. literalinclude:: ../../examples/functions.cpp + :start-after: _inlet_function_value_alternative_access_start + :end-before: _inlet_function_value_alternative_access_end + :language: C++ + +This example evaluates a supplied callback during conversion. An application that needs +deferred evaluation can instead call ``get()`` on the result +of ``getFunctionValueAlternative("bar")`` and store the returned ``std::function``. + +When Inlet expands the schema, ``relativeToCollectionElement("bar")`` resolves against +each concrete collection element rather than against the collection container or root. + An overload that takes a schema name first remains available when the callback needs an independently addressable schema entry. Both forms accept an ``InputPath`` descriptor when exact or collection-relative resolution needs to be explicit. @@ -155,8 +189,8 @@ it as part of an application's Lua interface. Accessing --------- -To retrieve a function, both the implicit conversion and ``get`` syntax is supported. For example, -a function can be retrieved as follows: +To retrieve a function, both the implicit conversion and ``get`` syntax is supported. +For example, a function can be retrieved as follows: .. literalinclude:: ../../examples/mfem_coefficient.cpp :start-after: _inlet_mfem_coef_simple_retrieve_start @@ -184,3 +218,19 @@ by calling it directly: Callbacks copied out of Inlet keep their Lua state alive and remain callable after the Inlet object is destroyed. Lua execution errors and invalid callback return values are reported as ``std::runtime_error`` at the call site. + +The lifetime behavior is also demonstrated by the compiled function callback example: + +.. literalinclude:: ../../examples/functions.cpp + :start-after: _inlet_callback_after_destruction_start + :end-before: _inlet_callback_after_destruction_end + :language: C++ + :dedent: 2 + +All callbacks obtained from one ``LuaReader`` retain and share that reader's Lua state. +Copying a returned ``std::function`` does not clone the state, and mutations to Lua globals +or captured Lua tables made by one callback are therefore visible to the others. +Callbacks from the same reader must not be invoked concurrently: +even apparently read-only calls use the same Lua interpreter state. +Applications that need concurrent callback evaluation must serialize access to each state +or create independent ``LuaReader`` instances and parse the input separately for each thread. diff --git a/src/axom/inlet/docs/sphinx/readers.rst b/src/axom/inlet/docs/sphinx/readers.rst index fb5984a074..cb28616573 100644 --- a/src/axom/inlet/docs/sphinx/readers.rst +++ b/src/axom/inlet/docs/sphinx/readers.rst @@ -54,8 +54,17 @@ reader class here: :end-before: _inlet_sol_state_end :language: C++ -Inlet opens four Lua libraries by default: ``base``, ``math``, ``string``, ``package``. All libraries are documented -in `Sol's open_library documentation `_. +Inlet opens four Lua libraries by default: ``base``, ``math``, ``string``, ``package``. +All libraries are documented in `Sol's open_library documentation `_. + +.. warning:: + + Lua input is trusted executable code, and is not interpreted in a security sandbox. + In particular, the ``package`` library can load additional Lua or native modules, + and Inlet does not impose CPU, memory, recursion, or execution-time limits. + Only parse Lua input from trusted sources. Exposing additional libraries + or modifying the state through ``solState()`` can grant the input further capabilities + and can also change values after Inlet has read or verified them. For example, you can add the ``io`` library by doing this: diff --git a/src/axom/inlet/examples/CMakeLists.txt b/src/axom/inlet/examples/CMakeLists.txt index 593ce27bf1..93b6c79f25 100644 --- a/src/axom/inlet/examples/CMakeLists.txt +++ b/src/axom/inlet/examples/CMakeLists.txt @@ -21,6 +21,7 @@ blt_list_append( arrays.cpp documentation_generation.cpp fields.cpp + functions.cpp homogeneous_collections.cpp lua_library.cpp user_defined_variant.cpp @@ -72,6 +73,9 @@ if (SOL_FOUND) axom_add_test( NAME inlet_fields_ex COMMAND inlet_fields_ex) + axom_add_test( NAME inlet_functions_ex + COMMAND inlet_functions_ex ) + axom_add_test( NAME inlet_homogeneous_collections_ex COMMAND inlet_homogeneous_collections_ex ) diff --git a/src/axom/inlet/examples/functions.cpp b/src/axom/inlet/examples/functions.cpp new file mode 100644 index 0000000000..f72e81eee6 --- /dev/null +++ b/src/axom/inlet/examples/functions.cpp @@ -0,0 +1,86 @@ +// Copyright (c) Lawrence Livermore National Security, LLC and other +// Axom Project Contributors. See top-level LICENSE and COPYRIGHT +// files for dates and other details. +// +// SPDX-License-Identifier: (BSD-3-Clause) + +#include "axom/inlet.hpp" +#include "axom/slic/core/SimpleLogger.hpp" + +#include +#include + +namespace inlet = axom::inlet; + +// _inlet_function_value_alternative_access_start +struct Foo +{ + double bar; +}; + +template <> +struct FromInlet +{ + Foo operator()(const inlet::Container& input) + { + if(input.containsFunctionValueAlternative("bar")) + { + return {input.getFunctionValueAlternative("bar").call()}; + } + + return {input["bar"].get()}; + } +}; +// _inlet_function_value_alternative_access_end + +bool runNestedCallbackExample() +{ + auto reader = std::make_unique(); + reader->parseString( + "foo = { [7] = { bar = 2 }, " + " [12] = { bar = function () return 3 end } }"); + inlet::Inlet inlet(std::move(reader)); + + // _inlet_nested_callback_alternative_start + auto& foo = inlet.addStructArray("foo"); + foo.addDouble("bar"); + foo.addFunctionAsValueAlternative( + inlet::FunctionTag::Double, + {}, + inlet::InputPath::relativeToCollectionElement("bar")); + // _inlet_nested_callback_alternative_end + + if(!inlet.verify()) + { + return false; + } + + const auto values = inlet["foo"].get>(); + return values.at(7).bar == 2.0 && values.at(12).bar == 3.0; +} + +bool runCallbackLifetimeExample() +{ + // _inlet_callback_after_destruction_start + std::function callback; + { + auto reader = std::make_unique(); + reader->parseString("offset = 3.0; function foo (value) return value + offset end"); + inlet::Inlet inlet(std::move(reader)); + inlet.addFunction("foo", + inlet::FunctionTag::Double, + {inlet::FunctionTag::Double}); + callback = inlet["foo"].get>(); + } + + const double result = callback(4.0); + // _inlet_callback_after_destruction_end + return result == 7.0; +} + +int main() +{ + axom::slic::SimpleLogger logger; + + return runNestedCallbackExample() && runCallbackLifetimeExample() ? 0 : 1; +} From 6d31e04e167c9fea98993f674946545c7107a09a Mon Sep 17 00:00:00 2001 From: Kenneth Weiss Date: Sun, 9 Aug 2026 14:44:31 -0700 Subject: [PATCH 29/52] Light touchup to docs for consistency --- src/axom/inlet/docs/sphinx/readers.rst | 6 +++--- src/axom/klee/KleeError.hpp | 2 +- .../klee/docs/sphinx/specifying_shapes.rst | 12 ++++++------ src/axom/quest/examples/shaping_driver.cpp | 8 ++++---- .../shaping_tutorial/lesson_03/README.md | 19 +++++++++++-------- .../klee_operators_and_validation.cpp | 15 ++++++++------- .../shaping_tutorial/lesson_04/README.md | 4 ++-- .../lesson_04/quest_sampling_shaper.cpp | 8 ++++---- 8 files changed, 39 insertions(+), 35 deletions(-) diff --git a/src/axom/inlet/docs/sphinx/readers.rst b/src/axom/inlet/docs/sphinx/readers.rst index cb28616573..e923ab1bef 100644 --- a/src/axom/inlet/docs/sphinx/readers.rst +++ b/src/axom/inlet/docs/sphinx/readers.rst @@ -54,15 +54,15 @@ reader class here: :end-before: _inlet_sol_state_end :language: C++ -Inlet opens four Lua libraries by default: ``base``, ``math``, ``string``, ``package``. -All libraries are documented in `Sol's open_library documentation `_. +Inlet opens four Lua libraries by default: ``base``, ``math``, ``string``, ``package``. +All libraries are documented in `Sol's open_library documentation `_. .. warning:: Lua input is trusted executable code, and is not interpreted in a security sandbox. In particular, the ``package`` library can load additional Lua or native modules, and Inlet does not impose CPU, memory, recursion, or execution-time limits. - Only parse Lua input from trusted sources. Exposing additional libraries + Only parse Lua input from trusted sources. Exposing additional libraries or modifying the state through ``solState()`` can grant the input further capabilities and can also change values after Inlet has read or verified them. diff --git a/src/axom/klee/KleeError.hpp b/src/axom/klee/KleeError.hpp index 52087f4988..3fc1c5781b 100644 --- a/src/axom/klee/KleeError.hpp +++ b/src/axom/klee/KleeError.hpp @@ -16,7 +16,7 @@ namespace axom namespace klee { /** - * Describes an error that occurred while parsing a Klee file. + * Describes an error that occurred while parsing Klee input. * * Klee throws this exception for user input validation failures so callers * can report path-aware feedback from Inlet and Klee semantic checks. diff --git a/src/axom/klee/docs/sphinx/specifying_shapes.rst b/src/axom/klee/docs/sphinx/specifying_shapes.rst index b4aca019a6..7a18db82c4 100644 --- a/src/axom/klee/docs/sphinx/specifying_shapes.rst +++ b/src/axom/klee/docs/sphinx/specifying_shapes.rst @@ -258,7 +258,7 @@ operator, fields are evaluated in this order: Klee does not coordinate Lua evaluation across MPI ranks. If an application calls :code:`readShapeSet` on every rank, each rank reads and evaluates the deck -and initialization chunk independently. +and initialization chunk independently. .. code-block:: lua @@ -406,10 +406,10 @@ will match that of the (global or per-shape) `dimensions`. Overlay Rules ------------- -Shapes are added to meshes in the order in which they appear in the YAML -file. By default, each one replaces all materials that occupy the space -specified by its geometry file. This can be overridden by using the -:code:`replaces` and :code:`does_not_replace` properties. +Shapes are added to meshes in the order in which they appear in the input file. +By default, each one replaces all materials that occupy the space specified by +its geometry file. This can be overridden by using the :code:`replaces` +and :code:`does_not_replace` properties. .. code-block:: yaml @@ -514,7 +514,7 @@ Supported Operators The supported operators are listed below. Unless otherwise specified, the only difference between the 2D and 3D versions are that whenever points or vectors are expected, the points and vectors must be of the dimensionality -specified by the shape file. +specified by the Klee input. Operators take the form of :code:`operator_name: value`, where :code:`operator_name` is the name of the operator, and diff --git a/src/axom/quest/examples/shaping_driver.cpp b/src/axom/quest/examples/shaping_driver.cpp index 0282ffafb6..3e96c24265 100644 --- a/src/axom/quest/examples/shaping_driver.cpp +++ b/src/axom/quest/examples/shaping_driver.cpp @@ -338,12 +338,12 @@ struct Input void parse(int argc, char** argv, axom::CLI::App& app) { app.add_option("-i,--shape-file", shapeFile) - ->description("Path to input shape file") + ->description("Path to Klee input file (YAML or Lua)") ->check(axom::CLI::ExistingFile) ->required(); app.add_option("--lua-init-file", luaInitializationFile) - ->description("Lua chunk that returns initial globals for a Lua shape file") + ->description("Lua chunk that returns initial globals for a Lua Klee input file") ->check(axom::CLI::ExistingFile); app.add_flag("-v,--verbose,!--no-verbose", m_verboseOutput) @@ -727,7 +727,7 @@ int main(int argc, char** argv) AXOM_ANNOTATE_BEGIN("init"); //--------------------------------------------------------------------------- - // Load the klee shape file and extract some information + // Load the Klee input file and extract some information //--------------------------------------------------------------------------- try { @@ -747,7 +747,7 @@ int main(int argc, char** argv) } SLIC_WARNING( - axom::fmt::format("Error during parsing klee input. Found the following errors:\n{}", + axom::fmt::format("Error during parsing Klee input. Found the following errors:\n{}", axom::fmt::join(errs, "\n"))); finalizeLogger(); diff --git a/src/examples/shaping_tutorial/lesson_03/README.md b/src/examples/shaping_tutorial/lesson_03/README.md index 6f3a9989f2..a514aaf88c 100644 --- a/src/examples/shaping_tutorial/lesson_03/README.md +++ b/src/examples/shaping_tutorial/lesson_03/README.md @@ -195,9 +195,9 @@ endsolid Mesh -#### Basic shape file +#### Basic Klee input -A minimal shape file includes a ``dimensions`` field and a ``shapes`` list. +A minimal Klee input includes a ``dimensions`` field and a ``shapes`` list. The following example has a single shape representing the boundary a tetrahedron. The name of the shape -- in this case ``my_tetrahedron`` -- is used internally, and it will get shaped into the ``steel`` material: @@ -393,7 +393,7 @@ shapes: ## Let's see some code! -The code example for this lesson loads a Klee file, performs some validation and then prints out details about the geometric setup +The code example for this lesson loads a Klee input file, performs some validation and then prints out details about the geometric setup. ### Load and validate the Klee input @@ -403,20 +403,23 @@ try { shapeSet = axom::klee::readShapeSet(inputFilename); } -catch(axom::klee::KleeError& error) +catch(const axom::klee::KleeError& error) { std::vector errs; - for(auto verificationError : error.getErrors()) + for(const auto& verificationError : error.getErrors()) { errs.push_back(axom::fmt::format(" - '{}': {}", - static_cast(verificationError.path), - verificationError.message)); + static_cast(verificationError.path), + verificationError.message)); } SLIC_WARNING( - axom::fmt::format("Error during parsing klee input. Found the following errors:\n{}", + axom::fmt::format("Error during parsing Klee input. Found the following errors:\n{}", axom::fmt::join(errs, "\n"))); + return 1; } + +printShapeSetInfo(shapeSet); ``` The validator example also accepts an optional `--initialization-file` argument diff --git a/src/examples/shaping_tutorial/lesson_03/klee_operators_and_validation.cpp b/src/examples/shaping_tutorial/lesson_03/klee_operators_and_validation.cpp index 7b7f5340d6..6017e0160e 100644 --- a/src/examples/shaping_tutorial/lesson_03/klee_operators_and_validation.cpp +++ b/src/examples/shaping_tutorial/lesson_03/klee_operators_and_validation.cpp @@ -170,16 +170,16 @@ int main(int argc, char** argv) return axom::klee::readShapeSet(inputFilename, options); }; - // Load the klee shape file and extract some information + // Load the Klee input file and extract some information + axom::klee::ShapeSet shapeSet; try { - auto shapeSet = loadShapeSet(); - printShapeSetInfo(shapeSet); + shapeSet = loadShapeSet(); } - catch(axom::klee::KleeError& error) + catch(const axom::klee::KleeError& error) { std::vector errs; - for(auto verificationError : error.getErrors()) + for(const auto& verificationError : error.getErrors()) { errs.push_back(axom::fmt::format(" - '{}': {}", static_cast(verificationError.path), @@ -187,10 +187,11 @@ int main(int argc, char** argv) } SLIC_WARNING( - axom::fmt::format("Error during parsing klee input. Found the following errors:\n{}", + axom::fmt::format("Error during parsing Klee input. Found the following errors:\n{}", axom::fmt::join(errs, "\n"))); - exit(1); + return 1; } + printShapeSetInfo(shapeSet); return 0; } diff --git a/src/examples/shaping_tutorial/lesson_04/README.md b/src/examples/shaping_tutorial/lesson_04/README.md index b019367460..999a0a6c5a 100644 --- a/src/examples/shaping_tutorial/lesson_04/README.md +++ b/src/examples/shaping_tutorial/lesson_04/README.md @@ -464,7 +464,7 @@ The replacement rules are implicit -- each new shape replaces all existing mater > -m ../src/examples/shaping_tutorial/lesson_04/circle_input.lua > ``` -The Klee shape file can also be Lua. In [`circles.lua`](circles.lua), the scale factors +The Klee input can also be written in Lua. In [`circles.lua`](circles.lua), the scale factors are zero-argument callbacks. Klee evaluates them while reading the deck and passes ordinary concrete operators to Quest: @@ -489,7 +489,7 @@ This option is valid only with a Lua Klee deck and using it with YAML produces a Klee validation error. In an MPI run, every rank reads the same deck and initialization file and evaluates them independently. Consequently, callbacks and initialization chunks must be deterministic and should not depend on rank, -random values, mutable external files, or unsynchronized side effects. +random values, mutable external files, or unsynchronized side effects. Klee does not currently parse on one rank and broadcast the resulting shape set. diff --git a/src/examples/shaping_tutorial/lesson_04/quest_sampling_shaper.cpp b/src/examples/shaping_tutorial/lesson_04/quest_sampling_shaper.cpp index d82db06c3f..1c58139d6a 100644 --- a/src/examples/shaping_tutorial/lesson_04/quest_sampling_shaper.cpp +++ b/src/examples/shaping_tutorial/lesson_04/quest_sampling_shaper.cpp @@ -308,7 +308,7 @@ int main(int argc, char** argv) // -------------------------------------------------------------------------- axom::CLI::App app {"Shaping pipeline using separate Inlet mesh metadata and Klee shapes"}; std::string inputFilename; // Mesh metadata Inlet Lua - std::string kleeFilename; // Klee shape set YAML or Lua + std::string kleeFilename; // Klee input file (YAML or Lua) std::string luaInitializationFilename; bool verbose = false; @@ -317,11 +317,11 @@ int main(int argc, char** argv) ->required() ->check(axom::CLI::ExistingFile); app.add_option("-k,--klee_file", kleeFilename) - ->description("Klee shape set YAML or Lua file") + ->description("Klee input file (YAML or Lua)") ->required() ->check(axom::CLI::ExistingFile); app.add_option("--lua-init-file", luaInitializationFilename) - ->description("Lua chunk that returns initial globals for a Lua shape file") + ->description("Lua chunk that returns initial globals for a Lua Klee input file") ->check(axom::CLI::ExistingFile); app.add_flag("-v,--verbose", verbose)->description("Enable verbose (debug) logging"); @@ -406,7 +406,7 @@ int main(int argc, char** argv) static_cast(verificationError.path), verificationError.message)); } - SLIC_WARNING(axom::fmt::format("Error parsing klee input:\n{}", axom::fmt::join(errs, "\n"))); + SLIC_WARNING(axom::fmt::format("Error parsing Klee input:\n{}", axom::fmt::join(errs, "\n"))); return 1; } From 67993950ccbeab2fbf42d07a89f51088bea7372d Mon Sep 17 00:00:00 2001 From: Kenneth Weiss Date: Sun, 9 Aug 2026 15:20:22 -0700 Subject: [PATCH 30/52] Klee: Improves error messages related to Slice callbacks --- src/axom/klee/io/GeometryOperatorsIO.cpp | 67 ++++++++++++++++++++---- src/axom/klee/tests/klee_io.cpp | 45 +++++++++++++++- 2 files changed, 102 insertions(+), 10 deletions(-) diff --git a/src/axom/klee/io/GeometryOperatorsIO.cpp b/src/axom/klee/io/GeometryOperatorsIO.cpp index 30047d1350..6ce14ede00 100644 --- a/src/axom/klee/io/GeometryOperatorsIO.cpp +++ b/src/axom/klee/io/GeometryOperatorsIO.cpp @@ -88,6 +88,22 @@ std::string callbackContext(const inlet::Container &container, operatorLabel); } +[[noreturn]] void throwCallbackAwareValidationError(const inlet::Container &container, + char const *fieldName, + const std::string &ownerLabel, + const Path &fallbackPath, + const std::string &message) +{ + if(hasCallback(container, fieldName)) + { + throw KleeError( + {fieldPath(container, fieldName), + axom::fmt::format("{}: {}", callbackContext(container, fieldName, ownerLabel), message)}); + } + + throw KleeError({fallbackPath, message}); +} + template Result wrapCallbackErrors(const inlet::Container &container, char const *fieldName, @@ -412,23 +428,48 @@ OpPtr parseRotate(const SingleOperatorData &data, * \param normal a vector normal to the plane * \param up a vector which defines the positive Y direction * \param startProperties the properties before the slice - * \param path the path where the slice is specified, for error reporting + * \param sliceContainer the Inlet container describing the slice + * \param ownerLabel a description of the owning shape or named operator * \return the created operator * \throws KleeError if any value is invalid */ OpPtr makeCheckedSlice(Point3D origin, Vector3D normal, Vector3D up, - const TransformableGeometryProperties& startProperties, - const Path& path) + const TransformableGeometryProperties &startProperties, + const inlet::Container &sliceContainer, + const std::string &ownerLabel) { if(normal.is_zero()) { - throw KleeError({path, "The 'normal' vector must not be a zero vector"}); + throwCallbackAwareValidationError(sliceContainer, + "normal", + ownerLabel, + Path {sliceContainer.name()}, + "The 'normal' vector must not be a zero vector"); } if(!utilities::isNearlyEqual(normal.dot(up), 0.)) { - throw KleeError({path, "The 'normal' and 'up' vectors must be perpendicular"}); + const std::string message = "The 'normal' and 'up' vectors must be perpendicular"; + if(hasCallback(sliceContainer, "up")) + { + throwCallbackAwareValidationError( + sliceContainer, + "up", + ownerLabel, + Path {sliceContainer.name()}, + message); + } + if(hasCallback(sliceContainer, "normal")) + { + throwCallbackAwareValidationError( + sliceContainer, + "normal", + ownerLabel, + Path {sliceContainer.name()}, + message); + } + throw KleeError({sliceContainer.name(), message}); } return std::make_shared(origin, normal, up, startProperties); } @@ -468,7 +509,11 @@ primal::Point3D getPerpendicularSliceOrigin(const inlet::Container &sliceContain primal::Point3D givenOrigin = getPoint(sliceContainer, "origin", Dimensions::Three, ownerLabel); if(givenOrigin[nonZeroIndex] != axisIntercept) { - throw KleeError({sliceContainer["origin"].name(), "The origin must be on the slice plane"}); + throwCallbackAwareValidationError(sliceContainer, + "origin", + ownerLabel, + Path {sliceContainer["origin"].name()}, + "The origin must be on the slice plane"); } return givenOrigin; } @@ -495,7 +540,11 @@ primal::Vector3D getPerpendicularSliceNormal(const inlet::Container &sliceContai bool parallel = cross.is_zero(); if(!parallel) { - throw KleeError({sliceContainer["normal"].name(), "Invalid normal"}); + throwCallbackAwareValidationError(sliceContainer, + "normal", + ownerLabel, + Path {sliceContainer["normal"].name()}, + "Invalid normal"); } return givenNormal; } @@ -525,7 +574,7 @@ OpPtr readPerpendicularSlice(const inlet::Container &sliceContainer, auto normal = getPerpendicularSliceNormal(sliceContainer, defaultNormalVec, ownerLabel); auto up = getVector(sliceContainer, "up", Dimensions::Three, defaultUp, ownerLabel); - return makeCheckedSlice(origin, normal, up, startProperties, sliceContainer.name()); + return makeCheckedSlice(origin, normal, up, startProperties, sliceContainer, ownerLabel); } /** @@ -580,7 +629,7 @@ OpPtr parseSlice(const SingleOperatorData &data, auto origin = getPoint(sliceContainer, "origin", Dimensions::Three, ownerLabel); auto normal = getVector(sliceContainer, "normal", Dimensions::Three, ownerLabel); auto up = getVector(sliceContainer, "up", Dimensions::Three, ownerLabel); - return makeCheckedSlice(origin, normal, up, startProperties, sliceContainer.name()); + return makeCheckedSlice(origin, normal, up, startProperties, sliceContainer, ownerLabel); } /** diff --git a/src/axom/klee/tests/klee_io.cpp b/src/axom/klee/tests/klee_io.cpp index df3dd4c731..422e8ecf84 100644 --- a/src/axom/klee/tests/klee_io.cpp +++ b/src/axom/klee/tests/klee_io.cpp @@ -2117,7 +2117,50 @@ TEST(IOTest, readShapeSet_luaPerpendicularSliceCallbacksAreValidated) } catch(const KleeError &err) { - EXPECT_THAT(err.what(), HasSubstr(testCase.field)); + EXPECT_THAT(err.what(), + HasSubstr(std::string {"callback for '"} + testCase.field + "'")); + EXPECT_THAT(err.what(), HasSubstr("shape 'slice_callback'")); + EXPECT_THAT(err.what(), HasSubstr("operator 1")); + EXPECT_THAT(err.what(), HasSubstr(testCase.expectedMessage)); + } + } +} + +TEST(IOTest, readShapeSet_luaArbitrarySliceCallbackValidationErrorsIncludeContext) +{ + struct ValidationCase + { + const char *field; + const char *sliceFields; + const char *expectedMessage; + }; + const std::array cases {{ + {"normal", + "origin = {0, 0, 0}, " + "normal = function() return {0, 0, 0} end, " + "up = {0, 1, 0}", + "zero"}, + {"up", + "origin = {0, 0, 0}, " + "normal = {1, 0, 0}, " + "up = function() return {1, 0, 0} end", + "perpendicular"}, + }}; + + for(const auto &testCase : cases) + { + SCOPED_TRACE(testCase.field); + try + { + readShapeSetFromString(makeLuaSliceCallbackInput(testCase.sliceFields), InputFormat::Lua); + FAIL() << "Should have thrown"; + } + catch(const KleeError &err) + { + EXPECT_THAT(err.what(), + HasSubstr(std::string {"callback for '"} + testCase.field + "'")); + EXPECT_THAT(err.what(), HasSubstr("shape 'slice_callback'")); + EXPECT_THAT(err.what(), HasSubstr("operator 1")); EXPECT_THAT(err.what(), HasSubstr(testCase.expectedMessage)); } } From 34915173103fc4931277e8e51056380b00892c11 Mon Sep 17 00:00:00 2001 From: Kenneth Weiss Date: Sun, 9 Aug 2026 18:22:32 -0700 Subject: [PATCH 31/52] Inlet: Simplifies changes to support callbacks as value alternates --- src/axom/inlet/Container.cpp | 266 +++++++++-------------- src/axom/inlet/Container.hpp | 147 ++----------- src/axom/inlet/Inlet.hpp | 88 +------- src/axom/inlet/docs/sphinx/functions.rst | 61 +----- src/axom/inlet/examples/functions.cpp | 4 +- src/axom/inlet/tests/inlet_function.cpp | 244 ++++++--------------- src/axom/klee/io/GeometryOperatorsIO.cpp | 4 +- 7 files changed, 199 insertions(+), 615 deletions(-) diff --git a/src/axom/inlet/Container.cpp b/src/axom/inlet/Container.cpp index 66d146233b..bd7372c859 100644 --- a/src/axom/inlet/Container.cpp +++ b/src/axom/inlet/Container.cpp @@ -947,100 +947,64 @@ Verifiable& Container::addFunction(const std::string& name, const std::string& description, const std::string& pathOverride) { - const auto pathMode = (isStructCollection() || !m_nested_aggregates.empty()) - ? InputPathMode::RelativeToCollectionElement - : InputPathMode::Exact; - return addFunctionWithInputPath( - name, - ret_type, - arg_types, - description, - InputPath {pathOverride, pathMode}, - false); -} - -Verifiable& Container::addFunction(const std::string& name, - const FunctionTag ret_type, - const std::vector& arg_types, - const InputPath& inputPath, - const std::string& description) -{ - SLIC_ERROR_IF(inputPath.value.empty(), - "[Inlet] An explicit function input path must be non-empty"); - return addFunctionWithInputPath( - name, - ret_type, - arg_types, - description, - inputPath, - false); -} - -Verifiable& Container::addFunctionAsValueAlternative( - const std::string& name, - const FunctionTag ret_type, - const std::vector& arg_types, - const std::string& inputPath, - const std::string& description) -{ - SLIC_ERROR_IF(inputPath.empty(), - "[Inlet] A function value alternative requires a non-empty input path"); - const auto pathMode = (isStructCollection() || !m_nested_aggregates.empty()) - ? InputPathMode::RelativeToCollectionElement - : InputPathMode::Exact; - return addFunctionWithInputPath( - name, - ret_type, - arg_types, - description, - InputPath {inputPath, pathMode}, - true); -} + // If it has indices, we're adding a function to an array of structs, + // so we need to iterate over the subcontainers corresponding to elements of the array + std::vector>> funcs; -Verifiable& Container::addFunctionAsValueAlternative( - const std::string& name, - const FunctionTag ret_type, - const std::vector& arg_types, - const InputPath& inputPath, - const std::string& description) -{ - SLIC_ERROR_IF(inputPath.value.empty(), - "[Inlet] A function value alternative requires a non-empty input path"); - return addFunctionWithInputPath( + const bool is_nested = transformFromNestedElements( + std::back_inserter(funcs), name, - ret_type, - arg_types, - description, - inputPath, - true); -} + [&name, &ret_type, &arg_types, &description](Container& subcontainer, + const std::string& path) -> Verifiable& { + return subcontainer.addFunction(name, ret_type, arg_types, description, path); + }); + if(is_nested) + { + // Create an aggregate function so requirements can be collectively imposed + // on all elements of the array + m_aggregate_funcs.emplace_back(std::move(funcs)); -Verifiable& Container::addFunctionAsValueAlternative( - const FunctionTag ret_type, - const std::vector& arg_types, - const std::string& inputPath, - const std::string& description) -{ - return addFunctionAsValueAlternative( - nextFunctionValueAlternativeName(), - ret_type, - arg_types, - inputPath, - description); + // Remove when C++17 is available + return m_aggregate_funcs.back(); + } + else + { + // Otherwise actually add a Function + std::string fullName = utilities::string::appendPrefix(m_name, name); + // First check if the function already exists + auto iter = m_functionChildren.find(fullName); + if(iter != m_functionChildren.end()) + { + return *iter->second; + } + axom::sidre::Group* sidreGroup = createSidreGroup(fullName, description); + SLIC_ERROR_IF(sidreGroup == nullptr, + fmt::format("Failed to create Sidre group with name '{0}'", fullName)); + detail::addSignatureToGroup(ret_type, arg_types, sidreGroup); + // If a pathOverride is specified, needed when Inlet-internal groups + // are part of fullName + std::string lookupPath = (pathOverride.empty()) ? fullName : pathOverride; + lookupPath = + utilities::string::removeAllInstances(lookupPath, detail::COLLECTION_GROUP_NAME + "/"); + detail::updateUnexpectedNames(lookupPath, m_unexpectedNames); + auto func = m_reader.getFunction(lookupPath, ret_type, arg_types); + return storeFunction(sidreGroup, std::move(func), fullName, name); + } } Verifiable& Container::addFunctionAsValueAlternative( + const std::string& valueName, const FunctionTag ret_type, const std::vector& arg_types, - const InputPath& inputPath, const std::string& description) { - return addFunctionAsValueAlternative( - nextFunctionValueAlternativeName(), - ret_type, - arg_types, - inputPath, - description); + SLIC_ERROR_IF(valueName.empty(), + "[Inlet] A function value alternative requires a non-empty value name"); + return addFunctionValueAlternative(valueName, + ret_type, + arg_types, + description, + ""); } std::string Container::nextFunctionValueAlternativeName() @@ -1056,103 +1020,69 @@ std::string Container::nextFunctionValueAlternativeName() return name; } -Verifiable& Container::addFunctionWithInputPath( - const std::string& name, +Verifiable& Container::addFunctionValueAlternative( + const std::string& valueName, const FunctionTag ret_type, const std::vector& arg_types, const std::string& description, - const InputPath& inputPath, - const bool isValueAlternative) + const std::string& resolvedValuePath) { - // If it has indices, we're adding a function to an array - // of structs, so we need to iterate over the subcontainers - // corresponding to elements of the array + // Expand the public value name across nested collections. The callback's + // internal storage name must not participate in input-path resolution. std::vector>> funcs; const bool is_nested = transformFromNestedElements( std::back_inserter(funcs), - name, - [&name, &ret_type, &arg_types, &description, &inputPath, isValueAlternative]( - Container& subcontainer, - const std::string& path) -> Verifiable& { - InputPath nestedInputPath = inputPath; - if(nestedInputPath.value.empty()) - { - nestedInputPath = InputPath::exact(path); - } - return subcontainer.addFunctionWithInputPath(name, - ret_type, - arg_types, - description, - nestedInputPath, - isValueAlternative); + valueName, + [&valueName, &ret_type, &arg_types, &description](Container& subcontainer, + const std::string& path) -> Verifiable& { + return subcontainer.addFunctionValueAlternative(valueName, + ret_type, + arg_types, + description, + path); }); if(is_nested) { - // Create an aggregate function so requirements can be collectively imposed - // on all elements of the array m_aggregate_funcs.emplace_back(std::move(funcs)); - - // Remove when C++17 is available return m_aggregate_funcs.back(); } - else + + const auto existingAlternative = m_functionValueAlternatives.find(valueName); + if(existingAlternative != m_functionValueAlternatives.end()) { - // Otherwise actually add a Function - std::string fullName = utilities::string::appendPrefix(m_name, name); - // First check if the function already exists - auto iter = m_functionChildren.find(fullName); - if(iter != m_functionChildren.end()) - { - return *iter->second; - } + SLIC_ERROR(fmt::format("[Inlet] Value '{0}' already has a function alternative " + "in container '{1}'", + valueName, + m_name)); + return *existingAlternative->second; + } - if(isValueAlternative) - { - const auto existingAlternative = m_functionValueAlternatives.find(inputPath.value); - if(existingAlternative != m_functionValueAlternatives.end()) - { - SLIC_ERROR(fmt::format("[Inlet] Input path '{0}' already has a function value " - "alternative in container '{1}'", - inputPath.value, - m_name)); - return *existingAlternative->second; - } - } + const std::string internalName = nextFunctionValueAlternativeName(); + const std::string fullName = utilities::string::appendPrefix(m_name, internalName); + axom::sidre::Group* sidreGroup = createSidreGroup(fullName, description); + SLIC_ERROR_IF(sidreGroup == nullptr, + fmt::format("Failed to create Sidre group with name '{0}'", fullName)); + detail::addSignatureToGroup(ret_type, arg_types, sidreGroup); + sidreGroup->createViewScalar(detail::FUNCTION_VALUE_ALTERNATIVE_FLAG, + static_cast(1)); - axom::sidre::Group* sidreGroup = createSidreGroup(fullName, description); - SLIC_ERROR_IF(sidreGroup == nullptr, - fmt::format("Failed to create Sidre group with name '{0}'", fullName)); - detail::addSignatureToGroup(ret_type, arg_types, sidreGroup); - std::string lookupPath = inputPath.value; - if(lookupPath.empty()) - { - lookupPath = fullName; - } - else if(inputPath.mode == InputPathMode::RelativeToCollectionElement) - { - lookupPath = Path::join({Path(m_name), Path(inputPath.value)}); - } - lookupPath = - utilities::string::removeAllInstances(lookupPath, detail::COLLECTION_GROUP_NAME + "/"); - if(isValueAlternative) - { - sidreGroup->createViewScalar(detail::FUNCTION_VALUE_ALTERNATIVE_FLAG, - static_cast(1)); - } - detail::updateUnexpectedNames(lookupPath, m_unexpectedNames); - auto func = m_reader.getFunction(lookupPath, ret_type, arg_types); - if(isValueAlternative && func) - { - registerFunctionAlternativePath(lookupPath); - } - auto& storedFunction = storeFunction(sidreGroup, std::move(func), fullName, name); - if(isValueAlternative) - { - m_functionValueAlternatives[inputPath.value] = &storedFunction; - } - return storedFunction; + std::string lookupPath = resolvedValuePath.empty() + ? utilities::string::appendPrefix(m_name, valueName) + : resolvedValuePath; + lookupPath = + utilities::string::removeAllInstances(lookupPath, detail::COLLECTION_GROUP_NAME + "/"); + detail::updateUnexpectedNames(lookupPath, m_unexpectedNames); + auto func = m_reader.getFunction(lookupPath, ret_type, arg_types); + if(func) + { + registerFunctionAlternativePath(lookupPath); } + + auto& storedFunction = + storeFunction(sidreGroup, std::move(func), fullName, internalName); + m_functionValueAlternatives[valueName] = &storedFunction; + return storedFunction; } Proxy Container::operator[](const std::string& name) const @@ -1548,19 +1478,19 @@ const std::unordered_map>& Container::get return m_functionChildren; } -bool Container::containsFunctionValueAlternative(const std::string& inputPath) const +bool Container::containsFunctionValueAlternative(const std::string& valueName) const { - const auto iter = m_functionValueAlternatives.find(inputPath); + const auto iter = m_functionValueAlternatives.find(valueName); return iter != m_functionValueAlternatives.end() && static_cast(*iter->second); } -Function& Container::getFunctionValueAlternative(const std::string& inputPath) const +Function& Container::getFunctionValueAlternative(const std::string& valueName) const { - const auto iter = m_functionValueAlternatives.find(inputPath); + const auto iter = m_functionValueAlternatives.find(valueName); SLIC_ERROR_IF( iter == m_functionValueAlternatives.end(), - axom::fmt::format("[Inlet] Function value alternative not found for input path: {0}", inputPath)); + axom::fmt::format("[Inlet] Function value alternative not found for value: {0}", valueName)); return *iter->second; } diff --git a/src/axom/inlet/Container.hpp b/src/axom/inlet/Container.hpp index 4fd56cde04..c9cab94967 100644 --- a/src/axom/inlet/Container.hpp +++ b/src/axom/inlet/Container.hpp @@ -68,44 +68,6 @@ class Container; template class VariantStructCollection; -/*! - ***************************************************************************** - * \brief Controls how an aliased input path is resolved. - ***************************************************************************** - */ -enum class InputPathMode -{ - Exact, - RelativeToCollectionElement -}; - -/*! - ***************************************************************************** - * \brief Describes an input path whose resolution semantics must remain - * explicit when a schema is expanded across a struct collection. - ***************************************************************************** - */ -struct InputPath -{ - InputPath(std::string path, InputPathMode pathMode) - : value(std::move(path)) - , mode(pathMode) - { } - - static InputPath exact(std::string path) - { - return InputPath(std::move(path), InputPathMode::Exact); - } - - static InputPath relativeToCollectionElement(std::string path) - { - return InputPath(std::move(path), InputPathMode::RelativeToCollectionElement); - } - - std::string value; - InputPathMode mode; -}; - namespace detail { struct VariantStructFactoryBase @@ -775,8 +737,7 @@ class Container : public Verifiable * \param [in] arg_types The argument types of the function * \param [in] description Description of the function * \param [in] pathOverride The path within the input file to read from, - * if different than the structure of the Sidre datastore. When adding to a - * struct collection, this is resolved relative to each concrete element. + * if different than the structure of the Sidre datastore * * \return Reference to the created Function ***************************************************************************** @@ -789,85 +750,27 @@ class Container : public Verifiable /*! ***************************************************************************** - * \brief Get a function from an explicitly resolved input path. - * - * \param [in] name Name of the function in the schema - * \param [in] ret_type The return type of the function - * \param [in] arg_types The argument types of the function - * \param [in] inputPath Explicit input path and resolution mode - * \param [in] description Description of the function - * - * \return Reference to the created Function - ***************************************************************************** - */ - Verifiable& addFunction(const std::string& name, - FunctionTag ret_type, - const std::vector& arg_types, - const InputPath& inputPath, - const std::string& description = ""); - - /*! - ***************************************************************************** - * \brief Get a function that is an alternative representation of a primitive + * \brief Add a function that is an alternative representation of a primitive * value or collection in the input deck. * - * The function is stored in the Inlet schema under \a name, but is read from - * \a inputPath. If a function exists there, a primitive field or collection - * that reads the same input path is treated as absent rather than as having - * the wrong type. The function and concrete value may be added in either order. + * The function is stored under an Inlet-managed internal name and read from + * the same public value name as the concrete field or collection. + * If a function exists there, the concrete schema entry is treated as absent + * rather than as having the wrong type. The function and concrete value + * may be added in either order. * - * \param [in] name Name under which to store the function + * \param [in] valueName Public name of the concrete value or collection * \param [in] ret_type The return type of the function * \param [in] arg_types The argument types of the function - * \param [in] inputPath Path of the function in the input deck * \param [in] description Description of the function * * \return Reference to the created Function ***************************************************************************** */ Verifiable& addFunctionAsValueAlternative( - const std::string& name, - FunctionTag ret_type, - const std::vector& arg_types, - const std::string& inputPath, - const std::string& description = ""); - - /*! - ********************************************************************************* - * \brief Get a function value alternative from an explicitly resolved input path. - ********************************************************************************* - */ - Verifiable& addFunctionAsValueAlternative( - const std::string& name, - FunctionTag ret_type, - const std::vector& arg_types, - const InputPath& inputPath, - const std::string& description = ""); - - /*! - ********************************************************************************* - * \brief Add an automatically named function value alternative. - * - * The function remains accessible through \a inputPath using - * getFunctionValueAlternative(), without exposing its internal storage name. - ********************************************************************************* - */ - Verifiable& addFunctionAsValueAlternative( - FunctionTag ret_type, - const std::vector& arg_types, - const std::string& inputPath, - const std::string& description = ""); - - /*! - ********************************************************************************* - * \brief Add an automatically named function value alternative from an - * explicitly resolved input path. - ********************************************************************************* - */ - Verifiable& addFunctionAsValueAlternative( + const std::string& valueName, FunctionTag ret_type, const std::vector& arg_types, - const InputPath& inputPath, const std::string& description = ""); /*! @@ -1153,23 +1056,23 @@ class Container : public Verifiable /*! ***************************************************************************** - * \brief Return whether a function value alternative was supplied at the - * given input path. + * \brief Return whether a function value alternative was supplied for the + * given public value name. ***************************************************************************** */ - bool containsFunctionValueAlternative(const std::string& inputPath) const; + bool containsFunctionValueAlternative(const std::string& valueName) const; /*! ***************************************************************************** - * \brief Retrieve the function value alternative associated with an input - * path. + * \brief Retrieve the function value alternative associated with a public + * value name. ***************************************************************************** */ - Function& getFunctionValueAlternative(const std::string& inputPath) const; + Function& getFunctionValueAlternative(const std::string& valueName) const; /*! ***************************************************************************** - * \brief Return the input paths of supplied function value alternatives. + * \brief Return the public value names of supplied function alternatives. ***************************************************************************** */ std::vector getFunctionValueAlternativeNames() const; @@ -1414,18 +1317,12 @@ class Container : public Verifiable const std::string& fullName, const std::string& name); - /*! - ***************************************************************************** - * \brief Adds a function using an input path that may be relative to each - * concrete nested container. - ***************************************************************************** - */ - Verifiable& addFunctionWithInputPath(const std::string& name, - FunctionTag ret_type, - const std::vector& arg_types, - const std::string& description, - const InputPath& inputPath, - bool isValueAlternative); + Verifiable& addFunctionValueAlternative( + const std::string& valueName, + FunctionTag ret_type, + const std::vector& arg_types, + const std::string& description, + const std::string& resolvedValuePath); std::string nextFunctionValueAlternativeName(); diff --git a/src/axom/inlet/Inlet.hpp b/src/axom/inlet/Inlet.hpp index 934d2c2953..5f0f178e0b 100644 --- a/src/axom/inlet/Inlet.hpp +++ b/src/axom/inlet/Inlet.hpp @@ -433,104 +433,22 @@ class Inlet /*! ***************************************************************************** - * \brief Get a function from an explicitly resolved input path. - * - * \see Container::addFunction - ***************************************************************************** - */ - Verifiable& addFunction(const std::string& name, - const FunctionTag ret_type, - const std::vector& arg_types, - const InputPath& inputPath, - const std::string& description = "") - { - return m_globalContainer.addFunction(name, ret_type, arg_types, inputPath, description); - } - - /*! - ***************************************************************************** - * \brief Get a function that is an alternative representation of a primitive + * \brief Add a function that is an alternative representation of a primitive * value or collection in the input deck. * * \see Container::addFunctionAsValueAlternative ***************************************************************************** */ Verifiable& addFunctionAsValueAlternative( - const std::string& name, - FunctionTag ret_type, - const std::vector& arg_types, - const std::string& inputPath, - const std::string& description = "") - { - return m_globalContainer.addFunctionAsValueAlternative( - name, - ret_type, - arg_types, - inputPath, - description); - } - - /*! - ***************************************************************************** - * \brief Get a function value alternative from an explicitly resolved input - * path. - * - * \see Container::addFunctionAsValueAlternative - ***************************************************************************** - */ - Verifiable& addFunctionAsValueAlternative( - const std::string& name, - FunctionTag ret_type, - const std::vector& arg_types, - const InputPath& inputPath, - const std::string& description = "") - { - return m_globalContainer.addFunctionAsValueAlternative( - name, - ret_type, - arg_types, - inputPath, - description); - } - - /*! - ***************************************************************************** - * \brief Add an automatically named function value alternative. - * - * \see Container::addFunctionAsValueAlternative - ***************************************************************************** - */ - Verifiable& addFunctionAsValueAlternative( - FunctionTag ret_type, - const std::vector& arg_types, - const std::string& inputPath, - const std::string& description = "") - { - return m_globalContainer.addFunctionAsValueAlternative( - ret_type, - arg_types, - inputPath, - description); - } - - /*! - ***************************************************************************** - * \brief Add an automatically named function value alternative from an - * explicitly resolved input path. - * - * \see Container::addFunctionAsValueAlternative - ***************************************************************************** - */ - Verifiable& addFunctionAsValueAlternative( + const std::string& valueName, FunctionTag ret_type, const std::vector& arg_types, - const InputPath& inputPath, const std::string& description = "") { return m_globalContainer.addFunctionAsValueAlternative( + valueName, ret_type, arg_types, - inputPath, description); } diff --git a/src/axom/inlet/docs/sphinx/functions.rst b/src/axom/inlet/docs/sphinx/functions.rst index 96050d0d61..b5aa0c3d8e 100644 --- a/src/axom/inlet/docs/sphinx/functions.rst +++ b/src/axom/inlet/docs/sphinx/functions.rst @@ -63,54 +63,20 @@ In Lua, the following operations on the ``Vector`` type are supported (for ``Vec #. Dimension retrieval: ``d = u.dim`` #. Component retrieval: ``d = u.x``, ``d = u.y``, ``d = u.z`` -Aliased input paths -------------------- - -A function's schema name and its path in the input do not need to match. -Inlet provides an ``InputPath`` descriptor to allow a path to retain the same meaning -when a schema is expanded across a struct array or dictionary: - -.. code-block:: C++ - - using axom::inlet::InputPath; - - // Every element reads the same root-level callback. - shapes.addFunction( - "transform_callback", - axom::inlet::FunctionTag::Vector, - {axom::inlet::FunctionTag::Vector}, - InputPath::exact("shared_transform")); - - // Every element reads its own "transform" callback. - shapes.addFunction( - "transform_callback", - axom::inlet::FunctionTag::Vector, - {axom::inlet::FunctionTag::Vector}, - InputPath::relativeToCollectionElement("transform")); - -An exact path is used unchanged, regardless of where the function is stored in the schema. -A collection-relative path is joined to each concrete collection element. -Outside a collection, it is relative to the current ``Container``. - -The ``addFunction(name, returnType, argumentTypes, description, pathOverride)`` overload -remains available. Its string override retains the rule that -it is exact outside a struct collection and relative to each element inside one. -Prefer ``InputPath`` when adding new aliases so this behavior is explicit at the call site. - Functions as value alternatives ------------------------------- Some schemas accept either a concrete value or a function that computes that value. Use ``addFunctionAsValueAlternative`` to declare this relationship explicitly. -The recommended overload lets Inlet own the callback's internal storage name and -associates it with the concrete field's input path: +Inlet owns the callback's internal storage name and associates it with the concrete +field's public name: .. code-block:: C++ inlet.addFunctionAsValueAlternative( + "scale", axom::inlet::FunctionTag::Vector, - {}, - "scale"); + {}); inlet.addDoubleArray("scale"); With this schema, ``scale = {2.0, 3.0, 4.0}`` populates ``scale``, @@ -119,9 +85,9 @@ alternative associated with ``scale``. Use ``containsFunctionValueAlternative("s and ``getFunctionValueAlternative("scale")`` on the containing ``Container`` to query and retrieve it without depending on an internal schema name. -Inside a struct collection, use a collection-relative path when each element supplies -its own value or callback. For example, this Lua input supplies ``bar`` directly -for one ``foo`` element and computes it with a callback for another: +Inside a struct collection, Inlet resolves the public value name against each concrete +element. For example, this Lua input supplies ``bar`` directly for one ``foo`` element +and computes it with a callback for another: .. code-block:: Lua @@ -150,24 +116,17 @@ This example evaluates a supplied callback during conversion. An application tha deferred evaluation can instead call ``get()`` on the result of ``getFunctionValueAlternative("bar")`` and store the returned ``std::function``. -When Inlet expands the schema, ``relativeToCollectionElement("bar")`` resolves against -each concrete collection element rather than against the collection container or root. - -An overload that takes a schema name first remains available when the callback needs -an independently addressable schema entry. Both forms accept an ``InputPath`` descriptor -when exact or collection-relative resolution needs to be explicit. - The two schema entries may be added in either order. A function encountered at a normal field path remains a type error unless this alternative has been declared. -The narrow API permits one callback alternative for a public input path in a given container -and declaring a second callback alternative for that path is an error. +The narrow API permits one callback alternative for a public value name in a given container +and declaring a second callback alternative for that name is an error. Since one Lua object cannot simultaneously be a concrete value and a function, at most one of the two supported representations can match. Only the selected representation exists: ``contains`` reports the concrete field when a value was supplied, and ``containsFunctionValueAlternative`` reports the callback when a function was supplied. A value with an unrelated type matches neither representation and -fails verification. The shared input path is recognized by strict containers and is not +fails verification. The shared public name is recognized by strict containers and is not reported as unexpected. The returned function and the concrete field remain independently verifiable schema entries. diff --git a/src/axom/inlet/examples/functions.cpp b/src/axom/inlet/examples/functions.cpp index f72e81eee6..25b347d820 100644 --- a/src/axom/inlet/examples/functions.cpp +++ b/src/axom/inlet/examples/functions.cpp @@ -45,9 +45,9 @@ bool runNestedCallbackExample() auto& foo = inlet.addStructArray("foo"); foo.addDouble("bar"); foo.addFunctionAsValueAlternative( + "bar", inlet::FunctionTag::Double, - {}, - inlet::InputPath::relativeToCollectionElement("bar")); + {}); // _inlet_nested_callback_alternative_end if(!inlet.verify()) diff --git a/src/axom/inlet/tests/inlet_function.cpp b/src/axom/inlet/tests/inlet_function.cpp index eb3f16787a..4bba9df302 100644 --- a/src/axom/inlet/tests/inlet_function.cpp +++ b/src/axom/inlet/tests/inlet_function.cpp @@ -28,7 +28,6 @@ using axom::inlet::FunctionTag; using axom::inlet::FunctionType; using axom::inlet::Inlet; using axom::inlet::InletType; -using axom::inlet::InputPath; using axom::inlet::JSONSchemaWriter; using axom::inlet::LuaReader; using axom::inlet::SphinxWriter; @@ -246,19 +245,6 @@ TEST(inlet_function, function_path_override) EXPECT_DOUBLE_EQ(callback(3.0), 5.0); } -TEST(inlet_function, explicit_exact_function_input_path) -{ - auto inlet = createBasicInlet("function public_name (x) return x + 2 end"); - - inlet.addFunction("internal_name", - FunctionTag::Double, - {FunctionTag::Double}, - InputPath::exact("public_name")); - - auto callback = inlet["internal_name"].get>(); - EXPECT_DOUBLE_EQ(callback(3.0), 5.0); -} - TEST(inlet_function, function_value_alternative_is_schema_order_independent) { // Both schema entries inspect "foo"; declaration order must not select one. @@ -267,11 +253,7 @@ TEST(inlet_function, function_value_alternative_is_schema_order_independent) { inlet.addDouble("foo"); } - inlet.addFunctionAsValueAlternative( - "foo_callback", - FunctionTag::Double, - {}, - "foo"); + inlet.addFunctionAsValueAlternative("foo", FunctionTag::Double, {}); if(functionFirst) { inlet.addDouble("foo"); @@ -285,33 +267,30 @@ TEST(inlet_function, function_value_alternative_is_schema_order_independent) EXPECT_TRUE(inlet.verify()); EXPECT_FALSE(inlet.contains("foo")); - ASSERT_TRUE(inlet.contains("foo_callback")); - EXPECT_DOUBLE_EQ(inlet["foo_callback"].call(), 2.0); + auto& container = inlet.getGlobalContainer(); + ASSERT_TRUE(container.containsFunctionValueAlternative("foo")); + EXPECT_DOUBLE_EQ(container.getFunctionValueAlternative("foo").call(), 2.0); } } TEST(inlet_function, function_value_alternative_preserves_concrete_value) { auto inlet = createBasicInlet("foo = 4.0"); - inlet.addFunctionAsValueAlternative( - "foo_callback", - FunctionTag::Double, - {}, - "foo"); + inlet.addFunctionAsValueAlternative("foo", FunctionTag::Double, {}); inlet.addDouble("foo"); EXPECT_TRUE(inlet.verify()); - EXPECT_FALSE(inlet.contains("foo_callback")); + EXPECT_FALSE(inlet.getGlobalContainer().containsFunctionValueAlternative("foo")); ASSERT_TRUE(inlet.contains("foo")); EXPECT_DOUBLE_EQ(inlet["foo"].get(), 4.0); } -TEST(inlet_function, auto_named_function_value_alternative_uses_input_path) +TEST(inlet_function, function_value_alternative_uses_public_value_name) { // set and access function in alternative { auto inlet = createBasicInlet("function foo () return 2.0 end"); - inlet.addFunctionAsValueAlternative(FunctionTag::Double, {}, "foo"); + inlet.addFunctionAsValueAlternative("foo", FunctionTag::Double, {}); inlet.addDouble("foo"); EXPECT_TRUE(inlet.verify()); @@ -329,7 +308,7 @@ TEST(inlet_function, auto_named_function_value_alternative_uses_input_path) // set and access value in alternative { auto concreteInlet = createBasicInlet("foo = 4.0"); - concreteInlet.addFunctionAsValueAlternative(FunctionTag::Double, {}, "foo"); + concreteInlet.addFunctionAsValueAlternative("foo", FunctionTag::Double, {}); concreteInlet.addDouble("foo"); EXPECT_TRUE(concreteInlet.verify()); @@ -346,18 +325,14 @@ TEST(inlet_function, required_function_value_alternative_missing) auto inlet = createBasicInlet(""); inlet.addDouble("foo"); inlet - .addFunctionAsValueAlternative( - "foo_callback", - FunctionTag::Double, - {}, - "foo") + .addFunctionAsValueAlternative("foo", FunctionTag::Double, {}) .required(); std::vector errors; EXPECT_FALSE(inlet.verify(&errors)); EXPECT_FALSE(errors.empty()); EXPECT_FALSE(inlet.contains("foo")); - EXPECT_FALSE(inlet.contains("foo_callback")); + EXPECT_FALSE(inlet.getGlobalContainer().containsFunctionValueAlternative("foo")); EXPECT_FALSE(inlet.getGlobalContainer().exists()); } @@ -368,11 +343,7 @@ TEST(inlet_function, function_value_alternative_rejects_unrelated_wrong_type) { inlet.addDouble("foo"); } - inlet.addFunctionAsValueAlternative( - "foo_callback", - FunctionTag::Double, - {}, - "foo"); + inlet.addFunctionAsValueAlternative("foo", FunctionTag::Double, {}); if(functionFirst) { inlet.addDouble("foo"); @@ -386,10 +357,9 @@ TEST(inlet_function, function_value_alternative_rejects_unrelated_wrong_type) EXPECT_FALSE(inlet.verify()); EXPECT_FALSE(inlet.contains("foo")); - EXPECT_FALSE(inlet.contains("foo_callback")); + EXPECT_FALSE(inlet.getGlobalContainer().containsFunctionValueAlternative("foo")); // The input exists even though neither schema entry accepts its type. EXPECT_TRUE(inlet.isUserProvided("foo")); - EXPECT_FALSE(inlet.isUserProvided("foo_callback")); EXPECT_FALSE(inlet.getGlobalContainer().exists()); } } @@ -401,16 +371,12 @@ TEST(inlet_function, function_value_alternative_is_valid_in_strict_container) auto inlet = createBasicInlet(useFunction ? "function foo () return 2.0 end" : "foo = 4.0"); inlet.getGlobalContainer().strict(); - inlet.addFunctionAsValueAlternative( - "foo_callback", - FunctionTag::Double, - {}, - "foo"); + inlet.addFunctionAsValueAlternative("foo", FunctionTag::Double, {}); inlet.addDouble("foo"); EXPECT_TRUE(inlet.verify()); EXPECT_TRUE(inlet.unexpectedNames().empty()); - EXPECT_EQ(inlet.contains("foo_callback"), useFunction); + EXPECT_EQ(inlet.getGlobalContainer().containsFunctionValueAlternative("foo"), useFunction); EXPECT_EQ(inlet.contains("foo"), !useFunction); EXPECT_TRUE(inlet.getGlobalContainer().exists()); } @@ -419,26 +385,10 @@ TEST(inlet_function, function_value_alternative_is_valid_in_strict_container) TEST(inlet_function, function_value_alternative_rejects_duplicate_callback_alternative) { auto inlet = createBasicInlet("function scale () return 2.0 end"); - auto& callback = inlet.addFunctionAsValueAlternative( - "scale_callback", - FunctionTag::Double, - {}, - "scale"); - auto& repeatedCallback = inlet.addFunctionAsValueAlternative( - "scale_callback", - FunctionTag::Double, - {}, - "scale"); - - // Like other Inlet schema entries, repeating the same name is idempotent. - EXPECT_EQ(&callback, &repeatedCallback); + inlet.addFunctionAsValueAlternative("scale", FunctionTag::Double, {}); axom::slic::ScopedAbortToThrow abortGuard; - EXPECT_THROW(inlet.addFunctionAsValueAlternative( - "another_scale_callback", - FunctionTag::Double, - {}, - "scale"), + EXPECT_THROW(inlet.addFunctionAsValueAlternative("scale", FunctionTag::Double, {}), axom::slic::SlicAbortException); EXPECT_EQ(inlet.getGlobalContainer().getChildFunctions().size(), 1u); } @@ -448,19 +398,15 @@ TEST(inlet_function, function_value_alternative_sidre_and_generated_documentatio const std::string sphinxFile = "inlet_function_value_alternative.rst"; const std::string jsonFile = "inlet_function_value_alternative.json"; auto inlet = createBasicInlet("scale = 2.0"); - inlet.addFunctionAsValueAlternative( - "scale_callback", - FunctionTag::Double, - {}, - "scale"); + inlet.addFunctionAsValueAlternative("scale", FunctionTag::Double, {}); inlet.addDouble("scale", "Scale factor"); // Sidre marks the internal callback schema group without storing the callable. - const auto* callbackGroup = inlet.getGlobalContainer() - .getChildFunctions() - .at("scale_callback") - ->sidreGroup(); + const auto& childFunctions = inlet.getGlobalContainer().getChildFunctions(); + ASSERT_EQ(childFunctions.size(), 1u); + const auto* callbackGroup = childFunctions.begin()->second->sidreGroup(); ASSERT_TRUE(callbackGroup->hasView(axom::inlet::detail::FUNCTION_VALUE_ALTERNATIVE_FLAG)); + const std::string internalName = callbackGroup->getName(); const std::int8_t alternativeFlag = callbackGroup->getView(axom::inlet::detail::FUNCTION_VALUE_ALTERNATIVE_FLAG)->getScalar(); EXPECT_EQ(alternativeFlag, 1); @@ -479,9 +425,9 @@ TEST(inlet_function, function_value_alternative_sidre_and_generated_documentatio std::remove(jsonFile.c_str()); EXPECT_NE(sphinx.find("scale"), std::string::npos); - EXPECT_EQ(sphinx.find("scale_callback"), std::string::npos); + EXPECT_EQ(sphinx.find(internalName), std::string::npos); EXPECT_NE(json.find("scale"), std::string::npos); - EXPECT_EQ(json.find("scale_callback"), std::string::npos); + EXPECT_EQ(json.find(internalName), std::string::npos); } TEST(inlet_function, function_value_alternative_restart_does_not_create_internal_field) @@ -491,7 +437,7 @@ TEST(inlet_function, function_value_alternative_restart_does_not_create_internal auto reader = std::make_unique(); reader->parseString("scale = 2.0"); Inlet inlet(std::move(reader), datastore.getRoot()); - inlet.addFunctionAsValueAlternative(FunctionTag::Double, {}, "scale"); + inlet.addFunctionAsValueAlternative("scale", FunctionTag::Double, {}); inlet.addDouble("scale"); } @@ -727,31 +673,35 @@ struct FromInlet } }; -struct FooDictionary +struct FooWithValueAlternative { - std::unordered_map values; + double bar; }; template <> -struct FromInlet +struct FromInlet { - FooDictionary operator()(const axom::inlet::Container& base) + FooWithValueAlternative operator()(const axom::inlet::Container& base) { - return {base["foo"].get>()}; + if(base.containsFunctionValueAlternative("bar")) + { + return {base.getFunctionValueAlternative("bar").call()}; + } + return {base["bar"].get()}; } }; -struct FooWithValueAlternative +struct FooWithValueAlternativeDictionary { - std::function bar; + std::unordered_map values; }; template <> -struct FromInlet +struct FromInlet { - FooWithValueAlternative operator()(const axom::inlet::Container& base) + FooWithValueAlternativeDictionary operator()(const axom::inlet::Container& base) { - return {base["bar_callback"]}; + return {base["foo"].get>()}; } }; @@ -803,93 +753,42 @@ TEST(inlet_function, simple_vec3_to_vec3_array_of_struct) EXPECT_FLOAT_EQ(second_result[2], 18); } -TEST(inlet_function, function_path_override_in_array_of_struct) -{ - std::string testString = - "foo = { [7] = { bar = true, " - " callback = function (v) return 2*v end }, " - " [12] = { bar = false, " - " callback = function (v) return 3*v end } " - "}"; - auto inlet = createBasicInlet(testString); - - auto& arr_container = inlet.addStructArray("foo"); - arr_container.addBool("bar"); - arr_container.addFunction("baz", - FunctionTag::Vector, - {FunctionTag::Vector}, - "", - "callback"); - - auto foos = inlet["foo"].get>(); - EXPECT_FLOAT_EQ(foos[7].baz({1, 2, 3})[0], 2); - EXPECT_FLOAT_EQ(foos[12].baz({1, 2, 3})[0], 3); -} - -TEST(inlet_function, explicit_exact_function_input_path_in_array_of_struct) +TEST(inlet_function, function_value_alternative_in_array_of_struct) { auto inlet = createBasicInlet( - "shared_callback = function (v) return 4*v end; " - "foo = { [7] = { bar = true }, [12] = { bar = false } }"); + "foo = { [7] = { bar = 2 }, " + " [12] = { bar = function () return 3 end } }"); auto& arr_container = inlet.addStructArray("foo"); - arr_container.addBool("bar"); - arr_container.addFunction("baz", - FunctionTag::Vector, - {FunctionTag::Vector}, - InputPath::exact("shared_callback")); + arr_container.addDouble("bar"); + arr_container.addFunctionAsValueAlternative("bar", FunctionTag::Double, {}); - auto foos = inlet["foo"].get>(); - EXPECT_FLOAT_EQ(foos[7].baz({1, 2, 3})[0], 4); - EXPECT_FLOAT_EQ(foos[12].baz({1, 2, 3})[0], 4); + EXPECT_TRUE(inlet.verify()); + auto foos = + inlet["foo"].get>(); + EXPECT_DOUBLE_EQ(foos[7].bar, 2.0); + EXPECT_DOUBLE_EQ(foos[12].bar, 3.0); } -TEST(inlet_function, explicit_relative_function_input_path_in_nested_dictionary_of_struct) +TEST(inlet_function, function_value_alternative_in_nested_dictionary_of_struct) { auto inlet = createBasicInlet( "groups = { " - " [0] = { foo = { first = { bar = true, " - " callback = function (v) return 2*v end }, " - " second = { bar = false, " - " callback = function (v) return 3*v end } } }, " - " [1] = { foo = { third = { bar = true, " - " callback = function (v) return 4*v end } } } }"); - - auto& group_container = inlet.addStructArray("groups"); - auto& dict_container = group_container.addStructDictionary("foo"); - dict_container.addBool("bar"); - // Resolve "callback" from each dictionary value, not the enclosing schema. - dict_container.addFunction( - "baz", - FunctionTag::Vector, - {FunctionTag::Vector}, - InputPath::relativeToCollectionElement("callback")); - - auto groups = inlet["groups"].get>(); - EXPECT_FLOAT_EQ(groups[0].values["first"].baz({1, 2, 3})[0], 2); - EXPECT_FLOAT_EQ(groups[0].values["second"].baz({1, 2, 3})[0], 3); - EXPECT_FLOAT_EQ(groups[1].values["third"].baz({1, 2, 3})[0], 4); -} + " [0] = { foo = { first = { bar = 2 }, " + " second = { bar = function () return 3 end } } }, " + " [1] = { foo = { third = { bar = function () return 4 end } } } }"); -TEST(inlet_function, explicit_relative_function_value_alternative_in_array_of_struct) -{ - auto inlet = createBasicInlet( - "foo = { [7] = { bar = function () return 2 end }, " - " [12] = { bar = function () return 3 end } }"); - - auto& arr_container = inlet.addStructArray("foo"); - arr_container.addDouble("bar"); - arr_container.addFunctionAsValueAlternative( - "bar_callback", - FunctionTag::Double, - {}, - InputPath::relativeToCollectionElement("bar")); + auto& groups = inlet.addStructArray("groups"); + auto& foos = groups.addStructDictionary("foo"); + foos.addDouble("bar"); + foos.addFunctionAsValueAlternative("bar", FunctionTag::Double, {}); EXPECT_TRUE(inlet.verify()); - auto foos = - inlet["foo"].get>(); - EXPECT_DOUBLE_EQ(foos[7].bar(), 2.0); - EXPECT_DOUBLE_EQ(foos[12].bar(), 3.0); + const auto values = + inlet["groups"].get>(); + EXPECT_DOUBLE_EQ(values.at(0).values.at("first").bar, 2.0); + EXPECT_DOUBLE_EQ(values.at(0).values.at("second").bar, 3.0); + EXPECT_DOUBLE_EQ(values.at(1).values.at("third").bar, 4.0); } TEST(inlet_function, dimension_dependent_result) @@ -963,25 +862,6 @@ TEST(inlet_function, nested_function_in_struct) EXPECT_DOUBLE_EQ(second_func(4.0), 7.0); } -TEST(inlet_function, explicit_relative_function_input_path_in_nested_struct) -{ - std::string testString = - "quux = { [0] = { foo = { callback = function (x) return x + 1 end } }, " - " [1] = { foo = { callback = function (x) return x + 3 end } } }"; - auto inlet = createBasicInlet(testString); - - auto& quux_schema = inlet.addStructArray("quux"); - auto& foo_schema = quux_schema.addStruct("foo"); - foo_schema.addFunction("bar", - FunctionTag::Double, - {FunctionTag::Double}, - InputPath::relativeToCollectionElement("callback")); - - auto foos = inlet["quux"].get>(); - EXPECT_DOUBLE_EQ(foos[0].bar(4.0), 5.0); - EXPECT_DOUBLE_EQ(foos[1].bar(4.0), 7.0); -} - template Ret checkedCall(const axom::sol::protected_function& func, Args&&... args) { diff --git a/src/axom/klee/io/GeometryOperatorsIO.cpp b/src/axom/klee/io/GeometryOperatorsIO.cpp index 6ce14ede00..cca55e2014 100644 --- a/src/axom/klee/io/GeometryOperatorsIO.cpp +++ b/src/axom/klee/io/GeometryOperatorsIO.cpp @@ -872,9 +872,9 @@ inlet::Container &GeometryOperatorData::defineSchema(inlet::Container &parent, const auto addCallbackAlternative = [](inlet::Container &container, const char *fieldName, inlet::FunctionTag returnType) { container.addFunctionAsValueAlternative( + fieldName, returnType, - {}, - fieldName); + {}); }; addCallbackAlternative(opContainer, "translate", inlet::FunctionTag::Vector); From 7f06c0bd90d7b2fd8e965729fa6857370cdd099a Mon Sep 17 00:00:00 2001 From: Kenneth Weiss Date: Sun, 9 Aug 2026 18:35:45 -0700 Subject: [PATCH 32/52] Adds missing doxygen to new/changed functions --- src/axom/inlet/Container.hpp | 53 ++++++- src/axom/inlet/Inlet.hpp | 7 + src/axom/inlet/LuaReader.cpp | 3 + src/axom/inlet/LuaReader.hpp | 49 +++++- src/axom/klee/Units.hpp | 4 +- src/axom/klee/io/GeometryOperatorsIO.cpp | 166 ++++++++++++++++++++- src/axom/klee/io/GeometryOperatorsIO.hpp | 3 + src/axom/klee/io/IO.cpp | 49 ++++++ src/axom/klee/io/IO.hpp | 6 + src/axom/quest/examples/shaping_driver.cpp | 6 + 10 files changed, 331 insertions(+), 15 deletions(-) diff --git a/src/axom/inlet/Container.hpp b/src/axom/inlet/Container.hpp index c9cab94967..6a1879dcfd 100644 --- a/src/axom/inlet/Container.hpp +++ b/src/axom/inlet/Container.hpp @@ -1058,6 +1058,10 @@ class Container : public Verifiable ***************************************************************************** * \brief Return whether a function value alternative was supplied for the * given public value name. + * + * \param [in] valueName Public name of the concrete value or collection + * + * \return True when the input supplied a function for \a valueName ***************************************************************************** */ bool containsFunctionValueAlternative(const std::string& valueName) const; @@ -1066,6 +1070,11 @@ class Container : public Verifiable ***************************************************************************** * \brief Retrieve the function value alternative associated with a public * value name. + * + * \param [in] valueName Public name of the concrete value or collection + * + * \return The function alternative declared for \a valueName. The returned + * Function is empty when the input did not supply the function representation. ***************************************************************************** */ Function& getFunctionValueAlternative(const std::string& valueName) const; @@ -1073,6 +1082,8 @@ class Container : public Verifiable /*! ***************************************************************************** * \brief Return the public value names of supplied function alternatives. + * + * \return Public value names whose function representation was supplied ***************************************************************************** */ std::vector getFunctionValueAlternativeNames() const; @@ -1303,11 +1314,10 @@ class Container : public Verifiable ***************************************************************************** * \brief Stores an already-read Function in this Container's schema. * - * \param [in] The Sidre Group corresponding to the Function that will be added. - * \param [in] func The actual callable to store - * \param [in] The complete Container sequence for the Container this Function will be added to. - * \param [in] The Container sequence for the Container this Function will be added to, - * relative to this Container. + * \param [in] sidreGroup The Sidre Group corresponding to the Function + * \param [in] func The callable to store + * \param [in] fullName Complete Container path for the Function + * \param [in] name Function path relative to this Container * * \return The child Function matching the target name. ***************************************************************************** @@ -1317,6 +1327,20 @@ class Container : public Verifiable const std::string& fullName, const std::string& name); + /*! + ***************************************************************************** + * \brief Add an internally named function alternative for a public value. + * + * \param [in] valueName Public name of the concrete value or collection + * \param [in] ret_type The return type of the function + * \param [in] arg_types The argument types of the function + * \param [in] description Description of the function + * \param [in] resolvedValuePath Concrete input path when expanding a struct + * collection; empty when it should be derived from this Container + * + * \return Reference to the created Function or aggregate Function + ***************************************************************************** + */ Verifiable& addFunctionValueAlternative( const std::string& valueName, FunctionTag ret_type, @@ -1324,12 +1348,26 @@ class Container : public Verifiable const std::string& description, const std::string& resolvedValuePath); + /*! + ***************************************************************************** + * \brief Generate an unused internal name for a function value alternative. + * + * \return An internal name that does not collide with this Container's + * Sidre groups + ***************************************************************************** + */ std::string nextFunctionValueAlternativeName(); /*! ***************************************************************************** * \brief Adjust a Reader result when a function satisfies a declared value * alternative at the same input path. + * + * \param [in] inputPath Path read by the concrete schema entry + * \param [in] result Result returned by the Reader + * + * \return \a result, or ReaderResult::NotFound when a function alternative + * satisfies a WrongType result ***************************************************************************** */ ReaderResult adjustForFunctionAlternative(const std::string& inputPath, @@ -1338,6 +1376,9 @@ class Container : public Verifiable /*! ***************************************************************************** * \brief Record the Sidre group populated from an input value path. + * + * \param [in] inputPath Path read by the concrete schema entry + * \param [in] group Sidre group holding that entry's retrieval status ***************************************************************************** */ void registerValueInputPath(const std::string& inputPath, axom::sidre::Group* group); @@ -1346,6 +1387,8 @@ class Container : public Verifiable ***************************************************************************** * \brief Record a successfully read function alternative and update any * value schema entry that was added first. + * + * \param [in] inputPath Path satisfied by the function alternative ***************************************************************************** */ void registerFunctionAlternativePath(const std::string& inputPath); diff --git a/src/axom/inlet/Inlet.hpp b/src/axom/inlet/Inlet.hpp index 5f0f178e0b..1c1ec4ea5a 100644 --- a/src/axom/inlet/Inlet.hpp +++ b/src/axom/inlet/Inlet.hpp @@ -436,6 +436,13 @@ class Inlet * \brief Add a function that is an alternative representation of a primitive * value or collection in the input deck. * + * \param [in] valueName Public name of the concrete value or collection + * \param [in] ret_type The return type of the function + * \param [in] arg_types The argument types of the function + * \param [in] description Description of the function + * + * \return Reference to the created Function + * * \see Container::addFunctionAsValueAlternative ***************************************************************************** */ diff --git a/src/axom/inlet/LuaReader.cpp b/src/axom/inlet/LuaReader.cpp index 3e13b50f90..a80f27e64f 100644 --- a/src/axom/inlet/LuaReader.cpp +++ b/src/axom/inlet/LuaReader.cpp @@ -417,6 +417,7 @@ namespace detail * \brief Templated function for calling a sol function * * \param [in] func The sol function of unknown concrete type + * \param [in] args Arguments forwarded to the Lua function * \tparam Args The argument types of the function * * \return A checkable version of the function's result @@ -540,6 +541,7 @@ FunctionType::Vector extractResult(axom::sol::protected_fu * corresponding to the function signature * * \param [in] func The sol object containing the lua function of unknown signature + * \param [in] lua_state Shared ownership of the Lua state used by \a func * \tparam Ret The return type of the function * \tparam Args... The argument types of the function * @@ -569,6 +571,7 @@ std::function::type...)> buil * * \param [in] func The sol object containing the lua function of unknown signature * \param [in] arg_types The vector of argument types + * \param [in] lua_state Shared ownership of the Lua state used by \a func * * \tparam I The number of arguments processed, or "stack size", used to mitigate * infinite compile-time recursion diff --git a/src/axom/inlet/LuaReader.hpp b/src/axom/inlet/LuaReader.hpp index 7dc8006269..412f69e024 100644 --- a/src/axom/inlet/LuaReader.hpp +++ b/src/axom/inlet/LuaReader.hpp @@ -111,20 +111,64 @@ class LuaReader : public Reader std::shared_ptr solState() { return m_lua; } private: - // Expect this to be called for only Inlet-supported types. + /*! + ***************************************************************************** + * \brief Read a scalar value of an Inlet-supported type. + * + * \tparam T Inlet-supported destination type + * \param [in] id Input path to read + * \param [out] value Receives the converted value on success + * + * \return Success, NotFound, or WrongType for the resolved Lua object + ***************************************************************************** + */ template ReaderResult getValue(const std::string& id, T& value); - // Expect this to be called for only Inlet-supported types. + /*! + ***************************************************************************** + * \brief Read a typed Lua table into an Inlet map. + * + * \tparam Key Supported map key type + * \tparam Val Inlet-supported map value type + * \param [in] id Input path to read + * \param [out] values Receives matching table entries; cleared before reading + * \param [in] type Expected Lua type for each mapped value + * + * \return The aggregate retrieval status for the table and its entries + ***************************************************************************** + */ template ReaderResult getMap(const std::string& id, std::unordered_map& values, axom::sol::type type); + /*! + ***************************************************************************** + * \brief Read a Lua table whose values may have different primitive types. + * + * \tparam Key Supported map key type + * \param [in] id Input path to read + * \param [out] values Receives supported table entries; cleared before reading + * + * \return The aggregate retrieval status for the table and its entries + ***************************************************************************** + */ template ReaderResult getVariantMapInternal(const std::string& id, std::unordered_map& values); + /*! + ***************************************************************************** + * \brief Read the keys from a Lua table. + * + * \tparam T Supported index type + * \param [in] id Input path to read + * \param [out] indices Receives the table keys; cleared before reading + * + * \return Success, NotFound, or WrongType for the resolved Lua object + ***************************************************************************** + */ template ReaderResult getIndicesInternal(const std::string& id, std::vector& indices); @@ -135,6 +179,7 @@ class LuaReader : public Reader * \param [in] id The path to resolve * * \return The object at \a id, or an invalid object if the path was not found + * or an intermediate path component was not a table ***************************************************************************** */ axom::sol::object getObject(const std::string& id); diff --git a/src/axom/klee/Units.hpp b/src/axom/klee/Units.hpp index 1fccbb5916..e1f73e075b 100644 --- a/src/axom/klee/Units.hpp +++ b/src/axom/klee/Units.hpp @@ -27,8 +27,8 @@ namespace internal /*! * \brief Parse a unit string, reporting an invalid value at the supplied input path. * - * \param unitsAsString the unit string to parse - * \param path the input path to report on failure + * \param [in] unitsAsString the unit string to parse + * \param [in] path the input path to report on failure * * \return A LengthUnit containing the unit type. * \throws KleeError if the unit string is invalid diff --git a/src/axom/klee/io/GeometryOperatorsIO.cpp b/src/axom/klee/io/GeometryOperatorsIO.cpp index cca55e2014..b7d889453a 100644 --- a/src/axom/klee/io/GeometryOperatorsIO.cpp +++ b/src/axom/klee/io/GeometryOperatorsIO.cpp @@ -49,21 +49,50 @@ std::string childName(const inlet::Container& container, const std::string& name return result; } +/** + * Determine whether an operator field was supplied as a callback. + * + * \param container the operator container + * \param fieldName the public operator field name + * \return true when \a fieldName has a supplied function alternative + */ bool hasCallback(const inlet::Container &container, char const *fieldName) { return container.containsFunctionValueAlternative(fieldName); } +/** + * Determine whether an operator field was supplied directly or as a callback. + * + * \param container the operator container + * \param fieldName the public operator field name + * \return true when either supported representation was supplied + */ bool containsFieldOrCallback(const inlet::Container &container, char const *fieldName) { return container.contains(fieldName) || hasCallback(container, fieldName); } +/** + * Construct the input path for an operator field. + * + * \param container the operator container + * \param fieldName the public operator field name + * \return the full path to \a fieldName + */ Path fieldPath(const inlet::Container &container, char const *fieldName) { return Path::join({Path {container.name()}, Path {std::string {fieldName}}}); } +/** + * Build the contextual prefix for a callback diagnostic. + * + * \param container the operator or slice container + * \param fieldName the callback field name + * \param ownerLabel description of the owning shape or named operator + * \return a diagnostic prefix identifying the callback and its owner + */ std::string callbackContext(const inlet::Container &container, char const *fieldName, const std::string &ownerLabel) @@ -88,6 +117,16 @@ std::string callbackContext(const inlet::Container &container, operatorLabel); } +/** + * Throw a semantic validation error with callback context when applicable. + * + * \param container the operator or slice container + * \param fieldName the field whose value failed validation + * \param ownerLabel description of the owning shape or named operator + * \param fallbackPath path used when the field was supplied directly + * \param message semantic validation message + * \throws KleeError unconditionally + */ [[noreturn]] void throwCallbackAwareValidationError(const inlet::Container &container, char const *fieldName, const std::string &ownerLabel, @@ -104,6 +143,18 @@ std::string callbackContext(const inlet::Container &container, throw KleeError({fallbackPath, message}); } +/** + * Invoke a callback and translate its failures to contextual Klee errors. + * + * \tparam Result expected callback result type + * \tparam Func callback invocation type + * \param container the operator or slice container + * \param fieldName the callback field name + * \param ownerLabel description of the owning shape or named operator + * \param func callable that invokes the Inlet callback + * \return the callback result + * \throws KleeError if callback invocation or result conversion fails + */ template Result wrapCallbackErrors(const inlet::Container &container, char const *fieldName, @@ -128,6 +179,15 @@ Result wrapCallbackErrors(const inlet::Container &container, } } +/** + * Read a scalar operator field from its direct or callback representation. + * + * \param container the operator container + * \param fieldName the public operator field name + * \param ownerLabel description of the owning shape or named operator + * \return the resolved scalar value + * \throws KleeError if callback evaluation fails + */ double getScalar(const inlet::Container &container, char const *fieldName, const std::string &ownerLabel) @@ -141,6 +201,15 @@ double getScalar(const inlet::Container &container, return container[fieldName].get(); } +/** + * Read a string operator field from its direct or callback representation. + * + * \param container the operator container + * \param fieldName the public operator field name + * \param ownerLabel description of the owning shape or named operator + * \return the resolved string value + * \throws KleeError if callback evaluation fails + */ std::string getString(const inlet::Container &container, char const *fieldName, const std::string &ownerLabel) @@ -154,6 +223,12 @@ std::string getString(const inlet::Container &container, return container[fieldName].get(); } +/** + * Convert an Inlet callback vector to ordinary doubles. + * + * \param value the callback vector + * \return the active components of \a value + */ std::vector callbackVectorToDoubleVector(const inlet::FunctionType::Vector &value) { std::vector result; @@ -165,6 +240,16 @@ std::vector callbackVectorToDoubleVector(const inlet::FunctionType::Vect return result; } +/** + * Read and validate a vector operator field. + * + * \param container the operator container + * \param fieldName the public operator field name + * \param expectedDims required vector dimension + * \param ownerLabel description of the owning shape or named operator + * \return the resolved vector components + * \throws KleeError if callback evaluation or dimension validation fails + */ std::vector getDoubleVector(const inlet::Container &container, char const *fieldName, Dimensions expectedDims, @@ -192,6 +277,16 @@ std::vector getDoubleVector(const inlet::Container &container, return toDoubleVector(container[fieldName], expectedDims, fieldName); } +/** + * Read a point- or vector-like operator field. + * + * \tparam T destination point or vector type + * \param parent the operator container + * \param fieldName the public operator field name + * \param expectedDims required dimension + * \param ownerLabel description of the owning shape or named operator + * \return the resolved value converted to \a T + */ template T toArrayLike(const inlet::Container &parent, char const *fieldName, @@ -202,6 +297,17 @@ T toArrayLike(const inlet::Container &parent, return T {values.data(), static_cast(expectedDims)}; } +/** + * Read an optional point- or vector-like operator field. + * + * \tparam T destination point or vector type + * \param parent the operator container + * \param fieldName the public operator field name + * \param expectedDims required dimension + * \param defaultValue value returned when the field is absent + * \param ownerLabel description of the owning shape or named operator + * \return the resolved value, or \a defaultValue when absent + */ template T toArrayLike(const inlet::Container &parent, char const *fieldName, @@ -216,6 +322,15 @@ T toArrayLike(const inlet::Container &parent, return defaultValue; } +/** + * Read a required point field. + * + * \param parent the operator container + * \param fieldName the public operator field name + * \param expectedDims required dimension + * \param ownerLabel description of the owning shape or named operator + * \return the resolved point + */ Point3D getPoint(const inlet::Container &parent, char const *fieldName, Dimensions expectedDims, @@ -224,6 +339,16 @@ Point3D getPoint(const inlet::Container &parent, return toArrayLike(parent, fieldName, expectedDims, ownerLabel); } +/** + * Read an optional point field. + * + * \param parent the operator container + * \param fieldName the public operator field name + * \param expectedDims required dimension + * \param defaultValue value returned when the field is absent + * \param ownerLabel description of the owning shape or named operator + * \return the resolved point, or \a defaultValue when absent + */ Point3D getPoint(const inlet::Container &parent, char const *fieldName, Dimensions expectedDims, @@ -233,6 +358,15 @@ Point3D getPoint(const inlet::Container &parent, return toArrayLike(parent, fieldName, expectedDims, defaultValue, ownerLabel); } +/** + * Read a required vector field. + * + * \param parent the operator container + * \param fieldName the public operator field name + * \param expectedDims required dimension + * \param ownerLabel description of the owning shape or named operator + * \return the resolved vector + */ Vector3D getVector(const inlet::Container &parent, char const *fieldName, Dimensions expectedDims, @@ -241,6 +375,16 @@ Vector3D getVector(const inlet::Container &parent, return toArrayLike(parent, fieldName, expectedDims, ownerLabel); } +/** + * Read an optional vector field. + * + * \param parent the operator container + * \param fieldName the public operator field name + * \param expectedDims required dimension + * \param defaultValue value returned when the field is absent + * \param ownerLabel description of the owning shape or named operator + * \return the resolved vector, or \a defaultValue when absent + */ Vector3D getVector(const inlet::Container &parent, char const *fieldName, Dimensions expectedDims, @@ -353,8 +497,9 @@ void verifyObjectFields(const inlet::Container& containerToTest, /** * Parse a "translate" operator. * - * \param opContainer the Container from which to read the operator + * \param data the Inlet data from which to read the operator * \param startProperties the properties prior to this operator + * \param ownerLabel description of the owning shape or named operator * \return the created operator * \throws KleeError if the operator fields or vector dimensions are invalid */ @@ -372,8 +517,9 @@ OpPtr parseTranslate(const SingleOperatorData &data, /** * Parse a "rotate" operator. * - * \param opContainer the Container from which to read the operator + * \param data the Inlet data from which to read the operator * \param startProperties the properties prior to this operator + * \param ownerLabel description of the owning shape or named operator * \return the created operator * \throws KleeError if the rotation is invalid for the start dimensions or operator fields */ @@ -480,6 +626,7 @@ OpPtr makeCheckedSlice(Point3D origin, * \param sliceContainer the Container describing the slice * \param planeName the name of the plane ("x", "y", or "z") * \param defaultNormal the default normal vector + * \param ownerLabel description of the owning shape or named operator * \return the point to use as the origin * \throws KleeError if the specified origin is not on the slice plane */ @@ -523,6 +670,7 @@ primal::Point3D getPerpendicularSliceOrigin(const inlet::Container &sliceContain * * \param sliceContainer the Container describing the slice * \param defaultNormal the default normal vector + * \param ownerLabel description of the owning shape or named operator * \return the vector to use as the normal * \throws KleeError if the specified normal is not parallel to the slice plane normal */ @@ -557,6 +705,7 @@ primal::Vector3D getPerpendicularSliceNormal(const inlet::Container &sliceContai * \param defaultNormal the default normal vector for the type of plane being parsed * \param defaultUp the default up vector for the plane being parsed * \param startProperties the properties prior to this operator + * \param ownerLabel description of the owning shape or named operator * \return the parsed plane * \throws KleeError if the slice fields or values are invalid */ @@ -580,8 +729,9 @@ OpPtr readPerpendicularSlice(const inlet::Container &sliceContainer, /** * Parse a "slice" operator. * - * \param opContainer the Container from which to read the operator + * \param data the Inlet data from which to read the operator * \param startProperties the properties prior to this operator + * \param ownerLabel description of the owning shape or named operator * \return the created operator * \throws KleeError if the slice fields or values are invalid */ @@ -635,8 +785,9 @@ OpPtr parseSlice(const SingleOperatorData &data, /** * Parse a "scale" operator. * - * \param opContainer the Container from which to read the operator + * \param data the Inlet data from which to read the operator * \param startProperties the properties prior to this operator + * \param ownerLabel description of the owning shape or named operator * \return the created operator * \throws KleeError if the scale fields or vector dimensions are invalid */ @@ -706,8 +857,9 @@ OpPtr parseScale(const SingleOperatorData &data, /** * Parse a "convert_units_to" operator. * - * \param opContainer the Container from which to read the operator + * \param data the Inlet data from which to read the operator * \param startProperties the properties prior to this operator + * \param ownerLabel description of the owning shape or named operator * \return the created operator * \throws KleeError if the unit string or operator fields are invalid */ @@ -743,9 +895,10 @@ OpPtr parseConvertUnits(const SingleOperatorData &data, /** * Parse an operator specified via the "ref" command. * - * \param opContainer the Container from which to read the operator + * \param data the Inlet data from which to read the operator * \param startProperties the properties before the "ref" command * \param namedOperators a map of named operators from which to get referenced operators + * \param ownerLabel description of the owning shape or named operator * \return the created operator * \throws KleeError if the reference is missing or the operator fields are invalid */ @@ -802,6 +955,7 @@ OpPtr parseRef(const SingleOperatorData &data, * \param data the data from which to convert the operator * \param startProperties the properties before the operator * \param namedOperators a map of named operators from which to get referenced operators + * \param ownerLabel description of the owning shape or named operator * \return the created operator * \throws KleeError if the operator type or fields are invalid */ diff --git a/src/axom/klee/io/GeometryOperatorsIO.hpp b/src/axom/klee/io/GeometryOperatorsIO.hpp index bdd03fbdb5..b66865f914 100644 --- a/src/axom/klee/io/GeometryOperatorsIO.hpp +++ b/src/axom/klee/io/GeometryOperatorsIO.hpp @@ -57,6 +57,7 @@ class GeometryOperatorData * @param parent the parent container * @param fieldName the name of the field * @param description a description of the field + * @param enableLuaCallbacks whether operator fields may be supplied as Lua callbacks * @return the Container for the new item */ static inlet::Container &defineSchema(inlet::Container &parent, @@ -102,6 +103,7 @@ struct NamedOperatorData * Define the schema for a named operator. * * @param container the container in which to describe a single named operator + * @param enableLuaCallbacks whether operator fields may be supplied as Lua callbacks */ static void defineSchema(inlet::Container &container, bool enableLuaCallbacks = false); }; @@ -134,6 +136,7 @@ struct NamedOperatorMapData * * @param parent the parent object in which to define the operator map * @param name the name of the map + * @param enableLuaCallbacks whether operator fields may be supplied as Lua callbacks */ static void defineSchema(inlet::Container &parent, const std::string &name, diff --git a/src/axom/klee/io/IO.cpp b/src/axom/klee/io/IO.cpp index eddad1ef7f..40ea6338a1 100644 --- a/src/axom/klee/io/IO.cpp +++ b/src/axom/klee/io/IO.cpp @@ -43,6 +43,11 @@ bool isLuaIdentifier(const std::string &name); class KleeLuaReader : public inlet::LuaReader { public: + /** + * Return the string keys currently installed in the Lua global environment. + * + * \return the current top-level Lua global names + */ std::unordered_set topLevelGlobalNames() { std::unordered_set names; @@ -57,12 +62,27 @@ class KleeLuaReader : public inlet::LuaReader return names; } + /** + * Install a caller-provided primitive as a mutable Lua global. + * + * \param name the global name + * \param value the primitive value to install + */ void setInitialGlobal(const std::string &name, const LuaGlobalValue &value) { auto lua = solState(); std::visit([&](const auto &typedValue) { (*lua)[name] = typedValue; }, value); } + /** + * Evaluate an initialization chunk and install its exported values as globals. + * + * \param initialization the source and diagnostic label for the chunk + * \param reservedNames built-in Lua globals that exports may not replace + * \param existingExternalNames caller-provided globals that exports may not replace + * \return the names exported by the chunk + * \throws KleeError if evaluation fails or the returned exports are invalid + */ std::unordered_set applyInitializationChunk( const LuaInitializationChunk &initialization, const std::unordered_set &reservedNames, @@ -296,6 +316,7 @@ namespace * Define the schema for the "geometry" member of shapes * * @param geometry the Container representing a "geometry" object. + * @param enableLuaCallbacks whether operator fields may be supplied as Lua callbacks */ void defineGeometry(inlet::Container &geometry, bool enableLuaCallbacks) { @@ -326,6 +347,7 @@ void defineGeometry(inlet::Container &geometry, bool enableLuaCallbacks) * Define the schema for the list of shapes * * @param document the Inlet document for which to define the schema + * @param enableLuaCallbacks whether operator fields may be supplied as Lua callbacks */ void defineShapeList(inlet::Inlet &document, bool enableLuaCallbacks) { @@ -374,6 +396,7 @@ void defineShapeList(inlet::Inlet &document, bool enableLuaCallbacks) * Define the schema for Klee documents. * * @param document the Inlet document for which to define the schema + * @param enableLuaCallbacks whether operator fields may be supplied as Lua callbacks */ void defineKleeSchema(inlet::Inlet &document, bool enableLuaCallbacks) { @@ -390,6 +413,7 @@ void defineKleeSchema(inlet::Inlet &document, bool enableLuaCallbacks) * \param data the data read from inlet * \param fileDimensions the number of dimensions the file expects shapes to have * \param namedOperators any named operators that were parsed from the file + * \param shapeName the owning shape name used in callback diagnostics * \return the geometry description for the shape * \throws KleeError if the converted geometry does not match the expected dimensions */ @@ -531,6 +555,12 @@ InputFormat inferInputFormat(const std::string& filePath) extension)}); } +/** + * Determine whether a name is a reserved Lua keyword. + * + * \param name the candidate name + * \return true when \a name is a Lua keyword + */ bool isLuaKeyword(const std::string &name) { static const std::unordered_set keywords { @@ -542,6 +572,12 @@ bool isLuaKeyword(const std::string &name) return keywords.find(name) != keywords.end(); } +/** + * Determine whether a name is an ASCII Lua identifier that is not a keyword. + * + * \param name the candidate name + * \return true when \a name may be used as a Lua identifier + */ bool isLuaIdentifier(const std::string &name) { if(name.empty()) @@ -568,6 +604,12 @@ bool isLuaIdentifier(const std::string &name) !isLuaKeyword(name); } +/** + * Validate names supplied through LuaInputOptions::initialGlobals. + * + * \param initialGlobals the caller-provided globals to validate + * \throws KleeError if any global name is not a valid Lua identifier + */ void validateInitialGlobals(const LuaInitialGlobals &initialGlobals) { for(const auto &entry : initialGlobals) @@ -704,6 +746,13 @@ void parseOrThrow(Parse&& parse, } } +/** + * Append errors for unexpected top-level Lua globals. + * + * \param doc the verified Inlet document + * \param errors receives errors for unexpected globals + * \param allowedGlobals caller-provided globals that are permitted in the deck + */ void appendUnexpectedGlobalErrors(const inlet::Inlet &doc, std::vector &errors, const std::unordered_set &allowedGlobals) diff --git a/src/axom/klee/io/IO.hpp b/src/axom/klee/io/IO.hpp index 33da0e9645..a7c07904e0 100644 --- a/src/axom/klee/io/IO.hpp +++ b/src/axom/klee/io/IO.hpp @@ -28,7 +28,10 @@ enum class InputFormat /// Lua initialization chunk evaluated before deck parsing in an isolated environment. struct LuaInitializationChunk { + /// Lua source to evaluate before the input deck. std::string source; + + /// Source label used in diagnostics from this chunk. std::string label {""}; }; @@ -53,6 +56,7 @@ struct LuaInputOptions * * \param stream the stream from which to read the ShapeSet * \note This overload reads YAML for backward compatibility. + * \return the ShapeSet read from the stream * \throws KleeError if parsing, schema verification, or semantic validation fails */ ShapeSet readShapeSet(std::istream& stream); @@ -62,6 +66,7 @@ ShapeSet readShapeSet(std::istream& stream); * * \param stream the stream from which to read the ShapeSet * \param format the input file format to use + * \return the ShapeSet read from the stream * \throws KleeError if parsing, schema verification, or semantic validation fails, * or if the requested input format is unsupported by this build */ @@ -74,6 +79,7 @@ ShapeSet readShapeSet(std::istream& stream, InputFormat format); * \param format the input deck format to use * \param options optional initial globals and initialization for a Lua input deck * \note Non-empty Lua input options are supported only for Lua input decks. + * \return the ShapeSet read from the stream * \throws KleeError if the input or Lua input options are invalid */ ShapeSet readShapeSet(std::istream &stream, diff --git a/src/axom/quest/examples/shaping_driver.cpp b/src/axom/quest/examples/shaping_driver.cpp index 3e96c24265..c659a50f24 100644 --- a/src/axom/quest/examples/shaping_driver.cpp +++ b/src/axom/quest/examples/shaping_driver.cpp @@ -557,6 +557,12 @@ struct Input slic::setLoggingMsgLevel(m_verboseOutput ? slic::message::Debug : slic::message::Info); } + /** + * Load the optional Lua initialization file selected on the command line. + * + * \return Lua input options containing the initialization source and label + * \throws klee::KleeError if the initialization file cannot be read + */ klee::LuaInputOptions loadLuaInputOptions() const { klee::LuaInputOptions options; From 3a6f114ab90239753113daea3e7c2e7e1f5357a2 Mon Sep 17 00:00:00 2001 From: Kenneth Weiss Date: Sun, 9 Aug 2026 20:43:01 -0700 Subject: [PATCH 33/52] Inlet: Function value alternatives now own their FunctionVariant callables directly --- src/axom/inlet/Container.cpp | 112 +++++++++-------------- src/axom/inlet/Container.hpp | 66 ++++++------- src/axom/inlet/Inlet.hpp | 17 +--- src/axom/inlet/SphinxWriter.cpp | 8 +- src/axom/inlet/docs/sphinx/functions.rst | 28 ++---- src/axom/inlet/inlet_utils.hpp | 1 - src/axom/inlet/tests/inlet_function.cpp | 90 ++---------------- 7 files changed, 97 insertions(+), 225 deletions(-) diff --git a/src/axom/inlet/Container.cpp b/src/axom/inlet/Container.cpp index bd7372c859..547c3f6285 100644 --- a/src/axom/inlet/Container.cpp +++ b/src/axom/inlet/Container.cpp @@ -53,13 +53,6 @@ Container::Container(const std::string& name, { if(group.isUsingMap() && group.hasView("InletType")) { - // Lua callables cannot be reconstructed from Sidre. In particular, - // do not reconstruct their internal schema groups as ordinary Fields. - if(group.hasView(detail::FUNCTION_VALUE_ALTERNATIVE_FLAG)) - { - continue; - } - const std::string inletType = group.getView("InletType")->getString(); const std::string childName = utilities::string::appendPrefix(m_name, group.getName()); @@ -109,17 +102,25 @@ void Container::forEachCollectionElement(Func&& func) const template bool Container::transformFromNestedElements(OutputIt output, const std::string& name, Func&& func) const +{ + return forEachNestedElement(name, [&](Container& container, const std::string& path) { + *output++ = func(container, path); + }); +} + +template +bool Container::forEachNestedElement(const std::string& name, Func&& func) const { for(Container& container : m_nested_aggregates) { - *output++ = func(container, {}); + func(container, {}); } if(isStructCollection()) { for(const auto& indexPath : detail::collectionIndicesWithPaths(*this, name)) { - *output++ = func(getContainer(indexPath.first), indexPath.second); + func(getContainer(indexPath.first), indexPath.second); } } return isStructCollection() || !m_nested_aggregates.empty(); @@ -992,60 +993,33 @@ Verifiable& Container::addFunction(const std::string& name, } } -Verifiable& Container::addFunctionAsValueAlternative( +void Container::addFunctionAsValueAlternative( const std::string& valueName, const FunctionTag ret_type, - const std::vector& arg_types, - const std::string& description) + const std::vector& arg_types) { SLIC_ERROR_IF(valueName.empty(), "[Inlet] A function value alternative requires a non-empty value name"); - return addFunctionValueAlternative(valueName, - ret_type, - arg_types, - description, - ""); -} - -std::string Container::nextFunctionValueAlternativeName() -{ - std::string name; - std::string fullName; - do - { - name = - axom::fmt::format("__inlet_function_value_alternative_{}", m_nextFunctionValueAlternativeId++); - fullName = utilities::string::appendPrefix(m_name, name); - } while(m_sidreRootGroup->hasGroup(fullName)); - return name; + SLIC_ERROR_IF(ret_type == FunctionTag::Void, + "[Inlet] A function value alternative requires a non-void return type"); + addFunctionValueAlternative(valueName, ret_type, arg_types, ""); } -Verifiable& Container::addFunctionValueAlternative( +void Container::addFunctionValueAlternative( const std::string& valueName, const FunctionTag ret_type, const std::vector& arg_types, - const std::string& description, const std::string& resolvedValuePath) { - // Expand the public value name across nested collections. The callback's - // internal storage name must not participate in input-path resolution. - std::vector>> funcs; - - const bool is_nested = transformFromNestedElements( - std::back_inserter(funcs), + // Expand the public value name across nested collections. + const bool is_nested = forEachNestedElement( valueName, - [&valueName, &ret_type, &arg_types, &description](Container& subcontainer, - const std::string& path) -> Verifiable& { - return subcontainer.addFunctionValueAlternative(valueName, - ret_type, - arg_types, - description, - path); + [&valueName, &ret_type, &arg_types](Container& subcontainer, const std::string& path) { + subcontainer.addFunctionValueAlternative(valueName, ret_type, arg_types, path); }); if(is_nested) { - m_aggregate_funcs.emplace_back(std::move(funcs)); - return m_aggregate_funcs.back(); + return; } const auto existingAlternative = m_functionValueAlternatives.find(valueName); @@ -1055,18 +1029,9 @@ Verifiable& Container::addFunctionValueAlternative( "in container '{1}'", valueName, m_name)); - return *existingAlternative->second; + return; } - const std::string internalName = nextFunctionValueAlternativeName(); - const std::string fullName = utilities::string::appendPrefix(m_name, internalName); - axom::sidre::Group* sidreGroup = createSidreGroup(fullName, description); - SLIC_ERROR_IF(sidreGroup == nullptr, - fmt::format("Failed to create Sidre group with name '{0}'", fullName)); - detail::addSignatureToGroup(ret_type, arg_types, sidreGroup); - sidreGroup->createViewScalar(detail::FUNCTION_VALUE_ALTERNATIVE_FLAG, - static_cast(1)); - std::string lookupPath = resolvedValuePath.empty() ? utilities::string::appendPrefix(m_name, valueName) : resolvedValuePath; @@ -1078,11 +1043,8 @@ Verifiable& Container::addFunctionValueAlternative( { registerFunctionAlternativePath(lookupPath); } - - auto& storedFunction = - storeFunction(sidreGroup, std::move(func), fullName, internalName); - m_functionValueAlternatives[valueName] = &storedFunction; - return storedFunction; + func.setName(std::move(lookupPath)); + m_functionValueAlternatives.emplace(valueName, std::move(func)); } Proxy Container::operator[](const std::string& name) const @@ -1415,7 +1377,14 @@ bool Container::exists() const return static_cast(*entry.second); }); - return has_containers || has_fields || has_functions; + const bool has_function_alternatives = + std::any_of(m_functionValueAlternatives.begin(), + m_functionValueAlternatives.end(), + [](const decltype(m_functionValueAlternatives)::value_type& entry) { + return static_cast(entry.second); + }); + + return has_containers || has_fields || has_functions || has_function_alternatives; } bool Container::isUserProvided() const @@ -1440,7 +1409,14 @@ bool Container::isUserProvided() const return static_cast(*entry.second); }); - return has_containers || has_fields || has_functions; + const bool has_function_alternatives = + std::any_of(m_functionValueAlternatives.begin(), + m_functionValueAlternatives.end(), + [](const decltype(m_functionValueAlternatives)::value_type& entry) { + return static_cast(entry.second); + }); + + return has_containers || has_fields || has_functions || has_function_alternatives; } bool Container::isUserProvided(const std::string& name) const @@ -1482,17 +1458,17 @@ bool Container::containsFunctionValueAlternative(const std::string& valueName) c { const auto iter = m_functionValueAlternatives.find(valueName); return iter != m_functionValueAlternatives.end() && - static_cast(*iter->second); + static_cast(iter->second); } -Function& Container::getFunctionValueAlternative(const std::string& valueName) const +const FunctionVariant& Container::getFunctionValueAlternative(const std::string& valueName) const { const auto iter = m_functionValueAlternatives.find(valueName); SLIC_ERROR_IF( iter == m_functionValueAlternatives.end(), axom::fmt::format("[Inlet] Function value alternative not found for value: {0}", valueName)); - return *iter->second; + return iter->second; } std::vector Container::getFunctionValueAlternativeNames() const @@ -1500,7 +1476,7 @@ std::vector Container::getFunctionValueAlternativeNames() const std::vector result; for(const auto& entry : m_functionValueAlternatives) { - if(static_cast(*entry.second)) + if(static_cast(entry.second)) { result.push_back(entry.first); } diff --git a/src/axom/inlet/Container.hpp b/src/axom/inlet/Container.hpp index 6a1879dcfd..c4468882e5 100644 --- a/src/axom/inlet/Container.hpp +++ b/src/axom/inlet/Container.hpp @@ -753,25 +753,19 @@ class Container : public Verifiable * \brief Add a function that is an alternative representation of a primitive * value or collection in the input deck. * - * The function is stored under an Inlet-managed internal name and read from - * the same public value name as the concrete field or collection. - * If a function exists there, the concrete schema entry is treated as absent - * rather than as having the wrong type. The function and concrete value - * may be added in either order. + * The function is read from the same public value name as the concrete field + * or collection. If a function exists there, the concrete schema entry is + * treated as absent rather than as having the wrong type. The function and + * concrete value may be added in either order. * * \param [in] valueName Public name of the concrete value or collection * \param [in] ret_type The return type of the function * \param [in] arg_types The argument types of the function - * \param [in] description Description of the function - * - * \return Reference to the created Function ***************************************************************************** */ - Verifiable& addFunctionAsValueAlternative( - const std::string& valueName, - FunctionTag ret_type, - const std::vector& arg_types, - const std::string& description = ""); + void addFunctionAsValueAlternative(const std::string& valueName, + FunctionTag ret_type, + const std::vector& arg_types); /*! ******************************************************************************* @@ -1074,10 +1068,10 @@ class Container : public Verifiable * \param [in] valueName Public name of the concrete value or collection * * \return The function alternative declared for \a valueName. The returned - * Function is empty when the input did not supply the function representation. + * wrapper is empty when the input did not supply the function representation. ***************************************************************************** */ - Function& getFunctionValueAlternative(const std::string& valueName) const; + const FunctionVariant& getFunctionValueAlternative(const std::string& valueName) const; /*! ***************************************************************************** @@ -1329,34 +1323,19 @@ class Container : public Verifiable /*! ***************************************************************************** - * \brief Add an internally named function alternative for a public value. + * \brief Add a function alternative for a public value. * * \param [in] valueName Public name of the concrete value or collection * \param [in] ret_type The return type of the function * \param [in] arg_types The argument types of the function - * \param [in] description Description of the function * \param [in] resolvedValuePath Concrete input path when expanding a struct * collection; empty when it should be derived from this Container - * - * \return Reference to the created Function or aggregate Function - ***************************************************************************** - */ - Verifiable& addFunctionValueAlternative( - const std::string& valueName, - FunctionTag ret_type, - const std::vector& arg_types, - const std::string& description, - const std::string& resolvedValuePath); - - /*! - ***************************************************************************** - * \brief Generate an unused internal name for a function value alternative. - * - * \return An internal name that does not collide with this Container's - * Sidre groups ***************************************************************************** */ - std::string nextFunctionValueAlternativeName(); + void addFunctionValueAlternative(const std::string& valueName, + FunctionTag ret_type, + const std::vector& arg_types, + const std::string& resolvedValuePath); /*! ***************************************************************************** @@ -1570,6 +1549,20 @@ class Container : public Verifiable template bool transformFromNestedElements(OutputIt output, const std::string& name, Func&& func) const; + /*! + ***************************************************************************** + * \brief Applies a provided function to nested elements of the calling table. + * + * \param [in] name The name to append to each nested element's input path + * \param [in] func Function accepting a Container and its resolved input path + * + * \return Whether the calling container had any nested elements (or was a + * struct collection) + ***************************************************************************** + */ + template + bool forEachNestedElement(const std::string& name, Func&& func) const; + std::string m_name; Reader& m_reader; // Inlet's Root Sidre Group @@ -1584,9 +1577,8 @@ class Container : public Verifiable std::unordered_map> m_fieldChildren; std::unordered_map> m_functionChildren; std::unordered_set m_functionAlternativePaths; - std::unordered_map m_functionValueAlternatives; + std::unordered_map m_functionValueAlternatives; std::unordered_multimap m_valueInputPathGroups; - std::size_t m_nextFunctionValueAlternativeId {0}; Verifier m_verifier; // Used for ownership only - need to take ownership of these so children diff --git a/src/axom/inlet/Inlet.hpp b/src/axom/inlet/Inlet.hpp index 1c1ec4ea5a..df0d713147 100644 --- a/src/axom/inlet/Inlet.hpp +++ b/src/axom/inlet/Inlet.hpp @@ -439,24 +439,15 @@ class Inlet * \param [in] valueName Public name of the concrete value or collection * \param [in] ret_type The return type of the function * \param [in] arg_types The argument types of the function - * \param [in] description Description of the function - * - * \return Reference to the created Function * * \see Container::addFunctionAsValueAlternative ***************************************************************************** */ - Verifiable& addFunctionAsValueAlternative( - const std::string& valueName, - FunctionTag ret_type, - const std::vector& arg_types, - const std::string& description = "") + void addFunctionAsValueAlternative(const std::string& valueName, + FunctionTag ret_type, + const std::vector& arg_types) { - return m_globalContainer.addFunctionAsValueAlternative( - valueName, - ret_type, - arg_types, - description); + m_globalContainer.addFunctionAsValueAlternative(valueName, ret_type, arg_types); } /*! diff --git a/src/axom/inlet/SphinxWriter.cpp b/src/axom/inlet/SphinxWriter.cpp index 57dd266783..dee52053e3 100644 --- a/src/axom/inlet/SphinxWriter.cpp +++ b/src/axom/inlet/SphinxWriter.cpp @@ -112,13 +112,7 @@ void SphinxWriter::documentContainer(const Container& container) for(const auto& function_entry : container.getChildFunctions()) { - const auto* functionGroup = function_entry.second->sidreGroup(); - const bool isValueAlternative = - functionGroup->hasView(detail::FUNCTION_VALUE_ALTERNATIVE_FLAG); - if(!isValueAlternative) - { - extractFunctionMetadata(functionGroup, currContainer); - } + extractFunctionMetadata(function_entry.second->sidreGroup(), currContainer); } } diff --git a/src/axom/inlet/docs/sphinx/functions.rst b/src/axom/inlet/docs/sphinx/functions.rst index b5aa0c3d8e..6b081d0a9f 100644 --- a/src/axom/inlet/docs/sphinx/functions.rst +++ b/src/axom/inlet/docs/sphinx/functions.rst @@ -68,8 +68,7 @@ Functions as value alternatives Some schemas accept either a concrete value or a function that computes that value. Use ``addFunctionAsValueAlternative`` to declare this relationship explicitly. -Inlet owns the callback's internal storage name and associates it with the concrete -field's public name: +The callback and concrete form share one public input name: .. code-block:: C++ @@ -83,7 +82,7 @@ With this schema, ``scale = {2.0, 3.0, 4.0}`` populates ``scale``, while ``scale = function() return {2.0, 3.0, 4.0} end`` supplies the function alternative associated with ``scale``. Use ``containsFunctionValueAlternative("scale")`` and ``getFunctionValueAlternative("scale")`` on the containing ``Container`` to -query and retrieve it without depending on an internal schema name. +query and retrieve it. Inside a struct collection, Inlet resolves the public value name against each concrete element. For example, this Lua input supplies ``bar`` directly for one ``foo`` element @@ -129,21 +128,14 @@ function was supplied. A value with an unrelated type matches neither representa fails verification. The shared public name is recognized by strict containers and is not reported as unexpected. -The returned function and the concrete field remain independently verifiable schema entries. -Consequently, ``required()`` and registered verifiers apply to the entry on -which they are configured. The narrow value-alternative API does not currently provide -a group-level annotation meaning "either representation is required." - -In Sidre, the callback entry retains the same signature metadata as an ordinary Inlet function -and is tagged as an internal value alternative. The live Lua callable remains in memory. -Inlet restart reconstructs containers and fields, not functions, so the tag prevents -the internal callback group from being mistaken for a field. -A persisted concrete value is reconstructed normally, while a Lua callback is not. - -Generated Sphinx and JSON Schema documentation describe the concrete value form. -The internal callback entry is omitted: its storage name is not an input-file path, -and JSON inputs cannot provide a Lua function. Document the callback form separately when exposing -it as part of an application's Lua interface. +The callback alternative is not an independent schema entry: it does not appear in +``getChildFunctions()``, cannot be marked ``required()``, and is not persisted in Sidre. +The concrete field's validation rules do not automatically apply to the callback's result. +Applications should validate any constraints shared by both forms after resolving the +selected representation. + +Generated Sphinx and JSON Schema documentation describe only the concrete value form. +Document the callback form separately when exposing it as part of an application's Lua interface. Accessing --------- diff --git a/src/axom/inlet/inlet_utils.hpp b/src/axom/inlet/inlet_utils.hpp index c59be1b664..7c0d93bc22 100644 --- a/src/axom/inlet/inlet_utils.hpp +++ b/src/axom/inlet/inlet_utils.hpp @@ -136,7 +136,6 @@ const std::string COLLECTION_GROUP_NAME = "_inlet_collection"; const std::string COLLECTION_INDICES_NAME = "_inlet_collection_indices"; const std::string STRUCT_COLLECTION_FLAG = "_inlet_struct_collection"; const std::string VARIANT_STRUCT_COLLECTION_FLAG = "_inlet_variant_struct_collection"; -const std::string FUNCTION_VALUE_ALTERNATIVE_FLAG = "_inlet_function_value_alternative"; const std::string REQUIRED_FLAG = "required"; const std::string STRICT_FLAG = "strict"; } // namespace detail diff --git a/src/axom/inlet/tests/inlet_function.cpp b/src/axom/inlet/tests/inlet_function.cpp index 4bba9df302..84e836e0cc 100644 --- a/src/axom/inlet/tests/inlet_function.cpp +++ b/src/axom/inlet/tests/inlet_function.cpp @@ -5,19 +5,13 @@ // SPDX-License-Identifier: (BSD-3-Clause) #include "axom/slic.hpp" -#include "axom/sidre.hpp" #include "axom/inlet/LuaReader.hpp" #include "axom/inlet/Inlet.hpp" -#include "axom/inlet/JSONSchemaWriter.hpp" -#include "axom/inlet/SphinxWriter.hpp" #include "gtest/gtest.h" #include -#include -#include -#include #include #include #include @@ -28,9 +22,7 @@ using axom::inlet::FunctionTag; using axom::inlet::FunctionType; using axom::inlet::Inlet; using axom::inlet::InletType; -using axom::inlet::JSONSchemaWriter; using axom::inlet::LuaReader; -using axom::inlet::SphinxWriter; using axom::inlet::VerificationError; #include "axom/sol.hpp" @@ -320,22 +312,6 @@ TEST(inlet_function, function_value_alternative_uses_public_value_name) } } -TEST(inlet_function, required_function_value_alternative_missing) -{ - auto inlet = createBasicInlet(""); - inlet.addDouble("foo"); - inlet - .addFunctionAsValueAlternative("foo", FunctionTag::Double, {}) - .required(); - - std::vector errors; - EXPECT_FALSE(inlet.verify(&errors)); - EXPECT_FALSE(errors.empty()); - EXPECT_FALSE(inlet.contains("foo")); - EXPECT_FALSE(inlet.getGlobalContainer().containsFunctionValueAlternative("foo")); - EXPECT_FALSE(inlet.getGlobalContainer().exists()); -} - TEST(inlet_function, function_value_alternative_rejects_unrelated_wrong_type) { const auto addSchema = [](Inlet& inlet, bool functionFirst) { @@ -390,67 +366,19 @@ TEST(inlet_function, function_value_alternative_rejects_duplicate_callback_alter axom::slic::ScopedAbortToThrow abortGuard; EXPECT_THROW(inlet.addFunctionAsValueAlternative("scale", FunctionTag::Double, {}), axom::slic::SlicAbortException); - EXPECT_EQ(inlet.getGlobalContainer().getChildFunctions().size(), 1u); + EXPECT_TRUE(inlet.getGlobalContainer().getChildFunctions().empty()); } -TEST(inlet_function, function_value_alternative_sidre_and_generated_documentation) +TEST(inlet_function, function_value_alternative_rejects_invalid_declaration) { - const std::string sphinxFile = "inlet_function_value_alternative.rst"; - const std::string jsonFile = "inlet_function_value_alternative.json"; - auto inlet = createBasicInlet("scale = 2.0"); - inlet.addFunctionAsValueAlternative("scale", FunctionTag::Double, {}); - inlet.addDouble("scale", "Scale factor"); - - // Sidre marks the internal callback schema group without storing the callable. - const auto& childFunctions = inlet.getGlobalContainer().getChildFunctions(); - ASSERT_EQ(childFunctions.size(), 1u); - const auto* callbackGroup = childFunctions.begin()->second->sidreGroup(); - ASSERT_TRUE(callbackGroup->hasView(axom::inlet::detail::FUNCTION_VALUE_ALTERNATIVE_FLAG)); - const std::string internalName = callbackGroup->getName(); - const std::int8_t alternativeFlag = - callbackGroup->getView(axom::inlet::detail::FUNCTION_VALUE_ALTERNATIVE_FLAG)->getScalar(); - EXPECT_EQ(alternativeFlag, 1); - - inlet.write(SphinxWriter(sphinxFile)); - inlet.write(JSONSchemaWriter(jsonFile)); - - const auto readFile = [](const std::string& path) { - std::ifstream stream(path); - return std::string(std::istreambuf_iterator(stream), - std::istreambuf_iterator()); - }; - const std::string sphinx = readFile(sphinxFile); - const std::string json = readFile(jsonFile); - std::remove(sphinxFile.c_str()); - std::remove(jsonFile.c_str()); - - EXPECT_NE(sphinx.find("scale"), std::string::npos); - EXPECT_EQ(sphinx.find(internalName), std::string::npos); - EXPECT_NE(json.find("scale"), std::string::npos); - EXPECT_EQ(json.find(internalName), std::string::npos); -} - -TEST(inlet_function, function_value_alternative_restart_does_not_create_internal_field) -{ - axom::sidre::DataStore datastore; - { - auto reader = std::make_unique(); - reader->parseString("scale = 2.0"); - Inlet inlet(std::move(reader), datastore.getRoot()); - inlet.addFunctionAsValueAlternative("scale", FunctionTag::Double, {}); - inlet.addDouble("scale"); - } + auto inlet = createBasicInlet(""); + axom::slic::ScopedAbortToThrow abortGuard; - { - auto reader = std::make_unique(); - Inlet restart(std::move(reader), datastore.getRoot(), true, true); - - EXPECT_TRUE(restart.contains("scale")); - EXPECT_DOUBLE_EQ(restart.get("scale"), 2.0); - EXPECT_EQ(restart.getGlobalContainer().getChildFields().size(), 1u); - EXPECT_TRUE(restart.getGlobalContainer().getChildFunctions().empty()); - EXPECT_FALSE(restart.getGlobalContainer().containsFunctionValueAlternative("scale")); - } + EXPECT_THROW(inlet.addFunctionAsValueAlternative("", FunctionTag::Double, {}), + axom::slic::SlicAbortException); + EXPECT_THROW(inlet.addFunctionAsValueAlternative("value", FunctionTag::Void, {}), + axom::slic::SlicAbortException); + EXPECT_TRUE(inlet.getGlobalContainer().getFunctionValueAlternativeNames().empty()); } TEST(inlet_function, returned_function_keeps_lua_state_alive) From 2794a91792207e02dde7ddba05989e8c4d11a0fc Mon Sep 17 00:00:00 2001 From: Kenneth Weiss Date: Sun, 9 Aug 2026 22:33:19 -0700 Subject: [PATCH 34/52] Inlet; Adds a single canonical path for handling functions as alternatives to values --- src/axom/inlet/Container.cpp | 396 ++++++++++++++++------- src/axom/inlet/Container.hpp | 74 ++--- src/axom/inlet/docs/sphinx/functions.rst | 11 +- src/axom/inlet/tests/inlet_function.cpp | 98 +++++- 4 files changed, 419 insertions(+), 160 deletions(-) diff --git a/src/axom/inlet/Container.cpp b/src/axom/inlet/Container.cpp index 547c3f6285..1478e6898b 100644 --- a/src/axom/inlet/Container.cpp +++ b/src/axom/inlet/Container.cpp @@ -15,6 +15,234 @@ namespace axom { namespace inlet { +namespace detail +{ +/*! + ***************************************************************************** + * \class FunctionValueAlternativeRegistry + * + * \brief Stores function alternatives for values in an Inlet hierarchy. + * + * Every Container in an Inlet hierarchy shares one registry. Entries are keyed + * by canonical input paths and own the declared function alternatives, + * including empty alternatives used to detect duplicate declarations. + * The registry also tracks concrete value groups so that declaring a function + * alternative before or after the corresponding value produces the same result. + * Registry state is runtime-only and is not persisted in Sidre. + ***************************************************************************** + */ +class FunctionValueAlternativeRegistry +{ +public: + /*! + *************************************************************************** + * \brief Adds a function alternative for an input value. + * + * Empty alternatives are retained so duplicate declarations can be detected. + * A valid alternative updates any concrete value group already registered at + * the same path. + * + * \param [in] inputPath The path of the input value. + * \param [in] function The function alternative to store. + *************************************************************************** + */ + void add(std::string inputPath, FunctionVariant&& function) + { + inputPath = canonicalPath(inputPath); + const auto inserted = m_alternatives.emplace(inputPath, std::move(function)); + if(!inserted.second) + { + SLIC_ERROR(fmt::format( + "[Inlet] Input path '{0}' already has a function value alternative", + inputPath)); + return; + } + + if(inserted.first->second) + { + markConcreteValueAbsent(inputPath); + } + } + + /*! + *************************************************************************** + * \brief Registers the Sidre group for a concrete input value. + * + * The group is retained so its retrieval status can be adjusted if a function + * alternative is declared later. + * + * \param [in] inputPath The path of the concrete input value. + * \param [in] group The Sidre group that stores the value's metadata. + *************************************************************************** + */ + void registerConcreteValue(const std::string& inputPath, axom::sidre::Group* group) + { + m_concreteGroups.emplace(canonicalPath(inputPath), group); + } + + /*! + *************************************************************************** + * \brief Adjusts a concrete value's retrieval result for a function alternative. + * + * \param [in] inputPath The path of the input value. + * \param [in] result The result reported for the concrete value. + * + * \return \a result, except that WrongType becomes NotFound when a valid + * function alternative exists at the same path. + *************************************************************************** + */ + ReaderResult adjust(const std::string& inputPath, ReaderResult result) const + { + return result == ReaderResult::WrongType && contains(inputPath) + ? ReaderResult::NotFound + : result; + } + + /*! + *************************************************************************** + * \brief Tests whether a supplied function alternative exists at a path. + * + * \param [in] inputPath The path of the input value. + * + * \return True when the path has a valid callable, rather than merely an empty + * declaration; otherwise false. + *************************************************************************** + */ + bool contains(const std::string& inputPath) const + { + const auto iter = m_alternatives.find(canonicalPath(inputPath)); + return iter != m_alternatives.end() && static_cast(iter->second); + } + + /*! + *************************************************************************** + * \brief Gets the function alternative declared at a path. + * + * \param [in] inputPath The path of the input value. + * + * \return The stored function wrapper, which may be empty when the alternative + * was declared without a callback. + * + * An error is reported if no alternative was declared at \a inputPath. + *************************************************************************** + */ + const FunctionVariant& get(const std::string& inputPath) const + { + const std::string path = canonicalPath(inputPath); + const auto iter = m_alternatives.find(path); + SLIC_ERROR_IF(iter == m_alternatives.end(), + fmt::format("[Inlet] Function value alternative not found for value: {0}", path)); + return iter->second; + } + + /*! + *************************************************************************** + * \brief Finds function alternatives that are direct children of a container. + * + * \param [in] containerPath The path of the parent container. + * + * \return The names of direct children that have valid function alternatives. + *************************************************************************** + */ + std::vector childNames(const std::string& containerPath) const + { + const std::string parentPath = canonicalPath(containerPath); + std::vector result; + for(const auto& entry : m_alternatives) + { + const Path path(entry.first); + if(static_cast(entry.second) && path.dirName() == parentPath) + { + result.push_back(path.baseName()); + } + } + return result; + } + + /*! + *************************************************************************** + * \brief Tests for valid function alternatives below a container. + * + * \param [in] containerPath The path of the container. + * + * \return True when a descendant has a valid function alternative, else false. + * An alternative at the container's own path is excluded so that a + * whole-collection callback does not make the concrete collection non-empty. + *************************************************************************** + */ + bool containsBelow(const std::string& containerPath) const + { + const std::string parentPath = canonicalPath(containerPath); + const std::string prefix = parentPath.empty() ? "" : parentPath + "/"; + return std::any_of( + m_alternatives.begin(), + m_alternatives.end(), + [&parentPath, &prefix](const decltype(m_alternatives)::value_type& entry) { + if(!static_cast(entry.second)) + { + return false; + } + return parentPath.empty() || entry.first.compare(0, prefix.size(), prefix) == 0; + }); + } + + /*! + ******************************************************************************** + * \brief Converts an input path to the registry's canonical representation. + * + * \param [in] inputPath The path to normalize. + * + * \return The normalized path with internal collection-group components removed. + ******************************************************************************** + */ + static std::string canonicalPath(const std::string& inputPath) + { + std::string result; + const Path path(inputPath); + for(const auto& part : path.parts()) + { + if(part != COLLECTION_GROUP_NAME) + { + result = utilities::string::appendPrefix(result, part); + } + } + return result; + } + +private: + /*! + *************************************************************************** + * \brief Marks concrete values at a path absent when they have the wrong type. + * + * Only WrongType retrieval statuses are changed; all other statuses are unchanged. + * + * \param [in] inputPath A canonical input path. + *************************************************************************** + */ + void markConcreteValueAbsent(const std::string& inputPath) + { + const auto groups = m_concreteGroups.equal_range(inputPath); + for(auto iter = groups.first; iter != groups.second; ++iter) + { + auto* group = iter->second; + if(group->hasView("retrieval_status")) + { + auto* statusView = group->getView("retrieval_status"); + const auto status = + static_cast(static_cast(statusView->getData())); + if(status == ReaderResult::WrongType) + { + statusView->setScalar(static_cast(ReaderResult::NotFound)); + } + } + } + } + + std::unordered_map m_alternatives; + std::unordered_multimap m_concreteGroups; +}; +} // namespace detail + Container::Container(const std::string& name, const std::string& description, Reader& reader, @@ -22,11 +250,31 @@ Container::Container(const std::string& name, std::vector& unexpectedNames, bool docEnabled, bool reconstruct) + : Container(name, + description, + reader, + sidreRootGroup, + unexpectedNames, + std::make_shared(), + docEnabled, + reconstruct) +{ } + +Container::Container( + const std::string& name, + const std::string& description, + Reader& reader, + axom::sidre::Group* sidreRootGroup, + std::vector& unexpectedNames, + std::shared_ptr functionAlternatives, + bool docEnabled, + bool reconstruct) : m_name(name) , m_reader(reader) , m_sidreRootGroup(sidreRootGroup) , m_unexpectedNames(unexpectedNames) , m_docEnabled(docEnabled) + , m_functionAlternatives(std::move(functionAlternatives)) { SLIC_ASSERT_MSG(m_sidreRootGroup != nullptr, "Inlet's Sidre Datastore class not set"); @@ -59,13 +307,7 @@ Container::Container(const std::string& name, if(inletType == "Container") { m_containerChildren.emplace(childName, - std::make_unique(childName, - "", - m_reader, - m_sidreRootGroup, - m_unexpectedNames, - m_docEnabled, - true)); + createChildContainer(childName, "", true)); } else if(inletType == "Field") { @@ -91,6 +333,21 @@ Container::Container(const std::string& name, } } +std::unique_ptr Container::createChildContainer(const std::string& name, + const std::string& description, + bool reconstruct) +{ + return std::unique_ptr { + new Container(name, + description, + m_reader, + m_sidreRootGroup, + m_unexpectedNames, + m_functionAlternatives, + m_docEnabled, + reconstruct)}; +} + template void Container::forEachCollectionElement(Func&& func) const { @@ -140,16 +397,9 @@ Container& Container::addContainer(const std::string& name, const std::string& d const std::string currDescr = (pathPart == name) ? description : ""; if(!currContainer->hasChild(pathPart)) { - // Will the copy always be elided here with a move ctor - // or do we need std::piecewise_construct/std::forward_as_tuple? - const auto& emplaceResult = - currContainer->m_containerChildren.emplace(currContainerName, - std::make_unique(currContainerName, - currDescr, - m_reader, - m_sidreRootGroup, - m_unexpectedNames, - m_docEnabled)); + const auto& emplaceResult = currContainer->m_containerChildren.emplace( + currContainerName, + currContainer->createChildContainer(currContainerName, currDescr)); // emplace_result is a pair whose first element is an iterator to the inserted element currContainer = emplaceResult.first->second.get(); } @@ -337,43 +587,6 @@ Function& Container::storeFunction(axom::sidre::Group* sidreGroup, return *(emplace_result.first->second); } -ReaderResult Container::adjustForFunctionAlternative(const std::string& inputPath, - ReaderResult result) const -{ - if(result == ReaderResult::WrongType && - m_functionAlternativePaths.find(inputPath) != m_functionAlternativePaths.end()) - { - return ReaderResult::NotFound; - } - return result; -} - -void Container::registerValueInputPath(const std::string& inputPath, axom::sidre::Group* group) -{ - m_valueInputPathGroups.emplace(inputPath, group); -} - -void Container::registerFunctionAlternativePath(const std::string& inputPath) -{ - m_functionAlternativePaths.insert(inputPath); - - const auto groups = m_valueInputPathGroups.equal_range(inputPath); - for(auto iter = groups.first; iter != groups.second; ++iter) - { - auto* group = iter->second; - if(group->hasView("retrieval_status")) - { - auto* statusView = group->getView("retrieval_status"); - const auto status = - static_cast(static_cast(statusView->getData())); - if(status == ReaderResult::WrongType) - { - statusView->setScalar(static_cast(ReaderResult::NotFound)); - } - } - } -} - template VerifiableScalar& Container::addPrimitive(const std::string& name, const std::string& description, @@ -421,7 +634,7 @@ VerifiableScalar& Container::addPrimitive(const std::string& name, lookupPath = utilities::string::removeAllInstances(lookupPath, detail::COLLECTION_GROUP_NAME + "/"); detail::updateUnexpectedNames(lookupPath, m_unexpectedNames); - registerValueInputPath(lookupPath, sidreGroup); + m_functionAlternatives->registerConcreteValue(lookupPath, sidreGroup); auto typeId = addPrimitiveHelper(sidreGroup, lookupPath, forArray, val); return addField(sidreGroup, typeId, fullName, name); } @@ -433,7 +646,7 @@ axom::sidre::DataTypeId Container::addPrimitiveHelper(axom::sidre::Group* bool forArray, bool val) { - const auto result = adjustForFunctionAlternative(lookupPath, m_reader.getBool(lookupPath, val)); + const auto result = m_functionAlternatives->adjust(lookupPath, m_reader.getBool(lookupPath, val)); if(forArray || result == ReaderResult::Success) { sidreGroup->createViewScalar("value", val ? std::int8_t(1) : std::int8_t(0)); @@ -451,7 +664,7 @@ axom::sidre::DataTypeId Container::addPrimitiveHelper(axom::sidre::Group* s bool forArray, int val) { - const auto result = adjustForFunctionAlternative(lookupPath, m_reader.getInt(lookupPath, val)); + const auto result = m_functionAlternatives->adjust(lookupPath, m_reader.getInt(lookupPath, val)); if(forArray || result == ReaderResult::Success) { sidreGroup->createViewScalar("value", val); @@ -469,7 +682,7 @@ axom::sidre::DataTypeId Container::addPrimitiveHelper(axom::sidre::Group bool forArray, double val) { - const auto result = adjustForFunctionAlternative(lookupPath, m_reader.getDouble(lookupPath, val)); + const auto result = m_functionAlternatives->adjust(lookupPath, m_reader.getDouble(lookupPath, val)); if(forArray || result == ReaderResult::Success) { sidreGroup->createViewScalar("value", val); @@ -487,7 +700,7 @@ axom::sidre::DataTypeId Container::addPrimitiveHelper(axom::sidre:: bool forArray, std::string val) { - const auto result = adjustForFunctionAlternative(lookupPath, m_reader.getString(lookupPath, val)); + const auto result = m_functionAlternatives->adjust(lookupPath, m_reader.getString(lookupPath, val)); if(forArray || result == ReaderResult::Success) { sidreGroup->createViewString("value", val); @@ -899,9 +1112,9 @@ Verifiable& Container::addPrimitiveArray(const std::string& name, lookupPath = utilities::string::removeAllInstances(lookupPath, detail::COLLECTION_GROUP_NAME + "/"); detail::updateUnexpectedNames(lookupPath, m_unexpectedNames); - registerValueInputPath(lookupPath, container.sidreGroup()); + m_functionAlternatives->registerConcreteValue(lookupPath, container.sidreGroup()); std::vector indices; - if(m_functionAlternativePaths.find(lookupPath) != m_functionAlternativePaths.end()) + if(m_functionAlternatives->contains(lookupPath)) { markRetrievalStatus(*container.sidreGroup(), ReaderResult::NotFound); } @@ -1022,29 +1235,14 @@ void Container::addFunctionValueAlternative( return; } - const auto existingAlternative = m_functionValueAlternatives.find(valueName); - if(existingAlternative != m_functionValueAlternatives.end()) - { - SLIC_ERROR(fmt::format("[Inlet] Value '{0}' already has a function alternative " - "in container '{1}'", - valueName, - m_name)); - return; - } - std::string lookupPath = resolvedValuePath.empty() ? utilities::string::appendPrefix(m_name, valueName) : resolvedValuePath; - lookupPath = - utilities::string::removeAllInstances(lookupPath, detail::COLLECTION_GROUP_NAME + "/"); + lookupPath = detail::FunctionValueAlternativeRegistry::canonicalPath(lookupPath); detail::updateUnexpectedNames(lookupPath, m_unexpectedNames); auto func = m_reader.getFunction(lookupPath, ret_type, arg_types); - if(func) - { - registerFunctionAlternativePath(lookupPath); - } - func.setName(std::move(lookupPath)); - m_functionValueAlternatives.emplace(valueName, std::move(func)); + func.setName(std::string {lookupPath}); + m_functionAlternatives->add(std::move(lookupPath), std::move(func)); } Proxy Container::operator[](const std::string& name) const @@ -1377,12 +1575,7 @@ bool Container::exists() const return static_cast(*entry.second); }); - const bool has_function_alternatives = - std::any_of(m_functionValueAlternatives.begin(), - m_functionValueAlternatives.end(), - [](const decltype(m_functionValueAlternatives)::value_type& entry) { - return static_cast(entry.second); - }); + const bool has_function_alternatives = m_functionAlternatives->containsBelow(m_name); return has_containers || has_fields || has_functions || has_function_alternatives; } @@ -1409,18 +1602,18 @@ bool Container::isUserProvided() const return static_cast(*entry.second); }); - const bool has_function_alternatives = - std::any_of(m_functionValueAlternatives.begin(), - m_functionValueAlternatives.end(), - [](const decltype(m_functionValueAlternatives)::value_type& entry) { - return static_cast(entry.second); - }); + const bool has_function_alternatives = m_functionAlternatives->containsBelow(m_name); return has_containers || has_fields || has_functions || has_function_alternatives; } bool Container::isUserProvided(const std::string& name) const { + if(m_functionAlternatives->contains(utilities::string::appendPrefix(m_name, name))) + { + return true; + } + if(auto container = getChildInternal(name)) { // Check if the container itself was provided by the user @@ -1456,32 +1649,17 @@ const std::unordered_map>& Container::get bool Container::containsFunctionValueAlternative(const std::string& valueName) const { - const auto iter = m_functionValueAlternatives.find(valueName); - return iter != m_functionValueAlternatives.end() && - static_cast(iter->second); + return m_functionAlternatives->contains(utilities::string::appendPrefix(m_name, valueName)); } const FunctionVariant& Container::getFunctionValueAlternative(const std::string& valueName) const { - const auto iter = m_functionValueAlternatives.find(valueName); - SLIC_ERROR_IF( - iter == m_functionValueAlternatives.end(), - axom::fmt::format("[Inlet] Function value alternative not found for value: {0}", valueName)); - - return iter->second; + return m_functionAlternatives->get(utilities::string::appendPrefix(m_name, valueName)); } std::vector Container::getFunctionValueAlternativeNames() const { - std::vector result; - for(const auto& entry : m_functionValueAlternatives) - { - if(static_cast(entry.second)) - { - result.push_back(entry.first); - } - } - return result; + return m_functionAlternatives->childNames(m_name); } } // namespace inlet diff --git a/src/axom/inlet/Container.hpp b/src/axom/inlet/Container.hpp index c4468882e5..541553d619 100644 --- a/src/axom/inlet/Container.hpp +++ b/src/axom/inlet/Container.hpp @@ -21,7 +21,6 @@ #include #include #include -#include #include #include #include @@ -70,6 +69,8 @@ class VariantStructCollection; namespace detail { +class FunctionValueAlternativeRegistry; + struct VariantStructFactoryBase { virtual ~VariantStructFactoryBase() = default; @@ -758,7 +759,8 @@ class Container : public Verifiable * treated as absent rather than as having the wrong type. The function and * concrete value may be added in either order. * - * \param [in] valueName Public name of the concrete value or collection + * \param [in] valueName Path of the concrete value or collection, + * relative to this Container * \param [in] ret_type The return type of the function * \param [in] arg_types The argument types of the function ***************************************************************************** @@ -1053,7 +1055,7 @@ class Container : public Verifiable * \brief Return whether a function value alternative was supplied for the * given public value name. * - * \param [in] valueName Public name of the concrete value or collection + * \param [in] valueName Value path relative to this Container * * \return True when the input supplied a function for \a valueName ***************************************************************************** @@ -1065,7 +1067,7 @@ class Container : public Verifiable * \brief Retrieve the function value alternative associated with a public * value name. * - * \param [in] valueName Public name of the concrete value or collection + * \param [in] valueName Value path relative to this Container * * \return The function alternative declared for \a valueName. The returned * wrapper is empty when the input did not supply the function representation. @@ -1077,7 +1079,7 @@ class Container : public Verifiable ***************************************************************************** * \brief Return the public value names of supplied function alternatives. * - * \return Public value names whose function representation was supplied + * \return Names of direct child values whose function representation was supplied ***************************************************************************** */ std::vector getFunctionValueAlternativeNames() const; @@ -1131,6 +1133,29 @@ class Container : public Verifiable const std::string& pathOverride = ""); private: + /*! + ***************************************************************************** + * \brief Construct a child Container that shares function-alternative state. + ***************************************************************************** + */ + Container(const std::string& name, + const std::string& description, + Reader& reader, + axom::sidre::Group* sidreRootGroup, + std::vector& unexpectedNames, + std::shared_ptr functionAlternatives, + bool docEnabled, + bool reconstruct); + + /*! + ***************************************************************************** + * \brief Create a child using this Container's shared Inlet state. + ***************************************************************************** + */ + std::unique_ptr createChildContainer(const std::string& name, + const std::string& description, + bool reconstruct = false); + /*! ***************************************************************************** * \brief Add a Container to the input file schema. @@ -1337,41 +1362,6 @@ class Container : public Verifiable const std::vector& arg_types, const std::string& resolvedValuePath); - /*! - ***************************************************************************** - * \brief Adjust a Reader result when a function satisfies a declared value - * alternative at the same input path. - * - * \param [in] inputPath Path read by the concrete schema entry - * \param [in] result Result returned by the Reader - * - * \return \a result, or ReaderResult::NotFound when a function alternative - * satisfies a WrongType result - ***************************************************************************** - */ - ReaderResult adjustForFunctionAlternative(const std::string& inputPath, - ReaderResult result) const; - - /*! - ***************************************************************************** - * \brief Record the Sidre group populated from an input value path. - * - * \param [in] inputPath Path read by the concrete schema entry - * \param [in] group Sidre group holding that entry's retrieval status - ***************************************************************************** - */ - void registerValueInputPath(const std::string& inputPath, axom::sidre::Group* group); - - /*! - ***************************************************************************** - * \brief Record a successfully read function alternative and update any - * value schema entry that was added first. - * - * \param [in] inputPath Path satisfied by the function alternative - ***************************************************************************** - */ - void registerFunctionAlternativePath(const std::string& inputPath); - axom::sidre::View* baseGet(const std::string& name) const; /*! @@ -1576,9 +1566,7 @@ class Container : public Verifiable std::unordered_map> m_containerChildren; std::unordered_map> m_fieldChildren; std::unordered_map> m_functionChildren; - std::unordered_set m_functionAlternativePaths; - std::unordered_map m_functionValueAlternatives; - std::unordered_multimap m_valueInputPathGroups; + std::shared_ptr m_functionAlternatives; Verifier m_verifier; // Used for ownership only - need to take ownership of these so children diff --git a/src/axom/inlet/docs/sphinx/functions.rst b/src/axom/inlet/docs/sphinx/functions.rst index 6b081d0a9f..2e45211d68 100644 --- a/src/axom/inlet/docs/sphinx/functions.rst +++ b/src/axom/inlet/docs/sphinx/functions.rst @@ -115,10 +115,13 @@ This example evaluates a supplied callback during conversion. An application tha deferred evaluation can instead call ``get()`` on the result of ``getFunctionValueAlternative("bar")`` and store the returned ``std::function``. -The two schema entries may be added in either order. A function encountered at a normal -field path remains a type error unless this alternative has been declared. -The narrow API permits one callback alternative for a public value name in a given container -and declaring a second callback alternative for that name is an error. +The two schema entries may be added in either order and through any equivalent relative path. +For example, ``inlet.addDouble("group/value")`` and +``group.addFunctionAsValueAlternative("value", ...)`` describe one input path. +A function encountered at a normal field path remains a type error +unless this alternative has been declared. +The API permits at most one callback alternative for each canonical input path; +declaring it again through either Container is an error. Since one Lua object cannot simultaneously be a concrete value and a function, at most one of the two supported representations can match. diff --git a/src/axom/inlet/tests/inlet_function.cpp b/src/axom/inlet/tests/inlet_function.cpp index 84e836e0cc..a8331183a3 100644 --- a/src/axom/inlet/tests/inlet_function.cpp +++ b/src/axom/inlet/tests/inlet_function.cpp @@ -358,15 +358,105 @@ TEST(inlet_function, function_value_alternative_is_valid_in_strict_container) } } -TEST(inlet_function, function_value_alternative_rejects_duplicate_callback_alternative) +TEST(inlet_function, function_value_alternative_is_container_independent) { - auto inlet = createBasicInlet("function scale () return 2.0 end"); - inlet.addFunctionAsValueAlternative("scale", FunctionTag::Double, {}); + for(const bool functionOnRoot : {true, false}) + { + for(const bool functionFirst : {true, false}) + { + auto inlet = createBasicInlet("group = { value = function() return 2.0 end }"); + auto& group = inlet.addStruct("group"); + + const auto addFunctionAlternative = [&]() { + if(functionOnRoot) + { + inlet.addFunctionAsValueAlternative("group/value", FunctionTag::Double, {}); + } + else + { + group.addFunctionAsValueAlternative("value", FunctionTag::Double, {}); + } + }; + + const auto addConcreteValue = [&]() { + if(functionOnRoot) + { + group.addDouble("value"); + } + else + { + inlet.addDouble("group/value"); + } + }; + + if(functionFirst) + { + addFunctionAlternative(); + addConcreteValue(); + } + else + { + addConcreteValue(); + addFunctionAlternative(); + } + + EXPECT_TRUE(inlet.verify()); + EXPECT_TRUE( + inlet.getGlobalContainer().containsFunctionValueAlternative("group/value")); + EXPECT_TRUE(group.containsFunctionValueAlternative("value")); + EXPECT_FALSE(inlet.contains("group/value")); + EXPECT_TRUE(inlet.isUserProvided("group/value")); + EXPECT_TRUE(group.isUserProvided("value")); + EXPECT_TRUE(group.exists()); + EXPECT_DOUBLE_EQ(group.getFunctionValueAlternative("value").call(), 2.0); + EXPECT_EQ(group.getFunctionValueAlternativeNames(), + std::vector {"value"}); + EXPECT_TRUE(inlet.getGlobalContainer().getFunctionValueAlternativeNames().empty()); + } + } +} + +TEST(inlet_function, function_value_alternative_array_is_container_and_order_independent) +{ + for(const bool functionFirst : {true, false}) + { + auto inlet = createBasicInlet( + "group = { values = function() return {1.0, 2.0, 3.0} end }"); + auto& group = inlet.addStruct("group"); + + if(functionFirst) + { + group.addFunctionAsValueAlternative("values", FunctionTag::Vector, {}); + } + inlet.addDoubleArray("group/values"); + if(!functionFirst) + { + group.addFunctionAsValueAlternative("values", FunctionTag::Vector, {}); + } + + EXPECT_TRUE(inlet.verify()); + EXPECT_FALSE(inlet.contains("group/values")); + EXPECT_TRUE(group.containsFunctionValueAlternative("values")); + const auto result = + group.getFunctionValueAlternative("values").call(); + EXPECT_DOUBLE_EQ(result[0], 1.0); + EXPECT_DOUBLE_EQ(result[1], 2.0); + EXPECT_DOUBLE_EQ(result[2], 3.0); + } +} + +TEST(inlet_function, function_value_alternative_rejects_duplicate_across_containers) +{ + auto inlet = createBasicInlet("group = { scale = function() return 2.0 end }"); + auto& group = inlet.addStruct("group"); + inlet.addFunctionAsValueAlternative("group/scale", FunctionTag::Double, {}); axom::slic::ScopedAbortToThrow abortGuard; - EXPECT_THROW(inlet.addFunctionAsValueAlternative("scale", FunctionTag::Double, {}), + EXPECT_THROW(group.addFunctionAsValueAlternative("scale", FunctionTag::Double, {}), axom::slic::SlicAbortException); EXPECT_TRUE(inlet.getGlobalContainer().getChildFunctions().empty()); + EXPECT_TRUE(group.getChildFunctions().empty()); + EXPECT_DOUBLE_EQ(group.getFunctionValueAlternative("scale").call(), 2.0); } TEST(inlet_function, function_value_alternative_rejects_invalid_declaration) From efa90db06810f68260bf06709769e19cdacab44b Mon Sep 17 00:00:00 2001 From: Kenneth Weiss Date: Sun, 9 Aug 2026 23:41:03 -0700 Subject: [PATCH 35/52] Inlet: Cleanup -- removes unnecessary traversal function --- src/axom/inlet/LuaReader.cpp | 112 +++++++++-------------------------- src/axom/inlet/LuaReader.hpp | 21 ------- 2 files changed, 27 insertions(+), 106 deletions(-) diff --git a/src/axom/inlet/LuaReader.cpp b/src/axom/inlet/LuaReader.cpp index a80f27e64f..787c540a0e 100644 --- a/src/axom/inlet/LuaReader.cpp +++ b/src/axom/inlet/LuaReader.cpp @@ -315,57 +315,6 @@ ReaderResult LuaReader::getVariantMap(const std::string& id, return getVariantMapInternal(id, values); } -template -bool LuaReader::traverseToTable(Iter begin, Iter end, axom::sol::table& table) -{ - // Nothing to traverse - if(begin == end) - { - return true; - } - - axom::sol::object object = (*m_lua)[*begin]; - if(!object.valid() || object.get_type() != axom::sol::type::table) - { - return false; - } - - // Use the first one to index into the global lua state - table = object.as(); - ++begin; - - // Then use the remaining keys to walk down to the requested table - for(auto curr = begin; curr != end; ++curr) - { - auto key = *curr; - bool is_int = conduit::utils::string_is_integer(key); - axom::sol::object child; - if(is_int) - { - const int key_as_int = conduit::utils::string_to_value(key); - if(table[key_as_int].valid()) - { - child = table[key_as_int]; - } - } - if(!child.valid() && table[key].valid()) - { - child = table[key]; - } - if(!child.valid()) - { - return false; - } - - if(child.get_type() != axom::sol::type::table) - { - return false; - } - table = child.as(); - } - return true; -} - axom::sol::object LuaReader::getObject(const std::string& id) { const auto tokens = axom::utilities::string::split(id, SCOPE_DELIMITER); @@ -374,28 +323,28 @@ axom::sol::object LuaReader::getObject(const std::string& id) return {}; } - if(tokens.size() == 1) - { - return (*m_lua)[tokens.front()]; - } - - axom::sol::table parent; - if(!traverseToTable(tokens.begin(), tokens.end() - 1, parent)) + axom::sol::object object = (*m_lua)[tokens.front()]; + for(std::size_t i = 1; i < tokens.size(); ++i) { - return {}; - } + if(!object.valid() || object.get_type() != axom::sol::type::table) + { + return {}; + } - const auto& key = tokens.back(); - const bool is_int = conduit::utils::string_is_integer(key); - if(is_int) - { - const int key_as_int = conduit::utils::string_to_value(key); - if(parent[key_as_int].valid()) + const auto table = object.as(); + const auto& key = tokens[i]; + axom::sol::object child; + if(conduit::utils::string_is_integer(key)) { - return parent[key_as_int]; + child = table[conduit::utils::string_to_value(key)]; + } + if(!child.valid()) + { + child = table[key]; } + object = std::move(child); } - return parent[key]; + return object; } ReaderResult LuaReader::getIndices(const std::string& id, std::vector& indices) @@ -627,21 +576,21 @@ typename std::enable_if::type bindArgType( /*! ***************************************************************************** - * \brief Performs a type-checked access to a Lua table + * \brief Performs a type-checked access to a Lua object * - * \param [in] proxy The axom::sol::proxy object to retrieve from + * \param [in] object The Lua object to retrieve from * \param [out] val The value to write to, if it is of the correct type * * \return ReaderResult::Success if the object was of the correct type, * ReaderResult::WrongType otherwise ***************************************************************************** */ -template -ReaderResult checkedGet(const Proxy& proxy, Value& val) +template +ReaderResult checkedGet(const axom::sol::object& object, Value& val) { - if(proxy.template is()) + if(object.template is()) { - val = proxy.template as(); + val = object.template as(); return ReaderResult::Success; } return ReaderResult::WrongType; @@ -656,26 +605,19 @@ FunctionVariant LuaReader::getFunction(const std::string& id, auto lua_func = getFunctionInternal(id); if(lua_func) { - FunctionVariant function; switch(ret_type) { case FunctionTag::Vector: - function = - detail::bindArgType<0u, FunctionType::Vector>(std::move(lua_func), arg_types, m_lua); - break; + return detail::bindArgType<0u, FunctionType::Vector>(std::move(lua_func), arg_types, m_lua); case FunctionTag::Double: - function = detail::bindArgType<0u, double>(std::move(lua_func), arg_types, m_lua); - break; + return detail::bindArgType<0u, double>(std::move(lua_func), arg_types, m_lua); case FunctionTag::Void: - function = detail::bindArgType<0u, void>(std::move(lua_func), arg_types, m_lua); - break; + return detail::bindArgType<0u, void>(std::move(lua_func), arg_types, m_lua); case FunctionTag::String: - function = detail::bindArgType<0u, std::string>(std::move(lua_func), arg_types, m_lua); - break; + return detail::bindArgType<0u, std::string>(std::move(lua_func), arg_types, m_lua); default: SLIC_ERROR("[Inlet] Unexpected function return type"); } - return function; } return {}; // Return an empty function to indicate that the function was not found } diff --git a/src/axom/inlet/LuaReader.hpp b/src/axom/inlet/LuaReader.hpp index 412f69e024..b1eb0dab3f 100644 --- a/src/axom/inlet/LuaReader.hpp +++ b/src/axom/inlet/LuaReader.hpp @@ -184,27 +184,6 @@ class LuaReader : public Reader */ axom::sol::object getObject(const std::string& id); - /*! - ***************************************************************************** - * \brief Obtains the Lua table reached by successive indexing through the - * range of keys described by a pair of iterators - * - * \note For a set of keys {key1, key2, key3, ...}, this function - * is equivalent to - * \code{.cpp} - * table = m_lua[key1][key2][key3][...]; - * \endcode - * - * \param [in] begin Iterator to the beginning of the range of keys - * \param [in] end Iterator to one-past-the-end of the range - * \param [out] t The table to traverse - * - * \return Whether the traversal was successful - ***************************************************************************** - */ - template - bool traverseToTable(Iter begin, Iter end, axom::sol::table& table); - /*! ***************************************************************************** * \brief Traverses the Lua state to retrieve a sol function object From 0183ea49a851141f36653eb4743d6d702181dd5e Mon Sep 17 00:00:00 2001 From: Kenneth Weiss Date: Sun, 9 Aug 2026 23:55:26 -0700 Subject: [PATCH 36/52] Inlet: Consolidates/streamlines new inlet tests --- src/axom/inlet/tests/inlet_Reader.cpp | 102 +----- src/axom/inlet/tests/inlet_function.cpp | 404 +++++++----------------- 2 files changed, 136 insertions(+), 370 deletions(-) diff --git a/src/axom/inlet/tests/inlet_Reader.cpp b/src/axom/inlet/tests/inlet_Reader.cpp index 55948f1cac..ea0e20e58e 100644 --- a/src/axom/inlet/tests/inlet_Reader.cpp +++ b/src/axom/inlet/tests/inlet_Reader.cpp @@ -470,115 +470,41 @@ TEST(inlet_Reader_lua, getDiscontiguousMap) EXPECT_EQ(expectedStrs, strs); } -TEST(inlet_Reader_lua, functionValueIsWrongTypeForFieldsAndMaps) -{ - axom::inlet::LuaReader reader; - reader.parseString( - "foo = function() return 1 end\n" - "bar = { baz = function() return {1, 2, 3} end }"); - - double scalar = 0.0; - EXPECT_EQ(ReaderResult::WrongType, reader.getDouble("foo", scalar)); - EXPECT_EQ(ReaderResult::WrongType, reader.getDouble("bar/baz", scalar)); - - std::unordered_map values; - EXPECT_EQ(ReaderResult::WrongType, reader.getDoubleMap("foo", values)); - EXPECT_EQ(ReaderResult::WrongType, reader.getDoubleMap("bar/baz", values)); -} - -TEST(inlet_Reader_lua, functionLookupDoesNotChangeFieldAndMapResults) -{ - axom::inlet::LuaReader reader; - reader.parseString( - "foo = function() return 1 end\n" - "bar = { baz = function() return {1, 2, 3} end }"); - - // Function lookup must not cache a coercion that changes later typed reads. - auto scalarFunction = - reader.getFunction("foo", axom::inlet::FunctionTag::Double, {}); - auto vectorFunction = - reader.getFunction("bar/baz", axom::inlet::FunctionTag::Vector, {}); - ASSERT_TRUE(scalarFunction); - ASSERT_TRUE(vectorFunction); - - double scalar = 0.0; - EXPECT_EQ(ReaderResult::WrongType, reader.getDouble("foo", scalar)); - EXPECT_EQ(ReaderResult::WrongType, reader.getDouble("bar/baz", scalar)); - - std::unordered_map values; - EXPECT_EQ(ReaderResult::WrongType, reader.getDoubleMap("foo", values)); - EXPECT_EQ(ReaderResult::WrongType, reader.getDoubleMap("bar/baz", values)); -} - -TEST(inlet_Reader_lua, variantMapsAndIndicesUseConsistentObjectLookup) +TEST(inlet_Reader_lua, objectLookupReportsConsistentReaderResults) { axom::inlet::LuaReader reader; reader.parseString(R"( callback = function() return {1, 2} end - nested = { - [7] = { - values = {[2] = 42, [5] = "five"}, - dictionary = {[2] = 42, label = true} - } - } + nested = {[7] = {values = {[2] = 42, [5] = "five"}}} )"); - auto callback = - reader.getFunction("callback", axom::inlet::FunctionTag::Vector, {}); - ASSERT_TRUE(callback); + double scalar = 0.0; + EXPECT_EQ(ReaderResult::WrongType, reader.getDouble("callback", scalar)); + EXPECT_EQ(ReaderResult::NotFound, reader.getDouble("callback/value", scalar)); + + std::unordered_map typedValues {{99, 99.0}}; + EXPECT_EQ(ReaderResult::WrongType, reader.getDoubleMap("callback", typedValues)); + EXPECT_TRUE(typedValues.empty()); - // Map and index queries should agree on missing, non-table, and nested objects. std::unordered_map values { {99, axom::inlet::VariantValue {99}}}; - EXPECT_EQ(ReaderResult::WrongType, - reader.getVariantMap("callback", values)); + EXPECT_EQ(ReaderResult::WrongType, reader.getVariantMap("callback", values)); EXPECT_TRUE(values.empty()); - EXPECT_EQ(ReaderResult::NotFound, - reader.getVariantMap("missing", values)); + EXPECT_EQ(ReaderResult::NotFound, reader.getVariantMap("missing", values)); EXPECT_TRUE(values.empty()); - EXPECT_EQ(ReaderResult::Success, - reader.getVariantMap("nested/7/values", values)); + EXPECT_EQ(ReaderResult::Success, reader.getVariantMap("nested/7/values", values)); const std::unordered_map expectedValues { {2, axom::inlet::VariantValue {42}}, {5, axom::inlet::VariantValue {std::string {"five"}}}}; EXPECT_EQ(expectedValues, values); std::vector indices {99}; - EXPECT_EQ(ReaderResult::WrongType, - reader.getIndices("callback", indices)); - EXPECT_TRUE(indices.empty()); - EXPECT_EQ(ReaderResult::NotFound, - reader.getIndices("missing", indices)); + EXPECT_EQ(ReaderResult::WrongType, reader.getIndices("callback", indices)); EXPECT_TRUE(indices.empty()); - - EXPECT_EQ(ReaderResult::Success, - reader.getIndices("nested/7/values", indices)); + EXPECT_EQ(ReaderResult::Success, reader.getIndices("nested/7/values", indices)); std::sort(indices.begin(), indices.end()); EXPECT_EQ((std::vector {2, 5}), indices); - - std::unordered_map - dictionary; - EXPECT_EQ(ReaderResult::Success, - reader.getVariantMap("nested/7/dictionary", dictionary)); - EXPECT_EQ(2u, dictionary.size()); - EXPECT_EQ(axom::inlet::VariantValue {42}, - dictionary[axom::inlet::VariantKey {2}]); - EXPECT_EQ(axom::inlet::VariantValue {true}, - dictionary[axom::inlet::VariantKey {"label"}]); - - std::vector dictionaryIndices; - EXPECT_EQ(ReaderResult::Success, - reader.getIndices("nested/7/dictionary", dictionaryIndices)); - EXPECT_EQ(2u, dictionaryIndices.size()); - EXPECT_NE(dictionaryIndices.end(), - std::find(dictionaryIndices.begin(), - dictionaryIndices.end(), - axom::inlet::VariantKey {2})); - EXPECT_NE(dictionaryIndices.end(), - std::find(dictionaryIndices.begin(), - dictionaryIndices.end(), - axom::inlet::VariantKey {"label"})); } #endif diff --git a/src/axom/inlet/tests/inlet_function.cpp b/src/axom/inlet/tests/inlet_function.cpp index a8331183a3..ed9aaced7f 100644 --- a/src/axom/inlet/tests/inlet_function.cpp +++ b/src/axom/inlet/tests/inlet_function.cpp @@ -66,94 +66,68 @@ TEST(inlet_function, simple_vec3_to_vec3_raw) EXPECT_FLOAT_EQ(result[2], 6); } -TEST(inlet_function, simple_vec3_to_vec3_raw_table_return) -{ - std::string testString = "function foo (v) return {v.x + 1, v.y + 2, v.z + 3} end"; - auto inlet = createBasicInlet(testString); - - auto func = inlet.reader().getFunction("foo", FunctionTag::Vector, {FunctionTag::Vector}); - - EXPECT_TRUE(func); - auto result = func.call(FunctionType::Vector {1, 2, 3}); - EXPECT_EQ(result.dim, 3); - EXPECT_FLOAT_EQ(result[0], 2); - EXPECT_FLOAT_EQ(result[1], 4); - EXPECT_FLOAT_EQ(result[2], 6); -} - -TEST(inlet_function, vector_function_accepts_one_and_two_entry_table_returns) -{ - auto inlet = createBasicInlet( - "function one () return {4.0} end\n" - "function two () return {4.0, 5.0} end"); - - auto one = inlet.reader().getFunction("one", FunctionTag::Vector, {}); - ASSERT_TRUE(one); - const auto oneResult = one.call(); - EXPECT_EQ(oneResult.dim, 1); - EXPECT_FLOAT_EQ(oneResult[0], 4.0); - - auto two = inlet.reader().getFunction("two", FunctionTag::Vector, {}); - ASSERT_TRUE(two); - const auto twoResult = two.call(); - EXPECT_EQ(twoResult.dim, 2); - EXPECT_FLOAT_EQ(twoResult[0], 4.0); - EXPECT_FLOAT_EQ(twoResult[1], 5.0); -} - -TEST(inlet_function, vector_function_rejects_scalar_return) -{ - auto inlet = createBasicInlet("function foo () return 2.0 end"); - auto func = inlet.reader().getFunction("foo", FunctionTag::Vector, {}); - - EXPECT_TRUE(func); - EXPECT_THROW(func.call(), std::runtime_error); +TEST(inlet_function, vector_function_accepts_lua_table_returns) +{ + auto inlet = createBasicInlet(R"( + function make_vector (dim) + local result = {} + for i = 1, dim do result[i] = i end + return result + end + )"); + + auto function = + inlet.reader().getFunction("make_vector", FunctionTag::Vector, {FunctionTag::Double}); + ASSERT_TRUE(function); + for(int dim = 1; dim <= 3; ++dim) + { + const auto result = function.call(static_cast(dim)); + ASSERT_EQ(result.dim, dim); + for(int component = 0; component < result.dim; ++component) + { + EXPECT_DOUBLE_EQ(result[component], component + 1.0); + } + } } TEST(inlet_function, vector_function_rejects_malformed_table_returns) { // Lua vectors must be dense numeric sequences with a supported dimension. - const std::array inputs {{ - "function foo () return {} end", - "function foo () return {1, 2, 3, 4} end", - "function foo () return {[1] = 1, [3] = 3} end", - "function foo () return {1, 'two'} end", - "function foo () return {1, 2, label = 3} end", + const std::array results {{ + "2.0", + "{}", + "{1, 2, 3, 4}", + "{[1] = 1, [3] = 3}", + "{1, 'two'}", + "{1, 2, label = 3}", }}; - for(const auto& input : inputs) + for(const auto& result : results) { - auto inlet = createBasicInlet(input); + auto inlet = createBasicInlet("function foo () return " + result + " end"); auto func = inlet.reader().getFunction("foo", FunctionTag::Vector, {}); ASSERT_TRUE(func); EXPECT_THROW(func.call(), std::runtime_error); } } -TEST(inlet_function, scalar_and_string_functions_reject_wrong_return_types) +TEST(inlet_function, lua_callback_failures_are_catchable) { - auto inlet = createBasicInlet( - "function scalar () return 'not a number' end\n" - "function string () return {} end"); - - auto scalar = inlet.reader().getFunction("scalar", FunctionTag::Double, {}); - ASSERT_TRUE(scalar); - EXPECT_THROW(scalar.call(), std::runtime_error); + auto inlet = createBasicInlet(R"( + function runtime_error () error('callback failed') end + function wrong_type () return 'not a number' end + )"); - auto string = inlet.reader().getFunction("string", FunctionTag::String, {}); - ASSERT_TRUE(string); - EXPECT_THROW(string.call(), std::runtime_error); -} + auto wrongType = inlet.reader().getFunction("wrong_type", FunctionTag::Double, {}); + ASSERT_TRUE(wrongType); + EXPECT_THROW(wrongType.call(), std::runtime_error); -TEST(inlet_function, lua_callback_runtime_error_is_catchable) -{ - auto inlet = createBasicInlet("function foo () error('callback failed') end"); - auto func = inlet.reader().getFunction("foo", FunctionTag::Double, {}); - ASSERT_TRUE(func); + auto runtimeError = inlet.reader().getFunction("runtime_error", FunctionTag::Double, {}); + ASSERT_TRUE(runtimeError); try { - func.call(); + runtimeError.call(); FAIL() << "Expected the Lua callback to throw"; } catch(const std::runtime_error& error) @@ -222,114 +196,44 @@ TEST(inlet_function, simple_double_to_double_through_container) EXPECT_FLOAT_EQ(result, (arg * 3.4) + 9.64); } -TEST(inlet_function, function_path_override) +TEST(inlet_function, function_value_alternative_selects_supplied_representation) { - auto inlet = createBasicInlet("function public_name (x) return x + 2 end"); + auto inlet = createBasicInlet(R"( + function label () return 'computed' end + scale = 4.0 + )"); - auto& schema = inlet.addStruct("internal_group"); - schema.addFunction("internal_name", - FunctionTag::Double, - {FunctionTag::Double}, - "", - "public_name"); - - auto callback = inlet["internal_group/internal_name"].get>(); - EXPECT_DOUBLE_EQ(callback(3.0), 5.0); -} - -TEST(inlet_function, function_value_alternative_is_schema_order_independent) -{ - // Both schema entries inspect "foo"; declaration order must not select one. - const auto addSchema = [](Inlet& inlet, bool functionFirst) { - if(!functionFirst) - { - inlet.addDouble("foo"); - } - inlet.addFunctionAsValueAlternative("foo", FunctionTag::Double, {}); - if(functionFirst) - { - inlet.addDouble("foo"); - } - }; - - for(const bool functionFirst : {true, false}) - { - auto inlet = createBasicInlet("function foo () return 2.0 end"); - addSchema(inlet, functionFirst); - - EXPECT_TRUE(inlet.verify()); - EXPECT_FALSE(inlet.contains("foo")); - auto& container = inlet.getGlobalContainer(); - ASSERT_TRUE(container.containsFunctionValueAlternative("foo")); - EXPECT_DOUBLE_EQ(container.getFunctionValueAlternative("foo").call(), 2.0); - } -} - -TEST(inlet_function, function_value_alternative_preserves_concrete_value) -{ - auto inlet = createBasicInlet("foo = 4.0"); - inlet.addFunctionAsValueAlternative("foo", FunctionTag::Double, {}); - inlet.addDouble("foo"); + inlet.addString("label"); + inlet.addFunctionAsValueAlternative("label", FunctionTag::String, {}); + inlet.addFunctionAsValueAlternative("scale", FunctionTag::Double, {}); + inlet.addDouble("scale"); EXPECT_TRUE(inlet.verify()); - EXPECT_FALSE(inlet.getGlobalContainer().containsFunctionValueAlternative("foo")); - ASSERT_TRUE(inlet.contains("foo")); - EXPECT_DOUBLE_EQ(inlet["foo"].get(), 4.0); -} + auto& container = inlet.getGlobalContainer(); + EXPECT_FALSE(inlet.contains("label")); + ASSERT_TRUE(container.containsFunctionValueAlternative("label")); + EXPECT_EQ(container.getFunctionValueAlternative("label").call(), + "computed"); -TEST(inlet_function, function_value_alternative_uses_public_value_name) -{ - // set and access function in alternative - { - auto inlet = createBasicInlet("function foo () return 2.0 end"); - inlet.addFunctionAsValueAlternative("foo", FunctionTag::Double, {}); - inlet.addDouble("foo"); - - EXPECT_TRUE(inlet.verify()); - auto& container = inlet.getGlobalContainer(); - EXPECT_TRUE(container.containsFunctionValueAlternative("foo")); - - const auto names = container.getFunctionValueAlternativeNames(); - ASSERT_EQ(names.size(), 1u); - EXPECT_EQ(names[0], "foo"); - EXPECT_DOUBLE_EQ( - container.getFunctionValueAlternative("foo").call(), - 2.0); - } - - // set and access value in alternative - { - auto concreteInlet = createBasicInlet("foo = 4.0"); - concreteInlet.addFunctionAsValueAlternative("foo", FunctionTag::Double, {}); - concreteInlet.addDouble("foo"); - - EXPECT_TRUE(concreteInlet.verify()); - auto& concreteContainer = concreteInlet.getGlobalContainer(); - EXPECT_FALSE(concreteContainer.containsFunctionValueAlternative("foo")); - - EXPECT_TRUE(concreteContainer.getFunctionValueAlternativeNames().empty()); - EXPECT_DOUBLE_EQ(concreteInlet["foo"].get(), 4.0); - } + EXPECT_TRUE(inlet.contains("scale")); + EXPECT_FALSE(container.containsFunctionValueAlternative("scale")); + EXPECT_DOUBLE_EQ(inlet["scale"].get(), 4.0); } TEST(inlet_function, function_value_alternative_rejects_unrelated_wrong_type) { - const auto addSchema = [](Inlet& inlet, bool functionFirst) { - if(!functionFirst) + for(const bool functionFirst : {true, false}) + { + auto inlet = createBasicInlet("foo = 'not a number or function'"); + if(functionFirst) { - inlet.addDouble("foo"); + inlet.addFunctionAsValueAlternative("foo", FunctionTag::Double, {}); } - inlet.addFunctionAsValueAlternative("foo", FunctionTag::Double, {}); - if(functionFirst) + inlet.addDouble("foo"); + if(!functionFirst) { - inlet.addDouble("foo"); + inlet.addFunctionAsValueAlternative("foo", FunctionTag::Double, {}); } - }; - - for(const bool functionFirst : {true, false}) - { - auto inlet = createBasicInlet("foo = 'not a number or function'"); - addSchema(inlet, functionFirst); EXPECT_FALSE(inlet.verify()); EXPECT_FALSE(inlet.contains("foo")); @@ -340,79 +244,38 @@ TEST(inlet_function, function_value_alternative_rejects_unrelated_wrong_type) } } -TEST(inlet_function, function_value_alternative_is_valid_in_strict_container) -{ - for(const bool useFunction : {true, false}) - { - auto inlet = - createBasicInlet(useFunction ? "function foo () return 2.0 end" : "foo = 4.0"); - inlet.getGlobalContainer().strict(); - inlet.addFunctionAsValueAlternative("foo", FunctionTag::Double, {}); - inlet.addDouble("foo"); - - EXPECT_TRUE(inlet.verify()); - EXPECT_TRUE(inlet.unexpectedNames().empty()); - EXPECT_EQ(inlet.getGlobalContainer().containsFunctionValueAlternative("foo"), useFunction); - EXPECT_EQ(inlet.contains("foo"), !useFunction); - EXPECT_TRUE(inlet.getGlobalContainer().exists()); - } -} - TEST(inlet_function, function_value_alternative_is_container_independent) { for(const bool functionOnRoot : {true, false}) { - for(const bool functionFirst : {true, false}) - { - auto inlet = createBasicInlet("group = { value = function() return 2.0 end }"); - auto& group = inlet.addStruct("group"); - - const auto addFunctionAlternative = [&]() { - if(functionOnRoot) - { - inlet.addFunctionAsValueAlternative("group/value", FunctionTag::Double, {}); - } - else - { - group.addFunctionAsValueAlternative("value", FunctionTag::Double, {}); - } - }; - - const auto addConcreteValue = [&]() { - if(functionOnRoot) - { - group.addDouble("value"); - } - else - { - inlet.addDouble("group/value"); - } - }; - - if(functionFirst) - { - addFunctionAlternative(); - addConcreteValue(); - } - else - { - addConcreteValue(); - addFunctionAlternative(); - } + auto inlet = createBasicInlet("group = { value = function() return 2.0 end }"); + inlet.getGlobalContainer().strict(); + auto& group = inlet.addStruct("group"); - EXPECT_TRUE(inlet.verify()); - EXPECT_TRUE( - inlet.getGlobalContainer().containsFunctionValueAlternative("group/value")); - EXPECT_TRUE(group.containsFunctionValueAlternative("value")); - EXPECT_FALSE(inlet.contains("group/value")); - EXPECT_TRUE(inlet.isUserProvided("group/value")); - EXPECT_TRUE(group.isUserProvided("value")); - EXPECT_TRUE(group.exists()); - EXPECT_DOUBLE_EQ(group.getFunctionValueAlternative("value").call(), 2.0); - EXPECT_EQ(group.getFunctionValueAlternativeNames(), - std::vector {"value"}); - EXPECT_TRUE(inlet.getGlobalContainer().getFunctionValueAlternativeNames().empty()); + // The two cases cover both Container directions and both declaration orders. + if(functionOnRoot) + { + inlet.addFunctionAsValueAlternative("group/value", FunctionTag::Double, {}); + group.addDouble("value"); + } + else + { + inlet.addDouble("group/value"); + group.addFunctionAsValueAlternative("value", FunctionTag::Double, {}); } + + EXPECT_TRUE(inlet.verify()); + EXPECT_TRUE(inlet.unexpectedNames().empty()); + EXPECT_TRUE(inlet.getGlobalContainer().containsFunctionValueAlternative("group/value")); + EXPECT_TRUE(group.containsFunctionValueAlternative("value")); + EXPECT_FALSE(inlet.contains("group/value")); + EXPECT_TRUE(inlet.isUserProvided("group/value")); + EXPECT_TRUE(group.isUserProvided("value")); + EXPECT_TRUE(group.exists()); + EXPECT_DOUBLE_EQ(group.getFunctionValueAlternative("value").call(), 2.0); + EXPECT_EQ(group.getFunctionValueAlternativeNames(), + std::vector {"value"}); + EXPECT_TRUE(inlet.getGlobalContainer().getFunctionValueAlternativeNames().empty()); } } @@ -485,26 +348,6 @@ TEST(inlet_function, returned_function_keeps_lua_state_alive) EXPECT_DOUBLE_EQ(callback(4.0), 7.0); } -TEST(inlet_function, returned_functions_share_their_lua_state) -{ - std::function increment; - std::function current; - { - auto inlet = createBasicInlet( - "value = 0\n" - "function increment () value = value + 1; return value end\n" - "function current () return value end"); - inlet.addFunction("increment", FunctionTag::Double, {}); - inlet.addFunction("current", FunctionTag::Double, {}); - increment = inlet["increment"].get>(); - current = inlet["current"].get>(); - } - - EXPECT_DOUBLE_EQ(current(), 0.0); - EXPECT_DOUBLE_EQ(increment(), 1.0); - EXPECT_DOUBLE_EQ(current(), 1.0); -} - TEST(inlet_function, simple_void_to_double_through_container) { std::string testString = "function foo () return 9.64 end"; @@ -771,42 +614,39 @@ TEST(inlet_function, simple_vec3_to_vec3_array_of_struct) EXPECT_FLOAT_EQ(second_result[2], 18); } -TEST(inlet_function, function_value_alternative_in_array_of_struct) -{ - auto inlet = createBasicInlet( - "foo = { [7] = { bar = 2 }, " - " [12] = { bar = function () return 3 end } }"); - - auto& arr_container = inlet.addStructArray("foo"); - arr_container.addDouble("bar"); - arr_container.addFunctionAsValueAlternative("bar", FunctionTag::Double, {}); - - EXPECT_TRUE(inlet.verify()); - auto foos = - inlet["foo"].get>(); - EXPECT_DOUBLE_EQ(foos[7].bar, 2.0); - EXPECT_DOUBLE_EQ(foos[12].bar, 3.0); -} - TEST(inlet_function, function_value_alternative_in_nested_dictionary_of_struct) { - auto inlet = createBasicInlet( - "groups = { " - " [0] = { foo = { first = { bar = 2 }, " - " second = { bar = function () return 3 end } } }, " - " [1] = { foo = { third = { bar = function () return 4 end } } } }"); + for(const bool functionFirst : {true, false}) + { + auto inlet = createBasicInlet(R"( + groups = { + [0] = { + foo = { + first = {bar = 2}, + second = {bar = function () return 3 end} + } + } + } + )"); - auto& groups = inlet.addStructArray("groups"); - auto& foos = groups.addStructDictionary("foo"); - foos.addDouble("bar"); - foos.addFunctionAsValueAlternative("bar", FunctionTag::Double, {}); + auto& groups = inlet.addStructArray("groups"); + auto& foos = groups.addStructDictionary("foo"); + if(functionFirst) + { + foos.addFunctionAsValueAlternative("bar", FunctionTag::Double, {}); + } + foos.addDouble("bar"); + if(!functionFirst) + { + foos.addFunctionAsValueAlternative("bar", FunctionTag::Double, {}); + } - EXPECT_TRUE(inlet.verify()); - const auto values = - inlet["groups"].get>(); - EXPECT_DOUBLE_EQ(values.at(0).values.at("first").bar, 2.0); - EXPECT_DOUBLE_EQ(values.at(0).values.at("second").bar, 3.0); - EXPECT_DOUBLE_EQ(values.at(1).values.at("third").bar, 4.0); + EXPECT_TRUE(inlet.verify()); + const auto values = + inlet["groups"].get>(); + EXPECT_DOUBLE_EQ(values.at(0).values.at("first").bar, 2.0); + EXPECT_DOUBLE_EQ(values.at(0).values.at("second").bar, 3.0); + } } TEST(inlet_function, dimension_dependent_result) From 1f78febcd1c752eba13b8e580b76670d5d39b89e Mon Sep 17 00:00:00 2001 From: Kenneth Weiss Date: Mon, 10 Aug 2026 00:32:10 -0700 Subject: [PATCH 37/52] Inlet: Streamlines new user and developer docs --- src/axom/inlet/Container.hpp | 35 +++++--- src/axom/inlet/Inlet.hpp | 5 +- src/axom/inlet/docs/sphinx/functions.rst | 109 +++++++---------------- src/axom/inlet/docs/sphinx/readers.rst | 13 ++- src/axom/inlet/examples/functions.cpp | 81 +++++------------ 5 files changed, 81 insertions(+), 162 deletions(-) diff --git a/src/axom/inlet/Container.hpp b/src/axom/inlet/Container.hpp index 541553d619..2166307d73 100644 --- a/src/axom/inlet/Container.hpp +++ b/src/axom/inlet/Container.hpp @@ -756,12 +756,12 @@ class Container : public Verifiable * * The function is read from the same public value name as the concrete field * or collection. If a function exists there, the concrete schema entry is - * treated as absent rather than as having the wrong type. The function and - * concrete value may be added in either order. + * treated as absent rather than as having the wrong type. A concrete schema + * entry, when used, may be added before or after the function alternative. * * \param [in] valueName Path of the concrete value or collection, * relative to this Container - * \param [in] ret_type The return type of the function + * \param [in] ret_type The return type. Must not be FunctionTag::Void * \param [in] arg_types The argument types of the function ***************************************************************************** */ @@ -990,36 +990,40 @@ class Container : public Verifiable /*! ***************************************************************************** - * \brief Return whether a Container or Field with the given name is present in - * this Container's subtree. + * \brief Return whether a Container, Field, or ordinary Function with the + * given name is present in this Container's subtree. * - * \return Boolean value indicating whether this Container's subtree contains a - * Field or Container with the given name. + * A function value alternative at \a name is not reported by this method. + * Use \a containsFunctionValueAlternative() to query that representation. + * An ancestor Container is present when an alternative exists below it. + * + * \param [in] name Path relative to this Container + * + * \return Whether the named schema entry is present ***************************************************************************** */ bool contains(const std::string& name) const; /*! ***************************************************************************** - * \brief Returns whether this container or any of its subcontainers exist, - * i.e., if they contain a Field or Function that exists + * \brief Return whether this Container contains a present Field, ordinary + * Function, function value alternative, or subcontainer containing one. ***************************************************************************** */ bool exists() const; /*! ***************************************************************************** - * \brief Returns whether this container or any of its subcontainers were - * provided in the input file, i.e., if they contain a Field or Function that - * was provided in the input file + * \brief Return whether this Container contains a user-provided Field, + * ordinary Function, function value alternative, or subcontainer containing one. ***************************************************************************** */ bool isUserProvided() const; /*! ***************************************************************************** - * \brief Return whether a Container or Field with the given name were - * provided in the input file + * \brief Return whether the named Container, Field, ordinary Function, + * or function value alternative was provided in the input file. * * \param [in] name The path relative to the calling Container to search ***************************************************************************** @@ -1067,6 +1071,9 @@ class Container : public Verifiable * \brief Retrieve the function value alternative associated with a public * value name. * + * Call containsFunctionValueAlternative() first to determine whether the + * function representation was supplied. + * * \param [in] valueName Value path relative to this Container * * \return The function alternative declared for \a valueName. The returned diff --git a/src/axom/inlet/Inlet.hpp b/src/axom/inlet/Inlet.hpp index df0d713147..dd3461d959 100644 --- a/src/axom/inlet/Inlet.hpp +++ b/src/axom/inlet/Inlet.hpp @@ -436,8 +436,9 @@ class Inlet * \brief Add a function that is an alternative representation of a primitive * value or collection in the input deck. * - * \param [in] valueName Public name of the concrete value or collection - * \param [in] ret_type The return type of the function + * \param [in] valueName Path of the concrete value or collection, + * relative to the root Container + * \param [in] ret_type The return type. Must not be FunctionTag::Void * \param [in] arg_types The argument types of the function * * \see Container::addFunctionAsValueAlternative diff --git a/src/axom/inlet/docs/sphinx/functions.rst b/src/axom/inlet/docs/sphinx/functions.rst index 2e45211d68..9c62f6b858 100644 --- a/src/axom/inlet/docs/sphinx/functions.rst +++ b/src/axom/inlet/docs/sphinx/functions.rst @@ -66,79 +66,45 @@ In Lua, the following operations on the ``Vector`` type are supported (for ``Vec Functions as value alternatives ------------------------------- -Some schemas accept either a concrete value or a function that computes that value. -Use ``addFunctionAsValueAlternative`` to declare this relationship explicitly. -The callback and concrete form share one public input name: +Some schemas allow an input to be either a concrete value or a function that computes it. +Declare the concrete entry normally, then associate a function with the same input name: -.. code-block:: C++ - - inlet.addFunctionAsValueAlternative( - "scale", - axom::inlet::FunctionTag::Vector, - {}); - inlet.addDoubleArray("scale"); - -With this schema, ``scale = {2.0, 3.0, 4.0}`` populates ``scale``, -while ``scale = function() return {2.0, 3.0, 4.0} end`` supplies the function -alternative associated with ``scale``. Use ``containsFunctionValueAlternative("scale")`` -and ``getFunctionValueAlternative("scale")`` on the containing ``Container`` to -query and retrieve it. +.. literalinclude:: ../../examples/functions.cpp + :start-after: _inlet_function_value_alternative_schema_start + :end-before: _inlet_function_value_alternative_schema_end + :language: C++ + :dedent: 2 -Inside a struct collection, Inlet resolves the public value name against each concrete -element. For example, this Lua input supplies ``bar`` directly for one ``foo`` element -and computes it with a callback for another: +The declarations may appear in either order and use the same relative and slash-delimited +paths as other ``Container`` methods. The example accepts either of these Lua inputs: .. code-block:: Lua - foo = { - [7] = { bar = 2 }, - [12] = { bar = function() return 3 end } - } - -The corresponding schema is compiled as part of Inlet's function callback example: + scale = 2.0 -.. literalinclude:: ../../examples/functions.cpp - :start-after: _inlet_nested_callback_alternative_start - :end-before: _inlet_nested_callback_alternative_end - :language: C++ - :dedent: 2 + -- or + scale = function() return 3.0 end -After verification, each concrete element contains only the representation that was provided. -A ``FromInlet`` specialization can normalize both representations to a scalar: +After verification, query which representation was supplied before retrieving it: .. literalinclude:: ../../examples/functions.cpp :start-after: _inlet_function_value_alternative_access_start :end-before: _inlet_function_value_alternative_access_end :language: C++ + :dedent: 2 + +For the shared input name, ``contains`` reports only the concrete representation and +``containsFunctionValueAlternative`` reports only the function representation. +Either form counts as user-provided input, and both are recognized by strict Containers. +An unrelated input type fails verification. + +A function value alternative is runtime-only: it is not returned by ``getChildFunctions()``, +cannot be marked required, and is not persisted in Sidre. Validation attached to the concrete +schema entry does not apply to the function result. Applications that require this input or impose +constraints on both forms should validate those conditions after resolving the representation. -This example evaluates a supplied callback during conversion. An application that needs -deferred evaluation can instead call ``get()`` on the result -of ``getFunctionValueAlternative("bar")`` and store the returned ``std::function``. - -The two schema entries may be added in either order and through any equivalent relative path. -For example, ``inlet.addDouble("group/value")`` and -``group.addFunctionAsValueAlternative("value", ...)`` describe one input path. -A function encountered at a normal field path remains a type error -unless this alternative has been declared. -The API permits at most one callback alternative for each canonical input path; -declaring it again through either Container is an error. -Since one Lua object cannot simultaneously be a concrete value and a function, -at most one of the two supported representations can match. - -Only the selected representation exists: ``contains`` reports the concrete field when a -value was supplied, and ``containsFunctionValueAlternative`` reports the callback when a -function was supplied. A value with an unrelated type matches neither representation and -fails verification. The shared public name is recognized by strict containers and is not -reported as unexpected. - -The callback alternative is not an independent schema entry: it does not appear in -``getChildFunctions()``, cannot be marked ``required()``, and is not persisted in Sidre. -The concrete field's validation rules do not automatically apply to the callback's result. -Applications should validate any constraints shared by both forms after resolving the -selected representation. - -Generated Sphinx and JSON Schema documentation describe only the concrete value form. -Document the callback form separately when exposing it as part of an application's Lua interface. +Generated Sphinx and JSON Schema documentation show only the concrete entry. +Application documentation should describe the function form when it is part of the Lua interface. Accessing --------- @@ -169,22 +135,7 @@ by calling it directly: signature defined as part of the schema. This is because the arguments do not participate in overload resolution. -Callbacks copied out of Inlet keep their Lua state alive and remain callable after the Inlet -object is destroyed. Lua execution errors and invalid callback return values are reported as -``std::runtime_error`` at the call site. - -The lifetime behavior is also demonstrated by the compiled function callback example: - -.. literalinclude:: ../../examples/functions.cpp - :start-after: _inlet_callback_after_destruction_start - :end-before: _inlet_callback_after_destruction_end - :language: C++ - :dedent: 2 - -All callbacks obtained from one ``LuaReader`` retain and share that reader's Lua state. -Copying a returned ``std::function`` does not clone the state, and mutations to Lua globals -or captured Lua tables made by one callback are therefore visible to the others. -Callbacks from the same reader must not be invoked concurrently: -even apparently read-only calls use the same Lua interpreter state. -Applications that need concurrent callback evaluation must serialize access to each state -or create independent ``LuaReader`` instances and parse the input separately for each thread. +Callbacks retrieved from Inlet keep their Lua state alive, so they remain callable after the +Inlet and Reader are destroyed. Callbacks from one ``LuaReader`` share mutable interpreter +state and must not be invoked concurrently without synchronization. Lua execution errors and +invalid callback return values throw ``std::runtime_error`` at the call site. diff --git a/src/axom/inlet/docs/sphinx/readers.rst b/src/axom/inlet/docs/sphinx/readers.rst index e923ab1bef..6f344df761 100644 --- a/src/axom/inlet/docs/sphinx/readers.rst +++ b/src/axom/inlet/docs/sphinx/readers.rst @@ -44,7 +44,7 @@ Extra Lua Functionality *********************** The `LuaReader` class has the ability to access the entire Lua State via the protected member function -``LuaReader::solState()``. This allows you fully utilize the Sol library, documented in +``LuaReader::solState()``. This allows you to fully utilize the Sol library, documented in `Sol's documentation `_. This is an advanced feature and not recommended unless there is a good reason. We provide an example on how to create a derived reader class here: @@ -59,12 +59,11 @@ All libraries are documented in `Sol's open_library documentation -#include +#include +#include +#include namespace inlet = axom::inlet; -// _inlet_function_value_alternative_access_start -struct Foo -{ - double bar; -}; - -template <> -struct FromInlet -{ - Foo operator()(const inlet::Container& input) - { - if(input.containsFunctionValueAlternative("bar")) - { - return {input.getFunctionValueAlternative("bar").call()}; - } - - return {input["bar"].get()}; - } -}; -// _inlet_function_value_alternative_access_end - -bool runNestedCallbackExample() +double readScale(const std::string& luaInput) { auto reader = std::make_unique(); - reader->parseString( - "foo = { [7] = { bar = 2 }, " - " [12] = { bar = function () return 3 end } }"); - inlet::Inlet inlet(std::move(reader)); + reader->parseString(luaInput); + inlet::Inlet input(std::move(reader)); - // _inlet_nested_callback_alternative_start - auto& foo = inlet.addStructArray("foo"); - foo.addDouble("bar"); - foo.addFunctionAsValueAlternative( - "bar", - inlet::FunctionTag::Double, - {}); - // _inlet_nested_callback_alternative_end + // _inlet_function_value_alternative_schema_start + input.addDouble("scale"); + input.addFunctionAsValueAlternative("scale", inlet::FunctionTag::Double, {}); + // _inlet_function_value_alternative_schema_end - if(!inlet.verify()) - { - return false; - } - - const auto values = inlet["foo"].get>(); - return values.at(7).bar == 2.0 && values.at(12).bar == 3.0; -} - -bool runCallbackLifetimeExample() -{ - // _inlet_callback_after_destruction_start - std::function callback; + if(!input.verify()) { - auto reader = std::make_unique(); - reader->parseString("offset = 3.0; function foo (value) return value + offset end"); - inlet::Inlet inlet(std::move(reader)); - inlet.addFunction("foo", - inlet::FunctionTag::Double, - {inlet::FunctionTag::Double}); - callback = inlet["foo"].get>(); + return 0.0; } - const double result = callback(4.0); - // _inlet_callback_after_destruction_end - return result == 7.0; + // _inlet_function_value_alternative_access_start + const auto& root = input.getGlobalContainer(); + return root.containsFunctionValueAlternative("scale") + ? root.getFunctionValueAlternative("scale").call() + : input["scale"].get(); + // _inlet_function_value_alternative_access_end } int main() { axom::slic::SimpleLogger logger; - return runNestedCallbackExample() && runCallbackLifetimeExample() ? 0 : 1; + const bool concreteWorks = readScale("scale = 2.0") == 2.0; + const bool callbackWorks = readScale("scale = function() return 3.0 end") == 3.0; + return concreteWorks && callbackWorks ? 0 : 1; } From d6cbb4ecedef51c2172661a06d3e1039342905f9 Mon Sep 17 00:00:00 2001 From: Kenneth Weiss Date: Tue, 11 Aug 2026 00:28:42 -0700 Subject: [PATCH 38/52] Minor fixup for formatting and a return variable --- src/axom/inlet/SphinxWriter.cpp | 10 +++++----- src/axom/inlet/inlet_utils.hpp | 12 ++++++------ src/axom/klee/io/IO.cpp | 3 +-- 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/src/axom/inlet/SphinxWriter.cpp b/src/axom/inlet/SphinxWriter.cpp index dee52053e3..1194aa7d9b 100644 --- a/src/axom/inlet/SphinxWriter.cpp +++ b/src/axom/inlet/SphinxWriter.cpp @@ -110,11 +110,11 @@ void SphinxWriter::documentContainer(const Container& container) extractFieldMetadata(field_entry.second->sidreGroup(), currContainer); } - for(const auto& function_entry : container.getChildFunctions()) - { - extractFunctionMetadata(function_entry.second->sidreGroup(), currContainer); - } -} + for(const auto& function_entry : container.getChildFunctions()) + { + extractFunctionMetadata(function_entry.second->sidreGroup(), currContainer); + } +} void SphinxWriter::finalize() { diff --git a/src/axom/inlet/inlet_utils.hpp b/src/axom/inlet/inlet_utils.hpp index 7c0d93bc22..f61f549cf8 100644 --- a/src/axom/inlet/inlet_utils.hpp +++ b/src/axom/inlet/inlet_utils.hpp @@ -133,12 +133,12 @@ namespace detail ******************************************************************************* */ const std::string COLLECTION_GROUP_NAME = "_inlet_collection"; -const std::string COLLECTION_INDICES_NAME = "_inlet_collection_indices"; -const std::string STRUCT_COLLECTION_FLAG = "_inlet_struct_collection"; -const std::string VARIANT_STRUCT_COLLECTION_FLAG = "_inlet_variant_struct_collection"; -const std::string REQUIRED_FLAG = "required"; -const std::string STRICT_FLAG = "strict"; -} // namespace detail +const std::string COLLECTION_INDICES_NAME = "_inlet_collection_indices"; +const std::string STRUCT_COLLECTION_FLAG = "_inlet_struct_collection"; +const std::string VARIANT_STRUCT_COLLECTION_FLAG = "_inlet_variant_struct_collection"; +const std::string REQUIRED_FLAG = "required"; +const std::string STRICT_FLAG = "strict"; +} // namespace detail /*! ***************************************************************************** diff --git a/src/axom/klee/io/IO.cpp b/src/axom/klee/io/IO.cpp index 40ea6338a1..f3eee73930 100644 --- a/src/axom/klee/io/IO.cpp +++ b/src/axom/klee/io/IO.cpp @@ -269,12 +269,11 @@ struct FromInlet { axom::klee::ShapeData operator()(const axom::inlet::Container& base) { - axom::klee::ShapeData data {base.get("name"), + return axom::klee::ShapeData {base.get("name"), base.get("material"), base["replaces"].get>(), base["does_not_replace"].get>(), base.get("geometry")}; - return data; } }; From 0f2599307ebd5e088bd4ab8850e9287e62a2cd71 Mon Sep 17 00:00:00 2001 From: Kenneth Weiss Date: Tue, 11 Aug 2026 00:59:37 -0700 Subject: [PATCH 39/52] Inlet: Simplifies addFunctionAsValueAlternative by requiring it to be first We previously overcomplicated the solution to allow order independence, which does not seem necessary. The simplified approach allows us to treat the result as an ordinary function. --- src/axom/inlet/Container.cpp | 473 ++++++----------------- src/axom/inlet/Container.hpp | 144 ++++--- src/axom/inlet/Inlet.hpp | 20 +- src/axom/inlet/SphinxWriter.cpp | 6 + src/axom/inlet/docs/sphinx/functions.rst | 15 +- src/axom/inlet/examples/functions.cpp | 16 +- src/axom/inlet/tests/inlet_function.cpp | 177 +++++---- 7 files changed, 352 insertions(+), 499 deletions(-) diff --git a/src/axom/inlet/Container.cpp b/src/axom/inlet/Container.cpp index 1478e6898b..35fd5e66a3 100644 --- a/src/axom/inlet/Container.cpp +++ b/src/axom/inlet/Container.cpp @@ -15,234 +15,6 @@ namespace axom { namespace inlet { -namespace detail -{ -/*! - ***************************************************************************** - * \class FunctionValueAlternativeRegistry - * - * \brief Stores function alternatives for values in an Inlet hierarchy. - * - * Every Container in an Inlet hierarchy shares one registry. Entries are keyed - * by canonical input paths and own the declared function alternatives, - * including empty alternatives used to detect duplicate declarations. - * The registry also tracks concrete value groups so that declaring a function - * alternative before or after the corresponding value produces the same result. - * Registry state is runtime-only and is not persisted in Sidre. - ***************************************************************************** - */ -class FunctionValueAlternativeRegistry -{ -public: - /*! - *************************************************************************** - * \brief Adds a function alternative for an input value. - * - * Empty alternatives are retained so duplicate declarations can be detected. - * A valid alternative updates any concrete value group already registered at - * the same path. - * - * \param [in] inputPath The path of the input value. - * \param [in] function The function alternative to store. - *************************************************************************** - */ - void add(std::string inputPath, FunctionVariant&& function) - { - inputPath = canonicalPath(inputPath); - const auto inserted = m_alternatives.emplace(inputPath, std::move(function)); - if(!inserted.second) - { - SLIC_ERROR(fmt::format( - "[Inlet] Input path '{0}' already has a function value alternative", - inputPath)); - return; - } - - if(inserted.first->second) - { - markConcreteValueAbsent(inputPath); - } - } - - /*! - *************************************************************************** - * \brief Registers the Sidre group for a concrete input value. - * - * The group is retained so its retrieval status can be adjusted if a function - * alternative is declared later. - * - * \param [in] inputPath The path of the concrete input value. - * \param [in] group The Sidre group that stores the value's metadata. - *************************************************************************** - */ - void registerConcreteValue(const std::string& inputPath, axom::sidre::Group* group) - { - m_concreteGroups.emplace(canonicalPath(inputPath), group); - } - - /*! - *************************************************************************** - * \brief Adjusts a concrete value's retrieval result for a function alternative. - * - * \param [in] inputPath The path of the input value. - * \param [in] result The result reported for the concrete value. - * - * \return \a result, except that WrongType becomes NotFound when a valid - * function alternative exists at the same path. - *************************************************************************** - */ - ReaderResult adjust(const std::string& inputPath, ReaderResult result) const - { - return result == ReaderResult::WrongType && contains(inputPath) - ? ReaderResult::NotFound - : result; - } - - /*! - *************************************************************************** - * \brief Tests whether a supplied function alternative exists at a path. - * - * \param [in] inputPath The path of the input value. - * - * \return True when the path has a valid callable, rather than merely an empty - * declaration; otherwise false. - *************************************************************************** - */ - bool contains(const std::string& inputPath) const - { - const auto iter = m_alternatives.find(canonicalPath(inputPath)); - return iter != m_alternatives.end() && static_cast(iter->second); - } - - /*! - *************************************************************************** - * \brief Gets the function alternative declared at a path. - * - * \param [in] inputPath The path of the input value. - * - * \return The stored function wrapper, which may be empty when the alternative - * was declared without a callback. - * - * An error is reported if no alternative was declared at \a inputPath. - *************************************************************************** - */ - const FunctionVariant& get(const std::string& inputPath) const - { - const std::string path = canonicalPath(inputPath); - const auto iter = m_alternatives.find(path); - SLIC_ERROR_IF(iter == m_alternatives.end(), - fmt::format("[Inlet] Function value alternative not found for value: {0}", path)); - return iter->second; - } - - /*! - *************************************************************************** - * \brief Finds function alternatives that are direct children of a container. - * - * \param [in] containerPath The path of the parent container. - * - * \return The names of direct children that have valid function alternatives. - *************************************************************************** - */ - std::vector childNames(const std::string& containerPath) const - { - const std::string parentPath = canonicalPath(containerPath); - std::vector result; - for(const auto& entry : m_alternatives) - { - const Path path(entry.first); - if(static_cast(entry.second) && path.dirName() == parentPath) - { - result.push_back(path.baseName()); - } - } - return result; - } - - /*! - *************************************************************************** - * \brief Tests for valid function alternatives below a container. - * - * \param [in] containerPath The path of the container. - * - * \return True when a descendant has a valid function alternative, else false. - * An alternative at the container's own path is excluded so that a - * whole-collection callback does not make the concrete collection non-empty. - *************************************************************************** - */ - bool containsBelow(const std::string& containerPath) const - { - const std::string parentPath = canonicalPath(containerPath); - const std::string prefix = parentPath.empty() ? "" : parentPath + "/"; - return std::any_of( - m_alternatives.begin(), - m_alternatives.end(), - [&parentPath, &prefix](const decltype(m_alternatives)::value_type& entry) { - if(!static_cast(entry.second)) - { - return false; - } - return parentPath.empty() || entry.first.compare(0, prefix.size(), prefix) == 0; - }); - } - - /*! - ******************************************************************************** - * \brief Converts an input path to the registry's canonical representation. - * - * \param [in] inputPath The path to normalize. - * - * \return The normalized path with internal collection-group components removed. - ******************************************************************************** - */ - static std::string canonicalPath(const std::string& inputPath) - { - std::string result; - const Path path(inputPath); - for(const auto& part : path.parts()) - { - if(part != COLLECTION_GROUP_NAME) - { - result = utilities::string::appendPrefix(result, part); - } - } - return result; - } - -private: - /*! - *************************************************************************** - * \brief Marks concrete values at a path absent when they have the wrong type. - * - * Only WrongType retrieval statuses are changed; all other statuses are unchanged. - * - * \param [in] inputPath A canonical input path. - *************************************************************************** - */ - void markConcreteValueAbsent(const std::string& inputPath) - { - const auto groups = m_concreteGroups.equal_range(inputPath); - for(auto iter = groups.first; iter != groups.second; ++iter) - { - auto* group = iter->second; - if(group->hasView("retrieval_status")) - { - auto* statusView = group->getView("retrieval_status"); - const auto status = - static_cast(static_cast(statusView->getData())); - if(status == ReaderResult::WrongType) - { - statusView->setScalar(static_cast(ReaderResult::NotFound)); - } - } - } - } - - std::unordered_map m_alternatives; - std::unordered_multimap m_concreteGroups; -}; -} // namespace detail - Container::Container(const std::string& name, const std::string& description, Reader& reader, @@ -250,31 +22,11 @@ Container::Container(const std::string& name, std::vector& unexpectedNames, bool docEnabled, bool reconstruct) - : Container(name, - description, - reader, - sidreRootGroup, - unexpectedNames, - std::make_shared(), - docEnabled, - reconstruct) -{ } - -Container::Container( - const std::string& name, - const std::string& description, - Reader& reader, - axom::sidre::Group* sidreRootGroup, - std::vector& unexpectedNames, - std::shared_ptr functionAlternatives, - bool docEnabled, - bool reconstruct) : m_name(name) , m_reader(reader) , m_sidreRootGroup(sidreRootGroup) , m_unexpectedNames(unexpectedNames) , m_docEnabled(docEnabled) - , m_functionAlternatives(std::move(functionAlternatives)) { SLIC_ASSERT_MSG(m_sidreRootGroup != nullptr, "Inlet's Sidre Datastore class not set"); @@ -307,7 +59,13 @@ Container::Container( if(inletType == "Container") { m_containerChildren.emplace(childName, - createChildContainer(childName, "", true)); + std::make_unique(childName, + "", + m_reader, + m_sidreRootGroup, + m_unexpectedNames, + m_docEnabled, + true)); } else if(inletType == "Field") { @@ -333,21 +91,6 @@ Container::Container( } } -std::unique_ptr Container::createChildContainer(const std::string& name, - const std::string& description, - bool reconstruct) -{ - return std::unique_ptr { - new Container(name, - description, - m_reader, - m_sidreRootGroup, - m_unexpectedNames, - m_functionAlternatives, - m_docEnabled, - reconstruct)}; -} - template void Container::forEachCollectionElement(Func&& func) const { @@ -359,25 +102,17 @@ void Container::forEachCollectionElement(Func&& func) const template bool Container::transformFromNestedElements(OutputIt output, const std::string& name, Func&& func) const -{ - return forEachNestedElement(name, [&](Container& container, const std::string& path) { - *output++ = func(container, path); - }); -} - -template -bool Container::forEachNestedElement(const std::string& name, Func&& func) const { for(Container& container : m_nested_aggregates) { - func(container, {}); + *output++ = func(container, {}); } if(isStructCollection()) { for(const auto& indexPath : detail::collectionIndicesWithPaths(*this, name)) { - func(getContainer(indexPath.first), indexPath.second); + *output++ = func(getContainer(indexPath.first), indexPath.second); } } return isStructCollection() || !m_nested_aggregates.empty(); @@ -397,9 +132,16 @@ Container& Container::addContainer(const std::string& name, const std::string& d const std::string currDescr = (pathPart == name) ? description : ""; if(!currContainer->hasChild(pathPart)) { - const auto& emplaceResult = currContainer->m_containerChildren.emplace( - currContainerName, - currContainer->createChildContainer(currContainerName, currDescr)); + // Will the copy always be elided here with a move ctor + // or do we need std::piecewise_construct/std::forward_as_tuple? + const auto& emplaceResult = + currContainer->m_containerChildren.emplace(currContainerName, + std::make_unique(currContainerName, + currDescr, + m_reader, + m_sidreRootGroup, + m_unexpectedNames, + m_docEnabled)); // emplace_result is a pair whose first element is an iterator to the inserted element currContainer = emplaceResult.first->second.get(); } @@ -634,8 +376,11 @@ VerifiableScalar& Container::addPrimitive(const std::string& name, lookupPath = utilities::string::removeAllInstances(lookupPath, detail::COLLECTION_GROUP_NAME + "/"); detail::updateUnexpectedNames(lookupPath, m_unexpectedNames); - m_functionAlternatives->registerConcreteValue(lookupPath, sidreGroup); - auto typeId = addPrimitiveHelper(sidreGroup, lookupPath, forArray, val); + auto typeId = addPrimitiveHelper(sidreGroup, + lookupPath, + forArray, + val, + containsFunctionValueAlternative(name)); return addField(sidreGroup, typeId, fullName, name); } } @@ -644,9 +389,16 @@ template <> axom::sidre::DataTypeId Container::addPrimitiveHelper(axom::sidre::Group* sidreGroup, const std::string& lookupPath, bool forArray, - bool val) + bool val, + bool hasFunctionAlternative) { - const auto result = m_functionAlternatives->adjust(lookupPath, m_reader.getBool(lookupPath, val)); + auto result = m_reader.getBool(lookupPath, val); + if(hasFunctionAlternative && result == ReaderResult::WrongType) + { + // The input supplied the function representation of this value, so the + // concrete entry is absent rather than of the wrong type. + result = ReaderResult::NotFound; + } if(forArray || result == ReaderResult::Success) { sidreGroup->createViewScalar("value", val ? std::int8_t(1) : std::int8_t(0)); @@ -662,9 +414,16 @@ template <> axom::sidre::DataTypeId Container::addPrimitiveHelper(axom::sidre::Group* sidreGroup, const std::string& lookupPath, bool forArray, - int val) + int val, + bool hasFunctionAlternative) { - const auto result = m_functionAlternatives->adjust(lookupPath, m_reader.getInt(lookupPath, val)); + auto result = m_reader.getInt(lookupPath, val); + if(hasFunctionAlternative && result == ReaderResult::WrongType) + { + // The input supplied the function representation of this value, so the + // concrete entry is absent rather than of the wrong type. + result = ReaderResult::NotFound; + } if(forArray || result == ReaderResult::Success) { sidreGroup->createViewScalar("value", val); @@ -680,9 +439,16 @@ template <> axom::sidre::DataTypeId Container::addPrimitiveHelper(axom::sidre::Group* sidreGroup, const std::string& lookupPath, bool forArray, - double val) + double val, + bool hasFunctionAlternative) { - const auto result = m_functionAlternatives->adjust(lookupPath, m_reader.getDouble(lookupPath, val)); + auto result = m_reader.getDouble(lookupPath, val); + if(hasFunctionAlternative && result == ReaderResult::WrongType) + { + // The input supplied the function representation of this value, so the + // concrete entry is absent rather than of the wrong type. + result = ReaderResult::NotFound; + } if(forArray || result == ReaderResult::Success) { sidreGroup->createViewScalar("value", val); @@ -698,9 +464,16 @@ template <> axom::sidre::DataTypeId Container::addPrimitiveHelper(axom::sidre::Group* sidreGroup, const std::string& lookupPath, bool forArray, - std::string val) + std::string val, + bool hasFunctionAlternative) { - const auto result = m_functionAlternatives->adjust(lookupPath, m_reader.getString(lookupPath, val)); + auto result = m_reader.getString(lookupPath, val); + if(hasFunctionAlternative && result == ReaderResult::WrongType) + { + // The input supplied the function representation of this value, + // so the concrete entry is absent rather than of the wrong type. + result = ReaderResult::NotFound; + } if(forArray || result == ReaderResult::Success) { sidreGroup->createViewString("value", val); @@ -1112,10 +885,11 @@ Verifiable& Container::addPrimitiveArray(const std::string& name, lookupPath = utilities::string::removeAllInstances(lookupPath, detail::COLLECTION_GROUP_NAME + "/"); detail::updateUnexpectedNames(lookupPath, m_unexpectedNames); - m_functionAlternatives->registerConcreteValue(lookupPath, container.sidreGroup()); std::vector indices; - if(m_functionAlternatives->contains(lookupPath)) + if(containsFunctionValueAlternative(name)) { + // The input supplied the function representation of this collection, + // so the concrete entry is absent rather than of the wrong type. markRetrievalStatus(*container.sidreGroup(), ReaderResult::NotFound); } else if(isDict) @@ -1160,6 +934,36 @@ Verifiable& Container::addFunction(const std::string& name, const std::vector& arg_types, const std::string& description, const std::string& pathOverride) +{ + return addFunctionInternal(name, name, ret_type, arg_types, description, pathOverride); +} + +Verifiable& Container::addFunctionAsValueAlternative( + const std::string& valueName, + const FunctionTag ret_type, + const std::vector& arg_types, + const std::string& description) +{ + SLIC_ERROR_IF(valueName.empty(), + "[Inlet] A function value alternative requires a non-empty value name"); + SLIC_ERROR_IF(ret_type == FunctionTag::Void, + "[Inlet] A function value alternative requires a non-void return type"); + // The alternative is read from the concrete value's input path but is stored + // under a distinct schema name so the two entries do not collide. + return addFunctionInternal(detail::functionAlternativeName(valueName), + valueName, + ret_type, + arg_types, + description, + ""); +} + +Verifiable& Container::addFunctionInternal(const std::string& schemaName, + const std::string& inputName, + const FunctionTag ret_type, + const std::vector& arg_types, + const std::string& description, + const std::string& pathOverride) { // If it has indices, we're adding a function to an array of structs, // so we need to iterate over the subcontainers corresponding to elements of the array @@ -1167,10 +971,12 @@ Verifiable& Container::addFunction(const std::string& name, const bool is_nested = transformFromNestedElements( std::back_inserter(funcs), - name, - [&name, &ret_type, &arg_types, &description](Container& subcontainer, - const std::string& path) -> Verifiable& { - return subcontainer.addFunction(name, ret_type, arg_types, description, path); + inputName, + [&schemaName, &inputName, &ret_type, &arg_types, &description]( + Container& subcontainer, + const std::string& path) -> Verifiable& { + return subcontainer + .addFunctionInternal(schemaName, inputName, ret_type, arg_types, description, path); }); if(is_nested) { @@ -1184,7 +990,7 @@ Verifiable& Container::addFunction(const std::string& name, else { // Otherwise actually add a Function - std::string fullName = utilities::string::appendPrefix(m_name, name); + std::string fullName = utilities::string::appendPrefix(m_name, schemaName); // First check if the function already exists auto iter = m_functionChildren.find(fullName); if(iter != m_functionChildren.end()) @@ -1196,55 +1002,17 @@ Verifiable& Container::addFunction(const std::string& name, fmt::format("Failed to create Sidre group with name '{0}'", fullName)); detail::addSignatureToGroup(ret_type, arg_types, sidreGroup); // If a pathOverride is specified, needed when Inlet-internal groups - // are part of fullName - std::string lookupPath = (pathOverride.empty()) ? fullName : pathOverride; + // are part of the input path + std::string lookupPath = + (pathOverride.empty()) ? utilities::string::appendPrefix(m_name, inputName) : pathOverride; lookupPath = utilities::string::removeAllInstances(lookupPath, detail::COLLECTION_GROUP_NAME + "/"); detail::updateUnexpectedNames(lookupPath, m_unexpectedNames); auto func = m_reader.getFunction(lookupPath, ret_type, arg_types); - return storeFunction(sidreGroup, std::move(func), fullName, name); + return storeFunction(sidreGroup, std::move(func), fullName, schemaName); } } -void Container::addFunctionAsValueAlternative( - const std::string& valueName, - const FunctionTag ret_type, - const std::vector& arg_types) -{ - SLIC_ERROR_IF(valueName.empty(), - "[Inlet] A function value alternative requires a non-empty value name"); - SLIC_ERROR_IF(ret_type == FunctionTag::Void, - "[Inlet] A function value alternative requires a non-void return type"); - addFunctionValueAlternative(valueName, ret_type, arg_types, ""); -} - -void Container::addFunctionValueAlternative( - const std::string& valueName, - const FunctionTag ret_type, - const std::vector& arg_types, - const std::string& resolvedValuePath) -{ - // Expand the public value name across nested collections. - const bool is_nested = forEachNestedElement( - valueName, - [&valueName, &ret_type, &arg_types](Container& subcontainer, const std::string& path) { - subcontainer.addFunctionValueAlternative(valueName, ret_type, arg_types, path); - }); - if(is_nested) - { - return; - } - - std::string lookupPath = resolvedValuePath.empty() - ? utilities::string::appendPrefix(m_name, valueName) - : resolvedValuePath; - lookupPath = detail::FunctionValueAlternativeRegistry::canonicalPath(lookupPath); - detail::updateUnexpectedNames(lookupPath, m_unexpectedNames); - auto func = m_reader.getFunction(lookupPath, ret_type, arg_types); - func.setName(std::string {lookupPath}); - m_functionAlternatives->add(std::move(lookupPath), std::move(func)); -} - Proxy Container::operator[](const std::string& name) const { const bool has_container = hasContainer(name); @@ -1575,9 +1343,7 @@ bool Container::exists() const return static_cast(*entry.second); }); - const bool has_function_alternatives = m_functionAlternatives->containsBelow(m_name); - - return has_containers || has_fields || has_functions || has_function_alternatives; + return has_containers || has_fields || has_functions; } bool Container::isUserProvided() const @@ -1602,14 +1368,14 @@ bool Container::isUserProvided() const return static_cast(*entry.second); }); - const bool has_function_alternatives = m_functionAlternatives->containsBelow(m_name); - - return has_containers || has_fields || has_functions || has_function_alternatives; + return has_containers || has_fields || has_functions; } bool Container::isUserProvided(const std::string& name) const { - if(m_functionAlternatives->contains(utilities::string::appendPrefix(m_name, name))) + // A function value alternative is stored under a different schema name than + // the value it applies to, so it is not found by the child lookups below + if(containsFunctionValueAlternative(name)) { return true; } @@ -1649,17 +1415,30 @@ const std::unordered_map>& Container::get bool Container::containsFunctionValueAlternative(const std::string& valueName) const { - return m_functionAlternatives->contains(utilities::string::appendPrefix(m_name, valueName)); + auto function = getChildInternal(detail::functionAlternativeName(valueName)); + return function != nullptr && static_cast(*function); } -const FunctionVariant& Container::getFunctionValueAlternative(const std::string& valueName) const +const Function& Container::getFunctionValueAlternative(const std::string& valueName) const { - return m_functionAlternatives->get(utilities::string::appendPrefix(m_name, valueName)); + return getFunction(detail::functionAlternativeName(valueName)); } std::vector Container::getFunctionValueAlternativeNames() const { - return m_functionAlternatives->childNames(m_name); + std::vector names; + for(const auto& entry : m_functionChildren) + { + const std::string childName = Path(entry.first).baseName(); + if(detail::isFunctionAlternativeName(childName) && static_cast(*entry.second)) + { + names.push_back( + childName.substr(0, childName.size() - detail::FUNCTION_ALTERNATIVE_SUFFIX.size())); + } + } + // m_functionChildren is unordered, so sort for a reproducible result + std::sort(names.begin(), names.end()); + return names; } } // namespace inlet diff --git a/src/axom/inlet/Container.hpp b/src/axom/inlet/Container.hpp index 2166307d73..18dd336df7 100644 --- a/src/axom/inlet/Container.hpp +++ b/src/axom/inlet/Container.hpp @@ -69,8 +69,6 @@ class VariantStructCollection; namespace detail { -class FunctionValueAlternativeRegistry; - struct VariantStructFactoryBase { virtual ~VariantStructFactoryBase() = default; @@ -351,6 +349,41 @@ std::vector> collectionIndicesWithPaths(cons void updateUnexpectedNames(const std::string& accessedName, std::vector& unexpectedNames); +/*! + ******************************************************************************* + * \brief Suffix distinguishing a function value alternative's schema entry from + * the concrete entry that shares its input path + ******************************************************************************* + */ +const std::string FUNCTION_ALTERNATIVE_SUFFIX = "_inlet_function_alternative"; + +/*! + ******************************************************************************* + * \brief Returns the schema name of the function alternative for a value + * + * \param [in] valueName The name of the concrete value or collection + * + * \note The alternative is read from \a valueName but stored under a distinct + * schema name so that it does not collide with the concrete entry + ******************************************************************************* + */ +inline std::string functionAlternativeName(const std::string& valueName) +{ + return valueName + FUNCTION_ALTERNATIVE_SUFFIX; +} + +/*! + ******************************************************************************* + * \brief Returns whether a schema name belongs to a function value alternative + * + * \param [in] schemaName The name of a schema entry + ******************************************************************************* + */ +inline bool isFunctionAlternativeName(const std::string& schemaName) +{ + return axom::utilities::string::endsWith(schemaName, FUNCTION_ALTERNATIVE_SUFFIX); +} + } // namespace detail class Proxy; @@ -756,18 +789,24 @@ class Container : public Verifiable * * The function is read from the same public value name as the concrete field * or collection. If a function exists there, the concrete schema entry is - * treated as absent rather than as having the wrong type. A concrete schema - * entry, when used, may be added before or after the function alternative. + * treated as absent rather than as having the wrong type. * - * \param [in] valueName Path of the concrete value or collection, - * relative to this Container - * \param [in] ret_type The return type. Must not be FunctionTag::Void - * \param [in] arg_types The argument types of the function + * \param [in] valueName Path of the concrete value or collection, + * relative to this Container + * \param [in] ret_type The return type. Must not be FunctionTag::Void + * \param [in] arg_types The argument types of the function + * \param [in] description Description of the function + * + * \return Reference to the created Function + * + * \note The alternative must be declared before the concrete schema entry it applies to, + * so that the concrete entry can be suppressed when the input supplies a function. ***************************************************************************** */ - void addFunctionAsValueAlternative(const std::string& valueName, - FunctionTag ret_type, - const std::vector& arg_types); + Verifiable& addFunctionAsValueAlternative(const std::string& valueName, + FunctionTag ret_type, + const std::vector& arg_types, + const std::string& description = ""); /*! ******************************************************************************* @@ -1076,17 +1115,17 @@ class Container : public Verifiable * * \param [in] valueName Value path relative to this Container * - * \return The function alternative declared for \a valueName. The returned - * wrapper is empty when the input did not supply the function representation. + * \return The Function declared as the alternative for \a valueName ***************************************************************************** */ - const FunctionVariant& getFunctionValueAlternative(const std::string& valueName) const; + const Function& getFunctionValueAlternative(const std::string& valueName) const; /*! ***************************************************************************** * \brief Return the public value names of supplied function alternatives. * - * \return Names of direct child values whose function representation was supplied + * \return Sorted names of direct child values whose function representation + * was supplied ***************************************************************************** */ std::vector getFunctionValueAlternativeNames() const; @@ -1140,29 +1179,6 @@ class Container : public Verifiable const std::string& pathOverride = ""); private: - /*! - ***************************************************************************** - * \brief Construct a child Container that shares function-alternative state. - ***************************************************************************** - */ - Container(const std::string& name, - const std::string& description, - Reader& reader, - axom::sidre::Group* sidreRootGroup, - std::vector& unexpectedNames, - std::shared_ptr functionAlternatives, - bool docEnabled, - bool reconstruct); - - /*! - ***************************************************************************** - * \brief Create a child using this Container's shared Inlet state. - ***************************************************************************** - */ - std::unique_ptr createChildContainer(const std::string& name, - const std::string& description, - bool reconstruct = false); - /*! ***************************************************************************** * \brief Add a Container to the input file schema. @@ -1279,6 +1295,9 @@ class Container : public Verifiable * the input file * \param [in] val A provided value, will be overwritten if found at specified * path in input file + * \param [in] hasFunctionAlternative Whether a function value alternative was + * supplied at \a lookupPath, in which case a wrong-type read is reported as + * absent instead * * \return Type ID for the inserted view ***************************************************************************** @@ -1288,7 +1307,8 @@ class Container : public Verifiable axom::sidre::DataTypeId addPrimitiveHelper(axom::sidre::Group* sidreGroup, const std::string& lookupPath, bool forArray, - T val); + T val, + bool hasFunctionAlternative); /*! ***************************************************************************** @@ -1355,19 +1375,30 @@ class Container : public Verifiable /*! ***************************************************************************** - * \brief Add a function alternative for a public value. + * \brief Adds a Function whose schema name may differ from the input path it + * is read from. + * + * This backs both addFunction(), where the two names are identical, and + * addFunctionAsValueAlternative(), where the function is read from a concrete + * value's input path but stored under a distinct schema name. * - * \param [in] valueName Public name of the concrete value or collection - * \param [in] ret_type The return type of the function - * \param [in] arg_types The argument types of the function - * \param [in] resolvedValuePath Concrete input path when expanding a struct - * collection; empty when it should be derived from this Container + * \param [in] schemaName The name of the Function within this Container + * \param [in] inputName The name to read from in the input file + * \param [in] ret_type The return type of the function + * \param [in] arg_types The argument types of the function + * \param [in] description Description of the function + * \param [in] pathOverride The path within the input file to read from, if + * different than the structure of the Sidre datastore + * + * \return Reference to the created Function ***************************************************************************** */ - void addFunctionValueAlternative(const std::string& valueName, - FunctionTag ret_type, - const std::vector& arg_types, - const std::string& resolvedValuePath); + Verifiable& addFunctionInternal(const std::string& schemaName, + const std::string& inputName, + FunctionTag ret_type, + const std::vector& arg_types, + const std::string& description, + const std::string& pathOverride); axom::sidre::View* baseGet(const std::string& name) const; @@ -1546,20 +1577,6 @@ class Container : public Verifiable template bool transformFromNestedElements(OutputIt output, const std::string& name, Func&& func) const; - /*! - ***************************************************************************** - * \brief Applies a provided function to nested elements of the calling table. - * - * \param [in] name The name to append to each nested element's input path - * \param [in] func Function accepting a Container and its resolved input path - * - * \return Whether the calling container had any nested elements (or was a - * struct collection) - ***************************************************************************** - */ - template - bool forEachNestedElement(const std::string& name, Func&& func) const; - std::string m_name; Reader& m_reader; // Inlet's Root Sidre Group @@ -1573,7 +1590,6 @@ class Container : public Verifiable std::unordered_map> m_containerChildren; std::unordered_map> m_fieldChildren; std::unordered_map> m_functionChildren; - std::shared_ptr m_functionAlternatives; Verifier m_verifier; // Used for ownership only - need to take ownership of these so children diff --git a/src/axom/inlet/Inlet.hpp b/src/axom/inlet/Inlet.hpp index dd3461d959..f81bcd7216 100644 --- a/src/axom/inlet/Inlet.hpp +++ b/src/axom/inlet/Inlet.hpp @@ -436,19 +436,23 @@ class Inlet * \brief Add a function that is an alternative representation of a primitive * value or collection in the input deck. * - * \param [in] valueName Path of the concrete value or collection, - * relative to the root Container - * \param [in] ret_type The return type. Must not be FunctionTag::Void - * \param [in] arg_types The argument types of the function + * \param [in] valueName Path of the concrete value or collection, + * relative to the root Container + * \param [in] ret_type The return type. Must not be FunctionTag::Void + * \param [in] arg_types The argument types of the function + * \param [in] description Description of the function + * + * \return Reference to the created Function * * \see Container::addFunctionAsValueAlternative ***************************************************************************** */ - void addFunctionAsValueAlternative(const std::string& valueName, - FunctionTag ret_type, - const std::vector& arg_types) + Verifiable& addFunctionAsValueAlternative(const std::string& valueName, + FunctionTag ret_type, + const std::vector& arg_types, + const std::string& description = "") { - m_globalContainer.addFunctionAsValueAlternative(valueName, ret_type, arg_types); + return m_globalContainer.addFunctionAsValueAlternative(valueName, ret_type, arg_types, description); } /*! diff --git a/src/axom/inlet/SphinxWriter.cpp b/src/axom/inlet/SphinxWriter.cpp index 1194aa7d9b..75a4c83572 100644 --- a/src/axom/inlet/SphinxWriter.cpp +++ b/src/axom/inlet/SphinxWriter.cpp @@ -112,6 +112,12 @@ void SphinxWriter::documentContainer(const Container& container) for(const auto& function_entry : container.getChildFunctions()) { + // A function value alternative is documented through the concrete entry + // that shares its input path, not under its internal schema name + if(detail::isFunctionAlternativeName(Path(function_entry.first).baseName())) + { + continue; + } extractFunctionMetadata(function_entry.second->sidreGroup(), currContainer); } } diff --git a/src/axom/inlet/docs/sphinx/functions.rst b/src/axom/inlet/docs/sphinx/functions.rst index 9c62f6b858..b5b6725b44 100644 --- a/src/axom/inlet/docs/sphinx/functions.rst +++ b/src/axom/inlet/docs/sphinx/functions.rst @@ -75,8 +75,9 @@ Declare the concrete entry normally, then associate a function with the same inp :language: C++ :dedent: 2 -The declarations may appear in either order and use the same relative and slash-delimited -paths as other ``Container`` methods. The example accepts either of these Lua inputs: +Declare the alternative before the concrete entry it applies to. Both use the same relative +and slash-delimited paths as other ``Container`` methods, and either may be declared through +a parent or a child ``Container``. The example accepts either of these Lua inputs: .. code-block:: Lua @@ -95,13 +96,13 @@ After verification, query which representation was supplied before retrieving it For the shared input name, ``contains`` reports only the concrete representation and ``containsFunctionValueAlternative`` reports only the function representation. -Either form counts as user-provided input, and both are recognized by strict Containers. +Either form counts as user-provided input, and both are recognized by strict Containers. An unrelated input type fails verification. -A function value alternative is runtime-only: it is not returned by ``getChildFunctions()``, -cannot be marked required, and is not persisted in Sidre. Validation attached to the concrete -schema entry does not apply to the function result. Applications that require this input or impose -constraints on both forms should validate those conditions after resolving the representation. +An alternative is an ordinary ``Function``, so it is returned by ``addFunctionAsValueAlternative`` +as a ``Verifiable`` and can carry the usual schema constraints, such as ``required()``. +Validation attached to the *concrete* schema entry does not apply to the function result, +so applications that constrain both forms should validate after resolving the representation. Generated Sphinx and JSON Schema documentation show only the concrete entry. Application documentation should describe the function form when it is part of the Lua interface. diff --git a/src/axom/inlet/examples/functions.cpp b/src/axom/inlet/examples/functions.cpp index da0d154914..7473165791 100644 --- a/src/axom/inlet/examples/functions.cpp +++ b/src/axom/inlet/examples/functions.cpp @@ -4,7 +4,9 @@ // // SPDX-License-Identifier: (BSD-3-Clause) +#include "axom/fmt.hpp" #include "axom/inlet.hpp" +#include "axom/slic.hpp" #include "axom/slic/core/SimpleLogger.hpp" #include @@ -20,8 +22,9 @@ double readScale(const std::string& luaInput) inlet::Inlet input(std::move(reader)); // _inlet_function_value_alternative_schema_start - input.addDouble("scale"); + // The alternative is declared before the concrete entry it applies to input.addFunctionAsValueAlternative("scale", inlet::FunctionTag::Double, {}); + input.addDouble("scale"); // _inlet_function_value_alternative_schema_end if(!input.verify()) @@ -41,7 +44,12 @@ int main() { axom::slic::SimpleLogger logger; - const bool concreteWorks = readScale("scale = 2.0") == 2.0; - const bool callbackWorks = readScale("scale = function() return 3.0 end") == 3.0; - return concreteWorks && callbackWorks ? 0 : 1; + const double concrete = readScale("scale = 2.0"); + const double callback = readScale("scale = function() return 3.0 end"); + + SLIC_ERROR_IF(concrete != 2.0, + axom::fmt::format("Expected a concrete scale of 2.0, got {0}", concrete)); + SLIC_ERROR_IF(callback != 3.0, + axom::fmt::format("Expected a callback scale of 3.0, got {0}", callback)); + return 0; } diff --git a/src/axom/inlet/tests/inlet_function.cpp b/src/axom/inlet/tests/inlet_function.cpp index ed9aaced7f..4de0c59db7 100644 --- a/src/axom/inlet/tests/inlet_function.cpp +++ b/src/axom/inlet/tests/inlet_function.cpp @@ -8,10 +8,13 @@ #include "axom/inlet/LuaReader.hpp" #include "axom/inlet/Inlet.hpp" +#include "axom/inlet/SphinxWriter.hpp" #include "gtest/gtest.h" #include +#include +#include #include #include #include @@ -203,8 +206,8 @@ TEST(inlet_function, function_value_alternative_selects_supplied_representation) scale = 4.0 )"); - inlet.addString("label"); inlet.addFunctionAsValueAlternative("label", FunctionTag::String, {}); + inlet.addString("label"); inlet.addFunctionAsValueAlternative("scale", FunctionTag::Double, {}); inlet.addDouble("scale"); @@ -212,8 +215,7 @@ TEST(inlet_function, function_value_alternative_selects_supplied_representation) auto& container = inlet.getGlobalContainer(); EXPECT_FALSE(inlet.contains("label")); ASSERT_TRUE(container.containsFunctionValueAlternative("label")); - EXPECT_EQ(container.getFunctionValueAlternative("label").call(), - "computed"); + EXPECT_EQ(container.getFunctionValueAlternative("label").call(), "computed"); EXPECT_TRUE(inlet.contains("scale")); EXPECT_FALSE(container.containsFunctionValueAlternative("scale")); @@ -222,26 +224,16 @@ TEST(inlet_function, function_value_alternative_selects_supplied_representation) TEST(inlet_function, function_value_alternative_rejects_unrelated_wrong_type) { - for(const bool functionFirst : {true, false}) - { - auto inlet = createBasicInlet("foo = 'not a number or function'"); - if(functionFirst) - { - inlet.addFunctionAsValueAlternative("foo", FunctionTag::Double, {}); - } - inlet.addDouble("foo"); - if(!functionFirst) - { - inlet.addFunctionAsValueAlternative("foo", FunctionTag::Double, {}); - } + auto inlet = createBasicInlet("foo = 'not a number or function'"); + inlet.addFunctionAsValueAlternative("foo", FunctionTag::Double, {}); + inlet.addDouble("foo"); - EXPECT_FALSE(inlet.verify()); - EXPECT_FALSE(inlet.contains("foo")); - EXPECT_FALSE(inlet.getGlobalContainer().containsFunctionValueAlternative("foo")); - // The input exists even though neither schema entry accepts its type. - EXPECT_TRUE(inlet.isUserProvided("foo")); - EXPECT_FALSE(inlet.getGlobalContainer().exists()); - } + EXPECT_FALSE(inlet.verify()); + EXPECT_FALSE(inlet.contains("foo")); + EXPECT_FALSE(inlet.getGlobalContainer().containsFunctionValueAlternative("foo")); + // The input exists even though neither schema entry accepts its type. + EXPECT_TRUE(inlet.isUserProvided("foo")); + EXPECT_FALSE(inlet.getGlobalContainer().exists()); } TEST(inlet_function, function_value_alternative_is_container_independent) @@ -252,7 +244,8 @@ TEST(inlet_function, function_value_alternative_is_container_independent) inlet.getGlobalContainer().strict(); auto& group = inlet.addStruct("group"); - // The two cases cover both Container directions and both declaration orders. + // The alternative and the concrete entry may be declared through different + // Containers, so long as the alternative comes first. if(functionOnRoot) { inlet.addFunctionAsValueAlternative("group/value", FunctionTag::Double, {}); @@ -260,8 +253,8 @@ TEST(inlet_function, function_value_alternative_is_container_independent) } else { - inlet.addDouble("group/value"); group.addFunctionAsValueAlternative("value", FunctionTag::Double, {}); + inlet.addDouble("group/value"); } EXPECT_TRUE(inlet.verify()); @@ -273,55 +266,109 @@ TEST(inlet_function, function_value_alternative_is_container_independent) EXPECT_TRUE(group.isUserProvided("value")); EXPECT_TRUE(group.exists()); EXPECT_DOUBLE_EQ(group.getFunctionValueAlternative("value").call(), 2.0); - EXPECT_EQ(group.getFunctionValueAlternativeNames(), - std::vector {"value"}); + EXPECT_EQ(group.getFunctionValueAlternativeNames(), std::vector {"value"}); EXPECT_TRUE(inlet.getGlobalContainer().getFunctionValueAlternativeNames().empty()); } } -TEST(inlet_function, function_value_alternative_array_is_container_and_order_independent) +TEST(inlet_function, function_value_alternative_names_are_sorted) +{ + auto inlet = createBasicInlet(R"( + zeta = function() return 1.0 end + alpha = function() return 2.0 end + plain = 3.0 + )"); + + inlet.addFunctionAsValueAlternative("zeta", FunctionTag::Double, {}); + inlet.addFunctionAsValueAlternative("alpha", FunctionTag::Double, {}); + // Declared but not supplied by the input, so it is not reported + inlet.addFunctionAsValueAlternative("plain", FunctionTag::Double, {}); + inlet.addDouble("plain"); + + EXPECT_TRUE(inlet.verify()); + EXPECT_EQ(inlet.getGlobalContainer().getFunctionValueAlternativeNames(), + (std::vector {"alpha", "zeta"})); +} + +TEST(inlet_function, function_value_alternative_array_is_container_independent) { - for(const bool functionFirst : {true, false}) + for(const bool functionOnRoot : {true, false}) { - auto inlet = createBasicInlet( - "group = { values = function() return {1.0, 2.0, 3.0} end }"); + auto inlet = createBasicInlet("group = { values = function() return {1.0, 2.0, 3.0} end }"); auto& group = inlet.addStruct("group"); - if(functionFirst) + if(functionOnRoot) { - group.addFunctionAsValueAlternative("values", FunctionTag::Vector, {}); + inlet.addFunctionAsValueAlternative("group/values", FunctionTag::Vector, {}); } - inlet.addDoubleArray("group/values"); - if(!functionFirst) + else { group.addFunctionAsValueAlternative("values", FunctionTag::Vector, {}); } + inlet.addDoubleArray("group/values"); EXPECT_TRUE(inlet.verify()); EXPECT_FALSE(inlet.contains("group/values")); EXPECT_TRUE(group.containsFunctionValueAlternative("values")); - const auto result = - group.getFunctionValueAlternative("values").call(); + const auto result = group.getFunctionValueAlternative("values").call(); EXPECT_DOUBLE_EQ(result[0], 1.0); EXPECT_DOUBLE_EQ(result[1], 2.0); EXPECT_DOUBLE_EQ(result[2], 3.0); } } -TEST(inlet_function, function_value_alternative_rejects_duplicate_across_containers) +TEST(inlet_function, function_value_alternative_declaration_is_idempotent) { + // Matches addFunction's behavior: redeclaring returns the existing entry auto inlet = createBasicInlet("group = { scale = function() return 2.0 end }"); auto& group = inlet.addStruct("group"); - inlet.addFunctionAsValueAlternative("group/scale", FunctionTag::Double, {}); - axom::slic::ScopedAbortToThrow abortGuard; - EXPECT_THROW(group.addFunctionAsValueAlternative("scale", FunctionTag::Double, {}), - axom::slic::SlicAbortException); + auto& first = inlet.addFunctionAsValueAlternative("group/scale", FunctionTag::Double, {}); + auto& second = group.addFunctionAsValueAlternative("scale", FunctionTag::Double, {}); + EXPECT_EQ(&first, &second); + + EXPECT_EQ(1u, group.getChildFunctions().size()); EXPECT_TRUE(inlet.getGlobalContainer().getChildFunctions().empty()); - EXPECT_TRUE(group.getChildFunctions().empty()); EXPECT_DOUBLE_EQ(group.getFunctionValueAlternative("scale").call(), 2.0); } +TEST(inlet_function, function_value_alternative_is_not_documented_under_its_schema_name) +{ + // The alternative is stored under an internal schema name, + // which must not leak into generated documentation + const std::string docFile = "inlet_function_value_alternative_docs.rst"; + { + auto inlet = createBasicInlet("scale = function() return 2.0 end"); + inlet.addFunctionAsValueAlternative("scale", FunctionTag::Double, {}, "a scale callback"); + inlet.addDouble("scale", "a scale"); + EXPECT_TRUE(inlet.verify()); + // The alternative is a real schema entry, just under an internal name + EXPECT_EQ(1u, inlet.getGlobalContainer().getChildFunctions().size()); + inlet.write(axom::inlet::SphinxWriter(docFile)); + } + + std::ifstream stream(docFile); + ASSERT_TRUE(stream.good()); + const std::string contents {std::istreambuf_iterator {stream}, + std::istreambuf_iterator {}}; + EXPECT_EQ(std::string::npos, contents.find("_inlet_function_alternative")); + EXPECT_NE(std::string::npos, contents.find("scale")); +} + +TEST(inlet_function, function_value_alternative_can_be_required) +{ + // The alternative is an ordinary Function, so schema constraints apply to it + auto missing = createBasicInlet("scale = 2.0"); + missing.addFunctionAsValueAlternative("scale", FunctionTag::Double, {}).required(); + missing.addDouble("scale"); + EXPECT_FALSE(missing.verify()); + + auto supplied = createBasicInlet("scale = function() return 2.0 end"); + supplied.addFunctionAsValueAlternative("scale", FunctionTag::Double, {}).required(); + supplied.addDouble("scale"); + EXPECT_TRUE(supplied.verify()); +} + TEST(inlet_function, function_value_alternative_rejects_invalid_declaration) { auto inlet = createBasicInlet(""); @@ -616,37 +663,29 @@ TEST(inlet_function, simple_vec3_to_vec3_array_of_struct) TEST(inlet_function, function_value_alternative_in_nested_dictionary_of_struct) { - for(const bool functionFirst : {true, false}) - { - auto inlet = createBasicInlet(R"( - groups = { - [0] = { - foo = { - first = {bar = 2}, - second = {bar = function () return 3 end} - } + // Both representations appear in the same collection, so the alternative must + // be expanded across the collection's elements exactly as the concrete entry is + auto inlet = createBasicInlet(R"( + groups = { + [0] = { + foo = { + first = {bar = 2}, + second = {bar = function () return 3 end} } } - )"); - - auto& groups = inlet.addStructArray("groups"); - auto& foos = groups.addStructDictionary("foo"); - if(functionFirst) - { - foos.addFunctionAsValueAlternative("bar", FunctionTag::Double, {}); - } - foos.addDouble("bar"); - if(!functionFirst) - { - foos.addFunctionAsValueAlternative("bar", FunctionTag::Double, {}); } + )"); - EXPECT_TRUE(inlet.verify()); - const auto values = - inlet["groups"].get>(); - EXPECT_DOUBLE_EQ(values.at(0).values.at("first").bar, 2.0); - EXPECT_DOUBLE_EQ(values.at(0).values.at("second").bar, 3.0); - } + auto& groups = inlet.addStructArray("groups"); + auto& foos = groups.addStructDictionary("foo"); + foos.addFunctionAsValueAlternative("bar", FunctionTag::Double, {}); + foos.addDouble("bar"); + + EXPECT_TRUE(inlet.verify()); + const auto values = + inlet["groups"].get>(); + EXPECT_DOUBLE_EQ(values.at(0).values.at("first").bar, 2.0); + EXPECT_DOUBLE_EQ(values.at(0).values.at("second").bar, 3.0); } TEST(inlet_function, dimension_dependent_result) From a778933a9932c8581896caea470ec7940a32408f Mon Sep 17 00:00:00 2001 From: Kenneth Weiss Date: Tue, 11 Aug 2026 01:05:13 -0700 Subject: [PATCH 40/52] Klee: Consistently declares callbacks before data for operators --- src/axom/klee/io/GeometryOperatorsIO.cpp | 50 ++++++++++++------------ 1 file changed, 24 insertions(+), 26 deletions(-) diff --git a/src/axom/klee/io/GeometryOperatorsIO.cpp b/src/axom/klee/io/GeometryOperatorsIO.cpp index b7d889453a..0f886929b3 100644 --- a/src/axom/klee/io/GeometryOperatorsIO.cpp +++ b/src/axom/klee/io/GeometryOperatorsIO.cpp @@ -1019,51 +1019,49 @@ inlet::Container &GeometryOperatorData::defineSchema(inlet::Container &parent, bool enableLuaCallbacks) { auto &opContainer = parent.addStructArray(fieldName, description).strict(); - auto &slice = opContainer.addStruct("slice"); - if(enableLuaCallbacks) - { - const auto addCallbackAlternative = - [](inlet::Container &container, const char *fieldName, inlet::FunctionTag returnType) { - container.addFunctionAsValueAlternative( - fieldName, - returnType, - {}); - }; - - addCallbackAlternative(opContainer, "translate", inlet::FunctionTag::Vector); - addCallbackAlternative(opContainer, "rotate", inlet::FunctionTag::Double); - addCallbackAlternative(opContainer, "center", inlet::FunctionTag::Vector); - addCallbackAlternative(opContainer, "axis", inlet::FunctionTag::Vector); - addCallbackAlternative(opContainer, "scale", inlet::FunctionTag::Vector); - addCallbackAlternative(opContainer, "convert_units_to", inlet::FunctionTag::String); - addCallbackAlternative(opContainer, "ref", inlet::FunctionTag::String); - - addCallbackAlternative(slice, "x", inlet::FunctionTag::Double); - addCallbackAlternative(slice, "y", inlet::FunctionTag::Double); - addCallbackAlternative(slice, "z", inlet::FunctionTag::Double); - addCallbackAlternative(slice, "origin", inlet::FunctionTag::Vector); - addCallbackAlternative(slice, "normal", inlet::FunctionTag::Vector); - addCallbackAlternative(slice, "up", inlet::FunctionTag::Vector); - } + // A callback alternative must be declared before the concrete field it may + // stand in for, so each pair is declared together. + const auto addCallback = [enableLuaCallbacks](inlet::Container &container, + const char *name, + inlet::FunctionTag returnType) { + if(enableLuaCallbacks) + { + container.addFunctionAsValueAlternative(name, returnType, {}); + } + }; + addCallback(opContainer, "translate", inlet::FunctionTag::Vector); opContainer.addDoubleArray("translate"); + addCallback(opContainer, "rotate", inlet::FunctionTag::Double); opContainer.addDouble("rotate"); + addCallback(opContainer, "center", inlet::FunctionTag::Vector); opContainer.addDoubleArray("center"); + addCallback(opContainer, "axis", inlet::FunctionTag::Vector); opContainer.addDoubleArray("axis"); + addCallback(opContainer, "scale", inlet::FunctionTag::Vector); opContainer.addDoubleArray("scale"); + addCallback(opContainer, "convert_units_to", inlet::FunctionTag::String); opContainer.addString("convert_units_to"); + auto &slice = opContainer.addStruct("slice"); + addCallback(slice, "x", inlet::FunctionTag::Double); slice.addDouble("x"); + addCallback(slice, "y", inlet::FunctionTag::Double); slice.addDouble("y"); + addCallback(slice, "z", inlet::FunctionTag::Double); slice.addDouble("z"); + addCallback(slice, "origin", inlet::FunctionTag::Vector); slice.addDoubleArray("origin"); + addCallback(slice, "normal", inlet::FunctionTag::Vector); slice.addDoubleArray("normal"); + addCallback(slice, "up", inlet::FunctionTag::Vector); slice.addDoubleArray("up"); + addCallback(opContainer, "ref", inlet::FunctionTag::String); opContainer.addString("ref"); return opContainer; } From dc6e9edc527bfdabf0bac71d127d8c692dff9c61 Mon Sep 17 00:00:00 2001 From: Kenneth Weiss Date: Tue, 11 Aug 2026 01:15:18 -0700 Subject: [PATCH 41/52] Klee: Removes SingleOperatorData -- it was an unnecessary abstraction --- src/axom/klee/io/GeometryOperatorsIO.cpp | 43 ++++++++++-------------- 1 file changed, 18 insertions(+), 25 deletions(-) diff --git a/src/axom/klee/io/GeometryOperatorsIO.cpp b/src/axom/klee/io/GeometryOperatorsIO.cpp index 0f886929b3..0b2508980c 100644 --- a/src/axom/klee/io/GeometryOperatorsIO.cpp +++ b/src/axom/klee/io/GeometryOperatorsIO.cpp @@ -30,10 +30,9 @@ namespace internal namespace { using OpPtr = CompositeOperator::OpPtr; -using OperatorParser = - std::function; +using OperatorParser = std::function; using internal::toDoubleVector; using primal::Point3D; using primal::Vector3D; @@ -497,17 +496,16 @@ void verifyObjectFields(const inlet::Container& containerToTest, /** * Parse a "translate" operator. * - * \param data the Inlet data from which to read the operator + * \param opContainer the Container from which to read the operator * \param startProperties the properties prior to this operator * \param ownerLabel description of the owning shape or named operator * \return the created operator * \throws KleeError if the operator fields or vector dimensions are invalid */ -OpPtr parseTranslate(const SingleOperatorData &data, +OpPtr parseTranslate(const inlet::Container &opContainer, const TransformableGeometryProperties &startProperties, const std::string &ownerLabel) { - const auto &opContainer = *data.m_container; verifyObjectFields(opContainer, "translate", FieldSet {}, FieldSet {}); return std::make_shared( getVector(opContainer, "translate", startProperties.dimensions, ownerLabel), @@ -517,17 +515,16 @@ OpPtr parseTranslate(const SingleOperatorData &data, /** * Parse a "rotate" operator. * - * \param data the Inlet data from which to read the operator + * \param opContainer the Container from which to read the operator * \param startProperties the properties prior to this operator * \param ownerLabel description of the owning shape or named operator * \return the created operator * \throws KleeError if the rotation is invalid for the start dimensions or operator fields */ -OpPtr parseRotate(const SingleOperatorData &data, +OpPtr parseRotate(const inlet::Container &opContainer, const TransformableGeometryProperties &startProperties, const std::string &ownerLabel) { - const auto &opContainer = *data.m_container; switch(startProperties.dimensions) { case Dimensions::Two: @@ -729,17 +726,16 @@ OpPtr readPerpendicularSlice(const inlet::Container &sliceContainer, /** * Parse a "slice" operator. * - * \param data the Inlet data from which to read the operator + * \param opContainer the Container from which to read the operator * \param startProperties the properties prior to this operator * \param ownerLabel description of the owning shape or named operator * \return the created operator * \throws KleeError if the slice fields or values are invalid */ -OpPtr parseSlice(const SingleOperatorData &data, +OpPtr parseSlice(const inlet::Container &opContainer, const TransformableGeometryProperties &startProperties, const std::string &ownerLabel) { - const auto &opContainer = *data.m_container; if(startProperties.dimensions != Dimensions::Three) { throw KleeError({opContainer.name(), "Cannot do a slice from 2D"}); @@ -785,17 +781,16 @@ OpPtr parseSlice(const SingleOperatorData &data, /** * Parse a "scale" operator. * - * \param data the Inlet data from which to read the operator + * \param opContainer the Container from which to read the operator * \param startProperties the properties prior to this operator * \param ownerLabel description of the owning shape or named operator * \return the created operator * \throws KleeError if the scale fields or vector dimensions are invalid */ -OpPtr parseScale(const SingleOperatorData &data, +OpPtr parseScale(const inlet::Container &opContainer, const TransformableGeometryProperties &startProperties, const std::string &ownerLabel) { - const auto &opContainer = *data.m_container; verifyObjectFields(opContainer, "scale", FieldSet {}, FieldSet {"center"}); auto factors = hasCallback(opContainer, "scale") ? wrapCallbackErrors>( @@ -857,17 +852,16 @@ OpPtr parseScale(const SingleOperatorData &data, /** * Parse a "convert_units_to" operator. * - * \param data the Inlet data from which to read the operator + * \param opContainer the Container from which to read the operator * \param startProperties the properties prior to this operator * \param ownerLabel description of the owning shape or named operator * \return the created operator * \throws KleeError if the unit string or operator fields are invalid */ -OpPtr parseConvertUnits(const SingleOperatorData &data, +OpPtr parseConvertUnits(const inlet::Container &opContainer, const TransformableGeometryProperties &startProperties, const std::string &ownerLabel) { - const auto &opContainer = *data.m_container; verifyObjectFields(opContainer, "convert_units_to", FieldSet {}, FieldSet {}); const auto unitName = getString(opContainer, "convert_units_to", ownerLabel); const auto path = fieldPath(opContainer, "convert_units_to"); @@ -895,19 +889,18 @@ OpPtr parseConvertUnits(const SingleOperatorData &data, /** * Parse an operator specified via the "ref" command. * - * \param data the Inlet data from which to read the operator + * \param opContainer the Container from which to read the operator * \param startProperties the properties before the "ref" command * \param namedOperators a map of named operators from which to get referenced operators * \param ownerLabel description of the owning shape or named operator * \return the created operator * \throws KleeError if the reference is missing or the operator fields are invalid */ -OpPtr parseRef(const SingleOperatorData &data, +OpPtr parseRef(const inlet::Container &opContainer, const TransformableGeometryProperties &startProperties, const NamedOperatorMap &namedOperators, const std::string &ownerLabel) { - const auto &opContainer = *data.m_container; verifyObjectFields(opContainer, "ref", FieldSet {}, FieldSet {}); const auto operatorName = getString(opContainer, "ref", ownerLabel); auto opIter = namedOperators.find(operatorName); @@ -971,10 +964,10 @@ OpPtr convertOperator(SingleOperatorData const& data, {"scale", parseScale}, {"convert_units_to", parseConvertUnits}, {"ref", - [&namedOperators](const SingleOperatorData &opData, + [&namedOperators](const inlet::Container &opContainer, const TransformableGeometryProperties &startProperties, const std::string &ownerLabel) { - return parseRef(opData, startProperties, namedOperators, ownerLabel); + return parseRef(opContainer, startProperties, namedOperators, ownerLabel); }}, }; @@ -982,7 +975,7 @@ OpPtr convertOperator(SingleOperatorData const& data, { if(containsFieldOrCallback(*data.m_container, entry.first.c_str())) { - return entry.second(data, startProperties, ownerLabel); + return entry.second(*data.m_container, startProperties, ownerLabel); } } From d77382655c36ed61723e38cffd01708d08bd0dee Mon Sep 17 00:00:00 2001 From: Kenneth Weiss Date: Tue, 11 Aug 2026 01:20:34 -0700 Subject: [PATCH 42/52] Klee: Simplifies error/exception messages in GeometryOperatorsIO --- src/axom/klee/io/GeometryOperatorsIO.cpp | 177 +++++++++-------------- src/axom/klee/tests/klee_io.cpp | 7 +- 2 files changed, 69 insertions(+), 115 deletions(-) diff --git a/src/axom/klee/io/GeometryOperatorsIO.cpp b/src/axom/klee/io/GeometryOperatorsIO.cpp index 0b2508980c..257a7b837b 100644 --- a/src/axom/klee/io/GeometryOperatorsIO.cpp +++ b/src/axom/klee/io/GeometryOperatorsIO.cpp @@ -85,17 +85,25 @@ Path fieldPath(const inlet::Container &container, char const *fieldName) } /** - * Build the contextual prefix for a callback diagnostic. + * Add callback context to a message when a field was supplied as a callback. * * \param container the operator or slice container - * \param fieldName the callback field name + * \param fieldName the field the message is about * \param ownerLabel description of the owning shape or named operator - * \return a diagnostic prefix identifying the callback and its owner + * \param message the message to report + * \return \a message, prefixed with the callback, owner and operator when + * \a fieldName was supplied as a callback, and unchanged otherwise */ -std::string callbackContext(const inlet::Container &container, - char const *fieldName, - const std::string &ownerLabel) +std::string fieldMessage(const inlet::Container &container, + char const *fieldName, + const std::string &ownerLabel, + const std::string &message) { + if(!hasCallback(container, fieldName)) + { + return message; + } + Path path {container.name()}; std::string operatorIndex = path.baseName(); if(operatorIndex == "slice") @@ -105,41 +113,11 @@ std::string callbackContext(const inlet::Container &container, const auto operatorLabel = operatorIndex.empty() ? std::string {"operator at "} + container.name() : std::string {"operator "} + operatorIndex; - if(ownerLabel.empty()) - { - return axom::fmt::format("Error evaluating callback for '{}' in {}", fieldName, operatorLabel); - } - return axom::fmt::format( - "Error evaluating callback for '{}' in {} {}", - fieldName, - ownerLabel, - operatorLabel); -} - -/** - * Throw a semantic validation error with callback context when applicable. - * - * \param container the operator or slice container - * \param fieldName the field whose value failed validation - * \param ownerLabel description of the owning shape or named operator - * \param fallbackPath path used when the field was supplied directly - * \param message semantic validation message - * \throws KleeError unconditionally - */ -[[noreturn]] void throwCallbackAwareValidationError(const inlet::Container &container, - char const *fieldName, - const std::string &ownerLabel, - const Path &fallbackPath, - const std::string &message) -{ - if(hasCallback(container, fieldName)) - { - throw KleeError( - {fieldPath(container, fieldName), - axom::fmt::format("{}: {}", callbackContext(container, fieldName, ownerLabel), message)}); - } - - throw KleeError({fallbackPath, message}); + const auto owner = ownerLabel.empty() ? operatorLabel : ownerLabel + " " + operatorLabel; + return axom::fmt::format("Error evaluating callback for '{}' in {}: {}", + fieldName, + owner, + message); } /** @@ -172,9 +150,8 @@ Result wrapCallbackErrors(const inlet::Container &container, } catch(const std::exception &ex) { - throw KleeError( - {fieldPath(container, fieldName), - axom::fmt::format("{}: {}", callbackContext(container, fieldName, ownerLabel), ex.what())}); + throw KleeError({fieldPath(container, fieldName), + fieldMessage(container, fieldName, ownerLabel, ex.what())}); } } @@ -264,12 +241,15 @@ std::vector getDoubleVector(const inlet::Container &container, auto expectedSize = static_cast(expectedDims); if(actualSize != expectedSize) { - throw KleeError({fieldPath(container, fieldName), - fmt::format("{}: Wrong size for {}. Expected {}. Got {}.", - callbackContext(container, fieldName, ownerLabel), - fieldName, - expectedSize, - actualSize)}); + throw KleeError( + {fieldPath(container, fieldName), + fieldMessage(container, + fieldName, + ownerLabel, + fmt::format("Wrong size for {}. Expected {}. Got {}.", + fieldName, + expectedSize, + actualSize))}); } return values; } @@ -546,14 +526,11 @@ OpPtr parseRotate(const inlet::Container &opContainer, auto axis = getVector(opContainer, "axis", Dimensions::Three, ownerLabel); if(axis.is_zero()) { - auto message = std::string {"The 'axis' vector must not be a zero vector"}; - if(hasCallback(opContainer, "axis")) - { - message = axom::fmt::format("{}: {}", - callbackContext(opContainer, "axis", ownerLabel), - message); - } - throw KleeError({fieldPath(opContainer, "axis"), message}); + throw KleeError({fieldPath(opContainer, "axis"), + fieldMessage(opContainer, + "axis", + ownerLabel, + "The 'axis' vector must not be a zero vector")}); } return std::make_shared(angle, center, axis, startProperties); } @@ -585,34 +562,20 @@ OpPtr makeCheckedSlice(Point3D origin, { if(normal.is_zero()) { - throwCallbackAwareValidationError(sliceContainer, - "normal", - ownerLabel, - Path {sliceContainer.name()}, - "The 'normal' vector must not be a zero vector"); + throw KleeError( + {Path {sliceContainer.name()}, + fieldMessage(sliceContainer, + "normal", + ownerLabel, + "The 'normal' vector must not be a zero vector")}); } if(!utilities::isNearlyEqual(normal.dot(up), 0.)) { + // Either vector may have come from a callback; report the first one that did const std::string message = "The 'normal' and 'up' vectors must be perpendicular"; - if(hasCallback(sliceContainer, "up")) - { - throwCallbackAwareValidationError( - sliceContainer, - "up", - ownerLabel, - Path {sliceContainer.name()}, - message); - } - if(hasCallback(sliceContainer, "normal")) - { - throwCallbackAwareValidationError( - sliceContainer, - "normal", - ownerLabel, - Path {sliceContainer.name()}, - message); - } - throw KleeError({sliceContainer.name(), message}); + char const *reported = hasCallback(sliceContainer, "up") ? "up" : "normal"; + throw KleeError({Path {sliceContainer.name()}, + fieldMessage(sliceContainer, reported, ownerLabel, message)}); } return std::make_shared(origin, normal, up, startProperties); } @@ -653,11 +616,11 @@ primal::Point3D getPerpendicularSliceOrigin(const inlet::Container &sliceContain primal::Point3D givenOrigin = getPoint(sliceContainer, "origin", Dimensions::Three, ownerLabel); if(givenOrigin[nonZeroIndex] != axisIntercept) { - throwCallbackAwareValidationError(sliceContainer, - "origin", - ownerLabel, - Path {sliceContainer["origin"].name()}, - "The origin must be on the slice plane"); + throw KleeError({Path {sliceContainer["origin"].name()}, + fieldMessage(sliceContainer, + "origin", + ownerLabel, + "The origin must be on the slice plane")}); } return givenOrigin; } @@ -685,11 +648,9 @@ primal::Vector3D getPerpendicularSliceNormal(const inlet::Container &sliceContai bool parallel = cross.is_zero(); if(!parallel) { - throwCallbackAwareValidationError(sliceContainer, - "normal", - ownerLabel, - Path {sliceContainer["normal"].name()}, - "Invalid normal"); + throw KleeError( + {Path {sliceContainer["normal"].name()}, + fieldMessage(sliceContainer, "normal", ownerLabel, "Invalid normal")}); } return givenNormal; } @@ -810,11 +771,14 @@ OpPtr parseScale(const inlet::Container &opContainer, auto expectedSize = static_cast(startProperties.dimensions); if(actualSize != expectedSize) { - throw KleeError({fieldPath(opContainer, "scale"), - fmt::format("{}: Wrong size for scale. Expected {}. Got {}.", - callbackContext(opContainer, "scale", ownerLabel), - expectedSize, - actualSize)}); + throw KleeError( + {fieldPath(opContainer, "scale"), + fieldMessage(opContainer, + "scale", + ownerLabel, + fmt::format("Wrong size for scale. Expected {}. Got {}.", + expectedSize, + actualSize))}); } } else if(!isUniform) @@ -877,11 +841,7 @@ OpPtr parseConvertUnits(const inlet::Container &opContainer, throw; } throw KleeError( - {path, - axom::fmt::format( - "{}: {}", - callbackContext(opContainer, "convert_units_to", ownerLabel), - err.what())}); + {path, fieldMessage(opContainer, "convert_units_to", ownerLabel, err.what())}); } return std::make_shared(endUnits, startProperties); } @@ -906,16 +866,9 @@ OpPtr parseRef(const inlet::Container &opContainer, auto opIter = namedOperators.find(operatorName); if(opIter == namedOperators.end()) { - std::string message = "No operator named '"; - message += operatorName; - message += '\''; - if(hasCallback(opContainer, "ref")) - { - message = axom::fmt::format("{}: {}", - callbackContext(opContainer, "ref", ownerLabel), - message); - } - throw KleeError({fieldPath(opContainer, "ref"), message}); + const auto message = axom::fmt::format("No operator named '{}'", operatorName); + throw KleeError({fieldPath(opContainer, "ref"), + fieldMessage(opContainer, "ref", ownerLabel, message)}); } auto referencedOperator = opIter->second; bool startUnitsMatch = startProperties.units == referencedOperator->getStartProperties().units; diff --git a/src/axom/klee/tests/klee_io.cpp b/src/axom/klee/tests/klee_io.cpp index 422e8ecf84..eedb637169 100644 --- a/src/axom/klee/tests/klee_io.cpp +++ b/src/axom/klee/tests/klee_io.cpp @@ -2275,9 +2275,10 @@ TEST(IOTest, readShapeSet_luaCallbackErrorIncludesContext) } catch(const KleeError& err) { - EXPECT_THAT(err.what(), HasSubstr("translate")); - EXPECT_THAT(err.what(), HasSubstr("bad_shape")); - EXPECT_THAT(err.what(), HasSubstr("operator")); + // The operator is identified by its Lua table key, which is 1-based + EXPECT_THAT(err.what(), + HasSubstr("Error evaluating callback for 'translate' in shape 'bad_shape' " + "operator 1")); EXPECT_THAT(err.what(), HasSubstr("callback boom")); } } From a26457a2b9909f3f9be0400047584950538c6599 Mon Sep 17 00:00:00 2001 From: Kenneth Weiss Date: Tue, 11 Aug 2026 01:43:19 -0700 Subject: [PATCH 43/52] Klee: Simplifies Lua initialization to Klee input Only support a Lua table as input, not a ket/value map. --- src/axom/inlet/docs/sphinx/readers.rst | 2 + .../klee/docs/sphinx/specifying_shapes.rst | 88 ++---- src/axom/klee/io/IO.cpp | 84 +----- src/axom/klee/io/IO.hpp | 25 +- src/axom/klee/tests/klee_io.cpp | 268 +----------------- 5 files changed, 47 insertions(+), 420 deletions(-) diff --git a/src/axom/inlet/docs/sphinx/readers.rst b/src/axom/inlet/docs/sphinx/readers.rst index 6f344df761..9a6a7c88ac 100644 --- a/src/axom/inlet/docs/sphinx/readers.rst +++ b/src/axom/inlet/docs/sphinx/readers.rst @@ -1,3 +1,5 @@ +.. _inlet_readers_label: + ####### Readers ####### diff --git a/src/axom/klee/docs/sphinx/specifying_shapes.rst b/src/axom/klee/docs/sphinx/specifying_shapes.rst index 7a18db82c4..7c22b8a69f 100644 --- a/src/axom/klee/docs/sphinx/specifying_shapes.rst +++ b/src/axom/klee/docs/sphinx/specifying_shapes.rst @@ -123,54 +123,11 @@ ordinary table values can be generated programmatically: } } -Caller-provided primitive values can be installed as initial Lua globals before a deck is -evaluated. This is useful when an application wants one deck to select between 2D and 3D -geometry, dimensions, or operator values at run time: - -.. code-block:: c++ - - axom::klee::LuaInputOptions options; - options.initialGlobals = { - {"dimensions", axom::klee::LuaGlobalValue {2}}, - {"shape_suffix", axom::klee::LuaGlobalValue {std::string {"2d"}}} - }; - auto shapeSet = axom::klee::readShapeSet("shape.lua", options); - -.. code-block:: lua - - local function shape_path() - return "part_" .. shape_suffix .. ".stl" - end - - shapes = { - { - name = "part", - material = "steel", - geometry = { - format = "stl", - path = shape_path(), - units = "cm", - operators = { - { translate = (dimensions == 2) and {1.0, 2.0} or {1.0, 2.0, 3.0} } - } - } - } - } - -Initial globals are Lua-only and may be booleans, integers, doubles, or strings. -Their names must be non-keyword ASCII Lua identifiers. They are ordinary mutable -globals—not a read-only context—and are allowed by Klee's unexpected-global check. -Deck code can reassign or delete them, so applications should treat them as initial -values rather than controls. Other helper values in the deck should still be -declared :code:`local`. Initial globals may not replace standard Lua globals such -as :code:`math` or :code:`package`. - -Applications that need richer runtime customization can also provide a Lua -initialization chunk. Klee evaluates the chunk after installing -:code:`initialGlobals` and before parsing the deck. The chunk must return a table; -those table entries are then installed as initial globals while unrelated -unexpected globals in the deck remain errors. This allows host code to provide -helper functions, tables, and local closures without recompiling the application: +An application can supply a Lua initialization chunk that runs before the deck is parsed. +This lets one deck select between 2D and 3D geometry, dimensions, or operator values +at run time, and lets host code provide helper functions and closures without recompiling. +The chunk must return a table; its entries become Lua globals that the deck may use, +while unrelated globals in the deck remain errors: .. code-block:: c++ @@ -213,24 +170,23 @@ helper functions, tables, and local closures without recompiling the application } } -Initialization chunks must return a table whose exported keys are non-keyword -ASCII Lua identifiers. Exported values may be booleans, numbers, strings, tables, -or functions and retain their original Lua representation. An exported Lua integer, -for example, is not converted through a C++ floating-point value. Export names may -not collide with standard Lua globals or :code:`initialGlobals`. - -Klee evaluates the chunk in an isolated Lua environment. Preloaded Lua -libraries and caller-provided initial globals remain visible, but global names -assigned by the chunk do not leak into the input deck unless they are returned -in the export table. Exported functions retain access to the chunk's private -environment. Exported globals are mutable: deck code can replace or delete them -and can mutate exported tables. - -The environment isolation is shallow. Inherited objects such as :code:`math` and -:code:`package` are shared, so mutating a member of an inherited table is visible -to the deck. Initialization chunks and decks are trusted code: this mechanism is -not a security sandbox, :code:`package` may load additional code, and Klee imposes -no CPU, memory, or recursion limits. +Exported keys must be non-keyword ASCII Lua identifiers that do not collide with +standard Lua globals such as :code:`math` or :code:`package`. +Exported values may be booleans, numbers, strings, tables, or functions, +and keep their original Lua representation. + +Klee evaluates the chunk in a separate Lua environment, so names that the chunk +assigns reach the deck only if they are returned in the export table, +and exported functions keep access to the chunk's private environment. +Exported names are ordinary mutable globals and can be modified. + +.. note:: + + The environment separation is not a sandbox. Inherited objects such as + :code:`math` and :code:`package` are shared with the deck, :code:`package` can + load additional code, and Klee imposes no CPU, memory, or recursion limits. + See :ref:`Inlet's reader documentation ` for the general + warning that applies to all Lua input. Use :code:`local` helper functions and constants for intermediate values so the global namespace contains only the Klee schema fields that Inlet should read. diff --git a/src/axom/klee/io/IO.cpp b/src/axom/klee/io/IO.cpp index f3eee73930..6d91d4dca7 100644 --- a/src/axom/klee/io/IO.cpp +++ b/src/axom/klee/io/IO.cpp @@ -62,31 +62,17 @@ class KleeLuaReader : public inlet::LuaReader return names; } - /** - * Install a caller-provided primitive as a mutable Lua global. - * - * \param name the global name - * \param value the primitive value to install - */ - void setInitialGlobal(const std::string &name, const LuaGlobalValue &value) - { - auto lua = solState(); - std::visit([&](const auto &typedValue) { (*lua)[name] = typedValue; }, value); - } - /** * Evaluate an initialization chunk and install its exported values as globals. * * \param initialization the source and diagnostic label for the chunk * \param reservedNames built-in Lua globals that exports may not replace - * \param existingExternalNames caller-provided globals that exports may not replace * \return the names exported by the chunk * \throws KleeError if evaluation fails or the returned exports are invalid */ std::unordered_set applyInitializationChunk( const LuaInitializationChunk &initialization, - const std::unordered_set &reservedNames, - const std::unordered_set &existingExternalNames) + const std::unordered_set &reservedNames) { auto lua = solState(); const std::string chunkName = @@ -184,15 +170,6 @@ class KleeLuaReader : public inlet::LuaReader "Exported Lua global name '{}' conflicts with an existing Lua global.", name))}); } - if(existingExternalNames.find(name) != existingExternalNames.end()) - { - throw KleeError( - {exportPath(name), - chunkMessage(axom::fmt::format( - "Exported Lua global name '{}' duplicates an initial Lua global.", - name))}); - } - switch(entry.second.get_type()) { case axom::sol::type::boolean: @@ -603,33 +580,11 @@ bool isLuaIdentifier(const std::string &name) !isLuaKeyword(name); } -/** - * Validate names supplied through LuaInputOptions::initialGlobals. - * - * \param initialGlobals the caller-provided globals to validate - * \throws KleeError if any global name is not a valid Lua identifier - */ -void validateInitialGlobals(const LuaInitialGlobals &initialGlobals) -{ - for(const auto &entry : initialGlobals) - { - if(!isLuaIdentifier(entry.first)) - { - const auto reason = isLuaKeyword(entry.first) - ? "Reserved Lua keywords cannot be used as initial global names." - : "Initial global names must be Lua identifiers."; - throw KleeError( - {Path {entry.first.empty() ? "" : entry.first}, - axom::fmt::format("Invalid initial Lua global name '{}'. {}", entry.first, reason)}); - } - } -} - /** * Create an Inlet reader for a Klee input format. * * \param format the input file format to read - * \param options optional globals and initialization for Lua input evaluation + * \param options optional initialization for Lua input evaluation * \param allowedGlobals receives external names permitted in the input * \return a reader for \a format * \throws KleeError if \a format is unsupported, Lua support was not enabled, @@ -640,13 +595,10 @@ std::unique_ptr createReader(InputFormat format, std::unordered_set &allowedGlobals) { allowedGlobals.clear(); - if(format != InputFormat::Lua && - (!options.initialGlobals.empty() || options.initialization)) + if(format != InputFormat::Lua && options.initialization) { throw KleeError({Path {""}, - options.initialization - ? "Klee Lua initialization is only supported for Lua input decks." - : "Klee initial Lua globals are only supported for Lua input decks."}); + "Klee Lua initialization is only supported for Lua input decks."}); } switch(format) @@ -657,31 +609,13 @@ std::unique_ptr createReader(InputFormat format, #ifdef AXOM_USE_LUA { auto reader = std::make_unique(); - const auto reservedGlobals = reader->topLevelGlobalNames(); - validateInitialGlobals(options.initialGlobals); - // External inputs are ordinary Lua globals installed before deck parsing. - // allowedGlobals only prevents Klee's unexpected-global check from rejecting - // those names; it does not make them read-only inside the deck. - for(const auto &entry : options.initialGlobals) - { - if(reservedGlobals.find(entry.first) != reservedGlobals.end()) - { - throw KleeError( - {Path {entry.first}, - axom::fmt::format("Initial Lua global name '{}' conflicts with an existing Lua global.", - entry.first)}); - } - reader->setInitialGlobal(entry.first, entry.second); - allowedGlobals.insert(entry.first); - } if(options.initialization) { - auto exportedNames = - reader->applyInitializationChunk( - *options.initialization, - reservedGlobals, - allowedGlobals); - allowedGlobals.insert(exportedNames.begin(), exportedNames.end()); + // Exported values are ordinary Lua globals installed before deck parsing. + // allowedGlobals only prevents Klee's unexpected-global check from rejecting + // those names; it does not make them read-only inside the deck. + allowedGlobals = reader->applyInitializationChunk(*options.initialization, + reader->topLevelGlobalNames()); } return reader; } diff --git a/src/axom/klee/io/IO.hpp b/src/axom/klee/io/IO.hpp index a7c07904e0..f77b58afbf 100644 --- a/src/axom/klee/io/IO.hpp +++ b/src/axom/klee/io/IO.hpp @@ -11,8 +11,6 @@ #include #include #include -#include -#include namespace axom { @@ -35,18 +33,9 @@ struct LuaInitializationChunk std::string label {""}; }; -/// Primitive value types that may be set as initial Lua globals. -using LuaGlobalValue = std::variant; - -/// Ordinary mutable globals to install before a Lua input deck is evaluated. -using LuaInitialGlobals = std::unordered_map; - /// Optional caller-provided initialization for a Lua input deck. struct LuaInputOptions { - /// Primitive values to install as initial mutable Lua globals. - LuaInitialGlobals initialGlobals; - /// Isolated chunk whose returned table entries become initial mutable Lua globals. std::optional initialization; }; @@ -77,8 +66,8 @@ ShapeSet readShapeSet(std::istream& stream, InputFormat format); * * \param stream the stream from which to read the ShapeSet * \param format the input deck format to use - * \param options optional initial globals and initialization for a Lua input deck - * \note Non-empty Lua input options are supported only for Lua input decks. + * \param options optional initialization for a Lua input deck + * \note Lua input options are supported only for Lua input decks. * \return the ShapeSet read from the stream * \throws KleeError if the input or Lua input options are invalid */ @@ -113,9 +102,9 @@ ShapeSet readShapeSet(const std::string& filePath, InputFormat format); * Read a ShapeSet from a specified file with caller-provided Lua inputs. * * \param filePath the file from which to read the ShapeSet - * \param options optional initial globals and initialization for a Lua input deck - * \note The input format is inferred from the file extension. Non-empty Lua - * input options are supported only for Lua input decks. + * \param options optional initialization for a Lua input deck + * \note The input format is inferred from the file extension. + * Lua input options are supported only for Lua input decks. * \return the ShapeSet read from the file * \throws KleeError if the input or Lua input options are invalid */ @@ -127,8 +116,8 @@ ShapeSet readShapeSet(const std::string &filePath, const LuaInputOptions &option * * \param filePath the file from which to read the ShapeSet * \param format the input file format to use, regardless of the file extension - * \param options optional initial globals and initialization for a Lua input deck - * \note Non-empty Lua input options are supported only for Lua input decks. + * \param options optional initialization for a Lua input deck + * \note Lua input options are supported only for Lua input decks. * \return the ShapeSet read from the file * \throws KleeError if the input or Lua input options are invalid */ diff --git a/src/axom/klee/tests/klee_io.cpp b/src/axom/klee/tests/klee_io.cpp index eedb637169..245aaaf9d9 100644 --- a/src/axom/klee/tests/klee_io.cpp +++ b/src/axom/klee/tests/klee_io.cpp @@ -30,7 +30,6 @@ using klee::Dimensions; using klee::InputFormat; using klee::KleeError; using klee::LengthUnit; -using klee::LuaInitialGlobals; using klee::LuaInitializationChunk; using klee::LuaInputOptions; using klee::Rotation; @@ -530,28 +529,6 @@ TEST(IOTest, readShapeSet_streamDefaultsToYaml) } } -TEST(IOTest, readShapeSet_yamlRejectsInitialLuaGlobals) -{ - LuaInputOptions options; - options.initialGlobals = {{"dimensions", klee::LuaGlobalValue {2}}}; - - try - { - readShapeSetFromString(R"( - dimensions: 2 - shapes: [] - )", - InputFormat::YAML, - options); - FAIL() << "Should have thrown"; - } - catch(const KleeError& err) - { - EXPECT_THAT(err.what(), HasSubstr("initial Lua globals")); - EXPECT_THAT(err.what(), HasSubstr("Lua")); - } -} - TEST(IOTest, readShapeSet_yamlRejectsLuaInitialization) { LuaInputOptions options; @@ -609,7 +586,7 @@ TEST(IOTest, readShapeSet_explicitLuaOverridesFileExtension) shapes = {})"); LuaInputOptions options; - options.initialGlobals = {{"dimensions", klee::LuaGlobalValue {2}}}; + options.initialization = LuaInitializationChunk {"return {dimensions = 2}"}; auto shapeSet = klee::readShapeSet(input.getPath(), InputFormat::Lua, options); EXPECT_EQ(Dimensions::Two, shapeSet.getDimensions()); EXPECT_EQ(input.getPath(), shapeSet.getPath()); @@ -621,7 +598,7 @@ TEST(IOTest, readShapeSet_inferredLuaAcceptsInitializationOptions) input.write("shapes = {}"); LuaInputOptions options; - options.initialGlobals = {{"dimensions", klee::LuaGlobalValue {2}}}; + options.initialization = LuaInitializationChunk {"return {dimensions = 2}"}; auto shapeSet = klee::readShapeSet(input.getPath(), options); EXPECT_EQ(Dimensions::Two, shapeSet.getDimensions()); EXPECT_EQ(input.getPath(), shapeSet.getPath()); @@ -668,95 +645,6 @@ TEST(IOTest, readShapeSet_luaStreamMinimalShapeList) EXPECT_EQ(Dimensions::Two, shapeSet.getDimensions()); } -TEST(IOTest, readShapeSet_luaInitialGlobalsProvideDimensionAndOperator) -{ - LuaInitialGlobals initialGlobals { - {"dimensions", klee::LuaGlobalValue {2}}, - {"shape_suffix", klee::LuaGlobalValue {std::string {"2d"}}}, - {"lift", klee::LuaGlobalValue {3.0}}, - {"use_suffix", klee::LuaGlobalValue {true}}, - }; - LuaInputOptions options; - options.initialGlobals = initialGlobals; - - auto shapeSet = readShapeSetFromString(R"( - local function shape_path() - return use_suffix and ("part_" .. shape_suffix .. ".stl") or "part.stl" - end - - shapes = { - { - name = "controlled", - material = "steel", - geometry = { - format = "stl", - path = shape_path(), - units = "cm", - operators = { - { translate = (dimensions == 2) and {1.0, lift} or {1.0, 0.0, lift} } - } - } - } - } - )", - InputFormat::Lua, - options); - - ASSERT_EQ(Dimensions::Two, shapeSet.getDimensions()); - ASSERT_EQ(1u, shapeSet.getShapes().size()); - const auto& geometry = shapeSet.getShapes()[0].getGeometry(); - EXPECT_EQ("part_2d.stl", geometry.getPath()); - auto composite = std::dynamic_pointer_cast(geometry.getGeometryOperator()); - ASSERT_TRUE(composite); - ASSERT_EQ(1u, composite->getOperators().size()); - auto translation = std::dynamic_pointer_cast(composite->getOperators()[0]); - ASSERT_TRUE(translation); - EXPECT_THAT(translation->getOffset(), AlmostEqVector(Vector3D {1.0, 3.0, 0.0})); -} - -TEST(IOTest, readShapeSet_luaInitialGlobalsAreMutable) -{ - LuaInitialGlobals initialGlobals { - {"dimensions", klee::LuaGlobalValue {2}}, - {"lift", klee::LuaGlobalValue {3.0}}, - }; - LuaInputOptions options; - options.initialGlobals = initialGlobals; - - auto shapeSet = readShapeSetFromString(R"( - dimensions = 3 - lift = nil - local resolved_lift = lift or 7.0 - - shapes = { - { - name = "overridden", - material = "steel", - geometry = { - format = "stl", - path = "part.stl", - units = "cm", - operators = { - { translate = {1.0, 2.0, resolved_lift} } - } - } - } - } - )", - InputFormat::Lua, - options); - - ASSERT_EQ(Dimensions::Three, shapeSet.getDimensions()); - ASSERT_EQ(1u, shapeSet.getShapes().size()); - const auto& geometry = shapeSet.getShapes()[0].getGeometry(); - auto composite = std::dynamic_pointer_cast(geometry.getGeometryOperator()); - ASSERT_TRUE(composite); - ASSERT_EQ(1u, composite->getOperators().size()); - auto translation = std::dynamic_pointer_cast(composite->getOperators()[0]); - ASSERT_TRUE(translation); - EXPECT_THAT(translation->getOffset(), AlmostEqVector(Vector3D {1.0, 2.0, 7.0})); -} - TEST(IOTest, readShapeSet_luaInitializationProvidesDimensionAndOperator) { LuaInitializationChunk initialization {R"( @@ -856,60 +744,6 @@ TEST(IOTest, readShapeSet_luaInitializationExportsMutableGlobals) EXPECT_THAT(translation->getOffset(), AlmostEqVector(Vector3D {1.0, 2.0, 7.0})); } -TEST(IOTest, readShapeSet_luaInitializationCanUseInitialGlobals) -{ - LuaInitializationChunk initialization {R"( - local lift = 3.0 - - return { - lift = lift - } - )", - "runtime_initialization"}; - - LuaInitialGlobals initialGlobals { - {"dimensions", klee::LuaGlobalValue {2}}, - {"shape_suffix", klee::LuaGlobalValue {std::string {"2d"}}}, - }; - LuaInputOptions options; - options.initialGlobals = initialGlobals; - options.initialization = initialization; - - auto shapeSet = readShapeSetFromString(R"( - local function shape_path() - return "part_" .. shape_suffix .. ".stl" - end - - shapes = { - { - name = "controlled", - material = "steel", - geometry = { - format = "stl", - path = shape_path(), - units = "cm", - operators = { - { translate = (dimensions == 2) and {1.0, lift} or {1.0, 0.0, lift} } - } - } - } - } - )", - InputFormat::Lua, - options); - - ASSERT_EQ(Dimensions::Two, shapeSet.getDimensions()); - ASSERT_EQ(1u, shapeSet.getShapes().size()); - const auto& geometry = shapeSet.getShapes()[0].getGeometry(); - EXPECT_EQ("part_2d.stl", geometry.getPath()); - auto composite = std::dynamic_pointer_cast(geometry.getGeometryOperator()); - ASSERT_TRUE(composite); - ASSERT_EQ(1u, composite->getOperators().size()); - auto translation = std::dynamic_pointer_cast(composite->getOperators()[0]); - ASSERT_TRUE(translation); - EXPECT_THAT(translation->getOffset(), AlmostEqVector(Vector3D {1.0, 3.0, 0.0})); -} - TEST(IOTest, readShapeSet_luaInitializationIsolatesUnexportedGlobals) { LuaInitializationChunk initialization {R"( @@ -979,22 +813,21 @@ TEST(IOTest, readShapeSet_luaInitializationCannotSetSchemaGlobalsWithoutExportin TEST(IOTest, readShapeSet_luaInitializationClosureRetainsEnvironment) { - LuaInitialGlobals initialGlobals { - {"dimensions", klee::LuaGlobalValue {2}}, - {"base_offset", klee::LuaGlobalValue {1.5}}, - }; + // private_offset is assigned in the chunk's isolated environment and is not + // exported, so it is reachable only through the exported closure LuaInitializationChunk initialization {R"( + local base_offset = 1.5 private_offset = 3.5 return { + dimensions = 2, offset = function() return {base_offset, private_offset} end } )", - "runtime_initialization"}; + "runtime_initialization"}; LuaInputOptions options; - options.initialGlobals = initialGlobals; options.initialization = initialization; auto shapeSet = readShapeSetFromString(R"( @@ -1126,66 +959,6 @@ TEST(IOTest, readShapeSet_luaInitializationRejectsInvalidChunks) } } -TEST(IOTest, readShapeSet_luaInitialGlobalRejectsInvalidName) -{ - const std::array invalidNames {{"", "shape-dim", "\xC3\xA9"}}; - for(const auto& name : invalidNames) - { - LuaInputOptions options; - options.initialGlobals = {{name, klee::LuaGlobalValue {2}}}; - - try - { - readShapeSetFromString(R"( - dimensions = 2 - shapes = {} - )", - InputFormat::Lua, - options); - FAIL() << "Should have thrown"; - } - catch(const KleeError& err) - { - EXPECT_THAT(err.what(), HasSubstr("Invalid initial Lua global name")); - EXPECT_THAT(err.what(), HasSubstr("Lua identifiers")); - } - } -} - -TEST(IOTest, readShapeSet_luaInitialGlobalRejectsKeyword) -{ - LuaInputOptions options; - options.initialGlobals = {{"end", klee::LuaGlobalValue {2}}}; - - try - { - readShapeSetFromString("dimensions = 2; shapes = {}", InputFormat::Lua, options); - FAIL() << "Should have thrown"; - } - catch(const KleeError& err) - { - EXPECT_THAT(err.what(), HasSubstr("Reserved Lua keywords")); - EXPECT_THAT(err.what(), HasSubstr("end")); - } -} - -TEST(IOTest, readShapeSet_luaInitialGlobalRejectsReservedGlobalName) -{ - LuaInputOptions options; - options.initialGlobals = {{"math", klee::LuaGlobalValue {2}}}; - - try - { - readShapeSetFromString("dimensions = 2; shapes = {}", InputFormat::Lua, options); - FAIL() << "Should have thrown"; - } - catch(const KleeError& err) - { - EXPECT_THAT(err.what(), HasSubstr("conflicts with an existing Lua global")); - EXPECT_THAT(err.what(), HasSubstr("math")); - } -} - TEST(IOTest, readShapeSet_luaInitializationRejectsInvalidExportName) { LuaInputOptions options; @@ -1260,33 +1033,6 @@ TEST(IOTest, readShapeSet_luaInitializationRejectsReservedGlobalName) } } -TEST(IOTest, readShapeSet_luaInitializationRejectsDuplicateInitialGlobal) -{ - LuaInputOptions options; - options.initialGlobals = {{"dimensions", klee::LuaGlobalValue {2}}}; - options.initialization = LuaInitializationChunk {R"( - return { - dimensions = 3 - } - )", - "runtime_initialization"}; - - try - { - readShapeSetFromString(R"( - shapes = {} - )", - InputFormat::Lua, - options); - FAIL() << "Should have thrown"; - } - catch(const KleeError& err) - { - EXPECT_THAT(err.what(), HasSubstr("duplicates an initial Lua global")); - EXPECT_THAT(err.what(), HasSubstr("dimensions")); - } -} - TEST(IOTest, readShapeSet_luaInitializationRequiresTableReturn) { LuaInputOptions options; From 4abd28355d93b6572d274d9fe0a1e6181e11efb4 Mon Sep 17 00:00:00 2001 From: Kenneth Weiss Date: Tue, 11 Aug 2026 01:49:46 -0700 Subject: [PATCH 44/52] Klee: Fixes transposed parameters in an error message and improves regexes for tests --- src/axom/klee/io/GeometryOperatorsIO.cpp | 9 ++-- .../klee/tests/klee_geometry_operators_io.cpp | 41 +++++++++++++++++++ src/axom/quest/examples/CMakeLists.txt | 3 +- src/examples/CMakeLists.txt | 10 +++++ src/examples/shaping_tutorial/CMakeLists.txt | 3 +- 5 files changed, 59 insertions(+), 7 deletions(-) diff --git a/src/axom/klee/io/GeometryOperatorsIO.cpp b/src/axom/klee/io/GeometryOperatorsIO.cpp index 257a7b837b..ba194f0f4a 100644 --- a/src/axom/klee/io/GeometryOperatorsIO.cpp +++ b/src/axom/klee/io/GeometryOperatorsIO.cpp @@ -426,10 +426,9 @@ std::unordered_set getChildNames(const inlet::Container& container) * * \endcode * - * In the above, "translate", "rotate", "center", and "axis" are all valid - * entries, but not in arbitrary combinations. You can't specify both - * "translate" and "axis", for example, or "translate" and "rotate" within - * the same entry. + * In the above, "translate", "rotate", "center", and "axis" are all valid entries, + * but not in arbitrary combinations. You can't specify both "translate" and "axis", + * for example, or "translate" and "rotate" within the same entry. * * This function can be used to handle cases like the above. * @@ -469,7 +468,7 @@ void verifyObjectFields(const inlet::Container& containerToTest, } throw KleeError({containerToTest.name(), - axom::fmt::format("Unexpected parameter '{}' for operator '{}'", name, child)}); + axom::fmt::format("Unexpected parameter '{}' for operator '{}'", child, name)}); } } diff --git a/src/axom/klee/tests/klee_geometry_operators_io.cpp b/src/axom/klee/tests/klee_geometry_operators_io.cpp index 16d207dc2b..f2f60a3489 100644 --- a/src/axom/klee/tests/klee_geometry_operators_io.cpp +++ b/src/axom/klee/tests/klee_geometry_operators_io.cpp @@ -368,6 +368,23 @@ TEST(GeometryOperatorsIO, readRotation_3D_axisMissing) } } +TEST(GeometryOperatorsIO, readOperator_unexpectedParameterNamesBothFields) +{ + try + { + readOperators({Dimensions::Three, LengthUnit::cm}, R"( + - translate: [1, 2, 3] + axis: [0, 0, 1] + )"); + FAIL() << "Should not have parsed"; + } + catch(const KleeError &ex) + { + // The unexpected parameter is "axis" and the operator is "translate" + EXPECT_THAT(ex.what(), HasSubstr("Unexpected parameter 'axis' for operator 'translate'")); + } +} + TEST(GeometryOperatorsIO, readScale_singleValue) { Dimensions all_dims[] = {Dimensions::Two, Dimensions::Three}; @@ -385,6 +402,30 @@ TEST(GeometryOperatorsIO, readScale_singleValue) } } +TEST(GeometryOperatorsIO, readScale_singleValue_withCenter) +{ + // A uniform scale honors "center" the same way a per-axis scale does + Dimensions all_dims[] = {Dimensions::Two, Dimensions::Three}; + for(Dimensions dims : all_dims) + { + auto scale = readSingleOperator({dims, LengthUnit::cm}, + dims == Dimensions::Two ? R"( + scale: 1.2 + center: [10, 20] + )" + : R"( + scale: 1.2 + center: [10, 20, 30] + )"); + EXPECT_DOUBLE_EQ(1.2, scale.getXFactor()); + EXPECT_DOUBLE_EQ(1.2, scale.getYFactor()); + EXPECT_DOUBLE_EQ(1.2, scale.getZFactor()); + const Point3D expectedCenter = dims == Dimensions::Two ? Point3D {10, 20, 0} + : Point3D {10, 20, 30}; + EXPECT_THAT(scale.getCenter(), AlmostEqPoint(expectedCenter)); + } +} + TEST(GeometryOperatorsIO, readScale_2d_array) { auto scale = readSingleOperator({Dimensions::Two, LengthUnit::cm}, R"( diff --git a/src/axom/quest/examples/CMakeLists.txt b/src/axom/quest/examples/CMakeLists.txt index 2322b60090..fab95ba165 100644 --- a/src/axom/quest/examples/CMakeLists.txt +++ b/src/axom/quest/examples/CMakeLists.txt @@ -389,7 +389,8 @@ if(AXOM_HAS_MFEM_WITH_MPI AND AXOM_ENABLE_SIDRE AND AXOM_ENABLE_KLEE) inline_mesh --min -6 -6 -6 --max 6 6 6 --resolution 16 16 16 -d 3 NUM_MPI_TASKS ${_nranks}) set_tests_properties(${_testname} PROPERTIES - WILL_FAIL TRUE) + PASS_REGULAR_EXPRESSION + "only supported for Lua input decks") endif() set(_testname quest_shaping_driver_ex_sampling_sphere) diff --git a/src/examples/CMakeLists.txt b/src/examples/CMakeLists.txt index 62641d2d03..b35839584a 100644 --- a/src/examples/CMakeLists.txt +++ b/src/examples/CMakeLists.txt @@ -228,6 +228,16 @@ if(AXOM_ENABLE_TUTORIALS AND AXOM_ENABLE_KLEE AND AXOM_ENABLE_QUEST) -m ${_lesson_04_dir}/circle_input.lua -k ${_lesson_04_dir}/circles_initialized.lua --lua-init-file ${_lesson_04_dir}/circles_initialization.lua) + + set(_testname + shaping_tutorial_lesson_04_quest_sampling_shaper_yaml_rejects_lua_initialization) + blt_add_test(NAME ${_testname} + COMMAND shaping_tutorial_lesson_04_quest_sampling_shaper + -m ${_lesson_04_dir}/circle_input.lua + -k ${_lesson_04_dir}/circles.yaml + --lua-init-file ${_lesson_04_dir}/circles_initialization.lua) + set_tests_properties(${_testname} PROPERTIES + PASS_REGULAR_EXPRESSION "only supported for Lua input decks") endif() endif() endif() diff --git a/src/examples/shaping_tutorial/CMakeLists.txt b/src/examples/shaping_tutorial/CMakeLists.txt index 7e4b24617b..c7d3691d5d 100644 --- a/src/examples/shaping_tutorial/CMakeLists.txt +++ b/src/examples/shaping_tutorial/CMakeLists.txt @@ -140,7 +140,8 @@ if(ENABLE_TESTS) -m ../lesson_04/circle_input.lua -k ../lesson_04/circles.yaml --lua-init-file ../lesson_04/circles_initialization.lua) - set_tests_properties(${_testname} PROPERTIES WILL_FAIL TRUE) + set_tests_properties(${_testname} PROPERTIES + PASS_REGULAR_EXPRESSION "only supported for Lua input decks") endif() endif() From 9dbd5e45a5f4b38d09f9727365e09f696103d5d2 Mon Sep 17 00:00:00 2001 From: Kenneth Weiss Date: Tue, 11 Aug 2026 02:01:07 -0700 Subject: [PATCH 45/52] Klee: Documents processing order for Klee operators and shapes --- .../klee/docs/sphinx/specifying_shapes.rst | 32 +++++++------------ src/axom/klee/tests/klee_io.cpp | 6 ++-- .../shaping_tutorial/lesson_04/README.md | 9 ++---- 3 files changed, 18 insertions(+), 29 deletions(-) diff --git a/src/axom/klee/docs/sphinx/specifying_shapes.rst b/src/axom/klee/docs/sphinx/specifying_shapes.rst index 7c22b8a69f..3d7f61ce01 100644 --- a/src/axom/klee/docs/sphinx/specifying_shapes.rst +++ b/src/axom/klee/docs/sphinx/specifying_shapes.rst @@ -188,6 +188,10 @@ Exported names are ordinary mutable globals and can be modified. See :ref:`Inlet's reader documentation ` for the general warning that applies to all Lua input. + Klee does not coordinate Lua evaluation across MPI ranks. If an application + calls :code:`readShapeSet` on every rank, each rank reads and evaluates the + deck and the initialization chunk independently, so both must be deterministic. + Use :code:`local` helper functions and constants for intermediate values so the global namespace contains only the Klee schema fields that Inlet should read. For Lua input, a one-value scale is written as a one-entry table, for example @@ -195,26 +199,14 @@ For Lua input, a one-value scale is written as a one-entry table, for example Selected operator fields may also be written as zero-argument Lua callbacks. Klee evaluates each callback exactly once while reading the deck; the resulting -shape still contains ordinary affine or slice operators, not runtime Lua -functions. Callbacks should be pure functions of local deck variables. -Callbacks in a named operator are evaluated when that named operator is -constructed. Each :code:`ref` reuses the resulting concrete operator rather -than evaluating its callbacks again for the referring shape. - -Callback evaluation order is deterministic. Klee processes -:code:`named_operators` before :code:`shapes`, entries in each list in source -order, and each geometry's operators in source order. Within a multi-field -operator, fields are evaluated in this order: - -* :code:`rotate`, then :code:`center`, then :code:`axis` (when present) -* :code:`scale`, then :code:`center` (when present) -* perpendicular slice :code:`x`, :code:`y`, or :code:`z`, then - :code:`origin`, :code:`normal`, and :code:`up` (when present) -* arbitrary slice :code:`origin`, then :code:`normal`, then :code:`up` - -Klee does not coordinate Lua evaluation across MPI ranks. If an application -calls :code:`readShapeSet` on every rank, each rank reads and evaluates the deck -and initialization chunk independently. +shape contains ordinary affine or slice operators, not runtime Lua functions. + +Write callbacks as pure functions of local deck variables. Klee does not define +the order in which it evaluates the callbacks within an operator, so a callback +must not depend on another having run. +Klee constructs :code:`named_operators` before :code:`shapes`: +a :code:`ref` reuses the concrete operator built for that named operator +rather than evaluating its callbacks again. .. code-block:: lua diff --git a/src/axom/klee/tests/klee_io.cpp b/src/axom/klee/tests/klee_io.cpp index 245aaaf9d9..9928041939 100644 --- a/src/axom/klee/tests/klee_io.cpp +++ b/src/axom/klee/tests/klee_io.cpp @@ -1322,7 +1322,7 @@ TEST(IOTest, readShapeSet_luaNamedOperatorCallbackIsEvaluatedOnceAndReused) EXPECT_THAT(translation->getOffset(), AlmostEqVector(Vector3D {1, 2, 0})); } -TEST(IOTest, readShapeSet_luaCallbacksHaveDeterministicEvaluationOrder) +TEST(IOTest, readShapeSet_luaCallbacksAreEachEvaluatedOnce) { auto shapeSet = readShapeSetFromString(R"( local callback_index = 0 @@ -1424,8 +1424,8 @@ TEST(IOTest, readShapeSet_luaCallbacksHaveDeterministicEvaluationOrder) )", InputFormat::Lua); - // Reaching the last callback proves ordering across top-level collections, - // operator lists, and the fields within each multi-field operator. + // Reaching the last callback demonstrates that each one ran exactly once. + // The specific order is an implementation detail. ASSERT_EQ(3u, shapeSet.getShapes().size()); } diff --git a/src/examples/shaping_tutorial/lesson_04/README.md b/src/examples/shaping_tutorial/lesson_04/README.md index 999a0a6c5a..0548a2ac71 100644 --- a/src/examples/shaping_tutorial/lesson_04/README.md +++ b/src/examples/shaping_tutorial/lesson_04/README.md @@ -485,12 +485,9 @@ Lua chunk that returns a table of initial globals: --lua-init-file ../src/examples/shaping_tutorial/lesson_04/circles_initialization.lua ``` -This option is valid only with a Lua Klee deck and using it with YAML produces a -Klee validation error. In an MPI run, every rank reads the same deck and -initialization file and evaluates them independently. Consequently, callbacks -and initialization chunks must be deterministic and should not depend on rank, -random values, mutable external files, or unsynchronized side effects. -Klee does not currently parse on one rank and broadcast the resulting shape set. +This option is valid only with a Lua Klee deck, and using it with YAML produces a +Klee validation error. See the Klee user guide for how initialization chunks +and callbacks behave. From 1bd1c0bff88e5412c30895381494597af0c623bf Mon Sep 17 00:00:00 2001 From: Kenneth Weiss Date: Tue, 11 Aug 2026 02:26:17 -0700 Subject: [PATCH 46/52] Bugfix for bump dependency in mesh clipper --- src/axom/quest/util/make_clipper_strategy.cpp | 25 ++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/src/axom/quest/util/make_clipper_strategy.cpp b/src/axom/quest/util/make_clipper_strategy.cpp index 9eec1e0efd..36ab48fa3d 100644 --- a/src/axom/quest/util/make_clipper_strategy.cpp +++ b/src/axom/quest/util/make_clipper_strategy.cpp @@ -10,13 +10,16 @@ #include "axom/quest/util/make_clipper_strategy.hpp" #include "axom/quest/detail/clipping/Plane3DClipper.hpp" #include "axom/quest/detail/clipping/TetClipper.hpp" - #include "axom/quest/detail/clipping/TetMeshClipper.hpp" #include "axom/quest/detail/clipping/HexClipper.hpp" #include "axom/quest/detail/clipping/SphereClipper.hpp" #include "axom/quest/detail/clipping/MonotonicZSORClipper.hpp" #include "axom/quest/detail/clipping/SORClipper.hpp" #include "axom/slic/interface/slic_macros.hpp" + #ifdef AXOM_USE_BUMP + #include "axom/quest/detail/clipping/TetMeshClipper.hpp" + #endif + namespace axom { namespace quest @@ -41,10 +44,6 @@ std::shared_ptr make_clipper_strategy(const axom::klee::Geo { strategy.reset(new TetClipper(kleeGeometry, name)); } - else if(format == "blueprint-tets") - { - strategy.reset(new TetMeshClipper(kleeGeometry, name)); - } else if(format == "hex3D") { strategy.reset(new HexClipper(kleeGeometry, name)); @@ -65,10 +64,24 @@ std::shared_ptr make_clipper_strategy(const axom::klee::Geo { strategy.reset(new MonotonicZSORClipper(kleeGeometry, name)); } + else if(format == "blueprint-tets") + { + #if defined(AXOM_USE_BUMP) + strategy.reset(new TetMeshClipper(kleeGeometry, name)); + #else + SLIC_WARNING(axom::fmt::format( + "klee::Geometry format '{}' requires Axom to be configured with bump " + "but this build does not have it, so shape '{}' cannot be clipped.", + format, + name)); + #endif + } else { SLIC_WARNING( - axom::fmt::format("klee::Geometry format {} is not supported by MeshClipper.", format)); + axom::fmt::format("klee::Geometry format '{}' is not supported by MeshClipper (shape '{}').", + format, + name)); } return strategy; From df9d446d273edfb50162f778fa654d39ba0cf1ef Mon Sep 17 00:00:00 2001 From: Kenneth Weiss Date: Tue, 11 Aug 2026 11:49:39 -0700 Subject: [PATCH 47/52] Inlet: Use InletError instead of std::runtime_error for errors during callback evaluation And documents when we use SLIC_ERROR/SLIC_WARNING vs. validation checks vs. throwing. --- src/axom/inlet/Container.hpp | 12 +--- src/axom/inlet/Function.hpp | 16 +++++ src/axom/inlet/LuaReader.cpp | 18 +++--- src/axom/inlet/Proxy.hpp | 2 + src/axom/inlet/docs/sphinx/functions.rst | 33 +++++++++- src/axom/inlet/inlet_utils.hpp | 76 +++++++++++++++--------- src/axom/inlet/tests/inlet_function.cpp | 6 +- src/axom/klee/io/GeometryOperatorsIO.cpp | 10 +--- 8 files changed, 115 insertions(+), 58 deletions(-) diff --git a/src/axom/inlet/Container.hpp b/src/axom/inlet/Container.hpp index 18dd336df7..cd6b374f72 100644 --- a/src/axom/inlet/Container.hpp +++ b/src/axom/inlet/Container.hpp @@ -349,14 +349,6 @@ std::vector> collectionIndicesWithPaths(cons void updateUnexpectedNames(const std::string& accessedName, std::vector& unexpectedNames); -/*! - ******************************************************************************* - * \brief Suffix distinguishing a function value alternative's schema entry from - * the concrete entry that shares its input path - ******************************************************************************* - */ -const std::string FUNCTION_ALTERNATIVE_SUFFIX = "_inlet_function_alternative"; - /*! ******************************************************************************* * \brief Returns the schema name of the function alternative for a value @@ -369,7 +361,7 @@ const std::string FUNCTION_ALTERNATIVE_SUFFIX = "_inlet_function_alternative"; */ inline std::string functionAlternativeName(const std::string& valueName) { - return valueName + FUNCTION_ALTERNATIVE_SUFFIX; + return valueName + detail::FUNCTION_ALTERNATIVE_SUFFIX; } /*! @@ -381,7 +373,7 @@ inline std::string functionAlternativeName(const std::string& valueName) */ inline bool isFunctionAlternativeName(const std::string& schemaName) { - return axom::utilities::string::endsWith(schemaName, FUNCTION_ALTERNATIVE_SUFFIX); + return axom::utilities::string::endsWith(schemaName, detail::FUNCTION_ALTERNATIVE_SUFFIX); } } // namespace detail diff --git a/src/axom/inlet/Function.hpp b/src/axom/inlet/Function.hpp index bd7dd9b4e7..b1dfaf28ca 100644 --- a/src/axom/inlet/Function.hpp +++ b/src/axom/inlet/Function.hpp @@ -206,6 +206,8 @@ class FunctionWrapper * \tparam Args The types of the user-specified arguments, deduced automatically * * \return The function's result + * \throws InletError if an input function fails during evaluation + * or its return value cannot be converted to the declared return type ******************************************************************************* */ template @@ -298,6 +300,20 @@ class Function : public Verifiable return m_func.get(); } + /*! + ***************************************************************************** + * \brief Calls the function + * + * \param [in] args The parameter pack for the function's arguments + * \tparam Ret The user-specified return type, needed to fully disambiguate the + * function to call + * \tparam Args The types of the user-specified arguments, deduced automatically + * + * \return The function's result + * \throws InletError if an input function fails during evaluation + * or its return value cannot be converted to the declared return type + ***************************************************************************** + */ template Ret call(Args&&... args) const { diff --git a/src/axom/inlet/LuaReader.cpp b/src/axom/inlet/LuaReader.cpp index 787c540a0e..f810042db1 100644 --- a/src/axom/inlet/LuaReader.cpp +++ b/src/axom/inlet/LuaReader.cpp @@ -370,6 +370,7 @@ namespace detail * \tparam Args The argument types of the function * * \return A checkable version of the function's result + * \throws InletError if the Lua function reports an execution error ***************************************************************************** */ template @@ -383,7 +384,7 @@ axom::sol::protected_function_result callWith(const axom::sol::protected_functio if(!tentative_result.valid()) { axom::sol::error err = tentative_result; - throw std::runtime_error(fmt::format("[Inlet] Lua function call failed: {0}", err.what())); + throw InletError(fmt::format("[Inlet] Lua function call failed: {0}", err.what())); } return tentative_result; } @@ -397,6 +398,7 @@ axom::sol::protected_function_result callWith(const axom::sol::protected_functio * \tparam Ret The return type of the function * * \return The function's result + * \throws InletError if the result cannot be converted to \a Ret ***************************************************************************** */ template @@ -407,7 +409,7 @@ Ret extractResult(axom::sol::protected_function_result&& res) { // A failed result conversion is a runtime input error for this function // call. Throwing avoids dereferencing an empty optional after a SLIC log. - throw std::runtime_error("[Inlet] Lua function call failed, return types possibly incorrect"); + throw InletError("[Inlet] Lua function call failed, return types possibly incorrect"); } return option.value(); } @@ -440,7 +442,7 @@ FunctionType::Vector extractResult(axom::sol::protected_fu { if(entry.first.get_type() != axom::sol::type::number) { - throw std::runtime_error( + throw InletError( "[Inlet] Lua vector function return must only contain numeric indices"); } @@ -448,12 +450,12 @@ FunctionType::Vector extractResult(axom::sol::protected_fu const int index = entry.first.as(); if(static_cast(index) != numeric_index || index < 1 || index > 3) { - throw std::runtime_error( + throw InletError( "[Inlet] Lua vector function return indices must be integers between 1 and 3"); } if(entry.second.get_type() != axom::sol::type::number) { - throw std::runtime_error( + throw InletError( "[Inlet] Lua vector function return components must be numeric"); } @@ -464,7 +466,7 @@ FunctionType::Vector extractResult(axom::sol::protected_fu if(count < 1 || count > 3) { - throw std::runtime_error(fmt::format( + throw InletError(fmt::format( "[Inlet] Lua vector function returned a table with {0} entries; " "expected 1 to 3 numeric entries", count)); @@ -473,7 +475,7 @@ FunctionType::Vector extractResult(axom::sol::protected_fu { if(!seen[i]) { - throw std::runtime_error( + throw InletError( "[Inlet] Lua vector function return indices must be contiguous starting at 1"); } } @@ -481,7 +483,7 @@ FunctionType::Vector extractResult(axom::sol::protected_fu return FunctionType::Vector {values.data(), count}; } - throw std::runtime_error("[Inlet] Lua function call failed, return types possibly incorrect"); + throw InletError("[Inlet] Lua function call failed, return types possibly incorrect"); } /*! diff --git a/src/axom/inlet/Proxy.hpp b/src/axom/inlet/Proxy.hpp index 9d12714165..78079d05f0 100644 --- a/src/axom/inlet/Proxy.hpp +++ b/src/axom/inlet/Proxy.hpp @@ -226,6 +226,8 @@ class Proxy * \tparam Args The types of the user-specified arguments, deduced automatically * * \return The function's result + * \throws InletError if an input function fails during evaluation + * or its return value cannot be converted to the declared return type ******************************************************************************* */ template diff --git a/src/axom/inlet/docs/sphinx/functions.rst b/src/axom/inlet/docs/sphinx/functions.rst index b5b6725b44..094230f658 100644 --- a/src/axom/inlet/docs/sphinx/functions.rst +++ b/src/axom/inlet/docs/sphinx/functions.rst @@ -138,5 +138,34 @@ by calling it directly: Callbacks retrieved from Inlet keep their Lua state alive, so they remain callable after the Inlet and Reader are destroyed. Callbacks from one ``LuaReader`` share mutable interpreter -state and must not be invoked concurrently without synchronization. Lua execution errors and -invalid callback return values throw ``std::runtime_error`` at the call site. +state and must not be invoked concurrently without synchronization. + +Lua execution errors and invalid callback return values throw ``axom::inlet::InletError`` +at the call site. This is the one place Inlet throws; everywhere else it reports through +SLIC or through ``verify()``: + +.. list-table:: + :header-rows: 1 + :widths: 30 30 40 + + * - Kind of problem + - Reported through + - Examples + * - API or schema misuse + - ``SLIC_ERROR`` + - an empty or malformed key, a lookup for an entry that was never defined, + a name that is ambiguous between a container, field, and function + * - Contents of the input file + - ``verify()`` and ``VerificationError`` + - a required entry is missing, a value has the wrong type or fails a + registered verifier + * - Failure while calling an input function + - ``InletError`` (derived from ``std::runtime_error``) + - the Lua function raises an error, or returns something that cannot be + converted to the declared return type + +The distinction is when the failure happens. A callback runs after verification, +at the point the application asks for its value, so the failure has to be recoverable: +the caller is the only one that knows which of its own concepts the function belonged to. +Klee, for example, catches ``InletError`` and re-reports it as a ``KleeError`` +naming the shape, operator, and field. diff --git a/src/axom/inlet/inlet_utils.hpp b/src/axom/inlet/inlet_utils.hpp index f61f549cf8..cf72f05b82 100644 --- a/src/axom/inlet/inlet_utils.hpp +++ b/src/axom/inlet/inlet_utils.hpp @@ -1,31 +1,49 @@ -// Copyright (c) Lawrence Livermore National Security, LLC and other -// Axom Project Contributors. See top-level LICENSE and COPYRIGHT -// files for dates and other details. -// -// SPDX-License-Identifier: (BSD-3-Clause) - -#pragma once - -#include -#include - -#include "axom/sidre.hpp" -#include "axom/fmt.hpp" -#include "axom/core/utilities/StringUtilities.hpp" -#include "axom/core/Path.hpp" - -namespace axom -{ -namespace inlet -{ -enum class ReaderResult -{ - Success, // Found with no issue - NotFound, // Path does not exist in the input file - NotHomogeneous, // Found, but elements of other type exist - WrongType // Found, but item at specified path was not of requested type -}; - +// Copyright (c) Lawrence Livermore National Security, LLC and other +// Axom Project Contributors. See top-level LICENSE and COPYRIGHT +// files for dates and other details. +// +// SPDX-License-Identifier: (BSD-3-Clause) + +#pragma once + +#include +#include +#include + +#include "axom/sidre.hpp" +#include "axom/fmt.hpp" +#include "axom/core/utilities/StringUtilities.hpp" +#include "axom/core/Path.hpp" + +namespace axom +{ +namespace inlet +{ +enum class ReaderResult +{ + Success, // Found with no issue + NotFound, // Path does not exist in the input file + NotHomogeneous, // Found, but elements of other type exist + WrongType // Found, but item at specified path was not of requested type +}; + +/*! + ***************************************************************************** + * \brief Exception thrown when evaluating an input function fails + * + * Inlet reports API and schema misuse -- a bad key, a missing entry, an + * ambiguous lookup -- through SLIC, and reports problems with the *contents* of + * an input file through verify() and VerificationError. Neither applies to a + * failure that happens while *calling* a function read from the input, which + * occurs after verification and must be recoverable so the caller can report it + * with its own context. Those failures throw this type. + ***************************************************************************** + */ +struct InletError : public std::runtime_error +{ + using std::runtime_error::runtime_error; +}; + /*! ***************************************************************************** * \brief Information on an Inlet verification error @@ -136,8 +154,10 @@ const std::string COLLECTION_GROUP_NAME = "_inlet_collection"; const std::string COLLECTION_INDICES_NAME = "_inlet_collection_indices"; const std::string STRUCT_COLLECTION_FLAG = "_inlet_struct_collection"; const std::string VARIANT_STRUCT_COLLECTION_FLAG = "_inlet_variant_struct_collection"; +const std::string FUNCTION_ALTERNATIVE_SUFFIX = "_inlet_function_alternative"; const std::string REQUIRED_FLAG = "required"; const std::string STRICT_FLAG = "strict"; + } // namespace detail /*! diff --git a/src/axom/inlet/tests/inlet_function.cpp b/src/axom/inlet/tests/inlet_function.cpp index 4de0c59db7..04ddd52721 100644 --- a/src/axom/inlet/tests/inlet_function.cpp +++ b/src/axom/inlet/tests/inlet_function.cpp @@ -110,7 +110,7 @@ TEST(inlet_function, vector_function_rejects_malformed_table_returns) auto inlet = createBasicInlet("function foo () return " + result + " end"); auto func = inlet.reader().getFunction("foo", FunctionTag::Vector, {}); ASSERT_TRUE(func); - EXPECT_THROW(func.call(), std::runtime_error); + EXPECT_THROW(func.call(), axom::inlet::InletError); } } @@ -123,7 +123,7 @@ TEST(inlet_function, lua_callback_failures_are_catchable) auto wrongType = inlet.reader().getFunction("wrong_type", FunctionTag::Double, {}); ASSERT_TRUE(wrongType); - EXPECT_THROW(wrongType.call(), std::runtime_error); + EXPECT_THROW(wrongType.call(), axom::inlet::InletError); auto runtimeError = inlet.reader().getFunction("runtime_error", FunctionTag::Double, {}); ASSERT_TRUE(runtimeError); @@ -133,7 +133,7 @@ TEST(inlet_function, lua_callback_failures_are_catchable) runtimeError.call(); FAIL() << "Expected the Lua callback to throw"; } - catch(const std::runtime_error& error) + catch(const axom::inlet::InletError& error) { EXPECT_NE(std::string(error.what()).find("callback failed"), std::string::npos); diff --git a/src/axom/klee/io/GeometryOperatorsIO.cpp b/src/axom/klee/io/GeometryOperatorsIO.cpp index ba194f0f4a..e19c5f8000 100644 --- a/src/axom/klee/io/GeometryOperatorsIO.cpp +++ b/src/axom/klee/io/GeometryOperatorsIO.cpp @@ -138,17 +138,13 @@ Result wrapCallbackErrors(const inlet::Container &container, const std::string &ownerLabel, Func &&func) { - // Convert generic Inlet/Lua callback failures into Klee diagnostics at the - // boundary where the shape, operator, and field context are all available. + // Convert Inlet callback failures into Klee diagnostics at the boundary + // where the shape, operator, and field context are all available. try { return func(); } - catch(const KleeError &) - { - throw; - } - catch(const std::exception &ex) + catch(const inlet::InletError &ex) { throw KleeError({fieldPath(container, fieldName), fieldMessage(container, fieldName, ownerLabel, ex.what())}); From d313f57e539e340c0ca57340b91e1e96e19a5d47 Mon Sep 17 00:00:00 2001 From: Kenneth Weiss Date: Tue, 11 Aug 2026 12:48:52 -0700 Subject: [PATCH 48/52] Inlet: Add error message if setting function alternative after setting double --- src/axom/inlet/Container.cpp | 7 +++++++ src/axom/inlet/Container.hpp | 7 ++++--- src/axom/inlet/docs/sphinx/functions.rst | 8 +++++--- src/axom/inlet/tests/inlet_function.cpp | 24 ++++++++++++++++++++++++ 4 files changed, 40 insertions(+), 6 deletions(-) diff --git a/src/axom/inlet/Container.cpp b/src/axom/inlet/Container.cpp index 35fd5e66a3..2dd7ded389 100644 --- a/src/axom/inlet/Container.cpp +++ b/src/axom/inlet/Container.cpp @@ -948,6 +948,13 @@ Verifiable& Container::addFunctionAsValueAlternative( "[Inlet] A function value alternative requires a non-empty value name"); SLIC_ERROR_IF(ret_type == FunctionTag::Void, "[Inlet] A function value alternative requires a non-void return type"); + // Declaring the concrete entry first would already have marked it as being of + // the wrong type, which surfaces later as a confusing verification failure + // blaming the input rather than the schema. Reject it here instead. + SLIC_ERROR_IF( + getChildInternal(valueName) != nullptr || getChildInternal(valueName) != nullptr, + fmt::format("[Inlet] The function value alternative for '{0}' must be declared before '{0}'", + valueName)); // The alternative is read from the concrete value's input path but is stored // under a distinct schema name so the two entries do not collide. return addFunctionInternal(detail::functionAlternativeName(valueName), diff --git a/src/axom/inlet/Container.hpp b/src/axom/inlet/Container.hpp index cd6b374f72..29b3538e81 100644 --- a/src/axom/inlet/Container.hpp +++ b/src/axom/inlet/Container.hpp @@ -779,9 +779,9 @@ class Container : public Verifiable * \brief Add a function that is an alternative representation of a primitive * value or collection in the input deck. * - * The function is read from the same public value name as the concrete field - * or collection. If a function exists there, the concrete schema entry is - * treated as absent rather than as having the wrong type. + * The function is read from the same public value name as the concrete field or collection. + * If a function exists there, the concrete schema entry is treated as absent + * rather than as having the wrong type. * * \param [in] valueName Path of the concrete value or collection, * relative to this Container @@ -793,6 +793,7 @@ class Container : public Verifiable * * \note The alternative must be declared before the concrete schema entry it applies to, * so that the concrete entry can be suppressed when the input supplies a function. + * Declaring it afterwards is an error. ***************************************************************************** */ Verifiable& addFunctionAsValueAlternative(const std::string& valueName, diff --git a/src/axom/inlet/docs/sphinx/functions.rst b/src/axom/inlet/docs/sphinx/functions.rst index 094230f658..ea8ac73edb 100644 --- a/src/axom/inlet/docs/sphinx/functions.rst +++ b/src/axom/inlet/docs/sphinx/functions.rst @@ -75,9 +75,11 @@ Declare the concrete entry normally, then associate a function with the same inp :language: C++ :dedent: 2 -Declare the alternative before the concrete entry it applies to. Both use the same relative -and slash-delimited paths as other ``Container`` methods, and either may be declared through -a parent or a child ``Container``. The example accepts either of these Lua inputs: +Note that you must declare the alternative before the concrete entry it applies to, +and declaring it afterwards is an error, since the concrete entry has already been read by then. +Both use the same relative and slash-delimited paths as other ``Container`` methods, +and either may be declared through a parent or a child ``Container``. +The following example accepts either of these Lua inputs: .. code-block:: Lua diff --git a/src/axom/inlet/tests/inlet_function.cpp b/src/axom/inlet/tests/inlet_function.cpp index 04ddd52721..24bf7288f7 100644 --- a/src/axom/inlet/tests/inlet_function.cpp +++ b/src/axom/inlet/tests/inlet_function.cpp @@ -381,6 +381,30 @@ TEST(inlet_function, function_value_alternative_rejects_invalid_declaration) EXPECT_TRUE(inlet.getGlobalContainer().getFunctionValueAlternativeNames().empty()); } +TEST(inlet_function, function_value_alternative_rejects_declaration_after_the_value) +{ + // Declaring the concrete entry first cannot work: it has already been read and + // marked as being of the wrong type. Without this check the schema author sees + // a verification failure blaming the input instead of the schema. + axom::slic::ScopedAbortToThrow abortGuard; + + auto scalar = createBasicInlet("scale = function() return 2.0 end"); + scalar.addDouble("scale"); + EXPECT_THROW(scalar.addFunctionAsValueAlternative("scale", FunctionTag::Double, {}), + axom::slic::SlicAbortException); + + auto collection = createBasicInlet("values = function() return {1.0, 2.0} end"); + collection.addDoubleArray("values"); + EXPECT_THROW(collection.addFunctionAsValueAlternative("values", FunctionTag::Vector, {}), + axom::slic::SlicAbortException); + + // Declaring through a parent Container is rejected the same way + auto nested = createBasicInlet("group = { value = function() return 2.0 end }"); + nested.addStruct("group").addDouble("value"); + EXPECT_THROW(nested.addFunctionAsValueAlternative("group/value", FunctionTag::Double, {}), + axom::slic::SlicAbortException); +} + TEST(inlet_function, returned_function_keeps_lua_state_alive) { // An extracted callback must retain its Lua state after Inlet is destroyed. From 740799fbd0a90a8ad4a816930a4dd9479b66bbb2 Mon Sep 17 00:00:00 2001 From: Kenneth Weiss Date: Tue, 11 Aug 2026 12:57:40 -0700 Subject: [PATCH 49/52] Klee: Use string_view to avoid warning --- src/axom/klee/docs/sphinx/specifying_shapes.rst | 2 +- src/axom/klee/tests/klee_io.cpp | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/axom/klee/docs/sphinx/specifying_shapes.rst b/src/axom/klee/docs/sphinx/specifying_shapes.rst index 3d7f61ce01..468cc792f1 100644 --- a/src/axom/klee/docs/sphinx/specifying_shapes.rst +++ b/src/axom/klee/docs/sphinx/specifying_shapes.rst @@ -203,7 +203,7 @@ shape contains ordinary affine or slice operators, not runtime Lua functions. Write callbacks as pure functions of local deck variables. Klee does not define the order in which it evaluates the callbacks within an operator, so a callback -must not depend on another having run. +must not depend on another having run. Klee constructs :code:`named_operators` before :code:`shapes`: a :code:`ref` reuses the concrete operator built for that named operator rather than evaluating its callbacks again. diff --git a/src/axom/klee/tests/klee_io.cpp b/src/axom/klee/tests/klee_io.cpp index 9928041939..a2af6bf927 100644 --- a/src/axom/klee/tests/klee_io.cpp +++ b/src/axom/klee/tests/klee_io.cpp @@ -19,6 +19,7 @@ #include #include #include +#include namespace klee = axom::klee; namespace inlet = axom::inlet; @@ -800,10 +801,9 @@ TEST(IOTest, readShapeSet_luaInitializationIsolatesUnexportedGlobals) TEST(IOTest, readShapeSet_luaInitializationCannotSetSchemaGlobalsWithoutExporting) { - for(const std::string& source : - {"dimensions = 2; return {}", "_G.dimensions = 2; return {}"}) + for(std::string_view source : {"dimensions = 2; return {}", "_G.dimensions = 2; return {}"}) { - LuaInitializationChunk initialization {source, "runtime_initialization"}; + LuaInitializationChunk initialization {std::string {source}, "runtime_initialization"}; LuaInputOptions options; options.initialization = initialization; EXPECT_THROW(readShapeSetFromString("shapes = {}", InputFormat::Lua, options), From 5a84b817e39cb59aaee33a232ef6566a28a9bc45 Mon Sep 17 00:00:00 2001 From: Kenneth Weiss Date: Tue, 11 Aug 2026 13:01:17 -0700 Subject: [PATCH 50/52] Klee: Removes redundant io tests And adds a minor missing one. --- src/axom/klee/tests/klee_io.cpp | 243 ++++++-------------------------- 1 file changed, 41 insertions(+), 202 deletions(-) diff --git a/src/axom/klee/tests/klee_io.cpp b/src/axom/klee/tests/klee_io.cpp index a2af6bf927..d9feee4a16 100644 --- a/src/axom/klee/tests/klee_io.cpp +++ b/src/axom/klee/tests/klee_io.cpp @@ -1428,6 +1428,7 @@ TEST(IOTest, readShapeSet_luaCallbacksAreEachEvaluatedOnce) // The specific order is an implementation detail. ASSERT_EQ(3u, shapeSet.getShapes().size()); } +#endif TEST(IOTest, readShapeSet_luaNamedOperatorCallbackErrorIncludesContext) { @@ -1553,6 +1554,46 @@ TEST(IOTest, readShapeSet_luaGeneratedOrdinaryTableValues) EXPECT_THAT(translation->getOffset(), AlmostEqVector(Vector3D {4, 8, 0})); } +TEST(IOTest, readShapeSet_lua2dOperatorCallbacks) +{ + // Vector-valued callbacks in a 2D deck return two components and are padded to three. + // Scale is covered here because readShapeSet_luaOperatorCallbacks is 3D. + auto shapeSet = readShapeSetFromString(R"( + dimensions = 2 + shapes = { + { + name = "part", + material = "steel", + geometry = { + format = "stl", + path = "part.stl", + units = "cm", + operators = { + { translate = function() return {4, 8} end }, + { scale = function() return {2.0, 3.0} end } + } + } + } + } + )", + InputFormat::Lua); + + auto composite = std::dynamic_pointer_cast( + shapeSet.getShapes()[0].getGeometry().getGeometryOperator()); + ASSERT_TRUE(composite); + ASSERT_EQ(2u, composite->getOperators().size()); + + auto translation = dynamic_cast(composite->getOperators()[0].get()); + ASSERT_NE(translation, nullptr); + EXPECT_THAT(translation->getOffset(), AlmostEqVector(Vector3D {4, 8, 0})); + + auto scale = dynamic_cast(composite->getOperators()[1].get()); + ASSERT_NE(scale, nullptr); + EXPECT_DOUBLE_EQ(2.0, scale->getXFactor()); + EXPECT_DOUBLE_EQ(3.0, scale->getYFactor()); + EXPECT_DOUBLE_EQ(1.0, scale->getZFactor()); +} + TEST(IOTest, readShapeSet_luaOperatorCallbacks) { auto shapeSet = readShapeSetFromString(R"( @@ -1948,49 +1989,6 @@ TEST(IOTest, readShapeSet_luaRotationCallbacksAreValidated) } } -TEST(IOTest, readShapeSet_luaDimensionDependentCallback) -{ - auto shapeSet = readShapeSetFromString(R"( - local dim = 2 - local r = 4.0 - local z = 8.0 - local x = 1.0 - local y = 2.0 - - dimensions = dim - - shapes = { - { - name = "part", - material = "steel", - geometry = { - format = "stl", - path = "part.stl", - units = "cm", - operators = { - { - translate = function() - if dim == 2 then - return {r, z} - end - return {x, y, z} - end - } - } - } - } - } - )", - InputFormat::Lua); - - auto composite = std::dynamic_pointer_cast( - shapeSet.getShapes()[0].getGeometry().getGeometryOperator()); - ASSERT_TRUE(composite); - auto translation = dynamic_cast(composite->getOperators()[0].get()); - ASSERT_NE(translation, nullptr); - EXPECT_THAT(translation->getOffset(), AlmostEqVector(Vector3D {4, 8, 0})); -} - TEST(IOTest, readShapeSet_luaCallbackErrorIncludesContext) { try @@ -2136,165 +2134,6 @@ TEST(IOTest, readShapeSet_luaNestedUnexpectedFieldsMatchYamlValidation) EXPECT_EQ("wheel", shapeSet.getShapes()[0].getName()); } -TEST(IOTest, readShapeSet_luaIntegratedWorkflowSmoke) -{ - auto shapeSet = readShapeSetFromString(R"( - local dim = 2 - local scale_factor = 1.25 - local lift = 3.0 - - local function point(x, y, z) - if dim == 2 then - return {x, y} - end - return {x, y, z or 0.0} - end - - local function lift_by(amount) - return function() - return point(0.0, amount) - end - end - - local function scale_callback() - return {scale_factor} - end - - dimensions = dim - shapes = { - { - name = "generated", - material = "steel", - geometry = { - format = "mfem", - path = "generated.mesh", - units = "cm", - operators = { - { scale = scale_callback }, - { translate = lift_by(lift) } - } - } - } - } - )", - InputFormat::Lua); - - ASSERT_EQ(1u, shapeSet.getShapes().size()); - const auto &geometry = shapeSet.getShapes()[0].getGeometry(); - auto composite = std::dynamic_pointer_cast(geometry.getGeometryOperator()); - ASSERT_TRUE(composite); - ASSERT_EQ(2u, composite->getOperators().size()); - - auto scale = std::dynamic_pointer_cast(composite->getOperators()[0]); - ASSERT_TRUE(scale); - EXPECT_DOUBLE_EQ(1.25, scale->getXFactor()); - EXPECT_DOUBLE_EQ(1.25, scale->getYFactor()); - EXPECT_DOUBLE_EQ(1.25, scale->getZFactor()); - - auto translation = std::dynamic_pointer_cast(composite->getOperators()[1]); - ASSERT_TRUE(translation); - EXPECT_THAT(translation->getOffset(), AlmostEqVector(Vector3D {0.0, 3.0, 0.0})); -} - -TEST(IOTest, readShapeSet_luaParseSmokeMatchesYaml) -{ - const std::string yaml = R"( - dimensions: 3 - shapes: - - name: one - material: steel - geometry: - format: stl - path: one.stl - units: cm - operators: - - rotate: 20 - axis: [0, 0, 1] - center: [1, 2, 3] - - translate: [4, 5, 6] - - name: two - material: glass - replaces: [steel] - geometry: - format: stl - path: two.stl - units: cm - operators: - - scale: [1.5, 2.0, 2.5] - center: [0, 0, 0] - )"; - - const std::string lua = R"( - local angle = 20 - local axis = {0, 0, 1} - local center = {1, 2, 3} - dimensions = 3 - shapes = { - { - name = "one", - material = "steel", - geometry = { - format = "stl", - path = "one.stl", - units = "cm", - operators = { - { rotate = angle, axis = axis, center = center }, - { translate = {4, 5, 6} } - } - } - }, - { - name = "two", - material = "glass", - replaces = {"steel"}, - geometry = { - format = "stl", - path = "two.stl", - units = "cm", - operators = { - { scale = {1.5, 2.0, 2.5}, center = {0, 0, 0} } - } - } - } - } - )"; - - auto yamlShapeSet = readShapeSetFromString(yaml, InputFormat::YAML); - auto luaShapeSet = readShapeSetFromString(lua, InputFormat::Lua); - - ASSERT_EQ(Dimensions::Three, yamlShapeSet.getDimensions()); - ASSERT_EQ(yamlShapeSet.getDimensions(), luaShapeSet.getDimensions()); - ASSERT_EQ(2u, yamlShapeSet.getShapes().size()); - ASSERT_EQ(yamlShapeSet.getShapes().size(), luaShapeSet.getShapes().size()); - - const auto &yamlFirstShape = yamlShapeSet.getShapes()[0]; - const auto &luaFirstShape = luaShapeSet.getShapes()[0]; - EXPECT_EQ(yamlFirstShape.getName(), luaFirstShape.getName()); - EXPECT_EQ(yamlFirstShape.getMaterial(), luaFirstShape.getMaterial()); - EXPECT_EQ(yamlFirstShape.getGeometry().getPath(), luaFirstShape.getGeometry().getPath()); - - const auto &yamlFirstOperator = yamlFirstShape.getGeometry().getGeometryOperator(); - const auto &luaFirstOperator = luaFirstShape.getGeometry().getGeometryOperator(); - auto yamlFirstComposite = std::dynamic_pointer_cast(yamlFirstOperator); - auto luaFirstComposite = std::dynamic_pointer_cast(luaFirstOperator); - ASSERT_TRUE(yamlFirstComposite); - ASSERT_TRUE(luaFirstComposite); - ASSERT_EQ(yamlFirstComposite->getOperators().size(), luaFirstComposite->getOperators().size()); - - const auto &yamlSecondShape = yamlShapeSet.getShapes()[1]; - const auto &luaSecondShape = luaShapeSet.getShapes()[1]; - EXPECT_EQ(yamlSecondShape.getName(), luaSecondShape.getName()); - EXPECT_EQ(yamlSecondShape.getMaterial(), luaSecondShape.getMaterial()); - EXPECT_TRUE(luaSecondShape.replaces("steel")); - - const auto &luaSecondOperator = luaSecondShape.getGeometry().getGeometryOperator(); - auto luaSecondComposite = std::dynamic_pointer_cast(luaSecondOperator); - ASSERT_TRUE(luaSecondComposite); - ASSERT_EQ(1u, luaSecondComposite->getOperators().size()); - EXPECT_TRUE(std::dynamic_pointer_cast(luaSecondComposite->getOperators()[0])); -} -#endif - TEST(IOTest, readShapeSet_shapeWithReplacesAndDoesNotReplaceLists) { auto input = R"( From bc2bad0a4eb08bb4a1c1372d877915b1626a7c28 Mon Sep 17 00:00:00 2001 From: Kenneth Weiss Date: Mon, 24 Aug 2026 20:12:31 -0700 Subject: [PATCH 51/52] Avoids race condition in shaping tutorial tests when writing shaping.root --- src/examples/CMakeLists.txt | 31 +++++++++--- src/examples/shaping_tutorial/CMakeLists.txt | 51 +++++++++++++------- 2 files changed, 56 insertions(+), 26 deletions(-) diff --git a/src/examples/CMakeLists.txt b/src/examples/CMakeLists.txt index b35839584a..ab6f91a150 100644 --- a/src/examples/CMakeLists.txt +++ b/src/examples/CMakeLists.txt @@ -210,24 +210,39 @@ if(AXOM_ENABLE_TUTORIALS AND AXOM_ENABLE_KLEE AND AXOM_ENABLE_QUEST) ${CMAKE_CURRENT_SOURCE_DIR}/shaping_tutorial/lesson_03/ice_cream_initialization.lua) if(MFEM_FOUND) - set(_lesson_04_dir - "${CMAKE_CURRENT_SOURCE_DIR}/shaping_tutorial/lesson_04") + set(_lesson_04_dir "${CMAKE_CURRENT_SOURCE_DIR}/shaping_tutorial/lesson_04") - blt_add_test(NAME shaping_tutorial_lesson_04_quest_sampling_shaper_yaml + set(_lesson_04_yaml_test shaping_tutorial_lesson_04_quest_sampling_shaper_yaml) + set(_lesson_04_lua_callbacks_test shaping_tutorial_lesson_04_quest_sampling_shaper_lua_callbacks) + set(_lesson_04_lua_initialization_test shaping_tutorial_lesson_04_quest_sampling_shaper_lua_initialization) + foreach(_testname + ${_lesson_04_yaml_test} + ${_lesson_04_lua_callbacks_test} + ${_lesson_04_lua_initialization_test}) + file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/${_testname}") + endforeach() + + blt_add_test(NAME ${_lesson_04_yaml_test} COMMAND shaping_tutorial_lesson_04_quest_sampling_shaper -m ${_lesson_04_dir}/circle_input.lua - -k ${_lesson_04_dir}/circles.yaml) + -k ${_lesson_04_dir}/circles.yaml + WORKING_DIRECTORY + ${CMAKE_CURRENT_BINARY_DIR}/${_lesson_04_yaml_test}) - blt_add_test(NAME shaping_tutorial_lesson_04_quest_sampling_shaper_lua_callbacks + blt_add_test(NAME ${_lesson_04_lua_callbacks_test} COMMAND shaping_tutorial_lesson_04_quest_sampling_shaper -m ${_lesson_04_dir}/circle_input.lua - -k ${_lesson_04_dir}/circles.lua) + -k ${_lesson_04_dir}/circles.lua + WORKING_DIRECTORY + ${CMAKE_CURRENT_BINARY_DIR}/${_lesson_04_lua_callbacks_test}) - blt_add_test(NAME shaping_tutorial_lesson_04_quest_sampling_shaper_lua_initialization + blt_add_test(NAME ${_lesson_04_lua_initialization_test} COMMAND shaping_tutorial_lesson_04_quest_sampling_shaper -m ${_lesson_04_dir}/circle_input.lua -k ${_lesson_04_dir}/circles_initialized.lua - --lua-init-file ${_lesson_04_dir}/circles_initialization.lua) + --lua-init-file ${_lesson_04_dir}/circles_initialization.lua + WORKING_DIRECTORY + ${CMAKE_CURRENT_BINARY_DIR}/${_lesson_04_lua_initialization_test}) set(_testname shaping_tutorial_lesson_04_quest_sampling_shaper_yaml_rejects_lua_initialization) diff --git a/src/examples/shaping_tutorial/CMakeLists.txt b/src/examples/shaping_tutorial/CMakeLists.txt index c7d3691d5d..b959eb2c6f 100644 --- a/src/examples/shaping_tutorial/CMakeLists.txt +++ b/src/examples/shaping_tutorial/CMakeLists.txt @@ -116,30 +116,46 @@ if(ENABLE_TESTS) endif() if(AXOM_USE_LUA AND AXOM_USE_MFEM) - blt_add_test(NAME lesson_04_quest_sampling_shaper - COMMAND lesson_04_quest_sampling_shaper -m ../lesson_04/circle_input.lua - -k ../lesson_04/circles.yaml -v) + set(_lesson_04_dir "${CMAKE_CURRENT_SOURCE_DIR}/lesson_04") + set(_lesson_04_yaml_test lesson_04_quest_sampling_shaper) + set(_lesson_04_lua_callbacks_test lesson_04_quest_sampling_shaper_lua_callbacks) + set(_lesson_04_lua_initialization_test lesson_04_quest_sampling_shaper_lua_initialization) + + foreach(_testname + ${_lesson_04_yaml_test} + ${_lesson_04_lua_callbacks_test} + ${_lesson_04_lua_initialization_test}) + file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/${_testname}") + endforeach() + + blt_add_test(NAME ${_lesson_04_yaml_test} + COMMAND lesson_04_quest_sampling_shaper + -m ${_lesson_04_dir}/circle_input.lua + -k ${_lesson_04_dir}/circles.yaml -v + WORKING_DIRECTORY + ${CMAKE_CURRENT_BINARY_DIR}/${_lesson_04_yaml_test}) - blt_add_test(NAME lesson_04_quest_sampling_shaper_lua_callbacks + blt_add_test(NAME ${_lesson_04_lua_callbacks_test} COMMAND lesson_04_quest_sampling_shaper - -m ../lesson_04/circle_input.lua - -k ../lesson_04/circles.lua - -v) + -m ${_lesson_04_dir}/circle_input.lua + -k ${_lesson_04_dir}/circles.lua -v + WORKING_DIRECTORY + ${CMAKE_CURRENT_BINARY_DIR}/${_lesson_04_lua_callbacks_test}) - blt_add_test(NAME lesson_04_quest_sampling_shaper_lua_initialization + blt_add_test(NAME ${_lesson_04_lua_initialization_test} COMMAND lesson_04_quest_sampling_shaper - -m ../lesson_04/circle_input.lua - -k ../lesson_04/circles_initialized.lua - --lua-init-file ../lesson_04/circles_initialization.lua - -v) + -m ${_lesson_04_dir}/circle_input.lua + -k ${_lesson_04_dir}/circles_initialized.lua + --lua-init-file ${_lesson_04_dir}/circles_initialization.lua -v + WORKING_DIRECTORY + ${CMAKE_CURRENT_BINARY_DIR}/${_lesson_04_lua_initialization_test}) - set(_testname - lesson_04_quest_sampling_shaper_yaml_rejects_lua_initialization) + set(_testname lesson_04_quest_sampling_shaper_yaml_rejects_lua_initialization) blt_add_test(NAME ${_testname} COMMAND lesson_04_quest_sampling_shaper - -m ../lesson_04/circle_input.lua - -k ../lesson_04/circles.yaml - --lua-init-file ../lesson_04/circles_initialization.lua) + -m ${_lesson_04_dir}/circle_input.lua + -k ${_lesson_04_dir}/circles.yaml + --lua-init-file ${_lesson_04_dir}/circles_initialization.lua) set_tests_properties(${_testname} PROPERTIES PASS_REGULAR_EXPRESSION "only supported for Lua input decks") endif() @@ -152,4 +168,3 @@ endif() if(EXAMPLE_VERBOSE_OUTPUT) blt_print_target_properties(TARGET axom CHILDREN TRUE) endif() - From b3663e1c3fbda90f3580877605c9e89fc9a3d409 Mon Sep 17 00:00:00 2001 From: format-robot Date: Wed, 26 Aug 2026 22:32:18 -0700 Subject: [PATCH 52/52] Formats code --- src/axom/inlet/Container.cpp | 16 +- src/axom/inlet/LuaReader.cpp | 14 +- src/axom/inlet/SphinxWriter.cpp | 672 +++++++++--------- src/axom/inlet/inlet_utils.hpp | 92 +-- src/axom/inlet/tests/inlet_Reader.cpp | 3 +- src/axom/inlet/tests/inlet_function.cpp | 6 +- src/axom/klee/Units.hpp | 2 +- src/axom/klee/io/GeometryOperatorsIO.cpp | 320 ++++----- src/axom/klee/io/GeometryOperatorsIO.hpp | 20 +- src/axom/klee/io/IO.cpp | 141 ++-- src/axom/klee/io/IO.hpp | 10 +- .../klee/tests/klee_geometry_operators_io.cpp | 8 +- src/axom/klee/tests/klee_io.cpp | 126 ++-- src/axom/quest/util/make_clipper_strategy.cpp | 10 +- .../klee_operators_and_validation.cpp | 9 +- 15 files changed, 676 insertions(+), 773 deletions(-) diff --git a/src/axom/inlet/Container.cpp b/src/axom/inlet/Container.cpp index 2dd7ded389..1c98d3b959 100644 --- a/src/axom/inlet/Container.cpp +++ b/src/axom/inlet/Container.cpp @@ -376,11 +376,8 @@ VerifiableScalar& Container::addPrimitive(const std::string& name, lookupPath = utilities::string::removeAllInstances(lookupPath, detail::COLLECTION_GROUP_NAME + "/"); detail::updateUnexpectedNames(lookupPath, m_unexpectedNames); - auto typeId = addPrimitiveHelper(sidreGroup, - lookupPath, - forArray, - val, - containsFunctionValueAlternative(name)); + auto typeId = + addPrimitiveHelper(sidreGroup, lookupPath, forArray, val, containsFunctionValueAlternative(name)); return addField(sidreGroup, typeId, fullName, name); } } @@ -938,11 +935,10 @@ Verifiable& Container::addFunction(const std::string& name, return addFunctionInternal(name, name, ret_type, arg_types, description, pathOverride); } -Verifiable& Container::addFunctionAsValueAlternative( - const std::string& valueName, - const FunctionTag ret_type, - const std::vector& arg_types, - const std::string& description) +Verifiable& Container::addFunctionAsValueAlternative(const std::string& valueName, + const FunctionTag ret_type, + const std::vector& arg_types, + const std::string& description) { SLIC_ERROR_IF(valueName.empty(), "[Inlet] A function value alternative requires a non-empty value name"); diff --git a/src/axom/inlet/LuaReader.cpp b/src/axom/inlet/LuaReader.cpp index f810042db1..0353f1b026 100644 --- a/src/axom/inlet/LuaReader.cpp +++ b/src/axom/inlet/LuaReader.cpp @@ -442,8 +442,7 @@ FunctionType::Vector extractResult(axom::sol::protected_fu { if(entry.first.get_type() != axom::sol::type::number) { - throw InletError( - "[Inlet] Lua vector function return must only contain numeric indices"); + throw InletError("[Inlet] Lua vector function return must only contain numeric indices"); } const double numeric_index = entry.first.as(); @@ -455,8 +454,7 @@ FunctionType::Vector extractResult(axom::sol::protected_fu } if(entry.second.get_type() != axom::sol::type::number) { - throw InletError( - "[Inlet] Lua vector function return components must be numeric"); + throw InletError("[Inlet] Lua vector function return components must be numeric"); } values[index - 1] = entry.second.as(); @@ -466,10 +464,10 @@ FunctionType::Vector extractResult(axom::sol::protected_fu if(count < 1 || count > 3) { - throw InletError(fmt::format( - "[Inlet] Lua vector function returned a table with {0} entries; " - "expected 1 to 3 numeric entries", - count)); + throw InletError( + fmt::format("[Inlet] Lua vector function returned a table with {0} entries; " + "expected 1 to 3 numeric entries", + count)); } for(int i = 0; i < count; ++i) { diff --git a/src/axom/inlet/SphinxWriter.cpp b/src/axom/inlet/SphinxWriter.cpp index 75a4c83572..471f9d5e30 100644 --- a/src/axom/inlet/SphinxWriter.cpp +++ b/src/axom/inlet/SphinxWriter.cpp @@ -47,339 +47,339 @@ bool isTrivial(const Container& container) * \param [in] e The enumeration value to convert * This function should be removed once C++23 is available * \see https://en.cppreference.com/w/cpp/utility/to_underlying - */ -template -constexpr typename std::underlying_type::type to_underlying(const E e) -{ - return static_cast::type>(e); -} - -} // namespace detail - -SphinxWriter::SphinxWriter(const std::string& fileName) - : m_fieldColLabels( - {"Field Name", "Description", "Default Value", "Range/Valid Values", "Required"}) - , m_functionColLabels({"Function Name", "Description", "Signature", "Required"}) -{ - m_fileName = fileName; - m_oss << ".. |uncheck| unicode:: U+2610 .. UNCHECKED BOX\n"; - m_oss << ".. |check| unicode:: U+2611 .. CHECKED BOX\n\n"; - writeTitle("Input file Options"); -} - -void SphinxWriter::documentContainer(const Container& container) -{ - const auto sidreGroup = container.sidreGroup(); - const std::string pathName = sidreGroup->getPathName(); - std::string containerName = sidreGroup->getName(); - bool isSelectedElement = false; - - // If the container is empty, ignore it - if(detail::isTrivial(container)) - { - return; - } - - // Replace the "implementation-defined" name with something a bit more readable - if(isCollectionGroup(containerName)) - { - containerName = "Collection contents:"; - } - - // If we've gotten to this point and are an element of an array/dict, - // mark it as the selected element - if(sidreGroup->getParent()->getName() == detail::COLLECTION_GROUP_NAME) - { - // The collection that this Container is a part of - const std::string collectionName = sidreGroup->getParent()->getParent()->getPathName(); - isSelectedElement = true; - } - - m_inletContainerPathNames.push_back(pathName); - auto& currContainer = - m_rstTables.emplace(pathName, ContainerData {m_fieldColLabels, m_functionColLabels}).first->second; - currContainer.containerName = containerName; - currContainer.isSelectedElement = isSelectedElement; - if(containerName != "" && sidreGroup->hasView("description")) - { - currContainer.description = sidreGroup->getView("description")->getString(); - } - - for(const auto& field_entry : container.getChildFields()) - { - extractFieldMetadata(field_entry.second->sidreGroup(), currContainer); - } - - for(const auto& function_entry : container.getChildFunctions()) - { - // A function value alternative is documented through the concrete entry - // that shares its input path, not under its internal schema name - if(detail::isFunctionAlternativeName(Path(function_entry.first).baseName())) - { - continue; - } - extractFunctionMetadata(function_entry.second->sidreGroup(), currContainer); - } -} - -void SphinxWriter::finalize() -{ - writeAllTables(); - m_outFile.open(m_fileName); - m_outFile << m_oss.str(); - m_outFile.close(); -} - -void SphinxWriter::writeTitle(const std::string& title) -{ - if(title != "") - { - std::string equals = std::string(title.length(), '='); - m_oss << equals << "\n" << title << "\n" << equals << "\n"; - } -} - -void SphinxWriter::writeSubtitle(const std::string& sub) -{ - if(sub != "") - { - std::string dashes = std::string(sub.length(), '-'); - m_oss << "\n" << dashes << "\n" << sub << "\n" << dashes << "\n\n"; - } -} - -void SphinxWriter::writeTable(const std::string& title, - const std::vector>& rstTable) -{ - SLIC_WARNING_IF(rstTable.size() <= 1, - "[Inlet] Vector for corresponding rst table must be nonempty"); - std::string result = ".. list-table:: " + title; - std::string widths = ":widths:"; - // This would be easier with an iterator adaptor like back_inserter but for - // concatenation - for(std::size_t i = 0u; i < rstTable.front().size(); i++) - { - widths += " 25"; - } - result += "\n " + widths + "\n"; - result += " :header-rows: 1\n :stub-columns: 1\n\n"; - for(unsigned int i = 0; i < rstTable.size(); ++i) - { - result += " * - "; - for(unsigned int j = 0; j < rstTable[i].size(); ++j) - { - if(j != 0) - { - result += " - "; - } - result += rstTable[i][j] + "\n"; - } - } - m_oss << result; -} - -void SphinxWriter::writeAllTables() -{ - for(std::string& pathName : m_inletContainerPathNames) - { - auto& currContainer = m_rstTables.at(pathName); - // If we're displaying a selected element, the title and description - // will already have been printed - if(currContainer.isSelectedElement) - { - m_oss << "The input schema defines a collection of this container.\n"; - m_oss << "For brevity, only one instance is displayed here.\n\n"; - } - else - { - writeSubtitle(currContainer.containerName); - if(currContainer.description != "") - { - m_oss << "Description: " << currContainer.description << "\n\n"; - } - } - if(currContainer.fieldTable.size() > 1) - { - writeTable("Fields", currContainer.fieldTable); - } - if(currContainer.functionTable.size() > 1) - { - writeTable("Functions", currContainer.functionTable); - } - } -} - -std::string SphinxWriter::getValueAsString(const axom::sidre::View* view) -{ - axom::sidre::TypeID type = view->getTypeID(); - if(type == axom::sidre::TypeID::INT8_ID) - { - std::int8_t val = view->getData(); - return val ? "True" : "False"; - } - else if(type == axom::sidre::TypeID::INT_ID) - { - int val = view->getData(); - return std::to_string(val); - } - else if(type == axom::sidre::TypeID::DOUBLE_ID) - { - double val = view->getData(); - return std::to_string(val); - } - return view->getString(); -} - -std::string SphinxWriter::getRangeAsString(const axom::sidre::View* view) -{ - std::ostringstream oss; - oss.precision(3); - oss << std::scientific; - - axom::sidre::TypeID type = view->getTypeID(); - if(type == axom::sidre::INT_ID) - { - const int* range = view->getData(); - oss << range[0] << " to " << range[1]; - } - else - { - const double* range = view->getData(); - oss << range[0] << " to " << range[1]; - } - return oss.str(); -} - -std::string SphinxWriter::getValidValuesAsString(const axom::sidre::View* view) -{ - const int* range = view->getData(); - size_t size = view->getBuffer()->getNumElements(); - std::string result = ""; - for(size_t i = 0; i < size; ++i) - { - if(i == size - 1) - { - result += std::to_string(range[i]); - } - else - { - result += std::to_string(range[i]) + ", "; - } - } - return result; -} - -std::string SphinxWriter::getValidStringValues(const axom::sidre::Group* sidreGroup) -{ - auto idx = sidreGroup->getFirstValidViewIndex(); - std::string validValues = ""; - while(axom::sidre::indexIsValid(idx)) - { - validValues += std::string(sidreGroup->getView(idx)->getString()); - idx = sidreGroup->getNextValidViewIndex(idx); - if(axom::sidre::indexIsValid(idx)) - { - validValues += ", "; - } - } - return validValues; -} - -void SphinxWriter::extractFieldMetadata(const axom::sidre::Group* sidreGroup, - ContainerData& currentContainer) -{ - std::vector fieldAttributes(m_fieldColLabels.size()); - - fieldAttributes[0] = sidreGroup->getName(); - - if(sidreGroup->hasView("description")) - { - fieldAttributes[1] = std::string(sidreGroup->getView("description")->getString()); - } - - if(sidreGroup->hasView("defaultValue")) - { - fieldAttributes[2] = getValueAsString(sidreGroup->getView("defaultValue")); - } - - if(sidreGroup->hasView("range")) - { - fieldAttributes[3] = getRangeAsString(sidreGroup->getView("range")); - } - else if(sidreGroup->hasView("validValues")) - { - fieldAttributes[3] = getValidValuesAsString(sidreGroup->getView("validValues")); - } - else if(sidreGroup->hasGroup("validStringValues")) - { - fieldAttributes[3] = getValidStringValues(sidreGroup->getGroup("validStringValues")); - } - - if(sidreGroup->hasView("required")) - { - std::int8_t required = sidreGroup->getView("required")->getData(); - fieldAttributes[4] = required ? "|check|" : "|uncheck|"; - } - else - { - fieldAttributes[4] = "|uncheck|"; - } - - currentContainer.fieldTable.push_back(fieldAttributes); -} - -std::string SphinxWriter::getSignatureAsString(const axom::sidre::Group* sidreGroup) -{ - using underlying = std::underlying_type::type; - static const auto type_names = []() { - std::unordered_map result; - result[detail::to_underlying(FunctionTag::Vector)] = "Vector"; - result[detail::to_underlying(FunctionTag::Double)] = "Double"; - result[detail::to_underlying(FunctionTag::Void)] = "Void"; - result[detail::to_underlying(FunctionTag::String)] = "String"; - return result; - }(); - - // View::getData does not have a const version... - const auto ret_type = static_cast(sidreGroup->getView("return_type")->getData()); - - const auto args_view = sidreGroup->getView("function_arguments"); - const underlying* arg_tags = args_view->getData(); - const int num_args = args_view->getNumElements(); - std::vector arg_types(num_args); - for(int i = 0; i < num_args; i++) - { - arg_types[i] = type_names.at(arg_tags[i]); - } - return fmt::format("{0}({1})", type_names.at(ret_type), fmt::join(arg_types, ", ")); -} - -void SphinxWriter::extractFunctionMetadata(const axom::sidre::Group* sidreGroup, - ContainerData& currentContainer) -{ - std::vector functionAttributes(m_functionColLabels.size()); - - functionAttributes[0] = sidreGroup->getName(); - - if(sidreGroup->hasView("description")) - { - functionAttributes[1] = std::string(sidreGroup->getView("description")->getString()); - } - - functionAttributes[2] = getSignatureAsString(sidreGroup); - - if(sidreGroup->hasView("required")) - { - std::int8_t required = sidreGroup->getView("required")->getData(); - functionAttributes[3] = required ? "|check|" : "|uncheck|"; - } - else - { - functionAttributes[3] = "|uncheck|"; - } - - currentContainer.functionTable.push_back(functionAttributes); -} - -} // namespace inlet -} // namespace axom + */ +template +constexpr typename std::underlying_type::type to_underlying(const E e) +{ + return static_cast::type>(e); +} + +} // namespace detail + +SphinxWriter::SphinxWriter(const std::string& fileName) + : m_fieldColLabels( + {"Field Name", "Description", "Default Value", "Range/Valid Values", "Required"}) + , m_functionColLabels({"Function Name", "Description", "Signature", "Required"}) +{ + m_fileName = fileName; + m_oss << ".. |uncheck| unicode:: U+2610 .. UNCHECKED BOX\n"; + m_oss << ".. |check| unicode:: U+2611 .. CHECKED BOX\n\n"; + writeTitle("Input file Options"); +} + +void SphinxWriter::documentContainer(const Container& container) +{ + const auto sidreGroup = container.sidreGroup(); + const std::string pathName = sidreGroup->getPathName(); + std::string containerName = sidreGroup->getName(); + bool isSelectedElement = false; + + // If the container is empty, ignore it + if(detail::isTrivial(container)) + { + return; + } + + // Replace the "implementation-defined" name with something a bit more readable + if(isCollectionGroup(containerName)) + { + containerName = "Collection contents:"; + } + + // If we've gotten to this point and are an element of an array/dict, + // mark it as the selected element + if(sidreGroup->getParent()->getName() == detail::COLLECTION_GROUP_NAME) + { + // The collection that this Container is a part of + const std::string collectionName = sidreGroup->getParent()->getParent()->getPathName(); + isSelectedElement = true; + } + + m_inletContainerPathNames.push_back(pathName); + auto& currContainer = + m_rstTables.emplace(pathName, ContainerData {m_fieldColLabels, m_functionColLabels}).first->second; + currContainer.containerName = containerName; + currContainer.isSelectedElement = isSelectedElement; + if(containerName != "" && sidreGroup->hasView("description")) + { + currContainer.description = sidreGroup->getView("description")->getString(); + } + + for(const auto& field_entry : container.getChildFields()) + { + extractFieldMetadata(field_entry.second->sidreGroup(), currContainer); + } + + for(const auto& function_entry : container.getChildFunctions()) + { + // A function value alternative is documented through the concrete entry + // that shares its input path, not under its internal schema name + if(detail::isFunctionAlternativeName(Path(function_entry.first).baseName())) + { + continue; + } + extractFunctionMetadata(function_entry.second->sidreGroup(), currContainer); + } +} + +void SphinxWriter::finalize() +{ + writeAllTables(); + m_outFile.open(m_fileName); + m_outFile << m_oss.str(); + m_outFile.close(); +} + +void SphinxWriter::writeTitle(const std::string& title) +{ + if(title != "") + { + std::string equals = std::string(title.length(), '='); + m_oss << equals << "\n" << title << "\n" << equals << "\n"; + } +} + +void SphinxWriter::writeSubtitle(const std::string& sub) +{ + if(sub != "") + { + std::string dashes = std::string(sub.length(), '-'); + m_oss << "\n" << dashes << "\n" << sub << "\n" << dashes << "\n\n"; + } +} + +void SphinxWriter::writeTable(const std::string& title, + const std::vector>& rstTable) +{ + SLIC_WARNING_IF(rstTable.size() <= 1, + "[Inlet] Vector for corresponding rst table must be nonempty"); + std::string result = ".. list-table:: " + title; + std::string widths = ":widths:"; + // This would be easier with an iterator adaptor like back_inserter but for + // concatenation + for(std::size_t i = 0u; i < rstTable.front().size(); i++) + { + widths += " 25"; + } + result += "\n " + widths + "\n"; + result += " :header-rows: 1\n :stub-columns: 1\n\n"; + for(unsigned int i = 0; i < rstTable.size(); ++i) + { + result += " * - "; + for(unsigned int j = 0; j < rstTable[i].size(); ++j) + { + if(j != 0) + { + result += " - "; + } + result += rstTable[i][j] + "\n"; + } + } + m_oss << result; +} + +void SphinxWriter::writeAllTables() +{ + for(std::string& pathName : m_inletContainerPathNames) + { + auto& currContainer = m_rstTables.at(pathName); + // If we're displaying a selected element, the title and description + // will already have been printed + if(currContainer.isSelectedElement) + { + m_oss << "The input schema defines a collection of this container.\n"; + m_oss << "For brevity, only one instance is displayed here.\n\n"; + } + else + { + writeSubtitle(currContainer.containerName); + if(currContainer.description != "") + { + m_oss << "Description: " << currContainer.description << "\n\n"; + } + } + if(currContainer.fieldTable.size() > 1) + { + writeTable("Fields", currContainer.fieldTable); + } + if(currContainer.functionTable.size() > 1) + { + writeTable("Functions", currContainer.functionTable); + } + } +} + +std::string SphinxWriter::getValueAsString(const axom::sidre::View* view) +{ + axom::sidre::TypeID type = view->getTypeID(); + if(type == axom::sidre::TypeID::INT8_ID) + { + std::int8_t val = view->getData(); + return val ? "True" : "False"; + } + else if(type == axom::sidre::TypeID::INT_ID) + { + int val = view->getData(); + return std::to_string(val); + } + else if(type == axom::sidre::TypeID::DOUBLE_ID) + { + double val = view->getData(); + return std::to_string(val); + } + return view->getString(); +} + +std::string SphinxWriter::getRangeAsString(const axom::sidre::View* view) +{ + std::ostringstream oss; + oss.precision(3); + oss << std::scientific; + + axom::sidre::TypeID type = view->getTypeID(); + if(type == axom::sidre::INT_ID) + { + const int* range = view->getData(); + oss << range[0] << " to " << range[1]; + } + else + { + const double* range = view->getData(); + oss << range[0] << " to " << range[1]; + } + return oss.str(); +} + +std::string SphinxWriter::getValidValuesAsString(const axom::sidre::View* view) +{ + const int* range = view->getData(); + size_t size = view->getBuffer()->getNumElements(); + std::string result = ""; + for(size_t i = 0; i < size; ++i) + { + if(i == size - 1) + { + result += std::to_string(range[i]); + } + else + { + result += std::to_string(range[i]) + ", "; + } + } + return result; +} + +std::string SphinxWriter::getValidStringValues(const axom::sidre::Group* sidreGroup) +{ + auto idx = sidreGroup->getFirstValidViewIndex(); + std::string validValues = ""; + while(axom::sidre::indexIsValid(idx)) + { + validValues += std::string(sidreGroup->getView(idx)->getString()); + idx = sidreGroup->getNextValidViewIndex(idx); + if(axom::sidre::indexIsValid(idx)) + { + validValues += ", "; + } + } + return validValues; +} + +void SphinxWriter::extractFieldMetadata(const axom::sidre::Group* sidreGroup, + ContainerData& currentContainer) +{ + std::vector fieldAttributes(m_fieldColLabels.size()); + + fieldAttributes[0] = sidreGroup->getName(); + + if(sidreGroup->hasView("description")) + { + fieldAttributes[1] = std::string(sidreGroup->getView("description")->getString()); + } + + if(sidreGroup->hasView("defaultValue")) + { + fieldAttributes[2] = getValueAsString(sidreGroup->getView("defaultValue")); + } + + if(sidreGroup->hasView("range")) + { + fieldAttributes[3] = getRangeAsString(sidreGroup->getView("range")); + } + else if(sidreGroup->hasView("validValues")) + { + fieldAttributes[3] = getValidValuesAsString(sidreGroup->getView("validValues")); + } + else if(sidreGroup->hasGroup("validStringValues")) + { + fieldAttributes[3] = getValidStringValues(sidreGroup->getGroup("validStringValues")); + } + + if(sidreGroup->hasView("required")) + { + std::int8_t required = sidreGroup->getView("required")->getData(); + fieldAttributes[4] = required ? "|check|" : "|uncheck|"; + } + else + { + fieldAttributes[4] = "|uncheck|"; + } + + currentContainer.fieldTable.push_back(fieldAttributes); +} + +std::string SphinxWriter::getSignatureAsString(const axom::sidre::Group* sidreGroup) +{ + using underlying = std::underlying_type::type; + static const auto type_names = []() { + std::unordered_map result; + result[detail::to_underlying(FunctionTag::Vector)] = "Vector"; + result[detail::to_underlying(FunctionTag::Double)] = "Double"; + result[detail::to_underlying(FunctionTag::Void)] = "Void"; + result[detail::to_underlying(FunctionTag::String)] = "String"; + return result; + }(); + + // View::getData does not have a const version... + const auto ret_type = static_cast(sidreGroup->getView("return_type")->getData()); + + const auto args_view = sidreGroup->getView("function_arguments"); + const underlying* arg_tags = args_view->getData(); + const int num_args = args_view->getNumElements(); + std::vector arg_types(num_args); + for(int i = 0; i < num_args; i++) + { + arg_types[i] = type_names.at(arg_tags[i]); + } + return fmt::format("{0}({1})", type_names.at(ret_type), fmt::join(arg_types, ", ")); +} + +void SphinxWriter::extractFunctionMetadata(const axom::sidre::Group* sidreGroup, + ContainerData& currentContainer) +{ + std::vector functionAttributes(m_functionColLabels.size()); + + functionAttributes[0] = sidreGroup->getName(); + + if(sidreGroup->hasView("description")) + { + functionAttributes[1] = std::string(sidreGroup->getView("description")->getString()); + } + + functionAttributes[2] = getSignatureAsString(sidreGroup); + + if(sidreGroup->hasView("required")) + { + std::int8_t required = sidreGroup->getView("required")->getData(); + functionAttributes[3] = required ? "|check|" : "|uncheck|"; + } + else + { + functionAttributes[3] = "|uncheck|"; + } + + currentContainer.functionTable.push_back(functionAttributes); +} + +} // namespace inlet +} // namespace axom diff --git a/src/axom/inlet/inlet_utils.hpp b/src/axom/inlet/inlet_utils.hpp index cf72f05b82..78f15e7028 100644 --- a/src/axom/inlet/inlet_utils.hpp +++ b/src/axom/inlet/inlet_utils.hpp @@ -1,32 +1,32 @@ -// Copyright (c) Lawrence Livermore National Security, LLC and other -// Axom Project Contributors. See top-level LICENSE and COPYRIGHT -// files for dates and other details. -// -// SPDX-License-Identifier: (BSD-3-Clause) - -#pragma once - -#include -#include -#include - -#include "axom/sidre.hpp" -#include "axom/fmt.hpp" -#include "axom/core/utilities/StringUtilities.hpp" -#include "axom/core/Path.hpp" - -namespace axom -{ -namespace inlet -{ -enum class ReaderResult -{ - Success, // Found with no issue - NotFound, // Path does not exist in the input file - NotHomogeneous, // Found, but elements of other type exist - WrongType // Found, but item at specified path was not of requested type -}; - +// Copyright (c) Lawrence Livermore National Security, LLC and other +// Axom Project Contributors. See top-level LICENSE and COPYRIGHT +// files for dates and other details. +// +// SPDX-License-Identifier: (BSD-3-Clause) + +#pragma once + +#include +#include +#include + +#include "axom/sidre.hpp" +#include "axom/fmt.hpp" +#include "axom/core/utilities/StringUtilities.hpp" +#include "axom/core/Path.hpp" + +namespace axom +{ +namespace inlet +{ +enum class ReaderResult +{ + Success, // Found with no issue + NotFound, // Path does not exist in the input file + NotHomogeneous, // Found, but elements of other type exist + WrongType // Found, but item at specified path was not of requested type +}; + /*! ***************************************************************************** * \brief Exception thrown when evaluating an input function fails @@ -38,12 +38,12 @@ enum class ReaderResult * occurs after verification and must be recoverable so the caller can report it * with its own context. Those failures throw this type. ***************************************************************************** - */ -struct InletError : public std::runtime_error -{ - using std::runtime_error::runtime_error; -}; - + */ +struct InletError : public std::runtime_error +{ + using std::runtime_error::runtime_error; +}; + /*! ***************************************************************************** * \brief Information on an Inlet verification error @@ -149,17 +149,17 @@ namespace detail * Names of the internal collection data and collection index groups/fields * used for managing arrays/dictionaries ******************************************************************************* - */ -const std::string COLLECTION_GROUP_NAME = "_inlet_collection"; -const std::string COLLECTION_INDICES_NAME = "_inlet_collection_indices"; -const std::string STRUCT_COLLECTION_FLAG = "_inlet_struct_collection"; -const std::string VARIANT_STRUCT_COLLECTION_FLAG = "_inlet_variant_struct_collection"; -const std::string FUNCTION_ALTERNATIVE_SUFFIX = "_inlet_function_alternative"; -const std::string REQUIRED_FLAG = "required"; -const std::string STRICT_FLAG = "strict"; - -} // namespace detail - + */ +const std::string COLLECTION_GROUP_NAME = "_inlet_collection"; +const std::string COLLECTION_INDICES_NAME = "_inlet_collection_indices"; +const std::string STRUCT_COLLECTION_FLAG = "_inlet_struct_collection"; +const std::string VARIANT_STRUCT_COLLECTION_FLAG = "_inlet_variant_struct_collection"; +const std::string FUNCTION_ALTERNATIVE_SUFFIX = "_inlet_function_alternative"; +const std::string REQUIRED_FLAG = "required"; +const std::string STRICT_FLAG = "strict"; + +} // namespace detail + /*! ***************************************************************************** * \brief Determines whether a Container is a collection group diff --git a/src/axom/inlet/tests/inlet_Reader.cpp b/src/axom/inlet/tests/inlet_Reader.cpp index ea0e20e58e..e49ee7fb0f 100644 --- a/src/axom/inlet/tests/inlet_Reader.cpp +++ b/src/axom/inlet/tests/inlet_Reader.cpp @@ -486,8 +486,7 @@ TEST(inlet_Reader_lua, objectLookupReportsConsistentReaderResults) EXPECT_EQ(ReaderResult::WrongType, reader.getDoubleMap("callback", typedValues)); EXPECT_TRUE(typedValues.empty()); - std::unordered_map values { - {99, axom::inlet::VariantValue {99}}}; + std::unordered_map values {{99, axom::inlet::VariantValue {99}}}; EXPECT_EQ(ReaderResult::WrongType, reader.getVariantMap("callback", values)); EXPECT_TRUE(values.empty()); EXPECT_EQ(ReaderResult::NotFound, reader.getVariantMap("missing", values)); diff --git a/src/axom/inlet/tests/inlet_function.cpp b/src/axom/inlet/tests/inlet_function.cpp index 24bf7288f7..fb56f472b5 100644 --- a/src/axom/inlet/tests/inlet_function.cpp +++ b/src/axom/inlet/tests/inlet_function.cpp @@ -135,8 +135,7 @@ TEST(inlet_function, lua_callback_failures_are_catchable) } catch(const axom::inlet::InletError& error) { - EXPECT_NE(std::string(error.what()).find("callback failed"), - std::string::npos); + EXPECT_NE(std::string(error.what()).find("callback failed"), std::string::npos); } } @@ -410,8 +409,7 @@ TEST(inlet_function, returned_function_keeps_lua_state_alive) // An extracted callback must retain its Lua state after Inlet is destroyed. std::function callback; { - auto inlet = createBasicInlet( - "offset = 3.0; function foo (value) return value + offset end"); + auto inlet = createBasicInlet("offset = 3.0; function foo (value) return value + offset end"); inlet.addFunction("foo", FunctionTag::Double, {FunctionTag::Double}); callback = inlet["foo"].get>(); } diff --git a/src/axom/klee/Units.hpp b/src/axom/klee/Units.hpp index e1f73e075b..8ad19745d4 100644 --- a/src/axom/klee/Units.hpp +++ b/src/axom/klee/Units.hpp @@ -33,7 +33,7 @@ namespace internal * \return A LengthUnit containing the unit type. * \throws KleeError if the unit string is invalid */ -LengthUnit parseLengthUnits(const std::string &unitsAsString, const std::string &path); +LengthUnit parseLengthUnits(const std::string& unitsAsString, const std::string& path); /*! * \brief This function parses a string and returns a LengthUnit. It is a compatibility diff --git a/src/axom/klee/io/GeometryOperatorsIO.cpp b/src/axom/klee/io/GeometryOperatorsIO.cpp index e19c5f8000..f5dd5f4845 100644 --- a/src/axom/klee/io/GeometryOperatorsIO.cpp +++ b/src/axom/klee/io/GeometryOperatorsIO.cpp @@ -30,9 +30,8 @@ namespace internal namespace { using OpPtr = CompositeOperator::OpPtr; -using OperatorParser = std::function; +using OperatorParser = + std::function; using internal::toDoubleVector; using primal::Point3D; using primal::Vector3D; @@ -55,7 +54,7 @@ std::string childName(const inlet::Container& container, const std::string& name * \param fieldName the public operator field name * \return true when \a fieldName has a supplied function alternative */ -bool hasCallback(const inlet::Container &container, char const *fieldName) +bool hasCallback(const inlet::Container& container, char const* fieldName) { return container.containsFunctionValueAlternative(fieldName); } @@ -67,7 +66,7 @@ bool hasCallback(const inlet::Container &container, char const *fieldName) * \param fieldName the public operator field name * \return true when either supported representation was supplied */ -bool containsFieldOrCallback(const inlet::Container &container, char const *fieldName) +bool containsFieldOrCallback(const inlet::Container& container, char const* fieldName) { return container.contains(fieldName) || hasCallback(container, fieldName); } @@ -79,7 +78,7 @@ bool containsFieldOrCallback(const inlet::Container &container, char const *fiel * \param fieldName the public operator field name * \return the full path to \a fieldName */ -Path fieldPath(const inlet::Container &container, char const *fieldName) +Path fieldPath(const inlet::Container& container, char const* fieldName) { return Path::join({Path {container.name()}, Path {std::string {fieldName}}}); } @@ -94,10 +93,10 @@ Path fieldPath(const inlet::Container &container, char const *fieldName) * \return \a message, prefixed with the callback, owner and operator when * \a fieldName was supplied as a callback, and unchanged otherwise */ -std::string fieldMessage(const inlet::Container &container, - char const *fieldName, - const std::string &ownerLabel, - const std::string &message) +std::string fieldMessage(const inlet::Container& container, + char const* fieldName, + const std::string& ownerLabel, + const std::string& message) { if(!hasCallback(container, fieldName)) { @@ -114,10 +113,7 @@ std::string fieldMessage(const inlet::Container &container, const auto operatorLabel = operatorIndex.empty() ? std::string {"operator at "} + container.name() : std::string {"operator "} + operatorIndex; const auto owner = ownerLabel.empty() ? operatorLabel : ownerLabel + " " + operatorLabel; - return axom::fmt::format("Error evaluating callback for '{}' in {}: {}", - fieldName, - owner, - message); + return axom::fmt::format("Error evaluating callback for '{}' in {}: {}", fieldName, owner, message); } /** @@ -133,10 +129,10 @@ std::string fieldMessage(const inlet::Container &container, * \throws KleeError if callback invocation or result conversion fails */ template -Result wrapCallbackErrors(const inlet::Container &container, - char const *fieldName, - const std::string &ownerLabel, - Func &&func) +Result wrapCallbackErrors(const inlet::Container& container, + char const* fieldName, + const std::string& ownerLabel, + Func&& func) { // Convert Inlet callback failures into Klee diagnostics at the boundary // where the shape, operator, and field context are all available. @@ -144,10 +140,10 @@ Result wrapCallbackErrors(const inlet::Container &container, { return func(); } - catch(const inlet::InletError &ex) + catch(const inlet::InletError& ex) { - throw KleeError({fieldPath(container, fieldName), - fieldMessage(container, fieldName, ownerLabel, ex.what())}); + throw KleeError( + {fieldPath(container, fieldName), fieldMessage(container, fieldName, ownerLabel, ex.what())}); } } @@ -160,9 +156,7 @@ Result wrapCallbackErrors(const inlet::Container &container, * \return the resolved scalar value * \throws KleeError if callback evaluation fails */ -double getScalar(const inlet::Container &container, - char const *fieldName, - const std::string &ownerLabel) +double getScalar(const inlet::Container& container, char const* fieldName, const std::string& ownerLabel) { if(hasCallback(container, fieldName)) { @@ -182,9 +176,9 @@ double getScalar(const inlet::Container &container, * \return the resolved string value * \throws KleeError if callback evaluation fails */ -std::string getString(const inlet::Container &container, - char const *fieldName, - const std::string &ownerLabel) +std::string getString(const inlet::Container& container, + char const* fieldName, + const std::string& ownerLabel) { if(hasCallback(container, fieldName)) { @@ -201,7 +195,7 @@ std::string getString(const inlet::Container &container, * \param value the callback vector * \return the active components of \a value */ -std::vector callbackVectorToDoubleVector(const inlet::FunctionType::Vector &value) +std::vector callbackVectorToDoubleVector(const inlet::FunctionType::Vector& value) { std::vector result; result.reserve(value.dim); @@ -222,10 +216,10 @@ std::vector callbackVectorToDoubleVector(const inlet::FunctionType::Vect * \return the resolved vector components * \throws KleeError if callback evaluation or dimension validation fails */ -std::vector getDoubleVector(const inlet::Container &container, - char const *fieldName, +std::vector getDoubleVector(const inlet::Container& container, + char const* fieldName, Dimensions expectedDims, - const std::string &ownerLabel) + const std::string& ownerLabel) { if(hasCallback(container, fieldName)) { @@ -237,15 +231,14 @@ std::vector getDoubleVector(const inlet::Container &container, auto expectedSize = static_cast(expectedDims); if(actualSize != expectedSize) { - throw KleeError( - {fieldPath(container, fieldName), - fieldMessage(container, - fieldName, - ownerLabel, - fmt::format("Wrong size for {}. Expected {}. Got {}.", - fieldName, - expectedSize, - actualSize))}); + throw KleeError({fieldPath(container, fieldName), + fieldMessage(container, + fieldName, + ownerLabel, + fmt::format("Wrong size for {}. Expected {}. Got {}.", + fieldName, + expectedSize, + actualSize))}); } return values; } @@ -263,10 +256,10 @@ std::vector getDoubleVector(const inlet::Container &container, * \return the resolved value converted to \a T */ template -T toArrayLike(const inlet::Container &parent, - char const *fieldName, +T toArrayLike(const inlet::Container& parent, + char const* fieldName, Dimensions expectedDims, - const std::string &ownerLabel) + const std::string& ownerLabel) { auto values = getDoubleVector(parent, fieldName, expectedDims, ownerLabel); return T {values.data(), static_cast(expectedDims)}; @@ -284,11 +277,11 @@ T toArrayLike(const inlet::Container &parent, * \return the resolved value, or \a defaultValue when absent */ template -T toArrayLike(const inlet::Container &parent, - char const *fieldName, +T toArrayLike(const inlet::Container& parent, + char const* fieldName, Dimensions expectedDims, - const T &defaultValue, - const std::string &ownerLabel) + const T& defaultValue, + const std::string& ownerLabel) { if(containsFieldOrCallback(parent, fieldName)) { @@ -306,10 +299,10 @@ T toArrayLike(const inlet::Container &parent, * \param ownerLabel description of the owning shape or named operator * \return the resolved point */ -Point3D getPoint(const inlet::Container &parent, - char const *fieldName, +Point3D getPoint(const inlet::Container& parent, + char const* fieldName, Dimensions expectedDims, - const std::string &ownerLabel) + const std::string& ownerLabel) { return toArrayLike(parent, fieldName, expectedDims, ownerLabel); } @@ -324,11 +317,11 @@ Point3D getPoint(const inlet::Container &parent, * \param ownerLabel description of the owning shape or named operator * \return the resolved point, or \a defaultValue when absent */ -Point3D getPoint(const inlet::Container &parent, - char const *fieldName, +Point3D getPoint(const inlet::Container& parent, + char const* fieldName, Dimensions expectedDims, - const Point3D &defaultValue, - const std::string &ownerLabel) + const Point3D& defaultValue, + const std::string& ownerLabel) { return toArrayLike(parent, fieldName, expectedDims, defaultValue, ownerLabel); } @@ -342,10 +335,10 @@ Point3D getPoint(const inlet::Container &parent, * \param ownerLabel description of the owning shape or named operator * \return the resolved vector */ -Vector3D getVector(const inlet::Container &parent, - char const *fieldName, +Vector3D getVector(const inlet::Container& parent, + char const* fieldName, Dimensions expectedDims, - const std::string &ownerLabel) + const std::string& ownerLabel) { return toArrayLike(parent, fieldName, expectedDims, ownerLabel); } @@ -360,11 +353,11 @@ Vector3D getVector(const inlet::Container &parent, * \param ownerLabel description of the owning shape or named operator * \return the resolved vector, or \a defaultValue when absent */ -Vector3D getVector(const inlet::Container &parent, - char const *fieldName, +Vector3D getVector(const inlet::Container& parent, + char const* fieldName, Dimensions expectedDims, - const Vector3D &defaultValue, - const std::string &ownerLabel) + const Vector3D& defaultValue, + const std::string& ownerLabel) { return toArrayLike(parent, fieldName, expectedDims, defaultValue, ownerLabel); } @@ -398,7 +391,7 @@ std::unordered_set getChildNames(const inlet::Container& container) } } - for(const auto &name : container.getFunctionValueAlternativeNames()) + for(const auto& name : container.getFunctionValueAlternativeNames()) { allChildren.insert(name); } @@ -477,9 +470,9 @@ void verifyObjectFields(const inlet::Container& containerToTest, * \return the created operator * \throws KleeError if the operator fields or vector dimensions are invalid */ -OpPtr parseTranslate(const inlet::Container &opContainer, - const TransformableGeometryProperties &startProperties, - const std::string &ownerLabel) +OpPtr parseTranslate(const inlet::Container& opContainer, + const TransformableGeometryProperties& startProperties, + const std::string& ownerLabel) { verifyObjectFields(opContainer, "translate", FieldSet {}, FieldSet {}); return std::make_shared( @@ -496,9 +489,9 @@ OpPtr parseTranslate(const inlet::Container &opContainer, * \return the created operator * \throws KleeError if the rotation is invalid for the start dimensions or operator fields */ -OpPtr parseRotate(const inlet::Container &opContainer, - const TransformableGeometryProperties &startProperties, - const std::string &ownerLabel) +OpPtr parseRotate(const inlet::Container& opContainer, + const TransformableGeometryProperties& startProperties, + const std::string& ownerLabel) { switch(startProperties.dimensions) { @@ -506,8 +499,7 @@ OpPtr parseRotate(const inlet::Container &opContainer, { verifyObjectFields(opContainer, "rotate", FieldSet {}, {"center"}); auto angle = getScalar(opContainer, "rotate", ownerLabel); - auto center = - getPoint(opContainer, "center", Dimensions::Two, Point3D {0, 0, 0}, ownerLabel); + auto center = getPoint(opContainer, "center", Dimensions::Two, Point3D {0, 0, 0}, ownerLabel); Vector3D axis {0, 0, 1}; return std::make_shared(angle, center, axis, startProperties); } @@ -516,8 +508,7 @@ OpPtr parseRotate(const inlet::Container &opContainer, { verifyObjectFields(opContainer, "rotate", {"axis"}, {"center"}); auto angle = getScalar(opContainer, "rotate", ownerLabel); - auto center = - getPoint(opContainer, "center", Dimensions::Three, Point3D {0, 0, 0}, ownerLabel); + auto center = getPoint(opContainer, "center", Dimensions::Three, Point3D {0, 0, 0}, ownerLabel); auto axis = getVector(opContainer, "axis", Dimensions::Three, ownerLabel); if(axis.is_zero()) { @@ -551,26 +542,25 @@ OpPtr parseRotate(const inlet::Container &opContainer, OpPtr makeCheckedSlice(Point3D origin, Vector3D normal, Vector3D up, - const TransformableGeometryProperties &startProperties, - const inlet::Container &sliceContainer, - const std::string &ownerLabel) + const TransformableGeometryProperties& startProperties, + const inlet::Container& sliceContainer, + const std::string& ownerLabel) { if(normal.is_zero()) { - throw KleeError( - {Path {sliceContainer.name()}, - fieldMessage(sliceContainer, - "normal", - ownerLabel, - "The 'normal' vector must not be a zero vector")}); + throw KleeError({Path {sliceContainer.name()}, + fieldMessage(sliceContainer, + "normal", + ownerLabel, + "The 'normal' vector must not be a zero vector")}); } if(!utilities::isNearlyEqual(normal.dot(up), 0.)) { // Either vector may have come from a callback; report the first one that did const std::string message = "The 'normal' and 'up' vectors must be perpendicular"; - char const *reported = hasCallback(sliceContainer, "up") ? "up" : "normal"; - throw KleeError({Path {sliceContainer.name()}, - fieldMessage(sliceContainer, reported, ownerLabel, message)}); + char const* reported = hasCallback(sliceContainer, "up") ? "up" : "normal"; + throw KleeError( + {Path {sliceContainer.name()}, fieldMessage(sliceContainer, reported, ownerLabel, message)}); } return std::make_shared(origin, normal, up, startProperties); } @@ -585,10 +575,10 @@ OpPtr makeCheckedSlice(Point3D origin, * \return the point to use as the origin * \throws KleeError if the specified origin is not on the slice plane */ -primal::Point3D getPerpendicularSliceOrigin(const inlet::Container &sliceContainer, - char const *planeName, - const primal::Vector3D &defaultNormal, - const std::string &ownerLabel) +primal::Point3D getPerpendicularSliceOrigin(const inlet::Container& sliceContainer, + char const* planeName, + const primal::Vector3D& defaultNormal, + const std::string& ownerLabel) { double axisIntercept = getScalar(sliceContainer, planeName, ownerLabel); @@ -611,11 +601,9 @@ primal::Point3D getPerpendicularSliceOrigin(const inlet::Container &sliceContain primal::Point3D givenOrigin = getPoint(sliceContainer, "origin", Dimensions::Three, ownerLabel); if(givenOrigin[nonZeroIndex] != axisIntercept) { - throw KleeError({Path {sliceContainer["origin"].name()}, - fieldMessage(sliceContainer, - "origin", - ownerLabel, - "The origin must be on the slice plane")}); + throw KleeError( + {Path {sliceContainer["origin"].name()}, + fieldMessage(sliceContainer, "origin", ownerLabel, "The origin must be on the slice plane")}); } return givenOrigin; } @@ -629,9 +617,9 @@ primal::Point3D getPerpendicularSliceOrigin(const inlet::Container &sliceContain * \return the vector to use as the normal * \throws KleeError if the specified normal is not parallel to the slice plane normal */ -primal::Vector3D getPerpendicularSliceNormal(const inlet::Container &sliceContainer, - const primal::Vector3D &defaultNormal, - const std::string &ownerLabel) +primal::Vector3D getPerpendicularSliceNormal(const inlet::Container& sliceContainer, + const primal::Vector3D& defaultNormal, + const std::string& ownerLabel) { if(!containsFieldOrCallback(sliceContainer, "normal")) { @@ -643,9 +631,8 @@ primal::Vector3D getPerpendicularSliceNormal(const inlet::Container &sliceContai bool parallel = cross.is_zero(); if(!parallel) { - throw KleeError( - {Path {sliceContainer["normal"].name()}, - fieldMessage(sliceContainer, "normal", ownerLabel, "Invalid normal")}); + throw KleeError({Path {sliceContainer["normal"].name()}, + fieldMessage(sliceContainer, "normal", ownerLabel, "Invalid normal")}); } return givenNormal; } @@ -662,12 +649,12 @@ primal::Vector3D getPerpendicularSliceNormal(const inlet::Container &sliceContai * \return the parsed plane * \throws KleeError if the slice fields or values are invalid */ -OpPtr readPerpendicularSlice(const inlet::Container &sliceContainer, - char const *planeName, - Vector3D const &defaultNormal, - Vector3D const &defaultUp, - const TransformableGeometryProperties &startProperties, - const std::string &ownerLabel) +OpPtr readPerpendicularSlice(const inlet::Container& sliceContainer, + char const* planeName, + Vector3D const& defaultNormal, + Vector3D const& defaultUp, + const TransformableGeometryProperties& startProperties, + const std::string& ownerLabel) { verifyObjectFields(sliceContainer, planeName, FieldSet {}, {"origin", "normal", "up"}); const primal::Vector3D defaultNormalVec {defaultNormal.data()}; @@ -688,42 +675,27 @@ OpPtr readPerpendicularSlice(const inlet::Container &sliceContainer, * \return the created operator * \throws KleeError if the slice fields or values are invalid */ -OpPtr parseSlice(const inlet::Container &opContainer, - const TransformableGeometryProperties &startProperties, - const std::string &ownerLabel) +OpPtr parseSlice(const inlet::Container& opContainer, + const TransformableGeometryProperties& startProperties, + const std::string& ownerLabel) { if(startProperties.dimensions != Dimensions::Three) { throw KleeError({opContainer.name(), "Cannot do a slice from 2D"}); } verifyObjectFields(opContainer, "slice", FieldSet {}, FieldSet {}); - auto &sliceContainer = *opContainer.getChildContainers().at(opContainer.name() + "/slice").get(); + auto& sliceContainer = *opContainer.getChildContainers().at(opContainer.name() + "/slice").get(); if(containsFieldOrCallback(sliceContainer, "x")) { - return readPerpendicularSlice(sliceContainer, - "x", - {1, 0, 0}, - {0, 0, 1}, - startProperties, - ownerLabel); + return readPerpendicularSlice(sliceContainer, "x", {1, 0, 0}, {0, 0, 1}, startProperties, ownerLabel); } else if(containsFieldOrCallback(sliceContainer, "y")) { - return readPerpendicularSlice(sliceContainer, - "y", - {0, 1, 0}, - {1, 0, 0}, - startProperties, - ownerLabel); + return readPerpendicularSlice(sliceContainer, "y", {0, 1, 0}, {1, 0, 0}, startProperties, ownerLabel); } else if(containsFieldOrCallback(sliceContainer, "z")) { - return readPerpendicularSlice(sliceContainer, - "z", - {0, 0, 1}, - {0, 1, 0}, - startProperties, - ownerLabel); + return readPerpendicularSlice(sliceContainer, "z", {0, 0, 1}, {0, 1, 0}, startProperties, ownerLabel); } verifyObjectFields(sliceContainer, "origin", {"normal", "up"}, FieldSet {}); @@ -743,9 +715,9 @@ OpPtr parseSlice(const inlet::Container &opContainer, * \return the created operator * \throws KleeError if the scale fields or vector dimensions are invalid */ -OpPtr parseScale(const inlet::Container &opContainer, - const TransformableGeometryProperties &startProperties, - const std::string &ownerLabel) +OpPtr parseScale(const inlet::Container& opContainer, + const TransformableGeometryProperties& startProperties, + const std::string& ownerLabel) { verifyObjectFields(opContainer, "scale", FieldSet {}, FieldSet {"center"}); auto factors = hasCallback(opContainer, "scale") @@ -768,12 +740,11 @@ OpPtr parseScale(const inlet::Container &opContainer, { throw KleeError( {fieldPath(opContainer, "scale"), - fieldMessage(opContainer, - "scale", - ownerLabel, - fmt::format("Wrong size for scale. Expected {}. Got {}.", - expectedSize, - actualSize))}); + fieldMessage( + opContainer, + "scale", + ownerLabel, + fmt::format("Wrong size for scale. Expected {}. Got {}.", expectedSize, actualSize))}); } } else if(!isUniform) @@ -788,21 +759,13 @@ OpPtr parseScale(const inlet::Container &opContainer, Point3D center {0., 0., 0.}; if(containsFieldOrCallback(opContainer, "center")) { - center = getPoint(opContainer, - "center", - startProperties.dimensions, - Point3D {0, 0, 0}, - ownerLabel); + center = + getPoint(opContainer, "center", startProperties.dimensions, Point3D {0, 0, 0}, ownerLabel); } if(isUniform) { - return std::make_shared( - factors[0], - factors[0], - factors[0], - center, - startProperties); + return std::make_shared(factors[0], factors[0], factors[0], center, startProperties); } return std::make_shared(factors[0], factors[1], factors[2], center, startProperties); @@ -817,9 +780,9 @@ OpPtr parseScale(const inlet::Container &opContainer, * \return the created operator * \throws KleeError if the unit string or operator fields are invalid */ -OpPtr parseConvertUnits(const inlet::Container &opContainer, - const TransformableGeometryProperties &startProperties, - const std::string &ownerLabel) +OpPtr parseConvertUnits(const inlet::Container& opContainer, + const TransformableGeometryProperties& startProperties, + const std::string& ownerLabel) { verifyObjectFields(opContainer, "convert_units_to", FieldSet {}, FieldSet {}); const auto unitName = getString(opContainer, "convert_units_to", ownerLabel); @@ -829,14 +792,13 @@ OpPtr parseConvertUnits(const inlet::Container &opContainer, { endUnits = internal::parseLengthUnits(unitName, static_cast(path)); } - catch(const KleeError &err) + catch(const KleeError& err) { if(!hasCallback(opContainer, "convert_units_to")) { throw; } - throw KleeError( - {path, fieldMessage(opContainer, "convert_units_to", ownerLabel, err.what())}); + throw KleeError({path, fieldMessage(opContainer, "convert_units_to", ownerLabel, err.what())}); } return std::make_shared(endUnits, startProperties); } @@ -851,10 +813,10 @@ OpPtr parseConvertUnits(const inlet::Container &opContainer, * \return the created operator * \throws KleeError if the reference is missing or the operator fields are invalid */ -OpPtr parseRef(const inlet::Container &opContainer, - const TransformableGeometryProperties &startProperties, - const NamedOperatorMap &namedOperators, - const std::string &ownerLabel) +OpPtr parseRef(const inlet::Container& opContainer, + const TransformableGeometryProperties& startProperties, + const NamedOperatorMap& namedOperators, + const std::string& ownerLabel) { verifyObjectFields(opContainer, "ref", FieldSet {}, FieldSet {}); const auto operatorName = getString(opContainer, "ref", ownerLabel); @@ -862,8 +824,8 @@ OpPtr parseRef(const inlet::Container &opContainer, if(opIter == namedOperators.end()) { const auto message = axom::fmt::format("No operator named '{}'", operatorName); - throw KleeError({fieldPath(opContainer, "ref"), - fieldMessage(opContainer, "ref", ownerLabel, message)}); + throw KleeError( + {fieldPath(opContainer, "ref"), fieldMessage(opContainer, "ref", ownerLabel, message)}); } auto referencedOperator = opIter->second; bool startUnitsMatch = startProperties.units == referencedOperator->getStartProperties().units; @@ -902,8 +864,8 @@ OpPtr parseRef(const inlet::Container &opContainer, */ OpPtr convertOperator(SingleOperatorData const& data, TransformableGeometryProperties startProperties, - const NamedOperatorMap &namedOperators, - const std::string &ownerLabel) + const NamedOperatorMap& namedOperators, + const std::string& ownerLabel) { std::unordered_map parsers { {"translate", parseTranslate}, @@ -912,9 +874,9 @@ OpPtr convertOperator(SingleOperatorData const& data, {"scale", parseScale}, {"convert_units_to", parseConvertUnits}, {"ref", - [&namedOperators](const inlet::Container &opContainer, - const TransformableGeometryProperties &startProperties, - const std::string &ownerLabel) { + [&namedOperators](const inlet::Container& opContainer, + const TransformableGeometryProperties& startProperties, + const std::string& ownerLabel) { return parseRef(opContainer, startProperties, namedOperators, ownerLabel); }}, }; @@ -932,7 +894,7 @@ OpPtr convertOperator(SingleOperatorData const& data, if(!childNames.empty()) { message += ". Found parameters:"; - for(const auto &name : childNames) + for(const auto& name : childNames) { message += " "; message += name; @@ -954,17 +916,17 @@ GeometryOperatorData::GeometryOperatorData(const Path& path, , m_singleOperatorData {std::move(singleOperatorData)} { } -inlet::Container &GeometryOperatorData::defineSchema(inlet::Container &parent, - const std::string &fieldName, - const std::string &description, +inlet::Container& GeometryOperatorData::defineSchema(inlet::Container& parent, + const std::string& fieldName, + const std::string& description, bool enableLuaCallbacks) { - auto &opContainer = parent.addStructArray(fieldName, description).strict(); + auto& opContainer = parent.addStructArray(fieldName, description).strict(); // A callback alternative must be declared before the concrete field it may // stand in for, so each pair is declared together. - const auto addCallback = [enableLuaCallbacks](inlet::Container &container, - const char *name, + const auto addCallback = [enableLuaCallbacks](inlet::Container& container, + const char* name, inlet::FunctionTag returnType) { if(enableLuaCallbacks) { @@ -988,7 +950,7 @@ inlet::Container &GeometryOperatorData::defineSchema(inlet::Container &parent, addCallback(opContainer, "convert_units_to", inlet::FunctionTag::String); opContainer.addString("convert_units_to"); - auto &slice = opContainer.addStruct("slice"); + auto& slice = opContainer.addStruct("slice"); addCallback(slice, "x", inlet::FunctionTag::Double); slice.addDouble("x"); addCallback(slice, "y", inlet::FunctionTag::Double); @@ -1008,9 +970,9 @@ inlet::Container &GeometryOperatorData::defineSchema(inlet::Container &parent, } std::shared_ptr GeometryOperatorData::makeOperator( - const TransformableGeometryProperties &startProperties, - const NamedOperatorMap &namedOperators, - const std::string &ownerLabel) const + const TransformableGeometryProperties& startProperties, + const NamedOperatorMap& namedOperators, + const std::string& ownerLabel) const { if(m_singleOperatorData.empty()) { @@ -1030,7 +992,7 @@ std::shared_ptr GeometryOperatorData::makeOperator( return composite; } -void NamedOperatorData::defineSchema(inlet::Container &container, bool enableLuaCallbacks) +void NamedOperatorData::defineSchema(inlet::Container& container, bool enableLuaCallbacks) { container.addString("name").required(); defineDimensionsField(container, "start_dimensions", "The initial dimensions of the operator"); @@ -1048,11 +1010,11 @@ NamedOperatorMapData::NamedOperatorMapData(std::vector&& oper : m_operatorData {operatorData} { } -void NamedOperatorMapData::defineSchema(inlet::Container &parent, - const std::string &name, +void NamedOperatorMapData::defineSchema(inlet::Container& parent, + const std::string& name, bool enableLuaCallbacks) { - auto &container = parent.addStructArray(name); + auto& container = parent.addStructArray(name); NamedOperatorData::defineSchema(container, enableLuaCallbacks); } diff --git a/src/axom/klee/io/GeometryOperatorsIO.hpp b/src/axom/klee/io/GeometryOperatorsIO.hpp index b66865f914..a1e6837339 100644 --- a/src/axom/klee/io/GeometryOperatorsIO.hpp +++ b/src/axom/klee/io/GeometryOperatorsIO.hpp @@ -28,7 +28,7 @@ using NamedOperatorMap = std::unordered_map makeOperator(const TransformableGeometryProperties &startProperties, - const NamedOperatorMap &namedOperators, - const std::string &ownerLabel) const; + std::shared_ptr makeOperator(const TransformableGeometryProperties& startProperties, + const NamedOperatorMap& namedOperators, + const std::string& ownerLabel) const; /** * Get the path of this operator in the source document @@ -105,7 +105,7 @@ struct NamedOperatorData * @param container the container in which to describe a single named operator * @param enableLuaCallbacks whether operator fields may be supplied as Lua callbacks */ - static void defineSchema(inlet::Container &container, bool enableLuaCallbacks = false); + static void defineSchema(inlet::Container& container, bool enableLuaCallbacks = false); }; /// Data for all a collection of named operators @@ -138,8 +138,8 @@ struct NamedOperatorMapData * @param name the name of the map * @param enableLuaCallbacks whether operator fields may be supplied as Lua callbacks */ - static void defineSchema(inlet::Container &parent, - const std::string &name, + static void defineSchema(inlet::Container& parent, + const std::string& name, bool enableLuaCallbacks = false); private: diff --git a/src/axom/klee/io/IO.cpp b/src/axom/klee/io/IO.cpp index 6d91d4dca7..1602ebdabd 100644 --- a/src/axom/klee/io/IO.cpp +++ b/src/axom/klee/io/IO.cpp @@ -36,8 +36,8 @@ namespace klee { namespace { -bool isLuaKeyword(const std::string &name); -bool isLuaIdentifier(const std::string &name); +bool isLuaKeyword(const std::string& name); +bool isLuaIdentifier(const std::string& name); #ifdef AXOM_USE_LUA class KleeLuaReader : public inlet::LuaReader @@ -52,7 +52,7 @@ class KleeLuaReader : public inlet::LuaReader { std::unordered_set names; auto lua = solState(); - for(const auto &entry : lua->globals()) + for(const auto& entry : lua->globals()) { if(entry.first.get_type() == axom::sol::type::string) { @@ -71,18 +71,15 @@ class KleeLuaReader : public inlet::LuaReader * \throws KleeError if evaluation fails or the returned exports are invalid */ std::unordered_set applyInitializationChunk( - const LuaInitializationChunk &initialization, - const std::unordered_set &reservedNames) + const LuaInitializationChunk& initialization, + const std::unordered_set& reservedNames) { auto lua = solState(); const std::string chunkName = initialization.label.empty() ? "" : initialization.label; const auto chunkPath = Path {chunkName}; - const auto chunkMessage = [&](const std::string &message) { - return axom::fmt::format( - "Klee Lua initialization chunk '{}': {}", - chunkName, - message); + const auto chunkMessage = [&](const std::string& message) { + return axom::fmt::format("Klee Lua initialization chunk '{}': {}", chunkName, message); }; if(initialization.source.empty()) { @@ -94,32 +91,24 @@ class KleeLuaReader : public inlet::LuaReader // Evaluate initialization in its own environment so assignments made by the // chunk do not mutate the input file's globals. The fallback keeps // preloaded libraries and caller-provided initial globals visible. - axom::sol::environment initializationEnvironment { - *lua, - axom::sol::create, - lua->globals()}; + axom::sol::environment initializationEnvironment {*lua, axom::sol::create, lua->globals()}; initializationEnvironment["_G"] = initializationEnvironment; auto result = lua->script(initialization.source, initializationEnvironment); if(!result.valid()) { axom::sol::error err = result; throw KleeError( - {chunkPath, - chunkMessage(axom::fmt::format("Failed to evaluate chunk: {}", err.what()))}); + {chunkPath, chunkMessage(axom::fmt::format("Failed to evaluate chunk: {}", err.what()))}); } axom::sol::optional tableOption = result; if(!tableOption) { - throw KleeError( - {chunkPath, - chunkMessage("Chunk must return a table of exported globals.")}); + throw KleeError({chunkPath, chunkMessage("Chunk must return a table of exported globals.")}); } std::unordered_set exportedNames; - auto exportPath = [&](const std::string &name) { - return Path::join({chunkPath, Path {name}}); - }; + auto exportPath = [&](const std::string& name) { return Path::join({chunkPath, Path {name}}); }; auto typeName = [](axom::sol::type type) { switch(type) { @@ -140,13 +129,11 @@ class KleeLuaReader : public inlet::LuaReader } }; - for(const auto &entry : tableOption.value()) + for(const auto& entry : tableOption.value()) { if(entry.first.get_type() != axom::sol::type::string) { - throw KleeError( - {chunkPath, - chunkMessage("Export table must contain only string keys.")}); + throw KleeError({chunkPath, chunkMessage("Export table must contain only string keys.")}); } const std::string name = entry.first.as(); @@ -157,18 +144,15 @@ class KleeLuaReader : public inlet::LuaReader : "Exported global names must be Lua identifiers."; throw KleeError( {exportPath(name), - chunkMessage(axom::fmt::format( - "Invalid exported Lua global name '{}'. {}", - name, - reason))}); + chunkMessage( + axom::fmt::format("Invalid exported Lua global name '{}'. {}", name, reason))}); } if(reservedNames.find(name) != reservedNames.end()) { - throw KleeError( - {exportPath(name), - chunkMessage(axom::fmt::format( - "Exported Lua global name '{}' conflicts with an existing Lua global.", - name))}); + throw KleeError({exportPath(name), + chunkMessage(axom::fmt::format( + "Exported Lua global name '{}' conflicts with an existing Lua global.", + name))}); } switch(entry.second.get_type()) { @@ -179,14 +163,13 @@ class KleeLuaReader : public inlet::LuaReader case axom::sol::type::table: break; default: - throw KleeError( - {exportPath(name), - chunkMessage(axom::fmt::format( - "Exported Lua global '{}' has unsupported value type '{}'. " - "Supported exported global value types are booleans, numbers, " - "strings, tables, and functions.", - name, - typeName(entry.second.get_type())))}); + throw KleeError({exportPath(name), + chunkMessage(axom::fmt::format( + "Exported Lua global '{}' has unsupported value type '{}'. " + "Supported exported global value types are booleans, numbers, " + "strings, tables, and functions.", + name, + typeName(entry.second.get_type())))}); } // Preserve the original Lua representation. In particular, copying a @@ -197,15 +180,14 @@ class KleeLuaReader : public inlet::LuaReader return exportedNames; } - catch(const KleeError &) + catch(const KleeError&) { throw; } - catch(const std::exception &ex) + catch(const std::exception& ex) { throw KleeError( - {chunkPath, - chunkMessage(axom::fmt::format("Failed to evaluate chunk: {}", ex.what()))}); + {chunkPath, chunkMessage(axom::fmt::format("Failed to evaluate chunk: {}", ex.what()))}); } } }; @@ -294,7 +276,7 @@ namespace * @param geometry the Container representing a "geometry" object. * @param enableLuaCallbacks whether operator fields may be supplied as Lua callbacks */ -void defineGeometry(inlet::Container &geometry, bool enableLuaCallbacks) +void defineGeometry(inlet::Container& geometry, bool enableLuaCallbacks) { geometry.addString("format", "The format of the input file").required(); geometry.addString("path", @@ -325,7 +307,7 @@ void defineGeometry(inlet::Container &geometry, bool enableLuaCallbacks) * @param document the Inlet document for which to define the schema * @param enableLuaCallbacks whether operator fields may be supplied as Lua callbacks */ -void defineShapeList(inlet::Inlet &document, bool enableLuaCallbacks) +void defineShapeList(inlet::Inlet& document, bool enableLuaCallbacks) { inlet::Container& shapeList = document.addStructArray("shapes", "The list of shapes"); @@ -374,7 +356,7 @@ void defineShapeList(inlet::Inlet &document, bool enableLuaCallbacks) * @param document the Inlet document for which to define the schema * @param enableLuaCallbacks whether operator fields may be supplied as Lua callbacks */ -void defineKleeSchema(inlet::Inlet &document, bool enableLuaCallbacks) +void defineKleeSchema(inlet::Inlet& document, bool enableLuaCallbacks) { internal::defineDimensionsField(document.getGlobalContainer(), "dimensions").required(); defineShapeList(document, enableLuaCallbacks); @@ -395,8 +377,8 @@ void defineKleeSchema(inlet::Inlet &document, bool enableLuaCallbacks) */ Geometry convert(GeometryData const& data, Dimensions fileDimensions, - internal::NamedOperatorMap const &namedOperators, - const std::string &shapeName) + internal::NamedOperatorMap const& namedOperators, + const std::string& shapeName) { const bool has_start_dims = data.startDimensions != Dimensions::Unspecified; const bool has_explicit_dims = data.explicitDimensions != Dimensions::Unspecified; @@ -537,13 +519,12 @@ InputFormat inferInputFormat(const std::string& filePath) * \param name the candidate name * \return true when \a name is a Lua keyword */ -bool isLuaKeyword(const std::string &name) +bool isLuaKeyword(const std::string& name) { static const std::unordered_set keywords { - "and", "break", "do", "else", "elseif", "end", "false", - "for", "function", "goto", "if", "in", "local", "nil", - "not", "or", "repeat", "return", "then", "true", "until", - "while", + "and", "break", "do", "else", "elseif", "end", "false", "for", + "function", "goto", "if", "in", "local", "nil", "not", "or", + "repeat", "return", "then", "true", "until", "while", }; return keywords.find(name) != keywords.end(); } @@ -554,7 +535,7 @@ bool isLuaKeyword(const std::string &name) * \param name the candidate name * \return true when \a name may be used as a Lua identifier */ -bool isLuaIdentifier(const std::string &name) +bool isLuaIdentifier(const std::string& name) { if(name.empty()) { @@ -574,9 +555,9 @@ bool isLuaIdentifier(const std::string &name) { return false; } - return std::all_of(name.begin() + 1, name.end(), [&](char ch) { - return isNameChar(static_cast(ch)); - }) && + return std::all_of(name.begin() + 1, + name.end(), + [&](char ch) { return isNameChar(static_cast(ch)); }) && !isLuaKeyword(name); } @@ -591,14 +572,14 @@ bool isLuaIdentifier(const std::string &name) * or the external Lua initialization is invalid for the selected format */ std::unique_ptr createReader(InputFormat format, - const LuaInputOptions &options, - std::unordered_set &allowedGlobals) + const LuaInputOptions& options, + std::unordered_set& allowedGlobals) { allowedGlobals.clear(); if(format != InputFormat::Lua && options.initialization) { - throw KleeError({Path {""}, - "Klee Lua initialization is only supported for Lua input decks."}); + throw KleeError( + {Path {""}, "Klee Lua initialization is only supported for Lua input decks."}); } switch(format) @@ -614,8 +595,8 @@ std::unique_ptr createReader(InputFormat format, // Exported values are ordinary Lua globals installed before deck parsing. // allowedGlobals only prevents Klee's unexpected-global check from rejecting // those names; it does not make them read-only inside the deck. - allowedGlobals = reader->applyInitializationChunk(*options.initialization, - reader->topLevelGlobalNames()); + allowedGlobals = + reader->applyInitializationChunk(*options.initialization, reader->topLevelGlobalNames()); } return reader; } @@ -686,9 +667,9 @@ void parseOrThrow(Parse&& parse, * \param errors receives errors for unexpected globals * \param allowedGlobals caller-provided globals that are permitted in the deck */ -void appendUnexpectedGlobalErrors(const inlet::Inlet &doc, - std::vector &errors, - const std::unordered_set &allowedGlobals) +void appendUnexpectedGlobalErrors(const inlet::Inlet& doc, + std::vector& errors, + const std::unordered_set& allowedGlobals) { for(const auto& name : doc.unexpectedNames()) { @@ -717,7 +698,7 @@ void appendUnexpectedGlobalErrors(const inlet::Inlet &doc, */ ShapeSet readShapeSetFromReader(std::unique_ptr reader, InputFormat format, - const std::unordered_set &allowedGlobals) + const std::unordered_set& allowedGlobals) { const bool isLuaInput = format == InputFormat::Lua; sidre::DataStore dataStore; @@ -757,9 +738,7 @@ ShapeSet readShapeSet(std::istream& stream, InputFormat format) return readShapeSet(stream, format, LuaInputOptions {}); } -ShapeSet readShapeSet(std::istream &stream, - InputFormat format, - const LuaInputOptions &options) +ShapeSet readShapeSet(std::istream& stream, InputFormat format, const LuaInputOptions& options) { std::string contents {std::istreambuf_iterator(stream), {}}; @@ -769,9 +748,7 @@ ShapeSet readShapeSet(std::istream &stream, format, Path {""}, "from stream"); - return readShapeSetFromReader(std::move(reader), - format, - allowedGlobals); + return readShapeSetFromReader(std::move(reader), format, allowedGlobals); } ShapeSet readShapeSet(const std::string& filePath) @@ -784,14 +761,12 @@ ShapeSet readShapeSet(const std::string& filePath, InputFormat format) return readShapeSet(filePath, format, LuaInputOptions {}); } -ShapeSet readShapeSet(const std::string &filePath, const LuaInputOptions &options) +ShapeSet readShapeSet(const std::string& filePath, const LuaInputOptions& options) { return readShapeSet(filePath, inferInputFormat(filePath), options); } -ShapeSet readShapeSet(const std::string &filePath, - InputFormat format, - const LuaInputOptions &options) +ShapeSet readShapeSet(const std::string& filePath, InputFormat format, const LuaInputOptions& options) { std::unordered_set allowedGlobals; auto reader = createReader(format, options, allowedGlobals); @@ -799,9 +774,7 @@ ShapeSet readShapeSet(const std::string &filePath, format, Path {filePath}, axom::fmt::format("from file '{}'", filePath)); - auto shapeSet = readShapeSetFromReader(std::move(reader), - format, - allowedGlobals); + auto shapeSet = readShapeSetFromReader(std::move(reader), format, allowedGlobals); shapeSet.setPath(filePath); return shapeSet; } diff --git a/src/axom/klee/io/IO.hpp b/src/axom/klee/io/IO.hpp index f77b58afbf..76ed11df82 100644 --- a/src/axom/klee/io/IO.hpp +++ b/src/axom/klee/io/IO.hpp @@ -71,9 +71,7 @@ ShapeSet readShapeSet(std::istream& stream, InputFormat format); * \return the ShapeSet read from the stream * \throws KleeError if the input or Lua input options are invalid */ -ShapeSet readShapeSet(std::istream &stream, - InputFormat format, - const LuaInputOptions &options); +ShapeSet readShapeSet(std::istream& stream, InputFormat format, const LuaInputOptions& options); /** * Read a ShapeSet from a specified file @@ -108,7 +106,7 @@ ShapeSet readShapeSet(const std::string& filePath, InputFormat format); * \return the ShapeSet read from the file * \throws KleeError if the input or Lua input options are invalid */ -ShapeSet readShapeSet(const std::string &filePath, const LuaInputOptions &options); +ShapeSet readShapeSet(const std::string& filePath, const LuaInputOptions& options); /** * Read a ShapeSet from a specified file using an explicit format and @@ -121,9 +119,7 @@ ShapeSet readShapeSet(const std::string &filePath, const LuaInputOptions &option * \return the ShapeSet read from the file * \throws KleeError if the input or Lua input options are invalid */ -ShapeSet readShapeSet(const std::string &filePath, - InputFormat format, - const LuaInputOptions &options); +ShapeSet readShapeSet(const std::string& filePath, InputFormat format, const LuaInputOptions& options); } // namespace klee } // namespace axom diff --git a/src/axom/klee/tests/klee_geometry_operators_io.cpp b/src/axom/klee/tests/klee_geometry_operators_io.cpp index f2f60a3489..460a1253d8 100644 --- a/src/axom/klee/tests/klee_geometry_operators_io.cpp +++ b/src/axom/klee/tests/klee_geometry_operators_io.cpp @@ -346,7 +346,7 @@ TEST(GeometryOperatorsIO, readRotation_3D_zeroAxis) )"); FAIL() << "Should have rejected a zero rotation axis"; } - catch(const KleeError &err) + catch(const KleeError& err) { EXPECT_THAT(err.what(), HasSubstr("axis")); EXPECT_THAT(err.what(), HasSubstr("zero")); @@ -378,7 +378,7 @@ TEST(GeometryOperatorsIO, readOperator_unexpectedParameterNamesBothFields) )"); FAIL() << "Should not have parsed"; } - catch(const KleeError &ex) + catch(const KleeError& ex) { // The unexpected parameter is "axis" and the operator is "translate" EXPECT_THAT(ex.what(), HasSubstr("Unexpected parameter 'axis' for operator 'translate'")); @@ -420,8 +420,8 @@ TEST(GeometryOperatorsIO, readScale_singleValue_withCenter) EXPECT_DOUBLE_EQ(1.2, scale.getXFactor()); EXPECT_DOUBLE_EQ(1.2, scale.getYFactor()); EXPECT_DOUBLE_EQ(1.2, scale.getZFactor()); - const Point3D expectedCenter = dims == Dimensions::Two ? Point3D {10, 20, 0} - : Point3D {10, 20, 30}; + const Point3D expectedCenter = + dims == Dimensions::Two ? Point3D {10, 20, 0} : Point3D {10, 20, 30}; EXPECT_THAT(scale.getCenter(), AlmostEqPoint(expectedCenter)); } } diff --git a/src/axom/klee/tests/klee_io.cpp b/src/axom/klee/tests/klee_io.cpp index d9feee4a16..e021b5c635 100644 --- a/src/axom/klee/tests/klee_io.cpp +++ b/src/axom/klee/tests/klee_io.cpp @@ -70,7 +70,7 @@ ShapeSet readShapeSetFromString(const std::string& input, return klee::readShapeSet(istream, format, options); } -std::string makeLuaSliceCallbackInput(const std::string &sliceFields) +std::string makeLuaSliceCallbackInput(const std::string& sliceFields) { std::ostringstream input; input << R"( @@ -88,8 +88,8 @@ std::string makeLuaSliceCallbackInput(const std::string &sliceFields) operators = { { slice = { - )" - << sliceFields << R"( + )" << sliceFields + << R"( } } } @@ -100,7 +100,7 @@ std::string makeLuaSliceCallbackInput(const std::string &sliceFields) return input.str(); } -std::string makeLuaRotationCallbackInput(const std::string &rotationFields) +std::string makeLuaRotationCallbackInput(const std::string& rotationFields) { std::ostringstream input; input << R"( @@ -115,8 +115,8 @@ std::string makeLuaRotationCallbackInput(const std::string &rotationFields) units = "cm", operators = { { - )" - << rotationFields << R"( + )" << rotationFields + << R"( } } } @@ -126,8 +126,8 @@ std::string makeLuaRotationCallbackInput(const std::string &rotationFields) return input.str(); } -std::string makeLuaStringOperatorCallbackInput(const std::string &fieldName, - const std::string &returnExpression) +std::string makeLuaStringOperatorCallbackInput(const std::string& fieldName, + const std::string& returnExpression) { std::ostringstream input; input << R"( @@ -153,8 +153,8 @@ std::string makeLuaStringOperatorCallbackInput(const std::string &fieldName, units = "cm", operators = { { - )" - << fieldName << " = function() return " << returnExpression << R"( end + )" << fieldName + << " = function() return " << returnExpression << R"( end } } } @@ -538,7 +538,7 @@ TEST(IOTest, readShapeSet_yamlRejectsLuaInitialization) dimensions = 2 } )", - "runtime_initialization"}; + "runtime_initialization"}; try { @@ -659,7 +659,7 @@ TEST(IOTest, readShapeSet_luaInitializationProvidesDimensionAndOperator) enabled = true } )", - "runtime_initialization"}; + "runtime_initialization"}; LuaInputOptions options; options.initialization = initialization; @@ -708,7 +708,7 @@ TEST(IOTest, readShapeSet_luaInitializationExportsMutableGlobals) } } )", - "runtime_initialization"}; + "runtime_initialization"}; LuaInputOptions options; options.initialization = initialization; @@ -759,7 +759,7 @@ TEST(IOTest, readShapeSet_luaInitializationIsolatesUnexportedGlobals) exported_lift = 4.0 } )", - "runtime_initialization"}; + "runtime_initialization"}; LuaInputOptions options; options.initialization = initialization; @@ -806,8 +806,7 @@ TEST(IOTest, readShapeSet_luaInitializationCannotSetSchemaGlobalsWithoutExportin LuaInitializationChunk initialization {std::string {source}, "runtime_initialization"}; LuaInputOptions options; options.initialization = initialization; - EXPECT_THROW(readShapeSetFromString("shapes = {}", InputFormat::Lua, options), - KleeError); + EXPECT_THROW(readShapeSetFromString("shapes = {}", InputFormat::Lua, options), KleeError); } } @@ -866,7 +865,7 @@ TEST(IOTest, readShapeSet_luaInitializationPreservesLuaInteger) exact_integer = 9007199254740993 } )", - "integer_initialization"}; + "integer_initialization"}; auto shapeSet = readShapeSetFromString(R"( dimensions = 2 @@ -897,7 +896,7 @@ TEST(IOTest, readShapeSet_luaInitializationIsolationIsShallow) math.initialization_value = 4.0 return {} )", - "shallow_initialization"}; + "shallow_initialization"}; auto shapeSet = readShapeSetFromString(R"( dimensions = 2 @@ -940,15 +939,11 @@ TEST(IOTest, readShapeSet_luaInitializationRejectsInvalidChunks) for(const auto& invalid : invalidInitializations) { LuaInputOptions options; - options.initialization = - LuaInitializationChunk {invalid.source, "invalid_initialization"}; + options.initialization = LuaInitializationChunk {invalid.source, "invalid_initialization"}; try { - readShapeSetFromString( - "dimensions = 2; shapes = {}", - InputFormat::Lua, - options); + readShapeSetFromString("dimensions = 2; shapes = {}", InputFormat::Lua, options); FAIL() << "Should have thrown"; } catch(const KleeError& err) @@ -967,7 +962,7 @@ TEST(IOTest, readShapeSet_luaInitializationRejectsInvalidExportName) ["shape-dim"] = 2 } )", - "runtime_initialization"}; + "runtime_initialization"}; try { @@ -993,7 +988,7 @@ TEST(IOTest, readShapeSet_luaInitializationRejectsKeywordExport) ["function"] = 2 } )", - "runtime_initialization"}; + "runtime_initialization"}; try { @@ -1015,7 +1010,7 @@ TEST(IOTest, readShapeSet_luaInitializationRejectsReservedGlobalName) math = 2 } )", - "runtime_initialization"}; + "runtime_initialization"}; try { @@ -1036,8 +1031,7 @@ TEST(IOTest, readShapeSet_luaInitializationRejectsReservedGlobalName) TEST(IOTest, readShapeSet_luaInitializationRequiresTableReturn) { LuaInputOptions options; - options.initialization = - LuaInitializationChunk {"return 2", "runtime_initialization"}; + options.initialization = LuaInitializationChunk {"return 2", "runtime_initialization"}; try { @@ -1316,8 +1310,7 @@ TEST(IOTest, readShapeSet_luaNamedOperatorCallbackIsEvaluatedOnceAndReused) std::dynamic_pointer_cast(firstComposite->getOperators()[0]); ASSERT_TRUE(sharedOperator); ASSERT_EQ(1u, sharedOperator->getOperators().size()); - auto translation = - std::dynamic_pointer_cast(sharedOperator->getOperators()[0]); + auto translation = std::dynamic_pointer_cast(sharedOperator->getOperators()[0]); ASSERT_TRUE(translation); EXPECT_THAT(translation->getOffset(), AlmostEqVector(Vector3D {1, 2, 0})); } @@ -1466,7 +1459,7 @@ TEST(IOTest, readShapeSet_luaNamedOperatorCallbackErrorIncludesContext) InputFormat::Lua); FAIL() << "Should have thrown"; } - catch(const KleeError &err) + catch(const KleeError& err) { EXPECT_THAT(err.what(), HasSubstr("translate")); EXPECT_THAT(err.what(), HasSubstr("named operator")); @@ -1739,12 +1732,10 @@ TEST(IOTest, readShapeSet_luaStringOperatorCallbacks) EXPECT_EQ(LengthUnit::m, converter->getStartProperties().units); EXPECT_EQ(LengthUnit::cm, converter->getEndProperties().units); - auto referenced = - std::dynamic_pointer_cast(composite->getOperators()[1]); + auto referenced = std::dynamic_pointer_cast(composite->getOperators()[1]); ASSERT_TRUE(referenced); ASSERT_EQ(1u, referenced->getOperators().size()); - auto translation = - std::dynamic_pointer_cast(referenced->getOperators()[0]); + auto translation = std::dynamic_pointer_cast(referenced->getOperators()[0]); ASSERT_TRUE(translation); EXPECT_THAT(translation->getOffset(), AlmostEqVector(Vector3D {1, 2, 0})); } @@ -1753,9 +1744,9 @@ TEST(IOTest, readShapeSet_luaStringOperatorCallbackErrorsIncludeContext) { struct FailureCase { - const char *field; - const char *returnExpression; - const char *expectedMessage; + const char* field; + const char* returnExpression; + const char* expectedMessage; }; const std::array cases {{ {"convert_units_to", "{}", "function call"}, @@ -1764,7 +1755,7 @@ TEST(IOTest, readShapeSet_luaStringOperatorCallbackErrorsIncludeContext) {"ref", "\"missing_operator\"", "No operator named"}, }}; - for(const auto &testCase : cases) + for(const auto& testCase : cases) { SCOPED_TRACE(testCase.expectedMessage); try @@ -1774,10 +1765,9 @@ TEST(IOTest, readShapeSet_luaStringOperatorCallbackErrorsIncludeContext) InputFormat::Lua); FAIL() << "Should have thrown"; } - catch(const KleeError &err) + catch(const KleeError& err) { - EXPECT_THAT(err.what(), - HasSubstr(std::string {"callback for '"} + testCase.field + "'")); + EXPECT_THAT(err.what(), HasSubstr(std::string {"callback for '"} + testCase.field + "'")); EXPECT_THAT(err.what(), HasSubstr("string_callback")); EXPECT_THAT(err.what(), HasSubstr("operator 1")); EXPECT_THAT(err.what(), HasSubstr(testCase.expectedMessage)); @@ -1859,7 +1849,7 @@ TEST(IOTest, readShapeSet_luaPerpendicularSliceCallbacks) TEST(IOTest, readShapeSet_luaPerpendicularSliceCallbackWrongTypeIncludesContext) { - for(const char *axis : {"x", "y", "z"}) + for(const char* axis : {"x", "y", "z"}) { SCOPED_TRACE(axis); const auto sliceFields = std::string {axis} + " = function() return {1} end"; @@ -1868,7 +1858,7 @@ TEST(IOTest, readShapeSet_luaPerpendicularSliceCallbackWrongTypeIncludesContext) readShapeSetFromString(makeLuaSliceCallbackInput(sliceFields), InputFormat::Lua); FAIL() << "Should have thrown"; } - catch(const KleeError &err) + catch(const KleeError& err) { EXPECT_THAT(err.what(), HasSubstr(std::string {"callback for '"} + axis + "'")); EXPECT_THAT(err.what(), HasSubstr("slice_callback")); @@ -1881,9 +1871,9 @@ TEST(IOTest, readShapeSet_luaPerpendicularSliceCallbacksAreValidated) { struct ValidationCase { - const char *field; - const char *returnValue; - const char *expectedMessage; + const char* field; + const char* returnValue; + const char* expectedMessage; }; const std::array cases {{ {"origin", "{20, 0, 0}", "slice plane"}, @@ -1891,21 +1881,19 @@ TEST(IOTest, readShapeSet_luaPerpendicularSliceCallbacksAreValidated) {"up", "{1, 0, 0}", "perpendicular"}, }}; - for(const auto &testCase : cases) + for(const auto& testCase : cases) { SCOPED_TRACE(testCase.field); - const auto sliceFields = - std::string {"x = function() return 10 end, "} + testCase.field + + const auto sliceFields = std::string {"x = function() return 10 end, "} + testCase.field + " = function() return " + testCase.returnValue + " end"; try { readShapeSetFromString(makeLuaSliceCallbackInput(sliceFields), InputFormat::Lua); FAIL() << "Should have thrown"; } - catch(const KleeError &err) + catch(const KleeError& err) { - EXPECT_THAT(err.what(), - HasSubstr(std::string {"callback for '"} + testCase.field + "'")); + EXPECT_THAT(err.what(), HasSubstr(std::string {"callback for '"} + testCase.field + "'")); EXPECT_THAT(err.what(), HasSubstr("shape 'slice_callback'")); EXPECT_THAT(err.what(), HasSubstr("operator 1")); EXPECT_THAT(err.what(), HasSubstr(testCase.expectedMessage)); @@ -1917,9 +1905,9 @@ TEST(IOTest, readShapeSet_luaArbitrarySliceCallbackValidationErrorsIncludeContex { struct ValidationCase { - const char *field; - const char *sliceFields; - const char *expectedMessage; + const char* field; + const char* sliceFields; + const char* expectedMessage; }; const std::array cases {{ {"normal", @@ -1934,7 +1922,7 @@ TEST(IOTest, readShapeSet_luaArbitrarySliceCallbackValidationErrorsIncludeContex "perpendicular"}, }}; - for(const auto &testCase : cases) + for(const auto& testCase : cases) { SCOPED_TRACE(testCase.field); try @@ -1942,10 +1930,9 @@ TEST(IOTest, readShapeSet_luaArbitrarySliceCallbackValidationErrorsIncludeContex readShapeSetFromString(makeLuaSliceCallbackInput(testCase.sliceFields), InputFormat::Lua); FAIL() << "Should have thrown"; } - catch(const KleeError &err) + catch(const KleeError& err) { - EXPECT_THAT(err.what(), - HasSubstr(std::string {"callback for '"} + testCase.field + "'")); + EXPECT_THAT(err.what(), HasSubstr(std::string {"callback for '"} + testCase.field + "'")); EXPECT_THAT(err.what(), HasSubstr("shape 'slice_callback'")); EXPECT_THAT(err.what(), HasSubstr("operator 1")); EXPECT_THAT(err.what(), HasSubstr(testCase.expectedMessage)); @@ -1957,20 +1944,18 @@ TEST(IOTest, readShapeSet_luaRotationCallbacksAreValidated) { struct ValidationCase { - const char *fields; - const char *field; - const char *expectedMessage; + const char* fields; + const char* field; + const char* expectedMessage; }; const std::array cases {{ {"rotate = function() return {45} end, axis = {0, 0, 1}", "rotate", "function call"}, {"rotate = 45, axis = function() return {0, 1} end", "axis", "Wrong size"}, - {"rotate = 45, axis = {0, 0, 1}, center = function() return {1, 2} end", - "center", - "Wrong size"}, + {"rotate = 45, axis = {0, 0, 1}, center = function() return {1, 2} end", "center", "Wrong size"}, {"rotate = 45, axis = function() return {0, 0, 0} end", "axis", "zero"}, }}; - for(const auto &testCase : cases) + for(const auto& testCase : cases) { SCOPED_TRACE(testCase.expectedMessage); try @@ -1978,10 +1963,9 @@ TEST(IOTest, readShapeSet_luaRotationCallbacksAreValidated) readShapeSetFromString(makeLuaRotationCallbackInput(testCase.fields), InputFormat::Lua); FAIL() << "Should have thrown"; } - catch(const KleeError &err) + catch(const KleeError& err) { - EXPECT_THAT(err.what(), - HasSubstr(std::string {"callback for '"} + testCase.field + "'")); + EXPECT_THAT(err.what(), HasSubstr(std::string {"callback for '"} + testCase.field + "'")); EXPECT_THAT(err.what(), HasSubstr("rotation_callback")); EXPECT_THAT(err.what(), HasSubstr("operator 1")); EXPECT_THAT(err.what(), HasSubstr(testCase.expectedMessage)); diff --git a/src/axom/quest/util/make_clipper_strategy.cpp b/src/axom/quest/util/make_clipper_strategy.cpp index 36ab48fa3d..e8dbc9f441 100644 --- a/src/axom/quest/util/make_clipper_strategy.cpp +++ b/src/axom/quest/util/make_clipper_strategy.cpp @@ -69,11 +69,11 @@ std::shared_ptr make_clipper_strategy(const axom::klee::Geo #if defined(AXOM_USE_BUMP) strategy.reset(new TetMeshClipper(kleeGeometry, name)); #else - SLIC_WARNING(axom::fmt::format( - "klee::Geometry format '{}' requires Axom to be configured with bump " - "but this build does not have it, so shape '{}' cannot be clipped.", - format, - name)); + SLIC_WARNING( + axom::fmt::format("klee::Geometry format '{}' requires Axom to be configured with bump " + "but this build does not have it, so shape '{}' cannot be clipped.", + format, + name)); #endif } else diff --git a/src/examples/shaping_tutorial/lesson_03/klee_operators_and_validation.cpp b/src/examples/shaping_tutorial/lesson_03/klee_operators_and_validation.cpp index 6017e0160e..539d1c4f7e 100644 --- a/src/examples/shaping_tutorial/lesson_03/klee_operators_and_validation.cpp +++ b/src/examples/shaping_tutorial/lesson_03/klee_operators_and_validation.cpp @@ -160,13 +160,10 @@ int main(int argc, char** argv) } std::ifstream initializationStream {initializationFilename}; - std::string initializationSource { - std::istreambuf_iterator(initializationStream), - {}}; + std::string initializationSource {std::istreambuf_iterator(initializationStream), {}}; axom::klee::LuaInputOptions options; - options.initialization = axom::klee::LuaInitializationChunk { - initializationSource, - initializationFilename}; + options.initialization = + axom::klee::LuaInitializationChunk {initializationSource, initializationFilename}; return axom::klee::readShapeSet(inputFilename, options); };