snail-lang 0.7.0__tar.gz → 0.7.2__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 (59) hide show
  1. {snail_lang-0.7.0 → snail_lang-0.7.2}/Cargo.lock +5 -5
  2. {snail_lang-0.7.0 → snail_lang-0.7.2}/Cargo.toml +6 -0
  3. {snail_lang-0.7.0 → snail_lang-0.7.2}/PKG-INFO +19 -7
  4. {snail_lang-0.7.0 → snail_lang-0.7.2}/README.md +16 -5
  5. {snail_lang-0.7.0 → snail_lang-0.7.2}/crates/snail-ast/Cargo.toml +1 -1
  6. {snail_lang-0.7.0 → snail_lang-0.7.2}/crates/snail-error/Cargo.toml +1 -1
  7. {snail_lang-0.7.0 → snail_lang-0.7.2}/crates/snail-parser/Cargo.toml +1 -1
  8. {snail_lang-0.7.0 → snail_lang-0.7.2}/crates/snail-parser/src/snail.pest +2 -2
  9. {snail_lang-0.7.0 → snail_lang-0.7.2}/crates/snail-parser/tests/syntax_expressions.rs +9 -0
  10. {snail_lang-0.7.0 → snail_lang-0.7.2}/crates/snail-python/Cargo.toml +2 -2
  11. {snail_lang-0.7.0 → snail_lang-0.7.2}/crates/snail-python/src/lower/constants.rs +3 -0
  12. {snail_lang-0.7.0 → snail_lang-0.7.2}/crates/snail-python/src/lower/expr.rs +57 -68
  13. {snail_lang-0.7.0 → snail_lang-0.7.2}/crates/snail-python/src/lower/py_ast.rs +28 -0
  14. {snail_lang-0.7.0 → snail_lang-0.7.2}/pyproject.toml +3 -2
  15. {snail_lang-0.7.0 → snail_lang-0.7.2}/python/snail/__init__.py +2 -0
  16. {snail_lang-0.7.0 → snail_lang-0.7.2}/python/snail/cli.py +53 -5
  17. {snail_lang-0.7.0 → snail_lang-0.7.2}/python/snail/runtime/__init__.py +13 -1
  18. snail_lang-0.7.2/python/snail/runtime/env.py +26 -0
  19. {snail_lang-0.7.0 → snail_lang-0.7.2}/LICENSE +0 -0
  20. {snail_lang-0.7.0 → snail_lang-0.7.2}/crates/snail-ast/README.md +0 -0
  21. {snail_lang-0.7.0 → snail_lang-0.7.2}/crates/snail-ast/src/ast.rs +0 -0
  22. {snail_lang-0.7.0 → snail_lang-0.7.2}/crates/snail-ast/src/awk.rs +0 -0
  23. {snail_lang-0.7.0 → snail_lang-0.7.2}/crates/snail-ast/src/lib.rs +0 -0
  24. {snail_lang-0.7.0 → snail_lang-0.7.2}/crates/snail-error/README.md +0 -0
  25. {snail_lang-0.7.0 → snail_lang-0.7.2}/crates/snail-error/src/lib.rs +0 -0
  26. {snail_lang-0.7.0 → snail_lang-0.7.2}/crates/snail-parser/README.md +0 -0
  27. {snail_lang-0.7.0 → snail_lang-0.7.2}/crates/snail-parser/src/awk.rs +0 -0
  28. {snail_lang-0.7.0 → snail_lang-0.7.2}/crates/snail-parser/src/expr.rs +0 -0
  29. {snail_lang-0.7.0 → snail_lang-0.7.2}/crates/snail-parser/src/lib.rs +0 -0
  30. {snail_lang-0.7.0 → snail_lang-0.7.2}/crates/snail-parser/src/literal.rs +0 -0
  31. {snail_lang-0.7.0 → snail_lang-0.7.2}/crates/snail-parser/src/stmt.rs +0 -0
  32. {snail_lang-0.7.0 → snail_lang-0.7.2}/crates/snail-parser/src/string.rs +0 -0
  33. {snail_lang-0.7.0 → snail_lang-0.7.2}/crates/snail-parser/src/util.rs +0 -0
  34. {snail_lang-0.7.0 → snail_lang-0.7.2}/crates/snail-parser/tests/common.rs +0 -0
  35. {snail_lang-0.7.0 → snail_lang-0.7.2}/crates/snail-parser/tests/errors.rs +0 -0
  36. {snail_lang-0.7.0 → snail_lang-0.7.2}/crates/snail-parser/tests/parser.rs +0 -0
  37. {snail_lang-0.7.0 → snail_lang-0.7.2}/crates/snail-parser/tests/statements.rs +0 -0
  38. {snail_lang-0.7.0 → snail_lang-0.7.2}/crates/snail-parser/tests/syntax_strings.rs +0 -0
  39. {snail_lang-0.7.0 → snail_lang-0.7.2}/crates/snail-python/build.rs +0 -0
  40. {snail_lang-0.7.0 → snail_lang-0.7.2}/crates/snail-python/src/compiler.rs +0 -0
  41. {snail_lang-0.7.0 → snail_lang-0.7.2}/crates/snail-python/src/lib.rs +0 -0
  42. {snail_lang-0.7.0 → snail_lang-0.7.2}/crates/snail-python/src/linecache.rs +0 -0
  43. {snail_lang-0.7.0 → snail_lang-0.7.2}/crates/snail-python/src/lower/awk.rs +0 -0
  44. {snail_lang-0.7.0 → snail_lang-0.7.2}/crates/snail-python/src/lower/desugar.rs +0 -0
  45. {snail_lang-0.7.0 → snail_lang-0.7.2}/crates/snail-python/src/lower/helpers.rs +0 -0
  46. {snail_lang-0.7.0 → snail_lang-0.7.2}/crates/snail-python/src/lower/map.rs +0 -0
  47. {snail_lang-0.7.0 → snail_lang-0.7.2}/crates/snail-python/src/lower/mod.rs +0 -0
  48. {snail_lang-0.7.0 → snail_lang-0.7.2}/crates/snail-python/src/lower/operators.rs +0 -0
  49. {snail_lang-0.7.0 → snail_lang-0.7.2}/crates/snail-python/src/lower/program.rs +0 -0
  50. {snail_lang-0.7.0 → snail_lang-0.7.2}/crates/snail-python/src/lower/stmt.rs +0 -0
  51. {snail_lang-0.7.0 → snail_lang-0.7.2}/crates/snail-python/src/lower/validate.rs +0 -0
  52. {snail_lang-0.7.0 → snail_lang-0.7.2}/crates/snail-python/src/profiling.rs +0 -0
  53. {snail_lang-0.7.0 → snail_lang-0.7.2}/python/snail/runtime/augmented.py +0 -0
  54. {snail_lang-0.7.0 → snail_lang-0.7.2}/python/snail/runtime/compact_try.py +0 -0
  55. {snail_lang-0.7.0 → snail_lang-0.7.2}/python/snail/runtime/lazy_file.py +0 -0
  56. {snail_lang-0.7.0 → snail_lang-0.7.2}/python/snail/runtime/lazy_text.py +0 -0
  57. {snail_lang-0.7.0 → snail_lang-0.7.2}/python/snail/runtime/regex.py +0 -0
  58. {snail_lang-0.7.0 → snail_lang-0.7.2}/python/snail/runtime/structured_accessor.py +0 -0
  59. {snail_lang-0.7.0 → snail_lang-0.7.2}/python/snail/runtime/subprocess.py +0 -0
