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