linked-data-python 0.0.4__py3-none-any.whl → 0.2.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.
Files changed (41) hide show
  1. ldpy/__init__.py +35 -11
  2. ldpy/__main__.py +75 -250
  3. ldpy/build.py +91 -0
  4. ldpy/console.py +116 -0
  5. ldpy/debug.py +235 -0
  6. ldpy/formatter.py +329 -0
  7. ldpy/importer.py +110 -0
  8. ldpy/lsp/__init__.py +11 -0
  9. ldpy/lsp/__main__.py +4 -0
  10. ldpy/lsp/backend.py +118 -0
  11. ldpy/lsp/rpc.py +104 -0
  12. ldpy/lsp/server.py +353 -0
  13. ldpy/lsp/translate.py +140 -0
  14. ldpy/pygments_lexer.py +613 -0
  15. ldpy/runtime.py +931 -0
  16. ldpy/sparql.py +551 -0
  17. ldpy/transpiler/__init__.py +14 -0
  18. ldpy/transpiler/core.py +2558 -0
  19. ldpy/transpiler/errors.py +38 -0
  20. ldpy/transpiler/linemap.py +292 -0
  21. linked_data_python-0.2.0.dist-info/METADATA +158 -0
  22. linked_data_python-0.2.0.dist-info/RECORD +26 -0
  23. {linked_data_python-0.0.4.dist-info → linked_data_python-0.2.0.dist-info}/WHEEL +1 -1
  24. linked_data_python-0.2.0.dist-info/entry_points.txt +9 -0
  25. {linked_data_python-0.0.4.dist-info → linked_data_python-0.2.0.dist-info/licenses}/LICENSE.md +0 -0
  26. {linked_data_python-0.0.4.dist-info → linked_data_python-0.2.0.dist-info}/top_level.txt +0 -0
  27. ldpy/grun/lib.py +0 -63
  28. ldpy/grun/util.py +0 -11
  29. ldpy/ldpy.py +0 -183
  30. ldpy/rewriter/IndentedStringWriter.py +0 -54
  31. ldpy/rewriter/LDPythonRewriter.py +0 -677
  32. ldpy/rewriter/MultiChannelTokenStream.py +0 -127
  33. ldpy/rewriter/Result.py +0 -49
  34. ldpy/rewriter/__init__.py +0 -7
  35. ldpy/rewriter/antlr/LDPythonLexer.py +0 -870
  36. ldpy/rewriter/antlr/LDPythonParser.py +0 -9336
  37. ldpy/rewriter/antlr/LDPythonVisitor.py +0 -573
  38. ldpy/sparql/builtin.py +0 -326
  39. linked_data_python-0.0.4.dist-info/METADATA +0 -139
  40. linked_data_python-0.0.4.dist-info/RECORD +0 -20
  41. linked_data_python-0.0.4.dist-info/entry_points.txt +0 -3
