snail-lang 0.7.3__tar.gz → 0.7.5__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.3 → snail_lang-0.7.5}/Cargo.lock +5 -5
  2. {snail_lang-0.7.3 → snail_lang-0.7.5}/PKG-INFO +38 -3
  3. {snail_lang-0.7.3 → snail_lang-0.7.5}/README.md +37 -2
  4. {snail_lang-0.7.3 → snail_lang-0.7.5}/crates/snail-ast/Cargo.toml +1 -1
  5. {snail_lang-0.7.3 → snail_lang-0.7.5}/crates/snail-error/Cargo.toml +1 -1
  6. {snail_lang-0.7.3 → snail_lang-0.7.5}/crates/snail-parser/Cargo.toml +1 -1
  7. {snail_lang-0.7.3 → snail_lang-0.7.5}/crates/snail-parser/src/lib.rs +89 -4
  8. {snail_lang-0.7.3 → snail_lang-0.7.5}/crates/snail-parser/src/snail.pest +8 -1
  9. {snail_lang-0.7.3 → snail_lang-0.7.5}/crates/snail-parser/tests/errors.rs +24 -1
  10. {snail_lang-0.7.3 → snail_lang-0.7.5}/crates/snail-python/Cargo.toml +1 -1
  11. {snail_lang-0.7.3 → snail_lang-0.7.5}/crates/snail-python/src/compiler.rs +25 -4
  12. {snail_lang-0.7.3 → snail_lang-0.7.5}/crates/snail-python/src/lib.rs +62 -13
  13. {snail_lang-0.7.3 → snail_lang-0.7.5}/crates/snail-python/src/lower/awk.rs +29 -16
  14. {snail_lang-0.7.3 → snail_lang-0.7.5}/crates/snail-python/src/lower/constants.rs +3 -0
  15. {snail_lang-0.7.3 → snail_lang-0.7.5}/crates/snail-python/src/lower/mod.rs +1 -1
  16. {snail_lang-0.7.3 → snail_lang-0.7.5}/crates/snail-python/src/lower/program.rs +42 -4
  17. {snail_lang-0.7.3 → snail_lang-0.7.5}/pyproject.toml +1 -1
  18. {snail_lang-0.7.3 → snail_lang-0.7.5}/python/snail/cli.py +35 -23
  19. {snail_lang-0.7.3 → snail_lang-0.7.5}/python/snail/runtime/__init__.py +27 -0
  20. snail_lang-0.7.5/python/snail/runtime/structured_accessor.py +166 -0
  21. snail_lang-0.7.3/python/snail/runtime/structured_accessor.py +0 -74
  22. {snail_lang-0.7.3 → snail_lang-0.7.5}/Cargo.toml +0 -0
  23. {snail_lang-0.7.3 → snail_lang-0.7.5}/LICENSE +0 -0
  24. {snail_lang-0.7.3 → snail_lang-0.7.5}/crates/snail-ast/README.md +0 -0
  25. {snail_lang-0.7.3 → snail_lang-0.7.5}/crates/snail-ast/src/ast.rs +0 -0
  26. {snail_lang-0.7.3 → snail_lang-0.7.5}/crates/snail-ast/src/awk.rs +0 -0
  27. {snail_lang-0.7.3 → snail_lang-0.7.5}/crates/snail-ast/src/lib.rs +0 -0
  28. {snail_lang-0.7.3 → snail_lang-0.7.5}/crates/snail-error/README.md +0 -0
  29. {snail_lang-0.7.3 → snail_lang-0.7.5}/crates/snail-error/src/lib.rs +0 -0
  30. {snail_lang-0.7.3 → snail_lang-0.7.5}/crates/snail-parser/README.md +0 -0
  31. {snail_lang-0.7.3 → snail_lang-0.7.5}/crates/snail-parser/src/awk.rs +0 -0
  32. {snail_lang-0.7.3 → snail_lang-0.7.5}/crates/snail-parser/src/expr.rs +0 -0
  33. {snail_lang-0.7.3 → snail_lang-0.7.5}/crates/snail-parser/src/literal.rs +0 -0
  34. {snail_lang-0.7.3 → snail_lang-0.7.5}/crates/snail-parser/src/stmt.rs +0 -0
  35. {snail_lang-0.7.3 → snail_lang-0.7.5}/crates/snail-parser/src/string.rs +0 -0
  36. {snail_lang-0.7.3 → snail_lang-0.7.5}/crates/snail-parser/src/util.rs +0 -0
  37. {snail_lang-0.7.3 → snail_lang-0.7.5}/crates/snail-parser/tests/common.rs +0 -0
  38. {snail_lang-0.7.3 → snail_lang-0.7.5}/crates/snail-parser/tests/parser.rs +0 -0
  39. {snail_lang-0.7.3 → snail_lang-0.7.5}/crates/snail-parser/tests/statements.rs +0 -0
  40. {snail_lang-0.7.3 → snail_lang-0.7.5}/crates/snail-parser/tests/syntax_expressions.rs +0 -0
  41. {snail_lang-0.7.3 → snail_lang-0.7.5}/crates/snail-parser/tests/syntax_strings.rs +0 -0
  42. {snail_lang-0.7.3 → snail_lang-0.7.5}/crates/snail-python/build.rs +0 -0
  43. {snail_lang-0.7.3 → snail_lang-0.7.5}/crates/snail-python/src/linecache.rs +0 -0
  44. {snail_lang-0.7.3 → snail_lang-0.7.5}/crates/snail-python/src/lower/desugar.rs +0 -0
  45. {snail_lang-0.7.3 → snail_lang-0.7.5}/crates/snail-python/src/lower/expr.rs +0 -0
  46. {snail_lang-0.7.3 → snail_lang-0.7.5}/crates/snail-python/src/lower/helpers.rs +0 -0
  47. {snail_lang-0.7.3 → snail_lang-0.7.5}/crates/snail-python/src/lower/map.rs +0 -0
  48. {snail_lang-0.7.3 → snail_lang-0.7.5}/crates/snail-python/src/lower/operators.rs +0 -0
  49. {snail_lang-0.7.3 → snail_lang-0.7.5}/crates/snail-python/src/lower/py_ast.rs +0 -0
  50. {snail_lang-0.7.3 → snail_lang-0.7.5}/crates/snail-python/src/lower/stmt.rs +0 -0
  51. {snail_lang-0.7.3 → snail_lang-0.7.5}/crates/snail-python/src/lower/validate.rs +0 -0
  52. {snail_lang-0.7.3 → snail_lang-0.7.5}/crates/snail-python/src/profiling.rs +0 -0
  53. {snail_lang-0.7.3 → snail_lang-0.7.5}/python/snail/__init__.py +0 -0
  54. {snail_lang-0.7.3 → snail_lang-0.7.5}/python/snail/runtime/augmented.py +0 -0
  55. {snail_lang-0.7.3 → snail_lang-0.7.5}/python/snail/runtime/compact_try.py +0 -0
  56. {snail_lang-0.7.3 → snail_lang-0.7.5}/python/snail/runtime/env.py +0 -0
  57. {snail_lang-0.7.3 → snail_lang-0.7.5}/python/snail/runtime/lazy_file.py +0 -0
  58. {snail_lang-0.7.3 → snail_lang-0.7.5}/python/snail/runtime/lazy_text.py +0 -0
  59. {snail_lang-0.7.3 → snail_lang-0.7.5}/python/snail/runtime/regex.py +0 -0
  60. {snail_lang-0.7.3 → snail_lang-0.7.5}/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.3"
