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.
@@ -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)