diff --git a/src/openscad_parser/ast/nodes.py b/src/openscad_parser/ast/nodes.py index 4a1649e..d5fa330 100644 --- a/src/openscad_parser/ast/nodes.py +++ b/src/openscad_parser/ast/nodes.py @@ -393,6 +393,11 @@ class RangeLiteral(Primary): step: Expression def __str__(self): + # A synthesised default step (2-arg source range) is tagged with the + # range node's own position; an explicit third argument has a distinct + # source position. + if self.step.position == self.position: + return f"[{self.start} : {self.end}]" return f"[{self.start} : {self.end} : {self.step}]" def build_scope(self, parent_scope: "Scope") -> None: diff --git a/tests/test_nodes.py b/tests/test_nodes.py index 7ca5797..62f7c26 100644 --- a/tests/test_nodes.py +++ b/tests/test_nodes.py @@ -172,8 +172,15 @@ def test_primary_and_range_str(): member = PrimaryMember(left=_ident("obj"), member=_ident("x"), position=_pos()) assert str(member) == "obj.x" - range_lit = RangeLiteral(start=_num(0.0), end=_num(5.0), step=_num(1.0), position=_pos()) - assert str(range_lit) == "[0 : 5 : 1]" + # 2-arg: step shares range position (synthesised default) → omit step + range_2arg = RangeLiteral(start=_num(0.0), end=_num(5.0), step=_num(1.0), position=_pos()) + assert str(range_2arg) == "[0:5]" + + # 3-arg: step has a distinct position (explicit in source) → include step + step_pos = Position(origin="", line=1, column=5) + step_explicit = NumberLiteral(val=2.0, position=step_pos) + range_3arg = RangeLiteral(start=_num(0.0), end=_num(5.0), step=step_explicit, position=_pos()) + assert str(range_3arg) == "[0: 5 : 2]" def test_list_comprehension_str(): diff --git a/tests/test_pretty_print.py b/tests/test_pretty_print.py index e46a926..b9b5cb3 100644 --- a/tests/test_pretty_print.py +++ b/tests/test_pretty_print.py @@ -398,7 +398,7 @@ def test_assert_no_child(self): class TestListCompForFormatting: def test_short_for_expands(self): out = _fmt("x = [for (i = [0:3]) i];") - assert out == "x = [\n for (i = [0 : 3 : 1])\n i\n ];" + assert out == "x = [\n for (i = [0 : 3])\n i\n ];" def test_long_for_body_on_new_line(self): out = _fmt("x = [for (long_variable_name = [start_value:step_value:end_value]) long_variable_name * scaling_factor_x];") @@ -408,7 +408,7 @@ def test_long_for_body_on_new_line(self): def test_long_for_assignments_multiline(self): out = _fmt("x = [for (very_long_variable_name_alpha = [start_value:end_value], very_long_variable_name_beta = [0:10]) very_long_variable_name_alpha];") assert "for (\n" in out - assert " very_long_variable_name_alpha = [start_value : end_value : 1]," in out + assert " very_long_variable_name_alpha = [start_value : end_value]," in out assert ")\n" in out def test_nested_for(self): diff --git a/tests/test_vectors.py b/tests/test_vectors.py index 1915e2f..9d3321c 100644 --- a/tests/test_vectors.py +++ b/tests/test_vectors.py @@ -68,6 +68,39 @@ def test_range_expressions(self, parser): code = "x = [0:2*5:10];" parse_success(parser, code) + def test_range_two_arg_round_trip(self): + """Two-argument range [start:end] must not gain a spurious :1 after round-trip.""" + import tempfile + import os + from openscad_parser.ast import getASTfromFile + from openscad_parser.ast.pretty_print import to_openscad + code = "x = [for(i = [0:5]) i];" + with tempfile.NamedTemporaryFile(mode='w', suffix='.scad', delete=False) as f: + f.write(code) + fname = f.name + output = to_openscad(getASTfromFile(fname, process_includes=False)) + os.unlink(fname) + assert ":1]" not in output, f"Spurious step in output: {output!r}" + assert "[0:5]" in output + + def test_range_three_arg_round_trip(self): + """Three-argument range [start:step:end] must preserve all three arguments.""" + import tempfile + import os + from openscad_parser.ast import getASTfromFile + from openscad_parser.ast.pretty_print import to_openscad + cases = [ + ("x = [for(i = [10:-1:0]) i];", "[10:-1:0]"), + ("x = [for(i = [0:1:4]) i];", "[0:1:4]"), + ] + for src, expected in cases: + with tempfile.NamedTemporaryFile(mode='w', suffix='.scad', delete=False) as f: + f.write(src) + fname = f.name + output = to_openscad(getASTfromFile(fname, process_includes=False)) + os.unlink(fname) + assert expected in output, f"{src!r} → missing {expected!r} in {output!r}" + class TestListComprehensionFor: """Test list comprehension with for."""