snail-lang 0.3.9__tar.gz → 0.4.1__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 (64) hide show
  1. {snail_lang-0.3.9 → snail_lang-0.4.1}/Cargo.lock +7 -7
  2. {snail_lang-0.3.9 → snail_lang-0.4.1}/PKG-INFO +7 -14
  3. {snail_lang-0.3.9 → snail_lang-0.4.1}/README.md +5 -13
  4. {snail_lang-0.3.9 → snail_lang-0.4.1}/crates/snail-ast/Cargo.toml +1 -1
  5. {snail_lang-0.3.9 → snail_lang-0.4.1}/crates/snail-core/Cargo.toml +1 -1
  6. {snail_lang-0.3.9 → snail_lang-0.4.1}/crates/snail-error/Cargo.toml +1 -1
  7. {snail_lang-0.3.9 → snail_lang-0.4.1}/crates/snail-lower/Cargo.toml +1 -1
  8. {snail_lang-0.3.9 → snail_lang-0.4.1}/crates/snail-lower/README.md +2 -2
  9. {snail_lang-0.3.9 → snail_lang-0.4.1}/crates/snail-lower/src/awk.rs +8 -3
  10. {snail_lang-0.3.9 → snail_lang-0.4.1}/crates/snail-lower/src/constants.rs +0 -4
  11. {snail_lang-0.3.9 → snail_lang-0.4.1}/crates/snail-lower/src/program.rs +4 -3
  12. {snail_lang-0.3.9 → snail_lang-0.4.1}/crates/snail-lower/src/py_ast.rs +2 -2
  13. {snail_lang-0.3.9 → snail_lang-0.4.1}/crates/snail-lower/src/stmt.rs +23 -14
  14. {snail_lang-0.3.9 → snail_lang-0.4.1}/crates/snail-parser/Cargo.toml +1 -1
  15. {snail_lang-0.3.9 → snail_lang-0.4.1}/crates/snail-parser/src/lib.rs +1 -1
  16. {snail_lang-0.3.9 → snail_lang-0.4.1}/crates/snail-parser/src/snail.pest +2 -2
  17. {snail_lang-0.3.9 → snail_lang-0.4.1}/crates/snail-parser/tests/errors.rs +9 -3
  18. {snail_lang-0.3.9 → snail_lang-0.4.1}/crates/snail-parser/tests/parser.rs +1 -1
  19. {snail_lang-0.3.9 → snail_lang-0.4.1}/crates/snail-python/Cargo.toml +1 -1
  20. {snail_lang-0.3.9 → snail_lang-0.4.1}/crates/snail-python/src/lib.rs +57 -4
  21. {snail_lang-0.3.9 → snail_lang-0.4.1}/pyproject.toml +4 -1
  22. snail_lang-0.4.1/python/snail/cli.py +142 -0
  23. {snail_lang-0.3.9 → snail_lang-0.4.1}/python/snail/runtime/structured_accessor.py +1 -1
  24. snail_lang-0.3.9/python/snail/cli.py +0 -69
  25. snail_lang-0.3.9/python/snail/vendor/__init__.py +0 -0
  26. snail_lang-0.3.9/python/snail/vendor/jmespath/LICENSE +0 -21
  27. snail_lang-0.3.9/python/snail/vendor/jmespath/__init__.py +0 -12
  28. snail_lang-0.3.9/python/snail/vendor/jmespath/ast.py +0 -90
  29. snail_lang-0.3.9/python/snail/vendor/jmespath/compat.py +0 -19
  30. snail_lang-0.3.9/python/snail/vendor/jmespath/exceptions.py +0 -137
  31. snail_lang-0.3.9/python/snail/vendor/jmespath/functions.py +0 -366
  32. snail_lang-0.3.9/python/snail/vendor/jmespath/lexer.py +0 -258
  33. snail_lang-0.3.9/python/snail/vendor/jmespath/parser.py +0 -526
  34. snail_lang-0.3.9/python/snail/vendor/jmespath/visitor.py +0 -329
  35. {snail_lang-0.3.9 → snail_lang-0.4.1}/Cargo.toml +0 -0
  36. {snail_lang-0.3.9 → snail_lang-0.4.1}/LICENSE +0 -0
  37. {snail_lang-0.3.9 → snail_lang-0.4.1}/crates/snail-ast/README.md +0 -0
  38. {snail_lang-0.3.9 → snail_lang-0.4.1}/crates/snail-ast/src/ast.rs +0 -0
  39. {snail_lang-0.3.9 → snail_lang-0.4.1}/crates/snail-ast/src/awk.rs +0 -0
  40. {snail_lang-0.3.9 → snail_lang-0.4.1}/crates/snail-ast/src/lib.rs +0 -0
  41. {snail_lang-0.3.9 → snail_lang-0.4.1}/crates/snail-core/README.md +0 -0
  42. {snail_lang-0.3.9 → snail_lang-0.4.1}/crates/snail-core/src/lib.rs +0 -0
  43. {snail_lang-0.3.9 → snail_lang-0.4.1}/crates/snail-error/README.md +0 -0
  44. {snail_lang-0.3.9 → snail_lang-0.4.1}/crates/snail-error/src/lib.rs +0 -0
  45. {snail_lang-0.3.9 → snail_lang-0.4.1}/crates/snail-lower/src/expr.rs +0 -0
  46. {snail_lang-0.3.9 → snail_lang-0.4.1}/crates/snail-lower/src/helpers.rs +0 -0
  47. {snail_lang-0.3.9 → snail_lang-0.4.1}/crates/snail-lower/src/lib.rs +0 -0
  48. {snail_lang-0.3.9 → snail_lang-0.4.1}/crates/snail-lower/src/operators.rs +0 -0
  49. {snail_lang-0.3.9 → snail_lang-0.4.1}/crates/snail-parser/README.md +0 -0
  50. {snail_lang-0.3.9 → snail_lang-0.4.1}/crates/snail-parser/src/awk.rs +0 -0
  51. {snail_lang-0.3.9 → snail_lang-0.4.1}/crates/snail-parser/src/expr.rs +0 -0
  52. {snail_lang-0.3.9 → snail_lang-0.4.1}/crates/snail-parser/src/literal.rs +0 -0
  53. {snail_lang-0.3.9 → snail_lang-0.4.1}/crates/snail-parser/src/stmt.rs +0 -0
  54. {snail_lang-0.3.9 → snail_lang-0.4.1}/crates/snail-parser/src/string.rs +0 -0
  55. {snail_lang-0.3.9 → snail_lang-0.4.1}/crates/snail-parser/src/util.rs +0 -0
  56. {snail_lang-0.3.9 → snail_lang-0.4.1}/crates/snail-parser/tests/common.rs +0 -0
  57. {snail_lang-0.3.9 → snail_lang-0.4.1}/crates/snail-parser/tests/statements.rs +0 -0
  58. {snail_lang-0.3.9 → snail_lang-0.4.1}/crates/snail-parser/tests/syntax_expressions.rs +0 -0
  59. {snail_lang-0.3.9 → snail_lang-0.4.1}/crates/snail-parser/tests/syntax_strings.rs +0 -0
  60. {snail_lang-0.3.9 → snail_lang-0.4.1}/python/snail/__init__.py +0 -0
  61. {snail_lang-0.3.9 → snail_lang-0.4.1}/python/snail/runtime/__init__.py +0 -0
  62. {snail_lang-0.3.9 → snail_lang-0.4.1}/python/snail/runtime/compact_try.py +0 -0
  63. {snail_lang-0.3.9 → snail_lang-0.4.1}/python/snail/runtime/regex.py +0 -0
  64. {snail_lang-0.3.9 → snail_lang-0.4.1}/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.3.9"
