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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion examples/recipe_registry_gen.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,18 @@ int main(int argc, char **argv) {

int exit_code = 0;
for (auto const &entry : numsim::examples::registry()) {
std::cout << entry.name << " -> " << target->target_name() << "\n";
auto model = entry.build();
// Capability query BEFORE emit (#137): a recipe outside the target's
// scope is reported and skipped, so one unsupported recipe cannot abort
// the rest of the catalogue (#135). Deliberately NO try/catch around
// emit() — a recipe that passes can_emit but fails to emit is a defect,
// and the crash keeps this generator useful as a loud smoke check.
if (auto const supported = target->can_emit(model); !supported) {
std::cout << entry.name << " -> " << target->target_name()
<< " SKIPPED (" << supported.error() << ")\n";
continue;
}
std::cout << entry.name << " -> " << target->target_name() << "\n";
for (auto const &file : target->emit(model)) {
if (!write_file(out_dir, file)) {
exit_code = 2;
Expand Down
5 changes: 5 additions & 0 deletions include/numsim_codegen/targets/moose_material.h
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ class MooseMaterialTarget : public Target {

[[nodiscard]] auto emit(ConstitutiveModel const &model) const
-> std::vector<EmittedFile> override;
// Up-front scope guards (stateful inputs, evolution without local Newton,
// >1 tangent, `Jacobian_mult` name collision) as a query; shares the exact
// reason strings with the emit() throws (#137).
[[nodiscard]] auto can_emit(ConstitutiveModel const &model) const
-> std::expected<void, std::string> override;
[[nodiscard]] auto target_name() const -> std::string override;

private:
Expand Down
8 changes: 8 additions & 0 deletions include/numsim_codegen/targets/numsim_material.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@ class NumSimMaterialTarget : public Target {
public:
[[nodiscard]] auto emit(ConstitutiveModel const &model) const
-> std::vector<EmittedFile> override;
// Up-front scope guards as a query, routed per sub-contract exactly like
// emit(): the Mode-B residual scope for residual recipes, the rk_integrator
// rate scope otherwise. Shares the exact reason strings with the emit()
// throws (#137). Success does not guarantee emit() succeeds — emit-time
// validation (name collisions, unbound leaves, non-finite defaults) may
// still throw.
[[nodiscard]] auto can_emit(ConstitutiveModel const &model) const
-> std::expected<void, std::string> override;
[[nodiscard]] auto target_name() const -> std::string override;
};

Expand Down
3 changes: 3 additions & 0 deletions include/numsim_codegen/targets/standalone_cxx.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ class StandaloneCxxTarget : public Target {
StandaloneCxxTarget(LinearAlgebraEmitter const &&) = delete;
[[nodiscard]] auto emit(ConstitutiveModel const &model) const
-> std::vector<EmittedFile> override;
// can_emit (#137): not overridden — this target has no up-front scope
// guards (it accepts every recipe shape), so the base's conservative
// "try emit" success is exact.
[[nodiscard]] auto target_name() const -> std::string override;

private:
Expand Down
18 changes: 18 additions & 0 deletions include/numsim_codegen/targets/target.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

#include <numsim_codegen/recipe.h>

#include <expected>
#include <string>
#include <vector>

Expand Down Expand Up @@ -39,6 +40,23 @@ class Target {
[[nodiscard]] virtual auto emit(ConstitutiveModel const &model) const
-> std::vector<EmittedFile> = 0;

// Capability query (#137): would this target's UP-FRONT scope guards accept
// the recipe's shape? On rejection the error carries the exact reason string
// the matching emit() throw uses — one message, two transports — so a
// generic driver can skip-and-report without catching exceptions.
//
// Success does NOT guarantee emit() succeeds: can_emit checks only the
// up-front recipe-shape guards; emit-time validation (name collisions with
// synthesized members, unbound expression leaves, non-finite parameter
// defaults, pass-level checks) may still throw. The default implementation
// is conservatively permissive — it reports success ("try emit"), so a
// target without shape guards needs no override and one with them still
// rejects loudly inside emit().
[[nodiscard]] virtual auto can_emit(ConstitutiveModel const & /*model*/) const
-> std::expected<void, std::string> {
return {};
}

// Human-readable name of the target framework — used in error messages
// and diagnostics.
[[nodiscard]] virtual auto target_name() const -> std::string = 0;
Expand Down
82 changes: 53 additions & 29 deletions src/targets/moose_material.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@
#include <numsim_codegen/recipe.h>

#include <cmath>
#include <expected>
#include <format>
#include <optional>
#include <ostream>
#include <sstream>
#include <stdexcept>
#include <string>
#include <utility>

namespace numsim::codegen {

Expand Down Expand Up @@ -438,12 +441,17 @@ auto emit_source(ConstitutiveModel const &model, std::string const &app_name,
return os.str();
}

} // anonymous namespace

// ─── Public methods ────────────────────────────────────────────

auto MooseMaterialTarget::emit(ConstitutiveModel const &model) const
-> std::vector<EmittedFile> {
// ─── Up-front scope guards ─────────────────────────────────────
//
// The recipe-SHAPE preconditions this backend rejects, computed ONCE and used
// by both can_emit (returned as std::unexpected) and emit (thrown) — one
// message, two transports, no drift (#137). Returns the first rejection
// reason, or nullopt when the shape is in scope. Deeper emit-time validation
// (tensor-rank storage mapping, non-finite parameter defaults, non-constant
// state initials) is NOT checked here — can_emit success does not guarantee
// emit success.
auto scope_rejection(ConstitutiveModel const &model)
-> std::optional<std::string> {
// Stateful symbols would need old/new MaterialProperty pair handling
// that the MOOSE backend doesn't implement yet. Fail loudly rather
// than silently emit a regular read.
Expand All @@ -455,12 +463,11 @@ auto MooseMaterialTarget::emit(ConstitutiveModel const &model) const
// the output guard, mirror the pattern below.
for (auto const &i : model.inputs()) {
if (i.role.is_stateful) {
throw std::runtime_error(
"MooseMaterialTarget: stateful role '" + i.role.name +
"' on input '" + i.name +
"' requires the History machinery (old/new MaterialProperty pair, "
"stateful initialisation) which is not implemented in this phase. "
"See the numsim-codegen Phase B roadmap.");
return "MooseMaterialTarget: stateful role '" + i.role.name +
"' on input '" + i.name +
"' requires the History machinery (old/new MaterialProperty pair, "
"stateful initialisation) which is not implemented in this phase. "
"See the numsim-codegen Phase B roadmap.";
}
}

Expand All @@ -473,24 +480,22 @@ auto MooseMaterialTarget::emit(ConstitutiveModel const &model) const
// residual/Jacobian-output mode is for external drivers via the
// standalone target.
if (!model.evolution_equations().empty() && !model.local_newton_enabled()) {
throw std::runtime_error(
"MooseMaterialTarget: recipe '" + model.name() +
"' has evolution equations but local Newton solving is not enabled. "
"Call enable_local_newton() so the generated MOOSE Material solves "
"its state variables internally. (The residual/Jacobian-output mode "
"is for external drivers — use StandaloneCxxTarget for that.)");
return "MooseMaterialTarget: recipe '" + model.name() +
"' has evolution equations but local Newton solving is not enabled. "
"Call enable_local_newton() so the generated MOOSE Material solves "
"its state variables internally. (The residual/Jacobian-output mode "
"is for external drivers — use StandaloneCxxTarget for that.)";
}
// Phase 5 (issue #37): all consistent tangents map to the single framework
// `_Jacobian_mult` property — more than one would emit a duplicate member /
// property. MOOSE has exactly one consistent-tangent slot. (Tangents live in
// `tangents()`, not `outputs()`, on the pre-pass model the backend sees.)
if (model.tangents().size() > 1) {
throw std::runtime_error(
"MooseMaterialTarget: recipe '" + model.name() +
"' requests more than one consistent tangent "
"(roles::ConsistentTangent)."
" MOOSE has a single _Jacobian_mult slot, so only one tangent can be "
"wired. Emit additional tangents via StandaloneCxxTarget.");
return "MooseMaterialTarget: recipe '" + model.name() +
"' requests more than one consistent tangent "
"(roles::ConsistentTangent)."
" MOOSE has a single _Jacobian_mult slot, so only one tangent can "
"be wired. Emit additional tangents via StandaloneCxxTarget.";
}
// PR #82 review: when a tangent is wired, the backend emits a hardcoded
// `_Jacobian_mult` member. A regular output literally named "Jacobian_mult"
Expand All @@ -499,14 +504,33 @@ auto MooseMaterialTarget::emit(ConstitutiveModel const &model) const
if (!model.tangents().empty()) {
for (auto const &o : model.outputs()) {
if (o.name == "Jacobian_mult") {
throw std::runtime_error(
"MooseMaterialTarget: recipe '" + model.name() +
"' has both a consistent tangent and an output named "
"'Jacobian_mult', which collides with the framework consistent-"
"tangent member. Rename the output.");
return "MooseMaterialTarget: recipe '" + model.name() +
"' has both a consistent tangent and an output named "
"'Jacobian_mult', which collides with the framework consistent-"
"tangent member. Rename the output.";
}
}
}
return std::nullopt;
}

} // anonymous namespace

// ─── Public methods ────────────────────────────────────────────

auto MooseMaterialTarget::can_emit(ConstitutiveModel const &model) const
-> std::expected<void, std::string> {
if (auto reason = scope_rejection(model)) {
return std::unexpected(std::move(*reason));
}
return {};
}

auto MooseMaterialTarget::emit(ConstitutiveModel const &model) const
-> std::vector<EmittedFile> {
if (auto const reason = scope_rejection(model)) {
throw std::runtime_error(*reason);
}
return {
EmittedFile{model.name() + ".h", emit_header(model), "include/materials",
EmittedFile::Kind::Header},
Expand Down
Loading
Loading