snail-lang 0.3.8__tar.gz → 0.4.0__tar.gz
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_lang-0.3.8 → snail_lang-0.4.0}/Cargo.lock +7 -7
- {snail_lang-0.3.8 → snail_lang-0.4.0}/PKG-INFO +97 -202
- snail_lang-0.4.0/README.md +237 -0
- {snail_lang-0.3.8 → snail_lang-0.4.0}/crates/snail-ast/Cargo.toml +1 -1
- {snail_lang-0.3.8 → snail_lang-0.4.0}/crates/snail-core/Cargo.toml +1 -1
- {snail_lang-0.3.8 → snail_lang-0.4.0}/crates/snail-error/Cargo.toml +1 -1
- {snail_lang-0.3.8 → snail_lang-0.4.0}/crates/snail-lower/Cargo.toml +1 -1
- {snail_lang-0.3.8 → snail_lang-0.4.0}/crates/snail-lower/README.md +3 -3
- {snail_lang-0.3.8 → snail_lang-0.4.0}/crates/snail-lower/src/awk.rs +2 -2
- {snail_lang-0.3.8 → snail_lang-0.4.0}/crates/snail-lower/src/constants.rs +0 -4
- {snail_lang-0.3.8 → snail_lang-0.4.0}/crates/snail-parser/Cargo.toml +1 -1
- {snail_lang-0.3.8 → snail_lang-0.4.0}/crates/snail-parser/src/lib.rs +1 -1
- {snail_lang-0.3.8 → snail_lang-0.4.0}/crates/snail-parser/src/snail.pest +1 -1
- {snail_lang-0.3.8 → snail_lang-0.4.0}/crates/snail-parser/tests/errors.rs +2 -2
- {snail_lang-0.3.8 → snail_lang-0.4.0}/crates/snail-python/Cargo.toml +1 -1
- {snail_lang-0.3.8 → snail_lang-0.4.0}/pyproject.toml +4 -1
- {snail_lang-0.3.8 → snail_lang-0.4.0}/python/snail/runtime/structured_accessor.py +1 -1
- snail_lang-0.3.8/README.md +0 -343
- snail_lang-0.3.8/python/snail/vendor/__init__.py +0 -0
- snail_lang-0.3.8/python/snail/vendor/jmespath/LICENSE +0 -21
- snail_lang-0.3.8/python/snail/vendor/jmespath/__init__.py +0 -12
- snail_lang-0.3.8/python/snail/vendor/jmespath/ast.py +0 -90
- snail_lang-0.3.8/python/snail/vendor/jmespath/compat.py +0 -19
- snail_lang-0.3.8/python/snail/vendor/jmespath/exceptions.py +0 -137
- snail_lang-0.3.8/python/snail/vendor/jmespath/functions.py +0 -366
- snail_lang-0.3.8/python/snail/vendor/jmespath/lexer.py +0 -258
- snail_lang-0.3.8/python/snail/vendor/jmespath/parser.py +0 -526
- snail_lang-0.3.8/python/snail/vendor/jmespath/visitor.py +0 -329
- {snail_lang-0.3.8 → snail_lang-0.4.0}/Cargo.toml +0 -0
- {snail_lang-0.3.8 → snail_lang-0.4.0}/LICENSE +0 -0
- {snail_lang-0.3.8 → snail_lang-0.4.0}/crates/snail-ast/README.md +0 -0
- {snail_lang-0.3.8 → snail_lang-0.4.0}/crates/snail-ast/src/ast.rs +0 -0
- {snail_lang-0.3.8 → snail_lang-0.4.0}/crates/snail-ast/src/awk.rs +0 -0
- {snail_lang-0.3.8 → snail_lang-0.4.0}/crates/snail-ast/src/lib.rs +0 -0
- {snail_lang-0.3.8 → snail_lang-0.4.0}/crates/snail-core/README.md +0 -0
- {snail_lang-0.3.8 → snail_lang-0.4.0}/crates/snail-core/src/lib.rs +0 -0
- {snail_lang-0.3.8 → snail_lang-0.4.0}/crates/snail-error/README.md +0 -0
- {snail_lang-0.3.8 → snail_lang-0.4.0}/crates/snail-error/src/lib.rs +0 -0
- {snail_lang-0.3.8 → snail_lang-0.4.0}/crates/snail-lower/src/expr.rs +0 -0
- {snail_lang-0.3.8 → snail_lang-0.4.0}/crates/snail-lower/src/helpers.rs +0 -0
- {snail_lang-0.3.8 → snail_lang-0.4.0}/crates/snail-lower/src/lib.rs +0 -0
- {snail_lang-0.3.8 → snail_lang-0.4.0}/crates/snail-lower/src/operators.rs +0 -0
- {snail_lang-0.3.8 → snail_lang-0.4.0}/crates/snail-lower/src/program.rs +0 -0
- {snail_lang-0.3.8 → snail_lang-0.4.0}/crates/snail-lower/src/py_ast.rs +0 -0
- {snail_lang-0.3.8 → snail_lang-0.4.0}/crates/snail-lower/src/stmt.rs +0 -0
- {snail_lang-0.3.8 → snail_lang-0.4.0}/crates/snail-parser/README.md +0 -0
- {snail_lang-0.3.8 → snail_lang-0.4.0}/crates/snail-parser/src/awk.rs +0 -0
- {snail_lang-0.3.8 → snail_lang-0.4.0}/crates/snail-parser/src/expr.rs +0 -0
- {snail_lang-0.3.8 → snail_lang-0.4.0}/crates/snail-parser/src/literal.rs +0 -0
- {snail_lang-0.3.8 → snail_lang-0.4.0}/crates/snail-parser/src/stmt.rs +0 -0
- {snail_lang-0.3.8 → snail_lang-0.4.0}/crates/snail-parser/src/string.rs +0 -0
- {snail_lang-0.3.8 → snail_lang-0.4.0}/crates/snail-parser/src/util.rs +0 -0
- {snail_lang-0.3.8 → snail_lang-0.4.0}/crates/snail-parser/tests/common.rs +0 -0
- {snail_lang-0.3.8 → snail_lang-0.4.0}/crates/snail-parser/tests/parser.rs +0 -0
- {snail_lang-0.3.8 → snail_lang-0.4.0}/crates/snail-parser/tests/statements.rs +0 -0
- {snail_lang-0.3.8 → snail_lang-0.4.0}/crates/snail-parser/tests/syntax_expressions.rs +0 -0
- {snail_lang-0.3.8 → snail_lang-0.4.0}/crates/snail-parser/tests/syntax_strings.rs +0 -0
- {snail_lang-0.3.8 → snail_lang-0.4.0}/crates/snail-python/src/lib.rs +0 -0
- {snail_lang-0.3.8 → snail_lang-0.4.0}/python/snail/__init__.py +0 -0
- {snail_lang-0.3.8 → snail_lang-0.4.0}/python/snail/cli.py +0 -0
- {snail_lang-0.3.8 → snail_lang-0.4.0}/python/snail/runtime/__init__.py +0 -0
- {snail_lang-0.3.8 → snail_lang-0.4.0}/python/snail/runtime/compact_try.py +0 -0
- {snail_lang-0.3.8 → snail_lang-0.4.0}/python/snail/runtime/regex.py +0 -0
- {snail_lang-0.3.8 → snail_lang-0.4.0}/python/snail/runtime/subprocess.py +0 -0
|
@@ -485,11 +485,11 @@ checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
|
|
|
485
485
|
|
|
486
486
|
[[package]]
|
|
487
487
|
name = "snail-ast"
|
|
488
|
-
version = "0.
|
|
488
|
+
version = "0.4.0"
|
|
489
489
|
|
|
490
490
|
[[package]]
|
|
491
491
|
name = "snail-core"
|
|
492
|
-
version = "0.
|
|
492
|
+
version = "0.4.0"
|
|
493
493
|
dependencies = [
|
|
494
494
|
"pyo3",
|
|
495
495
|
"snail-ast",
|
|
@@ -500,14 +500,14 @@ dependencies = [
|
|
|
500
500
|
|
|
501
501
|
[[package]]
|
|
502
502
|
name = "snail-error"
|
|
503
|
-
version = "0.
|
|
503
|
+
version = "0.4.0"
|
|
504
504
|
dependencies = [
|
|
505
505
|
"snail-ast",
|
|
506
506
|
]
|
|
507
507
|
|
|
508
508
|
[[package]]
|
|
509
509
|
name = "snail-lower"
|
|
510
|
-
version = "0.
|
|
510
|
+
version = "0.4.0"
|
|
511
511
|
dependencies = [
|
|
512
512
|
"pyo3",
|
|
513
513
|
"snail-ast",
|
|
@@ -516,7 +516,7 @@ dependencies = [
|
|
|
516
516
|
|
|
517
517
|
[[package]]
|
|
518
518
|
name = "snail-parser"
|
|
519
|
-
version = "0.
|
|
519
|
+
version = "0.4.0"
|
|
520
520
|
dependencies = [
|
|
521
521
|
"pest",
|
|
522
522
|
"pest_derive",
|
|
@@ -526,7 +526,7 @@ dependencies = [
|
|
|
526
526
|
|
|
527
527
|
[[package]]
|
|
528
528
|
name = "snail-proptest"
|
|
529
|
-
version = "0.
|
|
529
|
+
version = "0.4.0"
|
|
530
530
|
dependencies = [
|
|
531
531
|
"proptest",
|
|
532
532
|
"pyo3",
|
|
@@ -540,7 +540,7 @@ dependencies = [
|
|
|
540
540
|
|
|
541
541
|
[[package]]
|
|
542
542
|
name = "snail-python"
|
|
543
|
-
version = "0.
|
|
543
|
+
version = "0.4.0"
|
|
544
544
|
dependencies = [
|
|
545
545
|
"pyo3",
|
|
546
546
|
"snail-core",
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: snail-lang
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Requires-Dist: jmespath>=1.0.1
|
|
4
5
|
Requires-Dist: maturin>=1.5 ; extra == 'dev'
|
|
5
6
|
Requires-Dist: pytest ; extra == 'dev'
|
|
6
7
|
Provides-Extra: dev
|
|
@@ -12,23 +13,27 @@ Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
|
|
|
12
13
|
<p align="center">
|
|
13
14
|
<img src="logo.png" alt="Snail logo" width="200">
|
|
14
15
|
</p>
|
|
16
|
+
<p align="center"><em>What do you get when you shove a snake in a shell?</em></p>
|
|
15
17
|
|
|
16
18
|
<h1 align="center">Snail</h1>
|
|
17
|
-
<p align="center"><em>What do you get when you shove a snake in a shell?</em></p>
|
|
18
19
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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:
|
|
22
25
|
|
|
23
|
-
|
|
26
|
+
```bash
|
|
27
|
+
uv tool install -p 3.12 snail-lang
|
|
28
|
+
```
|
|
24
29
|
|
|
25
|
-
|
|
30
|
+
That installs the `snail` CLI for your user; try it with `snail "print('hello')"` once the install completes.
|
|
26
31
|
|
|
27
32
|
## ✨ What Makes Snail Unique
|
|
28
33
|
|
|
29
34
|
### Curly Braces, Not Indentation
|
|
30
35
|
|
|
31
|
-
Write Python logic without worrying about
|
|
36
|
+
Write Python logic without worrying about whitespace:
|
|
32
37
|
|
|
33
38
|
```snail
|
|
34
39
|
def process(items) {
|
|
@@ -39,69 +44,48 @@ def process(items) {
|
|
|
39
44
|
}
|
|
40
45
|
```
|
|
41
46
|
|
|
42
|
-
|
|
47
|
+
Note, since it is jarring to write python with semicolons everywhere,
|
|
48
|
+
semicolons are optional. You can separate statements with newlines.
|
|
43
49
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
```snail
|
|
47
|
-
# Capture command output with interpolation
|
|
48
|
-
name = "world"
|
|
49
|
-
greeting = $(echo hello {name})
|
|
50
|
+
### Awk Mode
|
|
50
51
|
|
|
51
|
-
|
|
52
|
-
result = "foo\nbar\nbaz" | $(grep bar) | $(cat -n)
|
|
52
|
+
Process files line-by-line with familiar awk semantics:
|
|
53
53
|
|
|
54
|
-
|
|
55
|
-
|
|
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}
|
|
56
58
|
```
|
|
57
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
|
+
|
|
58
63
|
### Compact Error Handling
|
|
59
64
|
|
|
60
65
|
The `?` operator makes error handling terse yet expressive:
|
|
61
66
|
|
|
62
67
|
```snail
|
|
63
|
-
# Swallow exception,
|
|
64
|
-
err =
|
|
68
|
+
# Swallow exception, return None
|
|
69
|
+
err = risky()?
|
|
70
|
+
|
|
71
|
+
# Swallow exception, return exception object
|
|
72
|
+
err = risky():$e?
|
|
65
73
|
|
|
66
74
|
# Provide a fallback value (exception available as $e)
|
|
67
|
-
value = js(
|
|
68
|
-
details = fetch_url(
|
|
75
|
+
value = js("malformed json"):{}?
|
|
76
|
+
details = fetch_url("foo.com"):"default html"?
|
|
77
|
+
exception_info = fetch_url("example.com"):$e.http_response_code?
|
|
69
78
|
|
|
70
79
|
# Access attributes directly
|
|
71
|
-
name = risky()?.__class__.__name__
|
|
72
|
-
args = risky()
|
|
80
|
+
name = risky("")?.__class__.__name__
|
|
81
|
+
args = risky("becomes a list"):[1,2,3]?[0]
|
|
73
82
|
```
|
|
74
83
|
|
|
75
|
-
### Regex Literals
|
|
76
|
-
|
|
77
|
-
Pattern matching without `import re`:
|
|
78
|
-
|
|
79
|
-
```snail
|
|
80
|
-
if email in /^[\w.]+@[\w.]+$/ {
|
|
81
|
-
print("Valid email")
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
# Compiled regex for reuse
|
|
85
|
-
pattern = /\d{3}-\d{4}/
|
|
86
|
-
match = pattern.search(phone)
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
### Awk Mode
|
|
90
|
-
|
|
91
|
-
Process files line-by-line with familiar awk semantics:
|
|
92
|
-
|
|
93
|
-
```snail
|
|
94
|
-
#!/usr/bin/env -S snail --awk -f
|
|
95
|
-
BEGIN { total = 0 }
|
|
96
|
-
/^[0-9]+/ { total = total + int($f[0]) }
|
|
97
|
-
END { print("Sum:", total) }
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
Built-in variables: `$l` (line), `$f` (fields), `$n` (line number), `$fn` (per-file line number), `$p` (file path), `$m` (last match).
|
|
101
|
-
|
|
102
84
|
### Pipeline Operator
|
|
103
85
|
|
|
104
|
-
The `|` operator enables data pipelining
|
|
86
|
+
The `|` operator enables data pipelining as syntactic sugar for nested
|
|
87
|
+
function calls. `x | y | z` becomes `z(y(x))`. This lets you stay in a
|
|
88
|
+
shell mindset.
|
|
105
89
|
|
|
106
90
|
```snail
|
|
107
91
|
# Pipe data to subprocess stdin
|
|
@@ -115,8 +99,11 @@ class Doubler {
|
|
|
115
99
|
def __call__(self, x) { return x * 2 }
|
|
116
100
|
}
|
|
117
101
|
doubled = 21 | Doubler() # yields 42
|
|
102
|
+
```
|
|
118
103
|
|
|
119
|
-
|
|
104
|
+
Arbitrary callables make up pipelines, even if they have multiple parameters.
|
|
105
|
+
Snail supports this via placeholders.
|
|
106
|
+
```snail
|
|
120
107
|
greeting = "World" | greet("Hello ", _) # greet("Hello ", "World")
|
|
121
108
|
excited = "World" | greet(_, "!") # greet("World", "!")
|
|
122
109
|
formal = "World" | greet("Hello ", suffix=_) # greet("Hello ", "World")
|
|
@@ -128,99 +115,83 @@ the piped value at that position (including keyword arguments). Only one
|
|
|
128
115
|
placeholder is allowed in a piped call. Outside of pipeline calls, `_` remains a
|
|
129
116
|
normal identifier.
|
|
130
117
|
|
|
118
|
+
### Built-in Subprocess
|
|
119
|
+
|
|
120
|
+
Shell commands are first-class citizens with capturing and non-capturing
|
|
121
|
+
forms.
|
|
122
|
+
|
|
123
|
+
```snail
|
|
124
|
+
# Capture command output with interpolation
|
|
125
|
+
greeting = $(echo hello {name})
|
|
126
|
+
|
|
127
|
+
# Pipe data through commands
|
|
128
|
+
result = "foo\nbar\nbaz" | $(grep bar) | $(cat -n)
|
|
129
|
+
|
|
130
|
+
# Check command status
|
|
131
|
+
@(make build)? # returns exit code on failure instead of raising
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
### Regex Literals
|
|
136
|
+
|
|
137
|
+
Snail supports first class patterns. Think of them as an infinte set.
|
|
138
|
+
|
|
139
|
+
```snail
|
|
140
|
+
if bad_email in /^[\w.]+@[\w.]+$/ {
|
|
141
|
+
print("Valid email")
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
# Compiled regex for reuse
|
|
145
|
+
pattern = /\d{3}-\d{4}/
|
|
146
|
+
match = pattern.search(phone)
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
NOTE: this feature is WIP.
|
|
150
|
+
|
|
131
151
|
### JSON Queries with JMESPath
|
|
132
152
|
|
|
133
153
|
Parse and query JSON data with the `js()` function and structured pipeline accessor:
|
|
134
154
|
|
|
135
155
|
```snail
|
|
136
156
|
# Parse JSON and query with $[jmespath]
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
157
|
+
|
|
158
|
+
# JSON query with JMESPath
|
|
159
|
+
data = js($(curl -s https://api.github.com/repos/sudonym1/snail))
|
|
160
|
+
counts = data | $[stargazers_count]
|
|
140
161
|
|
|
141
162
|
# Inline parsing and querying
|
|
142
|
-
result = js('{"foo": 12}') | $[foo]
|
|
163
|
+
result = js('{{"foo": 12}}') | $[foo]
|
|
143
164
|
|
|
144
165
|
# JSONL parsing returns a list
|
|
145
|
-
names = js('{"name": "Ada"}\n{"name": "Lin"}') | $[[*].name]
|
|
166
|
+
names = js('{{"name": "Ada"}}\n{{"name": "Lin"}}') | $[[*].name]
|
|
146
167
|
```
|
|
147
168
|
|
|
148
169
|
### Full Python Interoperability
|
|
149
170
|
|
|
150
|
-
Snail compiles to Python AST—import any Python module, use any library
|
|
151
|
-
|
|
152
|
-
```snail
|
|
153
|
-
import pandas as pd
|
|
154
|
-
from pathlib import Path
|
|
155
|
-
|
|
156
|
-
df = pd.read_csv(Path("data.csv"))
|
|
157
|
-
filtered = df[df["value"] > 100]
|
|
158
|
-
```
|
|
171
|
+
Snail compiles to Python AST—import any Python module, use any library, in any
|
|
172
|
+
environment. Assuming that you are using Python 3.10 or later.
|
|
159
173
|
|
|
160
174
|
## 🚀 Quick Start
|
|
161
175
|
|
|
162
176
|
```bash
|
|
163
|
-
#
|
|
164
|
-
|
|
177
|
+
# One-liner: arithmetic + interpolation
|
|
178
|
+
snail 'name="Snail"; print("{name} says: {6 * 7}")'
|
|
179
|
+
|
|
180
|
+
# JSON query with JMESPath
|
|
181
|
+
snail 'js($(curl -s https://api.github.com/repos/sudonym1/snail)) | $[stargazers_count]'
|
|
165
182
|
|
|
166
|
-
#
|
|
167
|
-
snail "print('
|
|
183
|
+
# Compact error handling with fallback
|
|
184
|
+
snail 'result = int("oops"):"bad int {$e}"?; print(result)'
|
|
168
185
|
|
|
169
|
-
#
|
|
170
|
-
snail
|
|
186
|
+
# Regex match and capture
|
|
187
|
+
snail 'm = "user@example.com" in /^[\\w.]+@([\\w.]+)$/; if m { print(m[1]) }'
|
|
171
188
|
|
|
172
|
-
# Awk mode for
|
|
173
|
-
|
|
189
|
+
# Awk mode: print line numbers for matches
|
|
190
|
+
rg -n "TODO" README.md | snail --awk '/TODO/ { print("{$n}: {$0}") }'
|
|
174
191
|
```
|
|
175
192
|
|
|
176
193
|
## 🏗️ Architecture
|
|
177
194
|
|
|
178
|
-
Snail compiles to Python through a multi-stage pipeline:
|
|
179
|
-
|
|
180
|
-
```mermaid
|
|
181
|
-
flowchart TB
|
|
182
|
-
subgraph Input
|
|
183
|
-
A[Snail Source Code]
|
|
184
|
-
end
|
|
185
|
-
|
|
186
|
-
subgraph Parsing["Parsing (Pest PEG Parser)"]
|
|
187
|
-
B1[crates/snail-parser/src/snail.pest<br/>Grammar Definition]
|
|
188
|
-
B2[crates/snail-parser/<br/>Parser Implementation]
|
|
189
|
-
end
|
|
190
|
-
|
|
191
|
-
subgraph AST["Abstract Syntax Tree"]
|
|
192
|
-
C1[crates/snail-ast/src/ast.rs<br/>Program AST]
|
|
193
|
-
C2[crates/snail-ast/src/awk.rs<br/>AwkProgram AST]
|
|
194
|
-
end
|
|
195
|
-
|
|
196
|
-
subgraph Lowering["Lowering"]
|
|
197
|
-
D1[crates/snail-lower/<br/>AST → Python AST Transform]
|
|
198
|
-
D2[python/snail/runtime/<br/>Runtime Helpers]
|
|
199
|
-
end
|
|
200
|
-
|
|
201
|
-
subgraph Execution
|
|
202
|
-
E1[python/snail/cli.py<br/>CLI Interface]
|
|
203
|
-
E2[pyo3 extension<br/>in-process exec]
|
|
204
|
-
end
|
|
205
|
-
|
|
206
|
-
A -->|Regular Mode| B1
|
|
207
|
-
A -->|Awk Mode| B1
|
|
208
|
-
B1 --> B2
|
|
209
|
-
B2 -->|Regular| C1
|
|
210
|
-
B2 -->|Awk| C2
|
|
211
|
-
C1 --> D1
|
|
212
|
-
C2 --> D1
|
|
213
|
-
D1 --> D2
|
|
214
|
-
D1 --> E1
|
|
215
|
-
D2 --> E1
|
|
216
|
-
E1 --> E2
|
|
217
|
-
E2 --> F[Python Execution]
|
|
218
|
-
|
|
219
|
-
style A fill:#e1f5ff
|
|
220
|
-
style F fill:#e1ffe1
|
|
221
|
-
style D2 fill:#fff4e1
|
|
222
|
-
```
|
|
223
|
-
|
|
224
195
|
**Key Components:**
|
|
225
196
|
|
|
226
197
|
- **Parser**: Uses [Pest](https://pest.rs/) parser generator with PEG grammar defined in `src/snail.pest`
|
|
@@ -263,93 +234,17 @@ Installation per platform:
|
|
|
263
234
|
- **macOS**: `brew install python@3.12` (or use the system Python 3)
|
|
264
235
|
- **Windows**: Download from [python.org](https://www.python.org/downloads/)
|
|
265
236
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
**Rust toolchain** (cargo and rustc)
|
|
269
|
-
|
|
270
|
-
Install Rust using [rustup](https://rustup.rs):
|
|
271
|
-
|
|
272
|
-
```bash
|
|
273
|
-
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
|
274
|
-
```
|
|
275
|
-
|
|
276
|
-
This installs `cargo` (Rust's package manager) and `rustc` (the Rust compiler). After installation, restart your shell or run:
|
|
277
|
-
|
|
278
|
-
```bash
|
|
279
|
-
source $HOME/.cargo/env
|
|
280
|
-
```
|
|
281
|
-
|
|
282
|
-
Verify installation:
|
|
283
|
-
|
|
284
|
-
```bash
|
|
285
|
-
cargo --version # Should show cargo 1.70+
|
|
286
|
-
rustc --version # Should show rustc 1.70+
|
|
287
|
-
python3 --version # Should show Python 3.10+
|
|
288
|
-
```
|
|
289
|
-
|
|
290
|
-
**maturin** (build tool)
|
|
291
|
-
|
|
292
|
-
```bash
|
|
293
|
-
pip install maturin
|
|
294
|
-
```
|
|
295
|
-
|
|
296
|
-
### Build and Install
|
|
237
|
+
### Build, Test, and Install
|
|
297
238
|
|
|
298
239
|
```bash
|
|
299
240
|
# Clone the repository
|
|
300
241
|
git clone https://github.com/sudonym1/snail.git
|
|
301
242
|
cd snail
|
|
302
243
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
source .venv/bin/activate # On Windows: .venv\Scripts\activate
|
|
306
|
-
|
|
307
|
-
# Build and install into the venv
|
|
308
|
-
maturin develop
|
|
309
|
-
|
|
310
|
-
# Or build wheels for distribution
|
|
311
|
-
maturin build --release
|
|
312
|
-
```
|
|
313
|
-
|
|
314
|
-
### Running Tests
|
|
315
|
-
|
|
316
|
-
```bash
|
|
317
|
-
# Run all Rust tests (parser, lowering, awk mode; excludes proptests by default)
|
|
318
|
-
cargo test
|
|
319
|
-
|
|
320
|
-
# Run tests including property-based tests (proptests)
|
|
321
|
-
cargo test --features run-proptests
|
|
322
|
-
|
|
323
|
-
# Check code formatting and linting
|
|
324
|
-
cargo fmt --check
|
|
325
|
-
cargo clippy -- -D warnings
|
|
326
|
-
|
|
327
|
-
# Build with all features enabled (required before committing)
|
|
328
|
-
cargo build --features run-proptests
|
|
329
|
-
|
|
330
|
-
# Run Python CLI tests
|
|
331
|
-
python -m pytest python/tests
|
|
332
|
-
```
|
|
333
|
-
|
|
334
|
-
**Note on Proptests**: The `snail-proptest` crate contains property-based tests that are skipped by default to keep development iteration fast. Use `--features run-proptests` to run them. Before committing, verify that `cargo build --features run-proptests` compiles successfully.
|
|
335
|
-
|
|
336
|
-
### Troubleshooting
|
|
337
|
-
|
|
338
|
-
**Using with virtual environments:**
|
|
339
|
-
|
|
340
|
-
Activate the environment before running snail so it uses the same interpreter:
|
|
341
|
-
|
|
342
|
-
```bash
|
|
343
|
-
# Create and activate a venv
|
|
344
|
-
python3 -m venv myenv
|
|
345
|
-
source myenv/bin/activate # On Windows: myenv\Scripts\activate
|
|
346
|
-
|
|
347
|
-
# Install and run
|
|
348
|
-
pip install snail-lang
|
|
349
|
-
snail "import sys; print(sys.prefix)"
|
|
244
|
+
make test
|
|
245
|
+
make install
|
|
350
246
|
```
|
|
351
247
|
|
|
352
|
-
## 📋 Project Status
|
|
353
248
|
|
|
354
|
-
|
|
249
|
+
**Note on Proptests**: The `snail-proptest` crate contains property-based tests that are skipped by default to keep development iteration fast.
|
|
355
250
|
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="logo.png" alt="Snail logo" width="200">
|
|
3
|
+
</p>
|
|
4
|
+
<p align="center"><em>What do you get when you shove a snake in a shell?</em></p>
|
|
5
|
+
|
|
6
|
+
<h1 align="center">Snail</h1>
|
|
7
|
+
|
|
8
|
+
**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.
|
|
9
|
+
|
|
10
|
+
## Installing Snail
|
|
11
|
+
|
|
12
|
+
Install [uv](https://docs.astral.sh/uv/getting-started/installation/) and then run:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
uv tool install -p 3.12 snail-lang
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
That installs the `snail` CLI for your user; try it with `snail "print('hello')"` once the install completes.
|
|
19
|
+
|
|
20
|
+
## ✨ What Makes Snail Unique
|
|
21
|
+
|
|
22
|
+
### Curly Braces, Not Indentation
|
|
23
|
+
|
|
24
|
+
Write Python logic without worrying about whitespace:
|
|
25
|
+
|
|
26
|
+
```snail
|
|
27
|
+
def process(items) {
|
|
28
|
+
for item in items {
|
|
29
|
+
if item > 0 { print(item) }
|
|
30
|
+
else { continue }
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Note, since it is jarring to write python with semicolons everywhere,
|
|
36
|
+
semicolons are optional. You can separate statements with newlines.
|
|
37
|
+
|
|
38
|
+
### Awk Mode
|
|
39
|
+
|
|
40
|
+
Process files line-by-line with familiar awk semantics:
|
|
41
|
+
|
|
42
|
+
```snail-awk("5\n4\n3\n2\n1\nbanana\n")
|
|
43
|
+
BEGIN { total = 0 }
|
|
44
|
+
/^[0-9]+/ { total = total + int($1) }
|
|
45
|
+
END { print("Sum:", total); assert total == 15}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Built-in variables: `$0` (line), `$1`, `$2` etc (access fields), `$n` (line number), `$fn` (per-file line number), `$p` (file path), `$m` (last match).
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
### Compact Error Handling
|
|
52
|
+
|
|
53
|
+
The `?` operator makes error handling terse yet expressive:
|
|
54
|
+
|
|
55
|
+
```snail
|
|
56
|
+
# Swallow exception, return None
|
|
57
|
+
err = risky()?
|
|
58
|
+
|
|
59
|
+
# Swallow exception, return exception object
|
|
60
|
+
err = risky():$e?
|
|
61
|
+
|
|
62
|
+
# Provide a fallback value (exception available as $e)
|
|
63
|
+
value = js("malformed json"):{}?
|
|
64
|
+
details = fetch_url("foo.com"):"default html"?
|
|
65
|
+
exception_info = fetch_url("example.com"):$e.http_response_code?
|
|
66
|
+
|
|
67
|
+
# Access attributes directly
|
|
68
|
+
name = risky("")?.__class__.__name__
|
|
69
|
+
args = risky("becomes a list"):[1,2,3]?[0]
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Pipeline Operator
|
|
73
|
+
|
|
74
|
+
The `|` operator enables data pipelining as syntactic sugar for nested
|
|
75
|
+
function calls. `x | y | z` becomes `z(y(x))`. This lets you stay in a
|
|
76
|
+
shell mindset.
|
|
77
|
+
|
|
78
|
+
```snail
|
|
79
|
+
# Pipe data to subprocess stdin
|
|
80
|
+
result = "hello\nworld" | $(grep hello)
|
|
81
|
+
|
|
82
|
+
# Chain multiple transformations
|
|
83
|
+
output = "foo\nbar" | $(grep foo) | $(wc -l)
|
|
84
|
+
|
|
85
|
+
# Custom pipeline handlers
|
|
86
|
+
class Doubler {
|
|
87
|
+
def __call__(self, x) { return x * 2 }
|
|
88
|
+
}
|
|
89
|
+
doubled = 21 | Doubler() # yields 42
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Arbitrary callables make up pipelines, even if they have multiple parameters.
|
|
93
|
+
Snail supports this via placeholders.
|
|
94
|
+
```snail
|
|
95
|
+
greeting = "World" | greet("Hello ", _) # greet("Hello ", "World")
|
|
96
|
+
excited = "World" | greet(_, "!") # greet("World", "!")
|
|
97
|
+
formal = "World" | greet("Hello ", suffix=_) # greet("Hello ", "World")
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
When a pipeline targets a call expression, the left-hand value is passed to the
|
|
101
|
+
resulting callable. If the call includes a single `_` placeholder, Snail substitutes
|
|
102
|
+
the piped value at that position (including keyword arguments). Only one
|
|
103
|
+
placeholder is allowed in a piped call. Outside of pipeline calls, `_` remains a
|
|
104
|
+
normal identifier.
|
|
105
|
+
|
|
106
|
+
### Built-in Subprocess
|
|
107
|
+
|
|
108
|
+
Shell commands are first-class citizens with capturing and non-capturing
|
|
109
|
+
forms.
|
|
110
|
+
|
|
111
|
+
```snail
|
|
112
|
+
# Capture command output with interpolation
|
|
113
|
+
greeting = $(echo hello {name})
|
|
114
|
+
|
|
115
|
+
# Pipe data through commands
|
|
116
|
+
result = "foo\nbar\nbaz" | $(grep bar) | $(cat -n)
|
|
117
|
+
|
|
118
|
+
# Check command status
|
|
119
|
+
@(make build)? # returns exit code on failure instead of raising
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
### Regex Literals
|
|
124
|
+
|
|
125
|
+
Snail supports first class patterns. Think of them as an infinte set.
|
|
126
|
+
|
|
127
|
+
```snail
|
|
128
|
+
if bad_email in /^[\w.]+@[\w.]+$/ {
|
|
129
|
+
print("Valid email")
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
# Compiled regex for reuse
|
|
133
|
+
pattern = /\d{3}-\d{4}/
|
|
134
|
+
match = pattern.search(phone)
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
NOTE: this feature is WIP.
|
|
138
|
+
|
|
139
|
+
### JSON Queries with JMESPath
|
|
140
|
+
|
|
141
|
+
Parse and query JSON data with the `js()` function and structured pipeline accessor:
|
|
142
|
+
|
|
143
|
+
```snail
|
|
144
|
+
# Parse JSON and query with $[jmespath]
|
|
145
|
+
|
|
146
|
+
# JSON query with JMESPath
|
|
147
|
+
data = js($(curl -s https://api.github.com/repos/sudonym1/snail))
|
|
148
|
+
counts = data | $[stargazers_count]
|
|
149
|
+
|
|
150
|
+
# Inline parsing and querying
|
|
151
|
+
result = js('{{"foo": 12}}') | $[foo]
|
|
152
|
+
|
|
153
|
+
# JSONL parsing returns a list
|
|
154
|
+
names = js('{{"name": "Ada"}}\n{{"name": "Lin"}}') | $[[*].name]
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Full Python Interoperability
|
|
158
|
+
|
|
159
|
+
Snail compiles to Python AST—import any Python module, use any library, in any
|
|
160
|
+
environment. Assuming that you are using Python 3.10 or later.
|
|
161
|
+
|
|
162
|
+
## 🚀 Quick Start
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
# One-liner: arithmetic + interpolation
|
|
166
|
+
snail 'name="Snail"; print("{name} says: {6 * 7}")'
|
|
167
|
+
|
|
168
|
+
# JSON query with JMESPath
|
|
169
|
+
snail 'js($(curl -s https://api.github.com/repos/sudonym1/snail)) | $[stargazers_count]'
|
|
170
|
+
|
|
171
|
+
# Compact error handling with fallback
|
|
172
|
+
snail 'result = int("oops"):"bad int {$e}"?; print(result)'
|
|
173
|
+
|
|
174
|
+
# Regex match and capture
|
|
175
|
+
snail 'm = "user@example.com" in /^[\\w.]+@([\\w.]+)$/; if m { print(m[1]) }'
|
|
176
|
+
|
|
177
|
+
# Awk mode: print line numbers for matches
|
|
178
|
+
rg -n "TODO" README.md | snail --awk '/TODO/ { print("{$n}: {$0}") }'
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## 🏗️ Architecture
|
|
182
|
+
|
|
183
|
+
**Key Components:**
|
|
184
|
+
|
|
185
|
+
- **Parser**: Uses [Pest](https://pest.rs/) parser generator with PEG grammar defined in `src/snail.pest`
|
|
186
|
+
- **AST**: Separate representations for regular Snail (`Program`) and awk mode (`AwkProgram`) with source spans for error reporting
|
|
187
|
+
- **Lowering**: Transforms Snail AST into Python AST, emitting helper calls backed by `snail.runtime`
|
|
188
|
+
- `?` operator → `__snail_compact_try`
|
|
189
|
+
- `$(cmd)` subprocess capture → `__SnailSubprocessCapture`
|
|
190
|
+
- `@(cmd)` subprocess status → `__SnailSubprocessStatus`
|
|
191
|
+
- Regex literals → `__snail_regex_search` and `__snail_regex_compile`
|
|
192
|
+
- **Execution**: Compiles Python AST directly for in-process execution
|
|
193
|
+
- **CLI**: Python wrapper (`python/snail/cli.py`) that executes via the extension module
|
|
194
|
+
|
|
195
|
+
## 📚 Documentation
|
|
196
|
+
|
|
197
|
+
- **[Language Reference](docs/REFERENCE.md)** — Complete syntax and semantics
|
|
198
|
+
- **[examples/all_syntax.snail](examples/all_syntax.snail)** — Every feature in one file
|
|
199
|
+
- **[examples/awk.snail](examples/awk.snail)** — Awk mode examples
|
|
200
|
+
|
|
201
|
+
## 🔌 Editor Support
|
|
202
|
+
|
|
203
|
+
Vim/Neovim plugin with syntax highlighting, formatting, and run commands:
|
|
204
|
+
|
|
205
|
+
```vim
|
|
206
|
+
Plug 'sudonym1/snail', { 'rtp': 'extras/vim' }
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
See [extras/vim/README.md](extras/vim/README.md) for details. Tree-sitter grammar available in `extras/tree-sitter-snail/`.
|
|
210
|
+
|
|
211
|
+
## 🛠️ Building from Source
|
|
212
|
+
|
|
213
|
+
### Prerequisites
|
|
214
|
+
|
|
215
|
+
**Python 3.10+** (required at runtime)
|
|
216
|
+
|
|
217
|
+
Snail runs in-process via a Pyo3 extension module, so it uses the active Python environment.
|
|
218
|
+
|
|
219
|
+
Installation per platform:
|
|
220
|
+
- **Ubuntu/Debian**: `sudo apt install python3 python3-dev`
|
|
221
|
+
- **Fedora/RHEL**: `sudo dnf install python3 python3-devel`
|
|
222
|
+
- **macOS**: `brew install python@3.12` (or use the system Python 3)
|
|
223
|
+
- **Windows**: Download from [python.org](https://www.python.org/downloads/)
|
|
224
|
+
|
|
225
|
+
### Build, Test, and Install
|
|
226
|
+
|
|
227
|
+
```bash
|
|
228
|
+
# Clone the repository
|
|
229
|
+
git clone https://github.com/sudonym1/snail.git
|
|
230
|
+
cd snail
|
|
231
|
+
|
|
232
|
+
make test
|
|
233
|
+
make install
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
**Note on Proptests**: The `snail-proptest` crate contains property-based tests that are skipped by default to keep development iteration fast.
|