488
+ version = "0.4.1"
489
489
 
490
490
  [[package]]
491
491
  name = "snail-core"
492
- version = "0.3.9"
492
+ version = "0.4.1"
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.9"
503
+ version = "0.4.1"
504
504
  dependencies = [
505
505
  "snail-ast",
506
506
  ]
507
507
 
508
508
  [[package]]
509
509
  name = "snail-lower"
510
- version = "0.3.9"
510
+ version = "0.4.1"
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.9"
519
+ version = "0.4.1"
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.9"
529
+ version = "0.4.1"
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.9"
543
+ version = "0.4.1"
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.9
3
+ Version: 0.4.1
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,17 +13,11 @@ 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>
15
-
16
- <h1 align="center">Snail</h1>
17
16
  <p align="center"><em>What do you get when you shove a snake in a shell?</em></p>
18
17
 
19
- <h1>Snail, while I hope it is useful to myself and others, is my attempt at
20
- improving my knowledge of AI code developement. Things are probably broken
21
- in interesting and horrible ways.</h1>
22
-
23
- ---
18
+ <h1 align="center">Snail</h1>
24
19
 
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.
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.
26
21
 
27
22
  ## Installing Snail
28
23
 
@@ -62,7 +57,7 @@ BEGIN { total = 0 }
62
57
  END { print("Sum:", total); assert total == 15}
