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.
- ldpy/__init__.py +35 -11
- ldpy/__main__.py +75 -250
- ldpy/build.py +91 -0
- ldpy/console.py +116 -0
- ldpy/debug.py +235 -0
- ldpy/formatter.py +329 -0
- ldpy/importer.py +110 -0
- ldpy/lsp/__init__.py +11 -0
- ldpy/lsp/__main__.py +4 -0
- ldpy/lsp/backend.py +118 -0
- ldpy/lsp/rpc.py +104 -0
- ldpy/lsp/server.py +353 -0
- ldpy/lsp/translate.py +140 -0
- ldpy/pygments_lexer.py +613 -0
- ldpy/runtime.py +931 -0
- ldpy/sparql.py +551 -0
- ldpy/transpiler/__init__.py +14 -0
- ldpy/transpiler/core.py +2558 -0
- ldpy/transpiler/errors.py +38 -0
- ldpy/transpiler/linemap.py +292 -0
- linked_data_python-0.2.0.dist-info/METADATA +158 -0
- linked_data_python-0.2.0.dist-info/RECORD +26 -0
- {linked_data_python-0.0.4.dist-info → linked_data_python-0.2.0.dist-info}/WHEEL +1 -1
- linked_data_python-0.2.0.dist-info/entry_points.txt +9 -0
- {linked_data_python-0.0.4.dist-info → linked_data_python-0.2.0.dist-info/licenses}/LICENSE.md +0 -0
- {linked_data_python-0.0.4.dist-info → linked_data_python-0.2.0.dist-info}/top_level.txt +0 -0
- ldpy/grun/lib.py +0 -63
- ldpy/grun/util.py +0 -11
- ldpy/ldpy.py +0 -183
- ldpy/rewriter/IndentedStringWriter.py +0 -54
- ldpy/rewriter/LDPythonRewriter.py +0 -677
- ldpy/rewriter/MultiChannelTokenStream.py +0 -127
- ldpy/rewriter/Result.py +0 -49
- ldpy/rewriter/__init__.py +0 -7
- ldpy/rewriter/antlr/LDPythonLexer.py +0 -870
- ldpy/rewriter/antlr/LDPythonParser.py +0 -9336
- ldpy/rewriter/antlr/LDPythonVisitor.py +0 -573
- ldpy/sparql/builtin.py +0 -326
- linked_data_python-0.0.4.dist-info/METADATA +0 -139
- linked_data_python-0.0.4.dist-info/RECORD +0 -20
- linked_data_python-0.0.4.dist-info/entry_points.txt +0 -3
ldpy/transpiler/core.py
ADDED
|
@@ -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()
|