openscad-parser 2.4.3__tar.gz → 2.4.5__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {openscad_parser-2.4.3 → openscad_parser-2.4.5}/PKG-INFO +1 -1
- {openscad_parser-2.4.3 → openscad_parser-2.4.5}/pyproject.toml +1 -1
- {openscad_parser-2.4.3 → openscad_parser-2.4.5}/src/openscad_parser/ast/nodes.py +88 -29
- openscad_parser-2.4.5/src/openscad_parser/ast/pretty_print.py +286 -0
- openscad_parser-2.4.3/src/openscad_parser/ast/pretty_print.py +0 -159
- {openscad_parser-2.4.3 → openscad_parser-2.4.5}/README.rst +0 -0
- {openscad_parser-2.4.3 → openscad_parser-2.4.5}/src/openscad_parser/__init__.py +0 -0
- {openscad_parser-2.4.3 → openscad_parser-2.4.5}/src/openscad_parser/ast/__init__.py +0 -0
- {openscad_parser-2.4.3 → openscad_parser-2.4.5}/src/openscad_parser/ast/builder.py +0 -0
- {openscad_parser-2.4.3 → openscad_parser-2.4.5}/src/openscad_parser/ast/scope.py +0 -0
- {openscad_parser-2.4.3 → openscad_parser-2.4.5}/src/openscad_parser/ast/serialization.py +0 -0
- {openscad_parser-2.4.3 → openscad_parser-2.4.5}/src/openscad_parser/ast/source_map.py +0 -0
- {openscad_parser-2.4.3 → openscad_parser-2.4.5}/src/openscad_parser/cli.py +0 -0
- {openscad_parser-2.4.3 → openscad_parser-2.4.5}/src/openscad_parser/grammar.py +0 -0
|
@@ -193,7 +193,7 @@ class BooleanLiteral(Primary):
|
|
|
193
193
|
val: bool
|
|
194
194
|
|
|
195
195
|
def __str__(self):
|
|
196
|
-
return
|
|
196
|
+
return "true" if self.val else "false"
|
|
197
197
|
|
|
198
198
|
|
|
199
199
|
@dataclass
|
|
@@ -235,7 +235,8 @@ class ParameterDeclaration(ASTNode):
|
|
|
235
235
|
default: Expression|None
|
|
236
236
|
|
|
237
237
|
def __str__(self):
|
|
238
|
-
|
|
238
|
+
has_default = self.default is not None and not isinstance(self.default, UndefinedLiteral)
|
|
239
|
+
return f"{self.name}{f'={self.default}' if has_default else ''}"
|
|
239
240
|
|
|
240
241
|
def build_scope(self, parent_scope: "Scope") -> None:
|
|
241
242
|
self.scope = parent_scope
|
|
@@ -469,21 +470,21 @@ class AssertOp(Expression):
|
|
|
469
470
|
@dataclass
|
|
470
471
|
class UnaryMinusOp(Expression):
|
|
471
472
|
"""Represents an OpenSCAD unary minus (negation) operation.
|
|
472
|
-
|
|
473
|
+
|
|
473
474
|
Negates a numeric expression, making positive values negative and vice versa.
|
|
474
|
-
|
|
475
|
+
|
|
475
476
|
Examples:
|
|
476
477
|
-5 // Result: -5
|
|
477
478
|
-x // Negate variable
|
|
478
479
|
-(x + y) // Negate expression
|
|
479
|
-
|
|
480
|
+
|
|
480
481
|
Attributes:
|
|
481
482
|
expr: The expression to negate.
|
|
482
483
|
"""
|
|
483
484
|
expr: Expression
|
|
484
485
|
|
|
485
486
|
def __str__(self):
|
|
486
|
-
return f"-{self.expr}"
|
|
487
|
+
return f"-{_lp(self.expr, _prec(self))}"
|
|
487
488
|
|
|
488
489
|
def build_scope(self, parent_scope: "Scope") -> None:
|
|
489
490
|
self.scope = parent_scope
|
|
@@ -493,15 +494,15 @@ class UnaryMinusOp(Expression):
|
|
|
493
494
|
@dataclass
|
|
494
495
|
class AdditionOp(Expression):
|
|
495
496
|
"""Represents an OpenSCAD addition operation.
|
|
496
|
-
|
|
497
|
+
|
|
497
498
|
Performs arithmetic addition of two expressions. Can be used with numbers
|
|
498
499
|
or vectors (element-wise addition for vectors).
|
|
499
|
-
|
|
500
|
+
|
|
500
501
|
Examples:
|
|
501
502
|
1 + 2 // Result: 3
|
|
502
503
|
[1, 2, 3] + [4, 5, 6] // Result: [5, 7, 9]
|
|
503
504
|
x + y // Variable addition
|
|
504
|
-
|
|
505
|
+
|
|
505
506
|
Attributes:
|
|
506
507
|
left: The left operand expression.
|
|
507
508
|
right: The right operand expression.
|
|
@@ -510,7 +511,8 @@ class AdditionOp(Expression):
|
|
|
510
511
|
right: Expression
|
|
511
512
|
|
|
512
513
|
def __str__(self):
|
|
513
|
-
|
|
514
|
+
p = _prec(self)
|
|
515
|
+
return f"{_lp(self.left, p)} + {_rp(self.right, p)}"
|
|
514
516
|
|
|
515
517
|
def build_scope(self, parent_scope: "Scope") -> None:
|
|
516
518
|
self.scope = parent_scope
|
|
@@ -538,7 +540,8 @@ class SubtractionOp(Expression):
|
|
|
538
540
|
right: Expression
|
|
539
541
|
|
|
540
542
|
def __str__(self):
|
|
541
|
-
|
|
543
|
+
p = _prec(self)
|
|
544
|
+
return f"{_lp(self.left, p)} - {_rp(self.right, p)}"
|
|
542
545
|
|
|
543
546
|
def build_scope(self, parent_scope: "Scope") -> None:
|
|
544
547
|
self.scope = parent_scope
|
|
@@ -566,7 +569,8 @@ class MultiplicationOp(Expression):
|
|
|
566
569
|
right: Expression
|
|
567
570
|
|
|
568
571
|
def __str__(self):
|
|
569
|
-
|
|
572
|
+
p = _prec(self)
|
|
573
|
+
return f"{_lp(self.left, p)} * {_rp(self.right, p)}"
|
|
570
574
|
|
|
571
575
|
def build_scope(self, parent_scope: "Scope") -> None:
|
|
572
576
|
self.scope = parent_scope
|
|
@@ -594,7 +598,8 @@ class DivisionOp(Expression):
|
|
|
594
598
|
right: Expression
|
|
595
599
|
|
|
596
600
|
def __str__(self):
|
|
597
|
-
|
|
601
|
+
p = _prec(self)
|
|
602
|
+
return f"{_lp(self.left, p)} / {_rp(self.right, p)}"
|
|
598
603
|
|
|
599
604
|
def build_scope(self, parent_scope: "Scope") -> None:
|
|
600
605
|
self.scope = parent_scope
|
|
@@ -621,7 +626,8 @@ class ModuloOp(Expression):
|
|
|
621
626
|
right: Expression
|
|
622
627
|
|
|
623
628
|
def __str__(self):
|
|
624
|
-
|
|
629
|
+
p = _prec(self)
|
|
630
|
+
return f"{_lp(self.left, p)} % {_rp(self.right, p)}"
|
|
625
631
|
|
|
626
632
|
def build_scope(self, parent_scope: "Scope") -> None:
|
|
627
633
|
self.scope = parent_scope
|
|
@@ -648,7 +654,8 @@ class ExponentOp(Expression):
|
|
|
648
654
|
right: Expression
|
|
649
655
|
|
|
650
656
|
def __str__(self):
|
|
651
|
-
|
|
657
|
+
p = _prec(self)
|
|
658
|
+
return f"{_rp(self.left, p)} ^ {_lp(self.right, p)}"
|
|
652
659
|
|
|
653
660
|
def build_scope(self, parent_scope: "Scope") -> None:
|
|
654
661
|
self.scope = parent_scope
|
|
@@ -675,7 +682,8 @@ class BitwiseAndOp(Expression):
|
|
|
675
682
|
right: Expression
|
|
676
683
|
|
|
677
684
|
def __str__(self):
|
|
678
|
-
|
|
685
|
+
p = _prec(self)
|
|
686
|
+
return f"{_lp(self.left, p)} & {_rp(self.right, p)}"
|
|
679
687
|
|
|
680
688
|
def build_scope(self, parent_scope: "Scope") -> None:
|
|
681
689
|
self.scope = parent_scope
|
|
@@ -702,7 +710,8 @@ class BitwiseOrOp(Expression):
|
|
|
702
710
|
right: Expression
|
|
703
711
|
|
|
704
712
|
def __str__(self):
|
|
705
|
-
|
|
713
|
+
p = _prec(self)
|
|
714
|
+
return f"{_lp(self.left, p)} | {_rp(self.right, p)}"
|
|
706
715
|
|
|
707
716
|
def build_scope(self, parent_scope: "Scope") -> None:
|
|
708
717
|
self.scope = parent_scope
|
|
@@ -726,7 +735,7 @@ class BitwiseNotOp(Expression):
|
|
|
726
735
|
expr: Expression
|
|
727
736
|
|
|
728
737
|
def __str__(self):
|
|
729
|
-
return f"~{self.expr}"
|
|
738
|
+
return f"~{_lp(self.expr, _prec(self))}"
|
|
730
739
|
|
|
731
740
|
def build_scope(self, parent_scope: "Scope") -> None:
|
|
732
741
|
self.scope = parent_scope
|
|
@@ -752,7 +761,8 @@ class BitwiseShiftLeftOp(Expression):
|
|
|
752
761
|
right: Expression
|
|
753
762
|
|
|
754
763
|
def __str__(self):
|
|
755
|
-
|
|
764
|
+
p = _prec(self)
|
|
765
|
+
return f"{_lp(self.left, p)} << {_rp(self.right, p)}"
|
|
756
766
|
|
|
757
767
|
def build_scope(self, parent_scope: "Scope") -> None:
|
|
758
768
|
self.scope = parent_scope
|
|
@@ -779,7 +789,8 @@ class BitwiseShiftRightOp(Expression):
|
|
|
779
789
|
right: Expression
|
|
780
790
|
|
|
781
791
|
def __str__(self):
|
|
782
|
-
|
|
792
|
+
p = _prec(self)
|
|
793
|
+
return f"{_lp(self.left, p)} >> {_rp(self.right, p)}"
|
|
783
794
|
|
|
784
795
|
def build_scope(self, parent_scope: "Scope") -> None:
|
|
785
796
|
self.scope = parent_scope
|
|
@@ -807,7 +818,8 @@ class LogicalAndOp(Expression):
|
|
|
807
818
|
right: Expression
|
|
808
819
|
|
|
809
820
|
def __str__(self):
|
|
810
|
-
|
|
821
|
+
p = _prec(self)
|
|
822
|
+
return f"{_lp(self.left, p)} && {_rp(self.right, p)}"
|
|
811
823
|
|
|
812
824
|
def build_scope(self, parent_scope: "Scope") -> None:
|
|
813
825
|
self.scope = parent_scope
|
|
@@ -835,7 +847,8 @@ class LogicalOrOp(Expression):
|
|
|
835
847
|
right: Expression
|
|
836
848
|
|
|
837
849
|
def __str__(self):
|
|
838
|
-
|
|
850
|
+
p = _prec(self)
|
|
851
|
+
return f"{_lp(self.left, p)} || {_rp(self.right, p)}"
|
|
839
852
|
|
|
840
853
|
def build_scope(self, parent_scope: "Scope") -> None:
|
|
841
854
|
self.scope = parent_scope
|
|
@@ -861,7 +874,7 @@ class LogicalNotOp(Expression):
|
|
|
861
874
|
expr: Expression
|
|
862
875
|
|
|
863
876
|
def __str__(self):
|
|
864
|
-
return f"!{self.expr}"
|
|
877
|
+
return f"!{_lp(self.expr, _prec(self))}"
|
|
865
878
|
|
|
866
879
|
def build_scope(self, parent_scope: "Scope") -> None:
|
|
867
880
|
self.scope = parent_scope
|
|
@@ -918,7 +931,8 @@ class EqualityOp(Expression):
|
|
|
918
931
|
right: Expression
|
|
919
932
|
|
|
920
933
|
def __str__(self):
|
|
921
|
-
|
|
934
|
+
p = _prec(self)
|
|
935
|
+
return f"{_lp(self.left, p)} == {_rp(self.right, p)}"
|
|
922
936
|
|
|
923
937
|
def build_scope(self, parent_scope: "Scope") -> None:
|
|
924
938
|
self.scope = parent_scope
|
|
@@ -945,7 +959,8 @@ class InequalityOp(Expression):
|
|
|
945
959
|
right: Expression
|
|
946
960
|
|
|
947
961
|
def __str__(self):
|
|
948
|
-
|
|
962
|
+
p = _prec(self)
|
|
963
|
+
return f"{_lp(self.left, p)} != {_rp(self.right, p)}"
|
|
949
964
|
|
|
950
965
|
def build_scope(self, parent_scope: "Scope") -> None:
|
|
951
966
|
self.scope = parent_scope
|
|
@@ -972,7 +987,8 @@ class GreaterThanOp(Expression):
|
|
|
972
987
|
right: Expression
|
|
973
988
|
|
|
974
989
|
def __str__(self):
|
|
975
|
-
|
|
990
|
+
p = _prec(self)
|
|
991
|
+
return f"{_lp(self.left, p)} > {_rp(self.right, p)}"
|
|
976
992
|
|
|
977
993
|
def build_scope(self, parent_scope: "Scope") -> None:
|
|
978
994
|
self.scope = parent_scope
|
|
@@ -999,7 +1015,8 @@ class GreaterThanOrEqualOp(Expression):
|
|
|
999
1015
|
right: Expression
|
|
1000
1016
|
|
|
1001
1017
|
def __str__(self):
|
|
1002
|
-
|
|
1018
|
+
p = _prec(self)
|
|
1019
|
+
return f"{_lp(self.left, p)} >= {_rp(self.right, p)}"
|
|
1003
1020
|
|
|
1004
1021
|
def build_scope(self, parent_scope: "Scope") -> None:
|
|
1005
1022
|
self.scope = parent_scope
|
|
@@ -1026,7 +1043,8 @@ class LessThanOp(Expression):
|
|
|
1026
1043
|
right: Expression
|
|
1027
1044
|
|
|
1028
1045
|
def __str__(self):
|
|
1029
|
-
|
|
1046
|
+
p = _prec(self)
|
|
1047
|
+
return f"{_lp(self.left, p)} < {_rp(self.right, p)}"
|
|
1030
1048
|
|
|
1031
1049
|
def build_scope(self, parent_scope: "Scope") -> None:
|
|
1032
1050
|
self.scope = parent_scope
|
|
@@ -1053,7 +1071,8 @@ class LessThanOrEqualOp(Expression):
|
|
|
1053
1071
|
right: Expression
|
|
1054
1072
|
|
|
1055
1073
|
def __str__(self):
|
|
1056
|
-
|
|
1074
|
+
p = _prec(self)
|
|
1075
|
+
return f"{_lp(self.left, p)} <= {_rp(self.right, p)}"
|
|
1057
1076
|
|
|
1058
1077
|
def build_scope(self, parent_scope: "Scope") -> None:
|
|
1059
1078
|
self.scope = parent_scope
|
|
@@ -1892,6 +1911,46 @@ class IncludeStatement(ASTNode):
|
|
|
1892
1911
|
return f"include <{self.filepath.val}>"
|
|
1893
1912
|
|
|
1894
1913
|
|
|
1914
|
+
# ---------------------------------------------------------------------------
|
|
1915
|
+
# Operator precedence helpers (used by __str__ methods to emit parentheses)
|
|
1916
|
+
# ---------------------------------------------------------------------------
|
|
1917
|
+
|
|
1918
|
+
_PREC: dict[type, int] = {
|
|
1919
|
+
# loosest
|
|
1920
|
+
TernaryOp: 10,
|
|
1921
|
+
LogicalOrOp: 20,
|
|
1922
|
+
LogicalAndOp: 30,
|
|
1923
|
+
EqualityOp: 40, InequalityOp: 40,
|
|
1924
|
+
LessThanOp: 50, LessThanOrEqualOp: 50, GreaterThanOp: 50, GreaterThanOrEqualOp: 50,
|
|
1925
|
+
BitwiseOrOp: 55,
|
|
1926
|
+
BitwiseAndOp: 57,
|
|
1927
|
+
BitwiseShiftLeftOp: 58, BitwiseShiftRightOp: 58,
|
|
1928
|
+
AdditionOp: 60, SubtractionOp: 60,
|
|
1929
|
+
MultiplicationOp: 70, DivisionOp: 70, ModuloOp: 70,
|
|
1930
|
+
UnaryMinusOp: 80, LogicalNotOp: 80, BitwiseNotOp: 80,
|
|
1931
|
+
ExponentOp: 90,
|
|
1932
|
+
# tightest — primary expressions (literals, identifiers, calls, etc.) default to 99
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1935
|
+
|
|
1936
|
+
def _prec(node) -> int:
|
|
1937
|
+
return _PREC.get(type(node), 99)
|
|
1938
|
+
|
|
1939
|
+
|
|
1940
|
+
def _lp(child, parent_prec: int) -> str:
|
|
1941
|
+
"""Left-operand: parenthesize when child binds strictly looser than parent."""
|
|
1942
|
+
return f"({child})" if _prec(child) < parent_prec else str(child)
|
|
1943
|
+
|
|
1944
|
+
|
|
1945
|
+
def _rp(child, parent_prec: int) -> str:
|
|
1946
|
+
"""Right-operand: parenthesize when child binds looser than OR equal to parent.
|
|
1947
|
+
|
|
1948
|
+
The stricter rule is needed for left-associative operators so that
|
|
1949
|
+
``a - (b - c)`` is not flattened to ``a - b - c``.
|
|
1950
|
+
"""
|
|
1951
|
+
return f"({child})" if _prec(child) <= parent_prec else str(child)
|
|
1952
|
+
|
|
1953
|
+
|
|
1895
1954
|
def _collect_hoisted_declarations(nodes, scope: "Scope") -> None:
|
|
1896
1955
|
"""Scan a list of nodes and register assignments, functions, and modules in scope.
|
|
1897
1956
|
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
"""Pretty-printer: convert an OpenSCAD AST back to formatted source code."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
from .nodes import (
|
|
4
|
+
ASTNode, Assignment, FunctionDeclaration, ModuleDeclaration,
|
|
5
|
+
UseStatement, IncludeStatement,
|
|
6
|
+
ModuleInstantiation,
|
|
7
|
+
ModularCall, ModularFor,
|
|
8
|
+
ModularIntersectionFor,
|
|
9
|
+
ModularLet, ModularEcho, ModularAssert,
|
|
10
|
+
ModularIf, ModularIfElse,
|
|
11
|
+
ModularModifierShowOnly, ModularModifierHighlight,
|
|
12
|
+
ModularModifierBackground, ModularModifierDisable,
|
|
13
|
+
CommentLine, CommentSpan,
|
|
14
|
+
TernaryOp,
|
|
15
|
+
EchoOp,
|
|
16
|
+
AssertOp,
|
|
17
|
+
LetOp,
|
|
18
|
+
PrimaryCall,
|
|
19
|
+
ListComprehension,
|
|
20
|
+
ListCompFor,
|
|
21
|
+
ListCompCFor,
|
|
22
|
+
ListCompLet,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def to_openscad(nodes: list[ASTNode], indent_width: int = 4) -> str:
|
|
27
|
+
"""Convert a list of AST nodes to formatted OpenSCAD source code.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
nodes: The AST nodes to format (top-level statements).
|
|
31
|
+
indent_width: Number of spaces per indentation level (default: 4).
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
Formatted OpenSCAD source code as a string.
|
|
35
|
+
"""
|
|
36
|
+
parts = []
|
|
37
|
+
prev_complex = False
|
|
38
|
+
for node in nodes:
|
|
39
|
+
is_complex = isinstance(node, (ModuleDeclaration, FunctionDeclaration))
|
|
40
|
+
if parts and (is_complex or prev_complex):
|
|
41
|
+
parts.append("")
|
|
42
|
+
parts.append(_fmt_node(node, 0, indent_width))
|
|
43
|
+
prev_complex = is_complex
|
|
44
|
+
return "\n".join(parts)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# Line length beyond which call arguments are formatted one-per-line.
|
|
48
|
+
_MULTILINE_CHAR_LIMIT = 100
|
|
49
|
+
|
|
50
|
+
# --- helpers ---
|
|
51
|
+
|
|
52
|
+
def _as_list(val) -> list:
|
|
53
|
+
if isinstance(val, list):
|
|
54
|
+
return val
|
|
55
|
+
if val is None:
|
|
56
|
+
return []
|
|
57
|
+
return [val]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _join_str(items) -> str:
|
|
61
|
+
return ", ".join(str(i) for i in items)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _fmt_list_elem(elem, indent: int, w: int) -> str:
|
|
65
|
+
"""Format a list comprehension element, placing the body on a new line for for loops."""
|
|
66
|
+
pad = " " * indent
|
|
67
|
+
inner_pad = " " * (indent + w)
|
|
68
|
+
if isinstance(elem, ListCompFor):
|
|
69
|
+
assigns_inline = ", ".join(str(a) for a in elem.assignments)
|
|
70
|
+
body = _fmt_list_elem(elem.body, indent + w, w)
|
|
71
|
+
if len(f"for ({assigns_inline})") + indent > _MULTILINE_CHAR_LIMIT:
|
|
72
|
+
assign_lines = (",\n" + inner_pad).join(str(a) for a in elem.assignments)
|
|
73
|
+
return f"for (\n{inner_pad}{assign_lines}\n{pad})\n{inner_pad}{body}"
|
|
74
|
+
return f"for ({assigns_inline})\n{inner_pad}{body}"
|
|
75
|
+
if isinstance(elem, ListCompCFor):
|
|
76
|
+
inits = ", ".join(str(a) for a in elem.inits)
|
|
77
|
+
incrs = ", ".join(str(a) for a in elem.incrs)
|
|
78
|
+
body = _fmt_list_elem(elem.body, indent + w, w)
|
|
79
|
+
return f"for ({inits}; {elem.condition}; {incrs})\n{inner_pad}{body}"
|
|
80
|
+
if isinstance(elem, ListCompLet):
|
|
81
|
+
formatted = [_fmt_assign(a, indent + w, w) for a in elem.assignments]
|
|
82
|
+
body = _fmt_list_elem(elem.body, indent, w)
|
|
83
|
+
if len(formatted) > 1 or any("\n" in fa for fa in formatted):
|
|
84
|
+
assign_lines = (",\n" + inner_pad).join(formatted)
|
|
85
|
+
return f"let(\n{inner_pad}{assign_lines}\n{pad})\n{pad}{body}"
|
|
86
|
+
assigns = ", ".join(formatted)
|
|
87
|
+
return f"let({assigns}) {body}"
|
|
88
|
+
if isinstance(elem, LetOp):
|
|
89
|
+
formatted = [_fmt_assign(a, indent + w, w) for a in elem.assignments]
|
|
90
|
+
body = str(elem.body)
|
|
91
|
+
if len(formatted) > 1 or any("\n" in fa for fa in formatted):
|
|
92
|
+
assign_lines = (",\n" + inner_pad).join(formatted)
|
|
93
|
+
return f"let(\n{inner_pad}{assign_lines}\n{pad})\n{pad}{body}"
|
|
94
|
+
assigns = ", ".join(formatted)
|
|
95
|
+
return f"let({assigns}) {body}"
|
|
96
|
+
return str(elem)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _fmt_multiline_args(head: str, args: list, indent: int, w: int) -> str:
|
|
100
|
+
"""Format `head(arg1, arg2, ...)` with each arg on its own line."""
|
|
101
|
+
inner_pad = " " * (indent + w)
|
|
102
|
+
pad = " " * indent
|
|
103
|
+
arg_lines = (",\n" + inner_pad).join(str(a) for a in args)
|
|
104
|
+
return f"{head}(\n{inner_pad}{arg_lines}\n{pad})"
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _fmt_assign(assign, indent: int, w: int) -> str:
|
|
108
|
+
"""Format an Assignment node, routing its expression through _fmt_expr."""
|
|
109
|
+
return f"{assign.name} = {_fmt_expr(assign.expr, indent, w)}"
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _fmt_expr(expr, indent: int, w: int) -> str:
|
|
113
|
+
"""Format an expression with indent-aware layout for ternary, assert, and echo."""
|
|
114
|
+
pad = " " * indent
|
|
115
|
+
if isinstance(expr, TernaryOp):
|
|
116
|
+
pad2 = " " * (indent + 2)
|
|
117
|
+
return (
|
|
118
|
+
f"{expr.condition}\n"
|
|
119
|
+
f"{pad2}? {_fmt_expr(expr.true_expr, indent + 2, w)}\n"
|
|
120
|
+
f"{pad2}: {_fmt_expr(expr.false_expr, indent + 2, w)}"
|
|
121
|
+
)
|
|
122
|
+
if isinstance(expr, AssertOp):
|
|
123
|
+
args = ", ".join(str(a) for a in expr.arguments)
|
|
124
|
+
return f"assert({args})\n{pad}{_fmt_expr(expr.body, indent, w)}"
|
|
125
|
+
if isinstance(expr, EchoOp):
|
|
126
|
+
args = ", ".join(str(a) for a in expr.arguments)
|
|
127
|
+
return f"echo({args})\n{pad}{_fmt_expr(expr.body, indent, w)}"
|
|
128
|
+
if isinstance(expr, LetOp):
|
|
129
|
+
inner_pad = " " * (indent + w)
|
|
130
|
+
formatted = [_fmt_assign(a, indent + w, w) for a in expr.assignments]
|
|
131
|
+
if len(formatted) > 1 or any("\n" in fa for fa in formatted):
|
|
132
|
+
assign_lines = (",\n" + inner_pad).join(formatted)
|
|
133
|
+
return (
|
|
134
|
+
f"let(\n{inner_pad}{assign_lines}\n{pad})\n"
|
|
135
|
+
f"{pad}{_fmt_expr(expr.body, indent, w)}"
|
|
136
|
+
)
|
|
137
|
+
assigns = ", ".join(formatted)
|
|
138
|
+
return f"let({assigns})\n{pad}{_fmt_expr(expr.body, indent, w)}"
|
|
139
|
+
if isinstance(expr, PrimaryCall):
|
|
140
|
+
inline = str(expr)
|
|
141
|
+
if len(inline) + indent > _MULTILINE_CHAR_LIMIT:
|
|
142
|
+
return _fmt_multiline_args(str(expr.left), expr.arguments, indent, w)
|
|
143
|
+
if isinstance(expr, ListComprehension):
|
|
144
|
+
inner_pad = " " * (indent + w)
|
|
145
|
+
let_multiline = any(
|
|
146
|
+
isinstance(e, (LetOp, ListCompLet)) and (
|
|
147
|
+
len(e.assignments) > 1 or
|
|
148
|
+
any("\n" in _fmt_assign(a, indent + w + w, w) for a in e.assignments)
|
|
149
|
+
)
|
|
150
|
+
for e in expr.elements
|
|
151
|
+
)
|
|
152
|
+
if len(str(expr)) + indent > _MULTILINE_CHAR_LIMIT or let_multiline:
|
|
153
|
+
formatted_elems = [_fmt_list_elem(e, indent + w, w) for e in expr.elements]
|
|
154
|
+
items = (",\n" + inner_pad).join(formatted_elems)
|
|
155
|
+
return f"[\n{inner_pad}{items}\n{pad}]"
|
|
156
|
+
return str(expr)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _fmt_node(node: ASTNode, indent: int, w: int) -> str:
|
|
160
|
+
"""Format any top-level or block-body node."""
|
|
161
|
+
pad = " " * indent
|
|
162
|
+
|
|
163
|
+
if isinstance(node, CommentLine):
|
|
164
|
+
return f"{pad}//{node.text}"
|
|
165
|
+
if isinstance(node, CommentSpan):
|
|
166
|
+
return f"{pad}/*{node.text}*/"
|
|
167
|
+
if isinstance(node, UseStatement):
|
|
168
|
+
return f"{pad}use <{node.filepath.val}>"
|
|
169
|
+
if isinstance(node, IncludeStatement):
|
|
170
|
+
return f"{pad}include <{node.filepath.val}>"
|
|
171
|
+
if isinstance(node, Assignment):
|
|
172
|
+
return f"{pad}{node.name} = {_fmt_expr(node.expr, indent, w)};"
|
|
173
|
+
if isinstance(node, FunctionDeclaration):
|
|
174
|
+
head = f"{pad}function {node.name}"
|
|
175
|
+
params_inline = _join_str(node.parameters)
|
|
176
|
+
expr_pad = " " * (indent + w)
|
|
177
|
+
if len(f"{head}({params_inline}) =") > _MULTILINE_CHAR_LIMIT:
|
|
178
|
+
param_block = _fmt_multiline_args(head, node.parameters, indent, w)
|
|
179
|
+
return f"{param_block} =\n{expr_pad}{_fmt_expr(node.expr, indent + w, w)};"
|
|
180
|
+
return f"{head}({params_inline}) =\n{expr_pad}{_fmt_expr(node.expr, indent + w, w)};"
|
|
181
|
+
if isinstance(node, ModuleDeclaration):
|
|
182
|
+
head = f"{pad}module {node.name}"
|
|
183
|
+
params_inline = _join_str(node.parameters)
|
|
184
|
+
block = _fmt_block(node.children, indent, w)
|
|
185
|
+
if len(f"{head}({params_inline})") > _MULTILINE_CHAR_LIMIT:
|
|
186
|
+
param_block = _fmt_multiline_args(head, node.parameters, indent, w)
|
|
187
|
+
return f"{param_block} {block}"
|
|
188
|
+
return f"{head}({params_inline}) {block}"
|
|
189
|
+
if isinstance(node, ModuleInstantiation):
|
|
190
|
+
return _fmt_inst(node, indent, w)
|
|
191
|
+
return f"{pad}{node}" # pragma: no cover
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _fmt_block(nodes: list, indent: int, w: int) -> str:
|
|
195
|
+
"""Format a list of nodes as a braced block."""
|
|
196
|
+
pad = " " * indent
|
|
197
|
+
if not nodes:
|
|
198
|
+
return "{}"
|
|
199
|
+
inner = "\n".join(_fmt_node(n, indent + w, w) for n in nodes)
|
|
200
|
+
return "{\n" + inner + "\n" + pad + "}"
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _fmt_child(body, indent: int, w: int) -> str:
|
|
204
|
+
"""Format the child body of a module instantiation.
|
|
205
|
+
|
|
206
|
+
Returns the tail string appended after the header:
|
|
207
|
+
- ``";"`` when there are no children
|
|
208
|
+
- ``"\\n child;"`` for a single inline child
|
|
209
|
+
- ``" {\\n ...\\n}"`` for a block of multiple children
|
|
210
|
+
"""
|
|
211
|
+
nodes = _as_list(body)
|
|
212
|
+
pad = " " * indent
|
|
213
|
+
|
|
214
|
+
if not nodes:
|
|
215
|
+
return ";"
|
|
216
|
+
if len(nodes) == 1:
|
|
217
|
+
return "\n" + _fmt_inst(nodes[0], indent + w, w)
|
|
218
|
+
inner = "\n".join(_fmt_inst(n, indent + w, w) for n in nodes)
|
|
219
|
+
return " {\n" + inner + "\n" + pad + "}"
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _fmt_inst(node: ModuleInstantiation, indent: int, w: int, prefix: str = "") -> str:
|
|
223
|
+
"""Format a ModuleInstantiation node.
|
|
224
|
+
|
|
225
|
+
``prefix`` accumulates modifier characters (``!``, ``#``, ``%``, ``*``)
|
|
226
|
+
so nested modifiers produce e.g. ``!#cube(10);``.
|
|
227
|
+
"""
|
|
228
|
+
pad = " " * indent
|
|
229
|
+
|
|
230
|
+
# Modifiers: push prefix down to the wrapped node
|
|
231
|
+
if isinstance(node, ModularModifierShowOnly):
|
|
232
|
+
return _fmt_inst(node.child, indent, w, "!" + prefix)
|
|
233
|
+
if isinstance(node, ModularModifierHighlight):
|
|
234
|
+
return _fmt_inst(node.child, indent, w, "#" + prefix)
|
|
235
|
+
if isinstance(node, ModularModifierBackground):
|
|
236
|
+
return _fmt_inst(node.child, indent, w, "%" + prefix)
|
|
237
|
+
if isinstance(node, ModularModifierDisable):
|
|
238
|
+
return _fmt_inst(node.child, indent, w, "*" + prefix)
|
|
239
|
+
|
|
240
|
+
if isinstance(node, ModularCall):
|
|
241
|
+
head = f"{pad}{prefix}{node.name}"
|
|
242
|
+
inline = f"{head}({_join_str(node.arguments)})"
|
|
243
|
+
if len(inline) > _MULTILINE_CHAR_LIMIT:
|
|
244
|
+
call = _fmt_multiline_args(head, node.arguments, indent, w)
|
|
245
|
+
else:
|
|
246
|
+
call = inline
|
|
247
|
+
return call + _fmt_child(node.children, indent, w)
|
|
248
|
+
|
|
249
|
+
if isinstance(node, ModularFor):
|
|
250
|
+
assigns = _join_str(_as_list(node.assignments))
|
|
251
|
+
return f"{pad}{prefix}for ({assigns})" + _fmt_child(node.body, indent, w)
|
|
252
|
+
|
|
253
|
+
if isinstance(node, ModularIntersectionFor):
|
|
254
|
+
assigns = _join_str(_as_list(node.assignments))
|
|
255
|
+
return f"{pad}{prefix}intersection_for ({assigns})" + _fmt_child(node.body, indent, w)
|
|
256
|
+
|
|
257
|
+
if isinstance(node, ModularLet):
|
|
258
|
+
inner_pad = " " * (indent + w)
|
|
259
|
+
formatted = [_fmt_assign(a, indent + w, w) for a in _as_list(node.assignments)]
|
|
260
|
+
if len(formatted) > 1 or any("\n" in fa for fa in formatted):
|
|
261
|
+
assign_lines = (",\n" + inner_pad).join(formatted)
|
|
262
|
+
tail = _fmt_child(node.children, indent, w)
|
|
263
|
+
return f"{pad}{prefix}let (\n{inner_pad}{assign_lines}\n{pad}){tail}"
|
|
264
|
+
assigns = ", ".join(formatted)
|
|
265
|
+
return f"{pad}{prefix}let ({assigns})" + _fmt_child(node.children, indent, w)
|
|
266
|
+
|
|
267
|
+
if isinstance(node, ModularEcho):
|
|
268
|
+
args = _join_str(node.arguments)
|
|
269
|
+
return f"{pad}{prefix}echo({args})" + _fmt_child(node.children, indent, w)
|
|
270
|
+
|
|
271
|
+
if isinstance(node, ModularAssert):
|
|
272
|
+
args = _join_str(node.arguments)
|
|
273
|
+
return f"{pad}{prefix}assert({args})" + _fmt_child(node.children, indent, w)
|
|
274
|
+
|
|
275
|
+
if isinstance(node, ModularIf):
|
|
276
|
+
header = f"{pad}{prefix}if ({node.condition})"
|
|
277
|
+
return header + _fmt_child(node.true_branch, indent, w)
|
|
278
|
+
|
|
279
|
+
if isinstance(node, ModularIfElse):
|
|
280
|
+
header = f"{pad}{prefix}if ({node.condition})"
|
|
281
|
+
true_tail = _fmt_child(node.true_branch, indent, w)
|
|
282
|
+
false_tail = _fmt_child(node.false_branch, indent, w)
|
|
283
|
+
connector = " else" if true_tail.startswith(" {") else f"\n{pad}else"
|
|
284
|
+
return header + true_tail + connector + false_tail
|
|
285
|
+
|
|
286
|
+
return f"{pad}{prefix}{node};" # pragma: no cover
|
|
@@ -1,159 +0,0 @@
|
|
|
1
|
-
"""Pretty-printer: convert an OpenSCAD AST back to formatted source code."""
|
|
2
|
-
from __future__ import annotations
|
|
3
|
-
from .nodes import (
|
|
4
|
-
ASTNode, Assignment, FunctionDeclaration, ModuleDeclaration,
|
|
5
|
-
UseStatement, IncludeStatement,
|
|
6
|
-
ModuleInstantiation,
|
|
7
|
-
ModularCall, ModularFor,
|
|
8
|
-
ModularIntersectionFor,
|
|
9
|
-
ModularLet, ModularEcho, ModularAssert,
|
|
10
|
-
ModularIf, ModularIfElse,
|
|
11
|
-
ModularModifierShowOnly, ModularModifierHighlight,
|
|
12
|
-
ModularModifierBackground, ModularModifierDisable,
|
|
13
|
-
CommentLine, CommentSpan,
|
|
14
|
-
)
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
def to_openscad(nodes: list[ASTNode], indent_width: int = 4) -> str:
|
|
18
|
-
"""Convert a list of AST nodes to formatted OpenSCAD source code.
|
|
19
|
-
|
|
20
|
-
Args:
|
|
21
|
-
nodes: The AST nodes to format (top-level statements).
|
|
22
|
-
indent_width: Number of spaces per indentation level (default: 4).
|
|
23
|
-
|
|
24
|
-
Returns:
|
|
25
|
-
Formatted OpenSCAD source code as a string.
|
|
26
|
-
"""
|
|
27
|
-
parts = []
|
|
28
|
-
prev_complex = False
|
|
29
|
-
for node in nodes:
|
|
30
|
-
is_complex = isinstance(node, (ModuleDeclaration, FunctionDeclaration))
|
|
31
|
-
if parts and (is_complex or prev_complex):
|
|
32
|
-
parts.append("")
|
|
33
|
-
parts.append(_fmt_node(node, 0, indent_width))
|
|
34
|
-
prev_complex = is_complex
|
|
35
|
-
return "\n".join(parts)
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
# --- helpers ---
|
|
39
|
-
|
|
40
|
-
def _as_list(val) -> list:
|
|
41
|
-
if isinstance(val, list):
|
|
42
|
-
return val
|
|
43
|
-
if val is None:
|
|
44
|
-
return []
|
|
45
|
-
return [val]
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
def _join_str(items) -> str:
|
|
49
|
-
return ", ".join(str(i) for i in items)
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
def _fmt_node(node: ASTNode, indent: int, w: int) -> str:
|
|
53
|
-
"""Format any top-level or block-body node."""
|
|
54
|
-
pad = " " * indent
|
|
55
|
-
|
|
56
|
-
if isinstance(node, CommentLine):
|
|
57
|
-
return f"{pad}//{node.text}"
|
|
58
|
-
if isinstance(node, CommentSpan):
|
|
59
|
-
return f"{pad}/*{node.text}*/"
|
|
60
|
-
if isinstance(node, UseStatement):
|
|
61
|
-
return f"{pad}use <{node.filepath.val}>"
|
|
62
|
-
if isinstance(node, IncludeStatement):
|
|
63
|
-
return f"{pad}include <{node.filepath.val}>"
|
|
64
|
-
if isinstance(node, Assignment):
|
|
65
|
-
return f"{pad}{node.name} = {node.expr};"
|
|
66
|
-
if isinstance(node, FunctionDeclaration):
|
|
67
|
-
params = _join_str(node.parameters)
|
|
68
|
-
return f"{pad}function {node.name}({params}) = {node.expr};"
|
|
69
|
-
if isinstance(node, ModuleDeclaration):
|
|
70
|
-
params = _join_str(node.parameters)
|
|
71
|
-
block = _fmt_block(node.children, indent, w)
|
|
72
|
-
return f"{pad}module {node.name}({params}) {block}"
|
|
73
|
-
if isinstance(node, ModuleInstantiation):
|
|
74
|
-
return _fmt_inst(node, indent, w)
|
|
75
|
-
return f"{pad}{node}" # pragma: no cover
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
def _fmt_block(nodes: list, indent: int, w: int) -> str:
|
|
79
|
-
"""Format a list of nodes as a braced block."""
|
|
80
|
-
pad = " " * indent
|
|
81
|
-
if not nodes:
|
|
82
|
-
return "{}"
|
|
83
|
-
inner = "\n".join(_fmt_node(n, indent + w, w) for n in nodes)
|
|
84
|
-
return "{\n" + inner + "\n" + pad + "}"
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
def _fmt_child(body, indent: int, w: int) -> str:
|
|
88
|
-
"""Format the child body of a module instantiation.
|
|
89
|
-
|
|
90
|
-
Returns the tail string appended after the header:
|
|
91
|
-
- ``";"`` when there are no children
|
|
92
|
-
- ``"\\n child;"`` for a single inline child
|
|
93
|
-
- ``" {\\n ...\\n}"`` for a block of multiple children
|
|
94
|
-
"""
|
|
95
|
-
nodes = _as_list(body)
|
|
96
|
-
pad = " " * indent
|
|
97
|
-
|
|
98
|
-
if not nodes:
|
|
99
|
-
return ";"
|
|
100
|
-
if len(nodes) == 1:
|
|
101
|
-
return "\n" + _fmt_inst(nodes[0], indent + w, w)
|
|
102
|
-
inner = "\n".join(_fmt_inst(n, indent + w, w) for n in nodes)
|
|
103
|
-
return " {\n" + inner + "\n" + pad + "}"
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
def _fmt_inst(node: ModuleInstantiation, indent: int, w: int, prefix: str = "") -> str:
|
|
107
|
-
"""Format a ModuleInstantiation node.
|
|
108
|
-
|
|
109
|
-
``prefix`` accumulates modifier characters (``!``, ``#``, ``%``, ``*``)
|
|
110
|
-
so nested modifiers produce e.g. ``!#cube(10);``.
|
|
111
|
-
"""
|
|
112
|
-
pad = " " * indent
|
|
113
|
-
|
|
114
|
-
# Modifiers: push prefix down to the wrapped node
|
|
115
|
-
if isinstance(node, ModularModifierShowOnly):
|
|
116
|
-
return _fmt_inst(node.child, indent, w, "!" + prefix)
|
|
117
|
-
if isinstance(node, ModularModifierHighlight):
|
|
118
|
-
return _fmt_inst(node.child, indent, w, "#" + prefix)
|
|
119
|
-
if isinstance(node, ModularModifierBackground):
|
|
120
|
-
return _fmt_inst(node.child, indent, w, "%" + prefix)
|
|
121
|
-
if isinstance(node, ModularModifierDisable):
|
|
122
|
-
return _fmt_inst(node.child, indent, w, "*" + prefix)
|
|
123
|
-
|
|
124
|
-
if isinstance(node, ModularCall):
|
|
125
|
-
args = _join_str(node.arguments)
|
|
126
|
-
return f"{pad}{prefix}{node.name}({args})" + _fmt_child(node.children, indent, w)
|
|
127
|
-
|
|
128
|
-
if isinstance(node, ModularFor):
|
|
129
|
-
assigns = _join_str(_as_list(node.assignments))
|
|
130
|
-
return f"{pad}{prefix}for ({assigns})" + _fmt_child(node.body, indent, w)
|
|
131
|
-
|
|
132
|
-
if isinstance(node, ModularIntersectionFor):
|
|
133
|
-
assigns = _join_str(_as_list(node.assignments))
|
|
134
|
-
return f"{pad}{prefix}intersection_for ({assigns})" + _fmt_child(node.body, indent, w)
|
|
135
|
-
|
|
136
|
-
if isinstance(node, ModularLet):
|
|
137
|
-
assigns = _join_str(_as_list(node.assignments))
|
|
138
|
-
return f"{pad}{prefix}let ({assigns})" + _fmt_child(node.children, indent, w)
|
|
139
|
-
|
|
140
|
-
if isinstance(node, ModularEcho):
|
|
141
|
-
args = _join_str(node.arguments)
|
|
142
|
-
return f"{pad}{prefix}echo({args})" + _fmt_child(node.children, indent, w)
|
|
143
|
-
|
|
144
|
-
if isinstance(node, ModularAssert):
|
|
145
|
-
args = _join_str(node.arguments)
|
|
146
|
-
return f"{pad}{prefix}assert({args})" + _fmt_child(node.children, indent, w)
|
|
147
|
-
|
|
148
|
-
if isinstance(node, ModularIf):
|
|
149
|
-
header = f"{pad}{prefix}if ({node.condition})"
|
|
150
|
-
return header + _fmt_child(node.true_branch, indent, w)
|
|
151
|
-
|
|
152
|
-
if isinstance(node, ModularIfElse):
|
|
153
|
-
header = f"{pad}{prefix}if ({node.condition})"
|
|
154
|
-
true_tail = _fmt_child(node.true_branch, indent, w)
|
|
155
|
-
false_tail = _fmt_child(node.false_branch, indent, w)
|
|
156
|
-
connector = " else" if true_tail.startswith(" {") else f"\n{pad}else"
|
|
157
|
-
return header + true_tail + connector + false_tail
|
|
158
|
-
|
|
159
|
-
return f"{pad}{prefix}{node};" # pragma: no cover
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|