snail-lang 0.4.0__tar.gz → 0.5.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.
Files changed (56) hide show
  1. {snail_lang-0.4.0 → snail_lang-0.5.0}/Cargo.lock +7 -7
  2. {snail_lang-0.4.0 → snail_lang-0.5.0}/PKG-INFO +28 -19
  3. {snail_lang-0.4.0 → snail_lang-0.5.0}/README.md +27 -18
  4. {snail_lang-0.4.0 → snail_lang-0.5.0}/crates/snail-ast/Cargo.toml +1 -1
  5. {snail_lang-0.4.0 → snail_lang-0.5.0}/crates/snail-ast/src/ast.rs +26 -3
  6. {snail_lang-0.4.0 → snail_lang-0.5.0}/crates/snail-core/Cargo.toml +1 -1
  7. {snail_lang-0.4.0 → snail_lang-0.5.0}/crates/snail-error/Cargo.toml +1 -1
  8. {snail_lang-0.4.0 → snail_lang-0.5.0}/crates/snail-lower/Cargo.toml +1 -1
  9. {snail_lang-0.4.0 → snail_lang-0.5.0}/crates/snail-lower/src/awk.rs +6 -1
  10. {snail_lang-0.4.0 → snail_lang-0.5.0}/crates/snail-lower/src/constants.rs +7 -0
  11. {snail_lang-0.4.0 → snail_lang-0.5.0}/crates/snail-lower/src/expr.rs +225 -36
  12. {snail_lang-0.4.0 → snail_lang-0.5.0}/crates/snail-lower/src/program.rs +4 -3
  13. {snail_lang-0.4.0 → snail_lang-0.5.0}/crates/snail-lower/src/py_ast.rs +2 -2
  14. {snail_lang-0.4.0 → snail_lang-0.5.0}/crates/snail-lower/src/stmt.rs +388 -25
  15. {snail_lang-0.4.0 → snail_lang-0.5.0}/crates/snail-parser/Cargo.toml +1 -1
  16. {snail_lang-0.4.0 → snail_lang-0.5.0}/crates/snail-parser/src/expr.rs +15 -0
  17. {snail_lang-0.4.0 → snail_lang-0.5.0}/crates/snail-parser/src/lib.rs +29 -3
  18. {snail_lang-0.4.0 → snail_lang-0.5.0}/crates/snail-parser/src/snail.pest +20 -6
  19. {snail_lang-0.4.0 → snail_lang-0.5.0}/crates/snail-parser/src/stmt.rs +249 -60
  20. {snail_lang-0.4.0 → snail_lang-0.5.0}/crates/snail-parser/tests/common.rs +13 -1
  21. {snail_lang-0.4.0 → snail_lang-0.5.0}/crates/snail-parser/tests/errors.rs +7 -1
  22. {snail_lang-0.4.0 → snail_lang-0.5.0}/crates/snail-parser/tests/parser.rs +168 -5
  23. {snail_lang-0.4.0 → snail_lang-0.5.0}/crates/snail-parser/tests/statements.rs +1 -1
  24. {snail_lang-0.4.0 → snail_lang-0.5.0}/crates/snail-python/Cargo.toml +2 -1
  25. snail_lang-0.5.0/crates/snail-python/build.rs +84 -0
  26. {snail_lang-0.4.0 → snail_lang-0.5.0}/crates/snail-python/src/lib.rs +91 -5
  27. {snail_lang-0.4.0 → snail_lang-0.5.0}/pyproject.toml +1 -1
  28. {snail_lang-0.4.0 → snail_lang-0.5.0}/python/snail/__init__.py +2 -2
  29. snail_lang-0.5.0/python/snail/cli.py +177 -0
  30. {snail_lang-0.4.0 → snail_lang-0.5.0}/python/snail/runtime/__init__.py +16 -0
  31. snail_lang-0.5.0/python/snail/runtime/regex.py +37 -0
  32. snail_lang-0.4.0/python/snail/cli.py +0 -69
  33. snail_lang-0.4.0/python/snail/runtime/regex.py +0 -11
  34. {snail_lang-0.4.0 → snail_lang-0.5.0}/Cargo.toml +0 -0
  35. {snail_lang-0.4.0 → snail_lang-0.5.0}/LICENSE +0 -0
  36. {snail_lang-0.4.0 → snail_lang-0.5.0}/crates/snail-ast/README.md +0 -0
  37. {snail_lang-0.4.0 → snail_lang-0.5.0}/crates/snail-ast/src/awk.rs +0 -0
  38. {snail_lang-0.4.0 → snail_lang-0.5.0}/crates/snail-ast/src/lib.rs +0 -0
  39. {snail_lang-0.4.0 → snail_lang-0.5.0}/crates/snail-core/README.md +0 -0
  40. {snail_lang-0.4.0 → snail_lang-0.5.0}/crates/snail-core/src/lib.rs +0 -0
  41. {snail_lang-0.4.0 → snail_lang-0.5.0}/crates/snail-error/README.md +0 -0
  42. {snail_lang-0.4.0 → snail_lang-0.5.0}/crates/snail-error/src/lib.rs +0 -0
  43. {snail_lang-0.4.0 → snail_lang-0.5.0}/crates/snail-lower/README.md +0 -0
  44. {snail_lang-0.4.0 → snail_lang-0.5.0}/crates/snail-lower/src/helpers.rs +0 -0
  45. {snail_lang-0.4.0 → snail_lang-0.5.0}/crates/snail-lower/src/lib.rs +0 -0
  46. {snail_lang-0.4.0 → snail_lang-0.5.0}/crates/snail-lower/src/operators.rs +0 -0
  47. {snail_lang-0.4.0 → snail_lang-0.5.0}/crates/snail-parser/README.md +0 -0
  48. {snail_lang-0.4.0 → snail_lang-0.5.0}/crates/snail-parser/src/awk.rs +0 -0
  49. {snail_lang-0.4.0 → snail_lang-0.5.0}/crates/snail-parser/src/literal.rs +0 -0
  50. {snail_lang-0.4.0 → snail_lang-0.5.0}/crates/snail-parser/src/string.rs +0 -0
  51. {snail_lang-0.4.0 → snail_lang-0.5.0}/crates/snail-parser/src/util.rs +0 -0
  52. {snail_lang-0.4.0 → snail_lang-0.5.0}/crates/snail-parser/tests/syntax_expressions.rs +0 -0
  53. {snail_lang-0.4.0 → snail_lang-0.5.0}/crates/snail-parser/tests/syntax_strings.rs +0 -0
  54. {snail_lang-0.4.0 → snail_lang-0.5.0}/python/snail/runtime/compact_try.py +0 -0
  55. {snail_lang-0.4.0 → snail_lang-0.5.0}/python/snail/runtime/structured_accessor.py +0 -0
  56. {snail_lang-0.4.0 → snail_lang-0.5.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.4.0"
488
+ version = "0.5.0"
489
489
 
490
490
  [[package]]
491
491
  name = "snail-core"
492
- version = "0.4.0"
492
+ version = "0.5.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.4.0"
503
+ version = "0.5.0"
504
504
  dependencies = [
505
505
  "snail-ast",
506
506
  ]
507
507
 
508
508
  [[package]]
509
509
  name = "snail-lower"
510
- version = "0.4.0"
510
+ version = "0.5.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.4.0"
519
+ version = "0.5.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.4.0"
529
+ version = "0.5.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.4.0"
543
+ version = "0.5.0"
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.4.0
3
+ Version: 0.5.0
4
4
  Requires-Dist: jmespath>=1.0.1
5
5
  Requires-Dist: maturin>=1.5 ; extra == 'dev'
6
6
  Requires-Dist: pytest ; extra == 'dev'
@@ -59,7 +59,6 @@ END { print("Sum:", total); assert total == 15}
59
59
 
60
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
61
 
62
-
63
62
  ### Compact Error Handling
64
63
 
65
64
  The `?` operator makes error handling terse yet expressive:
@@ -72,7 +71,7 @@ err = risky()?
72
71
  err = risky():$e?
73
72
 
74
73
  # Provide a fallback value (exception available as $e)
75
- value = js("malformed json"):{}?
74
+ value = js("malformed json"):{"error": "invalid json"}?
76
75
  details = fetch_url("foo.com"):"default html"?
77
76
  exception_info = fetch_url("example.com"):$e.http_response_code?
78
77
 
@@ -81,6 +80,25 @@ name = risky("")?.__class__.__name__
81
80
  args = risky("becomes a list"):[1,2,3]?[0]
82
81
  ```
83
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
+
84
102
  ### Pipeline Operator
85
103
 
86
104
  The `|` operator enables data pipelining as syntactic sugar for nested
@@ -144,9 +162,12 @@ if bad_email in /^[\w.]+@[\w.]+$/ {
144
162
  # Compiled regex for reuse
145
163
  pattern = /\d{3}-\d{4}/
146
164
  match = pattern.search(phone)
165
+ match2 = "555-1212" in pattern
147
166
  ```
148
167
 
149
- NOTE: this feature is WIP.
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).
150
171
 