63
58
  ```
64
59
 
65
- Built-in variables: `$l` (line), `$f` (fields), `$n` (line number), `$fn` (per-file line number), `$p` (file path), `$m` (last match).
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).
66
61
 
67
62
 
68
63
  ### Compact Error Handling
@@ -77,7 +72,7 @@ err = risky()?
77
72
  err = risky():$e?
78
73
 
79
74
  # Provide a fallback value (exception available as $e)
80
- value = js("malformed json"):{}?
75
+ value = js("malformed json"):{"error": "invalid json"}?
81
76
  details = fetch_url("foo.com"):"default html"?
82
77
  exception_info = fetch_url("example.com"):$e.http_response_code?
83
78
 
@@ -192,7 +187,7 @@ snail 'result = int("oops"):"bad int {$e}"?; print(result)'
192
187
  snail 'm = "user@example.com" in /^[\\w.]+@([\\w.]+)$/; if m { print(m[1]) }'
193
188
 
194
189
  # Awk mode: print line numbers for matches
195
- rg -n "TODO" README.md | snail --awk '/TODO/ { print("{$n}: {$l}") }'
190
+ rg -n "TODO" README.md | snail --awk '/TODO/ { print("{$n}: {$0}") }'
196
191
  ```
197
192
 
198
193
  ## 🏗️ Architecture
@@ -239,8 +234,6 @@ Installation per platform:
239
234
  - **macOS**: `brew install python@3.12` (or use the system Python 3)
240
235
  - **Windows**: Download from [python.org](https://www.python.org/downloads/)
241
236
 
242
- **No Python packages required**: Snail vendors jmespath under `snail.vendor`.
243
-
244
237
  ### Build, Test, and Install
245
238
 
246
239
  ```bash
@@ -1,17 +1,11 @@
1
1
  <p align="center">
2
2
  <img src="logo.png" alt="Snail logo" width="200">
3
3
  </p>
4
-
5
- <h1 align="center">Snail</h1>
6
4
  <p align="center"><em>What do you get when you shove a snake in a shell?</em></p>
7
5
 
8
- <h1>Snail, while I hope it is useful to myself and others, is my attempt at
9
- improving my knowledge of AI code developement. Things are probably broken
10
- in interesting and horrible ways.</h1>
11
-
12
- ---
6
+ <h1 align="center">Snail</h1>
13
7
 
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.
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.
15
9
 
16
10
  ## Installing Snail
17
11
 
@@ -51,7 +45,7 @@ BEGIN { total = 0 }
51
45
  END { print("Sum:", total); assert total == 15}
52
46
  ```
53
47
 
54
- Built-in variables: `$l` (line), `$f` (fields), `$n` (line number), `$fn` (per-file line number), `$p` (file path), `$m` (last match).
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).
55
49
 
56
50
 
57
51
  ### Compact Error Handling
@@ -66,7 +60,7 @@ err = risky()?
66
60
  err = risky():$e?
67
61
 
68
62
  # Provide a fallback value (exception available as $e)
69
- value = js("malformed json"):{}?
63
+ value = js("malformed json"):{"error": "invalid json"}?
70
64
  details = fetch_url("foo.com"):"default html"?
71
65
  exception_info = fetch_url("example.com"):$e.http_response_code?
72
66
 
@@ -181,7 +175,7 @@ snail 'result = int("oops"):"bad int {$e}"?; print(result)'
181
175
  snail 'm = "user@example.com" in /^[\\w.]+@([\\w.]+)$/; if m { print(m[1]) }'
182
176
 
183
177
  # Awk mode: print line numbers for matches