@@ -0,0 +1,2558 @@
1
+ """Transpileur Linked-Data Python v2 — « island parsing ».
2
+
3
+ Un seul passage sur le source : le Python est recopié verbatim, seuls les îlots
4
+ RDF sont parsés (descente récursive) et réécrits en UNE expression Python
5
+ s'appuyant sur le runtime ldpy (importé sous l'alias réservé `_ldpy_`).
6
+
7
+ La conception est expliquée dans docs/explanation/ ; la référence du
8
+ langage dans docs/reference/language.md.
9
+
10
+ Limitations assumées (documentées) :
11
+ - le contenu des chaînes Python est opaque (pas d'îlot dans une f-string) ;
12
+ - f-strings « PEP 701 » (guillemets identiques imbriqués) non supportées, comme
13
+ dans MicroPython.
14
+ """
15
+
16
+ import re
17
+
18
+ from ldpy.transpiler.errors import LdpySyntaxError, LdpyWarning
19
+ from ldpy.transpiler.linemap import LanguageMap
20
+
21
+ try:
22
+ from urllib.parse import urljoin as _urljoin
23
+ except ImportError: # MicroPython : résolution naïve
24
+ def _urljoin(base, rel):
25
+ return base + rel
26
+
27
+ RUNTIME_ALIAS = "_ldpy_"
28
+ PRELUDE = "import ldpy.runtime as _ldpy_; __namespaces__ = {}; __base__ = None"
29
+
30
+ KEYWORDS = frozenset((
31
+ "False", "None", "True", "and", "as", "assert", "async", "await", "break",
32
+ "class", "continue", "def", "del", "elif", "else", "except", "finally",
33
+ "for", "from", "global", "if", "import", "in", "is", "lambda", "nonlocal",
34
+ "not", "or", "pass", "raise", "return", "try", "while", "with", "yield",
35
+ ))
36
+ # mots-clés « valeurs » : terminent un opérande
37
+ VALUE_KEYWORDS = frozenset(("False", "None", "True"))
38
+
39
+ STRING_PREFIXES = frozenset((
40
+ "", "r", "b", "u", "f", "rb", "br", "rf", "fr",
41
+ "R", "B", "U", "F", "Rb", "rB", "RB", "bR", "Br", "BR",
42
+ "Rf", "rF", "RF", "fR", "Fr", "FR",
43
+ ))
44
+
45
+ _SCHEME_RE = re.compile(r"^[A-Za-z][A-Za-z0-9+.\-]*:")
46
+ _LANGTAG_RE = re.compile(r"@([A-Za-z]+(?:-[A-Za-z0-9]+)*)")
47
+ _NAME_RE = re.compile(r"[A-Za-z_][A-Za-z0-9_]*")
48
+ #: commentaire Python jusqu'à la fin de ligne (fiche 013, détection d'import)
49
+ _COMMENT_RE = re.compile(r"#[^\n]*")
50
+
51
+ _PYNAME_RE = re.compile(r"[^\W\d]\w*") # identifiant Python (unicode)
52
+
53
+ # --- Jeux de caractères Turtle (décision charsets, vérifiée par
54
+ # --- tools/charsets.py contre une transcription indépendante des specs) :
55
+ # --- tables EXACTES dans les îlots ; hors îlots, l'INTERSECTION avec les
56
+ # --- identifiants Python (on ne peut pas capturer du Python valide).
57
+ import bisect
58
+
59
+ _PN_BASE_RANGES = (
60
+ (0x41, 0x5A), (0x61, 0x7A), (0xC0, 0xD6), (0xD8, 0xF6), (0xF8, 0x2FF),
61
+ (0x370, 0x37D), (0x37F, 0x1FFF), (0x200C, 0x200D), (0x2070, 0x218F),
62
+ (0x2C00, 0x2FEF), (0x3001, 0xD7FF), (0xF900, 0xFDCF), (0xFDF0, 0xFFFD),
63
+ (0x10000, 0xEFFFF),
64
+ )
65
+ _PN_EXTRA_RANGES = ( # PN_CHARS \ (PN_CHARS_BASE + '_' + chiffres)
66
+ (0x2D, 0x2D), (0xB7, 0xB7), (0x300, 0x36F), (0x203F, 0x2040),
67
+ )
68
+
69
+
70
+ def _in_ranges(c, ranges, _starts_cache={}):
71
+ key = id(ranges)
72
+ starts = _starts_cache.get(key)
73
+ if starts is None:
74
+ starts = _starts_cache[key] = [r[0] for r in ranges]
75
+ cp = ord(c)
76
+ i = bisect.bisect_right(starts, cp) - 1
77
+ return i >= 0 and cp <= ranges[i][1]
78
+
79
+
80
+ def _pn_base(c):
81
+ """PN_CHARS_BASE de Turtle/SPARQL."""
82
+ return _in_ranges(c, _PN_BASE_RANGES)
83
+
84
+
85
+ def _pn_char(c):
86
+ """PN_CHARS de Turtle/SPARQL."""
87
+ return (_pn_base(c) or c == "_" or c.isdigit() and c.isascii()
88
+ or _in_ranges(c, _PN_EXTRA_RANGES))
89
+
90
+
91
+ def _py_id_continue(c):
92
+ return ("a" + c).isidentifier()
93
+
94
+
95
+ def _ix_start(c):
96
+ """Début de nom HORS îlots : identifiant Python ∩ PN_CHARS_BASE|_."""
97
+ return c.isidentifier() and (_pn_base(c) or c == "_")
98
+
99
+
100
+ def _ix_char(c):
101
+ """Continuation HORS îlots : identifiant Python ∩ PN_CHARS."""
102
+ return _py_id_continue(c) and _pn_char(c)
103
+
104
+
105
+ def _scan_pn_prefix(text, i, n):
106
+ """PN_PREFIX exact de Turtle (points intérieurs, tirets…) ; retourne la
107
+ fin, ou i si pas de préfixe à cette position."""
108
+ if i >= n or not _pn_base(text[i]):
109
+ return i
110
+ j = i + 1
111
+ while j < n and (_pn_char(text[j]) or text[j] == "."):
112
+ j += 1
113
+ while j > i + 1 and text[j - 1] == ".":
114
+ j -= 1
115
+ return j
116
+
117
+
118
+ def _scan_pn_local_island(text, i, n):
119
+ """PN_LOCAL de Turtle (sans ':' intérieur ni échappements PLX,
120
+ limitation documentée) ; points intérieurs, chiffres en tête."""
121
+ if i >= n or not (_pn_char(text[i]) or text[i] == "_"):
122
+ return i
123
+ j = i + 1
124
+ while j < n and (_pn_char(text[j]) or text[j] == "."):
125
+ j += 1
126
+ while j > i and text[j - 1] == ".":
127
+ j -= 1
128
+ return j
129
+
130
+
131
+ def _is_iri_char(c):
132
+ return ord(c) > 0x20 and c not in '<>"{}|^`\\'
133
+
134
+
135
+ def _name_start(c):
136
+ return c.isalpha() or c == "_"
137
+
138
+
139
+ def _name_char(c):
140
+ return c.isalnum() or c == "_"
141
+
142
+
143
+ class DynPrefix:
144
+ """Préfixe dynamique (importé, ou à IRI calculée) : la résolution passe
145
+ à l'exécution par la variable Python émise `var` (fiche 013). La table
146
+ lexicale ne connaît pas son IRI."""
147
+
148
+ __slots__ = ("var",)
149
+
150
+ def __init__(self, var):
151
+ self.var = var
152
+
153
+ def __repr__(self):
154
+ return "DynPrefix(%r)" % self.var
155
+
156
+
157
+ class TranspileResult:
158
+ """Résultat de transpile() : code généré, map, préfixes, warnings."""
159
+
160
+ def __init__(self, code, lmap, prefixes, base, warnings):
161
+ self.code = code # source Python généré
162
+ self.map = lmap # LanguageMap
163
+ self.prefixes = prefixes # dict prefix -> IRI (str) ou DynPrefix
164
+ self.base = base # IRI de base finale (str ou None)
165
+ self.warnings = warnings # list[LdpyWarning]
166
+
167
+
168
+ class Transpiler:
169
+ """Une instance par fichier. Usage : Transpiler(src, filename).run()."""
170
+
171
+ def __init__(self, text, filename="<ldpy>", emit_prelude=True):
172
+ self.text = text
173
+ self.emit_prelude = emit_prelude
174
+ self.n = len(text)
175
+ self.filename = filename
176
+ self.i = 0
177
+ # positions 0-based
178
+ self.src_line = 0
179
+ self.src_col = 0
180
+ self.gen_line = 0
181
+ self.gen_col = 0
182
+ self.out = []
183
+ self.map = LanguageMap(filename)
184
+ self._copy_anchor = None # (sl, sc, gl, gc) début du segment copy ouvert
185
+ self._sub = False # True pendant le scan d'une expression imbriquée
186
+ # état langage
187
+ self.prefixes = {} # prefix -> IRI str ('' = préfixe par défaut)
188
+ self._prefix_used = set()
189
+ self.base = None
190
+ self.warnings = []
191
+ self.uses_runtime = False
192
+ # état lexical Python
193
+ self.depth = 0
194
+ # sorte de chaque crochet ouvert : "(", "[" (liste), "s" (indice),
195
+ # "{" (dict/ensemble). Sert au nœud anonyme hors îlot, qui ne doit
196
+ # pas se confondre avec `{_: v}` ni avec la tranche `a[_:v]`.
197
+ self.brackets = []
198
+ # `lambda` ouvre une liste de paramètres qui se ferme sur ':' — un ':'
199
+ # que Python possède, et où aucun terme RDF ne peut vivre.
200
+ self._lambda_depth = None
201
+ self._after_def = False # on vient de lire 'def' : le '(' suivant
202
+ self.operand = True # un opérande peut commencer ici
203
+ self.stmt_start = True # début de ligne logique (hors espaces)
204
+ self.after_dot = False
205
+ # portée par bloc des @prefix/@base :
206
+ # chaque déclaration empile (indent, kind, nom, avait_prev, prev) ;
207
+ # une instruction moins indentée dépile et restaure.
208
+ self._scope_stack = []
209
+ self._retired = {} # préfixe sorti de portée -> ligne de décl.
210
+ self._prefix_col = {} # préfixe -> indentation de sa déclaration
211
+ # préfixes dynamiques (fiche 013)
212
+ self._ns_counter = 0 # variables fraîches _ldpy_ns*
213
+ self._import_lines = [] # (ligne 0-based, module) des imports de préfixes
214
+ # graphe courant (fiche 014)
215
+ self._graph_var = None # variable Python du graphe courant, ou None
216
+ # îlot de motif (fiche 016) : collecte des variables projetées
217
+ self._match_vars = None
218
+ # binding courant (fiche 017)
219
+ self._bindings_var = None
220
+
221
+ # ------------------------------------------------------------------
222
+ # primitives de position / émission
223
+ # ------------------------------------------------------------------
224
+
225
+ def _advance_src(self, s):
226
+ nl = s.count("\n")
227
+ if nl:
228
+ self.src_line += nl
229
+ self.src_col = len(s) - s.rfind("\n") - 1
230
+ else:
231
+ self.src_col += len(s)
232
+
233
+ def _advance_gen(self, s):
234
+ nl = s.count("\n")
235
+ if nl:
236
+ self.gen_line += nl
237
+ self.gen_col = len(s) - s.rfind("\n") - 1
238
+ else:
239
+ self.gen_col += len(s)
240
+
241
+ def _take(self, k):
242
+ """Consomme k caractères du source sans les émettre."""
243
+ s = self.text[self.i:self.i + k]
244
+ self.i += k
245
+ self._advance_src(s)
246
+ return s
247
+
248
+ def _put(self, s):
249
+ """Émet du texte généré."""
250
+ self.out.append(s)
251
+ if not self._sub:
252
+ self._advance_gen(s)
253
+
254
+ def _copy(self, k):
255
+ """Recopie k caractères du source vers la sortie (segment copy)."""
256
+ if k <= 0:
257
+ return
258
+ if self._copy_anchor is None and not self._sub:
259
+ self._copy_anchor = (self.src_line, self.src_col,
260
+ self.gen_line, self.gen_col)
261
+ s = self.text[self.i:self.i + k]
262
+ self.i += k
263
+ self._advance_src(s)
264
+ self._put(s)
265
+
266
+ def _close_copy(self):
267
+ if self._copy_anchor is not None:
268
+ sl, sc, gl, gc = self._copy_anchor
269
+ self.map.add("copy", (sl, sc, self.src_line, self.src_col),
270
+ (gl, gc, self.gen_line, self.gen_col))
271
+ self._copy_anchor = None
272
+
273
+ def _begin_island(self):
274
+ self._close_copy()
275
+ return (self.src_line, self.src_col, self.gen_line, self.gen_col)
276
+
277
+ def _end_island(self, kind, mark, gen_text):
278
+ self._put(gen_text)
279
+ if not self._sub:
280
+ sl, sc, gl, gc = mark
281
+ self.map.add("island:" + kind,
282
+ (sl, sc, self.src_line, self.src_col),
283
+ (gl, gc, self.gen_line, self.gen_col))
284
+ self.uses_runtime = True
285
+ self.operand = False
286
+ self.stmt_start = False
287
+
288
+ def _error(self, msg, line=None, col=None):
289
+ exc = LdpySyntaxError(msg, self.filename,
290
+ self.src_line if line is None else line,
291
+ self.src_col if col is None else col)
292
+ # la console s'en sert pour distinguer « entrée incomplète » (îlot
293
+ # non fermé en fin de tampon) d'une vraie erreur de syntaxe
294
+ exc.at_eof = self.i >= self.n
295
+ raise exc
296
+
297
+ def _warn(self, msg):
298
+ self.warnings.append(LdpyWarning(msg, self.filename,
299
+ self.src_line, self.src_col))
300
+
301
+ def _peek(self, off=0):
302
+ j = self.i + off
303
+ return self.text[j] if j < self.n else ""
304
+
305
+ # ------------------------------------------------------------------
306
+ # chaînes Python (lexique, fonction pure sur self.text)
307
+ # ------------------------------------------------------------------
308
+
309
+ def _string_end(self, i):
310
+ """i pointe sur le 1er guillemet. Retourne l'indice après la chaîne."""
311
+ t = self.text
312
+ q = t[i]
313
+ if t[i:i + 3] == q * 3:
314
+ quote, j = q * 3, i + 3
315
+ else:
316
+ quote, j = q, i + 1
317
+ lq = len(quote)
318
+ while j < self.n:
319
+ c = t[j]
320
+ if c == "\\":
321
+ j += 2
322
+ continue
323
+ if t[j:j + lq] == quote:
324
+ return j + lq
325
+ if c == "\n" and lq == 1:
326
+ self._error("chaîne non terminée", self.src_line, self.src_col)
327
+ j += 1
328
+ self._error("chaîne non terminée", self.src_line, self.src_col)
329
+
330
+ # ------------------------------------------------------------------
331
+ # boucle principale
332
+ # ------------------------------------------------------------------
333
+
334
+ def run(self):
335
+ """Transpile le fichier entier ; retourne un TranspileResult."""
336
+ self._scan()
337
+ self._close_copy()
338
+ code = "".join(self.out)
339
+ if self.uses_runtime and self.emit_prelude:
340
+ code = self._insert_prelude(code)
341
+ return TranspileResult(code, self.map, dict(self.prefixes),
342
+ self.base, self.warnings)
343
+
344
+ def _scan(self, stops=None, entry_depth=None):
345
+ """Scanne du Python. Si stops est fourni, s'arrête (sans consommer)
346
+ sur un de ces caractères à la profondeur entry_depth."""
347
+ t = self.text
348
+ while self.i < self.n:
349
+ c = t[self.i]
350
+ if stops is not None and self.depth == entry_depth and c in stops:
351
+ return
352
+ if self.stmt_start and self.depth == 0 and not self._sub \
353
+ and c not in " \t\r\n\\#":
354
+ self._unwind_scopes(self.src_col)
355
+ if c == "\n":
356
+ self._copy(1)
357
+ if self.depth == 0:
358
+ self.stmt_start = True
359
+ self.operand = True
360
+ self.after_dot = False
361
+ elif c in " \t\r":
362
+ self._copy(1)
363
+ elif c == "\\" and self._peek(1) == "\n":
364
+ self._copy(2)
365
+ elif c == "#":
366
+ j = t.find("\n", self.i)
367
+ self._copy((j if j != -1 else self.n) - self.i)
368
+ elif c in "\"'":
369
+ self._handle_string(self.i)
370
+ elif _name_start(c):
371
+ self._handle_name()
372
+ elif c.isdigit() or (c == "." and self._peek(1).isdigit()):
373
+ self._copy(self._number_end(self.i) - self.i)
374
+ self.operand = False
375
+ self.stmt_start = False
376
+ elif c == "@":
377
+ if self.stmt_start and not self._sub \
378
+ and self._try_prefix_or_base():
379
+ continue
380
+ self._copy(1)
381
+ self.operand = True
382
+ self.stmt_start = False
383
+ elif c in "+-" and self._peek(1) == "{" and self.stmt_start \
384
+ and self.depth == 0 and not self._sub:
385
+ # +{ ... } / -{ ... } : ajout/retrait sur le graphe courant
386
+ # (fiche 014) — position d'instruction uniquement ; ailleurs,
387
+ # + et - gardent leur sens Python.
388
+ self._addremove_island(c)
389
+ elif c in "?$":
390
+ self._var_island()
391
+ elif c == "<" and self.operand:
392
+ if not self._try_iri_island():
393
+ self._copy(1)
394
+ self.operand = True
395
+ self.stmt_start = False
396
+ elif c in "([{":
397
+ self.depth += 1
398
+ # un '[' qui suit un opérande complet est un INDICE, pas une
399
+ # liste : `a[i:j]` est une tranche, `[i:j]` n'existe pas
400
+ if c == "(" and self._after_def:
401
+ self.brackets.append("p") # paramètres d'un def
402
+ else:
403
+ self.brackets.append("s" if c == "[" and not self.operand
404
+ else c)
405
+ self._after_def = False
406
+ self._copy(1)
407
+ self.operand = True
408
+ self.stmt_start = False
409
+ self.after_dot = False
410
+ elif c in ")]}":
411
+ self.depth -= 1
412
+ if self.brackets:
413
+ self.brackets.pop()
414
+ if self._lambda_depth is not None \
415
+ and self.depth < self._lambda_depth:
416
+ self._lambda_depth = None
417
+ self._copy(1)
418
+ self.operand = False
419
+ self.stmt_start = False
420
+ elif c == ".":
421
+ if t[self.i:self.i + 3] == "...":
422
+ self._copy(3)
423
+ self.operand = False
424
+ else:
425
+ if not self.operand:
426
+ self.after_dot = True
427
+ self._copy(1)
428
+ self.stmt_start = False
429
+ elif c == ":":
430
+ if self._lambda_depth == self.depth:
431
+ self._lambda_depth = None # fin des paramètres
432
+ self._copy(1)
433
+ self.operand = True
434
+ self.stmt_start = False
435
+ self.after_dot = False
436
+ elif c == ";":
437
+ self._copy(1)
438
+ self.operand = True
439
+ if self.depth == 0:
440
+ self.stmt_start = True
441
+ else:
442
+ # opérateurs, ponctuation, identifiants unicode
443
+ if c.isidentifier():
444
+ j = self.i + 1
445
+ while j < self.n and (t[j].isalnum() or t[j] == "_"):
446
+ j += 1
447
+ self._copy(j - self.i)
448
+ self.operand = False
449
+ else:
450
+ # dans une liste de paramètres, le ':' est une annotation
451
+ # (Python) mais après un '=' on est dans une VALEUR par
452
+ # défaut, où un terme RDF a tout à fait sa place :
453
+ # `def f(x=ex:Thing)`. 'p' -> 'P' le note.
454
+ if self.brackets and self.brackets[-1] in "pP":
455
+ if c == "=" and self._peek(1) != "=" \
456
+ and (self.i == 0
457
+ or t[self.i - 1] not in "=!<>+-*/%&|^~:@"):
458
+ self.brackets[-1] = "P"
459
+ elif c == ",":
460
+ self.brackets[-1] = "p"
461
+ self._copy(1)
462
+ self.operand = True
463
+ self.stmt_start = False
464
+ self.after_dot = False
465
+
466
+ def _scan_embedded_expr(self, stops):
467
+ """Scanne une expression Python imbriquée dans un îlot ; retourne son
468
+ texte transpilé. S'arrête sur `stops` à la profondeur d'entrée."""
469
+ saved_out, self.out = self.out, []
470
+ saved_sub, self._sub = self._sub, True
471
+ saved_anchor, self._copy_anchor = self._copy_anchor, None
472
+ saved_ctx = (self.operand, self.stmt_start, self.after_dot)
473
+ self.operand = True
474
+ self.after_dot = False
475
+ try:
476
+ self._scan(stops=stops, entry_depth=self.depth)
477
+ return "".join(self.out)
478
+ finally:
479
+ self.out = saved_out
480
+ self._sub = saved_sub
481
+ self._copy_anchor = saved_anchor
482
+ self.operand, self.stmt_start, self.after_dot = saved_ctx
483
+
484
+ # ------------------------------------------------------------------
485
+ # nombres
486
+ # ------------------------------------------------------------------
487
+
488
+ def _number_end(self, i):
489
+ t = self.text
490
+ j = i
491
+ while j < self.n:
492
+ c = t[j]
493
+ if c.isalnum() or c in "._":
494
+ j += 1
495
+ elif c in "+-" and t[j - 1] in "eE" and j > i and \
496
+ t[i:j].lower().lstrip("0").startswith(("x",)) is False:
497
+ # exposant signé : 1e-5 (pas dans les hexas)
498
+ if not t[i:i + 2].lower() in ("0x", "0o", "0b"):
499
+ j += 1
500
+ else:
501
+ break
502
+ else:
503
+ break
504
+ # un nom collé derrière un nombre (ex. `1if`) n'existe plus en py3 ;
505
+ # on recopie tel quel, le compilateur Python tranchera.
506
+ return j
507
+
508
+ # ------------------------------------------------------------------
509
+ # chaînes et littéraux RDF
510
+ # ------------------------------------------------------------------
511
+
512
+ def _handle_string(self, start, prefix_start=None):
513
+ """start : indice du guillemet. prefix_start : indice du préfixe de
514
+ chaîne (r/b/f/u) déjà repéré, sinon None."""
515
+ tok_start = prefix_start if prefix_start is not None else start
516
+ end = self._string_end(start)
517
+ # suffixe RDF ?
518
+ t = self.text
519
+ suffix = None
520
+ if end < self.n and t[end] == "@":
521
+ m = _LANGTAG_RE.match(t, end)
522
+ if m:
523
+ suffix = ("lang", m.group(1), m.end())
524
+ if suffix is None and t[end:end + 2] == "^^":
525
+ suffix = ("datatype", None, end + 2)
526
+ if suffix is None:
527
+ self._copy(end - self.i)
528
+ self.operand = False
529
+ self.stmt_start = False
530
+ return
531
+ # îlot littéral RDF
532
+ mark = self._begin_island()
533
+ string_text = t[tok_start:end]
534
+ self._take(end - self.i) # consomme la chaîne
535
+ kind, lang, after = suffix
536
+ if kind == "lang":
537
+ self._take(after - self.i) # consomme @lang
538
+ gen = "%s.Literal(%s, lang=%r)" % (RUNTIME_ALIAS, string_text, lang)
539
+ else:
540
+ self._take(2) # consomme ^^
541
+ dt = self._parse_term_after_hats()
542
+ gen = "%s.Literal(%s, datatype=%s)" % (RUNTIME_ALIAS, string_text, dt)
543
+ self._end_island("literal", mark, gen)
544
+
545
+ def _parse_term_after_hats(self):
546
+ """Terme datatype juste après '^^' (collé) : iri, pname, firi,
547
+ interpolation {expr} (ou f{expr}, sa forme historique).
548
+
549
+ Un datatype est toujours un IRI : l'interpolation passe donc par
550
+ dtype() et non par node(), qui ferait un littéral d'une chaîne."""
551
+ c = self._peek()
552
+ if c == "<":
553
+ iri = self._take_iriref()
554
+ return "%s.URIRef(%r)" % (RUNTIME_ALIAS, self._resolve(iri))
555
+ if c == "f" and self._peek(1) == "<":
556
+ return self._take_firi()
557
+ if c == "{" or (c == "f" and self._peek(1) == "{"):
558
+ if c == "f":
559
+ self._take(1)
560
+ self._take(1) # consomme '{'
561
+ expr = self._scan_embedded_expr("}")
562
+ if self._peek() != "}":
563
+ self._error("'}' attendu pour fermer l'interpolation de "
564
+ "type de donnée")
565
+ self._take(1)
566
+ return "%s.dtype((%s))" % (RUNTIME_ALIAS, expr.strip())
567
+ if _name_start(c) or c == ":":
568
+ return self._take_pname(in_island=True)
569
+ self._error("type de donnée attendu après '^^'")
570
+
571
+ # ------------------------------------------------------------------
572
+ # NAME et déclencheurs d'îlots nominaux
573
+ # ------------------------------------------------------------------
574
+
575
+ def _handle_name(self):
576
+ t = self.text
577
+ m = _PYNAME_RE.match(t, self.i)
578
+ name = m.group(0)
579
+ nxt = t[m.end()] if m.end() < self.n else ""
580
+ operand_here = self.operand
581
+
582
+ # préfixe de chaîne ?
583
+ if name in STRING_PREFIXES and nxt in "\"'":
584
+ self._handle_string(m.end(), prefix_start=self.i)
585
+ return
586
+
587
+ if self.after_dot:
588
+ self._copy(len(name))
589
+ self.after_dot = False
590
+ self.operand = False
591
+ self.stmt_start = False
592
+ return
593
+
594
+ # import de préfixes (fiche 013) : from m import a, brick:, u: as v:
595
+ if name == "from" and self.stmt_start and not self._sub \
596
+ and self._import_has_prefix_item():
597
+ self._take_prefix_import()
598
+ return
599
+
600
+ # modificateurs de portée sur les déclarations d'îlot (fiche 018)
601
+ if name in ("global", "nonlocal") and self.stmt_start \
602
+ and not self._sub:
603
+ j = m.end()
604
+ while j < self.n and t[j] in " \t":
605
+ j += 1
606
+ if re.match(r"@(prefix|base|graph|bindings)\b", t[j:j + 10]):
607
+ self._close_copy()
608
+ self._take(m.end() - self.i) # le mot-clé
609
+ while self._peek() in " \t":
610
+ self._take(1)
611
+ if not self._try_prefix_or_base(scope_mode=name):
612
+ self._error("déclaration d'îlot attendue après "
613
+ "'%s @'" % name)
614
+ return
615
+
616
+ # bascule liaisons (fiche 017) : for @bindings [as b] in ...
617
+ if name == "for" and self.stmt_start and not self._sub:
618
+ j = m.end()
619
+ while j < self.n and t[j] in " \t":
620
+ j += 1
621
+ if t.startswith("@bindings", j) and (
622
+ j + 9 >= self.n or not _name_char(t[j + 9])):
623
+ self._for_bindings_island()
624
+ return
625
+
626
+ # __namespaces__ dans __all__ : casserait la sérialisation de
627
+ # l'importateur (fiche 013) — refusé explicitement.
628
+ if name == "__all__" and self.stmt_start and not self._sub:
629
+ stmt = self._stmt_text_ahead(m.end())
630
+ if "'__namespaces__'" in stmt or '"__namespaces__"' in stmt:
631
+ self._error("'__namespaces__' ne doit pas figurer dans "
632
+ "__all__ : il écraserait la table de préfixes "
633
+ "du module importateur (fiche 013)")
634
+
635
+ # îlots à délimiteur collé
636
+ if nxt == "{" and name in ("g", "f", "e", "s", "m"):
637
+ if name == "g":
638
+ self._graph_island()
639
+ return
640
+ if name == "s":
641
+ self._sparql_island()
642
+ return
643
+ if name == "m":
644
+ self._match_island()
645
+ return
646
+ if name == "f":
647
+ mark = self._begin_island()
648
+ gen = self._take_fnode()
649
+ self._end_island("fnode", mark, gen)
650
+ return
651
+ mark = self._begin_island()
652
+ gen = self._take_enode()
653
+ self._end_island("enode", mark, gen)
654
+ return
655
+ if nxt == "<" and name in ("f", "e") and operand_here:
656
+ if name == "e":
657
+ mark = self._begin_island()
658
+ gen = self._take_eiri()
659
+ self._end_island("eiri", mark, gen)
660
+ return
661
+ saved = (self.i, self.src_line, self.src_col)
662
+ mark = self._begin_island()
663
+ gen = self._try_take_firi()
664
+ if gen is not None:
665
+ self._end_island("firi", mark, gen)
666
+ return
667
+ self.i, self.src_line, self.src_col = saved # repli : comparaison
668
+
669
+ # nœud anonyme hors îlot (fiches 002 et 021) : `_:{expr}` a un sens
670
+ # partout — son identité vient de la valeur ; `_:label` n'en a pas,
671
+ # car un label ne dit la CO-RÉFÉRENCE que dans une portée, et la
672
+ # seule portée de labels du langage est l'îlot. Jusqu'ici les deux
673
+ # étaient recopiés tels quels et le module émis ne compilait pas,
674
+ # sans qu'aucune erreur ne soit levée.
675
+ if name == "_" and nxt == ":" and operand_here \
676
+ and self._bnode_free() and self._bnode_position():
677
+ after = t[m.end() + 1] if m.end() + 1 < self.n else ""
678
+ if after == "{":
679
+ mark = self._begin_island()
680
+ self._take(m.end() + 2 - self.i) # '_:' puis '{'
681
+ expr = self._scan_embedded_expr("}")
682
+ if self._peek() != "}":
683
+ self._error("'}' attendu pour fermer _:{...}")
684
+ self._take(1)
685
+ self._end_island("bnode", mark, "%s.bnode((%s))"
686
+ % (RUNTIME_ALIAS, expr.strip()))
687
+ self.operand = False
688
+ return
689
+ if _ix_start(after):
690
+ label = t[m.end() + 1:
691
+ _scan_pn_local_island(t, m.end() + 1, self.n)]
692
+ self._error(
693
+ "nœud anonyme '_:%s' hors d'un îlot : une étiquette ne "
694
+ "désigne la co-référence qu'à l'intérieur d'un g{ … }. "
695
+ "Écrire _:{%r} pour un nœud dont l'identité vient de la "
696
+ "valeur, ou %s.BNode() pour un nœud frais."
697
+ % (label, label, "ldpy"))
698
+
699
+ # pname hors îlot : préfixe déclaré, ':' collé, partie locale
700
+ if operand_here and nxt == ":" and name in self.prefixes \
701
+ and self._annotation_free():
702
+ after = t[m.end() + 1] if m.end() + 1 < self.n else ""
703
+ if _ix_start(after) or after == "{":
704
+ mark = self._begin_island()
705
+ gen = self._take_pname(in_island=False)
706
+ self._end_island("pname", mark, gen)
707
+ return
708
+ # préfixe sorti de portée : le texte reste du Python (R3), on avertit
709
+ if operand_here and nxt == ":" and name not in self.prefixes \
710
+ and name in self._retired:
711
+ after = t[m.end() + 1] if m.end() + 1 < self.n else ""
712
+ if _name_start(after) or after == "{":
713
+ self._warn("le préfixe '%s:' est hors de portée ici (sa "
714
+ "déclaration est dans un bloc terminé) ; le texte "
715
+ "est laissé tel quel" % name)
716
+
717
+ if name == "lambda":
718
+ self._lambda_depth = self.depth
719
+ elif name == "def":
720
+ self._after_def = True
721
+ self._copy(len(name))
722
+ if name in KEYWORDS:
723
+ self.operand = name not in VALUE_KEYWORDS
724
+ else:
725
+ self.operand = False
726
+ self.stmt_start = False
727
+
728
+ def _annotation_free(self):
729
+ """Sommes-nous ailleurs que dans une liste de paramètres ?
730
+
731
+ `lambda ex:ex` et `def f(ex:int = 0)` sont du Python parfaitement
732
+ valide, où le `:` appartient à Python. Ils ne figurent pas dans les
733
+ ambiguïtés assumées de la fiche 002 — et jusqu'ici le transpileur y
734
+ émettait du Python INVALIDE, sans lever d'erreur."""
735
+ return self._lambda_depth is None \
736
+ and (not self.brackets or self.brackets[-1] != "p")
737
+
738
+ def _bnode_free(self):
739
+ """Idem, mais un nœud anonyme n'a rien à faire non plus dans une
740
+ valeur par défaut annotée — inutile de distinguer 'p' de 'P'."""
741
+ return self._lambda_depth is None \
742
+ and (not self.brackets or self.brackets[-1] not in "pP")
743
+
744
+ def _bnode_position(self):
745
+ """Un `_:` peut-il être un nœud anonyme ICI ?
746
+
747
+ Non dans un dict/ensemble (`{_: v}`) ni dans un indice (`a[_:v]`) :
748
+ `_` est le nom jetable de Python, personne n'a « déclaré » ce
749
+ préfixe-là, et ces deux positions sont du Python parfaitement valide
750
+ que R3 oblige à laisser passer. C'est aussi ce que colorent les
751
+ grammaires (fiche 002 : pname/bnode absents des indices et des
752
+ dict/set)."""
753
+ return not self.brackets or self.brackets[-1] not in "{s"
754
+
755
+ # ------------------------------------------------------------------
756
+ # IRIs, pnames, f-IRIs, variables, fnodes (émission de termes)
757
+ # ------------------------------------------------------------------
758
+
759
+ def _resolve(self, iri):
760
+ if self.base and not _SCHEME_RE.match(iri):
761
+ return _urljoin(self.base, iri)
762
+ return iri
763
+
764
+ def _iriref_end(self, i):
765
+ """i sur '<'. Retourne l'indice après '>' ou None."""
766
+ t = self.text
767
+ j = i + 1
768
+ while j < self.n and _is_iri_char(t[j]):
769
+ j += 1
770
+ if j < self.n and t[j] == ">":
771
+ return j + 1
772
+ return None
773
+
774
+ def _take_iriref(self):
775
+ end = self._iriref_end(self.i)
776
+ if end is None:
777
+ j = self.i + 1
778
+ while j < self.n and self.text[j] not in "\n>":
779
+ j += 1
780
+ if "{" in self.text[self.i:j]:
781
+ self._error("interpolation dans une IRI : écrire f<...{expr}...> "
782
+ "(IRI formatée) et non <...{expr}...>")
783
+ self._error("IRI '<...>' non terminée")
784
+ return self._take(end - self.i)[1:-1]
785
+
786
+ def _try_iri_island(self):
787
+ end = self._iriref_end(self.i)
788
+ if end is None:
789
+ return False
790
+ mark = self._begin_island()
791
+ iri = self._take(end - self.i)[1:-1]
792
+ self._end_island("iri", mark,
793
+ "%s.URIRef(%r)" % (RUNTIME_ALIAS, self._resolve(iri)))
794
+ return True
795
+
796
+ def _try_take_firi(self):
797
+ """Sur 'f<'. Retourne l'expression générée ou None (repli)."""
798
+ t = self.text
799
+ # validation en avant : f< statiques* ( { ... } statiques* )* >
800
+ j = self.i + 2
801
+ while j < self.n and _is_iri_char(t[j]) and t[j] != "{":
802
+ j += 1
803
+ if j >= self.n or t[j] not in "{>":
804
+ return None
805
+ return self._take_firi()
806
+
807
+ def _take_firi_parts(self):
808
+ """Sur 'f<'. Consomme et retourne la liste de morceaux
809
+ (True, texte statique) | (False, expr transpilée)."""
810
+ self._take(2)
811
+ parts = []
812
+ static = []
813
+ while True:
814
+ c = self._peek()
815
+ if c == "":
816
+ self._error("f-IRI non terminée")
817
+ if c == ">":
818
+ self._take(1)
819
+ break
820
+ if c == "{":
821
+ self._take(1)
822
+ if static:
823
+ parts.append((True, "".join(static)))
824
+ static = []
825
+ expr = self._scan_embedded_expr("}")
826
+ if self._peek() != "}":
827
+ self._error("'}' attendu dans la f-IRI")
828
+ self._take(1)
829
+ parts.append((False, expr))
830
+ continue
831
+ if not _is_iri_char(c):
832
+ self._error("caractère %r interdit dans une f-IRI" % c)
833
+ static.append(self._take(1))
834
+ if static:
835
+ parts.append((True, "".join(static)))
836
+ return parts
837
+
838
+ def _firi_args(self, parts):
839
+ args = ", ".join(repr(p[1]) if p[0] else "(%s)" % p[1] for p in parts)
840
+ if self.base:
841
+ return "%s, base=%r" % (args, self.base)
842
+ return args
843
+
844
+ def _take_firi(self):
845
+ """Sur 'f<'. Consomme et retourne l'expression générée."""
846
+ parts = self._take_firi_parts()
847
+ if all(p[0] for p in parts): # aucune interpolation : statique
848
+ iri = "".join(p[1] for p in parts)
849
+ return "%s.URIRef(%r)" % (RUNTIME_ALIAS, self._resolve(iri))
850
+ return "%s.firi(%s)" % (RUNTIME_ALIAS, self._firi_args(parts))
851
+
852
+ def _take_fnode(self):
853
+ """Sur 'f{'. Consomme et retourne l'expression générée."""
854
+ self._take(2)
855
+ expr = self._scan_embedded_expr("}")
856
+ if self._peek() != "}":
857
+ self._error("'}' attendu pour fermer f{...}")
858
+ self._take(1)
859
+ return "%s.node((%s))" % (RUNTIME_ALIAS, expr.strip())
860
+
861
+ def _var_island(self):
862
+ mark = self._begin_island()
863
+ sigil = self._take(1) # ? ou $
864
+ if self._peek() == "{":
865
+ self._take(1)
866
+ expr = self._scan_embedded_expr("}")
867
+ if self._peek() != "}":
868
+ self._error("'}' attendu pour fermer ?{...}")
869
+ self._take(1)
870
+ self._end_island("fnode", mark,
871
+ "%s.node((%s))" % (RUNTIME_ALIAS, expr.strip()))
872
+ return
873
+ m = _NAME_RE.match(self.text, self.i)
874
+ if not m:
875
+ self._error("nom de variable attendu après '%s'" % sigil)
876
+ name = self._take(len(m.group(0)))
877
+ self._end_island("var", mark,
878
+ "%s.Variable(%r)" % (RUNTIME_ALIAS, name))
879
+
880
+ def _take_pname(self, in_island):
881
+ """Sur le début du préfixe (ou sur ':' si préfixe vide, en îlot).
882
+ Consomme `prefix:local` et retourne l'expression générée.
883
+ Hors îlot : partie locale = identifiant (+ interpolations {expr}).
884
+ En îlot : partie locale Turtle-like ([A-Za-z0-9_\\-.], sans '.' final)."""
885
+ t = self.text
886
+ if in_island:
887
+ end = _scan_pn_prefix(t, self.i, self.n)
888
+ else:
889
+ m = _PYNAME_RE.match(t, self.i)
890
+ end = m.end() if m else self.i
891
+ prefix = self._take(end - self.i) if end > self.i else ""
892
+ if self._peek() != ":":
893
+ self._error("':' attendu dans le nom préfixé")
894
+ self._take(1)
895
+ if prefix not in self.prefixes:
896
+ self._undeclared_prefix(prefix)
897
+ self._prefix_used.add(prefix)
898
+ ns = self.prefixes[prefix]
899
+ parts = []
900
+ static = []
901
+ while True:
902
+ c = self._peek()
903
+ if c == "{":
904
+ self._take(1)
905
+ if static:
906
+ parts.append((True, "".join(static)))
907
+ static = []
908
+ expr = self._scan_embedded_expr("}")
909
+ if self._peek() != "}":
910
+ self._error("'}' attendu dans le nom préfixé interpolé")
911
+ self._take(1)
912
+ parts.append((False, expr))
913
+ continue
914
+ if in_island:
915
+ # PN_LOCAL exact (chiffres en tête, tirets, points intérieurs)
916
+ ok = c != "" and (_pn_char(c) or c == "_")
917
+ if not ok and c == ".":
918
+ # '.' intérieur seulement (pas la ponctuation Turtle)
919
+ nc = self._peek(1)
920
+ ok = nc != "" and (_pn_char(nc) or nc in ".{")
921
+ else:
922
+ # intersection identifiant Python ∩ PN_CHARS
923
+ ok = c != "" and _ix_char(c)
924
+ if not ok:
925
+ break
926
+ static.append(self._take(1))
927
+ if static:
928
+ parts.append((True, "".join(static)))
929
+ if not parts and not in_island:
930
+ self._error("partie locale attendue après '%s:'" % prefix)
931
+ if isinstance(ns, DynPrefix):
932
+ # préfixe dynamique : résolution à l'exécution (fiche 013)
933
+ args = [ns.var]
934
+ for is_static, val in parts:
935
+ args.append(repr(val) if is_static else "(%s)" % val)
936
+ return "%s.pname(%s)" % (RUNTIME_ALIAS, ", ".join(args))
937
+ if all(p[0] for p in parts):
938
+ local = "".join(p[1] for p in parts)
939
+ return "%s.URIRef(%r)" % (RUNTIME_ALIAS, ns + local)
940
+ args = [repr(ns)]
941
+ for is_static, val in parts:
942
+ args.append(repr(val) if is_static else "(%s)" % val)
943
+ return "%s.firi(%s)" % (RUNTIME_ALIAS, ", ".join(args))
944
+
945
+ # ------------------------------------------------------------------
946
+ # import de préfixes (fiche 013)
947
+ # ------------------------------------------------------------------
948
+
949
+ def _stmt_text_ahead(self, j):
950
+ """Texte de l'instruction logique à partir de j (sans consommer)."""
951
+ t = self.text
952
+ depth = 0
953
+ k = j
954
+ while k < self.n:
955
+ c = t[k]
956
+ if c in "([{":
957
+ depth += 1
958
+ elif c in ")]}":
959
+ depth -= 1
960
+ elif c == "\\" and k + 1 < self.n and t[k + 1] == "\n":
961
+ k += 2
962
+ continue
963
+ elif depth <= 0 and c in "\n;#":
964
+ break
965
+ k += 1
966
+ return t[j:k]
967
+
968
+ def _import_has_prefix_item(self):
969
+ """Sur 'from' en début d'instruction : la liste d'import
970
+ contient-elle un nom préfixé (un ':' après le mot-clé import) ?
971
+
972
+ Les commentaires sont retirés d'abord : une liste parenthésée les
973
+ admet, et `# noqa: F401` porte un ':' sans qu'aucun préfixe ne soit
974
+ importé. Une liste d'import ne peut pas contenir de littéral chaîne,
975
+ donc retirer `#`→fin de ligne suffit. Hors commentaire, un ':' dans
976
+ une liste d'import n'est jamais du Python légal."""
977
+ stmt = _COMMENT_RE.sub("", self._stmt_text_ahead(self.i))
978
+ m = re.search(r"\bimport\b", stmt)
979
+ return m is not None and ":" in stmt[m.end():]
980
+
981
+ def _take_prefix_import(self):
982
+ """Consomme toute l'instruction from-import (self.i sur 'from') et
983
+ émet l'import Python + la liaison des préfixes (fiche 013)."""
984
+ decl_col = self.src_col
985
+ decl_line = self.src_line
986
+ mark = self._begin_island()
987
+ t = self.text
988
+ start_i = self.i
989
+ self._take(4) # 'from'
990
+ self._g_ws()
991
+ mod_start = self.i
992
+ while self.i < self.n and not t.startswith("import", self.i):
993
+ if t[self.i] == "\n":
994
+ self._error("mot-clé 'import' attendu dans l'instruction from")
995
+ self._take(1)
996
+ module = t[mod_start:self.i].strip()
997
+ if not module:
998
+ self._error("nom de module attendu après 'from'")
999
+ self._take(6) # 'import'
1000
+ self._imp_ws(paren=False)
1001
+ paren = False
1002
+ if self._peek() == "(":
1003
+ self._take(1)
1004
+ paren = True
1005
+ self._imp_ws(paren)
1006
+ py_items = []
1007
+ prefix_items = [] # (source, cible)
1008
+ while True:
1009
+ c = self._peek()
1010
+ if c == "":
1011
+ self._error("liste d'import non terminée")
1012
+ if c == "*":
1013
+ self._take(1)
1014
+ py_items.append("*")
1015
+ else:
1016
+ start = self.i
1017
+ pn_end = _scan_pn_prefix(t, self.i, self.n)
1018
+ pym = _PYNAME_RE.match(t, self.i)
1019
+ if pn_end > self.i and pn_end < self.n and t[pn_end] == ":":
1020
+ name = self._take(pn_end - self.i)
1021
+ self._take(1) # ':'
1022
+ alias = None
1023
+ save = (self.i, self.src_line, self.src_col)
1024
+ self._imp_ws(paren)
1025
+ if t.startswith("as", self.i) and not _name_char(
1026
+ self._peek(2)):
1027
+ self._take(2)
1028
+ self._imp_ws(paren)
1029
+ a_end = _scan_pn_prefix(t, self.i, self.n)
1030
+ if a_end == self.i:
1031
+ self._error("nom de préfixe attendu après 'as'")
1032
+ alias = self._take(a_end - self.i)
1033
+ if self._peek() != ":":
1034
+ self._error("l'alias d'un préfixe s'écrit "
1035
+ "'%s:' — avec le ':'" % alias)
1036
+ self._take(1)
1037
+ else:
1038
+ self.i, self.src_line, self.src_col = save
1039
+ prefix_items.append((name, alias or name))
1040
+ elif pym:
1041
+ name = self._take(pym.end() - self.i)
1042
+ item = name
1043
+ save = (self.i, self.src_line, self.src_col)
1044
+ self._imp_ws(paren)
1045
+ if t.startswith("as", self.i) and not _name_char(
1046
+ self._peek(2)):
1047
+ self._take(2)
1048
+ self._imp_ws(paren)
1049
+ am = _PYNAME_RE.match(t, self.i)
1050
+ if not am:
1051
+ self._error("nom attendu après 'as'")
1052
+ item += " as " + self._take(am.end() - self.i)
1053
+ else:
1054
+ self.i, self.src_line, self.src_col = save
1055
+ py_items.append(item)
1056
+ else:
1057
+ self._error("élément d'import attendu")
1058
+ self._imp_ws(paren)
1059
+ if self._peek() == ",":
1060
+ self._take(1)
1061
+ self._imp_ws(paren)
1062
+ if paren and self._peek() == ")":
1063
+ self._take(1)
1064
+ break
1065
+ continue
1066
+ if paren:
1067
+ if self._peek() != ")":
1068
+ self._error("')' attendu dans la liste d'import")
1069
+ self._take(1)
1070
+ break
1071
+ # liaison lexicale des préfixes importés (portée par bloc)
1072
+ self._ns_counter += 1
1073
+ nsvar = "_ldpy_nsi%d" % self._ns_counter
1074
+ binds = []
1075
+ updates = []
1076
+ for source, target in prefix_items:
1077
+ if target in self.prefixes and target in self._prefix_used \
1078
+ and decl_col <= self._prefix_col.get(target, 0):
1079
+ self._warn("redéclaration du préfixe '%s:' après usage "
1080
+ "(import de %s)" % (target, module))
1081
+ if decl_col > 0:
1082
+ self._scope_stack.append((decl_col, "prefix", target,
1083
+ target in self.prefixes,
1084
+ (self.prefixes.get(target),
1085
+ self._prefix_col.get(target))))
1086
+ var = self._fresh_ns_var(target)
1087
+ self.prefixes[target] = DynPrefix(var)
1088
+ self._prefix_col[target] = decl_col
1089
+ binds.append("%s = %s[%r]" % (var, nsvar, source))
1090
+ updates.append("%r: %s" % (target, var))
1091
+ if not prefix_items:
1092
+ # aucun préfixe finalement : l'instruction est du Python ordinaire
1093
+ # et doit ressortir telle quelle (transparence de l'hôte, R3).
1094
+ self._put(t[start_i:self.i])
1095
+ if not self._sub:
1096
+ sl, sc, gl, gc = mark
1097
+ self.map.add("copy", (sl, sc, self.src_line, self.src_col),
1098
+ (gl, gc, self.gen_line, self.gen_col))
1099
+ self.operand = False
1100
+ self.stmt_start = False
1101
+ return
1102
+ self._import_lines.append((decl_line, module))
1103
+ items = py_items + ["__namespaces__ as %s" % nsvar]
1104
+ gen = "; ".join(["from %s import %s" % (module, ", ".join(items))]
1105
+ + binds
1106
+ + ["__namespaces__.update({%s})" % ", ".join(updates)])
1107
+ self._end_island("import", mark, gen)
1108
+
1109
+ def _imp_ws(self, paren):
1110
+ """Blancs dans une liste d'import : continuations '\\'-newline
1111
+ partout, newlines et commentaires seulement entre parenthèses."""
1112
+ t = self.text
1113
+ while self.i < self.n:
1114
+ c = t[self.i]
1115
+ if c in " \t\r":
1116
+ self._take(1)
1117
+ elif c == "\\" and self._peek(1) == "\n":
1118
+ self._take(2)
1119
+ elif paren and c == "\n":
1120
+ self._take(1)
1121
+ elif paren and c == "#":
1122
+ j = t.find("\n", self.i)
1123
+ self._take((j if j != -1 else self.n) - self.i)
1124
+ else:
1125
+ break
1126
+
1127
+ # ------------------------------------------------------------------
1128
+ # @prefix / @base
1129
+ # ------------------------------------------------------------------
1130
+
1131
+ def _try_prefix_or_base(self, scope_mode=None):
1132
+ """Sur '@' en début d'instruction. Tente l'îlot déclaration.
1133
+ Retourne True si consommé (sinon rien n'est consommé).
1134
+ scope_mode : None, 'global' ou 'nonlocal' (fiche 018)."""
1135
+ t = self.text
1136
+ m = re.match(r"@(prefix|base|graph|bindings)\b", t[self.i:self.i + 10])
1137
+ if not m:
1138
+ return False
1139
+ kind = m.group(1)
1140
+ if kind == "graph":
1141
+ return self._try_graph_decl(scope_mode)
1142
+ if kind == "bindings":
1143
+ return self._try_bindings_decl(scope_mode)
1144
+ # une « déclaration ratée » (préfixe non ASCII, ponctuation absente…)
1145
+ # ne doit PAS retomber silencieusement sur le cas décorateur : la fin
1146
+ # de ligne déclencherait des îlots et produirait du code massacré.
1147
+ eol = t.find("\n", self.i)
1148
+ line_rest = t[self.i:eol if eol != -1 else self.n]
1149
+ looks_like_decl = re.search(r"<[^<>\s]*>\s*\.\s*$", line_rest)
1150
+ # validation en avant (sans consommer)
1151
+ j = self.i + 1 + len(kind)
1152
+ j = self._skip_ws_ahead(j)
1153
+ prefix = None
1154
+ if kind == "prefix":
1155
+ jend = _scan_pn_prefix(t, j, self.n)
1156
+ prefix = t[j:jend]
1157
+ j = jend
1158
+ if j >= self.n or t[j] != ":":
1159
+ if looks_like_decl:
1160
+ self._error("déclaration @prefix invalide — le nom de "
1161
+ "préfixe doit suivre PN_PREFIX de Turtle "
1162
+ "(docs/reference/language.md)")
1163
+ return False # décorateur nommé prefix
1164
+ j = self._skip_ws_ahead(j + 1)
1165
+ if kind == "prefix" and t[j:j + 2] == "f<":
1166
+ # IRI calculée : préfixe dynamique (fiche 013). `@prefix ex: f<`
1167
+ # n'est jamais un début de ligne Python valide : on s'engage.
1168
+ return self._prefix_firi_decl(j, prefix, scope_mode)
1169
+ if j >= self.n or t[j] != "<":
1170
+ if looks_like_decl:
1171
+ self._error("déclaration @%s invalide — IRI '<...>' attendue"
1172
+ % kind)
1173
+ return False
1174
+ end = self._iriref_end(j)
1175
+ if end is None:
1176
+ return False
1177
+ k = self._skip_ws_ahead(end)
1178
+ if k >= self.n or t[k] != ".":
1179
+ return False
1180
+ # consommation effective
1181
+ decl_col = self.src_col # indentation de la déclaration
1182
+ mark = self._begin_island()
1183
+ iri = t[j + 1:end - 1]
1184
+ self._take(k + 1 - self.i)
1185
+ resolved = self._resolve(iri)
1186
+ if kind == "base":
1187
+ self._scope_install("base", None, resolved, decl_col, scope_mode)
1188
+ gen = "__base__ = %r" % resolved
1189
+ if scope_mode:
1190
+ gen = "%s __base__; %s" % (scope_mode, gen)
1191
+ else:
1192
+ # redéclaration au même niveau après usage : warning ;
1193
+ # shadowing dans un bloc plus profond : légitime, silencieux.
1194
+ if scope_mode is None and prefix in self.prefixes \
1195
+ and prefix in self._prefix_used \
1196
+ and self.prefixes[prefix] != resolved \
1197
+ and decl_col <= self._prefix_col.get(prefix, 0):
1198
+ # un global/nonlocal est une réassignation voulue : pas
1199
+ # d'avertissement (fiche 018, « comme global x; x = 1 »)
1200
+ self._warn("redéclaration du préfixe '%s:' après usage "
1201
+ "(nouvelle IRI : %s)" % (prefix, resolved))
1202
+ self._scope_install("prefix", prefix, resolved, decl_col,
1203
+ scope_mode)
1204
+ gen = "__namespaces__[%r] = %s.Namespace(%r)" % (
1205
+ prefix, RUNTIME_ALIAS, resolved)
1206
+ self._end_island(kind, mark, gen)
1207
+ return True
1208
+
1209
+ def _prefix_firi_decl(self, j, prefix, scope_mode=None):
1210
+ """Consomme `@prefix p: f<...> .` (self.i sur '@', j sur 'f').
1211
+ Sans interpolation : déclaration statique ordinaire. Avec : préfixe
1212
+ dynamique, résolu à l'exécution par une variable fraîche."""
1213
+ decl_col = self.src_col
1214
+ mark = self._begin_island()
1215
+ self._take(j - self.i) # '@prefix p:' + blancs
1216
+ parts = self._take_firi_parts()
1217
+ self._g_ws()
1218
+ if self._peek() != ".":
1219
+ self._error("'.' attendu pour clore la déclaration @prefix")
1220
+ self._take(1)
1221
+ if scope_mode is None and prefix in self.prefixes \
1222
+ and prefix in self._prefix_used \
1223
+ and decl_col <= self._prefix_col.get(prefix, 0):
1224
+ prev = self.prefixes[prefix]
1225
+ static_same = (all(p[0] for p in parts) and isinstance(prev, str)
1226
+ and prev == self._resolve(
1227
+ "".join(p[1] for p in parts)))
1228
+ if not static_same:
1229
+ self._warn("redéclaration du préfixe '%s:' après usage"
1230
+ % prefix)
1231
+ if all(p[0] for p in parts): # aucune interpolation : statique
1232
+ resolved = self._resolve("".join(p[1] for p in parts))
1233
+ self._scope_install("prefix", prefix, resolved, decl_col,
1234
+ scope_mode)
1235
+ gen = "__namespaces__[%r] = %s.Namespace(%r)" % (
1236
+ prefix, RUNTIME_ALIAS, resolved)
1237
+ else:
1238
+ # global/nonlocal : réutiliser la variable de la liaison cible si
1239
+ # elle est dynamique — c'est elle que les usages du dehors lisent
1240
+ prev = self.prefixes.get(prefix)
1241
+ if scope_mode and isinstance(prev, DynPrefix):
1242
+ var = prev.var
1243
+ else:
1244
+ var = self._fresh_ns_var(prefix)
1245
+ self._scope_install("prefix", prefix, DynPrefix(var), decl_col,
1246
+ scope_mode)
1247
+ gen = "%s = %s.Namespace(%s.firi(%s)); __namespaces__[%r] = %s" % (
1248
+ var, RUNTIME_ALIAS, RUNTIME_ALIAS,
1249
+ self._firi_args(parts), prefix, var)
1250
+ if scope_mode:
1251
+ gen = "%s %s; %s" % (scope_mode, var, gen)
1252
+ self._end_island("prefix", mark, gen)
1253
+ return True
1254
+
1255
+ def _scope_install(self, kind, name, value, decl_col, mode):
1256
+ """Installe une liaison de déclaration d'îlot (fiches 004/018).
1257
+
1258
+ mode None : portée par bloc (pile) ; 'global' : portée module, les
1259
+ entrées de pile restaureront la nouvelle valeur ; 'nonlocal' : la
1260
+ portée englobante déclarante la plus proche, erreur s'il n'y en a
1261
+ pas — la sémantique de Python, appliquée aux déclarations."""
1262
+ def setval(v):
1263
+ if kind == "prefix":
1264
+ self.prefixes[name] = v
1265
+ elif kind == "base":
1266
+ self.base = v
1267
+ elif kind == "graph":
1268
+ self._graph_var = v
1269
+ else:
1270
+ self._bindings_var = v
1271
+
1272
+ if mode is None:
1273
+ if decl_col > 0:
1274
+ if kind == "prefix":
1275
+ self._scope_stack.append(
1276
+ (decl_col, kind, name, name in self.prefixes,
1277
+ (self.prefixes.get(name),
1278
+ self._prefix_col.get(name))))
1279
+ elif kind == "base":
1280
+ self._scope_stack.append((decl_col, kind, None,
1281
+ True, self.base))
1282
+ elif kind == "graph":
1283
+ self._scope_stack.append((decl_col, kind, None,
1284
+ True, self._graph_var))
1285
+ else:
1286
+ self._scope_stack.append((decl_col, kind, None,
1287
+ True, self._bindings_var))
1288
+ setval(value)
1289
+ if kind == "prefix":
1290
+ self._prefix_col[name] = decl_col
1291
+ return
1292
+ matches = [i for i, e in enumerate(self._scope_stack)
1293
+ if e[1] == kind and (kind != "prefix" or e[2] == name)]
1294
+ if mode == "nonlocal":
1295
+ encl = [i for i in matches
1296
+ if self._scope_stack[i][0] < decl_col]
1297
+ if not encl:
1298
+ what = "prefix %s:" % name if kind == "prefix" else kind
1299
+ self._error("nonlocal @%s : aucune déclaration englobante "
1300
+ "(comme le nonlocal de Python)" % what)
1301
+ keep = encl[-1]
1302
+ target_col = self._scope_stack[keep][0]
1303
+ touched = [i for i in matches if i > keep]
1304
+ else: # global
1305
+ keep = None
1306
+ target_col = 0
1307
+ touched = matches
1308
+ for i in touched:
1309
+ col, k, n, _, _ = self._scope_stack[i]
1310
+ newprev = ((value, target_col) if kind == "prefix" else value)
1311
+ self._scope_stack[i] = (col, k, n, True, newprev)
1312
+ setval(value)
1313
+ if kind == "prefix":
1314
+ self._prefix_col[name] = target_col
1315
+
1316
+ def _enclosing_value(self, kind, name, decl_col):
1317
+ """Valeur (variable émise) visible dans la portée englobante
1318
+ déclarante la plus proche — la cible d'un nonlocal (fiche 018).
1319
+ None si aucune déclaration englobante."""
1320
+ matches = [i for i, e in enumerate(self._scope_stack)
1321
+ if e[1] == kind and (kind != "prefix" or e[2] == name)]
1322
+ encl = [i for i in matches if self._scope_stack[i][0] < decl_col]
1323
+ if not encl:
1324
+ return None
1325
+ above = [i for i in matches if i > encl[-1]]
1326
+ if above:
1327
+ prev = self._scope_stack[above[0]][4]
1328
+ return prev[0] if kind == "prefix" else prev
1329
+ if kind == "prefix":
1330
+ return self.prefixes.get(name)
1331
+ if kind == "graph":
1332
+ return self._graph_var
1333
+ return self._bindings_var
1334
+
1335
+ def _unwind_scopes(self, col):
1336
+ """Ferme les portées des déclarations plus indentées que l'instruction
1337
+ qui commence à la colonne `col` (fin de leur bloc)."""
1338
+ while self._scope_stack and col < self._scope_stack[-1][0]:
1339
+ _, kind, name, had, prev = self._scope_stack.pop()
1340
+ if kind == "base":
1341
+ self.base = prev
1342
+ elif kind == "graph":
1343
+ self._graph_var = prev
1344
+ elif kind == "bindings":
1345
+ self._bindings_var = prev
1346
+ elif had:
1347
+ self.prefixes[name], self._prefix_col[name] = prev
1348
+ else:
1349
+ self.prefixes.pop(name, None)
1350
+ self._prefix_col.pop(name, None)
1351
+ self._retired.setdefault(name, self.src_line)
1352
+
1353
+ def _fresh_ns_var(self, name):
1354
+ """Variable Python fraîche portant un namespace dynamique."""
1355
+ self._ns_counter += 1
1356
+ safe = "".join(c if c.isascii() and (c.isalnum() or c == "_") else "_"
1357
+ for c in name)
1358
+ return "_ldpy_ns_%s_%d" % (safe, self._ns_counter)
1359
+
1360
+ def _undeclared_prefix(self, prefix):
1361
+ msg = "préfixe non déclaré : '%s:'" % prefix
1362
+ if self._import_lines:
1363
+ hints = ", ".join("ligne %d (from %s import ...)" % (ln + 1, mod)
1364
+ for ln, mod in self._import_lines)
1365
+ msg += (" — peut-être manque-t-il à une liste d'import de "
1366
+ "préfixes : %s" % hints)
1367
+ self._error(msg)
1368
+
1369
+ def _skip_ws_ahead(self, j):
1370
+ t = self.text
1371
+ while j < self.n and t[j] in " \t\r\n":
1372
+ j += 1
1373
+ return j
1374
+
1375
+ # ------------------------------------------------------------------
1376
+ # graphe courant : @graph, +{ }, -{ } (fiche 014)
1377
+ # ------------------------------------------------------------------
1378
+
1379
+ def _fresh_var(self, stem):
1380
+ self._ns_counter += 1
1381
+ return "_ldpy_%s%d" % (stem, self._ns_counter)
1382
+
1383
+ def _try_graph_decl(self, scope_mode=None):
1384
+ """Sur '@' devant 'graph' en début d'instruction. Désigne ou crée le
1385
+ graphe courant. Un décorateur reste un décorateur : '@graph' suivi de
1386
+ '(', '.', '[', d'une fin de ligne — ou d'une parenthèse après blancs —
1387
+ n'est pas un îlot."""
1388
+ t = self.text
1389
+ j = self.i + 6 # après '@graph'
1390
+ if j < self.n and t[j] not in " \t":
1391
+ return False # @graph( . [ … décorateur
1392
+ while j < self.n and t[j] in " \t":
1393
+ j += 1
1394
+ if j >= self.n or t[j] in "\n\r#(":
1395
+ return False # décorateur nu (ou appel)
1396
+ decl_col = self.src_col
1397
+ mark = self._begin_island()
1398
+ self._take(j - self.i) # '@graph' + blancs
1399
+ user_name = None # nom écrit après 'as'
1400
+ if t.startswith("as", self.i) and not _name_char(self._peek(2)):
1401
+ self._take(2)
1402
+ user_name = self._graph_decl_as_name()
1403
+ rhs = "%s.new_graph(__namespaces__, %s)" % (
1404
+ RUNTIME_ALIAS, repr(self.base) if self.base else "None")
1405
+ else:
1406
+ ident = None
1407
+ c = self._peek()
1408
+ if c == "<" and self._iriref_end(self.i) is not None:
1409
+ iri = self._take_iriref()
1410
+ ident = "%s.URIRef(%r)" % (RUNTIME_ALIAS, self._resolve(iri))
1411
+ elif c == "f" and self._peek(1) == "<":
1412
+ ident = self._take_firi()
1413
+ else:
1414
+ m2 = _PYNAME_RE.match(t, self.i)
1415
+ if m2 and m2.group(0) in self.prefixes \
1416
+ and t[m2.end():m2.end() + 1] == ":":
1417
+ ident = self._take_pname(in_island=False)
1418
+ if ident is not None:
1419
+ self._graph_ws_inline()
1420
+ if not (t.startswith("as", self.i)
1421
+ and not _name_char(self._peek(2))):
1422
+ self._error("'as' attendu : '@graph <iri> as g' crée un "
1423
+ "graphe nommé ; pour désigner un graphe "
1424
+ "existant, écrire '@graph expression'")
1425
+ self._take(2)
1426
+ user_name = self._graph_decl_as_name()
1427
+ rhs = "%s.new_graph(__namespaces__, %s, identifier=%s)" % (
1428
+ RUNTIME_ALIAS,
1429
+ repr(self.base) if self.base else "None", ident)
1430
+ else:
1431
+ expr = self._scan_embedded_expr("\n#").strip()
1432
+ if not expr:
1433
+ self._error("expression attendue après '@graph'")
1434
+ if "\n" in expr:
1435
+ self._error("l'expression de '@graph' tient sur sa ligne")
1436
+ rhs = "(%s)" % expr
1437
+ self._graph_ws_inline()
1438
+ if self._peek() not in "\n\r#;" and self._peek() != "":
1439
+ self._error("fin de ligne attendue après la déclaration @graph")
1440
+ gvar, gen = self._compose_decl("graph", None, user_name, rhs,
1441
+ decl_col, scope_mode, "g")
1442
+ self._scope_install("graph", None, gvar, decl_col, scope_mode)
1443
+ self._end_island("graph-decl", mark, gen)
1444
+ return True
1445
+
1446
+ def _compose_decl(self, kind, name, user_name, rhs, decl_col, mode, stem):
1447
+ """Compose l'émission d'une déclaration @graph/@bindings selon la
1448
+ portée visée (fiche 018) : nonlocal réutilise la variable de la
1449
+ liaison englobante — c'est elle que le code du dehors lit."""
1450
+ if mode == "nonlocal":
1451
+ target = self._enclosing_value(kind, name, decl_col)
1452
+ if target is None:
1453
+ # _scope_install produira l'erreur avec le bon message
1454
+ self._scope_install(kind, name, None, decl_col, mode)
1455
+ lhs = ([user_name, target] if user_name and user_name != target
1456
+ else [target])
1457
+ return target, "nonlocal %s; %s = %s" % (
1458
+ target, " = ".join(lhs), rhs)
1459
+ var = user_name if user_name else self._fresh_var(stem)
1460
+ gen = "%s = %s" % (var, rhs)
1461
+ if mode == "global":
1462
+ gen = "global %s; %s" % (var, gen)
1463
+ return var, gen
1464
+
1465
+ def _graph_decl_as_name(self):
1466
+ """Après 'as' : le nom Python créé par la déclaration."""
1467
+ self._graph_ws_inline()
1468
+ m = _PYNAME_RE.match(self.text, self.i)
1469
+ if not m:
1470
+ self._error("nom attendu après 'as'")
1471
+ return self._take(m.end() - self.i)
1472
+
1473
+ def _graph_ws_inline(self):
1474
+ while self._peek() in " \t":
1475
+ self._take(1)
1476
+
1477
+ def _addremove_island(self, sign):
1478
+ """Sur '+{' ou '-{' en position d'instruction : ajout ou retrait
1479
+ sur le graphe courant (fiche 014), ou sur le contexte donné par le
1480
+ suffixe d'appel — graphe d'abord, binding ensuite (fiche 019)."""
1481
+ mark = self._begin_island()
1482
+ self._take(2) # signe + '{'
1483
+ triples, gctx = self._g_parse_triples()
1484
+ triples = _share_impure(triples, gctx)
1485
+ sfx_graph = sfx_bindings = None
1486
+ if self._peek() == "(": # adjacence stricte (R2)
1487
+ sfx_graph, sfx_bindings = self._call_suffix()
1488
+ gvar = sfx_graph if sfx_graph else self._graph_var
1489
+ if gvar is None:
1490
+ self._error("'%s{ ... }' sans graphe courant : déclarez-le avec "
1491
+ "'@graph <expression>' ou '@graph as g' (fiche 014), "
1492
+ "ou donnez-le en suffixe — %s{ ... }(g)"
1493
+ % (sign, sign))
1494
+ bvar = sfx_bindings if sfx_bindings else self._bindings_var
1495
+ fn = "add_to" if sign == "+" else "remove_from"
1496
+ args = [gvar]
1497
+ args += ["(%s, %s, %s)" % tr for tr in triples]
1498
+ gen = "%s.%s(%s%s)" % (RUNTIME_ALIAS, fn, ", ".join(args),
1499
+ ", bindings=%s" % bvar if bvar else "")
1500
+ self._end_island("addto" if sign == "+" else "removefrom", mark, gen)
1501
+
1502
+ def _call_suffix(self):
1503
+ """Sur '(' collé après un îlot-instruction : la liste d'opérandes
1504
+ de contexte — (graphe), (graphe, binding), (bindings=binding)."""
1505
+ self._take(1)
1506
+ self._g_ws()
1507
+ graph_expr = bindings_expr = None
1508
+ if self._peek() == ")":
1509
+ self._take(1)
1510
+ return None, None
1511
+ if self._suffix_bindings_kw():
1512
+ bindings_expr = "(%s)" % self._scan_embedded_expr(")").strip()
1513
+ else:
1514
+ graph_expr = "(%s)" % self._scan_embedded_expr(",)").strip()
1515
+ if self._peek() == ",":
1516
+ self._take(1)
1517
+ self._g_ws()
1518
+ self._suffix_bindings_kw() # 'bindings=' optionnel
1519
+ bindings_expr = "(%s)" % self._scan_embedded_expr(")").strip()
1520
+ if self._peek() != ")":
1521
+ self._error("')' attendu pour clore le suffixe d'appel")
1522
+ self._take(1)
1523
+ return graph_expr, bindings_expr
1524
+
1525
+ def _suffix_bindings_kw(self):
1526
+ """Consomme 'bindings=' s'il est là (et pas 'bindings==')."""
1527
+ t = self.text
1528
+ j = self.i
1529
+ if not t.startswith("bindings", j):
1530
+ return False
1531
+ j += 8
1532
+ while j < self.n and t[j] in " \t":
1533
+ j += 1
1534
+ if j >= self.n or t[j] != "=" or t[j + 1:j + 2] == "=":
1535
+ return False
1536
+ self._take(j + 1 - self.i)
1537
+ self._g_ws()
1538
+ return True
1539
+
1540
+ # ------------------------------------------------------------------
1541
+ # binding courant : @bindings, for @bindings in (fiche 017)
1542
+ # ------------------------------------------------------------------
1543
+
1544
+ def _bkw(self):
1545
+ """Suffixe d'argument bindings= pour les îlots consommateurs."""
1546
+ if self._bindings_var:
1547
+ return ", bindings=%s" % self._bindings_var
1548
+ return ""
1549
+
1550
+ def _try_bindings_decl(self, scope_mode=None):
1551
+ """Sur '@' devant 'bindings' en début d'instruction. Désigne ou
1552
+ crée le binding courant — le parallèle exact de @graph."""
1553
+ t = self.text
1554
+ j = self.i + 9 # après '@bindings'
1555
+ if j < self.n and t[j] not in " \t":
1556
+ return False
1557
+ while j < self.n and t[j] in " \t":
1558
+ j += 1
1559
+ if j >= self.n or t[j] in "\n\r#(":
1560
+ return False
1561
+ decl_col = self.src_col
1562
+ mark = self._begin_island()
1563
+ self._take(j - self.i)
1564
+ user_name = None
1565
+ if t.startswith("as", self.i) and not _name_char(self._peek(2)):
1566
+ self._take(2)
1567
+ user_name = self._graph_decl_as_name()
1568
+ rhs = "%s.Bindings()" % RUNTIME_ALIAS
1569
+ else:
1570
+ expr = self._scan_embedded_expr("\n#").strip()
1571
+ if not expr:
1572
+ self._error("expression attendue après '@bindings'")
1573
+ if "\n" in expr:
1574
+ self._error("l'expression de '@bindings' tient sur sa ligne")
1575
+ rhs = "(%s)" % expr
1576
+ self._graph_ws_inline()
1577
+ if self._peek() not in "\n\r#;" and self._peek() != "":
1578
+ self._error("fin de ligne attendue après la déclaration "
1579
+ "@bindings")
1580
+ bvar, gen = self._compose_decl("bindings", None, user_name, rhs,
1581
+ decl_col, scope_mode, "b")
1582
+ self._scope_install("bindings", None, bvar, decl_col, scope_mode)
1583
+ self._end_island("bindings-decl", mark, gen)
1584
+ return True
1585
+
1586
+ def _for_bindings_island(self):
1587
+ """Sur 'for' suivi de '@bindings' : la bascule graphes -> liaisons.
1588
+ `for @bindings [as b] in ITER:` — chaque élément de l'itérable
1589
+ devient le binding courant du corps de la boucle (fiche 017)."""
1590
+ decl_col = self.src_col
1591
+ mark = self._begin_island()
1592
+ t = self.text
1593
+ self._take(3) # 'for'
1594
+ self._graph_ws_inline()
1595
+ self._take(9) # '@bindings'
1596
+ self._graph_ws_inline()
1597
+ if t.startswith("as", self.i) and not _name_char(self._peek(2)):
1598
+ self._take(2)
1599
+ bvar = self._graph_decl_as_name()
1600
+ self._graph_ws_inline()
1601
+ else:
1602
+ bvar = self._fresh_var("b")
1603
+ if not (t.startswith("in", self.i) and not _name_char(self._peek(2))):
1604
+ self._error("'in' attendu dans 'for @bindings [as b] in ...'")
1605
+ self._take(2)
1606
+ self._end_island("for-bindings", mark,
1607
+ "for %s in %s.as_bindings_iter(" % (
1608
+ bvar, RUNTIME_ALIAS))
1609
+ # l'itérable : scan normal (îlots inclus), jusqu'au ':' du for
1610
+ self._scan(stops=":", entry_depth=self.depth)
1611
+ if self._peek() != ":":
1612
+ self._error("':' attendu pour clore l'en-tête du for")
1613
+ self._close_copy()
1614
+ mark2 = self._begin_island()
1615
+ self._take(1)
1616
+ self._end_island("for-bindings-close", mark2, "):")
1617
+ # portée : le corps de la boucle
1618
+ self._scope_stack.append((decl_col + 1, "bindings", None,
1619
+ True, self._bindings_var))
1620
+ self._bindings_var = bvar
1621
+
1622
+ # ------------------------------------------------------------------
1623
+ # graphes g{ ... }
1624
+ # ------------------------------------------------------------------
1625
+
1626
+ def _g_parse_triples(self):
1627
+ """Corps d'îlot de graphe : self.i juste après le '{' ouvrant.
1628
+ Consomme jusqu'au '}' fermant inclus ; retourne (triples, gctx)."""
1629
+ gctx = _GraphCtx()
1630
+ triples = []
1631
+ self._g_ws()
1632
+ while self._peek() != "}":
1633
+ if self._peek() == "":
1634
+ self._error("'}' attendu pour fermer l'îlot de graphe")
1635
+ self._g_triples(triples, gctx)
1636
+ self._g_ws()
1637
+ if self._peek() == ".":
1638
+ self._take(1)
1639
+ self._g_ws()
1640
+ continue
1641
+ break
1642
+ if self._peek() != "}":
1643
+ self._error("'}' attendu pour fermer l'îlot de graphe")
1644
+ self._take(1)
1645
+ return triples, gctx
1646
+
1647
+ def _graph_island(self):
1648
+ mark = self._begin_island()
1649
+ self._take(2) # g{
1650
+ triples, gctx = self._g_parse_triples()
1651
+ triples = _share_impure(triples, gctx)
1652
+ base_repr = repr(self.base) if self.base else "None"
1653
+ args = ["__namespaces__", base_repr]
1654
+ args += ["(%s, %s, %s)" % tr for tr in triples]
1655
+ gen = "%s.graph(%s%s)" % (RUNTIME_ALIAS, ", ".join(args),
1656
+ self._bkw())
1657
+ self._end_island("graph", mark, gen)
1658
+
1659
+ def _g_ws(self):
1660
+ t = self.text
1661
+ while self.i < self.n:
1662
+ c = t[self.i]
1663
+ if c in " \t\r\n":
1664
+ self._take(1)
1665
+ elif c == "#":
1666
+ j = t.find("\n", self.i)
1667
+ self._take((j if j != -1 else self.n) - self.i)
1668
+ else:
1669
+ break
1670
+
1671
+ def _g_triples(self, triples, gctx):
1672
+ subj, is_composite = self._g_node(triples, gctx)
1673
+ self._g_ws()
1674
+ if self._peek() in "}.;," or self._peek() == "":
1675
+ if not is_composite:
1676
+ self._error("liste de propriétés attendue après le sujet")
1677
+ return
1678
+ self._g_props(subj, triples, gctx)
1679
+
1680
+ def _g_props(self, subj, triples, gctx):
1681
+ while True:
1682
+ verb = self._g_verb(triples, gctx)
1683
+ self._g_ws()
1684
+ while True:
1685
+ obj, _ = self._g_node(triples, gctx)
1686
+ triples.append((subj, verb, obj))
1687
+ self._g_ws()
1688
+ if self._peek() == ",":
1689
+ self._take(1)
1690
+ self._g_ws()
1691
+ continue
1692
+ break
1693
+ if self._peek() == ";":
1694
+ # Turtle 1.1 : predicateObjectList autorise des ';'
1695
+ # surnuméraires, aussi bien entre deux paires prédicat-objet
1696
+ # qu'en fin de liste — y compris devant le ']' d'un nœud
1697
+ # anonyme, qui manquait aux terminateurs (fiche ldpy/012).
1698
+ while self._peek() == ";":
1699
+ self._take(1)
1700
+ self._g_ws()
1701
+ if self._peek() in "}.]" or self._peek() == "":
1702
+ return
1703
+ continue
1704
+ return
1705
+
1706
+ def _g_verb(self, triples, gctx):
1707
+ t = self.text
1708
+ c = self._peek()
1709
+ if c == "a" and not (_name_char(self._peek(1)) or self._peek(1) == ":"):
1710
+ self._take(1)
1711
+ return RUNTIME_ALIAS + ".RDF.type"
1712
+ if c == "{":
1713
+ self._take(1)
1714
+ expr = self._scan_embedded_expr("}")
1715
+ if self._peek() != "}":
1716
+ self._error("'}' attendu")
1717
+ self._take(1)
1718
+ return _impure("%s.node((%s))" % (RUNTIME_ALIAS, expr.strip()), gctx)
1719
+ if c in "?$":
1720
+ return self._g_var(gctx)
1721
+ if c == "<":
1722
+ iri = self._take_iriref()
1723
+ return "%s.URIRef(%r)" % (RUNTIME_ALIAS, self._resolve(iri))
1724
+ if c == "f" and self._peek(1) == "<":
1725
+ return _maybe_impure(self._take_firi(), gctx)
1726
+ if c == "f" and self._peek(1) == "{":
1727
+ return _impure(self._take_fnode(), gctx)
1728
+ if c == "e" and self._peek(1) in "{<":
1729
+ if self._match_vars is not None:
1730
+ self._error("e{ } dans m{ } : les filtres d'appariement sont "
1731
+ "hors périmètre (fiche 017)")
1732
+ taker = (self._take_enode if self._peek(1) == "{"
1733
+ else self._take_eiri)
1734
+ return _impure(taker(), gctx)
1735
+ if _name_start(c) or c == ":":
1736
+ return _maybe_impure(self._take_pname(in_island=True), gctx)
1737
+ self._error("prédicat attendu (IRI, nom préfixé, 'a', variable ou "
1738
+ "interpolation)")
1739
+
1740
+ def _g_var(self, gctx):
1741
+ sigil = self._take(1)
1742
+ if self._peek() == "{":
1743
+ self._take(1)
1744
+ expr = self._scan_embedded_expr("}")
1745
+ if self._peek() != "}":
1746
+ self._error("'}' attendu pour fermer ?{...}")
1747
+ self._take(1)
1748
+ return _impure(self._interp_with_suffix(expr), gctx)
1749
+ m = _NAME_RE.match(self.text, self.i)
1750
+ if not m:
1751
+ self._error("nom de variable attendu après '%s'" % sigil)
1752
+ name = self._take(len(m.group(0)))
1753
+ if self._match_vars is not None and name not in self._match_vars:
1754
+ self._match_vars.append(name)
1755
+ return "%s.Variable(%r)" % (RUNTIME_ALIAS, name)
1756
+
1757
+ def _g_node(self, triples, gctx):
1758
+ """Parse un nœud de graphe. Retourne (expr, is_composite) ;
1759
+ is_composite vrai pour [..], (..) qui peuvent être sujets sans props."""
1760
+ t = self.text
1761
+ c = self._peek()
1762
+ if c == "[":
1763
+ self._take(1)
1764
+ self._g_ws()
1765
+ bn = gctx.new_bnode()
1766
+ if self._peek() == "]":
1767
+ self._take(1)
1768
+ return bn, True
1769
+ self._g_props(bn, triples, gctx)
1770
+ self._g_ws()
1771
+ if self._peek() != "]":
1772
+ self._error("']' attendu")
1773
+ self._take(1)
1774
+ return bn, True
1775
+ if c == "(":
1776
+ self._take(1)
1777
+ self._g_ws()
1778
+ items = []
1779
+ while self._peek() != ")":
1780
+ if self._peek() == "":
1781
+ self._error("')' attendu")
1782
+ node, _ = self._g_node(triples, gctx)
1783
+ items.append(node)
1784
+ self._g_ws()
1785
+ self._take(1)
1786
+ if not items:
1787
+ return RUNTIME_ALIAS + ".RDF.nil", True
1788
+ head = None
1789
+ prev = None
1790
+ for it in items:
1791
+ cell = gctx.new_bnode()
1792
+ if head is None:
1793
+ head = cell
1794
+ else:
1795
+ triples.append((prev, RUNTIME_ALIAS + ".RDF.rest", cell))
1796
+ triples.append((cell, RUNTIME_ALIAS + ".RDF.first", it))
1797
+ prev = cell
1798
+ triples.append((prev, RUNTIME_ALIAS + ".RDF.rest",
1799
+ RUNTIME_ALIAS + ".RDF.nil"))
1800
+ return head, True
1801
+ if c == "_" and self._peek(1) == ":":
1802
+ self._take(2)
1803
+ if self._peek() == "{":
1804
+ # _:{expr} : bnode à identité déterministe issue des données
1805
+ self._take(1)
1806
+ expr = self._scan_embedded_expr("}")
1807
+ if self._peek() != "}":
1808
+ self._error("'}' attendu pour fermer _:{...}")
1809
+ self._take(1)
1810
+ return _impure("%s.bnode((%s))" % (RUNTIME_ALIAS,
1811
+ expr.strip()), gctx), False
1812
+ end = _scan_pn_local_island(t, self.i, self.n)
1813
+ if end == self.i:
1814
+ self._error("étiquette de nœud anonyme attendue après '_:'")
1815
+ label = self._take(end - self.i)
1816
+ return gctx.labeled_bnode(label), False
1817
+ if c == "{":
1818
+ self._take(1)
1819
+ expr = self._scan_embedded_expr("}")
1820
+ if self._peek() != "}":
1821
+ self._error("'}' attendu")
1822
+ self._take(1)
1823
+ return _impure(self._interp_with_suffix(expr), gctx), False
1824
+ if c in "?$":
1825
+ term = self._g_var(gctx)
1826
+ return term, False
1827
+ if c == "<":
1828
+ iri = self._take_iriref()
1829
+ return "%s.URIRef(%r)" % (RUNTIME_ALIAS, self._resolve(iri)), False
1830
+ if c == "f" and self._peek(1) == "<":
1831
+ return _maybe_impure(self._take_firi(), gctx), False
1832
+ if c == "f" and self._peek(1) == "{":
1833
+ return _impure(self._take_fnode(), gctx), False
1834
+ if c == "e" and self._peek(1) in "{<":
1835
+ # expression différée en position de terme (fiches 007/017)
1836
+ if self._match_vars is not None:
1837
+ self._error("e{ } dans m{ } : les filtres d'appariement sont "
1838
+ "hors périmètre (fiche 017)")
1839
+ taker = (self._take_enode if self._peek(1) == "{"
1840
+ else self._take_eiri)
1841
+ return _impure(taker(), gctx), False
1842
+ if c in "\"'" or (c and c in "rbfuRBFU" and self._peek(1) in "\"'"):
1843
+ return self._g_literal(), False
1844
+ if c.isdigit() or c in "+-" or (c == "." and self._peek(1).isdigit()):
1845
+ start = self.i
1846
+ if c in "+-":
1847
+ self._take(1)
1848
+ end = self._number_end(self.i)
1849
+ if end == self.i:
1850
+ self._error("nombre attendu")
1851
+ self._take(end - self.i)
1852
+ return "%s.node(%s)" % (RUNTIME_ALIAS, t[start:self.i]), False
1853
+ if _name_start(c) or c == ":":
1854
+ m = _NAME_RE.match(t, self.i)
1855
+ word = m.group(0) if m else ""
1856
+ if word in ("True", "true"):
1857
+ self._take(len(word))
1858
+ return RUNTIME_ALIAS + ".node(True)", False
1859
+ if word in ("False", "false"):
1860
+ self._take(len(word))
1861
+ return RUNTIME_ALIAS + ".node(False)", False
1862
+ return _maybe_impure(self._take_pname(in_island=True), gctx), False
1863
+ self._error("terme RDF attendu")
1864
+
1865
+ def _interp_with_suffix(self, expr):
1866
+ """Terme interpolé de graphe, avec suffixe RDF optionnel COLLÉ :
1867
+ {expr}@lang -> Literal(expr, lang=...) ; {expr}^^dt -> Literal(expr,
1868
+ datatype=dt) ; sinon node(expr).."""
1869
+ expr = expr.strip()
1870
+ if self._peek() == "@":
1871
+ m = _LANGTAG_RE.match(self.text, self.i)
1872
+ if m:
1873
+ self._take(m.end() - self.i)
1874
+ return "%s.Literal((%s), lang=%r)" % (
1875
+ RUNTIME_ALIAS, expr, m.group(1))
1876
+ if self.text[self.i:self.i + 2] == "^^":
1877
+ self._take(2)
1878
+ dt = self._parse_term_after_hats()
1879
+ return "%s.Literal((%s), datatype=%s)" % (RUNTIME_ALIAS, expr, dt)
1880
+ return "%s.node((%s))" % (RUNTIME_ALIAS, expr)
1881
+
1882
+ def _g_literal(self):
1883
+ """Chaîne (± préfixe f/r/b) ± @lang / ^^type, dans un graphe."""
1884
+ t = self.text
1885
+ start = self.i
1886
+ qpos = self.i
1887
+ m = _NAME_RE.match(t, self.i)
1888
+ if m and m.group(0) in STRING_PREFIXES and \
1889
+ m.end() < self.n and t[m.end()] in "\"'":
1890
+ qpos = m.end()
1891
+ end = self._string_end(qpos)
1892
+ string_text = t[start:end]
1893
+ self._take(end - self.i)
1894
+ if self._peek() == "@":
1895
+ lm = _LANGTAG_RE.match(t, self.i)
1896
+ if lm:
1897
+ self._take(lm.end() - self.i)
1898
+ return "%s.Literal(%s, lang=%r)" % (
1899
+ RUNTIME_ALIAS, string_text, lm.group(1))
1900
+ if t[self.i:self.i + 2] == "^^":
1901
+ self._take(2)
1902
+ dt = self._parse_term_after_hats()
1903
+ return "%s.Literal(%s, datatype=%s)" % (
1904
+ RUNTIME_ALIAS, string_text, dt)
1905
+ return "%s.Literal(%s)" % (RUNTIME_ALIAS, string_text)
1906
+
1907
+ # ------------------------------------------------------------------
1908
+ # îlot de motif m{ ... } (fiche 016)
1909
+ # ------------------------------------------------------------------
1910
+
1911
+ def _match_island(self):
1912
+ """Sur 'm{'. Un BGP en syntaxe Turtle, à variables, évalué contre
1913
+ le graphe courant : rend des termes (arité 1) ou des lignes
1914
+ (arité >= 2). Projection = ordre de première apparition ; un nœud
1915
+ anonyme est une variable non distinguée."""
1916
+ mark = self._begin_island()
1917
+ saved_vars, self._match_vars = self._match_vars, []
1918
+ try:
1919
+ self._take(2) # m{
1920
+ triples, gctx = self._g_parse_triples()
1921
+ projected = list(self._match_vars)
1922
+ finally:
1923
+ self._match_vars = saved_vars
1924
+ triples = _share_impure(triples, gctx)
1925
+ gvar = self._graph_var if self._graph_var else "None"
1926
+ pats = ", ".join("(%s, %s, %s)" % tr for tr in triples)
1927
+ proj = ", ".join(repr(v) for v in projected)
1928
+ gen = "%s.match(%s, (%s,), (%s%s)%s)" % (
1929
+ RUNTIME_ALIAS, gvar, pats,
1930
+ proj, "," if projected else "", self._bkw())
1931
+ self._end_island("match", mark, gen)
1932
+
1933
+ # ------------------------------------------------------------------
1934
+ # îlot SPARQL s{ ... } (fiche 015)
1935
+ # ------------------------------------------------------------------
1936
+
1937
+ _S_UPDATE_RE = re.compile(
1938
+ r"\s*(?:INSERT|DELETE|CLEAR|DROP|CREATE|LOAD|COPY|MOVE|ADD|WITH)\b",
1939
+ re.I)
1940
+
1941
+ def _sparql_island(self):
1942
+ """Sur 's{'. Tout SPARQL : l'îlot ne lexe que les interpolations,
1943
+ les chaînes et l'équilibre des accolades ; rdflib valide à la
1944
+ transpilation (l'oracle, fiche 015)."""
1945
+ mark = self._begin_island()
1946
+ isl_line, isl_col = self.src_line, self.src_col
1947
+ self._take(2) # s{
1948
+ t = self.text
1949
+ pieces = []
1950
+ interps = [] # exprs transpilées
1951
+ depth = 0
1952
+ while True:
1953
+ c = self._peek()
1954
+ if c == "":
1955
+ self._error("'}' attendu pour fermer s{...}")
1956
+ if c == "}":
1957
+ if depth == 0:
1958
+ self._take(1)
1959
+ break
1960
+ depth -= 1
1961
+ pieces.append(self._take(1))
1962
+ elif c in "\"'":
1963
+ end = self._string_end(self.i)
1964
+ pieces.append(self._take(end - self.i))
1965
+ elif c == "#":
1966
+ j = t.find("\n", self.i)
1967
+ pieces.append(self._take((j if j != -1 else self.n) - self.i))
1968
+ elif c == "{":
1969
+ inner = self._sparql_brace_content(self.i)
1970
+ if inner is not None and self._is_ldpy_expression(inner):
1971
+ # interpolation en position de terme
1972
+ self._take(1)
1973
+ expr = self._scan_embedded_expr("}")
1974
+ if self._peek() != "}":
1975
+ self._error("'}' attendu pour fermer l'interpolation")
1976
+ self._take(1)
1977
+ name = "__i%d" % len(interps)
1978
+ interps.append(expr.strip())
1979
+ pieces.append(" ?%s " % name)
1980
+ else:
1981
+ depth += 1
1982
+ pieces.append(self._take(1))
1983
+ else:
1984
+ pieces.append(self._take(1))
1985
+ text = "".join(pieces).strip()
1986
+ if not text:
1987
+ self._error("requête vide dans s{ }", isl_line, isl_col)
1988
+ is_update = bool(self._S_UPDATE_RE.match(text))
1989
+ self._validate_sparql(text, is_update, isl_line, isl_col)
1990
+ gvar = self._graph_var if self._graph_var else "None"
1991
+ iargs = ", ".join("(%r, (%s))" % ("__i%d" % k, e)
1992
+ for k, e in enumerate(interps))
1993
+ gen = ("%s.prepared(%r, (%s), __namespaces__, %s, graph=%s%s, "
1994
+ "update=%r)" % (RUNTIME_ALIAS, text,
1995
+ iargs + "," if iargs else "",
1996
+ repr(self.base) if self.base else "None",
1997
+ gvar, self._bkw(), is_update))
1998
+ self._end_island("sparql", mark, gen)
1999
+
2000
+ def _sparql_brace_content(self, i):
2001
+ """Contenu du bloc {...} équilibré commençant en i (sans consommer),
2002
+ ou None si non fermé."""
2003
+ t = self.text
2004
+ depth = 0
2005
+ j = i
2006
+ while j < self.n:
2007
+ c = t[j]
2008
+ if c in "\"'":
2009
+ saved = (self.src_line, self.src_col)
2010
+ try:
2011
+ j = self._string_end(j)
2012
+ except LdpySyntaxError:
2013
+ return None
2014
+ self.src_line, self.src_col = saved
2015
+ continue
2016
+ if c == "#":
2017
+ k = t.find("\n", j)
2018
+ j = k if k != -1 else self.n
2019
+ continue
2020
+ if c == "{":
2021
+ depth += 1
2022
+ elif c == "}":
2023
+ depth -= 1
2024
+ if depth == 0:
2025
+ return t[i + 1:j]
2026
+ j += 1
2027
+ return None
2028
+
2029
+ def _is_ldpy_expression(self, text):
2030
+ """L'oracle interpolation/groupe : le texte est-il une expression
2031
+ ldpy (transpilable puis compilable en 'eval') ? Un groupe SPARQL ne
2032
+ l'est jamais ; une interpolation l'est par définition."""
2033
+ if not text.strip():
2034
+ return False
2035
+ sub = Transpiler(text, self.filename, emit_prelude=False)
2036
+ sub.prefixes = dict(self.prefixes)
2037
+ sub.base = self.base
2038
+ try:
2039
+ code = sub.run().code.strip()
2040
+ compile("(%s)" % code, "<interp>", "eval")
2041
+ return True
2042
+ except (LdpySyntaxError, SyntaxError, ValueError):
2043
+ return False
2044
+
2045
+ def _validate_sparql(self, text, is_update, line, col):
2046
+ """Valide la requête à la transpilation, rdflib en oracle. Les
2047
+ préfixes dynamiques reçoivent une IRI synthétique — une IRI en vaut
2048
+ une autre pour vérifier une syntaxe. rdflib absent : avertir et
2049
+ émettre sans valider."""
2050
+ try:
2051
+ from rdflib.plugins.sparql import prepareQuery, prepareUpdate
2052
+ except ImportError:
2053
+ self._warn("rdflib indisponible à la transpilation : "
2054
+ "s{ } émis sans validation syntaxique")
2055
+ return
2056
+ init_ns = {}
2057
+ for pfx, val in self.prefixes.items():
2058
+ init_ns[pfx] = (val if isinstance(val, str)
2059
+ else "urn:x-ldpy:dyn:%s/" % pfx)
2060
+ full = ("BASE <%s>\n" % self.base if self.base else "") + text
2061
+ try:
2062
+ (prepareUpdate if is_update else prepareQuery)(full,
2063
+ initNs=init_ns)
2064
+ except Exception as e:
2065
+ msg = str(e).split("\n")[0][:200]
2066
+ self._error("requête SPARQL invalide : %s" % msg, line, col)
2067
+
2068
+ # ------------------------------------------------------------------
2069
+ # nœuds expression SPARQL : e{ ... } et e<...>
2070
+ # ------------------------------------------------------------------
2071
+
2072
+ _E_BUILTINS = frozenset((
2073
+ "STR", "LANG", "DATATYPE", "IRI", "URI", "BNODE", "CONCAT", "UCASE",
2074
+ "LCASE", "STRLEN", "SUBSTR", "STRSTARTS", "STRENDS", "CONTAINS",
2075
+ "STRBEFORE", "STRAFTER", "REPLACE", "REGEX", "ABS", "ROUND", "CEIL",
2076
+ "FLOOR", "SAMETERM", "ISIRI", "ISURI", "ISBLANK", "ISLITERAL",
2077
+ "ISNUMERIC", "LANGMATCHES", "ENCODE_FOR_IRI",
2078
+ ))
2079
+
2080
+ def _take_enode(self):
2081
+ """Sur 'e{'. Émet _ldpy_.sparql.expr(lambda __sm__: <corps>, src)."""
2082
+ start = self.i
2083
+ self._take(2)
2084
+ self._e_ws()
2085
+ body = self._e_expr()
2086
+ self._e_ws()
2087
+ if self._peek() != "}":
2088
+ self._error("'}' attendu pour fermer e{...}")
2089
+ self._take(1)
2090
+ src_text = self.text[start + 2:self.i - 1].strip()
2091
+ return "%s.sparql.expr(lambda __sm__: %s, src=%r)" % (
2092
+ RUNTIME_ALIAS, body, src_text)
2093
+
2094
+ def _take_eiri(self):
2095
+ """Sur 'e<'. IRI différée : gabarit dont les trous sont des
2096
+ expressions SPARQL (STR + encodage IRI-safe à l'évaluation)."""
2097
+ start = self.i
2098
+ self._take(2)
2099
+ parts = []
2100
+ static = []
2101
+ while True:
2102
+ c = self._peek()
2103
+ if c == "":
2104
+ self._error("e-IRI non terminée")
2105
+ if c == ">":
2106
+ self._take(1)
2107
+ break
2108
+ if c == "{":
2109
+ self._take(1)
2110
+ if static:
2111
+ parts.append(repr("".join(static)))
2112
+ static = []
2113
+ self._e_ws()
2114
+ parts.append(self._e_expr())
2115
+ self._e_ws()
2116
+ if self._peek() != "}":
2117
+ self._error("'}' attendu dans la e-IRI")
2118
+ self._take(1)
2119
+ continue
2120
+ if not _is_iri_char(c):
2121
+ self._error("caractère %r interdit dans une e-IRI" % c)
2122
+ static.append(self._take(1))
2123
+ if static:
2124
+ parts.append(repr("".join(static)))
2125
+ src_text = self.text[start:self.i]
2126
+ base = ", base=%r" % self.base if self.base else ""
2127
+ return ("%s.sparql.expr(lambda __sm__: %s.sparql.build_iri((%s,)%s), "
2128
+ "src=%r)" % (RUNTIME_ALIAS, RUNTIME_ALIAS,
2129
+ ", ".join(parts), base, src_text))
2130
+
2131
+ def _e_ws(self):
2132
+ t = self.text
2133
+ while self.i < self.n:
2134
+ c = t[self.i]
2135
+ if c in " \t\r\n":
2136
+ self._take(1)
2137
+ elif c == "#":
2138
+ j = t.find("\n", self.i)
2139
+ self._take((j if j != -1 else self.n) - self.i)
2140
+ else:
2141
+ break
2142
+
2143
+ def _e_kw(self, word):
2144
+ """Consomme le mot-clé s'il est là (frontière de mot), sinon False."""
2145
+ t = self.text
2146
+ if t.startswith(word, self.i) and not _name_char(
2147
+ self._peek(len(word))):
2148
+ self._take(len(word))
2149
+ self._e_ws()
2150
+ return True
2151
+ return False
2152
+
2153
+ def _e_expr(self):
2154
+ """expression := or ('if' or 'else' expression)? (style dev-sparql)."""
2155
+ val = self._e_or()
2156
+ self._e_ws()
2157
+ if self._e_kw("if"):
2158
+ cond = self._e_or()
2159
+ self._e_ws()
2160
+ if not self._e_kw("else"):
2161
+ self._error("'else' attendu dans l'expression conditionnelle")
2162
+ other = self._e_expr()
2163
+ return "%s.sparql.if_(%s, lambda: %s, lambda: %s)" % (
2164
+ RUNTIME_ALIAS, cond, val, other)
2165
+ return val
2166
+
2167
+ def _e_or(self):
2168
+ val = self._e_and()
2169
+ while True:
2170
+ self._e_ws()
2171
+ if self.text.startswith("||", self.i):
2172
+ self._take(2)
2173
+ self._e_ws()
2174
+ val = "%s.sparql.or_(lambda: %s, lambda: %s)" % (
2175
+ RUNTIME_ALIAS, val, self._e_and())
2176
+ else:
2177
+ return val
2178
+
2179
+ def _e_and(self):
2180
+ val = self._e_rel()
2181
+ while True:
2182
+ self._e_ws()
2183
+ if self.text.startswith("&&", self.i):
2184
+ self._take(2)
2185
+ self._e_ws()
2186
+ val = "%s.sparql.and_(lambda: %s, lambda: %s)" % (
2187
+ RUNTIME_ALIAS, val, self._e_rel())
2188
+ else:
2189
+ return val
2190
+
2191
+ _E_RELOPS = (("!=", "ne"), ("<=", "le"), (">=", "ge"),
2192
+ ("=", "eq"), ("<", "lt"), (">", "gt"))
2193
+
2194
+ def _e_rel(self):
2195
+ val = self._e_add()
2196
+ self._e_ws()
2197
+ t = self.text
2198
+ if self._e_kw("NOT"):
2199
+ if not self._e_kw("IN"):
2200
+ self._error("'IN' attendu après 'NOT'")
2201
+ return "%s.sparql.not_in(%s, %s)" % (
2202
+ RUNTIME_ALIAS, val, self._e_list())
2203
+ if self._e_kw("IN"):
2204
+ return "%s.sparql.in_(%s, %s)" % (
2205
+ RUNTIME_ALIAS, val, self._e_list())
2206
+ for op, fn in self._E_RELOPS:
2207
+ if t.startswith(op, self.i):
2208
+ self._take(len(op))
2209
+ self._e_ws()
2210
+ return "%s.sparql.%s(%s, %s)" % (
2211
+ RUNTIME_ALIAS, fn, val, self._e_add())
2212
+ return val
2213
+
2214
+ def _e_list(self):
2215
+ if self._peek() != "(":
2216
+ self._error("'(' attendu après IN")
2217
+ self._take(1)
2218
+ items = []
2219
+ self._e_ws()
2220
+ while self._peek() != ")":
2221
+ items.append(self._e_expr())
2222
+ self._e_ws()
2223
+ if self._peek() == ",":
2224
+ self._take(1)
2225
+ self._e_ws()
2226
+ self._take(1)
2227
+ return "(%s,)" % ", ".join(items) if items else "()"
2228
+
2229
+ def _e_add(self):
2230
+ val = self._e_mul()
2231
+ while True:
2232
+ self._e_ws()
2233
+ c = self._peek()
2234
+ if c == "+" :
2235
+ self._take(1)
2236
+ self._e_ws()
2237
+ val = "%s.sparql.add(%s, %s)" % (RUNTIME_ALIAS, val,
2238
+ self._e_mul())
2239
+ elif c == "-":
2240
+ self._take(1)
2241
+ self._e_ws()
2242
+ val = "%s.sparql.sub(%s, %s)" % (RUNTIME_ALIAS, val,
2243
+ self._e_mul())
2244
+ else:
2245
+ return val
2246
+
2247
+ def _e_mul(self):
2248
+ val = self._e_unary()
2249
+ while True:
2250
+ self._e_ws()
2251
+ c = self._peek()
2252
+ if c == "*":
2253
+ self._take(1)
2254
+ self._e_ws()
2255
+ val = "%s.sparql.mul(%s, %s)" % (RUNTIME_ALIAS, val,
2256
+ self._e_unary())
2257
+ elif c == "/":
2258
+ self._take(1)
2259
+ self._e_ws()
2260
+ val = "%s.sparql.div(%s, %s)" % (RUNTIME_ALIAS, val,
2261
+ self._e_unary())
2262
+ else:
2263
+ return val
2264
+
2265
+ def _e_unary(self):
2266
+ c = self._peek()
2267
+ if c == "!":
2268
+ self._take(1)
2269
+ self._e_ws()
2270
+ return "%s.sparql.not_(%s)" % (RUNTIME_ALIAS, self._e_unary())
2271
+ if c == "-":
2272
+ self._take(1)
2273
+ self._e_ws()
2274
+ return "%s.sparql.neg(%s)" % (RUNTIME_ALIAS, self._e_unary())
2275
+ if c == "+":
2276
+ self._take(1)
2277
+ self._e_ws()
2278
+ return self._e_primary()
2279
+
2280
+ def _e_primary(self):
2281
+ t = self.text
2282
+ c = self._peek()
2283
+ if c == "(":
2284
+ self._take(1)
2285
+ self._e_ws()
2286
+ val = self._e_expr()
2287
+ self._e_ws()
2288
+ if self._peek() != ")":
2289
+ self._error("')' attendu")
2290
+ self._take(1)
2291
+ return val
2292
+ if c in "?$":
2293
+ self._take(1)
2294
+ m = _NAME_RE.match(t, self.i)
2295
+ if not m:
2296
+ self._error("nom de variable attendu")
2297
+ return "%s.sparql.var(__sm__, %r)" % (
2298
+ RUNTIME_ALIAS, self._take(len(m.group(0))))
2299
+ if c == "{":
2300
+ self._take(1)
2301
+ expr = self._scan_embedded_expr("}")
2302
+ if self._peek() != "}":
2303
+ self._error("'}' attendu")
2304
+ self._take(1)
2305
+ return "%s.sparql.py((%s))" % (RUNTIME_ALIAS, expr.strip())
2306
+ if c == "e" and self._peek(1) == "<":
2307
+ inner = self._take_eiri()
2308
+ return "(%s)(__sm__)" % inner # e-IRI imbriquée : évaluée là
2309
+ if c == "<":
2310
+ iri = self._take_iriref()
2311
+ return "%s.URIRef(%r)" % (RUNTIME_ALIAS, self._resolve(iri))
2312
+ if c in "\"'":
2313
+ start = self.i
2314
+ end = self._string_end(self.i)
2315
+ text = t[start:end]
2316
+ self._take(end - self.i)
2317
+ if self._peek() == "@":
2318
+ m = _LANGTAG_RE.match(t, self.i)
2319
+ if m:
2320
+ self._take(m.end() - self.i)
2321
+ return "%s.Literal(%s, lang=%r)" % (
2322
+ RUNTIME_ALIAS, text, m.group(1))
2323
+ if t.startswith("^^", self.i):
2324
+ self._take(2)
2325
+ dt = self._parse_term_after_hats()
2326
+ return "%s.Literal(%s, datatype=%s)" % (
2327
+ RUNTIME_ALIAS, text, dt)
2328
+ return "%s.Literal(%s)" % (RUNTIME_ALIAS, text)
2329
+ if c.isdigit() or (c == "." and self._peek(1).isdigit()):
2330
+ j = self.i
2331
+ while j < self.n and (t[j].isdigit() or t[j] in ".eE" or
2332
+ (t[j] in "+-" and t[j-1] in "eE")):
2333
+ j += 1
2334
+ lex = self._take(j - self.i)
2335
+ return "%s.sparql.number(%r)" % (RUNTIME_ALIAS, lex)
2336
+ m = _NAME_RE.match(t, self.i)
2337
+ if m:
2338
+ word = m.group(0)
2339
+ after = t[m.end()] if m.end() < self.n else ""
2340
+ if word.upper() == "BOUND" and after == "(":
2341
+ self._take(len(word) + 1)
2342
+ self._e_ws()
2343
+ if self._peek() not in "?$":
2344
+ self._error("BOUND attend une variable ?v")
2345
+ self._take(1)
2346
+ vm = _NAME_RE.match(t, self.i)
2347
+ name = self._take(len(vm.group(0)))
2348
+ self._e_ws()
2349
+ if self._peek() != ")":
2350
+ self._error("')' attendu")
2351
+ self._take(1)
2352
+ return "%s.sparql.bound(__sm__, %r)" % (RUNTIME_ALIAS, name)
2353
+ if word.upper() in ("IF", "COALESCE") and after == "(":
2354
+ fn = word.upper()
2355
+ self._take(len(word) + 1)
2356
+ self._e_ws()
2357
+ args = []
2358
+ while self._peek() != ")":
2359
+ args.append(self._e_expr())
2360
+ self._e_ws()
2361
+ if self._peek() == ",":
2362
+ self._take(1)
2363
+ self._e_ws()
2364
+ self._take(1)
2365
+ if fn == "IF":
2366
+ if len(args) != 3:
2367
+ self._error("IF attend 3 arguments")
2368
+ return ("%s.sparql.if_(%s, lambda: %s, lambda: %s)"
2369
+ % (RUNTIME_ALIAS, *args))
2370
+ return "%s.sparql.coalesce(%s)" % (
2371
+ RUNTIME_ALIAS,
2372
+ ", ".join("lambda: %s" % a for a in args))
2373
+ if word.upper() in self._E_BUILTINS and after == "(":
2374
+ fn = "ISIRI" if word.upper() == "ISURI" else word.upper()
2375
+ if fn == "URI":
2376
+ fn = "IRI"
2377
+ self._take(len(word) + 1)
2378
+ self._e_ws()
2379
+ args = []
2380
+ while self._peek() != ")":
2381
+ args.append(self._e_expr())
2382
+ self._e_ws()
2383
+ if self._peek() == ",":
2384
+ self._take(1)
2385
+ self._e_ws()
2386
+ self._take(1)
2387
+ if fn == "IRI" and self.base:
2388
+ args.append("base=%r" % self.base)
2389
+ return "%s.sparql.%s(%s)" % (RUNTIME_ALIAS, fn,
2390
+ ", ".join(args))
2391
+ if after == ":":
2392
+ return self._take_pname(in_island=True)
2393
+ if word in ("true", "false"):
2394
+ self._take(len(word))
2395
+ return "%s.Literal(%s)" % (RUNTIME_ALIAS,
2396
+ word == "true")
2397
+ self._error("expression SPARQL attendue")
2398
+
2399
+ # ------------------------------------------------------------------
2400
+ # prélude
2401
+ # ------------------------------------------------------------------
2402
+
2403
+ def _prelude_insert_line(self, code):
2404
+ """Ligne (0-based) où insérer le prélude : après commentaires de tête,
2405
+ docstring de module et imports __future__."""
2406
+ lines = code.split("\n")
2407
+ offsets = []
2408
+ off = 0
2409
+ for ln in lines:
2410
+ offsets.append(off)
2411
+ off += len(ln) + 1
2412
+ li = 0
2413
+ # commentaires / lignes vides
2414
+ while li < len(lines) and (not lines[li].strip()
2415
+ or lines[li].lstrip().startswith("#")):
2416
+ li += 1
2417
+ if li >= len(lines):
2418
+ return li
2419
+ stripped = lines[li].lstrip()
2420
+ m = _NAME_RE.match(stripped)
2421
+ pfx = m.group(0) if m else ""
2422
+ rest = stripped[len(pfx):] if pfx in STRING_PREFIXES else stripped
2423
+ if rest[:1] in "\"'":
2424
+ # docstring : trouver sa fin dans le texte généré
2425
+ qidx = offsets[li] + len(lines[li]) - len(rest)
2426
+ saved = (self.text, self.n)
2427
+ self.text, self.n = code, len(code)
2428
+ try:
2429
+ send = self._string_end(qidx)
2430
+ finally:
2431
+ self.text, self.n = saved
2432
+ eol = code.find("\n", send)
2433
+ li = (code.count("\n", 0, eol) + 1) if eol != -1 \
2434
+ else code.count("\n") + 1
2435
+ while li < len(lines) and re.match(r"\s*from\s+__future__\s+import",
2436
+ lines[li]):
2437
+ li += 1
2438
+ return li
2439
+
2440
+ def _insert_prelude(self, code):
2441
+ li = self._prelude_insert_line(code)
2442
+ lines = code.split("\n")
2443
+ lines.insert(li, PRELUDE)
2444
+ # découpe des segments copy chevauchant la frontière, puis décalage
2445
+ new_segments = []
2446
+ for seg in self.map.segments:
2447
+ if seg.kind == "copy" and seg.gen[0] < li <= seg.gen[2]:
2448
+ gl0, gc0, gl1, gc1 = seg.gen
2449
+ sl0, sc0, sl1, sc1 = seg.src
2450
+ dsl = li - gl0
2451
+ cut_src = (sl0 + dsl, 0)
2452
+ a = type(seg)("copy", (sl0, sc0) + cut_src, (gl0, gc0, li, 0))
2453
+ b = type(seg)("copy", cut_src + (sl1, sc1),
2454
+ (li + 1, 0, gl1 + 1, gc1))
2455
+ new_segments.extend([a, b])
2456
+ elif seg.gen[0] >= li:
2457
+ seg.gen = (seg.gen[0] + 1, seg.gen[1],
2458
+ seg.gen[2] + 1, seg.gen[3])
2459
+ new_segments.append(seg)
2460
+ else:
2461
+ new_segments.append(seg)
2462
+ from ldpy.transpiler.linemap import Segment
2463
+ new_segments.append(Segment("synthetic", None,
2464
+ (li, 0, li, len(PRELUDE))))
2465
+ new_segments.sort(key=lambda s: s.gen[:2])
2466
+ self.map.segments = new_segments
2467
+ return "\n".join(lines)
2468
+
2469
+
2470
+ class _Term(str):
2471
+ """Expression de terme RDF, portant l'identite de son occurrence source.
2472
+
2473
+ Deux `?{ v() }` ecrits a deux endroits differents sont deux occurrences
2474
+ distinctes (donc deux evaluations), meme si leur texte est identique ;
2475
+ le meme sujet reutilise par une liste de proprietes est UNE occurrence."""
2476
+
2477
+ __slots__ = ("occ",)
2478
+
2479
+ def __new__(cls, text, occ=None):
2480
+ t = str.__new__(cls, text)
2481
+ t.occ = occ
2482
+ return t
2483
+
2484
+
2485
+ def _maybe_impure(expr, gctx):
2486
+ """f-IRI et nom prefixe : impurs seulement s'ils portent une interpolation
2487
+ (sinon le transpileur a deja produit une URIRef constante)."""
2488
+ if expr.startswith(RUNTIME_ALIAS + ".firi("):
2489
+ return _impure(expr, gctx)
2490
+ return expr
2491
+
2492
+
2493
+ def _impure(expr, gctx):
2494
+ """Marque une expression de terme qui embarque du Python interpole : son
2495
+ evaluation peut avoir des effets de bord, elle doit n'avoir lieu qu'une
2496
+ fois par occurrence source."""
2497
+ gctx.occ += 1
2498
+ return _Term(expr, gctx.occ)
2499
+
2500
+
2501
+ def _share_impure(triples, gctx):
2502
+ """Un terme interpole partage par plusieurs triplets (typiquement le sujet
2503
+ d'une liste de proprietes) ne doit etre evalue qu'une fois : on l'emet a sa
2504
+ premiere occurrence dans `slot(i, expr)` et on le rappelle par `slot(i)`.
2505
+
2506
+ Sans cela, `g{ ex:{f()} ex:p 1 ; ex:q 2 }` appellerait f() deux fois et
2507
+ produirait deux sujets differents. Voir docs/explanation/emission-and-semantics.md."""
2508
+ counts = {}
2509
+ for tr in triples:
2510
+ for expr in tr:
2511
+ occ = getattr(expr, "occ", None)
2512
+ if occ is not None:
2513
+ counts[occ] = counts.get(occ, 0) + 1
2514
+ shared = {o for o, n in counts.items() if n > 1}
2515
+ if not shared:
2516
+ return triples
2517
+ seen = {}
2518
+ out = []
2519
+ for tr in triples:
2520
+ new_tr = []
2521
+ for expr in tr:
2522
+ occ = getattr(expr, "occ", None)
2523
+ if occ in shared:
2524
+ if occ not in seen:
2525
+ seen[occ] = len(seen)
2526
+ new_tr.append("%s.slot(%d, %s)" % (RUNTIME_ALIAS, seen[occ], expr))
2527
+ else:
2528
+ new_tr.append("%s.slot(%d)" % (RUNTIME_ALIAS, seen[occ]))
2529
+ else:
2530
+ new_tr.append(str(expr))
2531
+ out.append(tuple(new_tr))
2532
+ return out
2533
+
2534
+
2535
+ class _GraphCtx:
2536
+ def __init__(self):
2537
+ self.counter = 0
2538
+ self.occ = 0
2539
+ self.labels = {}
2540
+
2541
+ def new_bnode(self):
2542
+ expr = "%s.bn(%d)" % (RUNTIME_ALIAS, self.counter)
2543
+ self.counter += 1
2544
+ return expr
2545
+
2546
+ def labeled_bnode(self, label):
2547
+ if label not in self.labels:
2548
+ self.labels[label] = self.counter
2549
+ self.counter += 1
2550
+ return "%s.bn(%d)" % (RUNTIME_ALIAS, self.labels[label])
2551
+
2552
+
2553
+ def transpile(source, filename="<ldpy>"):
2554
+ """Transpile un source Linked-Data Python en Python.
2555
+
2556
+ Retourne un TranspileResult(code, map, prefixes, base, warnings).
2557
+ """
2558
+ return Transpiler(source, filename).run()