151
172
  ### JSON Queries with JMESPath
152
173
 
@@ -184,28 +205,16 @@ snail 'js($(curl -s https://api.github.com/repos/sudonym1/snail)) | $[stargazers
184
205
  snail 'result = int("oops"):"bad int {$e}"?; print(result)'
185
206
 
186
207
  # Regex match and capture
187
- snail 'm = "user@example.com" in /^[\\w.]+@([\\w.]+)$/; if m { print(m[1]) }'
208
+ snail 'if let [_, user, domain] = "user@example.com" in /^[\w.]+@([\w.]+)$/ { print(domain) }'
188
209
 
189
210
  # Awk mode: print line numbers for matches
190
211
  rg -n "TODO" README.md | snail --awk '/TODO/ { print("{$n}: {$0}") }'
191
212
  ```
192
213
 
193
- ## 🏗️ Architecture
194
-
195
- **Key Components:**
196
-
197
- - **Parser**: Uses [Pest](https://pest.rs/) parser generator with PEG grammar defined in `src/snail.pest`
198
- - **AST**: Separate representations for regular Snail (`Program`) and awk mode (`AwkProgram`) with source spans for error reporting
199
- - **Lowering**: Transforms Snail AST into Python AST, emitting helper calls backed by `snail.runtime`
200
- - `?` operator → `__snail_compact_try`
201
- - `$(cmd)` subprocess capture → `__SnailSubprocessCapture`
202
- - `@(cmd)` subprocess status → `__SnailSubprocessStatus`
203
- - Regex literals → `__snail_regex_search` and `__snail_regex_compile`
204
- - **Execution**: Compiles Python AST directly for in-process execution
205
- - **CLI**: Python wrapper (`python/snail/cli.py`) that executes via the extension module
206
-
207
214
  ## 📚 Documentation
208
215
 
216
+ Documentation is WIP
217
+
209
218
  - **[Language Reference](docs/REFERENCE.md)** — Complete syntax and semantics
210
219
  - **[examples/all_syntax.snail](examples/all_syntax.snail)** — Every feature in one file
211
220
  - **[examples/awk.snail](examples/awk.snail)** — Awk mode examples
@@ -47,7 +47,6 @@ END { print("Sum:", total); assert total == 15}
47
47
 
48
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
49
 
50
-
51
50
  ### Compact Error Handling
52
51
 
53
52
  The `?` operator makes error handling terse yet expressive:
@@ -60,7 +59,7 @@ err = risky()?
60
59
  err = risky():$e?
61
60
 
62
61
  # Provide a fallback value (exception available as $e)
63
- value = js("malformed json"):{}?
62
+ value = js("malformed json"):{"error": "invalid json"}?
64
63
  details = fetch_url("foo.com"):"default html"?
65
64
  exception_info = fetch_url("example.com"):$e.http_response_code?
66
65
 
@@ -69,6 +68,25 @@ name = risky("")?.__class__.__name__
69
68
  args = risky("becomes a list"):[1,2,3]?[0]
70
69
  ```