184
- rg -n "TODO" README.md | snail --awk '/TODO/ { print("{$n}: {$l}") }'
178
+ rg -n "TODO" README.md | snail --awk '/TODO/ { print("{$n}: {$0}") }'
185
179
  ```
186
180
 
187
181
  ## 🏗️ Architecture
@@ -228,8 +222,6 @@ Installation per platform:
228
222
  - **macOS**: `brew install python@3.12` (or use the system Python 3)
229
223
  - **Windows**: Download from [python.org](https://www.python.org/downloads/)
230
224
 
231
- **No Python packages required**: Snail vendors jmespath under `snail.vendor`.
232
-
233
225
  ### Build, Test, and Install
234
226
 
235
227
  ```bash
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "snail-ast"
3
- version = "0.3.9"
3
+ version = "0.4.1"
4
4
  edition = "2024"
5
5
  readme = "README.md"
6
6
 
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "snail-core"
3
- version = "0.3.9"
3
+ version = "0.4.1"
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.3.9"
3
+ version = "0.4.1"
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.3.9"
3
+ version = "0.4.1"
4
4
  edition = "2024"
5
5
  readme = "README.md"
6
6
 
@@ -27,7 +27,7 @@ This crate is the semantic transformation core of the Snail compiler. It takes S
27
27
  - **Regex expressions** (`/pattern/`): Transformed into `__snail_regex_compile(pattern)` call
28
28
  - **Regex matching** (`string in /pattern/`): Transformed into `__snail_regex_search(string, pattern)` call
29
29
  - **Structured accessors** (`$[query]`): Transformed into `__SnailStructuredAccessor(query)` instance
30
- - **Awk variables**: `$l`, `$f`, `$n`, `$fn`, `$p`, `$m` mapped to Python variable names
30
+ - **Awk variables**: `$0`, `$<num>`, `$n`, `$fn`, `$p`, `$m` mapped to Python variable names
31
31
 
32
32
  ## Awk Mode Lowering
33
33
 
@@ -35,7 +35,7 @@ When lowering awk programs, generates a complete Python AST that:
35
35
  1. Imports `sys` for accessing command-line arguments and stdin
36
36
  2. Executes BEGIN blocks before processing input
37
37
  3. Creates a main loop that reads lines from files or stdin
38
- 4. Updates awk variables (`$l`, `$f`, `$n`, etc.) for each line
38
+ 4. Updates awk variables (`$0`, `$<num>`, `$n`, etc.) for each line
39
39
  5. Evaluates patterns and executes actions for matching lines
40
40
  6. Executes END blocks after all input is processed
41
41
 
@@ -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
  };
@@ -427,8 +432,8 @@ fn regex_pattern_components(pattern: &Expr) -> Option<(Expr, RegexPattern, Sourc
427
432
  span,
428
433
  } => Some((*value.clone(), pattern.clone(), span.clone())),