315
+ version = "0.7.5"
316
316
 
317
317
  [[package]]
318
318
  name = "snail-error"
319
- version = "0.7.3"
319
+ version = "0.7.5"
320
320
  dependencies = [
321
321
  "snail-ast",
322
322
  ]
323
323
 
324
324
  [[package]]
325
325
  name = "snail-lower"
326
- version = "0.7.3"
326
+ version = "0.7.5"
327
327
  dependencies = [
328
328
  "snail-python",
329
329
  ]
330
330
 
331
331
  [[package]]
332
332
  name = "snail-parser"
333
- version = "0.7.3"
333
+ version = "0.7.5"
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.3"
343
+ version = "0.7.5"
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.3
3
+ Version: 0.7.5
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
@@ -287,7 +323,7 @@ Documentation is WIP
287
323
 
288
324
  ## 🔌 Editor Support
289
325
 
290
- Vim/Neovim plugin with syntax highlighting, formatting, and run commands:
326
+ Vim/Neovim plugin with Tree-sitter-based highlighting (Neovim), formatting, and run commands:
291
327
 
292
328
  ```vim
293
329
  Plug 'sudonym1/snail', { 'rtp': 'extras/vim' }
@@ -340,4 +376,3 @@ cd /tmp/snail-pkg
340
376
  makepkg -si
341
377
  ```
