Skip to content

Commit ade1d62

Browse files
revarbatclaude
andauthored
RangeLiteral records whether the step was written (#6)
`[a:b]` and `[a:1:b]` produced identical ASTs, because makeRangeLiteral synthesizes the 1.0 for the two-argument form. That is fine for evaluating the range -- the value is the same either way -- but it throws away the one thing the evaluator needs to tell a typo from a choice: `[5:0]` is almost always `[5:-1:0]` written wrong, while `[5:1:0]` is someone saying what they meant. So keep a flag. The synthesized step node stays exactly where it was, so every consumer that just wants the value is untouched. toString() now prints back the form that was written. Emitting the synthesized step for a two-argument range produced a program that behaves identically but reads as an explicit step -- so reformatting a file would have quietly suppressed the evaluator's warning for it. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 8a651c3 commit ade1d62

5 files changed

Lines changed: 34 additions & 5 deletions

File tree

‎include/openscad_cpp_parser/ast/expression.hpp‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,14 +80,22 @@ class UndefinedLiteral : public Primary {
8080
class RangeLiteral : public Primary {
8181
public:
8282
RangeLiteral(Position position, std::unique_ptr<Expression> start, std::unique_ptr<Expression> end,
83-
std::unique_ptr<Expression> step)
83+
std::unique_ptr<Expression> step, bool implicitStep = false)
8484
: Primary(NodeKind::RangeLiteral, std::move(position)), start(std::move(start)), end(std::move(end)),
85-
step(std::move(step)) {}
85+
step(std::move(step)), implicitStep(implicitStep) {}
8686

8787
std::unique_ptr<Expression> start;
8888
std::unique_ptr<Expression> end;
8989
std::unique_ptr<Expression> step;
9090

91+
// True when the source wrote the two-argument form `[a:b]` and the step
92+
// node below is the literal 1.0 this parser synthesized for it. Consumers
93+
// that only need the value can ignore this and read `step` as always
94+
// present; it exists because "the author did not choose a step" is not
95+
// recoverable from the synthesized node, and the evaluator's backwards-range
96+
// warning fires only for that case (an explicit step is taken as deliberate).
97+
bool implicitStep = false;
98+
9199
std::string toString() const override;
92100
void buildScope(Scope& parentScope) override;
93101
};

‎src/ast/expression.cpp‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ std::string NumberLiteral::toString() const {
2222
}
2323

2424
std::string RangeLiteral::toString() const {
25+
// Print back the form that was written. Emitting the synthesized step for
26+
// a two-argument range would round-trip `[5:0]` into `[5 : 1 : 0]`, which
27+
// reads identically but suppresses the evaluator's backwards-range warning.
28+
if (implicitStep) return "[" + start->toString() + " : " + end->toString() + "]";
2529
return "[" + start->toString() + " : " + step->toString() + " : " + end->toString() + "]";
2630
}
2731

‎src/grammar/driver.cpp‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,13 @@ NodePtr makeUndefinedLiteral(ParserDriver& driver, const OscadLocation& loc) {
4646
}
4747

4848
NodePtr makeRangeLiteral(ParserDriver& driver, const OscadLocation& loc, NodePtr start, NodePtr end, NodePtr step) {
49-
if (!step) {
49+
const bool implicitStep = !step;
50+
if (implicitStep) {
5051
step = std::make_unique<NumberLiteral>(driver.toPosition(loc), 1.0);
5152
}
5253
return std::make_unique<RangeLiteral>(driver.toPosition(loc), nodeCast<Expression>(std::move(start)),
53-
nodeCast<Expression>(std::move(end)), nodeCast<Expression>(std::move(step)));
54+
nodeCast<Expression>(std::move(end)), nodeCast<Expression>(std::move(step)),
55+
implicitStep);
5456
}
5557

5658
NodePtr makePositionalArgument(ParserDriver& driver, const OscadLocation& loc, NodePtr expr) {

‎src/serialization/json_io.cpp‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ json toJsonImpl(const ASTNode& node, bool includePos) {
112112
j["start"] = valueToJson(n.start.get(), includePos);
113113
j["end"] = valueToJson(n.end.get(), includePos);
114114
j["step"] = valueToJson(n.step.get(), includePos);
115+
if (n.implicitStep) j["implicitStep"] = true;
115116
break;
116117
}
117118
case NodeKind::ParameterDeclaration: {
@@ -410,7 +411,8 @@ const std::unordered_map<std::string, Builder>& registry() {
410411
{"RangeLiteral",
411412
[](const json& j, Position pos) -> std::unique_ptr<ASTNode> {
412413
return std::make_unique<RangeLiteral>(std::move(pos), childFromJson<Expression>(j, "start"),
413-
childFromJson<Expression>(j, "end"), childFromJson<Expression>(j, "step"));
414+
childFromJson<Expression>(j, "end"), childFromJson<Expression>(j, "step"),
415+
j.value("implicitStep", false));
414416
}},
415417
{"ParameterDeclaration",
416418
[](const json& j, Position pos) -> std::unique_ptr<ASTNode> {

‎tests/test_pretty_print.cpp‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -515,3 +515,16 @@ TEST(PrettyPrint, AssignmentRhsWithLeadingCommentAndTrailingLineComment) {
515515
// no other translation unit -- including this test file -- can name them.
516516
// Left as a known, permanent coverage ceiling rather than weakening the
517517
// encapsulation just to reach 100%.
518+
519+
// The two-argument range keeps its shape through a print/reparse cycle.
520+
// It would be easy to always print the synthesized step, and the result
521+
// would still be a correct program -- but `[5:0]` and `[5:1:0]` mean
522+
// different things to the evaluator's backwards-range warning, so
523+
// reformatting a file must not quietly convert one into the other.
524+
TEST(PrettyPrint, ImplicitRangeStepSurvivesRoundTrip) {
525+
auto ast = parseAst("a = [5:0];\nb = [5:1:0];\n");
526+
const std::string printed = toOpenscad(ast);
527+
EXPECT_NE(printed.find("[5 : 0]"), std::string::npos) << printed;
528+
EXPECT_NE(printed.find("[5 : 1 : 0]"), std::string::npos) << printed;
529+
expectStablePrint("a = [5:0];\nb = [5:1:0];\n");
530+
}

0 commit comments

Comments
 (0)