71
70
 
71
+ ### Destructuring + `if let` / `while let`
72
+
73
+ Unpack tuples and lists directly, including Python-style rest bindings:
74
+
75
+ ```snail
76
+ x, *xs = [1, 2, 3]
77
+
78
+ if let [head, *tail] = [1, 2, 3]; head > 0 {
79
+ print(head, tail)
80
+ }
81
+ ```
82
+
83
+ `if let`/`while let` only enter the block when the destructuring succeeds. A guard
84
+ after `;` lets you add a boolean check that runs after the bindings are created.
85
+
86
+ Note that this syntax is more powerful than the walrus operator as that does
87
+ not allow for destructuring.
88
+
89
+
72
90
  ### Pipeline Operator
73
91
 
74
92
  The `|` operator enables data pipelining as syntactic sugar for nested
@@ -132,9 +150,12 @@ if bad_email in /^[\w.]+@[\w.]+$/ {
132
150
  # Compiled regex for reuse
133
151
  pattern = /\d{3}-\d{4}/
134
152
  match = pattern.search(phone)
153
+ match2 = "555-1212" in pattern
135
154
  ```
136
155
 
137
- NOTE: this feature is WIP.
156
+ Snail regexes don't return a match object, rather they return a tuple
157
+ containing all of the match groups, including group 0. Both `search` and `in`
158
+ return the same tuple (or `()` when there is no match).
138
159
 
139
160
  ### JSON Queries with JMESPath
140
161
 
@@ -172,28 +193,16 @@ snail 'js($(curl -s https://api.github.com/repos/sudonym1/snail)) | $[stargazers
172
193
  snail 'result = int("oops"):"bad int {$e}"?; print(result)'
173
194
 
174
195
  # Regex match and capture
175
- snail 'm = "user@example.com" in /^[\\w.]+@([\\w.]+)$/; if m { print(m[1]) }'
196
+ snail 'if let [_, user, domain] = "user@example.com" in /^[\w.]+@([\w.]+)$/ { print(domain) }'
176
197
 
177
198
  # Awk mode: print line numbers for matches
178
199
  rg -n "TODO" README.md | snail --awk '/TODO/ { print("{$n}: {$0}") }'
179
200
  ```
180
201
 
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
202
  ## 📚 Documentation
196
203
 
204
+ Documentation is WIP
205
+
197
206
  - **[Language Reference](docs/REFERENCE.md)** — Complete syntax and semantics
198
207
  - **[examples/all_syntax.snail](examples/all_syntax.snail)** — Every feature in one file
199
208
  - **[examples/awk.snail](examples/awk.snail)** — Awk mode examples
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "snail-ast"
3
- version = "0.4.0"
3
+ version = "0.5.0"
4
4
  edition = "2024"
5
5
  readme = "README.md"
6
6
 
@@ -20,14 +20,14 @@ pub struct Program {
20
20
  #[derive(Debug, Clone, PartialEq)]
21
21
  pub enum Stmt {
22
22
  If {
23
- cond: Expr,
23
+ cond: Condition,
24
24
  body: Vec<Stmt>,
25
- elifs: Vec<(Expr, Vec<Stmt>)>,
25
+ elifs: Vec<(Condition, Vec<Stmt>)>,
26
26
  else_body: Option<Vec<Stmt>>,
27
27
  span: SourceSpan,
28
28
  },
29
29
  While {
30
- cond: Expr,
30
+ cond: Condition,
31
31
  body: Vec<Stmt>,
32
32
  else_body: Option<Vec<Stmt>>,
33
33
  span: SourceSpan,
@@ -148,6 +148,29 @@ pub enum AssignTarget {
148
148
  index: Box<Expr>,
149
149
  span: SourceSpan,
150
150
  },
151
+ Starred {
152
+ target: Box<AssignTarget>,
153
+ span: SourceSpan,
154
+ },
155
+ Tuple {
156
+ elements: Vec<AssignTarget>,
157
+ span: SourceSpan,
158
+ },
159
+ List {
160
+ elements: Vec<AssignTarget>,
161
+ span: SourceSpan,
162
+ },
163
+ }
164
+
165
+ #[derive(Debug, Clone, PartialEq)]
166
+ pub enum Condition {
167
+ Expr(Box<Expr>),
168
+ Let {
169
+ target: Box<AssignTarget>,
170
+ value: Box<Expr>,
171
+ guard: Option<Box<Expr>>,
172
+ span: SourceSpan,
173
+ },
151
174
  }
152
175
 
153
176
  #[derive(Debug, Clone, PartialEq)]
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "snail-core"
3
- version = "0.4.0"
3
+ version = "0.5.0"
4
4
  edition.workspace = true
5
5
  readme = "README.md"
6
6
 
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "snail-error"
3
- version = "0.4.0"
3
+ version = "0.5.0"
4
4
  edition = "2024"
5
5
  readme = "README.md"
6
6
 
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "snail-lower"
3
- version = "0.4.0"
3
+ version = "0.5.0"
4
4
  edition = "2024"
5
5
  readme = "README.md"
6
6
 
@@ -365,7 +365,12 @@ pub(crate) fn lower_awk_rules_with_auto_print(
365
365
  let mut stmts = Vec::new();
366
366
  for rule in rules {
367
367
  let mut action = if rule.has_explicit_action() {
368
- lower_block_with_auto_print(builder, rule.action.as_ref().unwrap(), auto_print)?
368
+ lower_block_with_auto_print(
369
+ builder,
370
+ rule.action.as_ref().unwrap(),
371
+ auto_print,
372
+ &rule.span,
373
+ )?
369
374
  } else {
370
375
  vec![awk_default_print(builder, &rule.span)?]
371
376
  };
@@ -7,6 +7,13 @@ pub const SNAIL_REGEX_SEARCH: &str = "__snail_regex_search";
7
7
  pub const SNAIL_REGEX_COMPILE: &str = "__snail_regex_compile";
8
8
  pub const SNAIL_JMESPATH_QUERY: &str = "__snail_jmespath_query";
9
9
  pub const SNAIL_PARTIAL_HELPER: &str = "__snail_partial";
10
+ pub const SNAIL_CONTAINS_HELPER: &str = "__snail_contains__";
11
+ pub const SNAIL_CONTAINS_NOT_HELPER: &str = "__snail_contains_not__";
12
+ pub(crate) const SNAIL_LET_VALUE: &str = "__snail_let_value";
13
+ pub(crate) const SNAIL_LET_OK: &str = "__snail_let_ok";
14
+ pub(crate) const SNAIL_LET_KEEP: &str = "__snail_let_keep";
15
+ pub(crate) const SNAIL_COMPARE_LEFT: &str = "__snail_compare_left";
16
+ pub(crate) const SNAIL_COMPARE_RIGHT: &str = "__snail_compare_right";
10
17
 
11
18
  // Awk-related constants (public within crate)
12
19
  pub(crate) const SNAIL_AWK_NR: &str = "$n";
@@ -36,6 +36,44 @@ pub(crate) fn lower_assign_target(
36
36
  .call_node("Subscript", vec![value_expr, index_expr, store_ctx], span)
37
37
  .map_err(py_err_to_lower)
38
38
  }
39
+ AssignTarget::Starred { target, span } => {
40
+ let value = lower_assign_target(builder, target)?;
41
+ builder
42
+ .call_node("Starred", vec![value, store_ctx.clone()], span)
43
+ .map_err(py_err_to_lower)
44
+ }
45
+ AssignTarget::Tuple { elements, span } => {
46
+ let mut lowered = Vec::with_capacity(elements.len());
47
+ for element in elements {
48
+ lowered.push(lower_assign_target(builder, element)?);
49
+ }
50
+ builder
51
+ .call_node(
52
+ "Tuple",
53
+ vec![
54
+ PyList::new_bound(builder.py(), lowered).into_py(builder.py()),
55
+ store_ctx,
56
+ ],
57
+ span,
58
+ )
59
+ .map_err(py_err_to_lower)
60
+ }
61
+ AssignTarget::List { elements, span } => {
62
+ let mut lowered = Vec::with_capacity(elements.len());
63
+ for element in elements {
64
+ lowered.push(lower_assign_target(builder, element)?);
65
+ }
66
+ builder
67
+ .call_node(
68
+ "List",
69
+ vec![
70
+ PyList::new_bound(builder.py(), lowered).into_py(builder.py()),
71
+ store_ctx,
72
+ ],
73
+ span,
74
+ )
75
+ .map_err(py_err_to_lower)
76
+ }
39
77
  }
40
78
  }
41
79
 
@@ -63,6 +101,41 @@ pub(crate) fn lower_delete_target(
63
101
  .call_node("Subscript", vec![value_expr, index_expr, del_ctx], span)
64
102
  .map_err(py_err_to_lower)
65
103
  }
104
+ AssignTarget::Starred { .. } => Err(LowerError::new(
105
+ "starred targets are not valid in del statements",
106
+ )),
107
+ AssignTarget::Tuple { elements, span } => {
108
+ let mut lowered = Vec::with_capacity(elements.len());
109
+ for element in elements {
110
+ lowered.push(lower_delete_target(builder, element)?);
111
+ }
112
+ builder
113
+ .call_node(
114
+ "Tuple",
115
+ vec![
116
+ PyList::new_bound(builder.py(), lowered).into_py(builder.py()),
117
+ del_ctx,
118
+ ],
119
+ span,
120
+ )
121
+ .map_err(py_err_to_lower)
122
+ }
123
+ AssignTarget::List { elements, span } => {
124
+ let mut lowered = Vec::with_capacity(elements.len());
125
+ for element in elements {
126
+ lowered.push(lower_delete_target(builder, element)?);
127
+ }
128
+ builder
129
+ .call_node(
130
+ "List",
131
+ vec![
132
+ PyList::new_bound(builder.py(), lowered).into_py(builder.py()),
133
+ del_ctx,
134
+ ],
135
+ span,
136
+ )
137
+ .map_err(py_err_to_lower)
138
+ }
66
139
  }
67
140
  }
68
141
 
@@ -116,6 +189,145 @@ fn lower_regex_pattern_expr(
116
189
  }
117
190
  }