342
378
 
343
-
@@ -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
@@ -274,7 +310,7 @@ Documentation is WIP
274
310
 
275
311
  ## 🔌 Editor Support
276
312
 
277
- Vim/Neovim plugin with syntax highlighting, formatting, and run commands:
313
+ Vim/Neovim plugin with Tree-sitter-based highlighting (Neovim), formatting, and run commands:
278
314
 
279
315
  ```vim
280
316
  Plug 'sudonym1/snail', { 'rtp': 'extras/vim' }
@@ -326,4 +362,3 @@ cd /tmp/snail-pkg
326
362
  # Update pkgver and sha256sums as needed, then build and install
327
363
  makepkg -si
328
364
  ```
329
-
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "snail-ast"
3
- version = "0.7.3"
3
+ version = "0.7.5"
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.3"
3
+ version = "0.7.5"
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.3"
3
+ version = "0.7.5"
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.3"
3
+ version = "0.7.5"
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)
@@ -107,6 +112,8 @@ fn prepare_globals<'py>(
107
112
  filename: &str,
108
113
  argv: &[String],
109
114
  auto_import: bool,
115
+ awk_field_separators: Option<String>,
116
+ awk_include_whitespace: Option<bool>,
110
117
  ) -> PyResult<Bound<'py, PyAny>> {
111
118
  let runtime = py.import_bound("snail.runtime")?;
112
119
 
@@ -126,6 +133,16 @@ fn prepare_globals<'py>(
126
133
  sys.setattr("argv", PyList::new_bound(py, argv))?;
127
134
 
128
135
  runtime.call_method1("install_helpers", (&globals,))?;
136
+ let separators = awk_field_separators
137
+ .as_deref()
138
+ .filter(|value| !value.is_empty());
139
+ let separators_value = match separators {
140
+ Some(separators) => separators.into_py(py),
141
+ None => py.None().into_py(py),
142
+ };
143
+ globals.set_item("__snail_awk_field_separators", separators_value)?;
144
+ let include_whitespace = awk_include_whitespace.unwrap_or(separators.is_none());
145
+ globals.set_item("__snail_awk_include_whitespace", include_whitespace)?;
129
146
 
130
147
  Ok(globals)
131
148
  }
@@ -196,7 +213,7 @@ fn compile_ast_py(
196
213
  }
197
214
 
198
215
  #[pyfunction(name = "exec")]
199
- #[pyo3(signature = (source, *, argv = Vec::new(), mode = "snail", auto_print = true, auto_import = true, filename = "<snail>", begin_code = Vec::new(), end_code = Vec::new()))]
216
+ #[pyo3(signature = (source, *, argv = Vec::new(), mode = "snail", auto_print = true, auto_import = true, filename = "<snail>", begin_code = Vec::new(), end_code = Vec::new(), field_separators = None, include_whitespace = None))]
200
217
  #[allow(clippy::too_many_arguments)]
