openscad-parser 2.3.0__tar.gz → 2.3.2__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.3.0 → openscad_parser-2.3.2}/PKG-INFO +2 -1
- {openscad_parser-2.3.0 → openscad_parser-2.3.2}/pyproject.toml +2 -1
- {openscad_parser-2.3.0 → openscad_parser-2.3.2}/src/openscad_parser/ast/builder.py +82 -51
- {openscad_parser-2.3.0 → openscad_parser-2.3.2}/src/openscad_parser/ast/serialization.py +4 -0
- {openscad_parser-2.3.0 → openscad_parser-2.3.2}/src/openscad_parser/ast/source_map.py +42 -31
- {openscad_parser-2.3.0 → openscad_parser-2.3.2}/src/openscad_parser/grammar.py +1 -1
- {openscad_parser-2.3.0 → openscad_parser-2.3.2}/README.rst +0 -0
- {openscad_parser-2.3.0 → openscad_parser-2.3.2}/src/openscad_parser/__init__.py +0 -0
- {openscad_parser-2.3.0 → openscad_parser-2.3.2}/src/openscad_parser/ast/__init__.py +0 -0
- {openscad_parser-2.3.0 → openscad_parser-2.3.2}/src/openscad_parser/ast/nodes.py +0 -0
- {openscad_parser-2.3.0 → openscad_parser-2.3.2}/src/openscad_parser/ast/scope.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: openscad_parser
|
|
3
|
-
Version: 2.3.
|
|
3
|
+
Version: 2.3.2
|
|
4
4
|
Summary: A PEG parser to read OpenSCAD language source code, with optional AST tree generation.
|
|
5
5
|
Keywords: openscad,openscad parser,parser
|
|
6
6
|
Author: Revar Desmera
|
|
@@ -22,6 +22,7 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
|
22
22
|
Requires-Dist: arpeggio>=2.0.3
|
|
23
23
|
Requires-Dist: pytest>=7.0.0 ; extra == 'dev'
|
|
24
24
|
Requires-Dist: pytest-cov>=7.1.0 ; extra == 'dev'
|
|
25
|
+
Requires-Dist: pyyaml>=6.0 ; extra == 'dev'
|
|
25
26
|
Requires-Dist: pyyaml>=6.0 ; extra == 'yaml'
|
|
26
27
|
Maintainer: Revar Desmera
|
|
27
28
|
Maintainer-email: Revar Desmera <revarbat@gmail.com>
|
|
@@ -4,7 +4,7 @@ build-backend = "uv_build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "openscad_parser"
|
|
7
|
-
version = "2.3.
|
|
7
|
+
version = "2.3.2"
|
|
8
8
|
description = "A PEG parser to read OpenSCAD language source code, with optional AST tree generation."
|
|
9
9
|
readme = "README.rst"
|
|
10
10
|
authors = [
|
|
@@ -39,6 +39,7 @@ dependencies = [
|
|
|
39
39
|
dev = [
|
|
40
40
|
"pytest>=7.0.0",
|
|
41
41
|
"pytest-cov>=7.1.0",
|
|
42
|
+
"PyYAML>=6.0",
|
|
42
43
|
]
|
|
43
44
|
yaml = [
|
|
44
45
|
"PyYAML>=6.0",
|
|
@@ -9,18 +9,22 @@ from .nodes import *
|
|
|
9
9
|
@dataclass
|
|
10
10
|
class Position:
|
|
11
11
|
"""Represents a location in a source origin.
|
|
12
|
-
|
|
12
|
+
|
|
13
13
|
Attributes:
|
|
14
14
|
origin: Identifier for the source origin (e.g., file path, "<editor>", etc.)
|
|
15
15
|
line: Line number (1-indexed)
|
|
16
16
|
column: Column number (1-indexed)
|
|
17
|
+
start_offset: 0-based character offset of the token start within the origin's content
|
|
18
|
+
end_offset: 0-based character offset one past the token end within the origin's content
|
|
17
19
|
"""
|
|
18
20
|
origin: str
|
|
19
21
|
line: int
|
|
20
22
|
column: int
|
|
23
|
+
start_offset: int = 0
|
|
24
|
+
end_offset: int = 0
|
|
21
25
|
|
|
22
26
|
def __repr__(self):
|
|
23
|
-
return f"{self.origin}:{self.line}:{self.column}"
|
|
27
|
+
return f"{self.origin}:{self.line}:{self.column}[{self.start_offset}:{self.end_offset}]"
|
|
24
28
|
|
|
25
29
|
|
|
26
30
|
class SemanticChildren(list):
|
|
@@ -33,6 +37,11 @@ class SemanticChildren(list):
|
|
|
33
37
|
def __getattr__(self, name):
|
|
34
38
|
return self._rule_map.get(name, [])
|
|
35
39
|
|
|
40
|
+
def get_rule(self, rule_name, index=0):
|
|
41
|
+
"""Return the index-th result for rule_name, or [] if absent/out of range."""
|
|
42
|
+
results = self._rule_map.get(rule_name, [])
|
|
43
|
+
return results[index] if index < len(results) else []
|
|
44
|
+
|
|
36
45
|
|
|
37
46
|
class ASTBuilderVisitor(PTNodeVisitor):
|
|
38
47
|
"""
|
|
@@ -136,26 +145,43 @@ class ASTBuilderVisitor(PTNodeVisitor):
|
|
|
136
145
|
# This is useful for operator nodes like TOK_BINARY_OR, TOK_EQUAL, etc.
|
|
137
146
|
return node
|
|
138
147
|
|
|
148
|
+
def _get_node_end_position(self, node) -> int:
|
|
149
|
+
"""Return the combined-string offset one past the last character of node."""
|
|
150
|
+
if node is None:
|
|
151
|
+
return 0
|
|
152
|
+
try:
|
|
153
|
+
# NonTerminal: recurse to find the furthest end among children
|
|
154
|
+
end = getattr(node, 'position', 0)
|
|
155
|
+
for child in node:
|
|
156
|
+
child_end = self._get_node_end_position(child)
|
|
157
|
+
if child_end > end:
|
|
158
|
+
end = child_end
|
|
159
|
+
return end
|
|
160
|
+
except TypeError:
|
|
161
|
+
pass
|
|
162
|
+
# Terminal: start + length of matched value
|
|
163
|
+
pos = getattr(node, 'position', 0)
|
|
164
|
+
val = getattr(node, 'value', '')
|
|
165
|
+
return pos + len(str(val))
|
|
166
|
+
|
|
139
167
|
def _get_node_position(self, node):
|
|
140
168
|
"""Extract position information from an Arpeggio parse tree node.
|
|
141
|
-
|
|
169
|
+
|
|
142
170
|
Uses SourceMap to map positions back to original origins if available.
|
|
143
|
-
|
|
171
|
+
start_offset and end_offset are 0-based offsets within the origin's content.
|
|
172
|
+
|
|
144
173
|
Args:
|
|
145
174
|
node: Arpeggio parse tree node
|
|
146
|
-
|
|
175
|
+
|
|
147
176
|
Returns:
|
|
148
177
|
Position object for the node
|
|
149
178
|
"""
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
char_pos = 0
|
|
154
|
-
|
|
179
|
+
char_pos = getattr(node, 'position', 0)
|
|
180
|
+
end_pos = self._get_node_end_position(node)
|
|
181
|
+
|
|
155
182
|
# Use SourceMap if available to map position back to original origin
|
|
156
183
|
if hasattr(self, 'source_map') and self.source_map.get_segments():
|
|
157
|
-
|
|
158
|
-
return Position(origin=location.origin, line=location.line, column=location.column)
|
|
184
|
+
return self.source_map.get_location(char_pos, end_pos)
|
|
159
185
|
else:
|
|
160
186
|
# Fallback: calculate line/column from character position
|
|
161
187
|
input_str = self.parser.input if hasattr(self.parser, 'input') else ""
|
|
@@ -167,8 +193,14 @@ class ASTBuilderVisitor(PTNodeVisitor):
|
|
|
167
193
|
line = text_before.count('\n') + 1
|
|
168
194
|
last_newline = text_before.rfind('\n')
|
|
169
195
|
column = char_pos - last_newline # 1-indexed
|
|
170
|
-
|
|
171
|
-
return Position(
|
|
196
|
+
|
|
197
|
+
return Position(
|
|
198
|
+
origin=self.file if self.file else "<unknown>",
|
|
199
|
+
line=line,
|
|
200
|
+
column=column,
|
|
201
|
+
start_offset=char_pos,
|
|
202
|
+
end_offset=end_pos,
|
|
203
|
+
)
|
|
172
204
|
|
|
173
205
|
|
|
174
206
|
# -- Basic Tokens --
|
|
@@ -455,7 +487,7 @@ class ASTBuilderVisitor(PTNodeVisitor):
|
|
|
455
487
|
# After visiting: [Identifier, parameters, statement] (KWD_MODULE returns None)
|
|
456
488
|
if hasattr(children, "module_name"):
|
|
457
489
|
name = children.module_name[0] if children.module_name else None
|
|
458
|
-
parameters = children.
|
|
490
|
+
parameters = children.get_rule("parameter_block")
|
|
459
491
|
statement = list(children.statement) if hasattr(children, "statement") and children.statement else []
|
|
460
492
|
else:
|
|
461
493
|
name = children[0] if children else None
|
|
@@ -491,7 +523,7 @@ class ASTBuilderVisitor(PTNodeVisitor):
|
|
|
491
523
|
# After visiting: [Identifier, parameters, expr] (KWD_FUNCTION, TOK_ASSIGN, TOK_SEMICOLON return None)
|
|
492
524
|
if hasattr(children, "function_name"):
|
|
493
525
|
name = children.function_name[0] if children.function_name else None
|
|
494
|
-
parameters = children.
|
|
526
|
+
parameters = children.get_rule("parameter_block")
|
|
495
527
|
expr = children.expr[0] if hasattr(children, "expr") and children.expr else None
|
|
496
528
|
else:
|
|
497
529
|
name = children[0] if children else None
|
|
@@ -551,7 +583,7 @@ class ASTBuilderVisitor(PTNodeVisitor):
|
|
|
551
583
|
# Grammar: (KWD_LET, TOK_PAREN, assignments_expr, TOK_ENDPAREN, expr)
|
|
552
584
|
# After visiting: [assignments_list, expr] (KWD_LET, TOK_PAREN, TOK_ENDPAREN return None)
|
|
553
585
|
if hasattr(children, "assignments_expr"):
|
|
554
|
-
assignments = children.assignments_expr
|
|
586
|
+
assignments = children.get_rule("assignments_expr")
|
|
555
587
|
body = children.expr[0] if hasattr(children, "expr") and children.expr else None
|
|
556
588
|
else:
|
|
557
589
|
assignments = children[0] if len(children) > 0 else []
|
|
@@ -559,12 +591,12 @@ class ASTBuilderVisitor(PTNodeVisitor):
|
|
|
559
591
|
if body is None:
|
|
560
592
|
raise ValueError("let_expr should have an Expression body")
|
|
561
593
|
return LetOp(assignments=assignments, body=body, position=self._get_node_position(node))
|
|
562
|
-
|
|
594
|
+
|
|
563
595
|
def visit_assert_expr(self, node, children):
|
|
564
596
|
# Grammar: (KWD_ASSERT, TOK_PAREN, arguments, TOK_ENDPAREN, Optional(expr))
|
|
565
597
|
# After visiting: [arguments_list, expr or None] (KWD_ASSERT, TOK_PAREN, TOK_ENDPAREN return None)
|
|
566
598
|
if hasattr(children, "arguments"):
|
|
567
|
-
arguments = children.arguments
|
|
599
|
+
arguments = children.get_rule("arguments")
|
|
568
600
|
body = children.expr[0] if hasattr(children, "expr") and children.expr else None
|
|
569
601
|
else:
|
|
570
602
|
arguments = children[0] if len(children) > 0 else []
|
|
@@ -572,12 +604,12 @@ class ASTBuilderVisitor(PTNodeVisitor):
|
|
|
572
604
|
if body is None:
|
|
573
605
|
raise ValueError("assert_expr should have an Expression body")
|
|
574
606
|
return AssertOp(arguments=arguments, body=body, position=self._get_node_position(node))
|
|
575
|
-
|
|
607
|
+
|
|
576
608
|
def visit_echo_expr(self, node, children):
|
|
577
609
|
# Grammar: (KWD_ECHO, TOK_PAREN, arguments, TOK_ENDPAREN, Optional(expr))
|
|
578
610
|
# After visiting: [arguments_list, expr or None] (KWD_ECHO, TOK_PAREN, TOK_ENDPAREN return None)
|
|
579
611
|
if hasattr(children, "arguments"):
|
|
580
|
-
arguments = children.arguments
|
|
612
|
+
arguments = children.get_rule("arguments")
|
|
581
613
|
body = children.expr[0] if hasattr(children, "expr") and children.expr else None
|
|
582
614
|
else:
|
|
583
615
|
arguments = children[0] if len(children) > 0 else []
|
|
@@ -940,9 +972,9 @@ class ASTBuilderVisitor(PTNodeVisitor):
|
|
|
940
972
|
for op in reversed(ops):
|
|
941
973
|
if op == '-':
|
|
942
974
|
result = UnaryMinusOp(expr=result, position=self._get_node_position(node))
|
|
943
|
-
elif op == '!':
|
|
975
|
+
elif op == '!': # pragma: no cover
|
|
944
976
|
result = LogicalNotOp(expr=result, position=self._get_node_position(node))
|
|
945
|
-
elif op == '~':
|
|
977
|
+
elif op == '~': # pragma: no cover
|
|
946
978
|
result = BitwiseNotOp(expr=result, position=self._get_node_position(node))
|
|
947
979
|
|
|
948
980
|
return result
|
|
@@ -1015,11 +1047,7 @@ class ASTBuilderVisitor(PTNodeVisitor):
|
|
|
1015
1047
|
return RangeLiteral(start=start, end=end, step=step, position=self._get_node_position(node))
|
|
1016
1048
|
|
|
1017
1049
|
def visit_vector_expr(self, node, children):
|
|
1018
|
-
elements = children
|
|
1019
|
-
if elements is None: # pragma: no cover
|
|
1020
|
-
elements = []
|
|
1021
|
-
if not isinstance(elements, list):
|
|
1022
|
-
elements = [elements]
|
|
1050
|
+
elements = children if children else []
|
|
1023
1051
|
return ListComprehension(elements=elements, position=self._get_node_position(node))
|
|
1024
1052
|
|
|
1025
1053
|
def visit_funclit_def(self, node, children):
|
|
@@ -1035,7 +1063,7 @@ class ASTBuilderVisitor(PTNodeVisitor):
|
|
|
1035
1063
|
return children[0]
|
|
1036
1064
|
|
|
1037
1065
|
def visit_listcomp_paren_expr(self, node, children):
|
|
1038
|
-
return children[0]
|
|
1066
|
+
return children[0]
|
|
1039
1067
|
|
|
1040
1068
|
def visit_listcomp_let(self, node, children):
|
|
1041
1069
|
return ListCompLet(assignments=children[0], body=children[1], position=self._get_node_position(node))
|
|
@@ -1080,22 +1108,14 @@ class ASTBuilderVisitor(PTNodeVisitor):
|
|
|
1080
1108
|
raise ValueError("modular_call should have a module name")
|
|
1081
1109
|
if not isinstance(name, Identifier):
|
|
1082
1110
|
name = Identifier(name=str(name), position=self._get_node_position(node))
|
|
1083
|
-
if hasattr(children, "
|
|
1084
|
-
arguments = children.arguments[0]
|
|
1085
|
-
else:
|
|
1086
|
-
arguments = children[1] if len(children) > 1 else []
|
|
1111
|
+
arguments = children.get_rule("arguments") if hasattr(children, "get_rule") else (children[1] if len(children) > 1 else [])
|
|
1087
1112
|
if arguments is None: # pragma: no cover
|
|
1088
1113
|
arguments = []
|
|
1089
1114
|
if not isinstance(arguments, list): # pragma: no cover
|
|
1090
1115
|
arguments = [arguments]
|
|
1091
|
-
if hasattr(children, "
|
|
1092
|
-
mods = children.child_statement[0]
|
|
1093
|
-
else:
|
|
1094
|
-
mods = children[2] if len(children) > 2 else []
|
|
1116
|
+
mods = children.get_rule("child_statement") if hasattr(children, "get_rule") else (children[2] if len(children) > 2 else [])
|
|
1095
1117
|
if mods is None: # pragma: no cover
|
|
1096
1118
|
mods = []
|
|
1097
|
-
if not isinstance(mods, list):
|
|
1098
|
-
mods = [mods]
|
|
1099
1119
|
return ModularCall(
|
|
1100
1120
|
name=name,
|
|
1101
1121
|
arguments=arguments,
|
|
@@ -1106,57 +1126,66 @@ class ASTBuilderVisitor(PTNodeVisitor):
|
|
|
1106
1126
|
def visit_modular_c_for(self, node, children):
|
|
1107
1127
|
initial = children[0] if isinstance(children[0], list) else [children[0]]
|
|
1108
1128
|
increment = children[2] if isinstance(children[2], list) else [children[2]]
|
|
1129
|
+
body = children.get_rule('child_statement')
|
|
1109
1130
|
return ModularCFor(
|
|
1110
1131
|
initial=initial,
|
|
1111
1132
|
condition=children[1],
|
|
1112
1133
|
increment=increment,
|
|
1113
|
-
body=
|
|
1134
|
+
body=body,
|
|
1114
1135
|
position=self._get_node_position(node)
|
|
1115
1136
|
)
|
|
1116
1137
|
|
|
1117
1138
|
def visit_modular_for(self, node, children):
|
|
1118
1139
|
assignments = children[0] if isinstance(children[0], list) else [children[0]]
|
|
1140
|
+
body = children.get_rule('child_statement')
|
|
1119
1141
|
return ModularFor(
|
|
1120
1142
|
assignments=assignments,
|
|
1121
|
-
body=
|
|
1143
|
+
body=body,
|
|
1122
1144
|
position=self._get_node_position(node)
|
|
1123
1145
|
)
|
|
1124
1146
|
|
|
1125
1147
|
def visit_modular_intersection_c_for(self, node, children):
|
|
1126
1148
|
initial = children[0] if isinstance(children[0], list) else [children[0]]
|
|
1127
1149
|
increment = children[2] if isinstance(children[2], list) else [children[2]]
|
|
1150
|
+
body = children.get_rule('child_statement')
|
|
1128
1151
|
return ModularIntersectionCFor(
|
|
1129
1152
|
initial=initial,
|
|
1130
1153
|
condition=children[1],
|
|
1131
1154
|
increment=increment,
|
|
1132
|
-
body=
|
|
1155
|
+
body=body,
|
|
1133
1156
|
position=self._get_node_position(node)
|
|
1134
1157
|
)
|
|
1135
1158
|
|
|
1136
1159
|
def visit_modular_intersection_for(self, node, children):
|
|
1137
1160
|
assignments = children[0] if isinstance(children[0], list) else [children[0]]
|
|
1138
|
-
|
|
1161
|
+
body = children.get_rule('child_statement')
|
|
1162
|
+
return ModularIntersectionFor(assignments=assignments, body=body, position=self._get_node_position(node))
|
|
1139
1163
|
|
|
1140
1164
|
def visit_modular_let(self, node, children):
|
|
1141
1165
|
assignments = children[0] if isinstance(children[0], list) else [children[0]]
|
|
1142
|
-
mods = children
|
|
1166
|
+
mods = children.get_rule('child_statement')
|
|
1143
1167
|
return ModularLet(assignments=assignments, children=mods, position=self._get_node_position(node))
|
|
1144
1168
|
|
|
1145
1169
|
def visit_modular_echo(self, node, children):
|
|
1146
1170
|
arguments = children[0] if isinstance(children[0], list) else [children[0]]
|
|
1147
|
-
mods = children
|
|
1171
|
+
mods = children.get_rule('child_statement')
|
|
1148
1172
|
return ModularEcho(arguments=arguments, children=mods, position=self._get_node_position(node))
|
|
1149
1173
|
|
|
1150
1174
|
def visit_modular_assert(self, node, children):
|
|
1151
1175
|
arguments = children[0] if isinstance(children[0], list) else [children[0]]
|
|
1152
|
-
mods = children
|
|
1176
|
+
mods = children.get_rule('child_statement')
|
|
1153
1177
|
return ModularAssert(arguments=arguments, children=mods, position=self._get_node_position(node))
|
|
1154
1178
|
|
|
1155
1179
|
def visit_if_statement(self, node, children):
|
|
1156
|
-
|
|
1157
|
-
|
|
1180
|
+
condition = children[0]
|
|
1181
|
+
true_branch = children.get_rule('child_statement')
|
|
1182
|
+
return ModularIf(condition=condition, true_branch=true_branch, position=self._get_node_position(node))
|
|
1183
|
+
|
|
1158
1184
|
def visit_ifelse_statement(self, node, children):
|
|
1159
|
-
|
|
1185
|
+
condition = children[0]
|
|
1186
|
+
true_branch = children.get_rule('child_statement')
|
|
1187
|
+
false_branch = children.get_rule('child_statement', index=1)
|
|
1188
|
+
return ModularIfElse(condition=condition, true_branch=true_branch, false_branch=false_branch, position=self._get_node_position(node))
|
|
1160
1189
|
|
|
1161
1190
|
def visit_modifier_show_only(self, node, children):
|
|
1162
1191
|
child = children[0] # We know modifiers can only have one child.
|
|
@@ -1229,8 +1258,10 @@ class ASTBuilderVisitor(PTNodeVisitor):
|
|
|
1229
1258
|
|
|
1230
1259
|
def visit_child_statement(self, node, children):
|
|
1231
1260
|
# Grammar: [empty_statement, statement_block, module_instantiation]
|
|
1232
|
-
# Return
|
|
1233
|
-
|
|
1261
|
+
# Return all children as a list. statement_block flattens its contents
|
|
1262
|
+
# into children via _visit_node's extend behavior, so we must return all
|
|
1263
|
+
# of them rather than just children[0] (which would silently drop siblings).
|
|
1264
|
+
return list(children)
|
|
1234
1265
|
|
|
1235
1266
|
def visit_expr(self, node, children) -> Expression:
|
|
1236
1267
|
if not children:
|
|
@@ -184,6 +184,8 @@ def _serialize_position(position: Position) -> dict[str, Any]:
|
|
|
184
184
|
"origin": position.origin,
|
|
185
185
|
"line": position.line,
|
|
186
186
|
"column": position.column,
|
|
187
|
+
"start_offset": position.start_offset,
|
|
188
|
+
"end_offset": position.end_offset,
|
|
187
189
|
}
|
|
188
190
|
|
|
189
191
|
|
|
@@ -277,6 +279,8 @@ def _deserialize_position(data: dict[str, Any]) -> Position:
|
|
|
277
279
|
origin=data["origin"],
|
|
278
280
|
line=data["line"],
|
|
279
281
|
column=data["column"],
|
|
282
|
+
start_offset=data.get("start_offset", 0),
|
|
283
|
+
end_offset=data.get("end_offset", 0),
|
|
280
284
|
)
|
|
281
285
|
|
|
282
286
|
|
|
@@ -118,9 +118,6 @@ class SourceMap:
|
|
|
118
118
|
start_pos: Starting position in the combined string (0-indexed)
|
|
119
119
|
length: Number of characters to replace
|
|
120
120
|
"""
|
|
121
|
-
if length <= 0:
|
|
122
|
-
return
|
|
123
|
-
|
|
124
121
|
end_pos = start_pos + length
|
|
125
122
|
|
|
126
123
|
# Collect segments to modify and new segments to add (for splits)
|
|
@@ -264,34 +261,48 @@ class SourceMap:
|
|
|
264
261
|
self._combined_string = ''.join(parts)
|
|
265
262
|
self._combined_string_dirty = False
|
|
266
263
|
|
|
267
|
-
def get_location(self, position: int):
|
|
264
|
+
def get_location(self, position: int, end_position: Optional[int] = None):
|
|
268
265
|
"""Get the original source location for a position in the combined string.
|
|
269
|
-
|
|
266
|
+
|
|
270
267
|
Args:
|
|
271
268
|
position: Character position in the combined string (0-indexed)
|
|
272
|
-
|
|
269
|
+
end_position: Optional end position (exclusive) in the combined string.
|
|
270
|
+
If provided, end_offset in the returned Position will reflect the
|
|
271
|
+
end of the token within the origin's content.
|
|
272
|
+
|
|
273
273
|
Returns:
|
|
274
|
-
Position with origin, line,
|
|
274
|
+
Position with origin, line, column, start_offset, and end_offset
|
|
275
275
|
"""
|
|
276
276
|
from .builder import Position # Lazy import to avoid circular dependency
|
|
277
|
-
|
|
277
|
+
|
|
278
278
|
if position < 0:
|
|
279
279
|
position = 0
|
|
280
|
-
|
|
280
|
+
|
|
281
281
|
# Find the segment containing this position
|
|
282
282
|
segment = self._find_segment(position)
|
|
283
|
-
|
|
283
|
+
|
|
284
284
|
if segment is None:
|
|
285
285
|
# Position is beyond all segments, return location from last segment
|
|
286
286
|
if self._segments:
|
|
287
287
|
last_segment = max(self._segments, key=lambda s: s.combined_start + len(s.content))
|
|
288
|
-
|
|
288
|
+
seg_offset = len(last_segment.content)
|
|
289
|
+
end_offset = (
|
|
290
|
+
(end_position - last_segment.combined_start)
|
|
291
|
+
if end_position is not None
|
|
292
|
+
else seg_offset
|
|
293
|
+
)
|
|
294
|
+
return self._calculate_location_in_segment(last_segment, seg_offset, end_offset)
|
|
289
295
|
else:
|
|
290
296
|
return Position(origin="", line=1, column=1)
|
|
291
|
-
|
|
292
|
-
# Calculate
|
|
297
|
+
|
|
298
|
+
# Calculate offsets within the segment
|
|
293
299
|
segment_offset = position - segment.combined_start
|
|
294
|
-
|
|
300
|
+
end_offset = (
|
|
301
|
+
(end_position - segment.combined_start)
|
|
302
|
+
if end_position is not None
|
|
303
|
+
else segment_offset
|
|
304
|
+
)
|
|
305
|
+
return self._calculate_location_in_segment(segment, segment_offset, end_offset)
|
|
295
306
|
|
|
296
307
|
def _find_segment(self, position: int) -> Optional[SourceSegment]:
|
|
297
308
|
"""Find the segment containing the given position.
|
|
@@ -316,38 +327,34 @@ class SourceMap:
|
|
|
316
327
|
else:
|
|
317
328
|
left = mid + 1
|
|
318
329
|
|
|
319
|
-
# Check if position is in the last segment
|
|
320
|
-
if self._segments:
|
|
321
|
-
last_segment = self._segments[-1]
|
|
322
|
-
if last_segment.combined_start <= position < last_segment.combined_start + len(last_segment.content):
|
|
323
|
-
return last_segment
|
|
324
|
-
|
|
325
330
|
return None
|
|
326
331
|
|
|
327
|
-
def _calculate_location_in_segment(self, segment: SourceSegment, offset: int
|
|
332
|
+
def _calculate_location_in_segment(self, segment: SourceSegment, offset: int,
|
|
333
|
+
end_offset: Optional[int] = None):
|
|
328
334
|
"""Calculate the source location for an offset within a segment.
|
|
329
|
-
|
|
335
|
+
|
|
330
336
|
Args:
|
|
331
337
|
segment: The source segment
|
|
332
|
-
offset: Character offset within the segment (0-indexed)
|
|
333
|
-
|
|
338
|
+
offset: Character offset within the segment (0-indexed), used for start
|
|
339
|
+
end_offset: Optional exclusive end offset within the segment (0-indexed)
|
|
340
|
+
|
|
334
341
|
Returns:
|
|
335
|
-
Position with the original source location
|
|
342
|
+
Position with the original source location, start_offset, and end_offset
|
|
336
343
|
"""
|
|
337
344
|
from .builder import Position # Lazy import to avoid circular dependency
|
|
338
|
-
|
|
345
|
+
|
|
339
346
|
if offset < 0:
|
|
340
347
|
offset = 0 # pragma: no cover
|
|
341
348
|
if offset > len(segment.content):
|
|
342
349
|
offset = len(segment.content) # pragma: no cover
|
|
343
|
-
|
|
350
|
+
|
|
344
351
|
# Count lines in the content up to the offset
|
|
345
352
|
content_before = segment.content[:offset]
|
|
346
353
|
line_count = content_before.count('\n')
|
|
347
|
-
|
|
354
|
+
|
|
348
355
|
# Calculate line number
|
|
349
356
|
line_number = segment.start_line + line_count
|
|
350
|
-
|
|
357
|
+
|
|
351
358
|
# Calculate column number
|
|
352
359
|
if line_count == 0:
|
|
353
360
|
# Same line as start
|
|
@@ -356,11 +363,15 @@ class SourceMap:
|
|
|
356
363
|
# Find the last newline before offset
|
|
357
364
|
last_newline = content_before.rfind('\n')
|
|
358
365
|
column_number = offset - last_newline
|
|
359
|
-
|
|
366
|
+
|
|
367
|
+
resolved_end = end_offset if end_offset is not None else offset
|
|
368
|
+
|
|
360
369
|
return Position(
|
|
361
370
|
origin=segment.origin,
|
|
362
371
|
line=line_number,
|
|
363
|
-
column=column_number
|
|
372
|
+
column=column_number,
|
|
373
|
+
start_offset=offset,
|
|
374
|
+
end_offset=resolved_end,
|
|
364
375
|
)
|
|
365
376
|
|
|
366
377
|
def get_segments(self) -> list[SourceSegment]:
|
|
@@ -454,7 +454,7 @@ def modular_echo():
|
|
|
454
454
|
|
|
455
455
|
|
|
456
456
|
def modular_call():
|
|
457
|
-
return (module_instantiation_name, TOK_PAREN, arguments, TOK_ENDPAREN, child_statement)
|
|
457
|
+
return (module_instantiation_name, TOK_PAREN, arguments, TOK_ENDPAREN, ZeroOrMore(comment), child_statement)
|
|
458
458
|
|
|
459
459
|
|
|
460
460
|
# --- Parameters used to define functions and modules ---
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|