fix: error on calculations with post-processed placeholders (#16597)

Placeholders that GCodeProcessor fills in after the G-code is generated
(print_time_total_sec and the other print_time_* parts,
used_filament_length, and the print statistics such as total_weight)
hold a tag such as @TOTAL_WEIGHT@ while the template is evaluated.
Multiplying one already failed with a type error. Adding a number
joined the tag as text, so {total_weight + 5} printed 123.455 for a
123.45 g print, and a comparison compared the tag as text, so
{if total_weight > 100} was always true. Neither gave an error.

PlaceholderParser::set_deferred() now registers such a value. Printing
it and joining it to text work as before. A calculation, a comparison,
a regex or a condition on it fails with an error that says the value is
known only after the G-code is generated, also after it was copied to a
variable or through regex_replace(). Adding a number or another
deferred value to one is treated as a sum and fails too. Text that
already contains one joins a number as text, like any other string.

filament_change() inside an {if} branch that is not taken now does
nothing. It used to set the current extruder from an uninitialized
value.
This commit is contained in:
Kris Austin authored and GitHub committed 2026-10-20 21:01:50 -05:00
1 parent 3d0cb36544
commit 8a14a0383f
5 files changed
+165 -22

No files matched your search

+14 -14
View File
@@ -3979,22 +3979,22 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
this->placeholder_parser().set("hold_chamber_temp_for_flat_print", new ConfigOptionBool(hold_chamber_temp_for_flat_print));
}
this->placeholder_parser().set("print_time_total_sec", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Print_Time_Total_Sec_Placeholder)));
this->placeholder_parser().set("print_time_day", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Print_Time_Day_Placeholder)));
this->placeholder_parser().set("print_time_hour", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Print_Time_Hour_Placeholder)));
this->placeholder_parser().set("print_time_minute", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Print_Time_Minute_Placeholder)));
this->placeholder_parser().set("print_time_sec", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Print_Time_Sec_Placeholder)));
this->placeholder_parser().set("used_filament_length", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Used_Filament_Length_Placeholder)));
this->placeholder_parser().set_deferred("print_time_total_sec", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Print_Time_Total_Sec_Placeholder));
this->placeholder_parser().set_deferred("print_time_day", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Print_Time_Day_Placeholder));
this->placeholder_parser().set_deferred("print_time_hour", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Print_Time_Hour_Placeholder));
this->placeholder_parser().set_deferred("print_time_minute", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Print_Time_Minute_Placeholder));
this->placeholder_parser().set_deferred("print_time_sec", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Print_Time_Sec_Placeholder));
this->placeholder_parser().set_deferred("used_filament_length", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Used_Filament_Length_Placeholder));
// The final print statistics are known only after the whole G-code is generated, so they are filled in during post-processing too.
// The running extruded_* totals are left alone; profiles use them as numbers.
this->placeholder_parser().set("used_filament", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Used_Filament_Length_Placeholder)));
this->placeholder_parser().set("print_time", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Normal_Print_Time_Placeholder)));
this->placeholder_parser().set("normal_print_time", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Normal_Print_Time_Placeholder)));
this->placeholder_parser().set("silent_print_time", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Silent_Print_Time_Placeholder)));
this->placeholder_parser().set("total_weight", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Total_Weight_Placeholder)));
this->placeholder_parser().set("total_cost", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Total_Cost_Placeholder)));
this->placeholder_parser().set("total_wipe_tower_cost", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Total_Wipe_Tower_Cost_Placeholder)));
this->placeholder_parser().set("total_wipe_tower_filament", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Total_Wipe_Tower_Filament_Placeholder)));
this->placeholder_parser().set_deferred("used_filament", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Used_Filament_Length_Placeholder));
this->placeholder_parser().set_deferred("print_time", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Normal_Print_Time_Placeholder));
this->placeholder_parser().set_deferred("normal_print_time", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Normal_Print_Time_Placeholder));
this->placeholder_parser().set_deferred("silent_print_time", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Silent_Print_Time_Placeholder));
this->placeholder_parser().set_deferred("total_weight", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Total_Weight_Placeholder));
this->placeholder_parser().set_deferred("total_cost", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Total_Cost_Placeholder));
this->placeholder_parser().set_deferred("total_wipe_tower_cost", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Total_Wipe_Tower_Cost_Placeholder));
this->placeholder_parser().set_deferred("total_wipe_tower_filament", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Total_Wipe_Tower_Filament_Placeholder));
// Sync variant-mapped params into placeholder_parser before processing start gcode
update_placeholder_parser_with_variant_params();
+81 -8
View File
@@ -243,7 +243,7 @@ namespace client
struct expr
{
expr() {}
expr(const expr &rhs) : m_type(rhs.type()), it_range(rhs.it_range)
expr(const expr &rhs) : m_type(rhs.type()), it_range(rhs.it_range), m_deferred(rhs.m_deferred)
{ if (rhs.type() == TYPE_STRING) m_data.s = new std::string(*rhs.m_data.s); else m_data.set(rhs.m_data); }
expr(expr &&rhs) : expr(std::move(rhs), rhs.it_range.begin(), rhs.it_range.end()) {}
@@ -258,13 +258,16 @@ namespace client
explicit expr(std::string &&s) : m_type(TYPE_STRING) { m_data.s = new std::string(std::move(s)); }
explicit expr(const std::string &s, const Iterator &it_begin, const Iterator &it_end) :
m_type(TYPE_STRING), it_range(it_begin, it_end) { m_data.s = new std::string(s); }
explicit expr(expr &&rhs, const Iterator &it_begin, const Iterator &it_end) : m_type(rhs.type()), it_range{ it_begin, it_end }
explicit expr(expr &&rhs, const Iterator &it_begin, const Iterator &it_end) : m_type(rhs.type()), it_range{ it_begin, it_end }, m_deferred(rhs.m_deferred)
{
m_data.set(rhs.m_data);
rhs.m_type = TYPE_EMPTY;
rhs.m_type = TYPE_EMPTY;
rhs.m_deferred = Deferred::None;
}
expr &operator=(const expr &rhs)
{
// set_s() clears m_deferred, which is also rhs.m_deferred on a self-assignment.
const Deferred deferred = rhs.m_deferred;
if (rhs.type() == TYPE_STRING) {
this->set_s(rhs.s());
} else {
@@ -272,6 +275,7 @@ namespace client
m_data.set(rhs.m_data);
}
this->it_range = rhs.it_range;
m_deferred = deferred;
return *this;
}
@@ -281,8 +285,10 @@ namespace client
this->reset();
m_type = rhs.type();
this->it_range = rhs.it_range;
m_deferred = rhs.m_deferred;
m_data.set(rhs.m_data);
rhs.m_type = TYPE_EMPTY;
rhs.m_deferred = Deferred::None;
}
return *this;
}
@@ -291,7 +297,8 @@ namespace client
{
if (this->type() == TYPE_STRING)
delete m_data.s;
m_type = TYPE_EMPTY;
m_type = TYPE_EMPTY;
m_deferred = Deferred::None;
}
~expr() { reset(); }
@@ -304,6 +311,10 @@ namespace client
};
Type type() const { return m_type; }
bool numeric_type() const { return m_type == TYPE_INT || m_type == TYPE_DOUBLE; }
// A deferred string is either one deferred value or text that contains at least one.
enum class Deferred : unsigned char { None, Value, Text };
bool deferred() const { return m_deferred != Deferred::None; }
void set_deferred(Deferred deferred) { m_deferred = deferred; }
bool& b() { return m_data.b; }
bool b() const { return m_data.b; }
@@ -323,18 +334,21 @@ namespace client
std::string& s() { return *m_data.s; }
const std::string& s() const { return *m_data.s; }
void set_s(const std::string &s) {
m_deferred = Deferred::None;
if (this->type() == TYPE_STRING)
*m_data.s = s;
else
this->set_s_take_ownership(new std::string(s));
}
void set_s(std::string &&s) {
m_deferred = Deferred::None;
if (this->type() == TYPE_STRING)
*m_data.s = std::move(s);
else
this->set_s_take_ownership(new std::string(std::move(s)));
}
void set_s(const char *s) {
m_deferred = Deferred::None;
if (this->type() == TYPE_STRING)
*m_data.s = s;
else
@@ -478,12 +492,21 @@ namespace client
{
if (this->type() == TYPE_EMPTY) {
// Inside an if / else block to be skipped.
} else if (m_deferred == Deferred::Value && (rhs.type() != TYPE_STRING || rhs.m_deferred == Deferred::Value)) {
// A deferred value added to a number or to another deferred value is meant as a sum.
this->throw_if_deferred();
} else if (rhs.m_deferred == Deferred::Value && this->type() != TYPE_STRING) {
rhs.throw_if_deferred();
} else if (this->type() == TYPE_STRING) {
// Convert the right hand side to string and append.
*m_data.s += rhs.to_string();
if (this->deferred() || rhs.deferred())
m_deferred = Deferred::Text;
} else if (rhs.type() == TYPE_STRING) {
// Conver the left hand side to string, append rhs.
this->set_s(this->to_string() + rhs.s());
if (rhs.deferred())
m_deferred = Deferred::Text;
} else {
const char *err_msg = L("Cannot add non-numeric types.");
this->throw_if_not_numeric(err_msg);
@@ -602,6 +625,8 @@ namespace client
if (lhs.type() == TYPE_EMPTY)
// Inside an if / else block to be skipped
return;
lhs.throw_if_deferred();
rhs.throw_if_deferred();
bool value = false;
if (lhs.numeric_type() && rhs.numeric_type()) {
// Both types are numeric.
@@ -731,6 +756,7 @@ namespace client
if (lhs.type() == TYPE_EMPTY)
// Inside an if / else block to be skipped
return;
lhs.throw_if_deferred();
const std::string *subject = nullptr;
if (lhs.type() == TYPE_STRING) {
// One type is string, the other could be converted to string.
@@ -761,6 +787,7 @@ namespace client
if (subject.type() == TYPE_EMPTY)
// Inside an if / else block to be skipped
return;
subject.throw_if_deferred();
if (subject.type() != TYPE_STRING)
subject.throw_exception(L("regex_replace() first parameter must be a string."));
try {
@@ -785,6 +812,8 @@ namespace client
return;
}
if (! out.b()) {
match.throw_if_deferred();
pattern.throw_if_deferred();
if (match.type() != TYPE_STRING)
match.throw_exception(L("one_of(): First parameter (the string to match against) has to be a string value"));
if (pattern.type() != TYPE_STRING)
@@ -818,6 +847,8 @@ namespace client
if (lhs.type() == TYPE_EMPTY)
// Inside an if / else block to be skipped
return;
lhs.throw_if_deferred();
rhs.throw_if_deferred();
bool value = false;
if (lhs.type() == TYPE_BOOL && rhs.type() == TYPE_BOOL) {
value = (op == '|') ? (lhs.b() || rhs.b()) : (lhs.b() && rhs.b());
@@ -830,8 +861,16 @@ namespace client
static void logical_or (expr &lhs, expr &rhs) { logical_op(lhs, rhs, '|'); }
static void logical_and(expr &lhs, expr &rhs) { logical_op(lhs, rhs, '&'); }
static const char* deferred_message()
{
return L("This value is known only after the G-code is generated. It can be printed, but not used in a calculation or a comparison.");
}
// Reports the deferred message for a type error on a deferred value, since it holds a tag in place of the value.
void throw_exception(const char *message) const
{
if (this->deferred())
message = deferred_message();
boost::throw_exception(qi::expectation_failure<Iterator>(
this->it_range.begin(), this->it_range.end(), spirit::info(std::string("*") + message)));
}
@@ -842,11 +881,18 @@ namespace client
this->throw_exception(message);
}
void throw_if_deferred() const
{
if (this->deferred())
this->throw_exception(deferred_message());
}
private:
// This object will take ownership of the parameter string object "s".
void set_s_take_ownership(std::string* s) { assert(this->type() != TYPE_STRING); Data tmp; tmp.s = s; m_data.set(tmp); m_type = TYPE_STRING; }
Type m_type = TYPE_EMPTY;
Deferred m_deferred = Deferred::None;
union Data {
bool b;
@@ -895,6 +941,8 @@ namespace client
std::string error_message;
// Local variables declared in {if} branches that were not taken, see PlaceholderParser::check_inactive_branches.
mutable std::set<std::string> inactive_local_variables;
// Tags of PlaceholderParser::set_deferred(), matched by value so that a copy in a variable is deferred too.
const std::set<std::string> *deferred_tags = nullptr;
// Table to translate symbol tag to a human readable error message.
static std::map<std::string, std::string> tag_to_error_message;
@@ -970,6 +1018,18 @@ namespace client
} else
this->config_local.set_key_value(opt_key, opt.release());
}
void mark_deferred(expr &output) const {
if (output.type() != expr::TYPE_STRING)
return;
expr::Deferred deferred = expr::Deferred::None;
if (deferred_tags != nullptr && output.s().find('@') != std::string::npos)
for (const std::string &tag : *deferred_tags)
if (output.s().find(tag) != std::string::npos) {
deferred = output.s() == tag ? expr::Deferred::Value : expr::Deferred::Text;
break;
}
output.set_deferred(deferred);
}
static void legacy_variable_expansion(const MyContext *ctx, IteratorRange &opt_key, std::string &output)
{
@@ -1430,6 +1490,7 @@ namespace client
vector_element_to_expr(ctx, opt, output);
else
scalar_variable_to_expr(ctx, opt, output);
ctx->mark_deferred(output);
}
output.it_range = opt.it_range;
}
@@ -1516,7 +1577,7 @@ namespace client
if (ctx->skipping()) {
} else if (lhs.opt) {
if (lhs.opt->is_vector())
rhs.throw_exception(L("Cannot assign a scalar value to a vector variable."));
ctx->throw_exception(L("Cannot assign a scalar value to a vector variable."), rhs.it_range);
OptWithPos lhs_opt{ lhs.opt, lhs.it_range, true };
scalar_variable_assign_scalar(ctx, lhs_opt, rhs);
} else {
@@ -1542,7 +1603,7 @@ namespace client
if (ctx->skipping()) {
} else if (lhs.opt) {
if (lhs.opt->is_scalar())
rhs_value.throw_exception(L("Cannot assign a vector value to a scalar variable."));
ctx->throw_exception(L("Cannot assign a vector value to a scalar variable."), rhs_value.it_range);
OptWithPos lhs_opt{ lhs.opt, lhs.it_range, true };
vector_variable_assign_expr_with_count(ctx, lhs_opt, rhs_count, rhs_value);
} else {
@@ -1568,7 +1629,7 @@ namespace client
if (! ctx->skipping()) {
check_writable(ctx, lhs);
if (lhs.opt->is_scalar())
rhs_value.throw_exception(L("Cannot assign a vector value to a scalar variable."));
ctx->throw_exception(L("Cannot assign a vector value to a scalar variable."), rhs_value.it_range);
vector_variable_assign_expr_with_count(ctx, lhs, rhs_count, rhs_value);
}
}
@@ -1803,10 +1864,21 @@ namespace client
static void filament_change(const MyContext* ctx, expr& param)
{
if (param.type() == expr::TYPE_EMPTY)
// Inside an if / else block to be skipped
return;
MyContext *context = const_cast<MyContext *>(ctx);
param.throw_if_deferred();
context->current_extruder_id = param.as_i();
}
// The replacement may put a deferred tag into the result.
static void regex_replace(const MyContext *ctx, expr &subject, IteratorRange &pattern, expr &replacement)
{
expr::regex_replace(subject, pattern, replacement);
ctx->mark_deferred(subject);
}
static void throw_exception(const std::string &msg, const IteratorRange &it_range)
{
// An asterix is added to the start of the string to differentiate the boost::spirit::info::tag content
@@ -2409,7 +2481,7 @@ namespace client
| (kw["zdigits"] > '(' > conditional_expression(_r1) [_val = _1] > ',' > conditional_expression(_r1) > optional_parameter(_r1))
[ px::bind(&expr::digits<true>, _val, _2, _3) ]
| (kw["regex_replace"] > '(' > conditional_expression(_r1) [_val = _1] > ',' > regular_expression > ',' > conditional_expression(_r1) > ')')
[ px::bind(&expr::regex_replace, _val, _2, _3) ]
[ px::bind(&MyContext::regex_replace, _r1, _val, _2, _3) ]
| (kw["int"] > '(' > conditional_expression(_r1) > ')') [ px::bind(&FactorActions::to_int, _1, _val) ]
| (kw["round"] > '(' > conditional_expression(_r1) > ')') [ px::bind(&FactorActions::round, _1, _val) ]
| (kw["ceil"] > '(' > conditional_expression(_r1) > ')') [ px::bind(&FactorActions::ceil, _1, _val) ]
@@ -2614,6 +2686,7 @@ std::string PlaceholderParser::process(const std::string &templ, unsigned int cu
context.config = &this->config();
context.config_override = config_override;
context.config_outputs = config_outputs;
context.deferred_tags = &m_deferred_tags;
context.current_extruder_id = current_extruder_id;
context.context_data = context_data;
return process_macro(templ, context);
+4
View File
@@ -60,6 +60,9 @@ public:
void set(const std::string &key, double value) { this->set(key, new ConfigOptionFloat(value)); }
void set(const std::string &key, const std::vector<std::string> &values) { this->set(key, new ConfigOptionStrings(values)); }
void set(const std::string &key, ConfigOption *opt) { m_config.set_key_value(key, opt); }
// Orca: sets a value known only after the G-code is generated, as a tag that post-processing replaces.
// A template may print it or join it to text, and a calculation or a comparison on it throws.
void set_deferred(const std::string &key, const std::string &tag) { this->set(key, tag); m_deferred_tags.insert(tag); }
DynamicConfig& config_writable() { return m_config; }
const DynamicConfig& config() const { return m_config; }
const ConfigOption* option(const std::string &key) const { return m_config.option(key); }
@@ -88,6 +91,7 @@ private:
// config has a higher priority than external_config when looking up a symbol.
DynamicConfig m_config;
const DynamicConfig *m_external_config;
std::set<std::string> m_deferred_tags;
};
}
+15
View File
@@ -22,6 +22,7 @@
#include "libslic3r/TriangleMesh.hpp"
#include <catch2/matchers/catch_matchers.hpp>
#include <catch2/matchers/catch_matchers_floating_point.hpp>
#include <catch2/matchers/catch_matchers_string.hpp>
#include <catch2/catch_all.hpp>
@@ -794,6 +795,20 @@ TEST_CASE("Print statistics in custom G-code match the final print statistics",
}
}
TEST_CASE("Print statistics in custom G-code cannot be used in a calculation", "[Print]")
{
const std::string key = GENERATE(as<std::string>{}, "total_weight", "used_filament", "total_cost", "print_time", "normal_print_time",
"silent_print_time", "total_wipe_tower_cost", "total_wipe_tower_filament", "print_time_total_sec",
"print_time_day", "print_time_hour", "print_time_minute", "print_time_sec", "used_filament_length");
CAPTURE(key);
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
config.set_deserialize_strict({ { "file_start_gcode", "; {" + key + " + 1}\n" } });
Print print;
Model model;
init_print({ cube(20) }, print, model, config);
CHECK_THROWS_WITH(Slic3r::Test::gcode(print), Catch::Matchers::ContainsSubstring("known only after the G-code is generated"));
}
TEST_CASE("export_gcode writes G-code without a result pointer", "[Print][export_gcode]")
{
Print print;
@@ -5,6 +5,7 @@
#include <catch2/catch_test_macros.hpp>
#include <catch2/catch_approx.hpp>
#include <catch2/catch_message.hpp>
#include <catch2/matchers/catch_matchers.hpp>
#include <catch2/matchers/catch_matchers_string.hpp>
#include "libslic3r/PlaceholderParser.hpp"
@@ -398,3 +399,53 @@ SCENARIO("Placeholder parser names in branches that are not taken", "[Placeholde
CHECK(PlaceholderParser::evaluate_boolean_expression("false ? no_such_var == 1 : true", config));
}
}
TEST_CASE("Placeholder parser deferred values can be printed but not computed with", "[PlaceholderParser]") {
PlaceholderParser parser;
parser.set_deferred("total_weight", "@TOTAL_WEIGHT@");
parser.set_deferred("used_filament", "@USED_FILAMENT_LENGTH@");
parser.set("label", "W");
PlaceholderParser::ContextData context;
context.global_config = std::make_unique<DynamicConfig>();
auto process = [&parser, &context](const std::string &templ) { return parser.process(templ, 0, nullptr, nullptr, &context); };
SECTION("printed") {
CHECK(process("{total_weight}") == "@TOTAL_WEIGHT@");
CHECK(process("[total_weight]") == "@TOTAL_WEIGHT@");
CHECK(process("{label + \": \" + total_weight + \"g\"}") == "W: @TOTAL_WEIGHT@g");
CHECK(process("{total_weight + \"g, \" + used_filament + \"m\"}") == "@TOTAL_WEIGHT@g, @USED_FILAMENT_LENGTH@m");
CHECK(process("{\"W \" + total_weight + \"g for \" + 3 + \" parts\"}") == "W @TOTAL_WEIGHT@g for 3 parts");
CHECK(process("{true ? total_weight : 0}") == "@TOTAL_WEIGHT@");
CHECK(process("{regex_replace(\"x\", /x/, total_weight)}") == "@TOTAL_WEIGHT@");
CHECK(process("{local w = total_weight}{w}") == "@TOTAL_WEIGHT@");
}
SECTION("used in a calculation or a comparison") {
const auto deferred_error = Catch::Matchers::ContainsSubstring("known only after the G-code is generated");
for (const std::string templ : {
"{total_weight + 1}", "{1 + total_weight}", "{total_weight * 2}", "{-total_weight}", "{int(total_weight)}",
"{min(total_weight, 5)}", "{digits(total_weight, 5)}", "{total_weight + used_filament}",
"{if total_weight > 100}heavy{endif}", "{if total_weight == \"1\"}one{endif}", "{total_weight and true}",
"{total_weight ? 1 : 0}", "{if total_weight =~ /1.*/}one{endif}", "{one_of(total_weight, \"1\")}",
"{regex_replace(total_weight, /1/, \"2\")}", "{(\"W\" + total_weight) == \"W1\"}", "{filament_change(total_weight)}",
"{regex_replace(\"x\", /x/, total_weight) * 2}", "{if regex_replace(\"x\", /x/, \"W\" + total_weight) > \"W1\"}heavy{endif}",
// A deferred value copied to a variable is still deferred.
"{local w = total_weight}{w > 1}", "{global g = \"W\" + total_weight}{if g == \"W1\"}one{endif}" }) {
CAPTURE(templ);
CHECK_THROWS_WITH(process(templ), deferred_error);
}
// A mistake unrelated to the value being deferred keeps its own message.
CHECK_THROWS_WITH(process("{global v = (1, 2)}{global v = \"W\" + total_weight}"),
Catch::Matchers::ContainsSubstring("Cannot assign a scalar value to a vector variable"));
}
SECTION("ignored in a branch that is not taken") {
CHECK(process("{if false}{total_weight * 2}{endif}ok") == "ok");
struct Restore { ~Restore() { PlaceholderParser::check_inactive_branches = false; } } restore;
PlaceholderParser::check_inactive_branches = true;
CHECK(process("{if false}{total_weight * 2}{endif}ok") == "ok");
}
SECTION("other strings keep their behavior") {
CHECK(process("{label + 1}") == "W1");
CHECK(process("{if label > \"A\"}yes{endif}") == "yes");
CHECK(process("{if regex_replace(label, /X/, total_weight) == \"W\"}yes{endif}") == "yes");
}
}