openscad-parser 2.2.1__tar.gz → 2.3.1__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.2.1 → openscad_parser-2.3.1}/PKG-INFO +2 -1
- {openscad_parser-2.2.1 → openscad_parser-2.3.1}/pyproject.toml +2 -1
- {openscad_parser-2.2.1 → openscad_parser-2.3.1}/src/openscad_parser/ast/builder.py +99 -44
- {openscad_parser-2.2.1 → openscad_parser-2.3.1}/src/openscad_parser/ast/serialization.py +4 -0
- {openscad_parser-2.2.1 → openscad_parser-2.3.1}/src/openscad_parser/ast/source_map.py +42 -22
- {openscad_parser-2.2.1 → openscad_parser-2.3.1}/src/openscad_parser/grammar.py +1 -1
- {openscad_parser-2.2.1 → openscad_parser-2.3.1}/README.rst +0 -0
- {openscad_parser-2.2.1 → openscad_parser-2.3.1}/src/openscad_parser/__init__.py +0 -0
- {openscad_parser-2.2.1 → openscad_parser-2.3.1}/src/openscad_parser/ast/__init__.py +0 -0
- {openscad_parser-2.2.1 → openscad_parser-2.3.1}/src/openscad_parser/ast/nodes.py +0 -0
- {openscad_parser-2.2.1 → openscad_parser-2.3.1}/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
|
+
Version: 2.3.1
|
|
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
|
|
@@ -21,6 +21,7 @@ Classifier: Topic :: Software Development :: Libraries
|
|
|
21
21
|
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
|
+
Requires-Dist: pytest-cov>=7.1.0 ; extra == 'dev'
|
|
24
25
|
Requires-Dist: pyyaml>=6.0 ; extra == 'yaml'
|
|
25
26
|
Maintainer: Revar Desmera
|
|
26
27
|
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.
|
|
7
|
+
version = "2.3.1"
|
|
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 = [
|
|
@@ -38,6 +38,7 @@ dependencies = [
|
|
|
38
38
|
[project.optional-dependencies]
|
|
39
39
|
dev = [
|
|
40
40
|
"pytest>=7.0.0",
|
|
41
|
+
"pytest-cov>=7.1.0",
|
|
41
42
|
]
|
|
42
43
|
yaml = [
|
|
43
44
|
"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 []
|
|
@@ -1015,9 +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 = []
|
|
1050
|
+
elements = children if children else []
|
|
1021
1051
|
if not isinstance(elements, list):
|
|
1022
1052
|
elements = [elements]
|
|
1023
1053
|
return ListComprehension(elements=elements, position=self._get_node_position(node))
|
|
@@ -1080,18 +1110,12 @@ class ASTBuilderVisitor(PTNodeVisitor):
|
|
|
1080
1110
|
raise ValueError("modular_call should have a module name")
|
|
1081
1111
|
if not isinstance(name, Identifier):
|
|
1082
1112
|
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 []
|
|
1113
|
+
arguments = children.get_rule("arguments") if hasattr(children, "get_rule") else (children[1] if len(children) > 1 else [])
|
|
1087
1114
|
if arguments is None: # pragma: no cover
|
|
1088
1115
|
arguments = []
|
|
1089
1116
|
if not isinstance(arguments, list): # pragma: no cover
|
|
1090
1117
|
arguments = [arguments]
|
|
1091
|
-
if hasattr(children, "
|
|
1092
|
-
mods = children.child_statement[0]
|
|
1093
|
-
else:
|
|
1094
|
-
mods = children[2] if len(children) > 2 else []
|
|
1118
|
+
mods = children.get_rule("child_statement") if hasattr(children, "get_rule") else (children[2] if len(children) > 2 else [])
|
|
1095
1119
|
if mods is None: # pragma: no cover
|
|
1096
1120
|
mods = []
|
|
1097
1121
|
if not isinstance(mods, list):
|
|
@@ -1106,57 +1130,86 @@ class ASTBuilderVisitor(PTNodeVisitor):
|
|
|
1106
1130
|
def visit_modular_c_for(self, node, children):
|
|
1107
1131
|
initial = children[0] if isinstance(children[0], list) else [children[0]]
|
|
1108
1132
|
increment = children[2] if isinstance(children[2], list) else [children[2]]
|
|
1133
|
+
body = children.get_rule('child_statement')
|
|
1134
|
+
if not isinstance(body, list):
|
|
1135
|
+
body = [body]
|
|
1109
1136
|
return ModularCFor(
|
|
1110
1137
|
initial=initial,
|
|
1111
1138
|
condition=children[1],
|
|
1112
1139
|
increment=increment,
|
|
1113
|
-
body=
|
|
1140
|
+
body=body,
|
|
1114
1141
|
position=self._get_node_position(node)
|
|
1115
1142
|
)
|
|
1116
1143
|
|
|
1117
1144
|
def visit_modular_for(self, node, children):
|
|
1118
1145
|
assignments = children[0] if isinstance(children[0], list) else [children[0]]
|
|
1146
|
+
body = children.get_rule('child_statement')
|
|
1147
|
+
if not isinstance(body, list):
|
|
1148
|
+
body = [body]
|
|
1119
1149
|
return ModularFor(
|
|
1120
1150
|
assignments=assignments,
|
|
1121
|
-
body=
|
|
1151
|
+
body=body,
|
|
1122
1152
|
position=self._get_node_position(node)
|
|
1123
1153
|
)
|
|
1124
1154
|
|
|
1125
1155
|
def visit_modular_intersection_c_for(self, node, children):
|
|
1126
1156
|
initial = children[0] if isinstance(children[0], list) else [children[0]]
|
|
1127
1157
|
increment = children[2] if isinstance(children[2], list) else [children[2]]
|
|
1158
|
+
body = children.get_rule('child_statement')
|
|
1159
|
+
if not isinstance(body, list):
|
|
1160
|
+
body = [body]
|
|
1128
1161
|
return ModularIntersectionCFor(
|
|
1129
1162
|
initial=initial,
|
|
1130
1163
|
condition=children[1],
|
|
1131
1164
|
increment=increment,
|
|
1132
|
-
body=
|
|
1165
|
+
body=body,
|
|
1133
1166
|
position=self._get_node_position(node)
|
|
1134
1167
|
)
|
|
1135
1168
|
|
|
1136
1169
|
def visit_modular_intersection_for(self, node, children):
|
|
1137
1170
|
assignments = children[0] if isinstance(children[0], list) else [children[0]]
|
|
1138
|
-
|
|
1171
|
+
body = children.get_rule('child_statement')
|
|
1172
|
+
if not isinstance(body, list):
|
|
1173
|
+
body = [body]
|
|
1174
|
+
return ModularIntersectionFor(assignments=assignments, body=body, position=self._get_node_position(node))
|
|
1139
1175
|
|
|
1140
1176
|
def visit_modular_let(self, node, children):
|
|
1141
1177
|
assignments = children[0] if isinstance(children[0], list) else [children[0]]
|
|
1142
|
-
mods = children
|
|
1178
|
+
mods = children.get_rule('child_statement')
|
|
1179
|
+
if not isinstance(mods, list):
|
|
1180
|
+
mods = [mods]
|
|
1143
1181
|
return ModularLet(assignments=assignments, children=mods, position=self._get_node_position(node))
|
|
1144
1182
|
|
|
1145
1183
|
def visit_modular_echo(self, node, children):
|
|
1146
1184
|
arguments = children[0] if isinstance(children[0], list) else [children[0]]
|
|
1147
|
-
mods = children
|
|
1185
|
+
mods = children.get_rule('child_statement')
|
|
1186
|
+
if not isinstance(mods, list):
|
|
1187
|
+
mods = [mods]
|
|
1148
1188
|
return ModularEcho(arguments=arguments, children=mods, position=self._get_node_position(node))
|
|
1149
1189
|
|
|
1150
1190
|
def visit_modular_assert(self, node, children):
|
|
1151
1191
|
arguments = children[0] if isinstance(children[0], list) else [children[0]]
|
|
1152
|
-
mods = children
|
|
1192
|
+
mods = children.get_rule('child_statement')
|
|
1193
|
+
if not isinstance(mods, list):
|
|
1194
|
+
mods = [mods]
|
|
1153
1195
|
return ModularAssert(arguments=arguments, children=mods, position=self._get_node_position(node))
|
|
1154
1196
|
|
|
1155
1197
|
def visit_if_statement(self, node, children):
|
|
1156
|
-
|
|
1157
|
-
|
|
1198
|
+
condition = children[0]
|
|
1199
|
+
true_branch = children.get_rule('child_statement')
|
|
1200
|
+
if not isinstance(true_branch, list):
|
|
1201
|
+
true_branch = [true_branch]
|
|
1202
|
+
return ModularIf(condition=condition, true_branch=true_branch, position=self._get_node_position(node))
|
|
1203
|
+
|
|
1158
1204
|
def visit_ifelse_statement(self, node, children):
|
|
1159
|
-
|
|
1205
|
+
condition = children[0]
|
|
1206
|
+
true_branch = children.get_rule('child_statement')
|
|
1207
|
+
false_branch = children.get_rule('child_statement', index=1)
|
|
1208
|
+
if not isinstance(true_branch, list):
|
|
1209
|
+
true_branch = [true_branch]
|
|
1210
|
+
if not isinstance(false_branch, list):
|
|
1211
|
+
false_branch = [false_branch]
|
|
1212
|
+
return ModularIfElse(condition=condition, true_branch=true_branch, false_branch=false_branch, position=self._get_node_position(node))
|
|
1160
1213
|
|
|
1161
1214
|
def visit_modifier_show_only(self, node, children):
|
|
1162
1215
|
child = children[0] # We know modifiers can only have one child.
|
|
@@ -1229,8 +1282,10 @@ class ASTBuilderVisitor(PTNodeVisitor):
|
|
|
1229
1282
|
|
|
1230
1283
|
def visit_child_statement(self, node, children):
|
|
1231
1284
|
# Grammar: [empty_statement, statement_block, module_instantiation]
|
|
1232
|
-
# Return
|
|
1233
|
-
|
|
1285
|
+
# Return all children as a list. statement_block flattens its contents
|
|
1286
|
+
# into children via _visit_node's extend behavior, so we must return all
|
|
1287
|
+
# of them rather than just children[0] (which would silently drop siblings).
|
|
1288
|
+
return list(children)
|
|
1234
1289
|
|
|
1235
1290
|
def visit_expr(self, node, children) -> Expression:
|
|
1236
1291
|
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
|
|
|
@@ -264,34 +264,48 @@ class SourceMap:
|
|
|
264
264
|
self._combined_string = ''.join(parts)
|
|
265
265
|
self._combined_string_dirty = False
|
|
266
266
|
|
|
267
|
-
def get_location(self, position: int):
|
|
267
|
+
def get_location(self, position: int, end_position: Optional[int] = None):
|
|
268
268
|
"""Get the original source location for a position in the combined string.
|
|
269
|
-
|
|
269
|
+
|
|
270
270
|
Args:
|
|
271
271
|
position: Character position in the combined string (0-indexed)
|
|
272
|
-
|
|
272
|
+
end_position: Optional end position (exclusive) in the combined string.
|
|
273
|
+
If provided, end_offset in the returned Position will reflect the
|
|
274
|
+
end of the token within the origin's content.
|
|
275
|
+
|
|
273
276
|
Returns:
|
|
274
|
-
Position with origin, line,
|
|
277
|
+
Position with origin, line, column, start_offset, and end_offset
|
|
275
278
|
"""
|
|
276
279
|
from .builder import Position # Lazy import to avoid circular dependency
|
|
277
|
-
|
|
280
|
+
|
|
278
281
|
if position < 0:
|
|
279
282
|
position = 0
|
|
280
|
-
|
|
283
|
+
|
|
281
284
|
# Find the segment containing this position
|
|
282
285
|
segment = self._find_segment(position)
|
|
283
|
-
|
|
286
|
+
|
|
284
287
|
if segment is None:
|
|
285
288
|
# Position is beyond all segments, return location from last segment
|
|
286
289
|
if self._segments:
|
|
287
290
|
last_segment = max(self._segments, key=lambda s: s.combined_start + len(s.content))
|
|
288
|
-
|
|
291
|
+
seg_offset = len(last_segment.content)
|
|
292
|
+
end_offset = (
|
|
293
|
+
(end_position - last_segment.combined_start)
|
|
294
|
+
if end_position is not None
|
|
295
|
+
else seg_offset
|
|
296
|
+
)
|
|
297
|
+
return self._calculate_location_in_segment(last_segment, seg_offset, end_offset)
|
|
289
298
|
else:
|
|
290
299
|
return Position(origin="", line=1, column=1)
|
|
291
|
-
|
|
292
|
-
# Calculate
|
|
300
|
+
|
|
301
|
+
# Calculate offsets within the segment
|
|
293
302
|
segment_offset = position - segment.combined_start
|
|
294
|
-
|
|
303
|
+
end_offset = (
|
|
304
|
+
(end_position - segment.combined_start)
|
|
305
|
+
if end_position is not None
|
|
306
|
+
else segment_offset
|
|
307
|
+
)
|
|
308
|
+
return self._calculate_location_in_segment(segment, segment_offset, end_offset)
|
|
295
309
|
|
|
296
310
|
def _find_segment(self, position: int) -> Optional[SourceSegment]:
|
|
297
311
|
"""Find the segment containing the given position.
|
|
@@ -324,30 +338,32 @@ class SourceMap:
|
|
|
324
338
|
|
|
325
339
|
return None
|
|
326
340
|
|
|
327
|
-
def _calculate_location_in_segment(self, segment: SourceSegment, offset: int
|
|
341
|
+
def _calculate_location_in_segment(self, segment: SourceSegment, offset: int,
|
|
342
|
+
end_offset: Optional[int] = None):
|
|
328
343
|
"""Calculate the source location for an offset within a segment.
|
|
329
|
-
|
|
344
|
+
|
|
330
345
|
Args:
|
|
331
346
|
segment: The source segment
|
|
332
|
-
offset: Character offset within the segment (0-indexed)
|
|
333
|
-
|
|
347
|
+
offset: Character offset within the segment (0-indexed), used for start
|
|
348
|
+
end_offset: Optional exclusive end offset within the segment (0-indexed)
|
|
349
|
+
|
|
334
350
|
Returns:
|
|
335
|
-
Position with the original source location
|
|
351
|
+
Position with the original source location, start_offset, and end_offset
|
|
336
352
|
"""
|
|
337
353
|
from .builder import Position # Lazy import to avoid circular dependency
|
|
338
|
-
|
|
354
|
+
|
|
339
355
|
if offset < 0:
|
|
340
356
|
offset = 0 # pragma: no cover
|
|
341
357
|
if offset > len(segment.content):
|
|
342
358
|
offset = len(segment.content) # pragma: no cover
|
|
343
|
-
|
|
359
|
+
|
|
344
360
|
# Count lines in the content up to the offset
|
|
345
361
|
content_before = segment.content[:offset]
|
|
346
362
|
line_count = content_before.count('\n')
|
|
347
|
-
|
|
363
|
+
|
|
348
364
|
# Calculate line number
|
|
349
365
|
line_number = segment.start_line + line_count
|
|
350
|
-
|
|
366
|
+
|
|
351
367
|
# Calculate column number
|
|
352
368
|
if line_count == 0:
|
|
353
369
|
# Same line as start
|
|
@@ -356,11 +372,15 @@ class SourceMap:
|
|
|
356
372
|
# Find the last newline before offset
|
|
357
373
|
last_newline = content_before.rfind('\n')
|
|
358
374
|
column_number = offset - last_newline
|
|
359
|
-
|
|
375
|
+
|
|
376
|
+
resolved_end = end_offset if end_offset is not None else offset
|
|
377
|
+
|
|
360
378
|
return Position(
|
|
361
379
|
origin=segment.origin,
|
|
362
380
|
line=line_number,
|
|
363
|
-
column=column_number
|
|
381
|
+
column=column_number,
|
|
382
|
+
start_offset=offset,
|
|
383
|
+
end_offset=resolved_end,
|
|
364
384
|
)
|
|
365
385
|
|
|
366
386
|
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
|