snail-lang 0.7.2__cp38-abi3-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,49 @@
1
+ from __future__ import annotations
2
+
3
+ import operator as _op
4
+
5
+ _OPS = {
6
+ "+": _op.add,
7
+ "-": _op.sub,
8
+ "*": _op.mul,
9
+ "/": _op.truediv,
10
+ "//": _op.floordiv,
11
+ "%": _op.mod,
12
+ "**": _op.pow,
13
+ }
14
+
15
+
16
+ def _apply_op(left, right, op: str):
17
+ try:
18
+ func = _OPS[op]
19
+ except KeyError as exc:
20
+ raise ValueError(f"unknown augmented op: {op}") from exc
21
+ return func(left, right)
22
+
23
+
24
+ def __snail_incr_attr(obj, attr: str, delta: int, pre: bool):
25
+ old = getattr(obj, attr)
26
+ new = old + delta
27
+ setattr(obj, attr, new)
28
+ return new if pre else old
29
+
30
+
31
+ def __snail_incr_index(obj, index, delta: int, pre: bool):
32
+ old = obj[index]
33
+ new = old + delta
34
+ obj[index] = new
35
+ return new if pre else old
36
+
37
+
38
+ def __snail_aug_attr(obj, attr: str, value, op: str):
39
+ old = getattr(obj, attr)
40
+ new = _apply_op(old, value, op)
41
+ setattr(obj, attr, new)
42
+ return new
43
+
44
+
45
+ def __snail_aug_index(obj, index, value, op: str):
46
+ old = obj[index]
47
+ new = _apply_op(old, value, op)
48
+ obj[index] = new
49
+ return new
@@ -0,0 +1,13 @@
1
+ from __future__ import annotations
2
+
3
+
4
+ def compact_try(expr_fn, fallback_fn=None):
5
+ try:
6
+ return expr_fn()
7
+ except Exception as exc:
8
+ if fallback_fn is None:
9
+ fallback_member = getattr(exc, "__fallback__", None)
10
+ if callable(fallback_member):
11
+ return fallback_member()
12
+ return None
13
+ return fallback_fn(exc)
snail/runtime/env.py ADDED
@@ -0,0 +1,26 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+
5
+
6
+ class EnvMap:
7
+ __slots__ = ("_env",)
8
+
9
+ def __init__(self, env=None) -> None:
10
+ self._env = os.environ if env is None else env
11
+
12
+ def __fallback__(self) -> str:
13
+ return ""
14
+
15
+ def _lookup(self, key):
16
+ try:
17
+ return self._env[key]
18
+ except KeyError as exc:
19
+ exc.__fallback__ = self.__fallback__
20
+ raise
21
+
22
+ def __getitem__(self, key):
23
+ return self._lookup(key)
24
+
25
+ def __getattr__(self, name):
26
+ return self._lookup(name)
@@ -0,0 +1,41 @@
1
+ """Lazy file opener for map mode."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ class LazyFile:
7
+ """Context manager that opens the file on first access."""
8
+
9
+ __slots__ = ("_path", "_mode", "_kwargs", "_fd", "_closed")
10
+
11
+ def __init__(self, path, mode="r", **kwargs):
12
+ self._path = path
13
+ self._mode = mode
14
+ self._kwargs = kwargs
15
+ self._fd = None
16
+ self._closed = False
17
+
18
+ def _ensure_open(self):
19
+ if self._closed:
20
+ raise ValueError("I/O operation on closed file.")
21
+ if self._fd is None:
22
+ self._fd = open(self._path, self._mode, **self._kwargs)
23
+ return self._fd
24
+
25
+ def __enter__(self):
26
+ return self
27
+
28
+ def __exit__(self, exc_type, exc, tb):
29
+ self._closed = True
30
+ if self._fd is not None:
31
+ self._fd.close()
32
+ return False
33
+
34
+ def __getattr__(self, name):
35
+ return getattr(self._ensure_open(), name)
36
+
37
+ def __iter__(self):
38
+ return iter(self._ensure_open())
39
+
40
+ def __next__(self):
41
+ return next(self._ensure_open())
@@ -0,0 +1,50 @@
1
+ """Lazy text content reader for map mode."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ class LazyText:
7
+ """Lazily reads file content on first access."""
8
+
9
+ __slots__ = ("_fd", "_text")
10
+
11
+ def __init__(self, fd):
12
+ self._fd = fd
13
+ self._text = None
14
+
15
+ def _ensure_loaded(self):
16
+ if self._text is None:
17
+ self._text = self._fd.read()
18
+ return self._text
19
+
20
+ def __str__(self):
21
+ return self._ensure_loaded()
22
+
23
+ def __repr__(self):
24
+ return repr(str(self))
25
+
26
+ def __eq__(self, other):
27
+ if isinstance(other, LazyText):
28
+ return str(self) == str(other)
29
+ return str(self) == other
30
+
31
+ def __hash__(self):
32
+ return hash(str(self))
33
+
34
+ def __len__(self):
35
+ return len(str(self))
36
+
37
+ def __iter__(self):
38
+ return iter(str(self))
39
+
40
+ def __contains__(self, item):
41
+ return item in str(self)
42
+
43
+ def __add__(self, other):
44
+ return str(self) + other
45
+
46
+ def __radd__(self, other):
47
+ return other + str(self)
48
+
49
+ def __getattr__(self, name):
50
+ return getattr(str(self), name)
snail/runtime/regex.py ADDED
@@ -0,0 +1,37 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+
5
+
6
+ class SnailRegex:
7
+ def __init__(self, pattern: str) -> None:
8
+ self.pattern = pattern
9
+ self._regex = re.compile(pattern)
10
+
11
+ def search(self, value):
12
+ return regex_search(value, self._regex)
13
+
14
+ def __contains__(self, value):
15
+ return bool(self.__snail_contains__(value))
16
+
17
+ def __snail_contains__(self, value):
18
+ return self.search(value)
19
+
20
+ def __repr__(self) -> str:
21
+ return f"/{self.pattern}/"
22
+
23
+
24
+ def regex_search(value, pattern):
25
+ if isinstance(pattern, SnailRegex):
26
+ return pattern.search(value)
27
+ if hasattr(pattern, "search"):
28
+ match = pattern.search(value)
29
+ else:
30
+ match = re.search(pattern, value)
31
+ if match is None:
32
+ return ()
33
+ return (match.group(0),) + match.groups()
34
+
35
+
36
+ def regex_compile(pattern):
37
+ return SnailRegex(pattern)
@@ -0,0 +1,74 @@
1
+ from __future__ import annotations
2
+
3
+ import json as _json
4
+ import os as _os
5
+ import sys as _sys
6
+
7
+ def __snail_jmespath_query(query: str):
8
+ """Create a callable that applies JMESPath query.
9
+
10
+ Used by the $[query] syntax which lowers to __snail_jmespath_query(query).
11
+ """
12
+
13
+ import jmespath as _jmespath
14
+
15
+ def apply(data):
16
+ return _jmespath.search(query, data)
17
+
18
+ return apply
19
+
20
+
21
+ def _parse_jsonl(content: str):
22
+ lines = [line for line in content.splitlines() if line.strip()]
23
+ if not lines:
24
+ return []
25
+
26
+ items = []
27
+ for line in lines:
28
+ try:
29
+ items.append(_json.loads(line))
30
+ except _json.JSONDecodeError as exc:
31
+ raise _json.JSONDecodeError(
32
+ f"Invalid JSONL line: {exc.msg}",
33
+ line,
34
+ exc.pos,
35
+ ) from exc
36
+ return items
37
+
38
+
39
+ def js(input_data=None):
40
+ """Parse JSON from various input sources.
41
+
42
+ Returns the parsed Python object (dict, list, etc.) directly.
43
+ If called with no arguments, reads from stdin.
44
+ """
45
+ if input_data is None:
46
+ input_data = _sys.stdin
47
+
48
+ if isinstance(input_data, str):
49
+ try:
50
+ return _json.loads(input_data)
51
+ except _json.JSONDecodeError:
52
+ if _os.path.exists(input_data):
53
+ with open(input_data, "r", encoding="utf-8") as handle:
54
+ content = handle.read()
55
+ try:
56
+ return _json.loads(content)
57
+ except _json.JSONDecodeError:
58
+ return _parse_jsonl(content)
59
+ else:
60
+ return _parse_jsonl(input_data)
61
+ elif hasattr(input_data, "read"):
62
+ content = input_data.read()
63
+ if isinstance(content, bytes):
64
+ content = content.decode("utf-8")
65
+ try:
66
+ return _json.loads(content)
67
+ except _json.JSONDecodeError:
68
+ return _parse_jsonl(content)
69
+ elif isinstance(input_data, (dict, list, int, float, bool)) or input_data is None:
70
+ return input_data
71
+ else:
72
+ raise TypeError(
73
+ f"js() input must be JSON-compatible, got {type(input_data).__name__}"
74
+ )
@@ -0,0 +1,66 @@
1
+ from __future__ import annotations
2
+
3
+ import subprocess
4
+
5
+
6
+ class SubprocessCapture:
7
+ def __init__(self, cmd: str) -> None:
8
+ self.cmd = cmd
9
+
10
+ def __call__(self, input_data=None):
11
+ try:
12
+ if input_data is None:
13
+ completed = subprocess.run(
14
+ self.cmd,
15
+ shell=True,
16
+ check=True,
17
+ text=True,
18
+ stdout=subprocess.PIPE,
19
+ )
20
+ else:
21
+ if not isinstance(input_data, (str, bytes)):
22
+ input_data = str(input_data)
23
+ completed = subprocess.run(
24
+ self.cmd,
25
+ shell=True,
26
+ check=True,
27
+ text=True,
28
+ input=input_data,
29
+ stdout=subprocess.PIPE,
30
+ )
31
+ return completed.stdout.rstrip("\n")
32
+ except subprocess.CalledProcessError as exc:
33
+
34
+ def __fallback(exc=exc):
35
+ raise exc
36
+
37
+ exc.__fallback__ = __fallback
38
+ raise
39
+
40
+
41
+ class SubprocessStatus:
42
+ def __init__(self, cmd: str) -> None:
43
+ self.cmd = cmd
44
+
45
+ def __call__(self, input_data=None):
46
+ try:
47
+ if input_data is None:
48
+ subprocess.run(self.cmd, shell=True, check=True)
49
+ else:
50
+ if not isinstance(input_data, (str, bytes)):
51
+ input_data = str(input_data)
52
+ subprocess.run(
53
+ self.cmd,
54
+ shell=True,
55
+ check=True,
56
+ text=True,
57
+ input=input_data,
58
+ )
59
+ return 0
60
+ except subprocess.CalledProcessError as exc:
61
+
62
+ def __fallback(exc=exc):
63
+ return exc.returncode
64
+
65
+ exc.__fallback__ = __fallback
66
+ raise
@@ -0,0 +1,329 @@
1
+ Metadata-Version: 2.4
2
+ Name: snail-lang
3
+ Version: 0.7.2
4
+ Requires-Dist: astunparse>=1.6.3 ; python_full_version < '3.9'
5
+ Requires-Dist: jmespath>=1.0.1
6
+ Requires-Dist: maturin>=1.5 ; extra == 'dev'
7
+ Requires-Dist: pytest ; extra == 'dev'
8
+ Provides-Extra: dev
9
+ License-File: LICENSE
10
+ Summary: Snail programming language interpreter
11
+ Requires-Python: >=3.8
12
+ Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
13
+
14
+ <p align="center">
15
+ <img src="logo.png" alt="Snail logo" width="200">
16
+ </p>
17
+
18
+ <h1 align="center">Snail</h1>
19
+
20
+ **Snail** is a programming language that compiles to Python, combining Python's familiarity and extensive libraries with Perl/awk-inspired syntax for quick scripts and one-liners. Its what you get when you shove a snake in a shell.
21
+
22
+ ## AI Slop!
23
+
24
+ Snail is me learning how to devlop code using LLMs. I think its neat, and
25
+ maybe useful. I don't think this is high quality. I am going to try and LLM my
26
+ way into something good, but its certainly not there yet.
27
+
28
+ ## Installing Snail
29
+
30
+ ```bash
31
+ pip install snail-lang
32
+ -or-
33
+ uv tool install snail-lang
34
+ ```
35
+
36
+ That installs the `snail` CLI for your user; try it with `snail "print('hello')"` once the install completes.
37
+
38
+ ## ✨ What Makes Snail Unique
39
+
40
+ ### Curly Braces, Not Indentation
41
+
42
+ Write Python logic without worrying about whitespace:
43
+
44
+ ```snail
45
+ def process(items) {
46
+ for item in items {
47
+ if item > 0 { print(item) }
48
+ else { continue }
49
+ }
50
+ }
51
+ ```
52
+
53
+ Note, since it is jarring to write python with semicolons everywhere,
54
+ semicolons are optional. You can separate statements with newlines.
55
+
56
+ ### Awk Mode
57
+
58
+ Process files line-by-line with familiar awk semantics:
59
+
60
+ ```snail-awk("hello world\nfoo bar\n")
61
+ BEGIN { print("start") }
62
+ /hello/ { print("matched:", $0) }
63
+ { print($1, "->", $2) }
64
+ END { print("done") }
65
+ ```
66
+
67
+ **Built-in variables:**
68
+
69
+ | Variable | Description |
70
+ |----------|-------------|
71
+ | `$0` | Current line (with newline stripped) |
72
+ | `$1`, `$2`, ... | Individual fields (whitespace-split) |
73
+ | `$f` | All fields as a list |
74
+ | `$n` | Global line number (across all files) |
75
+ | `$fn` | Per-file line number |
76
+ | `$p` | Current file path |
77
+ | `$m` | Last regex match object |
78
+
79
+
80
+ Begin/end blocks can live in the source file (`BEGIN { ... }` / `END { ... }`) or be supplied
81
+ via CLI flags (`-b`/`--begin`, `-e`/`--end`) for setup and teardown. CLI BEGIN blocks run
82
+ before in-file BEGIN blocks; CLI END blocks run after in-file END blocks.
83
+ `BEGIN` and `END` are reserved keywords in all modes.
84
+ BEGIN/END blocks are regular Snail blocks, so awk/map-only `$` variables are not available inside them.
85
+ ```bash
86
+ echo -e "5\n4\n3\n2\n1" | snail --awk --begin 'total = 0' --end 'print("Sum:", total)' '/^[0-9]+/ { total = total + int($1) }'
87
+ ```
88
+
89
+ ### Map Mode
90
+
91
+ Process files one at a time instead of line-by-line:
92
+
93
+ ```snail-map
94
+ BEGIN { print("start") }
95
+ print("File:", $src)
96
+ print("Size:", len($text), "bytes")
97
+ END { print("done") }
98
+ ```
99
+
100
+ **Built-in variables:**
101
+
102
+ | Variable | Description |
103
+ |----------|-------------|
104
+ | `$src` | Current file path |
105
+ | `$fd` | Open file handle for the current file |
106
+ | `$text` | Lazy text view of the current file contents |
107
+
108
+ Begin/end blocks can live in the source file (`BEGIN { ... }` / `END { ... }`) or be supplied
109
+ via CLI flags (`-b`/`--begin`, `-e`/`--end`) for setup and teardown. CLI BEGIN blocks run
110
+ before in-file BEGIN blocks; CLI END blocks run after in-file END blocks.
111
+ BEGIN/END blocks are regular Snail blocks, so awk/map-only `$` variables are not available inside them.
112
+ `BEGIN` and `END` are reserved keywords in all modes.
113
+ ```bash
114
+ snail --map --begin "print('start')" --end "print('done')" "print($src)" *.txt
115
+ ```
116
+
117
+ ### Built-in Variables (All Modes)
118
+
119
+ | Variable | Description |
120
+ |----------|-------------|
121
+ | `$e` | Exception object in `expr:fallback?` |
122
+ | `$env` | Environment map (wrapper around `os.environ`) |
123
+
124
+ ### Compact Error Handling
125
+
126
+ The `?` operator makes error handling terse yet expressive:
127
+
128
+ ```snail
129
+ # Swallow exception, return None
130
+ err = risky()?
131
+
132
+ # Swallow exception, return exception object
133
+ err = risky():$e?
134
+
135
+ # Provide a fallback value (exception available as $e)
136
+ value = js("malformed json"):%{"error": "invalid json"}?
137
+ details = fetch_url("foo.com"):"default html"?
138
+ exception_info = fetch_url("example.com"):$e.http_response_code?
139
+
140
+ # Access attributes directly
141
+ name = risky("")?.__class__.__name__
142
+ args = risky("becomes a list"):[1,2,3]?[0]
143
+ ```
144
+
145
+ ### Destructuring + `if let` / `while let`
146
+
147
+ Unpack tuples and lists directly, including Python-style rest bindings:
148
+
149
+ ```snail
150
+ x, *xs = [1, 2, 3]
151
+
152
+ if let [head, *tail] = [1, 2, 3]; head > 0 {
153
+ print(head, tail)
154
+ }
155
+ ```
156
+
157
+ `if let`/`while let` only enter the block when the destructuring succeeds. A guard
158
+ after `;` lets you add a boolean check that runs after the bindings are created.
159
+
160
+ Note that this syntax is more powerful than the walrus operator as that does
161
+ not allow for destructuring.
162
+
163
+
164
+ ### Pipeline Operator
165
+
166
+ The `|` operator enables data pipelining as syntactic sugar for nested
167
+ function calls. `x | y | z` becomes `z(y(x))`. This lets you stay in a
168
+ shell mindset.
169
+
170
+ ```snail
171
+ # Pipe data to subprocess stdin
172
+ result = "hello\nworld" | $(grep hello)
173
+
174
+ # Chain multiple transformations
175
+ output = "foo\nbar" | $(grep foo) | $(wc -l)
176
+
177
+ # Custom pipeline handlers
178
+ class Doubler {
179
+ def __call__(self, x) { return x * 2 }
180
+ }
181
+ doubled = 21 | Doubler() # yields 42
182
+ ```
183
+
184
+ Arbitrary callables make up pipelines, even if they have multiple parameters.
185
+ Snail supports this via placeholders.
186
+ ```snail
187
+ greeting = "World" | greet("Hello ", _) # greet("Hello ", "World")
188
+ excited = "World" | greet(_, "!") # greet("World", "!")
189
+ formal = "World" | greet("Hello ", suffix=_) # greet("Hello ", "World")
190
+ ```
191
+
192
+ When a pipeline targets a call expression, the left-hand value is passed to the
193
+ resulting callable. If the call includes a single `_` placeholder, Snail substitutes
194
+ the piped value at that position (including keyword arguments). Only one
195
+ placeholder is allowed in a piped call. Outside of pipeline calls, `_` remains a
196
+ normal identifier.
197
+
198
+ ### Built-in Subprocess
199
+
200
+ Shell commands are first-class citizens with capturing and non-capturing
201
+ forms.
202
+
203
+ ```snail
204
+ # Capture command output with interpolation
205
+ greeting = $(echo hello {name})
206
+
207
+ # Pipe data through commands
208
+ result = "foo\nbar\nbaz" | $(grep bar) | $(cat -n)
209
+
210
+ # Check command status
211
+ @(make build)? # returns exit code on failure instead of raising
212
+ ```
213
+
214
+
215
+ ### Regex Literals
216
+
217
+ Snail supports first class patterns. Think of them as an infinte set.
218
+
219
+ ```snail
220
+ if bad_email in /^[\w.]+@[\w.]+$/ {
221
+ print("Valid email")
222
+ }
223
+
224
+ # Compiled regex for reuse
225
+ pattern = /\d{3}-\d{4}/
226
+ match = pattern.search(phone)
227
+ match2 = "555-1212" in pattern
228
+ ```
229
+
230
+ Snail regexes don't return a match object, rather they return a tuple
231
+ containing all of the match groups, including group 0. Both `search` and `in`
232
+ return the same tuple (or `()` when there is no match).
233
+
234
+ ### JSON Queries with JMESPath
235
+
236
+ Parse and query JSON data with the `js()` function and structured pipeline accessor:
237
+
238
+ ```snail
239
+ # Parse JSON and query with $[jmespath]
240
+
241
+ # JSON query with JMESPath
242
+ data = js($(curl -s https://api.github.com/repos/sudonym1/snail))
243
+ counts = data | $[stargazers_count]
244
+
245
+ # Inline parsing and querying
246
+ result = js('{{"foo": 12}}') | $[foo]
247
+
248
+ # JSONL parsing returns a list
249
+ names = js('{{"name": "Ada"}}\n{{"name": "Lin"}}') | $[[*].name]
250
+ ```
251
+
252
+ ### Full Python Interoperability
253
+
254
+ Snail compiles to Python ASTβ€”import any Python module, use any library, in any
255
+ environment. Assuming that you are using Python 3.8 or later.
256
+
257
+ ## πŸš€ Quick Start
258
+
259
+ ```bash
260
+ # One-liner: arithmetic + interpolation
261
+ snail 'name="Snail"; print("{name} says: {6 * 7}")'
262
+
263
+ # JSON query with JMESPath
264
+ snail 'js($(curl -s https://api.github.com/repos/sudonym1/snail)) | $[stargazers_count]'
265
+
266
+ # Compact error handling with fallback
267
+ snail 'result = int("oops"):"bad int {$e}"?; print(result)'
268
+
269
+ # Regex match and capture
270
+ snail 'if let [_, user, domain] = "user@example.com" in /^[\w.]+@([\w.]+)$/ { print(domain) }'
271
+
272
+ # Awk mode: print line numbers for matches
273
+ rg -n "TODO" README.md | snail --awk '/TODO/ { print("{$n}: {$0}") }'
274
+
275
+ # Environment variables
276
+ snail 'print($env.PATH)'
277
+ ```
278
+
279
+ ## πŸ“š Documentation
280
+
281
+ Documentation is WIP
282
+
283
+ - **[Language Reference](docs/REFERENCE.md)** β€” Complete syntax and semantics
284
+ - **[examples/all_syntax.snail](examples/all_syntax.snail)** β€” Every feature in one file
285
+ - **[examples/awk.snail](examples/awk.snail)** β€” Awk mode examples
286
+ - **[examples/map.snail](examples/map.snail)** β€” Map mode examples
287
+
288
+ ## πŸ”Œ Editor Support
289
+
290
+ Vim/Neovim plugin with syntax highlighting, formatting, and run commands:
291
+
292
+ ```vim
293
+ Plug 'sudonym1/snail', { 'rtp': 'extras/vim' }
294
+ ```
295
+
296
+ See [extras/vim/README.md](extras/vim/README.md) for details. Tree-sitter grammar available in `extras/tree-sitter-snail/`.
297
+
298
+ ## Performance
299
+
300
+ Section is WIP
301
+
302
+ Startup performance is benchmarked with `./benchmarks/startup.py`. On my
303
+ machine snail adds 5 ms of overhead above the regular python3 interpreter.
304
+
305
+ ## πŸ› οΈ Building from Source
306
+
307
+ ### Prerequisites
308
+
309
+ **Python 3.8+** (required at runtime)
310
+
311
+ Snail runs in-process via a Pyo3 extension module, so it uses the active Python environment.
312
+
313
+ Installation per platform:
314
+ - **Ubuntu/Debian**: `sudo apt install python3 python3-dev`
315
+ - **Fedora/RHEL**: `sudo dnf install python3 python3-devel`
316
+ - **macOS**: `brew install python@3.12` (or use the system Python 3)
317
+ - **Windows**: Download from [python.org](https://www.python.org/downloads/)
318
+
319
+ ### Build, Test, and Install
320
+
321
+ ```bash
322
+ # Clone the repository
323
+ git clone https://github.com/sudonym1/snail.git
324
+ cd snail
325
+
326
+ make test
327
+ make install
328
+ ```
329
+