201
218
  fn exec_py(
202
219
  py: Python<'_>,
@@ -208,6 +225,8 @@ fn exec_py(
208
225
  filename: &str,
209
226
  begin_code: Vec<String>,
210
227
  end_code: Vec<String>,
228
+ field_separators: Option<String>,
229
+ include_whitespace: Option<bool>,
211
230
  ) -> PyResult<i32> {
212
231
  let profile = profile_enabled();
213
232
  let total_start = Instant::now();
@@ -236,7 +255,14 @@ fn exec_py(
236
255
  log_profile("py_compile", compile_start.elapsed());
237
256
  }
238
257
  let globals_start = Instant::now();
239
- let globals = prepare_globals(py, strip_display_prefix(filename), &argv, auto_import)?;
258
+ let globals = prepare_globals(
259
+ py,
260
+ strip_display_prefix(filename),
261
+ &argv,
262
+ auto_import,
263
+ field_separators,
264
+ include_whitespace,
265
+ )?;
240
266
  if profile {
241
267
  log_profile("prepare_globals", globals_start.elapsed());
242
268
  }
@@ -277,6 +303,14 @@ struct MapAst {
277
303
  end_blocks: Vec<Vec<snail_ast::Stmt>>,
278
304
  }
279
305
 
306
+ #[allow(dead_code)]
307
+ #[derive(Debug)]
308
+ struct SnailAst {
309
+ program: snail_ast::Program,
310
+ begin_blocks: Vec<Vec<snail_ast::Stmt>>,
311
+ end_blocks: Vec<Vec<snail_ast::Stmt>>,
312
+ }
313
+
280
314
  #[pyfunction(name = "parse_ast")]
281
315
  #[pyo3(signature = (source, *, mode = "snail", filename = "<snail>", begin_code = Vec::new(), end_code = Vec::new()))]
282
316
  fn parse_ast_py(
@@ -289,9 +323,24 @@ fn parse_ast_py(
289
323
  let err_to_syntax =
290
324
  |err: ParseError| PySyntaxError::new_err(format_snail_error(&err.into(), filename));
291
325
  match parse_mode(mode)? {
292
- CompileMode::Snail => parse_program(source)
293
- .map(|program| format!("{:#?}", program))
294
- .map_err(err_to_syntax),
326
+ CompileMode::Snail => {
327
+ let (program, begin_blocks, end_blocks) =
328
+ parse_program_with_begin_end(source).map_err(err_to_syntax)?;
329
+ let (begin_blocks, end_blocks) =
330
+ merge_map_cli_blocks(&begin_code, &end_code, begin_blocks, end_blocks)
331
+ .map_err(err_to_syntax)?;
332
+
333
+ if begin_blocks.is_empty() && end_blocks.is_empty() {
334
+ return Ok(format!("{:#?}", program));
335
+ }
336
+
337
+ let snail_ast = SnailAst {
338
+ program,
339
+ begin_blocks,
340
+ end_blocks,
341
+ };
342
+ Ok(format!("{:#?}", snail_ast))
343
+ }
295
344
  CompileMode::Awk => {
296
345
  let program = if begin_code.is_empty() && end_code.is_empty() {
297
346
  parse_awk_program(source).map_err(err_to_syntax)?
@@ -328,7 +377,7 @@ fn parse_ast_py(
328
377
  #[pyo3(signature = (source, *, mode = "snail", filename = "<snail>"))]
329
378
  fn parse_py(source: &str, mode: &str, filename: &str) -> PyResult<()> {
330
379
  match parse_mode(mode)? {
331
- CompileMode::Snail => parse_program(source)
380
+ CompileMode::Snail => parse_program_with_begin_end(source)
332
381
  .map(|_| ())
333
382
  .map_err(|err| PySyntaxError::new_err(format_snail_error(&err.into(), filename))),
334
383
  CompileMode::Awk => parse_awk_program(source)
@@ -270,23 +270,36 @@ pub(crate) fn lower_awk_line_loop_with_auto_print(
270
270
  .call_node(
271
271
  "Call",
272
272
  vec![
273
- builder
274
- .call_node(
275
- "Attribute",
276
- vec![
277
- name_expr(
278
- builder,
279
- SNAIL_AWK_LINE_PYVAR,
280
- span,
281
- builder.load_ctx().map_err(py_err_to_lower)?,
282
- )?,
283
- "split".to_string().into_py(builder.py()),
273
+ name_expr(
274
+ builder,
275
+ SNAIL_AWK_SPLIT_HELPER,
276
+ span,
277
+ builder.load_ctx().map_err(py_err_to_lower)?,
278
+ )?,
279
+ PyList::new_bound(
280
+ builder.py(),
281
+ vec![
282
+ name_expr(
283
+ builder,
284
+ SNAIL_AWK_LINE_PYVAR,
285
+ span,
284
286
  builder.load_ctx().map_err(py_err_to_lower)?,
285
- ],
286
- span,
287
- )
288
- .map_err(py_err_to_lower)?,
289
- PyList::empty_bound(builder.py()).into_py(builder.py()),
287
+ )?,
288
+ name_expr(
289
+ builder,
290
+ SNAIL_AWK_FIELD_SEPARATORS_PYVAR,
291
+ span,
292
+ builder.load_ctx().map_err(py_err_to_lower)?,
293
+ )?,
294
+ name_expr(
295
+ builder,
296
+ SNAIL_AWK_INCLUDE_WHITESPACE_PYVAR,
297
+ span,
298
+ builder.load_ctx().map_err(py_err_to_lower)?,
299
+ )?,
300
+ ],
301
+ )
302
+ .into_py(builder.py()),
290
303
  PyList::empty_bound(builder.py()).into_py(builder.py()),
291
304
  ],
292
305
  span,
@@ -13,6 +13,7 @@ pub const SNAIL_INCR_ATTR: &str = "__snail_incr_attr";
13
13
  pub const SNAIL_INCR_INDEX: &str = "__snail_incr_index";
14
14
  pub const SNAIL_AUG_ATTR: &str = "__snail_aug_attr";
15
15
  pub const SNAIL_AUG_INDEX: &str = "__snail_aug_index";
16
+ pub(crate) const SNAIL_AWK_SPLIT_HELPER: &str = "__snail_awk_split";
16
17
  pub(crate) const SNAIL_LET_VALUE: &str = "__snail_let_value";
17
18
  pub(crate) const SNAIL_LET_OK: &str = "__snail_let_ok";
18
19
  pub(crate) const SNAIL_LET_KEEP: &str = "__snail_let_keep";
@@ -28,6 +29,8 @@ pub(crate) const SNAIL_AWK_MATCH: &str = "$m";
28
29
  pub(crate) const SNAIL_AWK_FIELDS: &str = "$f";
29
30
  pub(crate) const SNAIL_AWK_LINE_PYVAR: &str = "__snail_line";
30
31
  pub(crate) const SNAIL_AWK_FIELDS_PYVAR: &str = "__snail_fields";
32
+ pub(crate) const SNAIL_AWK_FIELD_SEPARATORS_PYVAR: &str = "__snail_awk_field_separators";
33
+ pub(crate) const SNAIL_AWK_INCLUDE_WHITESPACE_PYVAR: &str = "__snail_awk_include_whitespace";
31
34
  pub(crate) const SNAIL_AWK_NR_PYVAR: &str = "__snail_nr_user";
32
35
  pub(crate) const SNAIL_AWK_FNR_PYVAR: &str = "__snail_fnr_user";
33
36
  pub(crate) const SNAIL_AWK_PATH_PYVAR: &str = "__snail_path_user";