snail-lang 0.5.2__cp310-abi3-macosx_11_0_arm64.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.
snail/__init__.py ADDED
@@ -0,0 +1,25 @@
1
+ from ._native import __build_info__, compile, compile_ast, exec, parse
2
+
3
+
4
+ def _resolve_version() -> str:
5
+ try:
6
+ from importlib.metadata import version
7
+
8
+ return version("snail-lang")
9
+ except Exception: # pragma: no cover - during development
10
+ return "0.0.0"
11
+
12
+
13
+ def __getattr__(name: str):
14
+ if name == "__version__":
15
+ value = _resolve_version()
16
+ globals()["__version__"] = value
17
+ return value
18
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
19
+
20
+
21
+ def __dir__() -> list[str]:
22
+ return sorted(list(globals().keys()) + ["__version__"])
23
+
24
+
25
+ __all__ = ["compile", "compile_ast", "exec", "parse", "__version__", "__build_info__"]
snail/_native.abi3.so ADDED
Binary file
snail/cli.py ADDED
@@ -0,0 +1,247 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import sys
5
+
6
+ from . import __build_info__, compile_ast, exec
7
+
8
+ _USAGE = (
9
+ "snail [options] -f <file> [args]...\n"
10
+ " snail [options] <code> [args]..."
11
+ )
12
+ _DESCRIPTION = "Snail programming language interpreter"
13
+
14
+
15
+ def _display_filename(filename: str) -> str:
16
+ if filename.startswith("snail:"):
17
+ return filename
18
+ return f"snail:{filename}"
19
+
20
+
21
+ def _trim_internal_prefix(stack, internal_files: set[str]) -> None:
22
+ if not stack:
23
+ return
24
+ trim_count = 0
25
+ for frame in stack:
26
+ filename = frame.filename
27
+ if filename.startswith("snail:"):
28
+ break
29
+ if filename in internal_files:
30
+ trim_count += 1
31
+ continue
32
+ if os.path.isabs(filename) and os.path.abspath(filename) in internal_files:
33
+ trim_count += 1
34
+ continue
35
+ break
36
+ if 0 < trim_count < len(stack):
37
+ del stack[:trim_count]
38
+
39
+
40
+ def _trim_traceback_exception(tb_exc, internal_files: set[str]) -> None:
41
+ _trim_internal_prefix(tb_exc.stack, internal_files)
42
+ cause = getattr(tb_exc, "__cause__", None)
43
+ if cause is not None:
44
+ _trim_traceback_exception(cause, internal_files)
45
+ context = getattr(tb_exc, "__context__", None)
46
+ if context is not None:
47
+ _trim_traceback_exception(context, internal_files)
48
+ for group_exc in getattr(tb_exc, "exceptions", ()) or ():
49
+ _trim_traceback_exception(group_exc, internal_files)
50
+
51
+
52
+ def _install_trimmed_excepthook() -> None:
53
+ entrypoint = os.path.abspath(sys.argv[0])
54
+ cli_path = os.path.abspath(__file__)
55
+ internal_files = {entrypoint, cli_path}
56
+ original_excepthook = sys.excepthook
57
+
58
+ def _snail_excepthook(
59
+ exc_type: type[BaseException],
60
+ exc: BaseException,
61
+ tb: object,
62
+ ) -> None:
63
+ if exc_type is KeyboardInterrupt:
64
+ return original_excepthook(exc_type, exc, tb)
65
+ import traceback
66
+
67
+ tb_exc = traceback.TracebackException(
68
+ exc_type,
69
+ exc,
70
+ tb,
71
+ capture_locals=False,
72
+ )
73
+ _trim_traceback_exception(tb_exc, internal_files)
74
+ try:
75
+ import _colorize
76
+
77
+ colorize = _colorize.can_colorize(file=sys.stderr)
78
+ except Exception:
79
+ colorize = hasattr(sys.stderr, "isatty") and sys.stderr.isatty()
80
+ for line in tb_exc.format(colorize=colorize):
81
+ sys.stderr.write(line)
82
+
83
+ sys.excepthook = _snail_excepthook
84
+
85
+
86
+ class _Args:
87
+ def __init__(self) -> None:
88
+ self.file: str | None = None
89
+ self.awk = False
90
+ self.no_print = False
91
+ self.no_auto_import = False
92
+ self.debug = False
93
+ self.version = False
94
+ self.help = False
95
+ self.args: list[str] = []
96
+
97
+
98
+ def _print_help(file=sys.stdout) -> None:
99
+ print(f"usage: {_USAGE}", file=file)
100
+ print("", file=file)
101
+ print(_DESCRIPTION, file=file)
102
+ print("", file=file)
103
+ print("options:", file=file)
104
+ print(" -f <file> read Snail source from file", file=file)
105
+ print(" -a, --awk awk mode", file=file)
106
+ print(" -P, --no-print disable auto-print of last expression", file=file)
107
+ print(" -I, --no-auto-import disable auto-imports", file=file)
108
+ print(" --debug parse and compile, then print, do not run", file=file)
109
+ print(" -v, --version show version and exit", file=file)
110
+ print(" -h, --help show this help message and exit", file=file)
111
+
112
+
113
+ def _parse_args(argv: list[str]) -> _Args:
114
+ args = _Args()
115
+ idx = 0
116
+ while idx < len(argv):
117
+ token = argv[idx]
118
+ if token == "--":
119
+ args.args = argv[idx + 1 :]
120
+ return args
121
+ if token == "-" or not token.startswith("-"):
122
+ args.args = argv[idx:]
123
+ return args
124
+ if token in ("-h", "--help"):
125
+ args.help = True
126
+ return args
127
+ if token in ("-v", "--version"):
128
+ args.version = True
129
+ idx += 1
130
+ continue
131
+ if token in ("-a", "--awk"):
132
+ args.awk = True
133
+ idx += 1
134
+ continue
135
+ if token in ("-P", "--no-print"):
136
+ args.no_print = True
137
+ idx += 1
138
+ continue
139
+ if token in ("-I", "--no-auto-import"):
140
+ args.no_auto_import = True
141
+ idx += 1
142
+ continue
143
+ if token == "--debug":
144
+ args.debug = True
145
+ idx += 1
146
+ continue
147
+ if token == "-f":
148
+ if idx + 1 >= len(argv):
149
+ raise ValueError("option -f requires an argument")
150
+ args.file = argv[idx + 1]
151
+ idx += 2
152
+ continue
153
+ raise ValueError(f"unknown option: {token}")
154
+ return args
155
+
156
+
157
+ def _format_version(version: str, build_info: dict[str, object] | None) -> str:
158
+ display_version = version if version.startswith("v") else f"v{version}"
159
+ if not build_info:
160
+ return display_version
161
+ git_rev = build_info.get("git_rev")
162
+ if not git_rev:
163
+ return display_version
164
+
165
+ suffixes: list[str] = []
166
+ if build_info.get("dirty"):
167
+ suffixes.append("!dirty")
168
+ if build_info.get("untagged"):
169
+ suffixes.append("!untagged")
170
+
171
+ if suffixes:
172
+ return f"{display_version} ({git_rev}) {' '.join(suffixes)}"
173
+ return f"{display_version} ({git_rev})"
174
+
175
+
176
+ def _get_version() -> str:
177
+ from . import __version__ as version
178
+
179
+ return version
180
+
181
+
182
+ def main(argv: list[str] | None = None) -> int:
183
+ if argv is None:
184
+ _install_trimmed_excepthook()
185
+ argv = sys.argv[1:]
186
+
187
+ try:
188
+ namespace = _parse_args(argv)
189
+ except ValueError as exc:
190
+ _print_help(file=sys.stderr)
191
+ print(f"error: {exc}", file=sys.stderr)
192
+ return 2
193
+
194
+ if namespace.help:
195
+ _print_help()
196
+ return 0
197
+ if namespace.version:
198
+ print(_format_version(_get_version(), __build_info__))
199
+ return 0
200
+
201
+ mode = "awk" if namespace.awk else "snail"
202
+
203
+ if namespace.file:
204
+ from pathlib import Path
205
+
206
+ path = Path(namespace.file)
207
+ try:
208
+ source = path.read_text()
209
+ except OSError as exc:
210
+ print(f"failed to read {path}: {exc}", file=sys.stderr)
211
+ return 1
212
+ filename = str(path)
213
+ args = [filename, *namespace.args]
214
+ else:
215
+ if not namespace.args:
216
+ print("no input provided", file=sys.stderr)
217
+ return 1
218
+ source = namespace.args[0]
219
+ filename = "<cmd>"
220
+ args = ["--", *namespace.args[1:]]
221
+
222
+ if namespace.debug:
223
+ import ast
224
+ import builtins
225
+
226
+ python_ast = compile_ast(
227
+ source,
228
+ mode=mode,
229
+ auto_print=not namespace.no_print,
230
+ filename=filename,
231
+ )
232
+ builtins.compile(python_ast, _display_filename(filename), "exec")
233
+ print(ast.unparse(python_ast))
234
+ return 0
235
+
236
+ return exec(
237
+ source,
238
+ argv=args,
239
+ mode=mode,
240
+ auto_print=not namespace.no_print,
241
+ auto_import=not namespace.no_auto_import,
242
+ filename=filename,
243
+ )
244
+
245
+
246
+ if __name__ == "__main__":
247
+ raise SystemExit(main())
@@ -0,0 +1,167 @@
1
+ from __future__ import annotations
2
+
3
+ import importlib
4
+
5
+ __all__ = ["install_helpers", "AutoImportDict", "AUTO_IMPORT_NAMES"]
6
+
7
+ # Names that can be auto-imported when first referenced.
8
+ # Maps name -> (module, attribute) where attribute is None for whole-module imports.
9
+ AUTO_IMPORT_NAMES: dict[str, tuple[str, str | None]] = {
10
+ # Whole module imports: import X
11
+ "sys": ("sys", None),
12
+ "os": ("os", None),
13
+ # Attribute imports: from X import Y
14
+ "Path": ("pathlib", "Path"),
15
+ }
16
+
17
+
18
+ class AutoImportDict(dict):
19
+ """A dict subclass that lazily imports allowed names on first access.
20
+
21
+ When a key lookup fails, if the key is in AUTO_IMPORT_NAMES,
22
+ the corresponding module/attribute is imported and stored in the dict.
23
+ Supports both whole-module imports (import sys) and attribute imports
24
+ (from pathlib import Path).
25
+ """
26
+
27
+ def __missing__(self, key: str) -> object:
28
+ if key in AUTO_IMPORT_NAMES:
29
+ module_name, attr_name = AUTO_IMPORT_NAMES[key]
30
+ module = importlib.import_module(module_name)
31
+ value = getattr(module, attr_name) if attr_name else module
32
+ self[key] = value
33
+ return value
34
+ raise KeyError(key)
35
+
36
+
37
+ _compact_try = None
38
+ _regex_search = None
39
+ _regex_compile = None
40
+ _subprocess_capture = None
41
+ _subprocess_status = None
42
+ _jmespath_query = None
43
+ _js = None
44
+
45
+
46
+ def _get_compact_try():
47
+ global _compact_try
48
+ if _compact_try is None:
49
+ from .compact_try import compact_try
50
+
51
+ _compact_try = compact_try
52
+ return _compact_try
53
+
54
+
55
+ def _get_regex_search():
56
+ global _regex_search
57
+ if _regex_search is None:
58
+ from .regex import regex_search
59
+
60
+ _regex_search = regex_search
61
+ return _regex_search
62
+
63
+
64
+ def _get_regex_compile():
65
+ global _regex_compile
66
+ if _regex_compile is None:
67
+ from .regex import regex_compile
68
+
69
+ _regex_compile = regex_compile
70
+ return _regex_compile
71
+
72
+
73
+ def _get_subprocess_capture():
74
+ global _subprocess_capture
75
+ if _subprocess_capture is None:
76
+ from .subprocess import SubprocessCapture
77
+
78
+ _subprocess_capture = SubprocessCapture
79
+ return _subprocess_capture
80
+
81
+
82
+ def _get_subprocess_status():
83
+ global _subprocess_status
84
+ if _subprocess_status is None:
85
+ from .subprocess import SubprocessStatus
86
+
87
+ _subprocess_status = SubprocessStatus
88
+ return _subprocess_status
89
+
90
+
91
+ def _get_jmespath_query():
92
+ global _jmespath_query
93
+ if _jmespath_query is None:
94
+ from .structured_accessor import __snail_jmespath_query
95
+
96
+ _jmespath_query = __snail_jmespath_query
97
+ return _jmespath_query
98
+
99
+
100
+ def _get_js():
101
+ global _js
102
+ if _js is None:
103
+ from .structured_accessor import js
104
+
105
+ _js = js
106
+ return _js
107
+
108
+
109
+ def _lazy_compact_try(expr_fn, fallback_fn=None):
110
+ return _get_compact_try()(expr_fn, fallback_fn)
111
+
112
+
113
+ def _lazy_regex_search(value, pattern):
114
+ return _get_regex_search()(value, pattern)
115
+
116
+
117
+ def _lazy_regex_compile(pattern):
118
+ return _get_regex_compile()(pattern)
119
+
120
+
121
+ def _lazy_subprocess_capture(cmd: str):
122
+ return _get_subprocess_capture()(cmd)
123
+
124
+
125
+ def _lazy_subprocess_status(cmd: str):
126
+ return _get_subprocess_status()(cmd)
127
+
128
+
129
+ def _lazy_jmespath_query(query: str):
130
+ return _get_jmespath_query()(query)
131
+
132
+
133
+ def _lazy_js(input_data=None):
134
+ return _get_js()(input_data)
135
+
136
+
137
+ def __snail_partial(func, /, *args, **kwargs):
138
+ import functools
139
+
140
+ return functools.partial(func, *args, **kwargs)
141
+
142
+
143
+ def __snail_contains__(left, right):
144
+ method = getattr(right, "__snail_contains__", None)
145
+ if method is not None:
146
+ return method(left)
147
+ return left in right
148
+
149
+
150
+ def __snail_contains_not__(left, right):
151
+ method = getattr(right, "__snail_contains__", None)
152
+ if method is not None:
153
+ return not bool(method(left))
154
+ return left not in right
155
+
156
+
157
+ def install_helpers(globals_dict: dict) -> None:
158
+ globals_dict["__snail_compact_try"] = _lazy_compact_try
159
+ globals_dict["__snail_regex_search"] = _lazy_regex_search
160
+ globals_dict["__snail_regex_compile"] = _lazy_regex_compile
161
+ globals_dict["__SnailSubprocessCapture"] = _lazy_subprocess_capture
162
+ globals_dict["__SnailSubprocessStatus"] = _lazy_subprocess_status
163
+ globals_dict["__snail_jmespath_query"] = _lazy_jmespath_query
164
+ globals_dict["__snail_partial"] = __snail_partial
165
+ globals_dict["__snail_contains__"] = __snail_contains__
166
+ globals_dict["__snail_contains_not__"] = __snail_contains_not__
167
+ globals_dict["js"] = _lazy_js
@@ -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/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,259 @@
1
+ Metadata-Version: 2.4
2
+ Name: snail-lang
3
+ Version: 0.5.2
4
+ Requires-Dist: jmespath>=1.0.1
5
+ Requires-Dist: maturin>=1.5 ; extra == 'dev'
6
+ Requires-Dist: pytest ; extra == 'dev'
7
+ Provides-Extra: dev
8
+ License-File: LICENSE
9
+ Summary: Snail programming language interpreter
10
+ Requires-Python: >=3.10
11
+ Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
12
+
13
+ <p align="center">
14
+ <img src="logo.png" alt="Snail logo" width="200">
15
+ </p>
16
+ <p align="center"><em>What do you get when you shove a snake in a shell?</em></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.
21
+
22
+ ## Installing Snail
23
+
24
+ Install [uv](https://docs.astral.sh/uv/getting-started/installation/) and then run:
25
+
26
+ ```bash
27
+ uv tool install -p 3.12 snail-lang
28
+ ```
29
+
30
+ That installs the `snail` CLI for your user; try it with `snail "print('hello')"` once the install completes.
31
+
32
+ ## ✨ What Makes Snail Unique
33
+
34
+ ### Curly Braces, Not Indentation
35
+
36
+ Write Python logic without worrying about whitespace:
37
+
38
+ ```snail
39
+ def process(items) {
40
+ for item in items {
41
+ if item > 0 { print(item) }
42
+ else { continue }
43
+ }
44
+ }
45
+ ```
46
+
47
+ Note, since it is jarring to write python with semicolons everywhere,
48
+ semicolons are optional. You can separate statements with newlines.
49
+
50
+ ### Awk Mode
51
+
52
+ Process files line-by-line with familiar awk semantics:
53
+
54
+ ```snail-awk("5\n4\n3\n2\n1\nbanana\n")
55
+ BEGIN { total = 0 }
56
+ /^[0-9]+/ { total = total + int($1) }
57
+ END { print("Sum:", total); assert total == 15}
58
+ ```
59
+
60
+ Built-in variables: `$0` (line), `$1`, `$2` etc (access fields), `$n` (line number), `$fn` (per-file line number), `$p` (file path), `$m` (last match).
61
+
62
+ ### Compact Error Handling
63
+
64
+ The `?` operator makes error handling terse yet expressive:
65
+
66
+ ```snail
67
+ # Swallow exception, return None
68
+ err = risky()?
69
+
70
+ # Swallow exception, return exception object
71
+ err = risky():$e?
72
+
73
+ # Provide a fallback value (exception available as $e)
74
+ value = js("malformed json"):{"error": "invalid json"}?
75
+ details = fetch_url("foo.com"):"default html"?
76
+ exception_info = fetch_url("example.com"):$e.http_response_code?
77
+
78
+ # Access attributes directly
79
+ name = risky("")?.__class__.__name__
80
+ args = risky("becomes a list"):[1,2,3]?[0]
81
+ ```
82
+
83
+ ### Destructuring + `if let` / `while let`
84
+
85
+ Unpack tuples and lists directly, including Python-style rest bindings:
86
+
87
+ ```snail
88
+ x, *xs = [1, 2, 3]
89
+
90
+ if let [head, *tail] = [1, 2, 3]; head > 0 {
91
+ print(head, tail)
92
+ }
93
+ ```
94
+
95
+ `if let`/`while let` only enter the block when the destructuring succeeds. A guard
96
+ after `;` lets you add a boolean check that runs after the bindings are created.
97
+
98
+ Note that this syntax is more powerful than the walrus operator as that does
99
+ not allow for destructuring.
100
+
101
+
102
+ ### Pipeline Operator
103
+
104
+ The `|` operator enables data pipelining as syntactic sugar for nested
105
+ function calls. `x | y | z` becomes `z(y(x))`. This lets you stay in a
106
+ shell mindset.
107
+
108
+ ```snail
109
+ # Pipe data to subprocess stdin
110
+ result = "hello\nworld" | $(grep hello)
111
+
112
+ # Chain multiple transformations
113
+ output = "foo\nbar" | $(grep foo) | $(wc -l)
114
+
115
+ # Custom pipeline handlers
116
+ class Doubler {
117
+ def __call__(self, x) { return x * 2 }
118
+ }
119
+ doubled = 21 | Doubler() # yields 42
120
+ ```
121
+
122
+ Arbitrary callables make up pipelines, even if they have multiple parameters.
123
+ Snail supports this via placeholders.
124
+ ```snail
125
+ greeting = "World" | greet("Hello ", _) # greet("Hello ", "World")
126
+ excited = "World" | greet(_, "!") # greet("World", "!")
127
+ formal = "World" | greet("Hello ", suffix=_) # greet("Hello ", "World")
128
+ ```
129
+
130
+ When a pipeline targets a call expression, the left-hand value is passed to the
131
+ resulting callable. If the call includes a single `_` placeholder, Snail substitutes
132
+ the piped value at that position (including keyword arguments). Only one
133
+ placeholder is allowed in a piped call. Outside of pipeline calls, `_` remains a
134
+ normal identifier.
135
+
136
+ ### Built-in Subprocess
137
+
138
+ Shell commands are first-class citizens with capturing and non-capturing
139
+ forms.
140
+
141
+ ```snail
142
+ # Capture command output with interpolation
143
+ greeting = $(echo hello {name})
144
+
145
+ # Pipe data through commands
146
+ result = "foo\nbar\nbaz" | $(grep bar) | $(cat -n)
147
+
148
+ # Check command status
149
+ @(make build)? # returns exit code on failure instead of raising
150
+ ```
151
+
152
+
153
+ ### Regex Literals
154
+
155
+ Snail supports first class patterns. Think of them as an infinte set.
156
+
157
+ ```snail
158
+ if bad_email in /^[\w.]+@[\w.]+$/ {
159
+ print("Valid email")
160
+ }
161
+
162
+ # Compiled regex for reuse
163
+ pattern = /\d{3}-\d{4}/
164
+ match = pattern.search(phone)
165
+ match2 = "555-1212" in pattern
166
+ ```
167
+
168
+ Snail regexes don't return a match object, rather they return a tuple
169
+ containing all of the match groups, including group 0. Both `search` and `in`
170
+ return the same tuple (or `()` when there is no match).
171
+
172
+ ### JSON Queries with JMESPath
173
+
174
+ Parse and query JSON data with the `js()` function and structured pipeline accessor:
175
+
176
+ ```snail
177
+ # Parse JSON and query with $[jmespath]
178
+
179
+ # JSON query with JMESPath
180
+ data = js($(curl -s https://api.github.com/repos/sudonym1/snail))
181
+ counts = data | $[stargazers_count]
182
+
183
+ # Inline parsing and querying
184
+ result = js('{{"foo": 12}}') | $[foo]
185
+
186
+ # JSONL parsing returns a list
187
+ names = js('{{"name": "Ada"}}\n{{"name": "Lin"}}') | $[[*].name]
188
+ ```
189
+
190
+ ### Full Python Interoperability
191
+
192
+ Snail compiles to Python AST—import any Python module, use any library, in any
193
+ environment. Assuming that you are using Python 3.10 or later.
194
+
195
+ ## 🚀 Quick Start
196
+
197
+ ```bash
198
+ # One-liner: arithmetic + interpolation
199
+ snail 'name="Snail"; print("{name} says: {6 * 7}")'
200
+
201
+ # JSON query with JMESPath
202
+ snail 'js($(curl -s https://api.github.com/repos/sudonym1/snail)) | $[stargazers_count]'
203
+
204
+ # Compact error handling with fallback
205
+ snail 'result = int("oops"):"bad int {$e}"?; print(result)'
206
+
207
+ # Regex match and capture
208
+ snail 'if let [_, user, domain] = "user@example.com" in /^[\w.]+@([\w.]+)$/ { print(domain) }'
209
+
210
+ # Awk mode: print line numbers for matches
211
+ rg -n "TODO" README.md | snail --awk '/TODO/ { print("{$n}: {$0}") }'
212
+ ```
213
+
214
+ ## 📚 Documentation
215
+
216
+ Documentation is WIP
217
+
218
+ - **[Language Reference](docs/REFERENCE.md)** — Complete syntax and semantics
219
+ - **[examples/all_syntax.snail](examples/all_syntax.snail)** — Every feature in one file
220
+ - **[examples/awk.snail](examples/awk.snail)** — Awk mode examples
221
+
222
+ ## 🔌 Editor Support
223
+
224
+ Vim/Neovim plugin with syntax highlighting, formatting, and run commands:
225
+
226
+ ```vim
227
+ Plug 'sudonym1/snail', { 'rtp': 'extras/vim' }
228
+ ```
229
+
230
+ See [extras/vim/README.md](extras/vim/README.md) for details. Tree-sitter grammar available in `extras/tree-sitter-snail/`.
231
+
232
+ ## 🛠️ Building from Source
233
+
234
+ ### Prerequisites
235
+
236
+ **Python 3.10+** (required at runtime)
237
+
238
+ Snail runs in-process via a Pyo3 extension module, so it uses the active Python environment.
239
+
240
+ Installation per platform:
241
+ - **Ubuntu/Debian**: `sudo apt install python3 python3-dev`
242
+ - **Fedora/RHEL**: `sudo dnf install python3 python3-devel`
243
+ - **macOS**: `brew install python@3.12` (or use the system Python 3)
244
+ - **Windows**: Download from [python.org](https://www.python.org/downloads/)
245
+
246
+ ### Build, Test, and Install
247
+
248
+ ```bash
249
+ # Clone the repository
250
+ git clone https://github.com/sudonym1/snail.git
251
+ cd snail
252
+
253
+ make test
254
+ make install
255
+ ```
256
+
257
+
258
+ **Note on Proptests**: The `snail-proptest` crate contains property-based tests that are skipped by default to keep development iteration fast.
259
+
@@ -0,0 +1,13 @@
1
+ snail/__init__.py,sha256=wuFoVD7mdEm8z-zzbU5bBb2svxKRcuPfWx_hWcdxXFU,695
2
+ snail/_native.abi3.so,sha256=p41cfjm7gqRUT9Polr6xDvrIhu2sjNxD3QUrOK_Y18I,1461008
3
+ snail/cli.py,sha256=aB1PopuCP0ReJ-Odks-MVotf_GOpT-o5--gbmitsFIM,7200
4
+ snail/runtime/__init__.py,sha256=P9bRT8oaXEwJPFbYbpPLSjHzU0eix1G1A808RGdLeo8,4461
5
+ snail/runtime/compact_try.py,sha256=LFT41W1XPNamZfCqG6PpiHz4oKd3s24XvzjYo0G7Iok,380
6
+ snail/runtime/regex.py,sha256=ciy6_HDkOHAJL2lip9T-OMqkzISYc37Jx4lm444RAJg,879
7
+ snail/runtime/structured_accessor.py,sha256=nj5jWZ3K5tEjqxMxgTz5WUNIykLLvFEsmEZkJn7izbQ,2163
8
+ snail/runtime/subprocess.py,sha256=-SrFrj5mXCjfSaqcm_93kG1WxWNc6jPKGpW7RKjpxJ8,1889
9
+ snail_lang-0.5.2.dist-info/METADATA,sha256=zSVaOL9EvqFuUs86suni8G6OuOAW9FdVBsQRl00wDZY,7363
10
+ snail_lang-0.5.2.dist-info/WHEEL,sha256=vZ12AMAE5CVtd8oYbYGrz3omfHuIZCNO_3P50V00s00,104
11
+ snail_lang-0.5.2.dist-info/entry_points.txt,sha256=8u6pjHZtKGdaZQlr6yKbftplObC07caDZ44j5PIPU6M,39
12
+ snail_lang-0.5.2.dist-info/licenses/LICENSE,sha256=erKVJCZPZ4jVaEjq4ZDw2OCXmmby5r1Z6xCpn2Akktc,1062
13
+ snail_lang-0.5.2.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: maturin (1.11.5)
3
+ Root-Is-Purelib: false
4
+ Tag: cp310-abi3-macosx_11_0_arm64
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ snail=snail.cli:main
@@ -0,0 +1,20 @@
1
+ Copyright 2026 Seamus Connor
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the “Software”), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ SOFTWARE.
20
+