118
191
 
192
+ fn lower_contains_call(
193
+ builder: &AstBuilder<'_>,
194
+ helper_name: &str,
195
+ left: PyObject,
196
+ right: PyObject,
197
+ span: &SourceSpan,
198
+ ) -> Result<PyObject, LowerError> {
199
+ let func = name_expr(
200
+ builder,
201
+ helper_name,
202
+ span,
203
+ builder.load_ctx().map_err(py_err_to_lower)?,
204
+ )?;
205
+ let args = vec![left, right];
206
+ builder
207
+ .call_node(
208
+ "Call",
209
+ vec![
210
+ func,
211
+ PyList::new_bound(builder.py(), args).into_py(builder.py()),
212
+ PyList::empty_bound(builder.py()).into_py(builder.py()),
213
+ ],
214
+ span,
215
+ )
216
+ .map_err(py_err_to_lower)
217
+ }
218
+
219
+ fn lower_compare_pair(
220
+ builder: &AstBuilder<'_>,
221
+ left: PyObject,
222
+ op: CompareOp,
223
+ right: PyObject,
224
+ span: &SourceSpan,
225
+ ) -> Result<PyObject, LowerError> {
226
+ match op {
227
+ CompareOp::In => lower_contains_call(builder, SNAIL_CONTAINS_HELPER, left, right, span),
228
+ CompareOp::NotIn => {
229
+ lower_contains_call(builder, SNAIL_CONTAINS_NOT_HELPER, left, right, span)
230
+ }
231
+ _ => {
232
+ let op = lower_compare_op(builder, op)?;
233
+ builder
234
+ .call_node(
235
+ "Compare",
236
+ vec![
237
+ left,
238
+ PyList::new_bound(builder.py(), vec![op]).into_py(builder.py()),
239
+ PyList::new_bound(builder.py(), vec![right]).into_py(builder.py()),
240
+ ],
241
+ span,
242
+ )
243
+ .map_err(py_err_to_lower)
244
+ }
245
+ }
246
+ }
247
+
248
+ fn named_expr(
249
+ builder: &AstBuilder<'_>,
250
+ name: &str,
251
+ value: PyObject,
252
+ span: &SourceSpan,
253
+ ) -> Result<PyObject, LowerError> {
254
+ let target = name_expr(
255
+ builder,
256
+ name,
257
+ span,
258
+ builder.store_ctx().map_err(py_err_to_lower)?,
259
+ )?;
260
+ builder
261
+ .call_node("NamedExpr", vec![target, value], span)
262
+ .map_err(py_err_to_lower)
263
+ }
264
+
265
+ fn lower_compare_chain(
266
+ builder: &AstBuilder<'_>,
267
+ left: &Expr,
268
+ ops: &[CompareOp],
269
+ comparators: &[Expr],
270
+ span: &SourceSpan,
271
+ exception_name: Option<&str>,
272
+ ) -> Result<PyObject, LowerError> {
273
+ if ops.len() != comparators.len() {
274
+ return Err(LowerError::new(
275
+ "comparison ops must match comparator count",
276
+ ));
277
+ }
278
+ if ops.is_empty() {
279
+ return Err(LowerError::new("comparison missing operator"));
280
+ }
281
+
282
+ let left_expr = lower_expr_with_exception(builder, left, exception_name)?;
283
+ let right_expr = lower_expr_with_exception(builder, &comparators[0], exception_name)?;
284
+ let left_named = named_expr(builder, SNAIL_COMPARE_LEFT, left_expr, span)?;
285
+ let right_named = named_expr(builder, SNAIL_COMPARE_RIGHT, right_expr, span)?;
286
+ let mut comparisons = Vec::with_capacity(ops.len());
287
+ comparisons.push(lower_compare_pair(
288
+ builder,
289
+ left_named,
290
+ ops[0],
291
+ right_named,
292
+ span,
293
+ )?);
294
+
295
+ for (index, op) in ops.iter().enumerate().skip(1) {
296
+ let prev_right = name_expr(
297
+ builder,
298
+ SNAIL_COMPARE_RIGHT,
299
+ span,
300
+ builder.load_ctx().map_err(py_err_to_lower)?,
301
+ )?;
302
+ let left_named = named_expr(builder, SNAIL_COMPARE_LEFT, prev_right, span)?;
303
+ let right_expr = lower_expr_with_exception(builder, &comparators[index], exception_name)?;
304
+ let right_named = named_expr(builder, SNAIL_COMPARE_RIGHT, right_expr, span)?;
305
+ comparisons.push(lower_compare_pair(
306
+ builder,
307
+ left_named,
308
+ *op,
309
+ right_named,
310
+ span,
311
+ )?);
312
+ }
313
+
314
+ if comparisons.len() == 1 {
315
+ return Ok(comparisons.remove(0));
316
+ }
317
+
318
+ let op = lower_bool_op(builder, BinaryOp::And)?;
319
+ builder
320
+ .call_node(
321
+ "BoolOp",
322
+ vec![
323
+ op,
324
+ PyList::new_bound(builder.py(), comparisons).into_py(builder.py()),
325
+ ],
326
+ span,
327
+ )
328
+ .map_err(py_err_to_lower)
329
+ }
330
+
119
331
  fn lower_fstring_parts(
120
332
  builder: &AstBuilder<'_>,
121
333
  parts: &[FStringPart],
@@ -404,26 +616,14 @@ pub(crate) fn lower_expr_with_exception(
404
616
  comparators,
405
617
  span,
406
618
  } => {
407
- let left_expr = lower_expr_with_exception(builder, left, exception_name)?;
408
- let ops = ops
409
- .iter()
410
- .map(|op| lower_compare_op(builder, *op))
411
- .collect::<Result<Vec<_>, _>>()?;
412
- let comparators = comparators
413
- .iter()
414
- .map(|expr| lower_expr_with_exception(builder, expr, exception_name))
415
- .collect::<Result<Vec<_>, _>>()?;
416
- builder
417
- .call_node(
418
- "Compare",
419
- vec![
420
- left_expr,
421
- PyList::new_bound(builder.py(), ops).into_py(builder.py()),
422
- PyList::new_bound(builder.py(), comparators).into_py(builder.py()),
423
- ],
424
- span,
425
- )
426
- .map_err(py_err_to_lower)
619
+ if ops.len() == 1 {
620
+ let left_expr = lower_expr_with_exception(builder, left, exception_name)?;
621
+ let right_expr =
622
+ lower_expr_with_exception(builder, &comparators[0], exception_name)?;
623
+ lower_compare_pair(builder, left_expr, ops[0], right_expr, span)
624
+ } else {
625
+ lower_compare_chain(builder, left, ops, comparators, span, exception_name)
626
+ }
427
627
  }
428
628
  Expr::IfExpr {
429
629
  test,
@@ -613,31 +813,20 @@ pub(crate) fn lower_expr_with_exception(
613
813
  Expr::Attribute { value, attr, span } => {
614
814
  let value = lower_expr_with_exception(builder, value, exception_name)?;
615
815
  if attr.chars().all(|ch| ch.is_ascii_digit()) {
616
- let group_index = attr
816
+ let index = attr
617
817
  .parse::<i32>()
618
818
  .map_err(|_| LowerError::new(format!("Invalid match group index: .{attr}")))?;
619
- let group_attr = builder
819
+ let index_expr = number_expr(builder, &index.to_string(), span)?;
820
+ return builder
620
821
  .call_node(
621
- "Attribute",
822
+ "Subscript",
622
823
  vec![
623
824
  value,
624
- "group".into_py(builder.py()),
825
+ index_expr,
625
826
  builder.load_ctx().map_err(py_err_to_lower)?,
626
827
  ],
627
828
  span,
628
829
  )
629
- .map_err(py_err_to_lower)?;
630
- let index_expr = number_expr(builder, &group_index.to_string(), span)?;
631
- return builder
632
- .call_node(
633
- "Call",
634
- vec![
635
- group_attr,
636
- PyList::new_bound(builder.py(), vec![index_expr]).into_py(builder.py()),
637
- PyList::empty_bound(builder.py()).into_py(builder.py()),
638
- ],
639
- span,
640
- )
641
830
  .map_err(py_err_to_lower);
642
831
  }
643
832
  builder
@@ -18,7 +18,8 @@ pub fn lower_program_with_auto_print(
18
18
  auto_print_last: bool,
19
19
  ) -> Result<PyObject, LowerError> {
20
20
  let builder = AstBuilder::new(py).map_err(py_err_to_lower)?;
21
- let body = lower_block_with_auto_print(&builder, &program.stmts, auto_print_last)?;
21
+ let body =
22
+ lower_block_with_auto_print(&builder, &program.stmts, auto_print_last, &program.span)?;
22
23
  builder.module(body, &program.span).map_err(py_err_to_lower)
23
24
  }
24
25
 
@@ -62,7 +63,7 @@ pub fn lower_awk_program_with_auto_print(
62
63
 
63
64
  let mut main_body = Vec::new();
64
65
  for block in &program.begin_blocks {
65
- let lowered = lower_block_with_auto_print(&builder, block, auto_print)?;
66
+ let lowered = lower_block_with_auto_print(&builder, block, auto_print, &span)?;
66
67
  main_body.extend(lowered);
67
68
  }
68
69
 
@@ -166,7 +167,7 @@ pub fn lower_awk_program_with_auto_print(
166
167
  main_body.push(for_loop);
167
168
 
168
169
  for block in &program.end_blocks {
169
- let lowered = lower_block_with_auto_print(&builder, block, auto_print)?;
170
+ let lowered = lower_block_with_auto_print(&builder, block, auto_print, &span)?;
170
171
  main_body.extend(lowered);
171
172
  }
172
173