snail-lang 0.7.2__tar.gz → 0.7.4__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 (60) hide show
  1. {snail_lang-0.7.2 → snail_lang-0.7.4}/Cargo.lock +5 -5
  2. {snail_lang-0.7.2 → snail_lang-0.7.4}/PKG-INFO +50 -1
  3. {snail_lang-0.7.2 → snail_lang-0.7.4}/README.md +49 -0
  4. {snail_lang-0.7.2 → snail_lang-0.7.4}/crates/snail-ast/Cargo.toml +1 -1
  5. {snail_lang-0.7.2 → snail_lang-0.7.4}/crates/snail-error/Cargo.toml +1 -1
  6. {snail_lang-0.7.2 → snail_lang-0.7.4}/crates/snail-parser/Cargo.toml +1 -1
  7. {snail_lang-0.7.2 → snail_lang-0.7.4}/crates/snail-parser/src/lib.rs +89 -4
  8. {snail_lang-0.7.2 → snail_lang-0.7.4}/crates/snail-parser/src/snail.pest +8 -1
  9. {snail_lang-0.7.2 → snail_lang-0.7.4}/crates/snail-parser/tests/errors.rs +24 -1
  10. {snail_lang-0.7.2 → snail_lang-0.7.4}/crates/snail-python/Cargo.toml +1 -1
  11. {snail_lang-0.7.2 → snail_lang-0.7.4}/crates/snail-python/src/compiler.rs +25 -4
  12. {snail_lang-0.7.2 → snail_lang-0.7.4}/crates/snail-python/src/lib.rs +39 -11
  13. {snail_lang-0.7.2 → snail_lang-0.7.4}/crates/snail-python/src/lower/mod.rs +1 -1
  14. {snail_lang-0.7.2 → snail_lang-0.7.4}/crates/snail-python/src/lower/program.rs +42 -4
  15. {snail_lang-0.7.2 → snail_lang-0.7.4}/pyproject.toml +1 -1
  16. {snail_lang-0.7.2 → snail_lang-0.7.4}/python/snail/cli.py +8 -21
  17. snail_lang-0.7.4/python/snail/runtime/structured_accessor.py +166 -0
  18. snail_lang-0.7.2/python/snail/runtime/structured_accessor.py +0 -74
  19. {snail_lang-0.7.2 → snail_lang-0.7.4}/Cargo.toml +0 -0
  20. {snail_lang-0.7.2 → snail_lang-0.7.4}/LICENSE +0 -0
  21. {snail_lang-0.7.2 → snail_lang-0.7.4}/crates/snail-ast/README.md +0 -0
  22. {snail_lang-0.7.2 → snail_lang-0.7.4}/crates/snail-ast/src/ast.rs +0 -0
  23. {snail_lang-0.7.2 → snail_lang-0.7.4}/crates/snail-ast/src/awk.rs +0 -0
  24. {snail_lang-0.7.2 → snail_lang-0.7.4}/crates/snail-ast/src/lib.rs +0 -0
  25. {snail_lang-0.7.2 → snail_lang-0.7.4}/crates/snail-error/README.md +0 -0
  26. {snail_lang-0.7.2 → snail_lang-0.7.4}/crates/snail-error/src/lib.rs +0 -0
  27. {snail_lang-0.7.2 → snail_lang-0.7.4}/crates/snail-parser/README.md +0 -0
  28. {snail_lang-0.7.2 → snail_lang-0.7.4}/crates/snail-parser/src/awk.rs +0 -0
  29. {snail_lang-0.7.2 → snail_lang-0.7.4}/crates/snail-parser/src/expr.rs +0 -0
  30. {snail_lang-0.7.2 → snail_lang-0.7.4}/crates/snail-parser/src/literal.rs +0 -0
  31. {snail_lang-0.7.2 → snail_lang-0.7.4}/crates/snail-parser/src/stmt.rs +0 -0
  32. {snail_lang-0.7.2 → snail_lang-0.7.4}/crates/snail-parser/src/string.rs +0 -0
  33. {snail_lang-0.7.2 → snail_lang-0.7.4}/crates/snail-parser/src/util.rs +0 -0
  34. {snail_lang-0.7.2 → snail_lang-0.7.4}/crates/snail-parser/tests/common.rs +0 -0
  35. {snail_lang-0.7.2 → snail_lang-0.7.4}/crates/snail-parser/tests/parser.rs +0 -0
  36. {snail_lang-0.7.2 → snail_lang-0.7.4}/crates/snail-parser/tests/statements.rs +0 -0
  37. {snail_lang-0.7.2 → snail_lang-0.7.4}/crates/snail-parser/tests/syntax_expressions.rs +0 -0
  38. {snail_lang-0.7.2 → snail_lang-0.7.4}/crates/snail-parser/tests/syntax_strings.rs +0 -0
  39. {snail_lang-0.7.2 → snail_lang-0.7.4}/crates/snail-python/build.rs +0 -0
  40. {snail_lang-0.7.2 → snail_lang-0.7.4}/crates/snail-python/src/linecache.rs +0 -0
  41. {snail_lang-0.7.2 → snail_lang-0.7.4}/crates/snail-python/src/lower/awk.rs +0 -0
  42. {snail_lang-0.7.2 → snail_lang-0.7.4}/crates/snail-python/src/lower/constants.rs +0 -0
  43. {snail_lang-0.7.2 → snail_lang-0.7.4}/crates/snail-python/src/lower/desugar.rs +0 -0
  44. {snail_lang-0.7.2 → snail_lang-0.7.4}/crates/snail-python/src/lower/expr.rs +0 -0
  45. {snail_lang-0.7.2 → snail_lang-0.7.4}/crates/snail-python/src/lower/helpers.rs +0 -0
  46. {snail_lang-0.7.2 → snail_lang-0.7.4}/crates/snail-python/src/lower/map.rs +0 -0
  47. {snail_lang-0.7.2 → snail_lang-0.7.4}/crates/snail-python/src/lower/operators.rs +0 -0
  48. {snail_lang-0.7.2 → snail_lang-0.7.4}/crates/snail-python/src/lower/py_ast.rs +0 -0
  49. {snail_lang-0.7.2 → snail_lang-0.7.4}/crates/snail-python/src/lower/stmt.rs +0 -0
  50. {snail_lang-0.7.2 → snail_lang-0.7.4}/crates/snail-python/src/lower/validate.rs +0 -0
  51. {snail_lang-0.7.2 → snail_lang-0.7.4}/crates/snail-python/src/profiling.rs +0 -0
  52. {snail_lang-0.7.2 → snail_lang-0.7.4}/python/snail/__init__.py +0 -0
  53. {snail_lang-0.7.2 → snail_lang-0.7.4}/python/snail/runtime/__init__.py +0 -0
  54. {snail_lang-0.7.2 → snail_lang-0.7.4}/python/snail/runtime/augmented.py +0 -0
  55. {snail_lang-0.7.2 → snail_lang-0.7.4}/python/snail/runtime/compact_try.py +0 -0
  56. {snail_lang-0.7.2 → snail_lang-0.7.4}/python/snail/runtime/env.py +0 -0
  57. {snail_lang-0.7.2 → snail_lang-0.7.4}/python/snail/runtime/lazy_file.py +0 -0
  58. {snail_lang-0.7.2 → snail_lang-0.7.4}/python/snail/runtime/lazy_text.py +0 -0
  59. {snail_lang-0.7.2 → snail_lang-0.7.4}/python/snail/runtime/regex.py +0 -0
  60. {snail_lang-0.7.2 → snail_lang-0.7.4}/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.2"
