SNOBOL4python 0.5.0__cp310-cp310-win_amd64.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.
- SNOBOL4python/SNOBOL4functions.py +262 -0
- SNOBOL4python/SNOBOL4patterns.py +30 -0
- SNOBOL4python/__init__.py +63 -0
- SNOBOL4python/_backend.py +241 -0
- SNOBOL4python/_backend_c.py +792 -0
- SNOBOL4python/_backend_pure.py +879 -0
- SNOBOL4python/_env.py +25 -0
- sno4py.cp310-win_amd64.pyd +0 -0
- snobol4python-0.5.0.dist-info/METADATA +212 -0
- snobol4python-0.5.0.dist-info/RECORD +13 -0
- snobol4python-0.5.0.dist-info/WHEEL +5 -0
- snobol4python-0.5.0.dist-info/licenses/LICENSE +674 -0
- snobol4python-0.5.0.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# SNOBOL4functions.py — SNOBOL4 built-in functions
|
|
3
|
+
#
|
|
4
|
+
# All functions that need the SNOBOL environment dict read it from _env._g.
|
|
5
|
+
# This module does NOT define GLOBALS() — that lives in _env.py and is
|
|
6
|
+
# re-exported via __init__.py.
|
|
7
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
8
|
+
import gc
|
|
9
|
+
import re
|
|
10
|
+
import sys
|
|
11
|
+
import time
|
|
12
|
+
import types
|
|
13
|
+
import logging
|
|
14
|
+
from datetime import date
|
|
15
|
+
from . import _env
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
# ── I/O unit table ────────────────────────────────────────────────────────────
|
|
20
|
+
_started = time.time_ns() // 1000
|
|
21
|
+
_units: dict = {} # unit-number → (varname, file-object)
|
|
22
|
+
|
|
23
|
+
# ── SNOBOL4 standard string constants ─────────────────────────────────────────
|
|
24
|
+
UCASE = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
|
25
|
+
LCASE = "abcdefghijklmnopqrstuvwxyz"
|
|
26
|
+
DIGITS = "0123456789"
|
|
27
|
+
ALPHABET = (
|
|
28
|
+
"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F"
|
|
29
|
+
"\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F"
|
|
30
|
+
"\x20\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2A\x2B\x2C\x2D\x2E\x2F"
|
|
31
|
+
"\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39\x3A\x3B\x3C\x3D\x3E\x3F"
|
|
32
|
+
"\x40\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4A\x4B\x4C\x4D\x4E\x4F"
|
|
33
|
+
"\x50\x51\x52\x53\x54\x55\x56\x57\x58\x59\x5A\x5B\x5C\x5D\x5E\x5F"
|
|
34
|
+
"\x60\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6A\x6B\x6C\x6D\x6E\x6F"
|
|
35
|
+
"\x70\x71\x72\x73\x74\x75\x76\x77\x78\x79\x7A\x7B\x7C\x7D\x7E\x7F"
|
|
36
|
+
"\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8A\x8B\x8C\x8D\x8E\x8F"
|
|
37
|
+
"\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9A\x9B\x9C\x9D\x9E\x9F"
|
|
38
|
+
"\xA0\xA1\xA2\xA3\xA4\xA5\xA6\xA7\xA8\xA9\xAA\xAB\xAC\xAD\xAE\xAF"
|
|
39
|
+
"\xB0\xB1\xB2\xB3\xB4\xB5\xB6\xB7\xB8\xB9\xBA\xBB\xBC\xBD\xBE\xBF"
|
|
40
|
+
"\xC0\xC1\xC2\xC3\xC4\xC5\xC6\xC7\xC8\xC9\xCA\xCB\xCC\xCD\xCE\xCF"
|
|
41
|
+
"\xD0\xD1\xD2\xD3\xD4\xD5\xD6\xD7\xD8\xD9\xDA\xDB\xDC\xDD\xDE\xDF"
|
|
42
|
+
"\xE0\xE1\xE2\xE3\xE4\xE5\xE6\xE7\xE8\xE9\xEA\xEB\xEC\xED\xEE\xEF"
|
|
43
|
+
"\xF0\xF1\xF2\xF3\xF4\xF5\xF6\xF7\xF8\xF9\xFA\xFB\xFC\xFD\xFE\xFF"
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
# ── Integer comparison functions ───────────────────────────────────────────────
|
|
47
|
+
def GT(i1, i2):
|
|
48
|
+
if int(i1) > int(i2): return ""
|
|
49
|
+
raise Exception()
|
|
50
|
+
def LT(i1, i2):
|
|
51
|
+
if int(i1) < int(i2): return ""
|
|
52
|
+
raise Exception()
|
|
53
|
+
def EQ(i1, i2):
|
|
54
|
+
if int(i1) == int(i2): return ""
|
|
55
|
+
raise Exception()
|
|
56
|
+
def GE(i1, i2):
|
|
57
|
+
if int(i1) >= int(i2): return ""
|
|
58
|
+
raise Exception()
|
|
59
|
+
def LE(i1, i2):
|
|
60
|
+
if int(i1) <= int(i2): return ""
|
|
61
|
+
raise Exception()
|
|
62
|
+
def NE(i1, i2):
|
|
63
|
+
if int(i1) != int(i2): return ""
|
|
64
|
+
raise Exception()
|
|
65
|
+
|
|
66
|
+
# ── String comparison functions ────────────────────────────────────────────────
|
|
67
|
+
def LGT(s1, s2):
|
|
68
|
+
if str(s1) > str(s2): return ""
|
|
69
|
+
raise Exception()
|
|
70
|
+
def LLT(s1, s2):
|
|
71
|
+
if str(s1) < str(s2): return ""
|
|
72
|
+
raise Exception()
|
|
73
|
+
def LEQ(s1, s2):
|
|
74
|
+
if str(s1) == str(s2): return ""
|
|
75
|
+
raise Exception()
|
|
76
|
+
def LGE(s1, s2):
|
|
77
|
+
if str(s1) >= str(s2): return ""
|
|
78
|
+
raise Exception()
|
|
79
|
+
def LLE(s1, s2):
|
|
80
|
+
if str(s1) <= str(s2): return ""
|
|
81
|
+
raise Exception()
|
|
82
|
+
def LNE(s1, s2):
|
|
83
|
+
if str(s1) != str(s2): return ""
|
|
84
|
+
raise Exception()
|
|
85
|
+
|
|
86
|
+
# ── Identity / difference ──────────────────────────────────────────────────────
|
|
87
|
+
def IDENT(d1, d2):
|
|
88
|
+
if d1 is d2: return ""
|
|
89
|
+
raise Exception()
|
|
90
|
+
def DIFFER(d1, d2):
|
|
91
|
+
if d1 is not d2: return ""
|
|
92
|
+
raise Exception()
|
|
93
|
+
|
|
94
|
+
# ── String utilities ───────────────────────────────────────────────────────────
|
|
95
|
+
def LPAD(s1, i, s2=' '): return (' ' * (i - len(s1))) + s1
|
|
96
|
+
def RPAD(s1, i, s2=' '): return s1 + (' ' * (i - len(s1)))
|
|
97
|
+
def DUPL(s, i): return s * i
|
|
98
|
+
def REPLACE(s1, s2, s3): return str(s1).translate(str.maketrans(str(s2), str(s3)))
|
|
99
|
+
def REVERSE(s): return s[::-1]
|
|
100
|
+
def SIZE(s): return len(s)
|
|
101
|
+
def TRIM(s): return s.strip()
|
|
102
|
+
|
|
103
|
+
def SUBSTITUTE(subject, slyce, replacement):
|
|
104
|
+
subject = str(subject)
|
|
105
|
+
return f"{subject[:slyce.start]}{replacement}{subject[slyce.stop:]}"
|
|
106
|
+
|
|
107
|
+
# ── Type / conversion functions ────────────────────────────────────────────────
|
|
108
|
+
def ASCII(c): return ord(c)
|
|
109
|
+
def CHAR(i): return chr(i)
|
|
110
|
+
def CODE(s): return compile(s, '<SNOBOL4>', 'exec')
|
|
111
|
+
def COLLECT(i): return gc.collect()
|
|
112
|
+
def COPY(d):
|
|
113
|
+
import copy
|
|
114
|
+
return copy.copy(d)
|
|
115
|
+
def DATATYPE(d): return type(d).__name__
|
|
116
|
+
def DATE(): return '{:%Y-%m-%d}'.format(date.today())
|
|
117
|
+
def TIME(): return (time.time_ns() // 1000) - _started
|
|
118
|
+
|
|
119
|
+
def INTEGER(d):
|
|
120
|
+
try: int(d); return ""
|
|
121
|
+
except (ValueError, TypeError): return None
|
|
122
|
+
|
|
123
|
+
def ITEM(d, *args):
|
|
124
|
+
match len(args):
|
|
125
|
+
case 1: return d[args[0]]
|
|
126
|
+
case 2: return d[args[0]][args[1]]
|
|
127
|
+
case 3: return d[args[0]][args[1]][args[2]]
|
|
128
|
+
case _: raise Exception()
|
|
129
|
+
|
|
130
|
+
def REMDR(i1, i2): return i1 % i2
|
|
131
|
+
def SORT(d): return d
|
|
132
|
+
def RSORT(d): return d
|
|
133
|
+
def TABLE(i1, i2): return dict()
|
|
134
|
+
|
|
135
|
+
def ARRAY(proto, d):
|
|
136
|
+
limits = tuple(int(x) for x in proto.split(','))
|
|
137
|
+
match len(limits):
|
|
138
|
+
case 1: return [d] * limits[0]
|
|
139
|
+
case 2: return [[d] * limits[1]] * limits[0]
|
|
140
|
+
case 3: return [[[d] * limits[2]] * limits[1]] * limits[0]
|
|
141
|
+
case _: raise Exception()
|
|
142
|
+
|
|
143
|
+
def CONVERT(d, s):
|
|
144
|
+
match s.upper():
|
|
145
|
+
case 'STRING':
|
|
146
|
+
match type(d).__name__:
|
|
147
|
+
case 'int' | 'float': return str(d)
|
|
148
|
+
case 'str' : return d
|
|
149
|
+
case 'list' : return 'ARRAY(' + PROTOTYPE(d) + ')'
|
|
150
|
+
case 'dict' : return 'TABLE(' + str(len(d)) + ')'
|
|
151
|
+
case _ : return type(d).__name__
|
|
152
|
+
case 'INTEGER': return int(d)
|
|
153
|
+
case 'REAL': return float(d)
|
|
154
|
+
case 'EXPRESSION': return compile(str(d), '<CONVERT>', 'single')
|
|
155
|
+
case 'CODE': return compile(str(d), '<CONVERT>', 'exec')
|
|
156
|
+
case _: return d
|
|
157
|
+
|
|
158
|
+
_re_proto = re.compile(r"\<function\ ([^\s]+)\ at\ 0x[0-9a-fA-F]+\>\(\*([0-9]+)\)")
|
|
159
|
+
def PROTOTYPE(P):
|
|
160
|
+
p = repr(P)
|
|
161
|
+
r = _re_proto.fullmatch(p)
|
|
162
|
+
if r: return f"{r.group(1)}(*{r.group(2)})"
|
|
163
|
+
return p
|
|
164
|
+
|
|
165
|
+
# ── Environment-dependent functions ───────────────────────────────────────────
|
|
166
|
+
# All of these read _env._g — the single shared SNOBOL environment dict.
|
|
167
|
+
|
|
168
|
+
def DUMP(i):
|
|
169
|
+
if int(i) != 0: print(_env._g)
|
|
170
|
+
|
|
171
|
+
def EVAL(s):
|
|
172
|
+
return eval(s, _env._g)
|
|
173
|
+
|
|
174
|
+
def EXEC(s):
|
|
175
|
+
return exec(s, _env._g)
|
|
176
|
+
|
|
177
|
+
def VALUE(n):
|
|
178
|
+
return _env._g[n]
|
|
179
|
+
|
|
180
|
+
# ── I/O association ───────────────────────────────────────────────────────────
|
|
181
|
+
def INPUT(n, u, len=None, fname=None):
|
|
182
|
+
if not u: u = 0
|
|
183
|
+
match u:
|
|
184
|
+
case 0: _env._g[n] = None; _units[u] = (n, sys.stdin)
|
|
185
|
+
case 1 | 2: raise Exception()
|
|
186
|
+
case _: _env._g[n] = None; _units[u] = (n, open(fname, "rt"))
|
|
187
|
+
return ""
|
|
188
|
+
|
|
189
|
+
def OUTPUT(n, u, len=None, fname=None):
|
|
190
|
+
if not u: u = 1
|
|
191
|
+
match u:
|
|
192
|
+
case 0: raise Exception()
|
|
193
|
+
case 1: _env._g[n] = None; _units[u] = (n, sys.stdout)
|
|
194
|
+
case 2: _env._g[n] = None; _units[u] = (n, sys.stderr)
|
|
195
|
+
case _: _env._g[n] = None; _units[u] = (n, open(fname, "wt"))
|
|
196
|
+
return ""
|
|
197
|
+
|
|
198
|
+
def DETACH(n):
|
|
199
|
+
del _env._g[n]
|
|
200
|
+
|
|
201
|
+
def ENDFILE(u):
|
|
202
|
+
if not u: u = 0
|
|
203
|
+
match u:
|
|
204
|
+
case 0 | 1 | 2:
|
|
205
|
+
del _env._g[_units[u][0]]; del _units[u]
|
|
206
|
+
case _:
|
|
207
|
+
del _env._g[_units[u][0]]; _units[u][1].close(); del _units[u]
|
|
208
|
+
return ""
|
|
209
|
+
|
|
210
|
+
def BACKSPACE(u): pass # backspace one record
|
|
211
|
+
def REWIND(): pass # reposition to first file
|
|
212
|
+
|
|
213
|
+
# ── DEFINE / APPLY ────────────────────────────────────────────────────────────
|
|
214
|
+
_rex_define = re.compile(r"^(\w+)\((\w+(?:,\w+)*)\)(\w+(?:,\w+)*)$")
|
|
215
|
+
|
|
216
|
+
def DEFINE(proto, n=None):
|
|
217
|
+
m = _rex_define.fullmatch(proto)
|
|
218
|
+
if not m:
|
|
219
|
+
return None
|
|
220
|
+
func_name = m.group(1)
|
|
221
|
+
func_params = tuple(p for p in m.group(2).split(','))
|
|
222
|
+
# func_locals = m.group(3) — reserved for future use
|
|
223
|
+
params = ', '.join(func_params)
|
|
224
|
+
body = f'def {func_name}({params}):\n print({params})'
|
|
225
|
+
code = compile(body, '<DEFINE>', 'exec')
|
|
226
|
+
func = types.FunctionType(code.co_consts[0], _env._g, func_name)
|
|
227
|
+
func.__defaults__ = (None,) * len(func_params)
|
|
228
|
+
_env._g[func_name] = func
|
|
229
|
+
return ""
|
|
230
|
+
|
|
231
|
+
def APPLY(n, *args): return _env._g[n](*args)
|
|
232
|
+
def ARG(n, i): pass
|
|
233
|
+
def LOCAL(n, i): pass
|
|
234
|
+
def LOAD(proto, lib): pass
|
|
235
|
+
def UNLOAD(s): pass
|
|
236
|
+
|
|
237
|
+
# ── DATA / FIELD ──────────────────────────────────────────────────────────────
|
|
238
|
+
_rex_data = re.compile(r"^(\w+)\((\w+(?:,\w+)*)\)$")
|
|
239
|
+
|
|
240
|
+
def FIELD(s, i): return s.__slots__[int(i)]
|
|
241
|
+
|
|
242
|
+
def DATA(s):
|
|
243
|
+
m = _rex_data.fullmatch(s)
|
|
244
|
+
if not m:
|
|
245
|
+
return None
|
|
246
|
+
name = m.group(1)
|
|
247
|
+
fields = tuple(f for f in m.group(2).split(','))
|
|
248
|
+
def __init__(self, *args):
|
|
249
|
+
for i, value in enumerate(args):
|
|
250
|
+
setattr(self, self.__slots__[i], value)
|
|
251
|
+
_env._g[name] = type(name, (object,), {'__slots__': fields, '__init__': __init__})
|
|
252
|
+
return ""
|
|
253
|
+
|
|
254
|
+
# ── Control-flow stubs (meaningful only in a full SNOBOL4 runtime) ────────────
|
|
255
|
+
def END(): pass
|
|
256
|
+
def RETURN(): pass
|
|
257
|
+
def FRETURN(): pass
|
|
258
|
+
def NRETURN(): pass
|
|
259
|
+
|
|
260
|
+
# ── Stubs for unimplemented features ──────────────────────────────────────────
|
|
261
|
+
def OPSYN(s1, s2, i): pass
|
|
262
|
+
def STOPTR(n, t): pass
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# SNOBOL4patterns.py — public shim for SNOBOL4python 0.5.0
|
|
3
|
+
#
|
|
4
|
+
# This file no longer contains the engine itself. It re-exports everything
|
|
5
|
+
# from whichever backend is currently active (_backend_c or _backend_pure),
|
|
6
|
+
# as determined by SNOBOL4python._backend at import time.
|
|
7
|
+
#
|
|
8
|
+
# To inspect or switch backends at runtime:
|
|
9
|
+
#
|
|
10
|
+
# import SNOBOL4python
|
|
11
|
+
# print(SNOBOL4python.current_backend()) # 'c' or 'pure'
|
|
12
|
+
# SNOBOL4python.use_pure() # switch to pure-Python
|
|
13
|
+
# SNOBOL4python.use_c() # switch back to C (if available)
|
|
14
|
+
#
|
|
15
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
16
|
+
from ._backend import * # noqa: F401, F403
|
|
17
|
+
from ._backend import ( # noqa: F401 (explicit for IDEs & type checkers)
|
|
18
|
+
F, PATTERN, STRING, NULL, Ϩ, Γ,
|
|
19
|
+
ε, σ, FAIL, ABORT, SUCCEED,
|
|
20
|
+
α, ω, ARB, MARB, BAL, REM, FENCE,
|
|
21
|
+
ANY, NOTANY, SPAN, BREAK, BREAKX, NSPAN,
|
|
22
|
+
POS, RPOS, LEN, TAB, RTAB,
|
|
23
|
+
ARBNO, MARBNO, π,
|
|
24
|
+
Σ, Π, ρ,
|
|
25
|
+
δ, Δ, Θ, θ, Λ, λ, ζ, Φ, φ,
|
|
26
|
+
nPush, nInc, nPop, Shift, Reduce, Pop,
|
|
27
|
+
GLOBALS, TRACE, SEARCH, MATCH, FULLMATCH,
|
|
28
|
+
# backend meta
|
|
29
|
+
C_AVAILABLE, use_c, use_pure, current_backend,
|
|
30
|
+
)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# SNOBOL4python 0.5.0
|
|
3
|
+
#
|
|
4
|
+
# The SNOBOL4 environment (variable namespace) is a single flat dict kept in
|
|
5
|
+
# _env._g. Set it once with GLOBALS(globals()); all pattern assignments,
|
|
6
|
+
# built-in functions, and deferred evaluations share that one reference.
|
|
7
|
+
# No module in this package keeps its own copy.
|
|
8
|
+
#
|
|
9
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
# ── pattern engine (backend-agnostic shim) ────────────────────────────────────
|
|
12
|
+
from .SNOBOL4patterns import (
|
|
13
|
+
GLOBALS, TRACE,
|
|
14
|
+
F, PATTERN, STRING, NULL, Ϩ, Γ,
|
|
15
|
+
ε, σ, π, λ, Λ, ζ, θ, Θ, φ, Φ, α, ω,
|
|
16
|
+
ABORT, ANY, ARB, ARBNO, BAL, BREAK, BREAKX, FAIL,
|
|
17
|
+
FENCE, LEN, MARB, MARBNO, NOTANY, NSPAN, POS, REM, RPOS,
|
|
18
|
+
RTAB, SPAN, SUCCEED, TAB,
|
|
19
|
+
nPush, nInc, nPop, Shift, Reduce, Pop,
|
|
20
|
+
Σ, Π, ρ, Δ, δ,
|
|
21
|
+
SEARCH, MATCH, FULLMATCH,
|
|
22
|
+
# backend control
|
|
23
|
+
C_AVAILABLE, use_c, use_pure, current_backend,
|
|
24
|
+
set_match_stack_size, DEFAULT_MATCH_STACK_SIZE,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
# ── built-in functions ────────────────────────────────────────────────────────
|
|
28
|
+
from .SNOBOL4functions import (
|
|
29
|
+
ALPHABET, DIGITS, UCASE, LCASE,
|
|
30
|
+
DEFINE, APPLY, REPLACE, SUBSTITUTE,
|
|
31
|
+
CHAR, DIFFER, IDENT, INTEGER,
|
|
32
|
+
END, RETURN, FRETURN, NRETURN,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
__version__ = '0.5.0'
|
|
36
|
+
__author__ = 'Lon Jones Cherryholmes'
|
|
37
|
+
|
|
38
|
+
__all__ = [
|
|
39
|
+
# backend control
|
|
40
|
+
'C_AVAILABLE', 'use_c', 'use_pure', 'current_backend',
|
|
41
|
+
'set_match_stack_size', 'DEFAULT_MATCH_STACK_SIZE',
|
|
42
|
+
# environment
|
|
43
|
+
'GLOBALS', 'TRACE',
|
|
44
|
+
# core types
|
|
45
|
+
'F', 'PATTERN', 'STRING', 'NULL', 'Ϩ', 'Γ',
|
|
46
|
+
# Greek-letter pattern constructors
|
|
47
|
+
'ε', 'σ', 'π', 'λ', 'Λ', 'ζ', 'θ', 'Θ', 'φ', 'Φ', 'α', 'ω',
|
|
48
|
+
'Σ', 'Π', 'ρ', 'Δ', 'δ',
|
|
49
|
+
# named pattern constructors
|
|
50
|
+
'ABORT', 'ANY', 'ARB', 'ARBNO', 'BAL', 'BREAK', 'BREAKX', 'FAIL',
|
|
51
|
+
'FENCE', 'LEN', 'MARB', 'MARBNO', 'NOTANY', 'NSPAN', 'POS', 'REM', 'RPOS',
|
|
52
|
+
'RTAB', 'SPAN', 'SUCCEED', 'TAB',
|
|
53
|
+
# shift-reduce parser stack
|
|
54
|
+
'nPush', 'nInc', 'nPop', 'Shift', 'Reduce', 'Pop',
|
|
55
|
+
# match API
|
|
56
|
+
'SEARCH', 'MATCH', 'FULLMATCH',
|
|
57
|
+
# built-in string constants
|
|
58
|
+
'ALPHABET', 'DIGITS', 'UCASE', 'LCASE', 'NULL',
|
|
59
|
+
# built-in functions
|
|
60
|
+
'DEFINE', 'APPLY', 'REPLACE', 'SUBSTITUTE',
|
|
61
|
+
'CHAR', 'DIFFER', 'IDENT', 'INTEGER',
|
|
62
|
+
'END', 'RETURN', 'FRETURN', 'NRETURN',
|
|
63
|
+
]
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# SNOBOL4python — backend selector
|
|
3
|
+
#
|
|
4
|
+
# This module decides which pattern-matching engine is loaded and provides
|
|
5
|
+
# use_c() / use_pure() / current_backend() for runtime switching.
|
|
6
|
+
#
|
|
7
|
+
# Resolution order for the initial backend:
|
|
8
|
+
# 1. Environment variable SNOBOL4_BACKEND = 'c' | 'pure'
|
|
9
|
+
# 2. If unset: try the C/SPIPAT extension; fall back to pure-Python.
|
|
10
|
+
#
|
|
11
|
+
# The selected backend is exposed as the module-level name `backend`
|
|
12
|
+
# ('c' or 'pure'), and all public symbols are re-exported from here so
|
|
13
|
+
# that SNOBOL4patterns.py can do a single "from ._backend import *".
|
|
14
|
+
#
|
|
15
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
16
|
+
import os as _os
|
|
17
|
+
import sys as _sys
|
|
18
|
+
import importlib as _importlib
|
|
19
|
+
from . import _env
|
|
20
|
+
|
|
21
|
+
# ── env and backend state ────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
_C_MODULE_NAME = 'sno4py' # the compiled extension
|
|
24
|
+
_BACKEND_C_PKG = 'SNOBOL4python._backend_c'
|
|
25
|
+
_BACKEND_PURE_PKG = 'SNOBOL4python._backend_pure'
|
|
26
|
+
|
|
27
|
+
_current: str = '' # 'c' or 'pure'
|
|
28
|
+
_mod = None # currently active backend module
|
|
29
|
+
|
|
30
|
+
# ── public names exported from whichever backend is loaded ────────────────────
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
# meta
|
|
34
|
+
'backend', 'C_AVAILABLE', 'use_c', 'use_pure', 'current_backend',
|
|
35
|
+
'set_match_stack_size', 'DEFAULT_MATCH_STACK_SIZE',
|
|
36
|
+
# patterns & types
|
|
37
|
+
'F', 'PATTERN', 'STRING', 'NULL', 'Ϩ', 'Γ',
|
|
38
|
+
'ε', 'σ', 'FAIL', 'ABORT', 'SUCCEED',
|
|
39
|
+
'α', 'ω', 'ARB', 'MARB', 'BAL', 'REM', 'FENCE',
|
|
40
|
+
'ANY', 'NOTANY', 'SPAN', 'BREAK', 'BREAKX', 'NSPAN',
|
|
41
|
+
'POS', 'RPOS', 'LEN', 'TAB', 'RTAB',
|
|
42
|
+
'ARBNO', 'MARBNO', 'π',
|
|
43
|
+
'Σ', 'Π', 'ρ',
|
|
44
|
+
'δ', 'Δ', 'Θ', 'θ', 'Λ', 'λ', 'ζ', 'Φ', 'φ',
|
|
45
|
+
'nPush', 'nInc', 'nPop', 'Shift', 'Reduce', 'Pop',
|
|
46
|
+
# API
|
|
47
|
+
'GLOBALS', 'TRACE', 'SEARCH', 'MATCH', 'FULLMATCH',
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _c_available() -> bool:
|
|
52
|
+
"""True if the sno4py C extension can be imported."""
|
|
53
|
+
try:
|
|
54
|
+
_importlib.import_module(_C_MODULE_NAME)
|
|
55
|
+
return True
|
|
56
|
+
except ImportError:
|
|
57
|
+
return False
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
C_AVAILABLE: bool = _c_available()
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _load_backend(name: str):
|
|
64
|
+
"""Import and activate one backend module ('c' or 'pure')."""
|
|
65
|
+
global _current, _mod
|
|
66
|
+
|
|
67
|
+
if name == 'c':
|
|
68
|
+
if not C_AVAILABLE:
|
|
69
|
+
raise ImportError(
|
|
70
|
+
"The sno4py C extension is not available on this system. "
|
|
71
|
+
"Use use_pure() or install sno4py."
|
|
72
|
+
)
|
|
73
|
+
pkg = _BACKEND_C_PKG
|
|
74
|
+
elif name == 'pure':
|
|
75
|
+
pkg = _BACKEND_PURE_PKG
|
|
76
|
+
else:
|
|
77
|
+
raise ValueError(f"Unknown backend {name!r}: choose 'c' or 'pure'.")
|
|
78
|
+
|
|
79
|
+
mod = _importlib.import_module(pkg)
|
|
80
|
+
_current = name
|
|
81
|
+
_mod = mod
|
|
82
|
+
return mod
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _inject(mod):
|
|
86
|
+
"""
|
|
87
|
+
Copy every public symbol from *mod* into this module's namespace so that
|
|
88
|
+
callers who did `from ._backend import *` earlier pick up the new ones.
|
|
89
|
+
We also update the SNOBOL4python package namespace if it's already loaded.
|
|
90
|
+
"""
|
|
91
|
+
_skip = ('backend', 'C_AVAILABLE', 'use_c', 'use_pure', 'current_backend', 'set_match_stack_size', 'DEFAULT_MATCH_STACK_SIZE')
|
|
92
|
+
g = _sys.modules[__name__].__dict__
|
|
93
|
+
for name in __all__:
|
|
94
|
+
if name in _skip:
|
|
95
|
+
continue
|
|
96
|
+
obj = getattr(mod, name, None)
|
|
97
|
+
if obj is not None:
|
|
98
|
+
g[name] = obj
|
|
99
|
+
|
|
100
|
+
# propagate into the parent package if already initialised
|
|
101
|
+
pkg_mod = _sys.modules.get('SNOBOL4python')
|
|
102
|
+
if pkg_mod is not None:
|
|
103
|
+
for name in __all__:
|
|
104
|
+
if name in _skip:
|
|
105
|
+
continue
|
|
106
|
+
obj = getattr(mod, name, None)
|
|
107
|
+
if obj is not None:
|
|
108
|
+
setattr(pkg_mod, name, obj)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
# ── GLOBALS — defined here once, not per-backend ─────────────────────────────
|
|
113
|
+
# Both _backend_pure and _backend_c also define GLOBALS(g) as thin wrappers
|
|
114
|
+
# to _env.set(g), but this definition is the canonical one injected into the
|
|
115
|
+
# package namespace. Switching backends never changes what GLOBALS does.
|
|
116
|
+
|
|
117
|
+
def GLOBALS(g: dict) -> None:
|
|
118
|
+
"""
|
|
119
|
+
Register *g* as the SNOBOL environment — the single flat variable space
|
|
120
|
+
shared by all pattern operations, assignments, and built-in functions.
|
|
121
|
+
|
|
122
|
+
Call once at the top of your script/module:
|
|
123
|
+
|
|
124
|
+
from SNOBOL4python import *
|
|
125
|
+
GLOBALS(globals())
|
|
126
|
+
|
|
127
|
+
If you switch backends with use_c() / use_pure(), call GLOBALS(globals())
|
|
128
|
+
again so the new backend's SEARCH sees the correct namespace.
|
|
129
|
+
"""
|
|
130
|
+
_env.set(g)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
# ── public API ────────────────────────────────────────────────────────────────
|
|
134
|
+
|
|
135
|
+
def use_c() -> None:
|
|
136
|
+
"""
|
|
137
|
+
Switch to the C/SPIPAT backend.
|
|
138
|
+
|
|
139
|
+
Raises ImportError if sno4py is not installed.
|
|
140
|
+
All subsequently constructed patterns will use the C engine.
|
|
141
|
+
Patterns built with the previous backend remain valid for the duration of
|
|
142
|
+
any in-progress match but should not be mixed with new ones.
|
|
143
|
+
"""
|
|
144
|
+
mod = _load_backend('c')
|
|
145
|
+
_inject(mod)
|
|
146
|
+
# Re-apply the stored stack size to the C extension
|
|
147
|
+
if _match_stack_size != DEFAULT_MATCH_STACK_SIZE:
|
|
148
|
+
import sno4py as _C
|
|
149
|
+
_C.set_stack_size(_match_stack_size)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def use_pure() -> None:
|
|
153
|
+
"""
|
|
154
|
+
Switch to the pure-Python backend.
|
|
155
|
+
|
|
156
|
+
Useful for debugging, testing, or deployment on platforms without a C
|
|
157
|
+
compiler. All subsequently constructed patterns will use the generator
|
|
158
|
+
engine from SNOBOL4python ≤ 0.4.x.
|
|
159
|
+
"""
|
|
160
|
+
mod = _load_backend('pure')
|
|
161
|
+
_inject(mod)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def current_backend() -> str:
|
|
165
|
+
"""Return 'c' or 'pure'."""
|
|
166
|
+
return _current
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
DEFAULT_MATCH_STACK_SIZE: int = 10_000
|
|
170
|
+
_match_stack_size: int = DEFAULT_MATCH_STACK_SIZE
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def set_match_stack_size(n: int) -> None:
|
|
174
|
+
"""
|
|
175
|
+
Set the number of entries in the internal pattern-match backtracking stack.
|
|
176
|
+
|
|
177
|
+
The default (10,000) is sufficient for virtually all patterns. Each entry
|
|
178
|
+
is 16 bytes, so the default allocates ~160 KB per match call.
|
|
179
|
+
|
|
180
|
+
Increase this only if you hit a ``RuntimeError: Pattern stack overflow``
|
|
181
|
+
with extremely deep or highly recursive patterns. Decrease it on very
|
|
182
|
+
memory-constrained systems (minimum 64).
|
|
183
|
+
|
|
184
|
+
Takes effect immediately for all subsequent matches. Has no effect when
|
|
185
|
+
the pure-Python backend is active (it uses Python's own call stack).
|
|
186
|
+
|
|
187
|
+
Example::
|
|
188
|
+
|
|
189
|
+
import SNOBOL4python as S4
|
|
190
|
+
S4.set_match_stack_size(50_000) # allow deeper backtracking
|
|
191
|
+
"""
|
|
192
|
+
global _match_stack_size
|
|
193
|
+
if not isinstance(n, int) or n < 64:
|
|
194
|
+
raise ValueError("match stack size must be an integer >= 64")
|
|
195
|
+
_match_stack_size = n
|
|
196
|
+
if _current == 'c' and C_AVAILABLE:
|
|
197
|
+
import sno4py as _C
|
|
198
|
+
_C.set_stack_size(n)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
# ── property-style shim so ``backend`` reads the live value ──────────────────
|
|
202
|
+
|
|
203
|
+
class _BackendDescriptor:
|
|
204
|
+
"""Makes ``from SNOBOL4python._backend import backend`` always current."""
|
|
205
|
+
def __get__(self, obj, objtype=None):
|
|
206
|
+
return _current
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
class _BackendModule(_sys.modules[__name__].__class__):
|
|
210
|
+
"""Module subclass so `SNOBOL4python._backend.backend` is always live."""
|
|
211
|
+
@property
|
|
212
|
+
def backend(self):
|
|
213
|
+
return _current
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
_sys.modules[__name__].__class__ = _BackendModule
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
# ── initial backend selection ────────────────────────────────────────────────
|
|
220
|
+
|
|
221
|
+
def _resolve_initial() -> str:
|
|
222
|
+
env = _os.environ.get('SNOBOL4_BACKEND', '').strip().lower()
|
|
223
|
+
if env == 'pure':
|
|
224
|
+
return 'pure'
|
|
225
|
+
if env == 'c':
|
|
226
|
+
if not C_AVAILABLE:
|
|
227
|
+
import warnings
|
|
228
|
+
warnings.warn(
|
|
229
|
+
"SNOBOL4_BACKEND=c requested but sno4py is not available; "
|
|
230
|
+
"falling back to pure-Python backend.",
|
|
231
|
+
RuntimeWarning,
|
|
232
|
+
stacklevel=2,
|
|
233
|
+
)
|
|
234
|
+
return 'pure'
|
|
235
|
+
return 'c'
|
|
236
|
+
# auto: prefer C if available
|
|
237
|
+
return 'c' if C_AVAILABLE else 'pure'
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
_load_backend(_resolve_initial())
|
|
241
|
+
_inject(_mod)
|