429
434
  Expr::Regex { pattern, span } => Some((
430
- Expr::Name {
431
- name: SNAIL_AWK_LINE.to_string(),
435
+ Expr::FieldIndex {
436
+ index: "0".to_string(),
432
437
  span: span.clone(),
433
438
  },
434
439
  pattern.clone(),
@@ -9,8 +9,6 @@ pub const SNAIL_JMESPATH_QUERY: &str = "__snail_jmespath_query";
9
9
  pub const SNAIL_PARTIAL_HELPER: &str = "__snail_partial";
10
10
 
11
11
  // Awk-related constants (public within crate)
12
- pub(crate) const SNAIL_AWK_LINE: &str = "$l";
13
- pub(crate) const SNAIL_AWK_FIELDS: &str = "$f";
14
12
  pub(crate) const SNAIL_AWK_NR: &str = "$n";
15
13
  pub(crate) const SNAIL_AWK_FNR: &str = "$fn";
16
14
  pub(crate) const SNAIL_AWK_PATH: &str = "$p";
@@ -24,8 +22,6 @@ pub(crate) const SNAIL_AWK_MATCH_PYVAR: &str = "__snail_match";
24
22
 
25
23
  pub(crate) fn injected_py_name(name: &str) -> Option<&'static str> {
26
24
  match name {
27
- SNAIL_AWK_LINE => Some(SNAIL_AWK_LINE_PYVAR),
28
- SNAIL_AWK_FIELDS => Some(SNAIL_AWK_FIELDS_PYVAR),
29
25
  SNAIL_AWK_NR => Some(SNAIL_AWK_NR_PYVAR),
30
26
  SNAIL_AWK_FNR => Some(SNAIL_AWK_FNR_PYVAR),
31
27
  SNAIL_AWK_PATH => Some(SNAIL_AWK_PATH_PYVAR),
@@ -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
 
@@ -69,9 +69,9 @@ impl<'py> AstBuilder<'py> {
69
69
 
70
70
  pub fn set_location(node: &Bound<'_, PyAny>, span: &SourceSpan) -> PyResult<()> {
71
71
  node.setattr("lineno", span.start.line)?;
72
- node.setattr("col_offset", span.start.column)?;
72
+ node.setattr("col_offset", span.start.column.saturating_sub(1))?;
73
73
  node.setattr("end_lineno", span.end.line)?;
74
- node.setattr("end_col_offset", span.end.column)?;
74
+ node.setattr("end_col_offset", span.end.column.saturating_sub(1))?;
75
75
  Ok(())
76
76
  }
77
77
 
@@ -24,10 +24,10 @@ pub(crate) fn lower_stmt(builder: &AstBuilder<'_>, stmt: &Stmt) -> Result<PyObje
24
24
  span,
25
25
  } => {
26
26
  let test = lower_expr(builder, cond)?;
27
- let body = lower_block(builder, body)?;
27
+ let body = lower_block(builder, body, span)?;
28
28
  let orelse = else_body
29
29
  .as_ref()
30
- .map(|items| lower_block(builder, items))
30
+ .map(|items| lower_block(builder, items, span))
31
31
  .transpose()?
32
32
  .unwrap_or_default();
33
33
  builder
@@ -51,10 +51,10 @@ pub(crate) fn lower_stmt(builder: &AstBuilder<'_>, stmt: &Stmt) -> Result<PyObje
51
51
  } => {
52
52
  let target = lower_assign_target(builder, target)?;
53
53
  let iter = lower_expr(builder, iter)?;
54
- let body = lower_block(builder, body)?;
54
+ let body = lower_block(builder, body, span)?;
55
55
  let orelse = else_body
56
56
  .as_ref()
57
- .map(|items| lower_block(builder, items))
57
+ .map(|items| lower_block(builder, items, span))
58
58
  .transpose()?
59
59
  .unwrap_or_default();
60
60
  builder
@@ -77,7 +77,7 @@ pub(crate) fn lower_stmt(builder: &AstBuilder<'_>, stmt: &Stmt) -> Result<PyObje
77
77
  span,
78
78
  } => {
79
79
  let args = lower_parameters(builder, params)?;
80
- let body = lower_block(builder, body)?;
80
+ let body = lower_block(builder, body, span)?;
81
81
  builder
82
82
  .call_node(
83
83
  "FunctionDef",
@@ -94,7 +94,7 @@ pub(crate) fn lower_stmt(builder: &AstBuilder<'_>, stmt: &Stmt) -> Result<PyObje
94
94
  .map_err(py_err_to_lower)
95
95
  }
96
96
  Stmt::Class { name, body, span } => {
97
- let body = lower_block(builder, body)?;
97
+ let body = lower_block(builder, body, span)?;
98
98
  builder
99
99
  .call_node(
100
100
  "ClassDef",
@@ -116,19 +116,19 @@ pub(crate) fn lower_stmt(builder: &AstBuilder<'_>, stmt: &Stmt) -> Result<PyObje
116
116
  finally_body,
117
117
  span,
118
118
  } => {
119
- let body = lower_block(builder, body)?;
119
+ let body = lower_block(builder, body, span)?;
120
120
  let handlers = handlers
121
121
  .iter()
122
122
  .map(|handler| lower_except_handler(builder, handler))
123
123
  .collect::<Result<Vec<_>, _>>()?;
124
124
  let orelse = else_body
125
125
  .as_ref()
126
- .map(|items| lower_block(builder, items))
126
+ .map(|items| lower_block(builder, items, span))
127
127
  .transpose()?
128
128
  .unwrap_or_default();
129
129
  let finalbody = finally_body
130
130
  .as_ref()
131
- .map(|items| lower_block(builder, items))
131
+ .map(|items| lower_block(builder, items, span))
132
132
  .transpose()?
133
133
  .unwrap_or_default();
134
134
  builder
@@ -149,7 +149,7 @@ pub(crate) fn lower_stmt(builder: &AstBuilder<'_>, stmt: &Stmt) -> Result<PyObje
149
149
  .iter()
150
150
  .map(|item| lower_with_item(builder, item))
151
151
  .collect::<Result<Vec<_>, _>>()?;
152
- let body = lower_block(builder, body)?;
152
+ let body = lower_block(builder, body, span)?;
153
153
  builder
154
154
  .call_node(
155
155
  "With",
@@ -313,14 +313,16 @@ pub(crate) fn lower_stmt(builder: &AstBuilder<'_>, stmt: &Stmt) -> Result<PyObje
313
313
  pub(crate) fn lower_block(
314
314
  builder: &AstBuilder<'_>,
315
315
  block: &[Stmt],
316
+ span: &SourceSpan,
316
317
  ) -> Result<Vec<PyObject>, LowerError> {
317
- lower_block_with_auto_print(builder, block, false)
318
+ lower_block_with_auto_print(builder, block, false, span)
318
319
  }
319
320
 
320
321
  pub(crate) fn lower_block_with_auto_print(
321
322
  builder: &AstBuilder<'_>,
322
323
  block: &[Stmt],
323
324
  auto_print: bool,
325
+ span: &SourceSpan,
324
326
  ) -> Result<Vec<PyObject>, LowerError> {
325
327
  let mut stmts = Vec::new();
326
328
  for (idx, stmt) in block.iter().enumerate() {
@@ -340,6 +342,13 @@ pub(crate) fn lower_block_with_auto_print(
340
342
  }
341
343
  stmts.push(lower_stmt(builder, stmt)?);
342
344
  }
345
+ if stmts.is_empty() {
346
+ stmts.push(
347
+ builder
348
+ .call_node("Pass", Vec::new(), span)
349
+ .map_err(py_err_to_lower)?,
350
+ );
351
+ }
343
352
  Ok(stmts)
344
353
  }
345
354
 
@@ -352,7 +361,7 @@ fn lower_if(
352
361
  span: &SourceSpan,
353
362
  ) -> Result<PyObject, LowerError> {
354
363
  let test = lower_expr(builder, cond)?;
355
- let body = lower_block(builder, body)?;
364
+ let body = lower_block(builder, body, span)?;
356
365
  let orelse = if let Some((elif_cond, elif_body)) = elifs.first() {
357
366
  vec![lower_if(
358
367
  builder,
@@ -363,7 +372,7 @@ fn lower_if(
363
372
  span,
364
373
  )?]
365
374
  } else if let Some(else_body) = else_body {
366
- lower_block(builder, else_body)?
375
+ lower_block(builder, else_body, span)?
367
376
  } else {
368
377
  Vec::new()
369
378
  };
@@ -468,7 +477,7 @@ fn lower_except_handler(
468
477
  .as_ref()
469
478
  .map(|name| name.to_string().into_py(builder.py()))
470
479
  .unwrap_or_else(|| builder.py().None().into_py(builder.py()));
471
- let body = lower_block(builder, &handler.body)?;
480
+ let body = lower_block(builder, &handler.body, &handler.span)?;
472
481
  builder
473
482
  .call_node(
474
483
  "ExceptHandler",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "snail-parser"
3
- version = "0.3.9"
3
+ version = "0.4.1"
4
4
  edition = "2024"
5
5
  readme = "README.md"
6
6
 
@@ -86,7 +86,7 @@ pub fn parse_awk_program(source: &str) -> Result<AwkProgram, ParseError> {
86
86
  })
87
87
  }
88
88
 
89
- const AWK_ONLY_NAMES: [&str; 6] = ["$l", "$f", "$n", "$fn", "$p", "$m"];
89
+ const AWK_ONLY_NAMES: [&str; 4] = ["$n", "$fn", "$p", "$m"];
90
90
  const AWK_ONLY_MESSAGE: &str = "awk variables are only valid in awk mode; use --awk";
91
91
 
92
92
  fn validate_no_awk_syntax(program: &Program, source: &str) -> Result<(), ParseError> {
@@ -191,7 +191,7 @@ tuple_literal = { "(" ~ ")" | "(" ~ expr ~ "," ~ (expr ~ ("," ~ expr)* ~ ","?)?
191
191
  list_comp = { "[" ~ expr ~ comp_for ~ "]" }
192
192
  list_literal = { "[" ~ (expr ~ ("," ~ expr)*)? ~ "]" }
193
193
  dict_comp = { "{" ~ expr ~ ":" ~ expr ~ comp_for ~ "}" }
194
- dict_literal = { "{" ~ (dict_entry ~ ("," ~ dict_entry)*)? ~ "}" }
194
+ dict_literal = { "{" ~ dict_entry ~ ("," ~ dict_entry)* ~ "}" }
195
195
  dict_entry = { expr ~ ":" ~ expr }
196
196
 
197
197
  // Comprehension clauses
@@ -206,7 +206,7 @@ none = { "None" }
206
206
  // Special variables: exception, AWK fields, injected vars
207
207
  exception_var = { "$e" }
208
208
  field_index_var = @{ "$" ~ ASCII_DIGIT+ }
209
- injected_var = { "$fn" | "$l" | "$f" | "$n" | "$p" | "$m" }
209
+ injected_var = { "$fn" | "$n" | "$p" | "$m" }
210
210
 
211
211
  // Number, string, and regex literals
212
212
  number = @{ ASCII_DIGIT+ ~ ("." ~ ASCII_DIGIT+)? }
@@ -9,7 +9,7 @@ fn reports_parse_error_with_location() {
9
9
  let message = err.to_string();
10
10
  assert!(message.contains("-->"));
11
11
  assert!(message.contains("if"));
12
- expect_err_span(&err, 1, 7);
12
+ expect_err_span(&err, 1, 6);
13
13
  }
14
14
 
15
15
  #[test]
@@ -22,9 +22,9 @@ fn rejects_user_defined_dollar_identifiers() {
22
22
 
23
23
  #[test]
24
24
  fn rejects_awk_only_variables_in_regular_mode() {
25
- let err = parse_err("value = $l");
25
+ let err = parse_err("value = $n");
26
26
  let message = err.to_string();
27
- assert!(message.contains("$l"));
27
+ assert!(message.contains("$n"));
28
28
  assert!(message.contains("--awk"));
29
29
  expect_err_span(&err, 1, 9);
30
30
  }
@@ -84,6 +84,12 @@ fn parser_reports_error_on_missing_colon_in_dict() {
84
84
  assert!(err.span.is_some());
85
85
  }
86
86
 
87
+ #[test]
88
+ fn parser_rejects_empty_dict_literal() {
89
+ let err = parse_err("d = {}");
90
+ assert!(err.span.is_some());
91
+ }
92
+
87
93
  #[test]
88
94
  fn parser_rejects_incomplete_function_def() {
89
95
  let err = parse_err("def foo");
@@ -683,7 +683,7 @@ fn parses_class_followed_by_stmt_without_separator() {
683
683
 
684
684
  #[test]
685
685
  fn parses_try_followed_by_stmt_without_separator() {
686
- // Note: using explicit exception type since bare `except { }` is ambiguous with set literals
686
+ // Note: using explicit exception type since bare `except { }` is ambiguous
687
687
  let source = "try { x } except Exception { y } z";
688
688
  let program = parse_ok(source);
689
689
  assert_eq!(program.stmts.len(), 2);
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "snail-python"
3
- version = "0.3.9"
3
+ version = "0.4.1"
4
4
  edition.workspace = true
5
5
 
6
6
  [lib]
@@ -3,12 +3,14 @@
3
3
  use pyo3::Bound;
4
4
  use pyo3::exceptions::{PyRuntimeError, PySyntaxError, PySystemExit};
5
5
  use pyo3::prelude::*;
6
- use pyo3::types::{PyDict, PyList, PyModule};
6
+ use pyo3::types::{PyDict, PyList, PyModule, PyTuple};
7
7
  use snail_core::{
8
8
  CompileMode, compile_snail_source_with_auto_print, format_snail_error, parse_awk_program,
9
9
  parse_program,
10
10
  };
11
11
 
12
+ const SNAIL_TRACE_PREFIX: &str = "snail:";
13
+
12
14
  fn parse_mode(mode: &str) -> PyResult<CompileMode> {
13
15
  match mode {
14
16
  "snail" => Ok(CompileMode::Snail),
@@ -19,6 +21,53 @@ fn parse_mode(mode: &str) -> PyResult<CompileMode> {
19
21
  }
20
22
  }
21
23
 
24
+ fn display_filename(filename: &str) -> String {
25
+ if filename.starts_with(SNAIL_TRACE_PREFIX) {
26
+ filename.to_string()
27
+ } else {
28
+ format!("{SNAIL_TRACE_PREFIX}{filename}")
29
+ }
30
+ }
31
+
32
+ fn strip_display_prefix(filename: &str) -> &str {
33
+ filename
34
+ .strip_prefix(SNAIL_TRACE_PREFIX)
35
+ .unwrap_or(filename)
36
+ }
37
+
38
+ fn split_source_lines(source: &str) -> Vec<String> {
39
+ let mut lines = Vec::new();
40
+ let mut start = 0;
41
+ for (idx, ch) in source.char_indices() {
42
+ if ch == '\n' {
43
+ let end = idx + 1;
44
+ lines.push(source[start..end].to_string());
45
+ start = end;
46
+ }
47
+ }
48
+ if start < source.len() {
49
+ lines.push(source[start..].to_string());
50
+ }
51
+ lines
52
+ }
53
+
54
+ fn register_linecache(py: Python<'_>, filename: &str, source: &str) -> PyResult<()> {
55
+ let linecache = py.import_bound("linecache")?;
56
+ let cache = linecache.getattr("cache")?;
57
+ let lines = split_source_lines(source);
58
+ let entry = PyTuple::new_bound(
59
+ py,
60
+ vec![
61
+ source.len().into_py(py),
62
+ py.None().into_py(py),
63
+ PyList::new_bound(py, lines).into_py(py),
64
+ filename.into_py(py),
65
+ ],
66
+ );
67
+ cache.set_item(filename, entry)?;
68
+ Ok(())
69
+ }
70
+
22
71
  fn compile_source(
23
72
  py: Python<'_>,
24
73
  source: &str,
@@ -74,10 +123,12 @@ fn compile_py(
74
123
  ) -> PyResult<PyObject> {
75
124
  let mode = parse_mode(mode)?;
76
125
  let python_ast = compile_source(py, source, mode, auto_print, filename)?;
126
+ let display = display_filename(filename);
127
+ register_linecache(py, &display, source)?;
77
128
  let builtins = py.import_bound("builtins")?;
78
129
  let code = builtins
79
130
  .getattr("compile")?
80
- .call1((python_ast, filename, "exec"))?;
131
+ .call1((python_ast, display, "exec"))?;
81
132
  Ok(code.unbind())
82
133
  }
83
134
 
@@ -94,11 +145,13 @@ fn exec_py(
94
145
  ) -> PyResult<i32> {
95
146
  let mode = parse_mode(mode)?;
96
147
  let python_ast = compile_source(py, source, mode, auto_print, filename)?;
148
+ let display = display_filename(filename);
149
+ register_linecache(py, &display, source)?;
97
150
  let builtins = py.import_bound("builtins")?;
98
151
  let code = builtins
99
152
  .getattr("compile")?
100
- .call1((python_ast, filename, "exec"))?;
101
- let globals = prepare_globals(py, filename, &argv, auto_import)?;
153
+ .call1((python_ast, display, "exec"))?;
154
+ let globals = prepare_globals(py, strip_display_prefix(filename), &argv, auto_import)?;
102
155
 
103
156
  match builtins.getattr("exec")?.call1((code.as_any(), &globals)) {
104
157
  Ok(_) => Ok(0),
@@ -4,11 +4,14 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "snail-lang"
7
- version = "0.3.9"
7
+ version = "0.4.1"
8
8
  description = "Snail programming language interpreter"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
11
11
  license = { file = "LICENSE" }
12
+ dependencies = [
13
+ "jmespath>=1.0.1",
14
+ ]
12
15
 
13
16
  [project.optional-dependencies]
14
17
  dev = [