@@ -312,25 +312,25 @@ checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
312
312
 
313
313
  [[package]]
314
314
  name = "snail-ast"
315
- version = "0.7.0"
315
+ version = "0.7.2"
316
316
 
317
317
  [[package]]
318
318
  name = "snail-error"
319
- version = "0.7.0"
319
+ version = "0.7.2"
320
320
  dependencies = [
321
321
  "snail-ast",
322
322
  ]
323
323
 
324
324
  [[package]]
325
325
  name = "snail-lower"
326
- version = "0.7.0"
326
+ version = "0.7.2"
327
327
  dependencies = [
328
328
  "snail-python",
329
329
  ]
330
330
 
331
331
  [[package]]
332
332
  name = "snail-parser"
333
- version = "0.7.0"
333
+ version = "0.7.2"
334
334
  dependencies = [
335
335
  "pest",
336
336
  "pest_derive",
@@ -340,7 +340,7 @@ dependencies = [
340
340
 
341
341
  [[package]]
342
342
  name = "snail-python"
343
- version = "0.7.0"
343
+ version = "0.7.2"
344
344
  dependencies = [
345
345
  "pyo3",
346
346
  "snail-ast",
@@ -4,3 +4,9 @@ members = ["crates/snail-python"]
4
4
 
5
5
  [workspace.package]
6
6
  edition = "2024"
7
+
8
+ [workspace.lints.rust]
9
+ warnings = "deny"
10
+
11
+ [workspace.lints.clippy]
12
+ all = "deny"
@@ -1,13 +1,14 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: snail-lang
3
- Version: 0.7.0
3
+ Version: 0.7.2
4
+ Requires-Dist: astunparse>=1.6.3 ; python_full_version < '3.9'
4
5
  Requires-Dist: jmespath>=1.0.1
5
6
  Requires-Dist: maturin>=1.5 ; extra == 'dev'
6
7
  Requires-Dist: pytest ; extra == 'dev'
7
8
  Provides-Extra: dev
8
9
  License-File: LICENSE
9
10
  Summary: Snail programming language interpreter
10
- Requires-Python: >=3.10
11
+ Requires-Python: >=3.8
11
12
  Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
12
13
 
13
14
  <p align="center">
@@ -26,10 +27,10 @@ way into something good, but its certainly not there yet.
26
27
 
27
28
  ## Installing Snail
28
29
 
29
- Install [uv](https://docs.astral.sh/uv/getting-started/installation/) and then run:
30
-
31
30
  ```bash
32
- uv tool install -p 3.12 snail-lang
31
+ pip install snail-lang
32
+ -or-
33
+ uv tool install snail-lang
33
34
  ```
34
35
 
35
36
  That installs the `snail` CLI for your user; try it with `snail "print('hello')"` once the install completes.
@@ -75,6 +76,7 @@ END { print("done") }
75
76
  | `$p` | Current file path |
76
77
  | `$m` | Last regex match object |
77
78
 
79
+
78
80
  Begin/end blocks can live in the source file (`BEGIN { ... }` / `END { ... }`) or be supplied
79
81
  via CLI flags (`-b`/`--begin`, `-e`/`--end`) for setup and teardown. CLI BEGIN blocks run
80
82
  before in-file BEGIN blocks; CLI END blocks run after in-file END blocks.
@@ -112,6 +114,13 @@ BEGIN/END blocks are regular Snail blocks, so awk/map-only `$` variables are not
112
114
  snail --map --begin "print('start')" --end "print('done')" "print($src)" *.txt
113
115
  ```
114
116
 
117
+ ### Built-in Variables (All Modes)
118
+
119
+ | Variable | Description |
120
+ |----------|-------------|
121
+ | `$e` | Exception object in `expr:fallback?` |
122
+ | `$env` | Environment map (wrapper around `os.environ`) |
123
+
115
124
  ### Compact Error Handling
116
125
 
117
126
  The `?` operator makes error handling terse yet expressive:
@@ -243,7 +252,7 @@ names = js('{{"name": "Ada"}}\n{{"name": "Lin"}}') | $[[*].name]
243
252
  ### Full Python Interoperability
244
253
 
245
254
  Snail compiles to Python AST—import any Python module, use any library, in any
246
- environment. Assuming that you are using Python 3.10 or later.
255
+ environment. Assuming that you are using Python 3.8 or later.
247
256
 
248
257
  ## 🚀 Quick Start
249
258
 
@@ -262,6 +271,9 @@ snail 'if let [_, user, domain] = "user@example.com" in /^[\w.]+@([\w.]+)$/ { pr
262
271
 
263
272
  # Awk mode: print line numbers for matches
264
273
  rg -n "TODO" README.md | snail --awk '/TODO/ { print("{$n}: {$0}") }'
274
+
275
+ # Environment variables
276
+ snail 'print($env.PATH)'
265
277
  ```
266
278
 
267
279
  ## 📚 Documentation
@@ -294,7 +306,7 @@ machine snail adds 5 ms of overhead above the regular python3 interpreter.
294
306
 
295
307
  ### Prerequisites
296
308
 
297
- **Python 3.10+** (required at runtime)
309
+ **Python 3.8+** (required at runtime)
298
310
 
299
311
  Snail runs in-process via a Pyo3 extension module, so it uses the active Python environment.
300
312
 
@@ -14,10 +14,10 @@ way into something good, but its certainly not there yet.
14
14
 
15
15
  ## Installing Snail
16
16
 
17
- Install [uv](https://docs.astral.sh/uv/getting-started/installation/) and then run:
18
-
19
17
  ```bash
20
- uv tool install -p 3.12 snail-lang
18
+ pip install snail-lang
19
+ -or-
20
+ uv tool install snail-lang
21
21
  ```
22
22
 
23
23
  That installs the `snail` CLI for your user; try it with `snail "print('hello')"` once the install completes.
@@ -63,6 +63,7 @@ END { print("done") }
63
63
  | `$p` | Current file path |
64
64
  | `$m` | Last regex match object |
65
65
 
66
+
66
67
  Begin/end blocks can live in the source file (`BEGIN { ... }` / `END { ... }`) or be supplied
67
68
  via CLI flags (`-b`/`--begin`, `-e`/`--end`) for setup and teardown. CLI BEGIN blocks run
68
69
  before in-file BEGIN blocks; CLI END blocks run after in-file END blocks.
@@ -100,6 +101,13 @@ BEGIN/END blocks are regular Snail blocks, so awk/map-only `$` variables are not
100
101
  snail --map --begin "print('start')" --end "print('done')" "print($src)" *.txt
101
102
  ```
102
103
 
104
+ ### Built-in Variables (All Modes)
105
+
106
+ | Variable | Description |
107
+ |----------|-------------|
108
+ | `$e` | Exception object in `expr:fallback?` |
109
+ | `$env` | Environment map (wrapper around `os.environ`) |
110
+
103
111
  ### Compact Error Handling
104
112
 
105
113
  The `?` operator makes error handling terse yet expressive:
@@ -231,7 +239,7 @@ names = js('{{"name": "Ada"}}\n{{"name": "Lin"}}') | $[[*].name]
231
239
  ### Full Python Interoperability
232
240
 
233
241
  Snail compiles to Python AST—import any Python module, use any library, in any
234
- environment. Assuming that you are using Python 3.10 or later.
242
+ environment. Assuming that you are using Python 3.8 or later.
235
243
 
236
244
  ## 🚀 Quick Start
237
245
 
@@ -250,6 +258,9 @@ snail 'if let [_, user, domain] = "user@example.com" in /^[\w.]+@([\w.]+)$/ { pr
250
258
 
251
259
  # Awk mode: print line numbers for matches
252
260
  rg -n "TODO" README.md | snail --awk '/TODO/ { print("{$n}: {$0}") }'
261
+
262
+ # Environment variables
263
+ snail 'print($env.PATH)'
253
264
  ```
254
265
 
255
266
  ## 📚 Documentation
@@ -282,7 +293,7 @@ machine snail adds 5 ms of overhead above the regular python3 interpreter.
282
293
 
283
294
  ### Prerequisites
284
295
 
285
- **Python 3.10+** (required at runtime)
296
+ **Python 3.8+** (required at runtime)
286
297
 
287
298
  Snail runs in-process via a Pyo3 extension module, so it uses the active Python environment.
288
299
 
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "snail-ast"
3
- version = "0.7.0"
3
+ version = "0.7.2"
4
4
  edition = "2024"
5
5
  readme = "README.md"
6
6
 
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "snail-error"
3
- version = "0.7.0"
3
+ version = "0.7.2"
4
4
  edition = "2024"
5
5
  readme = "README.md"
6
6
 
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "snail-parser"
3
- version = "0.7.0"
3
+ version = "0.7.2"
4
4
  edition = "2024"
5
5
  readme = "README.md"
6
6
 
@@ -250,9 +250,9 @@ boolean = { "True" | "False" }
250
250
  none = { "None" }
251
251
 
252
252
  // Special variables: exception, AWK fields, map vars, injected vars
253
- exception_var = { "$e" }
253
+ exception_var = { "$e" ~ !ident_continue }
254
254
  field_index_var = @{ "$" ~ ASCII_DIGIT+ }
255
- injected_var = { "$text" | "$src" | "$fn" | "$fd" | "$n" | "$p" | "$m" | "$f" }
255
+ injected_var = { "$text" | "$src" | "$env" | "$fn" | "$fd" | "$n" | "$p" | "$m" | "$f" }
256
256
 
257
257
  // Number, string, and regex literals
258
258
  number = @{ ASCII_DIGIT+ ~ ("." ~ ASCII_DIGIT+)? }
@@ -293,6 +293,15 @@ fn parses_structured_accessor_with_pipeline() {
293
293
  }
294
294
  }
295
295
 
296
+ #[test]
297
+ fn parses_env_var() {
298
+ let program = parse_ok("value = $env");
299
+ assert_eq!(program.stmts.len(), 1);
300
+
301
+ let (_, value) = expect_assign(&program.stmts[0]);
302
+ expect_name(value, "$env");
303
+ }
304
+
296
305
  #[test]
297
306
  fn parses_empty_structured_accessor() {
298
307
  let program = parse_ok("result = $[]");
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "snail-python"
3
- version = "0.7.0"
3
+ version = "0.7.2"
4
4
  edition.workspace = true
5
5
  build = "build.rs"
6
6
 
@@ -9,7 +9,7 @@ name = "snail_python"
9
9
  crate-type = ["cdylib", "rlib"]
10
10
 
11
11
  [dependencies]
12
- pyo3 = { version = "0.21", features = ["extension-module", "abi3-py310"] }
12
+ pyo3 = { version = "0.21", features = ["extension-module", "abi3-py38"] }
13
13
  snail-ast = { path = "../snail-ast" }
14
14
  snail-error = { path = "../snail-error" }
15
15
  snail-parser = { path = "../snail-parser" }
@@ -37,9 +37,11 @@ pub(crate) const SNAIL_AWK_MATCH_PYVAR: &str = "__snail_match";
37
37
  pub(crate) const SNAIL_MAP_SRC: &str = "$src";
38
38
  pub(crate) const SNAIL_MAP_FD: &str = "$fd";
39
39
  pub(crate) const SNAIL_MAP_TEXT: &str = "$text";
40
+ pub(crate) const SNAIL_ENV: &str = "$env";
40
41
  pub(crate) const SNAIL_MAP_SRC_PYVAR: &str = "__snail_src";
41
42
  pub(crate) const SNAIL_MAP_FD_PYVAR: &str = "__snail_fd";
42
43
  pub(crate) const SNAIL_MAP_TEXT_PYVAR: &str = "__snail_text";
44
+ pub(crate) const SNAIL_ENV_PYVAR: &str = "__snail_env";
43
45
  pub const SNAIL_LAZY_TEXT_CLASS: &str = "__SnailLazyText";
44
46
  pub const SNAIL_LAZY_FILE_CLASS: &str = "__SnailLazyFile";
45
47
 
@@ -55,6 +57,7 @@ pub(crate) fn injected_py_name(name: &str) -> Option<&'static str> {
55
57
  SNAIL_MAP_SRC => Some(SNAIL_MAP_SRC_PYVAR),
56
58
  SNAIL_MAP_FD => Some(SNAIL_MAP_FD_PYVAR),
57
59
  SNAIL_MAP_TEXT => Some(SNAIL_MAP_TEXT_PYVAR),
60
+ SNAIL_ENV => Some(SNAIL_ENV_PYVAR),
58
61
  _ => None,
59
62
  }
60
63
  }
@@ -36,9 +36,7 @@ pub(crate) fn lower_assign_target(
36
36
  AssignTarget::Index { value, index, span } => {
37
37
  let value_expr = lower_expr_with_exception(builder, value, None)?;
38
38
  let index_expr = lower_expr_with_exception(builder, index, None)?;
39
- builder
40
- .call_node("Subscript", vec![value_expr, index_expr, store_ctx], span)
41
- .map_err(py_err_to_lower)
39
+ subscript_expr(builder, value_expr, index_expr, store_ctx, span)
42
40
  }
43
41
  AssignTarget::Starred { target, span } => {
44
42
  let value = lower_assign_target(builder, target)?;
@@ -101,9 +99,7 @@ pub(crate) fn lower_delete_target(
101
99
  AssignTarget::Index { value, index, span } => {
102
100
  let value_expr = lower_expr_with_exception(builder, value, None)?;
103
101
  let index_expr = lower_expr_with_exception(builder, index, None)?;
104
- builder
105
- .call_node("Subscript", vec![value_expr, index_expr, del_ctx], span)
106
- .map_err(py_err_to_lower)
102
+ subscript_expr(builder, value_expr, index_expr, del_ctx, span)
107
103
  }
108
104
  AssignTarget::Starred { .. } => Err(LowerError::new(
109
105
  "starred targets are not valid in del statements",
@@ -143,6 +139,19 @@ pub(crate) fn lower_delete_target(
143
139
  }
144
140
  }
145
141
 
142
+ fn subscript_expr(
143
+ builder: &AstBuilder<'_>,
144
+ value: PyObject,
145
+ index: PyObject,
146
+ ctx: PyObject,
147
+ span: &SourceSpan,
148
+ ) -> Result<PyObject, LowerError> {
149
+ let slice = builder.wrap_index(index, span).map_err(py_err_to_lower)?;
150
+ builder
151
+ .call_node("Subscript", vec![value, slice, ctx], span)
152
+ .map_err(py_err_to_lower)
153
+ }
154
+
146
155
  pub(crate) fn lower_regex_match(
147
156
  builder: &AstBuilder<'_>,
148
157
  value: &Expr,
@@ -450,17 +459,13 @@ pub(crate) fn lower_expr_with_exception(
450
459
  builder.load_ctx().map_err(py_err_to_lower)?,
451
460
  )?;
452
461
  let index_expr = number_expr(builder, &python_index.to_string(), span)?;
453
- builder
454
- .call_node(
455
- "Subscript",
456
- vec![
457
- value,
458
- index_expr,
459
- builder.load_ctx().map_err(py_err_to_lower)?,
460
- ],
461
- span,
462
- )
463
- .map_err(py_err_to_lower)
462
+ subscript_expr(
463
+ builder,
464
+ value,
465
+ index_expr,
466
+ builder.load_ctx().map_err(py_err_to_lower)?,
467
+ span,
468
+ )
464
469
  }
465
470
  Expr::Number { value, span } => number_expr(builder, value, span),
466
471
  Expr::String {
@@ -805,17 +810,13 @@ pub(crate) fn lower_expr_with_exception(
805
810
  span,
806
811
  )
807
812
  .map_err(py_err_to_lower)?;
808
- builder
809
- .call_node(
810
- "Subscript",
811
- vec![
812
- tuple_expr,
813
- index_expr,
814
- builder.load_ctx().map_err(py_err_to_lower)?,
815
- ],
816
- span,
817
- )
818
- .map_err(py_err_to_lower)
813
+ subscript_expr(
814
+ builder,
815
+ tuple_expr,
816
+ index_expr,
817
+ builder.load_ctx().map_err(py_err_to_lower)?,
818
+ span,
819
+ )
819
820
  }
820
821
  Expr::Regex { pattern, span } => {
821
822
  let func = name_expr(
@@ -908,17 +909,13 @@ pub(crate) fn lower_expr_with_exception(
908
909
  .parse::<i32>()
909
910
  .map_err(|_| LowerError::new(format!("Invalid match group index: .{attr}")))?;
910
911
  let index_expr = number_expr(builder, &index.to_string(), span)?;
911
- return builder
912
- .call_node(
913
- "Subscript",
914
- vec![
915
- value,
916
- index_expr,
917
- builder.load_ctx().map_err(py_err_to_lower)?,
918
- ],
919
- span,
920
- )
921
- .map_err(py_err_to_lower);
912
+ return subscript_expr(
913
+ builder,
914
+ value,
915
+ index_expr,
916
+ builder.load_ctx().map_err(py_err_to_lower)?,
917
+ span,
918
+ );
922
919
  }
923
920
  builder
924
921
  .call_node(
@@ -935,13 +932,13 @@ pub(crate) fn lower_expr_with_exception(
935
932
  Expr::Index { value, index, span } => {
936
933
  let value = lower_expr_with_exception(builder, value, exception_name)?;
937
934
  let index = lower_expr_with_exception(builder, index, exception_name)?;
938
- builder
939
- .call_node(
940
- "Subscript",
941
- vec![value, index, builder.load_ctx().map_err(py_err_to_lower)?],
942
- span,
943
- )
944
- .map_err(py_err_to_lower)
935
+ subscript_expr(
936
+ builder,
937
+ value,
938
+ index,
939
+ builder.load_ctx().map_err(py_err_to_lower)?,
940
+ span,
941
+ )
945
942
  }
946
943
  Expr::Paren { expr, .. } => lower_expr_with_exception(builder, expr, exception_name),
947
944
  Expr::List { elements, span } => {
@@ -1344,17 +1341,13 @@ fn lower_postfix_name_incr(
1344
1341
  span,
1345
1342
  )
1346
1343
  .map_err(py_err_to_lower)?;
1347
- builder
1348
- .call_node(
1349
- "Subscript",
1350
- vec![
1351
- tuple_expr,
1352
- index_expr,
1353
- builder.load_ctx().map_err(py_err_to_lower)?,
1354
- ],
1355
- span,
1356
- )
1357
- .map_err(py_err_to_lower)
1344
+ subscript_expr(
1345
+ builder,
1346
+ tuple_expr,
1347
+ index_expr,
1348
+ builder.load_ctx().map_err(py_err_to_lower)?,
1349
+ span,
1350
+ )
1358
1351
  }
1359
1352
 
1360
1353
  fn incr_delta(op: IncrOp) -> &'static str {
@@ -1506,17 +1499,13 @@ fn lower_lambda_body_expr(
1506
1499
  span,
1507
1500
  )
1508
1501
  .map_err(py_err_to_lower)?;
1509
- builder
1510
- .call_node(
1511
- "Subscript",
1512
- vec![
1513
- tuple_expr,
1514
- index_expr,
1515
- builder.load_ctx().map_err(py_err_to_lower)?,
1516
- ],
1517
- span,
1518
- )
1519
- .map_err(py_err_to_lower)
1502
+ subscript_expr(
1503
+ builder,
1504
+ tuple_expr,
1505
+ index_expr,
1506
+ builder.load_ctx().map_err(py_err_to_lower)?,
1507
+ span,
1508
+ )
1520
1509
  }
1521
1510
 
1522
1511
  fn lower_call_arguments(
@@ -6,13 +6,19 @@ use snail_error::LowerError;
6
6
  pub struct AstBuilder<'py> {
7
7
  py: Python<'py>,
8
8
  ast: Bound<'py, PyModule>,
9
+ needs_index_wrapper: bool,
9
10
  }
10
11
 
11
12
  impl<'py> AstBuilder<'py> {
12
13
  pub fn new(py: Python<'py>) -> PyResult<Self> {
14
+ let version_info = py.import_bound("sys")?.getattr("version_info")?;
15
+ let major: u8 = version_info.get_item(0)?.extract()?;
16
+ let minor: u8 = version_info.get_item(1)?.extract()?;
17
+ let needs_index_wrapper = major == 3 && minor < 9;
13
18
  Ok(Self {
14
19
  py,
15
20
  ast: py.import_bound("ast")?,
21
+ needs_index_wrapper,
16
22
  })
17
23
  }
18
24
 
@@ -65,6 +71,28 @@ impl<'py> AstBuilder<'py> {
65
71
  let node = self.ast.getattr(name)?.call1(tuple)?;
66
72
  Ok(node.into_py(self.py))
67
73
  }
74
+
75
+ pub fn wrap_index(&self, slice: PyObject, span: &SourceSpan) -> PyResult<PyObject> {
76
+ if self.needs_index_wrapper {
77
+ let slice_obj = slice.bind(self.py);
78
+ if let Ok(slice_type) = self.ast.getattr("Slice")
79
+ && slice_obj.is_instance(&slice_type)?
80
+ {
81
+ return Ok(slice);
82
+ }
83
+ if let Ok(ext_slice_type) = self.ast.getattr("ExtSlice")
84
+ && slice_obj.is_instance(&ext_slice_type)?
85
+ {
86
+ return Ok(slice);
87
+ }
88
+ if let Ok(index_type) = self.ast.getattr("Index") {
89
+ let index = index_type.call1((slice,))?;
90
+ set_location(&index, span)?;
91
+ return Ok(index.into_py(self.py));
92
+ }
93
+ }
94
+ Ok(slice)
95
+ }
68
96
  }
69
97
 
70
98
  pub fn set_location(node: &Bound<'_, PyAny>, span: &SourceSpan) -> PyResult<()> {
@@ -4,12 +4,13 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "snail-lang"
7
- version = "0.7.0"
7
+ version = "0.7.2"
8
8
  description = "Snail programming language interpreter"
9
9
  readme = "README.md"
10
- requires-python = ">=3.10"
10
+ requires-python = ">=3.8"
11
11
  license = { file = "LICENSE" }
12
12
  dependencies = [
13
+ "astunparse>=1.6.3; python_version < '3.9'",
13
14
  "jmespath>=1.0.1",
14
15
  ]
15
16
 
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from ._native import __build_info__, compile, compile_ast, exec, parse, parse_ast
2
4
 
3
5
 
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import os
4
4
  import sys
5
+ from typing import Optional
5
6
 
6
7
  from . import __build_info__, compile_ast, exec
7
8
 
@@ -79,7 +80,11 @@ def _install_trimmed_excepthook() -> None:
79
80
  colorize = _colorize.can_colorize(file=sys.stderr)
80
81
  except Exception:
81
82
  colorize = hasattr(sys.stderr, "isatty") and sys.stderr.isatty()
82
- for line in tb_exc.format(colorize=colorize):
83
+ try:
84
+ formatted = tb_exc.format(colorize=colorize)
85
+ except TypeError:
86
+ formatted = tb_exc.format()
87
+ for line in formatted:
83
88
  sys.stderr.write(line)
84
89
 
85
90
  sys.excepthook = _snail_excepthook
@@ -87,13 +92,14 @@ def _install_trimmed_excepthook() -> None:
87
92
 
88
93
  class _Args:
89
94
  def __init__(self) -> None:
90
- self.file: str | None = None
95
+ self.file: Optional[str] = None
91
96
  self.awk = False
92
97
  self.map = False
93
98
  self.no_print = False
94
99
  self.no_auto_import = False
95
100
  self.debug = False
96
101
  self.debug_snail_ast = False
102
+ self.debug_python_ast = False
97
103
  self.version = False
98
104
  self.help = False
99
105
  self.begin_code: list[str] = []
@@ -127,6 +133,7 @@ def _print_help(file=None) -> None:
127
133
  print(" -I, --no-auto-import disable auto-imports", file=file)
128
134
  print(" --debug parse and compile, then print, do not run", file=file)
129
135
  print(" --debug-snail-ast parse and print Snail AST, do not run", file=file)
136
+ print(" --debug-python-ast parse and print Python AST, do not run", file=file)
130
137
  print(" -v, --version show version and exit", file=file)
131
138
  print(" -h, --help show this help message and exit", file=file)
132
139
 
@@ -229,6 +236,10 @@ def _parse_args(argv: list[str]) -> _Args:
229
236
  args.debug_snail_ast = True
230
237
  idx += 1
231
238
  continue
239
+ if token == "--debug-python-ast":
240
+ args.debug_python_ast = True
241
+ idx += 1
242
+ continue
232
243
  if token == "-f":
233
244
  if idx + 1 >= len(argv):
234
245
  raise ValueError("option -f requires an argument")
@@ -252,7 +263,7 @@ def _parse_args(argv: list[str]) -> _Args:
252
263
  return args
253
264
 
254
265
 
255
- def _format_version(version: str, build_info: dict[str, object] | None) -> str:
266
+ def _format_version(version: str, build_info: Optional[dict[str, object]]) -> str:
256
267
  display_version = version if version.startswith("v") else f"v{version}"
257
268
  if not build_info:
258
269
  return display_version
@@ -277,7 +288,19 @@ def _get_version() -> str:
277
288
  return version
278
289
 
279
290
 
280
- def main(argv: list[str] | None = None) -> int:
291
+ def _format_python_runtime() -> str:
292
+ version = (
293
+ f"{sys.version_info.major}."
294
+ f"{sys.version_info.minor}."
295
+ f"{sys.version_info.micro}"
296
+ )
297
+ executable = sys.executable or "<unknown>"
298
+ if executable != "<unknown>":
299
+ executable = os.path.abspath(executable)
300
+ return f"Python {version} ({executable})"
301
+
302
+
303
+ def main(argv: Optional[list[str]] = None) -> int:
281
304
  if argv is None:
282
305
  _install_trimmed_excepthook()
283
306
  argv = sys.argv[1:]
@@ -294,6 +317,7 @@ def main(argv: list[str] | None = None) -> int:
294
317
  return 0
295
318
  if namespace.version:
296
319
  print(_format_version(_get_version(), __build_info__))
320
+ print(_format_python_runtime())
297
321
  return 0
298
322
 
299
323
  # Validate --awk and --map are mutually exclusive
@@ -343,6 +367,24 @@ def main(argv: list[str] | None = None) -> int:
343
367
  print(snail_ast)
344
368
  return 0
345
369
 
370
+ if namespace.debug_python_ast:
371
+ import ast
372
+
373
+ python_ast = compile_ast(
374
+ source,
375
+ mode=mode,
376
+ auto_print=not namespace.no_print,
377
+ filename=filename,
378
+ begin_code=namespace.begin_code,
379
+ end_code=namespace.end_code,
380
+ )
381
+ try:
382
+ output = ast.dump(python_ast, indent=2)
383
+ except TypeError:
384
+ output = ast.dump(python_ast)
385
+ print(output)
386
+ return 0
387
+
346
388
  if namespace.debug:
347
389
  import ast
348
390
  import builtins
@@ -356,7 +398,13 @@ def main(argv: list[str] | None = None) -> int:
356
398
  end_code=namespace.end_code,
357
399
  )
358
400
  builtins.compile(python_ast, _display_filename(filename), "exec")
359
- print(ast.unparse(python_ast))
401
+ try:
402
+ output = ast.unparse(python_ast)
403
+ except AttributeError:
404
+ import astunparse
405
+
406
+ output = astunparse.unparse(python_ast).rstrip("\n")
407
+ print(output)
360
408
  return 0
361
409
 
362
410
  return exec(
@@ -1,12 +1,13 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import importlib
4
+ from typing import Optional
4
5
 
5
6
  __all__ = ["install_helpers", "AutoImportDict", "AUTO_IMPORT_NAMES"]
6
7
 
7
8
  # Names that can be auto-imported when first referenced.
8
9
  # Maps name -> (module, attribute) where attribute is None for whole-module imports.
9
- AUTO_IMPORT_NAMES: dict[str, tuple[str, str | None]] = {
10
+ AUTO_IMPORT_NAMES: dict[str, tuple[str, Optional[str]]] = {
10
11
  # Whole module imports: import X
11
12
  "sys": ("sys", None),
12
13
  "os": ("os", None),
@@ -47,6 +48,7 @@ _incr_attr = None
47
48
  _incr_index = None
48
49
  _aug_attr = None
49
50
  _aug_index = None
51
+ _env_map = None
50
52
 
51
53
 
52
54
  def _get_compact_try():
@@ -130,6 +132,15 @@ def _get_lazy_file_class():
130
132
  return _lazy_file_class
131
133
 
132
134
 
135
+ def _get_env_map():
136
+ global _env_map
137
+ if _env_map is None:
138
+ from .env import EnvMap
139
+
140
+ _env_map = EnvMap()
141
+ return _env_map
142
+
143
+
133
144
  def _get_incr_attr():
134
145
  global _incr_attr
135
146
  if _incr_attr is None:
@@ -244,6 +255,7 @@ def install_helpers(globals_dict: dict) -> None:
244
255
  globals_dict["__snail_incr_index"] = _lazy_incr_index
245
256
  globals_dict["__snail_aug_attr"] = _lazy_aug_attr
246
257
  globals_dict["__snail_aug_index"] = _lazy_aug_index
258
+ globals_dict["__snail_env"] = _get_env_map()
247
259
  globals_dict["js"] = _lazy_js
248
260
  globals_dict["__SnailLazyText"] = _get_lazy_text_class()
249
261
  globals_dict["__SnailLazyFile"] = _get_lazy_file_class()
@@ -0,0 +1,26 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+
5
+
6
+ class EnvMap:
7
+ __slots__ = ("_env",)
8
+
9
+ def __init__(self, env=None) -> None:
10
+ self._env = os.environ if env is None else env
11
+
12
+ def __fallback__(self) -> str:
13
+ return ""
14
+
15
+ def _lookup(self, key):
16
+ try:
17
+ return self._env[key]
18
+ except KeyError as exc:
19
+ exc.__fallback__ = self.__fallback__
20
+ raise
21
+
22
+ def __getitem__(self, key):
23
+ return self._lookup(key)
24
+
25
+ def __getattr__(self, name):
26
+ return self._lookup(name)
File without changes