infinity-data 1.0.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.
- infinity_data/__init__.py +41 -0
- infinity_data/emit/__init__.py +9 -0
- infinity_data/emit/converter.py +55 -0
- infinity_data/frontend.py +37 -0
- infinity_data/infra/__init__.py +4 -0
- infinity_data/infra/diagnostics.py +212 -0
- infinity_data/infra/file.py +83 -0
- infinity_data/infra/ll1_stream.py +71 -0
- infinity_data/infra/location.py +80 -0
- infinity_data/infra/path.py +46 -0
- infinity_data/parser/__init__.py +79 -0
- infinity_data/parser/diagnostics.py +92 -0
- infinity_data/parser/models.py +295 -0
- infinity_data/parser/parser.py +988 -0
- infinity_data/parser/token_stream.py +128 -0
- infinity_data/pipeline.py +252 -0
- infinity_data/sandbox/__init__.py +32 -0
- infinity_data/sandbox/config.py +61 -0
- infinity_data/sandbox/errors.py +106 -0
- infinity_data/sandbox/mediator.py +214 -0
- infinity_data/sandbox/schema.py +21 -0
- infinity_data/semantic/__init__.py +59 -0
- infinity_data/semantic/builder/__init__.py +29 -0
- infinity_data/semantic/builder/builder.py +523 -0
- infinity_data/semantic/builder/models.py +163 -0
- infinity_data/semantic/constraints.py +163 -0
- infinity_data/semantic/diagnostics.py +172 -0
- infinity_data/semantic/executor/__init__.py +10 -0
- infinity_data/semantic/executor/executor.py +259 -0
- infinity_data/semantic/registry/__init__.py +168 -0
- infinity_data/semantic/registry/_core.py +212 -0
- infinity_data/semantic/registry/dict_constraints.py +56 -0
- infinity_data/semantic/registry/general.py +362 -0
- infinity_data/semantic/registry/logic.py +134 -0
- infinity_data/semantic/registry/types.py +136 -0
- infinity_data/semantic/resolver/__init__.py +20 -0
- infinity_data/semantic/resolver/imports.py +191 -0
- infinity_data/semantic/resolver/models.py +67 -0
- infinity_data/semantic/resolver/resolver.py +345 -0
- infinity_data/tokenizer/__init__.py +16 -0
- infinity_data/tokenizer/char_stream.py +72 -0
- infinity_data/tokenizer/diagnostics.py +62 -0
- infinity_data/tokenizer/finalizer.py +271 -0
- infinity_data/tokenizer/models/__init__.py +17 -0
- infinity_data/tokenizer/models/raw_tokens.py +61 -0
- infinity_data/tokenizer/models/tokens.py +245 -0
- infinity_data/tokenizer/tokenizer.py +531 -0
- infinity_data-1.0.0.dist-info/METADATA +60 -0
- infinity_data-1.0.0.dist-info/RECORD +52 -0
- infinity_data-1.0.0.dist-info/WHEEL +5 -0
- infinity_data-1.0.0.dist-info/licenses/LICENSE +21 -0
- infinity_data-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,531 @@
|
|
|
1
|
+
"""词法分析器"""
|
|
2
|
+
|
|
3
|
+
from infinity_data.infra.diagnostics import Diagnostic, DiagnosticCollector, Severity
|
|
4
|
+
from infinity_data.infra.file import File
|
|
5
|
+
from infinity_data.infra.ll1_stream import NoNextType
|
|
6
|
+
from infinity_data.tokenizer.char_stream import CharStream
|
|
7
|
+
from infinity_data.tokenizer.diagnostics import diag
|
|
8
|
+
from infinity_data.tokenizer.models.raw_tokens import (
|
|
9
|
+
RawToken,
|
|
10
|
+
RawTokenType,
|
|
11
|
+
SourceInfo,
|
|
12
|
+
SourceRange,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class RawTokenizer:
|
|
17
|
+
"""将 infd/inft 源码转为 RawToken 流。"""
|
|
18
|
+
|
|
19
|
+
_single_char_map: dict[str, RawTokenType] = {
|
|
20
|
+
'{': RawTokenType.LBRACE,
|
|
21
|
+
'}': RawTokenType.RBRACE,
|
|
22
|
+
'[': RawTokenType.LBRACKET,
|
|
23
|
+
']': RawTokenType.RBRACKET,
|
|
24
|
+
'(': RawTokenType.LPAREN,
|
|
25
|
+
')': RawTokenType.RPAREN,
|
|
26
|
+
'<': RawTokenType.LANGLE,
|
|
27
|
+
'>': RawTokenType.RANGLE,
|
|
28
|
+
'=': RawTokenType.EQUALS,
|
|
29
|
+
':': RawTokenType.COLON,
|
|
30
|
+
',': RawTokenType.COMMA,
|
|
31
|
+
'~': RawTokenType.TILDE,
|
|
32
|
+
'?': RawTokenType.QUESTION,
|
|
33
|
+
'$': RawTokenType.DOLLAR,
|
|
34
|
+
'.': RawTokenType.DOT,
|
|
35
|
+
'\n': RawTokenType.NEWLINE,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
_keywords_map: dict[str, RawTokenType] = {
|
|
39
|
+
'null': RawTokenType.NULL,
|
|
40
|
+
'noexist': RawTokenType.NOEXIST,
|
|
41
|
+
'true': RawTokenType.BOOL,
|
|
42
|
+
'false': RawTokenType.BOOL,
|
|
43
|
+
'nan': RawTokenType.FLOAT,
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
_open_brackets: frozenset[str] = frozenset({'{', '[', '(', '<'})
|
|
47
|
+
_close_brackets: frozenset[str] = frozenset({'}', ']', ')', '>'})
|
|
48
|
+
|
|
49
|
+
def __init__(
|
|
50
|
+
self,
|
|
51
|
+
file: File,
|
|
52
|
+
error_collector: DiagnosticCollector | None = None,
|
|
53
|
+
) -> None:
|
|
54
|
+
self._file: File = file
|
|
55
|
+
self._stream: CharStream = CharStream(file.chars())
|
|
56
|
+
# 注意:不能用 `or` —— 空 DiagnosticCollector 的 __bool__ 为 False,会静默丢弃传入的收集器
|
|
57
|
+
self._errors = error_collector if error_collector is not None else DiagnosticCollector()
|
|
58
|
+
self._eof_sent: bool = False
|
|
59
|
+
self._open_stack: list[tuple[str, SourceInfo]] = []
|
|
60
|
+
self._detect_bom()
|
|
61
|
+
|
|
62
|
+
def _detect_bom(self) -> None:
|
|
63
|
+
"""检测文件 BOM(规范要求 UTF-8 NO BOM):报 tokenize.bom 警告并跳过。"""
|
|
64
|
+
if not self._stream.eof() and self._stream.peek() == '\ufeff':
|
|
65
|
+
self._errors.add(
|
|
66
|
+
Diagnostic(
|
|
67
|
+
Severity.WARNING,
|
|
68
|
+
'tokenize.bom',
|
|
69
|
+
{},
|
|
70
|
+
SourceRange.at(self._file, self._stream.info()),
|
|
71
|
+
)
|
|
72
|
+
)
|
|
73
|
+
self._stream.advance()
|
|
74
|
+
|
|
75
|
+
def __iter__(self) -> 'RawTokenizer':
|
|
76
|
+
return self
|
|
77
|
+
|
|
78
|
+
def __next__(self) -> RawToken:
|
|
79
|
+
if self._eof_sent:
|
|
80
|
+
raise StopIteration
|
|
81
|
+
tok: RawToken = self.next()
|
|
82
|
+
if tok.type is RawTokenType.EOF:
|
|
83
|
+
self._eof_sent = True
|
|
84
|
+
return tok
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def error_collector(self) -> DiagnosticCollector:
|
|
88
|
+
return self._errors
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def file(self) -> File:
|
|
92
|
+
return self._file
|
|
93
|
+
|
|
94
|
+
def _current_source_info(self) -> SourceInfo:
|
|
95
|
+
return self._stream.info()
|
|
96
|
+
|
|
97
|
+
def next(self) -> RawToken:
|
|
98
|
+
"""返回下一个 token。"""
|
|
99
|
+
while True:
|
|
100
|
+
self._skip_whitespace_and_comments()
|
|
101
|
+
|
|
102
|
+
if self._stream.eof():
|
|
103
|
+
self._report_unclosed_brackets()
|
|
104
|
+
return self._make_token(RawTokenType.EOF, '', self._current_source_info())
|
|
105
|
+
|
|
106
|
+
ch = self._stream.peek()
|
|
107
|
+
assert not isinstance(ch, NoNextType)
|
|
108
|
+
|
|
109
|
+
if ch in self._single_char_map:
|
|
110
|
+
tok = self._single_char(self._single_char_map[ch])
|
|
111
|
+
self._track_bracket(ch, tok)
|
|
112
|
+
return tok
|
|
113
|
+
|
|
114
|
+
if ch == '!':
|
|
115
|
+
tok = self._read_bang()
|
|
116
|
+
if tok is not None:
|
|
117
|
+
return tok
|
|
118
|
+
continue # 词法错误已报,跳过该 ! 序列
|
|
119
|
+
|
|
120
|
+
if ch == '"':
|
|
121
|
+
return self._read_string()
|
|
122
|
+
|
|
123
|
+
if ch == '`':
|
|
124
|
+
return self._read_multiline_string()
|
|
125
|
+
|
|
126
|
+
if ch.isdigit() or ch in ['+', '-']:
|
|
127
|
+
return self._read_number_fallback()
|
|
128
|
+
|
|
129
|
+
# ── 标识符 / 关键字 ───────────────────────
|
|
130
|
+
if ch.isalpha() or ch == '_':
|
|
131
|
+
return self._read_identifier_or_keyword()
|
|
132
|
+
|
|
133
|
+
# ── 无法识别的字符 ────────────────────────
|
|
134
|
+
self._errors.add(
|
|
135
|
+
diag('tokenize.unknown_char', {'char': ch}, SourceRange.at(self._file, self._current_source_info()))
|
|
136
|
+
)
|
|
137
|
+
self._stream.advance()
|
|
138
|
+
|
|
139
|
+
def _make_token(
|
|
140
|
+
self,
|
|
141
|
+
token_type: RawTokenType,
|
|
142
|
+
raw: str,
|
|
143
|
+
start: SourceInfo,
|
|
144
|
+
) -> RawToken:
|
|
145
|
+
return RawToken(
|
|
146
|
+
type=token_type,
|
|
147
|
+
raw=raw,
|
|
148
|
+
source=SourceRange(
|
|
149
|
+
file=self._file,
|
|
150
|
+
start=start,
|
|
151
|
+
end=self._current_source_info(),
|
|
152
|
+
),
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
# ── 空白与注释跳过 ────────────────────────────────
|
|
156
|
+
|
|
157
|
+
def _skip_whitespace_and_comments(self) -> None:
|
|
158
|
+
"""跳过空白及注释(单行 # 和多行 #+...#-)。"""
|
|
159
|
+
while not self._stream.eof():
|
|
160
|
+
ch = self._stream.peek()
|
|
161
|
+
assert not isinstance(ch, NoNextType)
|
|
162
|
+
|
|
163
|
+
# 跳过除换行外的空白
|
|
164
|
+
if ch != '\n' and ch.isspace():
|
|
165
|
+
self._stream.advance()
|
|
166
|
+
continue
|
|
167
|
+
|
|
168
|
+
# 单行注释: # 到行尾
|
|
169
|
+
if ch == '#':
|
|
170
|
+
self._handle_comment()
|
|
171
|
+
continue
|
|
172
|
+
|
|
173
|
+
break
|
|
174
|
+
|
|
175
|
+
def _handle_comment(self) -> None:
|
|
176
|
+
"""处理注释:单行 # 或多行 #+...#-"""
|
|
177
|
+
self._stream.advance() # 消费 '#'
|
|
178
|
+
|
|
179
|
+
if self._stream.eof():
|
|
180
|
+
return
|
|
181
|
+
|
|
182
|
+
ch = self._stream.peek()
|
|
183
|
+
assert not isinstance(ch, NoNextType)
|
|
184
|
+
|
|
185
|
+
# 检查是否为多行注释起始标记 #+
|
|
186
|
+
plus_count = 0
|
|
187
|
+
while ch == '+':
|
|
188
|
+
plus_count += 1
|
|
189
|
+
self._stream.advance()
|
|
190
|
+
if self._stream.eof():
|
|
191
|
+
self._errors.add(
|
|
192
|
+
diag(
|
|
193
|
+
'tokenize.unterminated_comment',
|
|
194
|
+
{'flag': '#' + '-' * plus_count},
|
|
195
|
+
SourceRange.at(self._file, self._current_source_info()),
|
|
196
|
+
)
|
|
197
|
+
)
|
|
198
|
+
return
|
|
199
|
+
ch = self._stream.peek()
|
|
200
|
+
assert not isinstance(ch, NoNextType)
|
|
201
|
+
|
|
202
|
+
if plus_count > 0:
|
|
203
|
+
# 多行注释模式: 需要找到匹配的 # + '-' * plus_count
|
|
204
|
+
self._skip_multiline_comment(plus_count)
|
|
205
|
+
else:
|
|
206
|
+
# 单行注释: 跳到行尾
|
|
207
|
+
while not self._stream.eof() and self._stream.peek() != '\n':
|
|
208
|
+
self._stream.advance()
|
|
209
|
+
|
|
210
|
+
def _skip_multiline_comment(self, depth: int) -> None:
|
|
211
|
+
"""跳过多行注释直到找到匹配的结束标记 # + '-' * depth。"""
|
|
212
|
+
while not self._stream.eof():
|
|
213
|
+
ch = self._stream.peek()
|
|
214
|
+
assert not isinstance(ch, NoNextType)
|
|
215
|
+
|
|
216
|
+
if ch == '#':
|
|
217
|
+
self._stream.advance()
|
|
218
|
+
if self._stream.eof():
|
|
219
|
+
break
|
|
220
|
+
# 检查后续字符是否全是 '-'
|
|
221
|
+
minus_count = 0
|
|
222
|
+
while not self._stream.eof() and self._stream.peek() == '-':
|
|
223
|
+
minus_count += 1
|
|
224
|
+
self._stream.advance()
|
|
225
|
+
if minus_count == depth:
|
|
226
|
+
return
|
|
227
|
+
continue
|
|
228
|
+
|
|
229
|
+
self._stream.advance()
|
|
230
|
+
|
|
231
|
+
self._errors.add(
|
|
232
|
+
diag(
|
|
233
|
+
'tokenize.unterminated_comment',
|
|
234
|
+
{'flag': '#' + '-' * depth},
|
|
235
|
+
SourceRange.at(self._file, self._current_source_info()),
|
|
236
|
+
)
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
# ── 单字符 token ──────────────────────────────────
|
|
240
|
+
def _single_char(self, token_type: RawTokenType) -> RawToken:
|
|
241
|
+
ch = self._stream.peek()
|
|
242
|
+
assert not isinstance(ch, NoNextType)
|
|
243
|
+
start = self._current_source_info()
|
|
244
|
+
self._stream.advance()
|
|
245
|
+
return self._make_token(token_type, ch, start=start)
|
|
246
|
+
|
|
247
|
+
# ── 括号栈(EOF 时报告未闭合)────────────────────
|
|
248
|
+
def _track_bracket(self, ch: str, tok: RawToken) -> None:
|
|
249
|
+
"""维护括号栈:开括号压栈、闭括号弹栈,供 EOF 时报告未闭合括号。"""
|
|
250
|
+
if ch in self._open_brackets:
|
|
251
|
+
self._open_stack.append((ch, tok.source.start))
|
|
252
|
+
elif ch in self._close_brackets:
|
|
253
|
+
if self._open_stack:
|
|
254
|
+
self._open_stack.pop()
|
|
255
|
+
|
|
256
|
+
def _report_unclosed_brackets(self) -> None:
|
|
257
|
+
"""EOF 时仍开着的括号 → 报 tokenize.unterminated_bracket(按开括号顺序)。"""
|
|
258
|
+
for bracket, start in self._open_stack:
|
|
259
|
+
self._errors.add(
|
|
260
|
+
diag('tokenize.unterminated_bracket', {'bracket': bracket}, SourceRange.at(self._file, start))
|
|
261
|
+
)
|
|
262
|
+
self._open_stack.clear()
|
|
263
|
+
|
|
264
|
+
# ── ! 导入关键字(词法组合,! 无独立语法)──
|
|
265
|
+
|
|
266
|
+
def _read_bang(self) -> RawToken | None:
|
|
267
|
+
"""读取 ``!`` 起始的 token。
|
|
268
|
+
|
|
269
|
+
- ``!env`` / ``!file`` / ``!from`` → 组合导入关键字 token
|
|
270
|
+
- 其他任何情况 → 词法错误(语言不允许单独 ``!``),返回 None 跳过
|
|
271
|
+
"""
|
|
272
|
+
start = self._current_source_info()
|
|
273
|
+
self._stream.advance() # 消费 '!'
|
|
274
|
+
|
|
275
|
+
if self._stream.eof():
|
|
276
|
+
self._errors.add(diag('tokenize.invalid_bang', {'actual': 'EOF'}, SourceRange.at(self._file, start)))
|
|
277
|
+
return None
|
|
278
|
+
ch = self._stream.peek()
|
|
279
|
+
assert not isinstance(ch, NoNextType)
|
|
280
|
+
if not (ch.isalpha() or ch == '_'):
|
|
281
|
+
self._errors.add(diag('tokenize.invalid_bang', {'actual': repr(ch)}, SourceRange.at(self._file, start)))
|
|
282
|
+
return None
|
|
283
|
+
|
|
284
|
+
ident_tok = self._read_identifier_or_keyword()
|
|
285
|
+
match ident_tok.raw:
|
|
286
|
+
case 'env':
|
|
287
|
+
return self._make_token(RawTokenType.ENV_IMPORT, '!env', start=start)
|
|
288
|
+
case 'file':
|
|
289
|
+
return self._make_token(RawTokenType.FILE_IMPORT, '!file', start=start)
|
|
290
|
+
case 'from':
|
|
291
|
+
return self._make_token(RawTokenType.FROM_IMPORT, '!from', start=start)
|
|
292
|
+
case _:
|
|
293
|
+
self._errors.add(
|
|
294
|
+
diag('tokenize.invalid_bang', {'actual': repr(ident_tok.raw)}, SourceRange.at(self._file, start))
|
|
295
|
+
)
|
|
296
|
+
return None
|
|
297
|
+
|
|
298
|
+
# ── 单行字符串 ────────────────────────────────────
|
|
299
|
+
|
|
300
|
+
def _read_string(self) -> RawToken:
|
|
301
|
+
"""读取双引号包裹的单行字符串"""
|
|
302
|
+
start = self._current_source_info()
|
|
303
|
+
raw_parts: list[str] = [self._stream.advance()] # 消费 '"'
|
|
304
|
+
|
|
305
|
+
while not self._stream.eof():
|
|
306
|
+
ch = self._stream.peek()
|
|
307
|
+
assert not isinstance(ch, NoNextType)
|
|
308
|
+
|
|
309
|
+
if ch == '\\':
|
|
310
|
+
raw_parts.append(self._stream.advance())
|
|
311
|
+
if self._stream.eof():
|
|
312
|
+
self._errors.add(
|
|
313
|
+
diag('tokenize.unterminated_string', {'str_type': '字符串'}, SourceRange.at(self._file, start))
|
|
314
|
+
)
|
|
315
|
+
return self._make_token(RawTokenType.STRING, ''.join(raw_parts), start=start)
|
|
316
|
+
raw_parts.append(self._stream.advance())
|
|
317
|
+
continue
|
|
318
|
+
|
|
319
|
+
if ch == '"':
|
|
320
|
+
raw_parts.append(ch)
|
|
321
|
+
self._stream.advance()
|
|
322
|
+
return self._make_token(RawTokenType.STRING, ''.join(raw_parts), start=start)
|
|
323
|
+
|
|
324
|
+
if ch == '\n':
|
|
325
|
+
self._errors.add(
|
|
326
|
+
diag('tokenize.unterminated_string', {'str_type': '字符串'}, SourceRange.at(self._file, start))
|
|
327
|
+
)
|
|
328
|
+
return self._make_token(RawTokenType.STRING, ''.join(raw_parts), start=start)
|
|
329
|
+
|
|
330
|
+
raw_parts.append(ch)
|
|
331
|
+
self._stream.advance()
|
|
332
|
+
|
|
333
|
+
self._errors.add(
|
|
334
|
+
diag('tokenize.unterminated_string', {'str_type': '字符串'}, SourceRange.at(self._file, start))
|
|
335
|
+
)
|
|
336
|
+
raw_parts.append('"') # 补上缺失的结束引号
|
|
337
|
+
return self._make_token(RawTokenType.STRING, ''.join(raw_parts), start=start)
|
|
338
|
+
|
|
339
|
+
# ── 多行字符串(Markdown 风格) ────────────────────
|
|
340
|
+
|
|
341
|
+
def _read_multiline_string(self) -> RawToken:
|
|
342
|
+
"""读取反引号包裹的多行字符串。
|
|
343
|
+
|
|
344
|
+
语法: `...`
|
|
345
|
+
- 起始 ` 可变长(>= 1 个反引号)
|
|
346
|
+
"""
|
|
347
|
+
start = self._current_source_info()
|
|
348
|
+
|
|
349
|
+
# 统计起始反引号数量
|
|
350
|
+
backtick_count = 0
|
|
351
|
+
while not self._stream.eof():
|
|
352
|
+
ch = self._stream.peek()
|
|
353
|
+
if ch == '`':
|
|
354
|
+
backtick_count += 1
|
|
355
|
+
self._stream.advance()
|
|
356
|
+
else:
|
|
357
|
+
break
|
|
358
|
+
|
|
359
|
+
# 读取内容直到匹配的结束反引号
|
|
360
|
+
raw = '`' * backtick_count
|
|
361
|
+
while not self._stream.eof():
|
|
362
|
+
ch = self._stream.peek()
|
|
363
|
+
assert not isinstance(ch, NoNextType)
|
|
364
|
+
|
|
365
|
+
if ch == '`':
|
|
366
|
+
# 检查是否有足够的反引号匹配
|
|
367
|
+
temp_count = 0
|
|
368
|
+
while temp_count < backtick_count and not self._stream.eof() and self._stream.peek() == '`':
|
|
369
|
+
temp_count += 1
|
|
370
|
+
self._stream.advance()
|
|
371
|
+
if temp_count == backtick_count:
|
|
372
|
+
raw += '`' * temp_count
|
|
373
|
+
return self._make_token(RawTokenType.MULTILINE_STRING, raw, start=start)
|
|
374
|
+
else:
|
|
375
|
+
raw += '`' * temp_count
|
|
376
|
+
continue
|
|
377
|
+
|
|
378
|
+
raw += ch
|
|
379
|
+
self._stream.advance()
|
|
380
|
+
|
|
381
|
+
self._errors.add(
|
|
382
|
+
diag('tokenize.unterminated_string', {'str_type': '多行字符串'}, SourceRange.at(self._file, start))
|
|
383
|
+
)
|
|
384
|
+
raw += '`' * backtick_count
|
|
385
|
+
return self._make_token(RawTokenType.MULTILINE_STRING, raw, start=start)
|
|
386
|
+
|
|
387
|
+
# ── 数字 / 特殊浮点字面量 ─────────────────────────
|
|
388
|
+
|
|
389
|
+
def _read_number_fallback(self) -> RawToken:
|
|
390
|
+
"""读取数字或特殊浮点字面量(nan, +inf, -inf)。
|
|
391
|
+
|
|
392
|
+
支持的格式:
|
|
393
|
+
- 整数: 42, -80
|
|
394
|
+
- 浮点: 3.14, 5.0, 1e10, 2.5e-3
|
|
395
|
+
- 特殊: +inf, -inf(nan 由关键字路径处理)
|
|
396
|
+
"""
|
|
397
|
+
start = self._current_source_info()
|
|
398
|
+
raw_parts: list[str] = []
|
|
399
|
+
is_float = False
|
|
400
|
+
|
|
401
|
+
# ── 1. 可选正负号 ──
|
|
402
|
+
ch = self._stream.peek()
|
|
403
|
+
if not isinstance(ch, NoNextType) and ch in '+-':
|
|
404
|
+
raw_parts.append(ch)
|
|
405
|
+
self._stream.advance()
|
|
406
|
+
|
|
407
|
+
# ── 检查是否为特殊字面量 ──
|
|
408
|
+
if raw_parts and raw_parts[0] in '+-':
|
|
409
|
+
ch = self._stream.peek()
|
|
410
|
+
if not isinstance(ch, NoNextType) and ch.isalpha():
|
|
411
|
+
# 尝试读取标识符 (如 +inf, -inf)
|
|
412
|
+
ident_parts: list[str] = []
|
|
413
|
+
while not self._stream.eof():
|
|
414
|
+
c = self._stream.peek()
|
|
415
|
+
if not isinstance(c, NoNextType) and (c.isalnum() or c == '_'):
|
|
416
|
+
ident_parts.append(c)
|
|
417
|
+
self._stream.advance()
|
|
418
|
+
else:
|
|
419
|
+
break
|
|
420
|
+
ident = ''.join(ident_parts)
|
|
421
|
+
full = raw_parts[0] + ident
|
|
422
|
+
if full == '+inf':
|
|
423
|
+
return self._make_token(RawTokenType.FLOAT, full, start=start)
|
|
424
|
+
if full == '-inf':
|
|
425
|
+
return self._make_token(RawTokenType.FLOAT, full, start=start)
|
|
426
|
+
self._errors.add(diag('tokenize.invalid_number', {'raw': full}, SourceRange.at(self._file, start)))
|
|
427
|
+
return self._make_token(RawTokenType.IDENTIFIER, full, start=start)
|
|
428
|
+
|
|
429
|
+
# ── 2. 整数部分 ──
|
|
430
|
+
ch = self._stream.peek()
|
|
431
|
+
if not isinstance(ch, NoNextType) and ch.isdigit():
|
|
432
|
+
raw_parts.append(ch)
|
|
433
|
+
self._stream.advance()
|
|
434
|
+
while not self._stream.eof():
|
|
435
|
+
ch = self._stream.peek()
|
|
436
|
+
if not isinstance(ch, NoNextType) and ch.isdigit():
|
|
437
|
+
raw_parts.append(ch)
|
|
438
|
+
self._stream.advance()
|
|
439
|
+
else:
|
|
440
|
+
break
|
|
441
|
+
|
|
442
|
+
# ── 3. 可选小数部分 ──
|
|
443
|
+
ch = self._stream.peek()
|
|
444
|
+
if ch == '.':
|
|
445
|
+
self._stream.advance() # 消费 '.'
|
|
446
|
+
if not self._stream.eof():
|
|
447
|
+
next_ch = self._stream.peek()
|
|
448
|
+
if not isinstance(next_ch, NoNextType) and next_ch.isdigit():
|
|
449
|
+
is_float = True
|
|
450
|
+
raw_parts.append('.')
|
|
451
|
+
raw_parts.append(next_ch)
|
|
452
|
+
self._stream.advance()
|
|
453
|
+
while not self._stream.eof():
|
|
454
|
+
ch = self._stream.peek()
|
|
455
|
+
if not isinstance(ch, NoNextType) and ch.isdigit():
|
|
456
|
+
raw_parts.append(ch)
|
|
457
|
+
self._stream.advance()
|
|
458
|
+
else:
|
|
459
|
+
break
|
|
460
|
+
else:
|
|
461
|
+
# 有前置数字但 . 后无数字 (如 "42.") → 记录错误
|
|
462
|
+
self._errors.add(
|
|
463
|
+
diag(
|
|
464
|
+
'tokenize.invalid_number',
|
|
465
|
+
{'raw': ''.join(raw_parts) + '.'},
|
|
466
|
+
SourceRange.at(self._file, start),
|
|
467
|
+
)
|
|
468
|
+
)
|
|
469
|
+
|
|
470
|
+
# ── 4. 可选指数部分 ──
|
|
471
|
+
ch = self._stream.peek()
|
|
472
|
+
if not isinstance(ch, NoNextType) and ch in ['e', 'E']:
|
|
473
|
+
raw_parts.append(ch)
|
|
474
|
+
self._stream.advance()
|
|
475
|
+
is_float = True
|
|
476
|
+
|
|
477
|
+
if not self._stream.eof():
|
|
478
|
+
ch = self._stream.peek()
|
|
479
|
+
if not isinstance(ch, NoNextType) and ch in '+-':
|
|
480
|
+
raw_parts.append(ch)
|
|
481
|
+
self._stream.advance()
|
|
482
|
+
|
|
483
|
+
if self._stream.eof():
|
|
484
|
+
self._errors.add(
|
|
485
|
+
diag('tokenize.invalid_number', {'raw': ''.join(raw_parts)}, SourceRange.at(self._file, start))
|
|
486
|
+
)
|
|
487
|
+
else:
|
|
488
|
+
ch = self._stream.peek()
|
|
489
|
+
if not isinstance(ch, NoNextType) and ch.isdigit():
|
|
490
|
+
raw_parts.append(ch)
|
|
491
|
+
self._stream.advance()
|
|
492
|
+
while not self._stream.eof():
|
|
493
|
+
ch = self._stream.peek()
|
|
494
|
+
if not isinstance(ch, NoNextType) and ch.isdigit():
|
|
495
|
+
raw_parts.append(ch)
|
|
496
|
+
self._stream.advance()
|
|
497
|
+
else:
|
|
498
|
+
break
|
|
499
|
+
else:
|
|
500
|
+
self._errors.add(
|
|
501
|
+
diag('tokenize.invalid_number', {'raw': ''.join(raw_parts)}, SourceRange.at(self._file, start))
|
|
502
|
+
)
|
|
503
|
+
|
|
504
|
+
# ── 5. 确保至少有一位数字 ──
|
|
505
|
+
raw = ''.join(raw_parts)
|
|
506
|
+
if not any(c.isdigit() for c in raw):
|
|
507
|
+
self._errors.add(
|
|
508
|
+
diag('tokenize.invalid_number', {'raw': ''.join(raw_parts)}, SourceRange.at(self._file, start))
|
|
509
|
+
)
|
|
510
|
+
|
|
511
|
+
token_type = RawTokenType.FLOAT if is_float else RawTokenType.INTEGER
|
|
512
|
+
return self._make_token(token_type, raw, start=start)
|
|
513
|
+
|
|
514
|
+
# ── 标识符 / 关键字 ───────────────────────────────
|
|
515
|
+
|
|
516
|
+
def _read_identifier_or_keyword(self) -> RawToken:
|
|
517
|
+
"""读取标识符,识别关键字。"""
|
|
518
|
+
start = self._current_source_info()
|
|
519
|
+
raw_parts: list[str] = []
|
|
520
|
+
|
|
521
|
+
while not self._stream.eof():
|
|
522
|
+
ch = self._stream.peek()
|
|
523
|
+
if not isinstance(ch, NoNextType) and (ch.isalnum() or ch == '_'):
|
|
524
|
+
raw_parts.append(ch)
|
|
525
|
+
self._stream.advance()
|
|
526
|
+
else:
|
|
527
|
+
break
|
|
528
|
+
|
|
529
|
+
raw = ''.join(raw_parts)
|
|
530
|
+
token_type = self._keywords_map.get(raw, RawTokenType.IDENTIFIER)
|
|
531
|
+
return self._make_token(token_type, raw, start=start)
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: infinity-data
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: InfinityData:声明式配置语言(.infd/.inft)的 Python 编译器库
|
|
5
|
+
Author: infinity_system: yin_bailiang
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Repository, https://github.com/yinbailiang/infinity_data
|
|
8
|
+
Project-URL: Documentation, https://github.com/yinbailiang/infinity_data#readme
|
|
9
|
+
Keywords: config,configuration,declarative,dsl,compiler,infinity-data
|
|
10
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
18
|
+
Requires-Python: >=3.12
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Dynamic: license-file
|
|
22
|
+
|
|
23
|
+
# InfinityData
|
|
24
|
+
|
|
25
|
+
声明式配置语言(`.infd` / `.inft`)的 Python 编译器库。
|
|
26
|
+
|
|
27
|
+
- **编译优先**:`.infd` 是源码,JSON/YAML/TOML 是构建产物
|
|
28
|
+
- **模板即约束**:模板定义自动注册为约束校验器
|
|
29
|
+
- **零信任默认**:库默认 `deny_all` 沙盒,显式开放而非事后限制
|
|
30
|
+
- **结构化错误**:稳定错误码 + 结构化参数 + 多语言渲染
|
|
31
|
+
|
|
32
|
+
## 快速使用
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
from infinity_data import load, safe_load, SandboxConfig, Schema
|
|
36
|
+
|
|
37
|
+
result = load('app.infd') # 默认零信任
|
|
38
|
+
if result.has_errors:
|
|
39
|
+
for d in result.diagnostics:
|
|
40
|
+
print(d.location, d.code, d.message)
|
|
41
|
+
else:
|
|
42
|
+
print(result.value) # 降维后的 dict
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## 文档导航
|
|
46
|
+
|
|
47
|
+
| 文档 | 内容 |
|
|
48
|
+
|---|---|
|
|
49
|
+
| [neo_desg.md](./neo_desg.md) | 语言基础设计(语法 / 类型 / 模板 / 约束) |
|
|
50
|
+
| [extra_desg.md](./extra_desg.md) | 库 API / CLI / 生态设计 |
|
|
51
|
+
| [impl_desg.md](./impl_desg.md) | 编译器实现层的取舍与假设 |
|
|
52
|
+
|
|
53
|
+
## 开发
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
uv run pytest -q # 测试
|
|
57
|
+
uv run ruff check src tests # lint
|
|
58
|
+
uv run pyright # 类型检查(strict)
|
|
59
|
+
uv run python test.py # 错误报告演示
|
|
60
|
+
```
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
infinity_data/__init__.py,sha256=soRyEbA8B2BvU8H_VQJgx3HDFR6-dwT188du-sxF-sU,979
|
|
2
|
+
infinity_data/frontend.py,sha256=3OTZOkpgM8uI1eTAjwrsvo_Q3jNemT0Sd7rW1dEU0zY,1577
|
|
3
|
+
infinity_data/pipeline.py,sha256=VpOnnX8LYf6xgs3QwS2kYzI49QUuodypQtscE1X-b5Q,9827
|
|
4
|
+
infinity_data/emit/__init__.py,sha256=VaLebPtYb4_lHF8b_19FTLc9gkWUUYMbMueUoq4IFqQ,398
|
|
5
|
+
infinity_data/emit/converter.py,sha256=26fdvzfKlvxinEJ79px6fMZ6fbM4PNRsEMxJXGPTe_M,1918
|
|
6
|
+
infinity_data/infra/__init__.py,sha256=LRAUkn-2gQKCOhPj4MGsjwZgYBAnBHre6amzZOnhug4,213
|
|
7
|
+
infinity_data/infra/diagnostics.py,sha256=MsS4cn_KzTq_gBhTV6LGQRF11XCe3SKe32QXGoGWewY,7274
|
|
8
|
+
infinity_data/infra/file.py,sha256=ty0VSDKBcdCT-QcO1x8-NGqsRs9bMU-JN5T1KqrZJbc,2668
|
|
9
|
+
infinity_data/infra/ll1_stream.py,sha256=U3bAMAVfV3I-uui-EvWN2JeL_7ZQGz6PP107WTDh1Uk,2029
|
|
10
|
+
infinity_data/infra/location.py,sha256=5ej8mr2QCG7lbNynKz9AvSi3B3WzpvRuZirH-pZHrBI,2058
|
|
11
|
+
infinity_data/infra/path.py,sha256=S1rj5M60kNKQqJYh3Pod_P4NFD7WDBbGrqdY9SjtwO8,1930
|
|
12
|
+
infinity_data/parser/__init__.py,sha256=4g5x2Gp6qCSF6ChXd6bfhZoxKa1qWqw8LqkOy3ACbOg,1830
|
|
13
|
+
infinity_data/parser/diagnostics.py,sha256=sU2RntPnzv7c7VWZaneXnDHpluq_9CEko5WzYgYJ9BU,4254
|
|
14
|
+
infinity_data/parser/models.py,sha256=sXi6yb7pkWT3AMwB7p_9edZxzTCMbIANdLl4-fSNXbg,9055
|
|
15
|
+
infinity_data/parser/parser.py,sha256=rI25qZDSeUOi-7LhCWSQYkNdVItwj7W5_880ptld3_s,40479
|
|
16
|
+
infinity_data/parser/token_stream.py,sha256=mwbFjoBG_4cR-j1KFs5b7kkShT4vU52HcOZ_FSFyk6A,5191
|
|
17
|
+
infinity_data/sandbox/__init__.py,sha256=qFgTsZhAzFTtRKu-RJEJLNtQNLWWU_uk9WJjhWI5VnA,909
|
|
18
|
+
infinity_data/sandbox/config.py,sha256=MgFgxMEKvUk6J0S9mNTPCp8JRxcv83r6OmL_qec7lV0,2235
|
|
19
|
+
infinity_data/sandbox/errors.py,sha256=D48DnWicB5fCT9h3c2cce34eue5CA-Oq0w38DrC2i0k,3781
|
|
20
|
+
infinity_data/sandbox/mediator.py,sha256=4HWENRUORaMhvjgRf0irCThpTQ8TZjqNeJudIHi-Szg,8148
|
|
21
|
+
infinity_data/sandbox/schema.py,sha256=P_rvBBEixeThYw-kgpoLImlpf4u4K7_FpytDncteZmw,686
|
|
22
|
+
infinity_data/semantic/__init__.py,sha256=eazCCWg8o_P-atwgKbzrfPDQGjlCMRTMlr3W1ulbUWc,1632
|
|
23
|
+
infinity_data/semantic/constraints.py,sha256=3Hh-LjOkWh8wexzbZmjTb1K3tunQ_t3LV2P5TNNYXsU,5896
|
|
24
|
+
infinity_data/semantic/diagnostics.py,sha256=Tmdb0VcA_XPUSWYfyBZZ2EknSowZH22SFBrK1XM-Coc,11762
|
|
25
|
+
infinity_data/semantic/builder/__init__.py,sha256=7aJXEq_lcsyGj8JjbvRBNYx9rmU5INHUAcrSp-aU_M8,735
|
|
26
|
+
infinity_data/semantic/builder/builder.py,sha256=GZeHM6fuowWoyuw6-weRSSHEHgxGO_uBS05BncwB8GM,23717
|
|
27
|
+
infinity_data/semantic/builder/models.py,sha256=EIxc6og4LraTPcX9kSG0EsjtyVbdITyQJw4pwcypRLg,6338
|
|
28
|
+
infinity_data/semantic/executor/__init__.py,sha256=b-XF7P88jmaqBovLaU8YMsYOGHxw0GHyaDm3P2KrMWo,364
|
|
29
|
+
infinity_data/semantic/executor/executor.py,sha256=vvtSj5lykRGpsuaFPyi0-qrvj-PWfedPIMwKlCA3QSI,11439
|
|
30
|
+
infinity_data/semantic/registry/__init__.py,sha256=mEmCXO_MNaBpdN3FcPfawrW7eT3osZ-HB55OQ71pBQ4,7162
|
|
31
|
+
infinity_data/semantic/registry/_core.py,sha256=YWXv8Z6RCOkvDu_rbooF-PGLlV_fVbmNrkE41t9DBkI,7167
|
|
32
|
+
infinity_data/semantic/registry/dict_constraints.py,sha256=PtER1nYL1_ScVPduyF_B-_pFG9T9W-UbYveHiOnE-II,1572
|
|
33
|
+
infinity_data/semantic/registry/general.py,sha256=kLCOKTV46LioS-SeLzoUcVzeO9t90rnlCfHcbK12OK0,11579
|
|
34
|
+
infinity_data/semantic/registry/logic.py,sha256=AVKepdmyQ_YSx6VDDw8p_0rzRNQLf5vYS9CRH-n5v5E,3683
|
|
35
|
+
infinity_data/semantic/registry/types.py,sha256=SkePeIXmimpVzPKOsPnzNLM6Zgn6y1hvzXrAhXOuBZM,4086
|
|
36
|
+
infinity_data/semantic/resolver/__init__.py,sha256=uue2RoEOe0F7a4MZ7C2fBPyMbOXFdqOqMY689JdE-FQ,755
|
|
37
|
+
infinity_data/semantic/resolver/imports.py,sha256=Xi4WeVO6S2puiX478gs2rGOMLeyL106PaOgmWididmc,7418
|
|
38
|
+
infinity_data/semantic/resolver/models.py,sha256=hpKNEHYPkWJK3u9AuP53sIsebxbW3ukHpdD2hh5EGIw,2767
|
|
39
|
+
infinity_data/semantic/resolver/resolver.py,sha256=8Hr-d9f1Q7dwWq_o6B1pSEJRdCosQvUabgMNmKijgLg,15207
|
|
40
|
+
infinity_data/tokenizer/__init__.py,sha256=dkPkCtnPNVNzHjepCPsxhwJUeLJsl13vIvPPbv_06Hw,532
|
|
41
|
+
infinity_data/tokenizer/char_stream.py,sha256=hn6putg7374lDGR6dSIZTuDWwEnJz4CpQmV-dcygv3I,2026
|
|
42
|
+
infinity_data/tokenizer/diagnostics.py,sha256=pRiNKFh6-I2Vt9HSkoBKUgsKy1z7s9-Fq6oZ9OZlG5s,2433
|
|
43
|
+
infinity_data/tokenizer/finalizer.py,sha256=3Sxhd7PIYoY_jw8HrO6BaPo9uapbWGpwqtd9B9hQ9cQ,9652
|
|
44
|
+
infinity_data/tokenizer/tokenizer.py,sha256=wccZs7vOQDu_lcA_kEqMRvkkcriNDzuUXEVMB9A23XA,20521
|
|
45
|
+
infinity_data/tokenizer/models/__init__.py,sha256=vr4OWsEb0-5cNmWJb15qY2O4MwJ3hAPM4Zd5a6phK70,356
|
|
46
|
+
infinity_data/tokenizer/models/raw_tokens.py,sha256=YmX1KF8BzFjxazQAzKMT2UXQJNNXiXDijW1sAi7cIVw,1896
|
|
47
|
+
infinity_data/tokenizer/models/tokens.py,sha256=Ozdrrc06bhfiu04y1-AC6kMU6nSApExyNvglYMwFEYQ,3975
|
|
48
|
+
infinity_data-1.0.0.dist-info/licenses/LICENSE,sha256=SQtaU1Umg4scJDkot4OxhqzIB26vkK50CvciBhaGJyc,1086
|
|
49
|
+
infinity_data-1.0.0.dist-info/METADATA,sha256=mcn9RNs6zMJmaCk0-TUCyyTnaYngry_AjTsFwzZmUHQ,2119
|
|
50
|
+
infinity_data-1.0.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
51
|
+
infinity_data-1.0.0.dist-info/top_level.txt,sha256=aHZLBFrKFdz2uxSgNYqD5Q7enbDMIzorit96IMU0VL0,14
|
|
52
|
+
infinity_data-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 InfinitySystem (yin_bailiang)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
infinity_data
|