nadata 0.1.0__py3-none-any.whl
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.
- nadata/__init__.py +15 -0
- nadata/api.py +894 -0
- nadata/cli.py +445 -0
- nadata/console.py +486 -0
- nadata/core/__init__.py +0 -0
- nadata/core/database/__init__.py +38 -0
- nadata/core/database/builtins.py +87 -0
- nadata/core/database/database.py +958 -0
- nadata/core/database/dumper.py +285 -0
- nadata/core/database/entity_id.py +142 -0
- nadata/core/database/instance.py +706 -0
- nadata/core/database/network_index.py +1142 -0
- nadata/core/database/page_manager.py +175 -0
- nadata/core/database/persistence/__init__.py +28 -0
- nadata/core/database/persistence/binary_cells.py +1141 -0
- nadata/core/database/persistence/compaction.py +97 -0
- nadata/core/database/persistence/dumper.py +798 -0
- nadata/core/database/persistence/lazy_instances.py +1537 -0
- nadata/core/database/persistence/loader.py +424 -0
- nadata/core/database/persistence/record_store.py +228 -0
- nadata/core/database/persistence/serialization.py +605 -0
- nadata/core/database/query_engine.py +222 -0
- nadata/core/database/queryset.py +508 -0
- nadata/core/database/reconciliation.py +287 -0
- nadata/core/database/slotted_page.py +190 -0
- nadata/core/database/spatial_index.py +1080 -0
- nadata/core/expressions/__init__.py +44 -0
- nadata/core/expressions/autograd.py +2909 -0
- nadata/core/expressions/builtins.py +4673 -0
- nadata/core/expressions/expression.py +2658 -0
- nadata/core/expressions/io_builtins.py +972 -0
- nadata/core/expressions/scope.py +336 -0
- nadata/core/expressions/statement.py +953 -0
- nadata/core/struct/__init__.py +14 -0
- nadata/core/struct/struct.py +218 -0
- nadata/core/struct/struct_attribute.py +100 -0
- nadata/core/struct/struct_method.py +324 -0
- nadata/core/types/__init__.py +33 -0
- nadata/core/types/bits/__init__.py +0 -0
- nadata/core/types/bits/bits.py +686 -0
- nadata/core/types/home_types/__init__.py +19 -0
- nadata/core/types/home_types/color.py +102 -0
- nadata/core/types/home_types/coordinate.py +1008 -0
- nadata/core/types/home_types/datetime.py +362 -0
- nadata/core/types/home_types/geogrid.py +92 -0
- nadata/core/types/home_types/geometry.py +1223 -0
- nadata/core/types/home_types/graph.py +0 -0
- nadata/core/types/home_types/planet.py +359 -0
- nadata/core/types/home_types/sea_level.py +98 -0
- nadata/core/types/home_types/vec.py +0 -0
- nadata/core/types/types.py +1345 -0
- nadata/core/values/__init__.py +14 -0
- nadata/core/values/values.py +1568 -0
- nadata/parser/__init__.py +0 -0
- nadata/parser/expressions/__init__.py +6 -0
- nadata/parser/expressions/dependencies.py +318 -0
- nadata/parser/expressions/expression_parser.py +993 -0
- nadata/parser/expressions/splitter.py +98 -0
- nadata/parser/expressions/statement_parser.py +1425 -0
- nadata/parser/expressions/tokenizer.py +402 -0
- nadata/parser/expressions/type_parser.py +187 -0
- nadata/parser/nadata_loader/__init__.py +8 -0
- nadata/parser/nadata_loader/loader.py +951 -0
- nadata/parser/nastruct_parser/__init__.py +0 -0
- nadata/parser/nastruct_parser/declaration.py +105 -0
- nadata/parser/nastruct_parser/imports.py +164 -0
- nadata/parser/nastruct_parser/nastruct_parser.py +209 -0
- nadata/parser/nastruct_parser/parse_attributes.py +149 -0
- nadata/parser/nastruct_parser/parse_callables.py +436 -0
- nadata/parser/nastruct_parser/parse_expression.py +59 -0
- nadata/parser/nastruct_parser/parse_structs.py +206 -0
- nadata/parser/nastruct_parser/parser_state.py +84 -0
- nadata/parser/nastruct_parser/resolver.py +1412 -0
- nadata/parser/nastruct_parser/struct_body_state.py +50 -0
- nadata/parser/nastruct_parser/tmp_structs.py +156 -0
- nadata/parser/utils/__init__.py +20 -0
- nadata/parser/utils/brackets.py +252 -0
- nadata/parser/utils/comments_remove.py +149 -0
- nadata/parser/utils/identifiers.py +121 -0
- nadata/session.py +868 -0
- nadata-0.1.0.dist-info/METADATA +191 -0
- nadata-0.1.0.dist-info/RECORD +86 -0
- nadata-0.1.0.dist-info/WHEEL +5 -0
- nadata-0.1.0.dist-info/entry_points.txt +2 -0
- nadata-0.1.0.dist-info/licenses/LICENSE +21 -0
- nadata-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tokenizer for Nadata expressions and declarations.
|
|
3
|
+
|
|
4
|
+
Converts raw source text into a flat list of typed tokens, handling string
|
|
5
|
+
literals with escapes, numeric literals, identifiers, keywords, operators, and
|
|
6
|
+
punctuation. Used by the expression parser and the declaration splitters.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
# Import Modules
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from enum import Enum
|
|
13
|
+
from typing import Optional
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class TokenType(Enum):
|
|
17
|
+
"""
|
|
18
|
+
Enumeration of the token types produced by the Tokenizer.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
IDENTIFIER = "identifier"
|
|
22
|
+
KEYWORD = "keyword"
|
|
23
|
+
INT = "int"
|
|
24
|
+
FLOAT = "float"
|
|
25
|
+
STRING = "string"
|
|
26
|
+
OPERATOR = "operator"
|
|
27
|
+
PUNCTUATION = "punctuation"
|
|
28
|
+
EOF = "eof"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class Token:
|
|
33
|
+
"""
|
|
34
|
+
Single lexical token with its position in the source text.
|
|
35
|
+
|
|
36
|
+
Attributes:
|
|
37
|
+
token_type (TokenType): Category of the token.
|
|
38
|
+
value (str): Decoded token value (escapes resolved for strings).
|
|
39
|
+
position (int): Index of the first source character of the token.
|
|
40
|
+
end_position (int): Index just after the last source character.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
token_type: TokenType
|
|
44
|
+
value: str
|
|
45
|
+
position: int
|
|
46
|
+
end_position: int
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# Language keywords (logical operators are keyword-only per the specification)
|
|
50
|
+
KEYWORDS: frozenset[str] = frozenset({
|
|
51
|
+
"class", "fn", "if", "elif", "else", "return",
|
|
52
|
+
"true", "false", "and", "or", "xor", "nand", "not",
|
|
53
|
+
"for", "while", "break", "continue", "in",
|
|
54
|
+
"try", "catch", "raise",
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
# Two-character operators, checked before single-character ones
|
|
58
|
+
MULTI_CHAR_OPERATORS: tuple[str, ...] = (
|
|
59
|
+
"==", "!=", "<=", ">=", "->",
|
|
60
|
+
"::", "+=", "-=", "*=", "/=", "%=", "++", "--",
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
# Single-character operator symbols
|
|
64
|
+
SINGLE_CHAR_OPERATORS: str = "+-*/%<>=!"
|
|
65
|
+
|
|
66
|
+
# Punctuation characters
|
|
67
|
+
PUNCTUATION_CHARS: str = ".,:;()[]{}"
|
|
68
|
+
|
|
69
|
+
# Escape character used inside string literals
|
|
70
|
+
ESCAPE_CHAR: str = "\\"
|
|
71
|
+
|
|
72
|
+
# Supported escape sequence decodings inside string literals
|
|
73
|
+
ESCAPE_MAPPINGS: dict[str, str] = {
|
|
74
|
+
'"': '"',
|
|
75
|
+
"\\": "\\",
|
|
76
|
+
"n": "\n",
|
|
77
|
+
"t": "\t",
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class Tokenizer:
|
|
82
|
+
"""
|
|
83
|
+
Converts source text into a list of typed tokens in a single left-to-right
|
|
84
|
+
scan. Comments are expected to have been removed beforehand.
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
def __init__(self, content: str) -> None:
|
|
88
|
+
"""
|
|
89
|
+
Initializes the Tokenizer instance.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
content (str): Comment-free source text to tokenize.
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
self.content: str = content
|
|
96
|
+
self.cursor: int = 0
|
|
97
|
+
self.token_start: int = 0
|
|
98
|
+
|
|
99
|
+
def tokenize(self) -> list[Token]:
|
|
100
|
+
"""
|
|
101
|
+
Tokenizes the whole content into a token list.
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
list[Token]: Tokens in source order, terminated by an EOF token.
|
|
105
|
+
|
|
106
|
+
Raises:
|
|
107
|
+
SyntaxError: On unterminated strings, invalid escapes, or
|
|
108
|
+
unexpected characters.
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
tokens: list[Token] = []
|
|
112
|
+
|
|
113
|
+
# Scan until the end of the content
|
|
114
|
+
while self.cursor < len(self.content):
|
|
115
|
+
|
|
116
|
+
# Skip insignificant whitespace between tokens
|
|
117
|
+
if self.content[self.cursor].isspace():
|
|
118
|
+
self.cursor += 1
|
|
119
|
+
continue
|
|
120
|
+
|
|
121
|
+
previous_cursor: int = self.cursor
|
|
122
|
+
|
|
123
|
+
tokens.append(self._scan_next_token())
|
|
124
|
+
|
|
125
|
+
# Guard against non-advancing scans (would loop forever)
|
|
126
|
+
if self.cursor <= previous_cursor:
|
|
127
|
+
raise RuntimeError(
|
|
128
|
+
f"Tokenizer failed to advance at index {previous_cursor}"
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
# Terminate with an EOF token for the parser convenience
|
|
132
|
+
tokens.append(Token(
|
|
133
|
+
token_type=TokenType.EOF,
|
|
134
|
+
value="",
|
|
135
|
+
position=len(self.content),
|
|
136
|
+
end_position=len(self.content)
|
|
137
|
+
))
|
|
138
|
+
|
|
139
|
+
return tokens
|
|
140
|
+
|
|
141
|
+
def _scan_next_token(self) -> Token:
|
|
142
|
+
"""
|
|
143
|
+
Scans a single token starting at the current cursor.
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
Token: The next token.
|
|
147
|
+
|
|
148
|
+
Raises:
|
|
149
|
+
SyntaxError: If no token can start at the cursor.
|
|
150
|
+
"""
|
|
151
|
+
|
|
152
|
+
char: str = self.content[self.cursor]
|
|
153
|
+
|
|
154
|
+
# Dispatch on the first character
|
|
155
|
+
if char == "\"":
|
|
156
|
+
return self._scan_string()
|
|
157
|
+
|
|
158
|
+
if char.isdigit():
|
|
159
|
+
return self._scan_number()
|
|
160
|
+
|
|
161
|
+
if char.isalpha() or char == "_":
|
|
162
|
+
return self._scan_identifier()
|
|
163
|
+
|
|
164
|
+
if self.content[self.cursor:self.cursor + 2] in MULTI_CHAR_OPERATORS:
|
|
165
|
+
return self._scan_multi_char_operator()
|
|
166
|
+
|
|
167
|
+
if char in SINGLE_CHAR_OPERATORS:
|
|
168
|
+
self.token_start = self.cursor
|
|
169
|
+
self.cursor += 1
|
|
170
|
+
|
|
171
|
+
return self._make_token(token_type=TokenType.OPERATOR, value=char)
|
|
172
|
+
|
|
173
|
+
if char in PUNCTUATION_CHARS:
|
|
174
|
+
self.token_start = self.cursor
|
|
175
|
+
self.cursor += 1
|
|
176
|
+
|
|
177
|
+
return self._make_token(token_type=TokenType.PUNCTUATION, value=char)
|
|
178
|
+
|
|
179
|
+
raise SyntaxError(
|
|
180
|
+
f"Unexpected character '{char}' at index {self.cursor}"
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
def _make_token(self, token_type: TokenType, value: Optional[str] = None) -> Token:
|
|
184
|
+
"""
|
|
185
|
+
Builds a token spanning the characters consumed so far.
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
token_type (TokenType): Category of the token.
|
|
189
|
+
value (Optional[str], optional): Decoded token value. Defaults to
|
|
190
|
+
None (sliced from the source text).
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
Token: The built token.
|
|
194
|
+
"""
|
|
195
|
+
|
|
196
|
+
# Default value is the raw source slice (used by numbers, identifiers...)
|
|
197
|
+
if value is None:
|
|
198
|
+
value = self.content[self.token_start:self.cursor]
|
|
199
|
+
|
|
200
|
+
token: Token = Token(
|
|
201
|
+
token_type=token_type,
|
|
202
|
+
value=value,
|
|
203
|
+
position=self.token_start,
|
|
204
|
+
end_position=self.cursor
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
return token
|
|
208
|
+
|
|
209
|
+
def _scan_string(self) -> Token:
|
|
210
|
+
"""
|
|
211
|
+
Scans a double quoted string literal with escape decoding.
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
Token: The STRING token with the decoded value.
|
|
215
|
+
|
|
216
|
+
Raises:
|
|
217
|
+
SyntaxError: On unterminated strings or invalid escape sequences.
|
|
218
|
+
"""
|
|
219
|
+
|
|
220
|
+
self.token_start = self.cursor
|
|
221
|
+
self.cursor += 1
|
|
222
|
+
|
|
223
|
+
decoded: list[str] = []
|
|
224
|
+
|
|
225
|
+
# Consume characters until the closing quote
|
|
226
|
+
while self.cursor < len(self.content) and self.content[self.cursor] != "\"":
|
|
227
|
+
|
|
228
|
+
# Handle escape sequences
|
|
229
|
+
if self.content[self.cursor] == ESCAPE_CHAR:
|
|
230
|
+
decoded.append(self._consume_escape())
|
|
231
|
+
continue
|
|
232
|
+
|
|
233
|
+
# Raw newline inside a string is a syntax error
|
|
234
|
+
if self.content[self.cursor] == "\n":
|
|
235
|
+
raise SyntaxError(
|
|
236
|
+
f"Unterminated string literal starting at index {self.token_start}"
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
decoded.append(self.content[self.cursor])
|
|
240
|
+
self.cursor += 1
|
|
241
|
+
|
|
242
|
+
# Check the string is terminated
|
|
243
|
+
if self.cursor >= len(self.content):
|
|
244
|
+
raise SyntaxError(
|
|
245
|
+
f"Unterminated string literal starting at index {self.token_start}"
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
# Consume the closing quote
|
|
249
|
+
self.cursor += 1
|
|
250
|
+
|
|
251
|
+
return self._make_token(token_type=TokenType.STRING, value="".join(decoded))
|
|
252
|
+
|
|
253
|
+
def _consume_escape(self) -> str:
|
|
254
|
+
"""
|
|
255
|
+
Consumes a backslash escape sequence at the cursor.
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
str: The decoded character.
|
|
259
|
+
|
|
260
|
+
Raises:
|
|
261
|
+
SyntaxError: If the escape sequence is invalid.
|
|
262
|
+
"""
|
|
263
|
+
|
|
264
|
+
# Check the escaped character exists
|
|
265
|
+
if self.cursor + 1 >= len(self.content):
|
|
266
|
+
raise SyntaxError(
|
|
267
|
+
f"Unterminated escape sequence at index {self.cursor}"
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
escaped_char: str = self.content[self.cursor + 1]
|
|
271
|
+
|
|
272
|
+
# Check the escape is supported
|
|
273
|
+
if escaped_char not in ESCAPE_MAPPINGS:
|
|
274
|
+
raise SyntaxError(
|
|
275
|
+
f"Invalid escape sequence '\\{escaped_char}' at index {self.cursor}"
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
self.cursor += 2
|
|
279
|
+
|
|
280
|
+
return ESCAPE_MAPPINGS[escaped_char]
|
|
281
|
+
|
|
282
|
+
def _scan_number(self) -> Token:
|
|
283
|
+
"""
|
|
284
|
+
Scans an integer or float numeric literal.
|
|
285
|
+
|
|
286
|
+
Floats include fractional parts ('1.5') and scientific notation
|
|
287
|
+
('1e6', '3.5e-2', '1.5E+3'). The exponent is only consumed when a
|
|
288
|
+
complete 'e[+|-]<digits>' sequence follows, so '1.5e' keeps the
|
|
289
|
+
float and leaves 'e' for later postfix handling.
|
|
290
|
+
|
|
291
|
+
Returns:
|
|
292
|
+
Token: The INT or FLOAT token.
|
|
293
|
+
|
|
294
|
+
Raises:
|
|
295
|
+
SyntaxError: On malformed fractional parts.
|
|
296
|
+
"""
|
|
297
|
+
|
|
298
|
+
self.token_start = self.cursor
|
|
299
|
+
|
|
300
|
+
# Consume integer digits
|
|
301
|
+
while self.cursor < len(self.content) and self.content[self.cursor].isdigit():
|
|
302
|
+
self.cursor += 1
|
|
303
|
+
|
|
304
|
+
# Check for a fractional part (a '.' followed by a digit)
|
|
305
|
+
has_fraction: bool = (
|
|
306
|
+
self.cursor + 1 < len(self.content)
|
|
307
|
+
and self.content[self.cursor] == "."
|
|
308
|
+
and self.content[self.cursor + 1].isdigit()
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
if has_fraction:
|
|
312
|
+
self.cursor += 1
|
|
313
|
+
|
|
314
|
+
# Consume fractional digits
|
|
315
|
+
while self.cursor < len(self.content) and self.content[self.cursor].isdigit():
|
|
316
|
+
self.cursor += 1
|
|
317
|
+
|
|
318
|
+
# Check for a complete exponent ('e'/'E', optional sign, digits)
|
|
319
|
+
has_exponent: bool = self._scan_exponent()
|
|
320
|
+
|
|
321
|
+
if has_fraction or has_exponent:
|
|
322
|
+
return self._make_token(token_type=TokenType.FLOAT)
|
|
323
|
+
|
|
324
|
+
# A '.' not followed by a digit belongs to a later postfix token
|
|
325
|
+
# (e.g. member access on a float literal like '1.5.method()')
|
|
326
|
+
return self._make_token(token_type=TokenType.INT)
|
|
327
|
+
|
|
328
|
+
def _scan_exponent(self) -> bool:
|
|
329
|
+
"""
|
|
330
|
+
Scans an optional scientific-notation exponent at the cursor.
|
|
331
|
+
|
|
332
|
+
Consumes 'e'/'E', an optional sign, and at least one digit only
|
|
333
|
+
when the full sequence is present; otherwise leaves the cursor
|
|
334
|
+
untouched.
|
|
335
|
+
|
|
336
|
+
Returns:
|
|
337
|
+
bool: True if a complete exponent was consumed.
|
|
338
|
+
"""
|
|
339
|
+
|
|
340
|
+
cursor: int = self.cursor
|
|
341
|
+
|
|
342
|
+
# Exponent marker
|
|
343
|
+
if cursor >= len(self.content) or self.content[cursor] not in "eE":
|
|
344
|
+
return False
|
|
345
|
+
|
|
346
|
+
cursor += 1
|
|
347
|
+
|
|
348
|
+
# Optional sign
|
|
349
|
+
if cursor < len(self.content) and self.content[cursor] in "+-":
|
|
350
|
+
cursor += 1
|
|
351
|
+
|
|
352
|
+
# At least one digit is required for a valid exponent
|
|
353
|
+
if cursor >= len(self.content) or not self.content[cursor].isdigit():
|
|
354
|
+
return False
|
|
355
|
+
|
|
356
|
+
while cursor < len(self.content) and self.content[cursor].isdigit():
|
|
357
|
+
cursor += 1
|
|
358
|
+
|
|
359
|
+
self.cursor = cursor
|
|
360
|
+
|
|
361
|
+
return True
|
|
362
|
+
|
|
363
|
+
def _scan_identifier(self) -> Token:
|
|
364
|
+
"""
|
|
365
|
+
Scans an identifier or keyword.
|
|
366
|
+
|
|
367
|
+
Returns:
|
|
368
|
+
Token: The IDENTIFIER or KEYWORD token.
|
|
369
|
+
"""
|
|
370
|
+
|
|
371
|
+
self.token_start = self.cursor
|
|
372
|
+
|
|
373
|
+
# Consume identifier characters
|
|
374
|
+
while (
|
|
375
|
+
self.cursor < len(self.content)
|
|
376
|
+
and (self.content[self.cursor].isalnum() or self.content[self.cursor] == "_")
|
|
377
|
+
):
|
|
378
|
+
self.cursor += 1
|
|
379
|
+
|
|
380
|
+
value: str = self.content[self.token_start:self.cursor]
|
|
381
|
+
|
|
382
|
+
# Distinguish keywords from plain identifiers
|
|
383
|
+
token_type: TokenType = (
|
|
384
|
+
TokenType.KEYWORD if value in KEYWORDS else TokenType.IDENTIFIER
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
return self._make_token(token_type=token_type, value=value)
|
|
388
|
+
|
|
389
|
+
def _scan_multi_char_operator(self) -> Token:
|
|
390
|
+
"""
|
|
391
|
+
Scans a two-character operator at the cursor.
|
|
392
|
+
|
|
393
|
+
Returns:
|
|
394
|
+
Token: The OPERATOR token.
|
|
395
|
+
"""
|
|
396
|
+
|
|
397
|
+
self.token_start = self.cursor
|
|
398
|
+
operator: str = self.content[self.cursor:self.cursor + 2]
|
|
399
|
+
|
|
400
|
+
self.cursor += 2
|
|
401
|
+
|
|
402
|
+
return self._make_token(token_type=TokenType.OPERATOR, value=operator)
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Type annotation parsing for Nadata.
|
|
3
|
+
|
|
4
|
+
Provides the TmpType intermediate representation (a type name with nested
|
|
5
|
+
children) and the TypeParser converting a raw annotation string into it.
|
|
6
|
+
Shared by the .nastruct and .nadata parsers so types are parsed once into
|
|
7
|
+
structural data instead of being carried as raw strings.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
# Import Modules
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from nadata.parser.expressions.tokenizer import Token, TokenType, Tokenizer
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class TmpType:
|
|
20
|
+
"""
|
|
21
|
+
Intermediate representation of a parsed type annotation.
|
|
22
|
+
|
|
23
|
+
A simple type (e.g. 'Str') has no children; a generic type (e.g.
|
|
24
|
+
'Array[Entity]') has its name plus one child per type argument.
|
|
25
|
+
|
|
26
|
+
Attributes:
|
|
27
|
+
name (str): Type constructor name (e.g. 'Str', 'Array', 'Dict').
|
|
28
|
+
children (list[TmpType]): Type arguments for generic types.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
name: str
|
|
32
|
+
children: list[TmpType] = field(default_factory=list)
|
|
33
|
+
|
|
34
|
+
def to_dict(self) -> dict[str, Any]:
|
|
35
|
+
"""
|
|
36
|
+
Converts the type to a plain JSON serializable dict.
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
dict[str, Any]: Dict with 'name' and recursive 'children'.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
"name": self.name,
|
|
44
|
+
"children": [child.to_dict() for child in self.children],
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
def collect_names(self) -> set[str]:
|
|
48
|
+
"""
|
|
49
|
+
Collects every type constructor name of the tree.
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
set[str]: This type name plus all nested children names.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
names: set[str] = {self.name}
|
|
56
|
+
|
|
57
|
+
for child in self.children:
|
|
58
|
+
names |= child.collect_names()
|
|
59
|
+
|
|
60
|
+
return names
|
|
61
|
+
|
|
62
|
+
def __repr__(self) -> str:
|
|
63
|
+
"""
|
|
64
|
+
Returns string representation of the TmpType.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
str: Annotation style representation (e.g. 'Array[Entity]').
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
if not self.children:
|
|
71
|
+
return self.name
|
|
72
|
+
|
|
73
|
+
children_text: str = ", ".join(repr(child) for child in self.children)
|
|
74
|
+
|
|
75
|
+
return f"{self.name}[{children_text}]"
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class TypeParser:
|
|
79
|
+
"""
|
|
80
|
+
Parses a token stream into a TmpType tree.
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
def __init__(self, tokens: list[Token]) -> None:
|
|
84
|
+
"""
|
|
85
|
+
Initializes the TypeParser instance.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
tokens (list[Token]): Token stream to parse (terminated by EOF).
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
self.tokens: list[Token] = tokens
|
|
92
|
+
self.cursor: int = 0
|
|
93
|
+
|
|
94
|
+
@classmethod
|
|
95
|
+
def parse_text(cls, text: str) -> TmpType:
|
|
96
|
+
"""
|
|
97
|
+
Tokenizes and parses a raw type annotation in one step.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
text (str): Raw type annotation (e.g. 'Array[Entity]').
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
TmpType: Parsed type tree.
|
|
104
|
+
|
|
105
|
+
Raises:
|
|
106
|
+
SyntaxError: On malformed or empty annotations.
|
|
107
|
+
"""
|
|
108
|
+
|
|
109
|
+
tokens: list[Token] = Tokenizer(content=text).tokenize()
|
|
110
|
+
|
|
111
|
+
parsed_type: TmpType
|
|
112
|
+
cursor: int
|
|
113
|
+
parsed_type, cursor = cls(tokens=tokens).parse_type_at(cursor=0)
|
|
114
|
+
|
|
115
|
+
# Check nothing remains after the type
|
|
116
|
+
if tokens[cursor].token_type != TokenType.EOF:
|
|
117
|
+
raise SyntaxError(
|
|
118
|
+
f"Unexpected token '{tokens[cursor].value}' in type '{text}'"
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
return parsed_type
|
|
122
|
+
|
|
123
|
+
def parse_type_at(self, cursor: int) -> tuple[TmpType, int]:
|
|
124
|
+
"""
|
|
125
|
+
Parses a single type starting at the given token cursor.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
cursor (int): Token index to start parsing from.
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
tuple[TmpType, int]: The parsed type and the new cursor.
|
|
132
|
+
|
|
133
|
+
Raises:
|
|
134
|
+
SyntaxError: On malformed type annotations.
|
|
135
|
+
"""
|
|
136
|
+
|
|
137
|
+
token: Token = self.tokens[cursor]
|
|
138
|
+
|
|
139
|
+
# A dimension literal (e.g. the 3 in 'Tensor[Float32, 3, 4]') is a
|
|
140
|
+
# valid type argument and is carried as its digit-string name
|
|
141
|
+
if token.token_type == TokenType.INT:
|
|
142
|
+
cursor += 1
|
|
143
|
+
|
|
144
|
+
return TmpType(name=token.value), cursor
|
|
145
|
+
|
|
146
|
+
# A type starts with its constructor name
|
|
147
|
+
if token.token_type != TokenType.IDENTIFIER:
|
|
148
|
+
raise SyntaxError(
|
|
149
|
+
f"Expected a type name but found '{token.value}' "
|
|
150
|
+
f"at index {token.position}"
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
name: str = token.value
|
|
154
|
+
cursor += 1
|
|
155
|
+
|
|
156
|
+
children: list[TmpType] = []
|
|
157
|
+
|
|
158
|
+
# Parse the optional generic argument list '[...]'
|
|
159
|
+
if (
|
|
160
|
+
self.tokens[cursor].token_type == TokenType.PUNCTUATION
|
|
161
|
+
and self.tokens[cursor].value == "["
|
|
162
|
+
):
|
|
163
|
+
cursor += 1
|
|
164
|
+
|
|
165
|
+
# Parse comma separated type arguments
|
|
166
|
+
while True:
|
|
167
|
+
child: TmpType
|
|
168
|
+
child, cursor = self.parse_type_at(cursor=cursor)
|
|
169
|
+
children.append(child)
|
|
170
|
+
|
|
171
|
+
current: Token = self.tokens[cursor]
|
|
172
|
+
|
|
173
|
+
# Continue on comma, close on bracket
|
|
174
|
+
if current.token_type == TokenType.PUNCTUATION and current.value == ",":
|
|
175
|
+
cursor += 1
|
|
176
|
+
continue
|
|
177
|
+
|
|
178
|
+
if current.token_type == TokenType.PUNCTUATION and current.value == "]":
|
|
179
|
+
cursor += 1
|
|
180
|
+
break
|
|
181
|
+
|
|
182
|
+
raise SyntaxError(
|
|
183
|
+
f"Expected ',' or ']' in type arguments of '{name}' "
|
|
184
|
+
f"but found '{current.value}' at index {current.position}"
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
return TmpType(name=name, children=children), cursor
|