Slice-validate every custom G-code and filename_format in system profiles (#15966)

# Description

Follow-up to #15950, where a `filename_format` using
`initial_no_support_extruder` shipped broken because the profile
validator's slice sweep never expands `filename_format`. The sweep now
expands every custom G-code and `filename_format` text shipped in any
system profile. Each printer's slice also fires the pause, template
custom G-code and clumping-detection hooks and names the output file,
and the first printer shipping a `printing_by_object_gcode` also slices
by object. Beyond each printer's default process and filament, every
compatible system process and filament carrying a template text no
earlier slice has expanded is sliced once, which takes the sweep from
1,110 to 1,248 slices (about 49 s locally, up from 43 s). While the
sweep runs, the placeholder parser also resolves variable names inside
`{if}` branches a slice does not take, so one expansion checks every
branch.

The stricter sweep found two profile bugs, fixed here: the Anycubic
Kobra X filament change G-code carried an unreachable block reading
variables only Anycubic's own slicer defines, and the Wanhao France D12
template custom G-code had an unterminated `{if}`, so adding a template
custom G-code on those printers failed the slice. It also fixes a parser
bug where a declaration such as `{local a = layer_height + 1}` failed to
parse inside a branch that is not taken.

No change to slicing output for templates that already worked: the
untaken-branch check is enabled only by the validator's slice mode, and
the parser fix only lets previously rejected templates parse.

# Screenshots/Recordings/Graphs

<!--
> Please attach relevant screenshots to showcase the UI changes.
> Please attach images that can help explain the changes.
-->

## Tests

New placeholder-parser cases cover the parse fix and the untaken-branch
check: off by default it changes nothing; on, it rejects undefined names
in branches not taken, accepts names declared there, and leaves boolean
expressions (compatibility conditions) alone. The full sweep passes on
all system profiles, and planting an undefined variable in an untaken
branch of a printer's start G-code, of a non-default filament's start
G-code or of a non-default process's `filename_format`, or reverting
#15950, each fails it and names the preset. `scripts/check_profile.sh`,
`libslic3r_tests` and `fff_print_tests` pass.

<!--
> A guide for users on how to download the artifacts from this PR.
-->

[How to Download Pull Requests Artifacts for
Testing](https://www.orcaslicer.com/wiki/how_to_download_pr_artifacts)
This commit is contained in:
SoftFever
2026-09-29 10:39:17 +08:00
committed by GitHub
8 changed files with 361 additions and 114 deletions
@@ -356,3 +356,38 @@ SCENARIO("Placeholder parser coFloatsOrPercents vector access", "[PlaceholderPar
REQUIRE(std::stod(parser.process("{pressure_advance[2]}")) == Catch::Approx(3.0));
}
}
SCENARIO("Placeholder parser names in branches that are not taken", "[PlaceholderParser]") {
PlaceholderParser parser;
auto config = DynamicPrintConfig::full_print_config();
parser.apply_config(config);
parser.set("idx", 0);
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("a declaration continuing after a variable reference parses when not taken") {
REQUIRE(process("{if false}{local a = layer_height + 1}{endif}ok") == "ok");
}
SECTION("names are not checked by default") {
REQUIRE(process("{if false}{no_such_var}[no_such_var]{endif}ok") == "ok");
}
SECTION("names must resolve when check_inactive_branches is set") {
struct Restore { ~Restore() { PlaceholderParser::check_inactive_branches = false; } } restore;
PlaceholderParser::check_inactive_branches = true;
CHECK_THROWS_WITH(process("{if false}{no_such_var}{endif}"), Catch::Matchers::ContainsSubstring("Not a variable name (in an inactive branch)"));
CHECK_THROWS_WITH(process("{if false}[no_such_var]{endif}"), Catch::Matchers::ContainsSubstring("Variable does not exist (in an inactive branch)"));
CHECK_THROWS_WITH(process("{if false}[nozzle_temperature[no_such_var]]{endif}"), Catch::Matchers::ContainsSubstring("Variable does not exist (in an inactive branch)"));
CHECK_THROWS(process("{if true}{else}{no_such_var}{endif}"));
CHECK_THROWS(process("{if false}{local a = no_such_var + 1}{endif}"));
CHECK(process("{if false}{layer_height}[layer_height][nozzle_temperature_0][nozzle_temperature[idx]]{endif}ok") == "ok");
CHECK(process("{if false}{local a = 1}{a = a + 1}{a}{endif}{if false}{a}{endif}ok") == "ok");
// A global declared in a branch that is not taken counts as defined for later expansions sharing the context.
CHECK(process("{if false}{global g = 1}{endif}{if false}{g}{endif}ok") == "ok");
CHECK(process("{if false}{g}{endif}ok") == "ok");
// Boolean expressions are not checked, so compatibility conditions keep their behaviour.
CHECK(PlaceholderParser::evaluate_boolean_expression("false ? no_such_var == 1 : true", config));
}
}