315
+ version = "0.7.4"
316
316
 
317
317
  [[package]]
318
318
  name = "snail-error"
319
- version = "0.7.2"
319
+ version = "0.7.4"
320
320
  dependencies = [
321
321
  "snail-ast",
322
322
  ]
323
323
 
324
324
  [[package]]
325
325
  name = "snail-lower"
326
- version = "0.7.2"
326
+ version = "0.7.4"
327
327
  dependencies = [
328
328
  "snail-python",
329
329
  ]
330
330
 
331
331
  [[package]]
332
332
  name = "snail-parser"
333
- version = "0.7.2"
333
+ version = "0.7.4"
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.2"
343
+ version = "0.7.4"
344
344
  dependencies = [
345
345
  "pyo3",
346
346
  "snail-ast",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: snail-lang
3
- Version: 0.7.2
3
+ Version: 0.7.4
4
4
  Requires-Dist: astunparse>=1.6.3 ; python_full_version < '3.9'
5
5
  Requires-Dist: jmespath>=1.0.1
6
6
  Requires-Dist: maturin>=1.5 ; extra == 'dev'
@@ -121,6 +121,35 @@ snail --map --begin "print('start')" --end "print('done')" "print($src)" *.txt
121
121
  | `$e` | Exception object in `expr:fallback?` |
122
122
  | `$env` | Environment map (wrapper around `os.environ`) |
123
123
 
124
+ ### Begin/End Blocks
125
+
126
+ Regular Snail programs can include `BEGIN { ... }` and `END { ... }` blocks for
127
+ setup and teardown. These blocks can also be supplied via CLI flags (`-b`/`--begin`,
128
+ `-e`/`--end`) in all modes. CLI BEGIN blocks run before in-file BEGIN blocks; CLI
129
+ END blocks run after in-file END blocks.
130
+ BEGIN/END blocks are regular Snail blocks, so awk/map-only `$` variables are not
131
+ available inside them.
132
+
133
+ ```snail
134
+ print("running")
135
+ BEGIN { print("start") }
136
+ END { print("done") }
137
+ ```
138
+
139
+ In regular mode, my main use case for this feature is passing unexported
140
+ variables
141
+
142
+ ```bash
143
+ my_bashvar=123
144
+ snail -b x=$my_bashvar 'int(x) + 1'
145
+ ```
146
+
147
+ This is roughly the same as using $env to access an exported variable.
148
+
149
+ ```bash
150
+ my_bashvar=123 snail 'int($env.my_bashvar) + 1'
151
+ ```
152
+
124
153
  ### Compact Error Handling
125
154
 
126
155
  The `?` operator makes error handling terse yet expressive:
@@ -249,6 +278,13 @@ result = js('{{"foo": 12}}') | $[foo]
249
278
  names = js('{{"name": "Ada"}}\n{{"name": "Lin"}}') | $[[*].name]
250
279
  ```
251
280
 
281
+ Snail rewrites JMESPath queries in `$[query]` so that double-quoted segments are
282
+ treated as string literals. This lets you write
283
+ `$[items[?ifname=="eth0"].ifname]` inside a single-quoted shell command. If you
284
+ need JMESPath quoted identifiers (for keys like `"foo-bar"`), escape the quotes
285
+ in the query (for example, `$[\"foo-bar\"]`). JSON literal backticks
286
+ (`` `...` ``) are left unchanged.
287
+
252
288
  ### Full Python Interoperability
253
289
 
254
290
  Snail compiles to Python AST—import any Python module, use any library, in any
@@ -327,3 +363,16 @@ make test
327
363
  make install
328
364
  ```
329
365
 
366
+ ### Arch Linux (PKGBUILD)
367
+
368
+ An Arch package build file is available at `extras/arch/PKGBUILD`.
369
+
370
+ ```bash
371
+ mkdir -p /tmp/snail-pkg
372
+ cp extras/arch/PKGBUILD /tmp/snail-pkg/
373
+ cd /tmp/snail-pkg
374
+
375
+ # Update pkgver and sha256sums as needed, then build and install
376
+ makepkg -si
377
+ ```
378
+
@@ -108,6 +108,35 @@ snail --map --begin "print('start')" --end "print('done')" "print($src)" *.txt
108
108
  | `$e` | Exception object in `expr:fallback?` |
109
109
  | `$env` | Environment map (wrapper around `os.environ`) |
110
110
 
111
+ ### Begin/End Blocks
112
+
113
+ Regular Snail programs can include `BEGIN { ... }` and `END { ... }` blocks for
114
+ setup and teardown. These blocks can also be supplied via CLI flags (`-b`/`--begin`,
115
+ `-e`/`--end`) in all modes. CLI BEGIN blocks run before in-file BEGIN blocks; CLI
116
+ END blocks run after in-file END blocks.
117
+ BEGIN/END blocks are regular Snail blocks, so awk/map-only `$` variables are not
118
+ available inside them.
119
+
120
+ ```snail
121
+ print("running")
122
+ BEGIN { print("start") }
123
+ END { print("done") }
124
+ ```
125
+
126
+ In regular mode, my main use case for this feature is passing unexported
127
+ variables
128
+
129
+ ```bash
130
+ my_bashvar=123
131
+ snail -b x=$my_bashvar 'int(x) + 1'
132
+ ```
133
+
134
+ This is roughly the same as using $env to access an exported variable.
135
+
136
+ ```bash
137
+ my_bashvar=123 snail 'int($env.my_bashvar) + 1'
138
+ ```
139
+
111
140
  ### Compact Error Handling
112
141
 
113
142
  The `?` operator makes error handling terse yet expressive:
@@ -236,6 +265,13 @@ result = js('{{"foo": 12}}') | $[foo]
236
265
  names = js('{{"name": "Ada"}}\n{{"name": "Lin"}}') | $[[*].name]
237
266
  ```
238
267
 
268
+ Snail rewrites JMESPath queries in `$[query]` so that double-quoted segments are
269
+ treated as string literals. This lets you write
270
+ `$[items[?ifname=="eth0"].ifname]` inside a single-quoted shell command. If you
271
+ need JMESPath quoted identifiers (for keys like `"foo-bar"`), escape the quotes
272
+ in the query (for example, `$[\"foo-bar\"]`). JSON literal backticks
273
+ (`` `...` ``) are left unchanged.
274
+
239
275
  ### Full Python Interoperability
240
276
 
241
277
  Snail compiles to Python AST—import any Python module, use any library, in any
@@ -313,3 +349,16 @@ cd snail
313
349
  make test
314
350
  make install
315
351
  ```
352
+
353
+ ### Arch Linux (PKGBUILD)
354
+
355
+ An Arch package build file is available at `extras/arch/PKGBUILD`.
356
+
357
+ ```bash
358
+ mkdir -p /tmp/snail-pkg
359
+ cp extras/arch/PKGBUILD /tmp/snail-pkg/
360
+ cd /tmp/snail-pkg
361
+
362
+ # Update pkgver and sha256sums as needed, then build and install
363
+ makepkg -si
364
+ ```
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "snail-ast"
3
- version = "0.7.2"
3
+ version = "0.7.4"
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.2"
3
+ version = "0.7.4"
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.2"
3
+ version = "0.7.4"
4
4
  edition = "2024"
5
5
  readme = "README.md"
6
6
 
@@ -12,7 +12,7 @@ mod string;
12
12
  mod util;
13
13
 
14
14
  use awk::parse_awk_rule;
15
- use stmt::{parse_block, parse_stmt, parse_stmt_list};
15
+ use stmt::{parse_block, parse_stmt};
16
16
  use util::{error_with_span, full_span, parse_error_from_pest, span_from_offset, span_from_pair};
17
17
 
18
18
  #[derive(Parser)]
@@ -20,8 +20,16 @@ use util::{error_with_span, full_span, parse_error_from_pest, span_from_offset,
20
20
  pub struct SnailParser;
21
21
 
22
22
  pub type MapProgramWithBeginEnd = (Program, Vec<Vec<Stmt>>, Vec<Vec<Stmt>>);
23
+ pub type ProgramWithBeginEnd = (Program, Vec<Vec<Stmt>>, Vec<Vec<Stmt>>);
23
24
 
24
25
  pub fn parse_program(source: &str) -> Result<Program, ParseError> {
26
+ let (program, _, _) = parse_program_with_begin_end(source)?;
27
+ Ok(program)
28
+ }
29
+
30
+ /// Parses a regular Snail program with in-file BEGIN/END blocks.
31
+ /// BEGIN/END blocks are parsed as regular Snail statement blocks (no map/awk vars).
32
+ pub fn parse_program_with_begin_end(source: &str) -> Result<ProgramWithBeginEnd, ParseError> {
25
33
  let mut pairs = SnailParser::parse(Rule::program, source)
26
34
  .map_err(|err| parse_error_from_pest(err, source))?;
27
35
  let pair = pairs
@@ -29,14 +37,51 @@ pub fn parse_program(source: &str) -> Result<Program, ParseError> {
29
37
  .ok_or_else(|| ParseError::new("missing program root"))?;
30
38
  let span = full_span(source);
31
39
  let mut stmts = Vec::new();
40
+ let mut begin_blocks = Vec::new();
41
+ let mut end_blocks = Vec::new();
42
+ let mut entries = Vec::new();
43
+
32
44
  for inner in pair.into_inner() {
33
- if inner.as_rule() == Rule::stmt_list {
34
- stmts = parse_stmt_list(inner, source)?;
45
+ if inner.as_rule() == Rule::program_entry_list {
46
+ for entry in inner.into_inner() {
47
+ if entry.as_rule() != Rule::program_entry {
48
+ continue;
49
+ }
50
+ let entry_span = span_from_pair(&entry, source);
51
+ let mut entry_inner = entry.into_inner();
52
+ let entry_pair = entry_inner.next().ok_or_else(|| {
53
+ error_with_span("missing program entry", entry_span.clone(), source)
54
+ })?;
55
+ match entry_pair.as_rule() {
56
+ Rule::program_begin => {
57
+ let block = parse_begin_end_block(entry_pair, source, "BEGIN")?;
58
+ if !block.is_empty() {
59
+ begin_blocks.push(block);
60
+ }
61
+ entries.push((entry_span, ProgramEntryKind::BeginEnd));
62
+ }
63
+ Rule::program_end => {
64
+ let block = parse_begin_end_block(entry_pair, source, "END")?;
65
+ if !block.is_empty() {
66
+ end_blocks.push(block);
67
+ }
68
+ entries.push((entry_span, ProgramEntryKind::BeginEnd));
69
+ }
70
+ _ => {
71
+ let stmt = parse_stmt(entry_pair, source)?;
72
+ entries.push((entry_span, program_entry_kind_for_stmt(&stmt)));
73
+ stmts.push(stmt);
74
+ }
75
+ }
76
+ }
35
77
  }
36
78
  }
79
+
80
+ validate_program_entry_separators(&entries, source)?;
81
+
37
82
  let program = Program { stmts, span };
38
83
  validate_no_awk_syntax(&program, source)?;
39
- Ok(program)
84
+ Ok((program, begin_blocks, end_blocks))
40
85
  }
41
86
 
42
87
  pub fn parse_awk_program(source: &str) -> Result<AwkProgram, ParseError> {
@@ -210,6 +255,46 @@ fn parse_begin_end_block(
210
255
  Ok(block)
211
256
  }
212
257
 
258
+ #[derive(Clone, Copy)]
259
+ enum ProgramEntryKind {
260
+ BeginEnd,
261
+ Simple,
262
+ Compound,
263
+ }
264
+
265
+ fn program_entry_kind_for_stmt(stmt: &Stmt) -> ProgramEntryKind {
266
+ match stmt {
267
+ Stmt::If { .. }
268
+ | Stmt::While { .. }
269
+ | Stmt::For { .. }
270
+ | Stmt::Def { .. }
271
+ | Stmt::Class { .. }
272
+ | Stmt::Try { .. }
273
+ | Stmt::With { .. } => ProgramEntryKind::Compound,
274
+ _ => ProgramEntryKind::Simple,
275
+ }
276
+ }
277
+
278
+ fn validate_program_entry_separators(
279
+ entries: &[(SourceSpan, ProgramEntryKind)],
280
+ source: &str,
281
+ ) -> Result<(), ParseError> {
282
+ for window in entries.windows(2) {
283
+ let (prev_span, prev_kind) = &window[0];
284
+ let (next_span, _) = &window[1];
285
+ let gap = &source[prev_span.end.offset..next_span.start.offset];
286
+ let has_sep = gap.contains('\n') || gap.contains(';');
287
+ if !has_sep && matches!(prev_kind, ProgramEntryKind::Simple) {
288
+ return Err(error_with_span(
289
+ "expected statement separator",
290
+ span_from_offset(next_span.start.offset, next_span.start.offset, source),
291
+ source,
292
+ ));
293
+ }
294
+ }
295
+ Ok(())
296
+ }
297
+
213
298
  #[derive(Clone, Copy)]
214
299
  enum MapEntryKind {
215
300
  BeginEnd,
@@ -1,8 +1,15 @@
1
1
  // Top-level program entry points
2
- program = { SOI ~ stmt_sep* ~ stmt_list? ~ stmt_sep* ~ EOI }
2
+ program = { SOI ~ stmt_sep* ~ program_entry_list? ~ stmt_sep* ~ EOI }
3
3
  awk_program = { SOI ~ stmt_sep* ~ awk_entry_list? ~ stmt_sep* ~ EOI }
4
4
  map_program = { SOI ~ stmt_sep* ~ map_entry_list? ~ stmt_sep* ~ EOI }
5
5
 
6
+ // Regular mode: program with optional BEGIN/END blocks
7
+ program_entry_list = { program_entry ~ (stmt_sep* ~ program_entry)* ~ stmt_sep* }
8
+ program_entry = { program_begin_end | stmt }
9
+ program_begin_end = _{ program_begin | program_end }
10
+ program_begin = { "BEGIN" ~ block }
11
+ program_end = { "END" ~ block }
12
+
6
13
  // AWK mode: pattern-action rules
7
14
  awk_entry_list = { awk_entry ~ (stmt_sep* ~ awk_entry)* ~ stmt_sep* }
8
15
  awk_entry = _{ awk_begin | awk_end | awk_rule }
@@ -3,7 +3,7 @@ mod common;
3
3
  use common::*;
4
4
  use snail_parser::{
5
5
  parse_awk_program, parse_awk_program_with_begin_end, parse_map_program_with_begin_end,
6
- parse_program,
6
+ parse_program, parse_program_with_begin_end,
7
7
  };
8
8
 
9
9
  #[test]
@@ -203,6 +203,29 @@ fn parser_rejects_invalid_parameter_syntax() {
203
203
  assert!(err.span.is_some());
204
204
  }
205
205
 
206
+ // ========== Regular Mode BEGIN/END Parser Tests ==========
207
+
208
+ #[test]
209
+ fn program_begin_end_parsed_as_blocks() {
210
+ let (program, begin_blocks, end_blocks) =
211
+ parse_program_with_begin_end("BEGIN { print(1) }\nprint(2)\nEND { print(3) }")
212
+ .expect("should parse");
213
+ assert_eq!(program.stmts.len(), 1);
214
+ assert_eq!(begin_blocks.len(), 1);
215
+ assert_eq!(end_blocks.len(), 1);
216
+ }
217
+
218
+ #[test]
219
+ fn program_begin_end_rejects_awk_map_vars() {
220
+ let err =
221
+ parse_program_with_begin_end("BEGIN { print($0) }").expect_err("should reject awk vars");
222
+ assert!(err.to_string().contains("$0"));
223
+
224
+ let err =
225
+ parse_program_with_begin_end("BEGIN { print($src) }").expect_err("should reject map vars");
226
+ assert!(err.to_string().contains("$src"));
227
+ }
228
+
206
229
  // ========== AWK Mode Parser Tests ==========
207
230
 
208
231
  #[test]
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "snail-python"
3
- version = "0.7.2"
3
+ version = "0.7.4"
4
4
  edition.workspace = true
5
5
  build = "build.rs"
6
6
 
@@ -1,13 +1,13 @@
1
1
  use crate::lower::{
2
2
  lower_awk_program_with_auto_print, lower_map_program_with_begin_end,
3
- lower_program_with_auto_print,
3
+ lower_program_with_begin_end,
4
4
  };
5
5
  use pyo3::prelude::*;
6
6
  use snail_ast::{CompileMode, Stmt};
7
7
  use snail_error::{ParseError, SnailError};
8
8
  use snail_parser::{
9
9
  parse_awk_program, parse_awk_program_with_begin_end, parse_map_program_with_begin_end,
10
- parse_program,
10
+ parse_program, parse_program_with_begin_end,
11
11
  };
12
12
 
13
13
  type BlockList = Vec<Vec<Stmt>>;
@@ -21,8 +21,14 @@ pub fn compile_snail_source_with_auto_print(
21
21
  ) -> Result<PyObject, SnailError> {
22
22
  match mode {
23
23
  CompileMode::Snail => {
24
- let program = parse_program(source)?;
25
- let module = lower_program_with_auto_print(py, &program, auto_print_last)?;
24
+ let (program, begin_blocks, end_blocks) = parse_program_with_begin_end(source)?;
25
+ let module = lower_program_with_begin_end(
26
+ py,
27
+ &program,
28
+ &begin_blocks,
29
+ &end_blocks,
30
+ auto_print_last,
31
+ )?;
26
32
  Ok(module)
27
33
  }
28
34
  CompileMode::Awk => {
@@ -44,6 +50,21 @@ pub fn compile_snail_source_with_auto_print(
44
50
  }
45
51
  }
46
52
 
53
+ pub fn compile_snail_source_with_begin_end(
54
+ py: Python<'_>,
55
+ main_source: &str,
56
+ begin_sources: &[&str],
57
+ end_sources: &[&str],
58
+ auto_print_last: bool,
59
+ ) -> Result<PyObject, SnailError> {
60
+ let (program, begin_blocks, end_blocks) = parse_program_with_begin_end(main_source)?;
61
+ let begin_blocks = merge_cli_begin_blocks(begin_sources, begin_blocks)?;
62
+ let end_blocks = merge_cli_end_blocks(end_sources, end_blocks)?;
63
+ let module =
64
+ lower_program_with_begin_end(py, &program, &begin_blocks, &end_blocks, auto_print_last)?;
65
+ Ok(module)
66
+ }
67
+
47
68
  pub fn compile_awk_source_with_begin_end(
48
69
  py: Python<'_>,
49
70
  main_source: &str,
@@ -8,12 +8,15 @@ mod profiling;
8
8
  pub use lower::{
9
9
  lower_awk_program, lower_awk_program_with_auto_print, lower_map_program,
10
10
  lower_map_program_with_auto_print, lower_map_program_with_begin_end, lower_program,
11
- lower_program_with_auto_print,
11
+ lower_program_with_auto_print, lower_program_with_begin_end,
12
12
  };
13
13
  pub use pyo3::prelude::{PyObject, Python};
14
14
 
15
- use compiler::{compile_awk_source_with_begin_end, compile_map_source_with_begin_end};
16
- use compiler::{compile_snail_source_with_auto_print, merge_map_cli_blocks};
15
+ use compiler::{
16
+ compile_awk_source_with_begin_end, compile_map_source_with_begin_end,
17
+ compile_snail_source_with_auto_print, compile_snail_source_with_begin_end,
18
+ merge_map_cli_blocks,
19
+ };
17
20
  use linecache::{display_filename, register_linecache, strip_display_prefix};
18
21
  use profiling::{log_profile, profile_enabled};
19
22
  use pyo3::Bound;
@@ -24,7 +27,7 @@ use snail_ast::CompileMode;
24
27
  use snail_error::{ParseError, format_snail_error};
25
28
  use snail_parser::{
26
29
  parse_awk_program, parse_awk_program_with_begin_end, parse_map_program_with_begin_end,
27
- parse_program,
30
+ parse_program_with_begin_end,
28
31
  };
29
32
  use std::time::Instant;
30
33
 
@@ -66,7 +69,7 @@ fn compile_source(
66
69
  let total_start = Instant::now();
67
70
  let compile_start = Instant::now();
68
71
 
69
- // If mode is awk/map and we have begin/end code, use the specialized function
72
+ // If we have begin/end code, use the specialized function
70
73
  let module = if !begin_code.is_empty() || !end_code.is_empty() {
71
74
  let begin_refs: Vec<&str> = begin_code.iter().map(|s| s.as_str()).collect();
72
75
  let end_refs: Vec<&str> = end_code.iter().map(|s| s.as_str()).collect();
@@ -79,8 +82,10 @@ fn compile_source(
79
82
  compile_map_source_with_begin_end(py, source, &begin_refs, &end_refs, auto_print)
80
83
  .map_err(|err| PySyntaxError::new_err(format_snail_error(&err, filename)))?
81
84
  }
82
- _ => compile_snail_source_with_auto_print(py, source, mode, auto_print)
83
- .map_err(|err| PySyntaxError::new_err(format_snail_error(&err, filename)))?,
85
+ CompileMode::Snail => {
86
+ compile_snail_source_with_begin_end(py, source, &begin_refs, &end_refs, auto_print)
87
+ .map_err(|err| PySyntaxError::new_err(format_snail_error(&err, filename)))?
88
+ }
84
89
  }
85
90
  } else {
86
91
  compile_snail_source_with_auto_print(py, source, mode, auto_print)
@@ -277,6 +282,14 @@ struct MapAst {
277
282
  end_blocks: Vec<Vec<snail_ast::Stmt>>,
278
283
  }
279
284
 
285
+ #[allow(dead_code)]
286
+ #[derive(Debug)]
287
+ struct SnailAst {
288
+ program: snail_ast::Program,
289
+ begin_blocks: Vec<Vec<snail_ast::Stmt>>,
290
+ end_blocks: Vec<Vec<snail_ast::Stmt>>,
291
+ }
292
+
280
293
  #[pyfunction(name = "parse_ast")]
281
294
  #[pyo3(signature = (source, *, mode = "snail", filename = "<snail>", begin_code = Vec::new(), end_code = Vec::new()))]
282
295
  fn parse_ast_py(
@@ -289,9 +302,24 @@ fn parse_ast_py(
289
302
  let err_to_syntax =
290
303
  |err: ParseError| PySyntaxError::new_err(format_snail_error(&err.into(), filename));
291
304
  match parse_mode(mode)? {
292
- CompileMode::Snail => parse_program(source)
293
- .map(|program| format!("{:#?}", program))
294
- .map_err(err_to_syntax),
305
+ CompileMode::Snail => {
306
+ let (program, begin_blocks, end_blocks) =
307
+ parse_program_with_begin_end(source).map_err(err_to_syntax)?;
308
+ let (begin_blocks, end_blocks) =
309
+ merge_map_cli_blocks(&begin_code, &end_code, begin_blocks, end_blocks)
310
+ .map_err(err_to_syntax)?;
311
+
312
+ if begin_blocks.is_empty() && end_blocks.is_empty() {
313
+ return Ok(format!("{:#?}", program));
314
+ }
315
+
316
+ let snail_ast = SnailAst {
317
+ program,
318
+ begin_blocks,
319
+ end_blocks,
320
+ };
321
+ Ok(format!("{:#?}", snail_ast))
322
+ }
295
323
  CompileMode::Awk => {
296
324
  let program = if begin_code.is_empty() && end_code.is_empty() {
297
325
  parse_awk_program(source).map_err(err_to_syntax)?
@@ -328,7 +356,7 @@ fn parse_ast_py(
328
356
  #[pyo3(signature = (source, *, mode = "snail", filename = "<snail>"))]
329
357
  fn parse_py(source: &str, mode: &str, filename: &str) -> PyResult<()> {
330
358
  match parse_mode(mode)? {
331
- CompileMode::Snail => parse_program(source)
359
+ CompileMode::Snail => parse_program_with_begin_end(source)
332
360
  .map(|_| ())
333
361
  .map_err(|err| PySyntaxError::new_err(format_snail_error(&err.into(), filename))),
334
362
  CompileMode::Awk => parse_awk_program(source)
@@ -15,5 +15,5 @@ pub use map::{
15
15
  };
16
16
  pub use program::{
17
17
  lower_awk_program, lower_awk_program_with_auto_print, lower_program,
18
- lower_program_with_auto_print,
18
+ lower_program_with_auto_print, lower_program_with_begin_end,
19
19
  };
@@ -8,7 +8,9 @@ use super::desugar::LambdaHoister;
8
8
  use super::helpers::{assign_name, name_expr, number_expr, string_expr};
9
9
  use super::py_ast::{AstBuilder, py_err_to_lower};
10
10
  use super::stmt::lower_block_with_auto_print;
11
- use super::validate::{validate_yield_usage_awk, validate_yield_usage_program};
11
+ use super::validate::{
12
+ validate_yield_usage_awk, validate_yield_usage_blocks, validate_yield_usage_program,
13
+ };
12
14
 
13
15
  pub fn lower_program(py: Python<'_>, program: &Program) -> Result<PyObject, LowerError> {
14
16
  lower_program_with_auto_print(py, program, false)
@@ -18,14 +20,50 @@ pub fn lower_program_with_auto_print(
18
20
  py: Python<'_>,
19
21
  program: &Program,
20
22
  auto_print_last: bool,
23
+ ) -> Result<PyObject, LowerError> {
24
+ lower_program_with_begin_end(py, program, &[], &[], auto_print_last)
25
+ }
26
+
27
+ pub fn lower_program_with_begin_end(
28
+ py: Python<'_>,
29
+ program: &Program,
30
+ begin_blocks: &[Vec<Stmt>],
31
+ end_blocks: &[Vec<Stmt>],
32
+ auto_print_last: bool,
21
33
  ) -> Result<PyObject, LowerError> {
22
34
  let mut hoister = LambdaHoister::new();
35
+ let begin_blocks: Vec<Vec<Stmt>> = begin_blocks
36
+ .iter()
37
+ .map(|block| hoister.desugar_block(block))
38
+ .collect();
23
39
  let program = hoister.desugar_program(program);
40
+ let end_blocks: Vec<Vec<Stmt>> = end_blocks
41
+ .iter()
42
+ .map(|block| hoister.desugar_block(block))
43
+ .collect();
24
44
  validate_yield_usage_program(&program)?;
45
+ validate_yield_usage_blocks(&begin_blocks)?;
46
+ validate_yield_usage_blocks(&end_blocks)?;
25
47
  let builder = AstBuilder::new(py).map_err(py_err_to_lower)?;
26
- let body =
27
- lower_block_with_auto_print(&builder, &program.stmts, auto_print_last, &program.span)?;
28
- builder.module(body, &program.span).map_err(py_err_to_lower)
48
+ let span = program.span.clone();
49
+ let mut body = Vec::new();
50
+
51
+ for block in begin_blocks {
52
+ let lowered =
53
+ lower_block_with_auto_print(&builder, block.as_slice(), auto_print_last, &span)?;
54
+ body.extend(lowered);
55
+ }
56
+
57
+ let main_body = lower_block_with_auto_print(&builder, &program.stmts, auto_print_last, &span)?;
58
+ body.extend(main_body);
59
+
60
+ for block in end_blocks {
61
+ let lowered =
62
+ lower_block_with_auto_print(&builder, block.as_slice(), auto_print_last, &span)?;
63
+ body.extend(lowered);
64
+ }
65
+
66
+ builder.module(body, &span).map_err(py_err_to_lower)
29
67
  }
30
68
 
31
69
  pub fn lower_awk_program(py: Python<'_>, program: &AwkProgram) -> Result<PyObject, LowerError> {
@@ -4,7 +4,7 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "snail-lang"
7
- version = "0.7.2"
7
+ version = "0.7.4"
8
8
  description = "Snail programming language interpreter"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
@@ -6,10 +6,7 @@ from typing import Optional
6
6
 
7
7
  from . import __build_info__, compile_ast, exec
8
8
 
9
- _USAGE = (
10
- "snail [options] -f <file> [args]...\n"
11
- " snail [options] <code> [args]..."
12
- )
9
+ _USAGE = "snail [options] -f <file> [args]...\n snail [options] <code> [args]..."
13
10
  _DESCRIPTION = "Snail programming language interpreter"
14
11
  _BOOLEAN_FLAGS = frozenset("amPIvh")
15
12
  _VALUE_FLAGS = frozenset("fbe")
@@ -119,11 +116,11 @@ def _print_help(file=None) -> None:
119
116
  print(" -a, --awk awk mode", file=file)
120
117
  print(" -m, --map map mode (process files one at a time)", file=file)
121
118
  print(
122
- " -b, --begin <code> begin block code (awk/map mode, repeatable)",
119
+ " -b, --begin <code> begin block code (repeatable)",
123
120
  file=file,
124
121
  )
125
122
  print(
126
- " -e, --end <code> end block code (awk/map mode, repeatable)",
123
+ " -e, --end <code> end block code (repeatable)",
127
124
  file=file,
128
125
  )
129
126
  print(
@@ -131,7 +128,9 @@ def _print_help(file=None) -> None:
131
128
  file=file,
132
129
  )
133
130
  print(" -I, --no-auto-import disable auto-imports", file=file)
134
- print(" --debug parse and compile, then print, do not run", file=file)
131
+ print(
132
+ " --debug parse and compile, then print, do not run", file=file
133
+ )
135
134
  print(" --debug-snail-ast parse and print Snail AST, do not run", file=file)
136
135
  print(" --debug-python-ast parse and print Python AST, do not run", file=file)
137
136
  print(" -v, --version show version and exit", file=file)
@@ -170,9 +169,7 @@ def _expand_short_options(argv: list[str]) -> list[str]:
170
169
  expanded.append(f"-{flag}")
171
170
  pos += 1
172
171
  continue
173
- if all(
174
- ch in _BOOLEAN_FLAGS or ch in _VALUE_FLAGS for ch in remainder
175
- ):
172
+ if all(ch in _BOOLEAN_FLAGS or ch in _VALUE_FLAGS for ch in remainder):
176
173
  raise ValueError(
177
174
  f"option -{flag} requires an argument and must be last in a "
178
175
  "combined flag group"
@@ -290,9 +287,7 @@ def _get_version() -> str:
290
287
 
291
288
  def _format_python_runtime() -> str:
292
289
  version = (
293
- f"{sys.version_info.major}."
294
- f"{sys.version_info.minor}."
295
- f"{sys.version_info.micro}"
290
+ f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
296
291
  )
297
292
  executable = sys.executable or "<unknown>"
298
293
  if executable != "<unknown>":
@@ -325,14 +320,6 @@ def main(argv: Optional[list[str]] = None) -> int:
325
320
  print("error: --awk and --map cannot be used together", file=sys.stderr)
326
321
  return 2
327
322
 
328
- # Validate -b/--begin and -e/--end only with --awk or --map mode
329
- if (namespace.begin_code or namespace.end_code) and not (namespace.awk or namespace.map):
330
- print(
331
- "error: -b/--begin and -e/--end options require --awk or --map mode",
332
- file=sys.stderr,
333
- )
334
- return 2
335
-
336
323
  mode = "map" if namespace.map else ("awk" if namespace.awk else "snail")
337
324
 
338
325
  if namespace.file:
@@ -0,0 +1,166 @@
1
+ from __future__ import annotations
2
+
3
+ import json as _json
4
+ import os as _os
5
+ import sys as _sys
6
+
7
+
8
+ def _append_transpiled_char(out: list[str], ch: str) -> None:
9
+ if ch == "'":
10
+ out.append("\\")
11
+ out.append("'")
12
+ return
13
+ if ch == "\\":
14
+ out.append("\\")
15
+ out.append("\\")
16
+ return
17
+ out.append(ch)
18
+
19
+
20
+ def _transpile_jmespath_query(query: str) -> str:
21
+ out: list[str] = []
22
+ state = "normal"
23
+ i = 0
24
+ while i < len(query):
25
+ ch = query[i]
26
+ if state == "normal":
27
+ if ch == "\\" and i + 1 < len(query) and query[i + 1] == '"':
28
+ out.append('"')
29
+ i += 2
30
+ continue
31
+ if ch == "'":
32
+ state = "single"
33
+ out.append(ch)
34
+ i += 1
35
+ continue
36
+ if ch == "`":
37
+ state = "backtick"
38
+ out.append(ch)
39
+ i += 1
40
+ continue
41
+ if ch == '"':
42
+ state = "double"
43
+ out.append("'")
44
+ i += 1
45
+ continue
46
+ out.append(ch)
47
+ i += 1
48
+ continue
49
+
50
+ if state == "single":
51
+ out.append(ch)
52
+ if ch == "\\" and i + 1 < len(query):
53
+ out.append(query[i + 1])
54
+ i += 2
55
+ continue
56
+ if ch == "'":
57
+ state = "normal"
58
+ i += 1
59
+ continue
60
+
61
+ if state == "backtick":
62
+ out.append(ch)
63
+ if ch == "\\" and i + 1 < len(query):
64
+ out.append(query[i + 1])
65
+ i += 2
66
+ continue
67
+ if ch == "`":
68
+ state = "normal"
69
+ i += 1
70
+ continue
71
+
72
+ if state == "double":
73
+ if ch == '"':
74
+ out.append("'")
75
+ state = "normal"
76
+ i += 1
77
+ continue
78
+ if ch == "\\" and i + 1 < len(query):
79
+ nxt = query[i + 1]
80
+ if nxt == '"':
81
+ _append_transpiled_char(out, '"')
82
+ i += 2
83
+ continue
84
+ if nxt == "\\":
85
+ _append_transpiled_char(out, "\\")
86
+ i += 2
87
+ continue
88
+ _append_transpiled_char(out, "\\")
89
+ i += 1
90
+ continue
91
+ _append_transpiled_char(out, ch)
92
+ i += 1
93
+
94
+ return "".join(out)
95
+
96
+
97
+ def __snail_jmespath_query(query: str):
98
+ """Create a callable that applies JMESPath query.
99
+
100
+ Used by the $[query] syntax which lowers to __snail_jmespath_query(query).
101
+ """
102
+
103
+ import jmespath as _jmespath
104
+
105
+ transpiled = _transpile_jmespath_query(query)
106
+
107
+ def apply(data):
108
+ return _jmespath.search(transpiled, data)
109
+
110
+ return apply
111
+
112
+
113
+ def _parse_jsonl(content: str):
114
+ lines = [line for line in content.splitlines() if line.strip()]
115
+ if not lines:
116
+ return []
117
+
118
+ items = []
119
+ for line in lines:
120
+ try:
121
+ items.append(_json.loads(line))
122
+ except _json.JSONDecodeError as exc:
123
+ raise _json.JSONDecodeError(
124
+ f"Invalid JSONL line: {exc.msg}",
125
+ line,
126
+ exc.pos,
127
+ ) from exc
128
+ return items
129
+
130
+
131
+ def js(input_data=None):
132
+ """Parse JSON from various input sources.
133
+
134
+ Returns the parsed Python object (dict, list, etc.) directly.
135
+ If called with no arguments, reads from stdin.
136
+ """
137
+ if input_data is None:
138
+ input_data = _sys.stdin
139
+
140
+ if isinstance(input_data, str):
141
+ try:
142
+ return _json.loads(input_data)
143
+ except _json.JSONDecodeError:
144
+ if _os.path.exists(input_data):
145
+ with open(input_data, "r", encoding="utf-8") as handle:
146
+ content = handle.read()
147
+ try:
148
+ return _json.loads(content)
149
+ except _json.JSONDecodeError:
150
+ return _parse_jsonl(content)
151
+ else:
152
+ return _parse_jsonl(input_data)
153
+ elif hasattr(input_data, "read"):
154
+ content = input_data.read()
155
+ if isinstance(content, bytes):
156
+ content = content.decode("utf-8")
157
+ try:
158
+ return _json.loads(content)
159
+ except _json.JSONDecodeError:
160
+ return _parse_jsonl(content)
161
+ elif isinstance(input_data, (dict, list, int, float, bool)) or input_data is None:
162
+ return input_data
163
+ else:
164
+ raise TypeError(
165
+ f"js() input must be JSON-compatible, got {type(input_data).__name__}"
166
+ )
@@ -1,74 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import json as _json
4
- import os as _os
5
- import sys as _sys
6
-
7
- def __snail_jmespath_query(query: str):
8
- """Create a callable that applies JMESPath query.
9
-
10
- Used by the $[query] syntax which lowers to __snail_jmespath_query(query).
11
- """
12
-
13
- import jmespath as _jmespath
14
-
15
- def apply(data):
16
- return _jmespath.search(query, data)
17
-
18
- return apply
19
-
20
-
21
- def _parse_jsonl(content: str):
22
- lines = [line for line in content.splitlines() if line.strip()]
23
- if not lines:
24
- return []
25
-
26
- items = []
27
- for line in lines:
28
- try:
29
- items.append(_json.loads(line))
30
- except _json.JSONDecodeError as exc:
31
- raise _json.JSONDecodeError(
32
- f"Invalid JSONL line: {exc.msg}",
33
- line,
34
- exc.pos,
35
- ) from exc
36
- return items
37
-
38
-
39
- def js(input_data=None):
40
- """Parse JSON from various input sources.
41
-
42
- Returns the parsed Python object (dict, list, etc.) directly.
43
- If called with no arguments, reads from stdin.
44
- """
45
- if input_data is None:
46
- input_data = _sys.stdin
47
-
48
- if isinstance(input_data, str):
49
- try:
50
- return _json.loads(input_data)
51
- except _json.JSONDecodeError:
52
- if _os.path.exists(input_data):
53
- with open(input_data, "r", encoding="utf-8") as handle:
54
- content = handle.read()
55
- try:
56
- return _json.loads(content)
57
- except _json.JSONDecodeError:
58
- return _parse_jsonl(content)
59
- else:
60
- return _parse_jsonl(input_data)
61
- elif hasattr(input_data, "read"):
62
- content = input_data.read()
63
- if isinstance(content, bytes):
64
- content = content.decode("utf-8")
65
- try:
66
- return _json.loads(content)
67
- except _json.JSONDecodeError:
68
- return _parse_jsonl(content)
69
- elif isinstance(input_data, (dict, list, int, float, bool)) or input_data is None:
70
- return input_data
71
- else:
72
- raise TypeError(
73
- f"js() input must be JSON-compatible, got {type(input_data).__name__}"
74
- )
File without changes
File without changes