snail-lang 0.3.7__tar.gz → 0.3.8__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 (63) hide show
  1. {snail_lang-0.3.7 → snail_lang-0.3.8}/Cargo.lock +7 -7
  2. {snail_lang-0.3.7 → snail_lang-0.3.8}/PKG-INFO +1 -1
  3. {snail_lang-0.3.7 → snail_lang-0.3.8}/crates/snail-ast/Cargo.toml +1 -1
  4. {snail_lang-0.3.7 → snail_lang-0.3.8}/crates/snail-ast/src/ast.rs +0 -4
  5. {snail_lang-0.3.7 → snail_lang-0.3.8}/crates/snail-core/Cargo.toml +1 -1
  6. {snail_lang-0.3.7 → snail_lang-0.3.8}/crates/snail-error/Cargo.toml +1 -1
  7. {snail_lang-0.3.7 → snail_lang-0.3.8}/crates/snail-lower/Cargo.toml +1 -1
  8. {snail_lang-0.3.7 → snail_lang-0.3.8}/crates/snail-lower/src/expr.rs +29 -21
  9. {snail_lang-0.3.7 → snail_lang-0.3.8}/crates/snail-parser/Cargo.toml +1 -1
  10. {snail_lang-0.3.7 → snail_lang-0.3.8}/crates/snail-parser/src/expr.rs +2 -4
  11. {snail_lang-0.3.7 → snail_lang-0.3.8}/crates/snail-parser/src/lib.rs +1 -1
  12. {snail_lang-0.3.7 → snail_lang-0.3.8}/crates/snail-parser/src/literal.rs +0 -11
  13. {snail_lang-0.3.7 → snail_lang-0.3.8}/crates/snail-parser/src/snail.pest +7 -4
  14. {snail_lang-0.3.7 → snail_lang-0.3.8}/crates/snail-parser/src/string.rs +0 -1
  15. {snail_lang-0.3.7 → snail_lang-0.3.8}/crates/snail-parser/src/util.rs +0 -1
  16. {snail_lang-0.3.7 → snail_lang-0.3.8}/crates/snail-parser/tests/parser.rs +104 -0
  17. {snail_lang-0.3.7 → snail_lang-0.3.8}/crates/snail-parser/tests/statements.rs +5 -17
  18. {snail_lang-0.3.7 → snail_lang-0.3.8}/crates/snail-parser/tests/syntax_expressions.rs +14 -0
  19. {snail_lang-0.3.7 → snail_lang-0.3.8}/crates/snail-python/Cargo.toml +1 -1
  20. {snail_lang-0.3.7 → snail_lang-0.3.8}/pyproject.toml +1 -1
  21. {snail_lang-0.3.7 → snail_lang-0.3.8}/python/snail/runtime/compact_try.py +1 -1
  22. {snail_lang-0.3.7 → snail_lang-0.3.8}/Cargo.toml +0 -0
  23. {snail_lang-0.3.7 → snail_lang-0.3.8}/LICENSE +0 -0
  24. {snail_lang-0.3.7 → snail_lang-0.3.8}/README.md +0 -0
  25. {snail_lang-0.3.7 → snail_lang-0.3.8}/crates/snail-ast/README.md +0 -0
  26. {snail_lang-0.3.7 → snail_lang-0.3.8}/crates/snail-ast/src/awk.rs +0 -0
  27. {snail_lang-0.3.7 → snail_lang-0.3.8}/crates/snail-ast/src/lib.rs +0 -0
  28. {snail_lang-0.3.7 → snail_lang-0.3.8}/crates/snail-core/README.md +0 -0
  29. {snail_lang-0.3.7 → snail_lang-0.3.8}/crates/snail-core/src/lib.rs +0 -0
  30. {snail_lang-0.3.7 → snail_lang-0.3.8}/crates/snail-error/README.md +0 -0
  31. {snail_lang-0.3.7 → snail_lang-0.3.8}/crates/snail-error/src/lib.rs +0 -0
  32. {snail_lang-0.3.7 → snail_lang-0.3.8}/crates/snail-lower/README.md +0 -0
  33. {snail_lang-0.3.7 → snail_lang-0.3.8}/crates/snail-lower/src/awk.rs +0 -0
  34. {snail_lang-0.3.7 → snail_lang-0.3.8}/crates/snail-lower/src/constants.rs +0 -0
  35. {snail_lang-0.3.7 → snail_lang-0.3.8}/crates/snail-lower/src/helpers.rs +0 -0
  36. {snail_lang-0.3.7 → snail_lang-0.3.8}/crates/snail-lower/src/lib.rs +0 -0
  37. {snail_lang-0.3.7 → snail_lang-0.3.8}/crates/snail-lower/src/operators.rs +0 -0
  38. {snail_lang-0.3.7 → snail_lang-0.3.8}/crates/snail-lower/src/program.rs +0 -0
  39. {snail_lang-0.3.7 → snail_lang-0.3.8}/crates/snail-lower/src/py_ast.rs +0 -0
  40. {snail_lang-0.3.7 → snail_lang-0.3.8}/crates/snail-lower/src/stmt.rs +0 -0
  41. {snail_lang-0.3.7 → snail_lang-0.3.8}/crates/snail-parser/README.md +0 -0
  42. {snail_lang-0.3.7 → snail_lang-0.3.8}/crates/snail-parser/src/awk.rs +0 -0
  43. {snail_lang-0.3.7 → snail_lang-0.3.8}/crates/snail-parser/src/stmt.rs +0 -0
  44. {snail_lang-0.3.7 → snail_lang-0.3.8}/crates/snail-parser/tests/common.rs +0 -0
  45. {snail_lang-0.3.7 → snail_lang-0.3.8}/crates/snail-parser/tests/errors.rs +0 -0
  46. {snail_lang-0.3.7 → snail_lang-0.3.8}/crates/snail-parser/tests/syntax_strings.rs +0 -0
  47. {snail_lang-0.3.7 → snail_lang-0.3.8}/crates/snail-python/src/lib.rs +0 -0
  48. {snail_lang-0.3.7 → snail_lang-0.3.8}/python/snail/__init__.py +0 -0
  49. {snail_lang-0.3.7 → snail_lang-0.3.8}/python/snail/cli.py +0 -0
  50. {snail_lang-0.3.7 → snail_lang-0.3.8}/python/snail/runtime/__init__.py +0 -0
  51. {snail_lang-0.3.7 → snail_lang-0.3.8}/python/snail/runtime/regex.py +0 -0
  52. {snail_lang-0.3.7 → snail_lang-0.3.8}/python/snail/runtime/structured_accessor.py +0 -0
  53. {snail_lang-0.3.7 → snail_lang-0.3.8}/python/snail/runtime/subprocess.py +0 -0
  54. {snail_lang-0.3.7 → snail_lang-0.3.8}/python/snail/vendor/__init__.py +0 -0
  55. {snail_lang-0.3.7 → snail_lang-0.3.8}/python/snail/vendor/jmespath/LICENSE +0 -0
  56. {snail_lang-0.3.7 → snail_lang-0.3.8}/python/snail/vendor/jmespath/__init__.py +0 -0
  57. {snail_lang-0.3.7 → snail_lang-0.3.8}/python/snail/vendor/jmespath/ast.py +0 -0
  58. {snail_lang-0.3.7 → snail_lang-0.3.8}/python/snail/vendor/jmespath/compat.py +0 -0
  59. {snail_lang-0.3.7 → snail_lang-0.3.8}/python/snail/vendor/jmespath/exceptions.py +0 -0
  60. {snail_lang-0.3.7 → snail_lang-0.3.8}/python/snail/vendor/jmespath/functions.py +0 -0
  61. {snail_lang-0.3.7 → snail_lang-0.3.8}/python/snail/vendor/jmespath/lexer.py +0 -0
  62. {snail_lang-0.3.7 → snail_lang-0.3.8}/python/snail/vendor/jmespath/parser.py +0 -0
  63. {snail_lang-0.3.7 → snail_lang-0.3.8}/python/snail/vendor/jmespath/visitor.py +0 -0
@@ -485,11 +485,11 @@ checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
485
485
 
486
486
  [[package]]
487
487
  name = "snail-ast"
488
- version = "0.3.7"
488
+ version = "0.3.8"
489
489
 
490
490
  [[package]]
491
491
  name = "snail-core"
492
- version = "0.3.7"
492
+ version = "0.3.8"
493
493
  dependencies = [
494
494
  "pyo3",
495
495
  "snail-ast",
@@ -500,14 +500,14 @@ dependencies = [
500
500
 
501
501
  [[package]]
502
502
  name = "snail-error"
503
- version = "0.3.7"
503
+ version = "0.3.8"
504
504
  dependencies = [
505
505
  "snail-ast",
506
506
  ]
507
507
 
508
508
  [[package]]
509
509
  name = "snail-lower"
510
- version = "0.3.7"
510
+ version = "0.3.8"
511
511
  dependencies = [
512
512
  "pyo3",
513
513
  "snail-ast",
@@ -516,7 +516,7 @@ dependencies = [
516
516
 
517
517
  [[package]]
518
518
  name = "snail-parser"
519
- version = "0.3.7"
519
+ version = "0.3.8"
520
520
  dependencies = [
521
521
  "pest",
522
522
  "pest_derive",
@@ -526,7 +526,7 @@ dependencies = [
526
526
 
527
527
  [[package]]
528
528
  name = "snail-proptest"
529
- version = "0.3.7"
529
+ version = "0.3.8"
530
530
  dependencies = [
531
531
  "proptest",
532
532
  "pyo3",
@@ -540,7 +540,7 @@ dependencies = [
540
540
 
541
541
  [[package]]
542
542
  name = "snail-python"
543
- version = "0.3.7"
543
+ version = "0.3.8"
544
544
  dependencies = [
545
545
  "pyo3",
546
546
  "snail-core",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: snail-lang
3
- Version: 0.3.7
3
+ Version: 0.3.8
4
4
  Requires-Dist: maturin>=1.5 ; extra == 'dev'
5
5
  Requires-Dist: pytest ; extra == 'dev'
6
6
  Provides-Extra: dev
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "snail-ast"
3
- version = "0.3.7"
3
+ version = "0.3.8"
4
4
  edition = "2024"
5
5
  readme = "README.md"
6
6
 
@@ -265,10 +265,6 @@ pub enum Expr {
265
265
  entries: Vec<(Expr, Expr)>,
266
266
  span: SourceSpan,
267
267
  },
268
- Set {
269
- elements: Vec<Expr>,
270
- span: SourceSpan,
271
- },
272
268
  Slice {
273
269
  start: Option<Box<Expr>>,
274
270
  end: Option<Box<Expr>>,
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "snail-core"
3
- version = "0.3.7"
3
+ version = "0.3.8"
4
4
  edition.workspace = true
5
5
  readme = "README.md"
6
6
 
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "snail-error"
3
- version = "0.3.7"
3
+ version = "0.3.8"
4
4
  edition = "2024"
5
5
  readme = "README.md"
6
6
 
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "snail-lower"
3
- version = "0.3.7"
3
+ version = "0.3.8"
4
4
  edition = "2024"
5
5
  readme = "README.md"
6
6
 
@@ -612,6 +612,34 @@ pub(crate) fn lower_expr_with_exception(
612
612
  }
613
613
  Expr::Attribute { value, attr, span } => {
614
614
  let value = lower_expr_with_exception(builder, value, exception_name)?;
615
+ if attr.chars().all(|ch| ch.is_ascii_digit()) {
616
+ let group_index = attr
617
+ .parse::<i32>()
618
+ .map_err(|_| LowerError::new(format!("Invalid match group index: .{attr}")))?;
619
+ let group_attr = builder
620
+ .call_node(
621
+ "Attribute",
622
+ vec![
623
+ value,
624
+ "group".into_py(builder.py()),
625
+ builder.load_ctx().map_err(py_err_to_lower)?,
626
+ ],
627
+ span,
628
+ )
629
+ .map_err(py_err_to_lower)?;
630
+ let index_expr = number_expr(builder, &group_index.to_string(), span)?;
631
+ return builder
632
+ .call_node(
633
+ "Call",
634
+ vec![
635
+ group_attr,
636
+ PyList::new_bound(builder.py(), vec![index_expr]).into_py(builder.py()),
637
+ PyList::empty_bound(builder.py()).into_py(builder.py()),
638
+ ],
639
+ span,
640
+ )
641
+ .map_err(py_err_to_lower);
642
+ }
615
643
  builder
616
644
  .call_node(
617
645
  "Attribute",
@@ -686,19 +714,6 @@ pub(crate) fn lower_expr_with_exception(
686
714
  )
687
715
  .map_err(py_err_to_lower)
688
716
  }
689
- Expr::Set { elements, span } => {
690
- let mut lowered = Vec::with_capacity(elements.len());
691
- for element in elements {
692
- lowered.push(lower_expr_with_exception(builder, element, exception_name)?);
693
- }
694
- builder
695
- .call_node(
696
- "Set",
697
- vec![PyList::new_bound(builder.py(), lowered).into_py(builder.py())],
698
- span,
699
- )
700
- .map_err(py_err_to_lower)
701
- }
702
717
  Expr::ListComp {
703
718
  element,
704
719
  target,
@@ -1031,7 +1046,7 @@ fn count_placeholders(expr: &Expr, info: &mut PlaceholderInfo) {
1031
1046
  count_placeholders(index, info);
1032
1047
  }
1033
1048
  Expr::Paren { expr, .. } => count_placeholders(expr, info),
1034
- Expr::List { elements, .. } | Expr::Tuple { elements, .. } | Expr::Set { elements, .. } => {
1049
+ Expr::List { elements, .. } | Expr::Tuple { elements, .. } => {
1035
1050
  for expr in elements {
1036
1051
  count_placeholders(expr, info);
1037
1052
  }
@@ -1262,13 +1277,6 @@ fn substitute_placeholder(expr: &Expr, replacement: &Expr) -> Expr {
1262
1277
  .collect(),
1263
1278
  span: span.clone(),
1264
1279
  },
1265
- Expr::Set { elements, span } => Expr::Set {
1266
- elements: elements
1267
- .iter()
1268
- .map(|expr| substitute_placeholder(expr, replacement))
1269
- .collect(),
1270
- span: span.clone(),
1271
- },
1272
1280
  Expr::Slice { start, end, span } => Expr::Slice {
1273
1281
  start: start
1274
1282
  .as_ref()
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "snail-parser"
3
- version = "0.3.7"
3
+ version = "0.3.8"
4
4
  edition = "2024"
5
5
  readme = "README.md"
6
6
 
@@ -5,8 +5,8 @@ use snail_error::ParseError;
5
5
  use crate::Rule;
6
6
  use crate::literal::{
7
7
  parse_dict_comp, parse_dict_literal, parse_list_comp, parse_list_literal, parse_literal,
8
- parse_regex_literal, parse_set_literal, parse_slice, parse_structured_accessor,
9
- parse_subprocess, parse_tuple_literal,
8
+ parse_regex_literal, parse_slice, parse_structured_accessor, parse_subprocess,
9
+ parse_tuple_literal,
10
10
  };
11
11
  use crate::util::{error_with_span, expr_span, merge_span, span_from_pair};
12
12
 
@@ -57,7 +57,6 @@ pub fn parse_expr_pair(pair: Pair<'_, Rule>, source: &str) -> Result<Expr, Parse
57
57
  Rule::list_literal => parse_list_literal(pair, source),
58
58
  Rule::dict_literal => parse_dict_literal(pair, source),
59
59
  Rule::tuple_literal => parse_tuple_literal(pair, source),
60
- Rule::set_literal => parse_set_literal(pair, source),
61
60
  Rule::list_comp => parse_list_comp(pair, source),
62
61
  Rule::dict_comp => parse_dict_comp(pair, source),
63
62
  Rule::regex => parse_regex_literal(pair, source),
@@ -586,7 +585,6 @@ fn parse_atom(pair: Pair<'_, Rule>, source: &str) -> Result<Expr, ParseError> {
586
585
  Rule::list_literal => parse_list_literal(inner_pair, source),
587
586
  Rule::dict_literal => parse_dict_literal(inner_pair, source),
588
587
  Rule::tuple_literal => parse_tuple_literal(inner_pair, source),
589
- Rule::set_literal => parse_set_literal(inner_pair, source),
590
588
  Rule::list_comp => parse_list_comp(inner_pair, source),
591
589
  Rule::dict_comp => parse_dict_comp(inner_pair, source),
592
590
  Rule::regex => parse_regex_literal(inner_pair, source),
@@ -355,7 +355,7 @@ fn validate_expr(expr: &Expr, source: &str) -> Result<(), ParseError> {
355
355
  Expr::Paren { expr, .. } => {
356
356
  validate_expr(expr, source)?;
357
357
  }
358
- Expr::List { elements, .. } | Expr::Tuple { elements, .. } | Expr::Set { elements, .. } => {
358
+ Expr::List { elements, .. } | Expr::Tuple { elements, .. } => {
359
359
  for expr in elements {
360
360
  validate_expr(expr, source)?;
361
361
  }
@@ -45,17 +45,6 @@ pub fn parse_tuple_literal(pair: Pair<'_, Rule>, source: &str) -> Result<Expr, P
45
45
  Ok(Expr::Tuple { elements, span })
46
46
  }
47
47
 
48
- pub fn parse_set_literal(pair: Pair<'_, Rule>, source: &str) -> Result<Expr, ParseError> {
49
- let span = span_from_pair(&pair, source);
50
- let mut elements = Vec::new();
51
- for inner in pair.into_inner() {
52
- if inner.as_rule() == Rule::expr {
53
- elements.push(crate::expr::parse_expr_pair(inner, source)?);
54
- }
55
- }
56
- Ok(Expr::Set { elements, span })
57
- }
58
-
59
48
  pub fn parse_list_literal(pair: Pair<'_, Rule>, source: &str) -> Result<Expr, ParseError> {
60
49
  let span = span_from_pair(&pair, source);
61
50
  let mut elements = Vec::new();
@@ -11,7 +11,11 @@ awk_rule = { block | awk_pattern ~ block? }
11
11
  awk_pattern = { expr }
12
12
 
13
13
  // Statements: separated by semicolons or newlines
14
- stmt_list = { stmt ~ (stmt_sep ~ stmt)* ~ stmt_sep? }
14
+ // Compound statements (ending with }) don't need trailing separators
15
+ // Simple statements need separators unless they're the last statement
16
+ stmt_list = { stmt_item* ~ final_stmt? }
17
+ stmt_item = _{ compound_stmt ~ stmt_sep* | simple_stmt ~ stmt_sep+ }
18
+ final_stmt = _{ compound_stmt | simple_stmt }
15
19
  stmt_sep = _{ ";" ~ NEWLINE* | NEWLINE+ }
16
20
 
17
21
  stmt = _{ compound_stmt | simple_stmt }
@@ -136,7 +140,8 @@ star_arg = { "*" ~ expr }
136
140
  kw_star_arg = { "**" ~ expr }
137
141
 
138
142
  // Attribute access and indexing/slicing
139
- attribute = { "." ~ identifier }
143
+ attribute = { "." ~ (identifier | attribute_index) }
144
+ attribute_index = @{ ASCII_DIGIT+ }
140
145
  index = { "[" ~ slice ~ "]" }
141
146
  slice = { slice_expr | expr }
142
147
  slice_expr = { slice_start? ~ ":" ~ slice_end? }
@@ -158,7 +163,6 @@ atom = _{
158
163
  | list_literal
159
164
  | dict_comp
160
165
  | dict_literal
161
- | set_literal
162
166
  | tuple_literal
163
167
  | compound_expr
164
168
  | "(" ~ expr ~ ")"
@@ -188,7 +192,6 @@ list_comp = { "[" ~ expr ~ comp_for ~ "]" }
188
192
  list_literal = { "[" ~ (expr ~ ("," ~ expr)*)? ~ "]" }
189
193
  dict_comp = { "{" ~ expr ~ ":" ~ expr ~ comp_for ~ "}" }
190
194
  dict_literal = { "{" ~ (dict_entry ~ ("," ~ dict_entry)*)? ~ "}" }
191
- set_literal = { "{" ~ expr ~ ("," ~ expr)* ~ ","? ~ "}" }
192
195
  dict_entry = { expr ~ ":" ~ expr }
193
196
 
194
197
  // Comprehension clauses
@@ -324,7 +324,6 @@ pub fn shift_expr_spans(expr: &mut Expr, offset: usize, source: &str) {
324
324
  | Expr::List { span, .. }
325
325
  | Expr::Tuple { span, .. }
326
326
  | Expr::Dict { span, .. }
327
- | Expr::Set { span, .. }
328
327
  | Expr::Slice { span, .. } => {
329
328
  *span = shift_span(span, offset, source);
330
329
  }
@@ -162,7 +162,6 @@ pub fn expr_span(expr: &snail_ast::Expr) -> &SourceSpan {
162
162
  | snail_ast::Expr::List { span, .. }
163
163
  | snail_ast::Expr::Tuple { span, .. }
164
164
  | snail_ast::Expr::Dict { span, .. }
165
- | snail_ast::Expr::Set { span, .. }
166
165
  | snail_ast::Expr::ListComp { span, .. }
167
166
  | snail_ast::Expr::DictComp { span, .. }
168
167
  | snail_ast::Expr::Slice { span, .. } => span,
@@ -632,3 +632,107 @@ fn parses_ternary_with_is_not_operator() {
632
632
  other => panic!("Expected assignment, got {:?}", other),
633
633
  }
634
634
  }
635
+
636
+ // Tests for compound statement separator behavior (no semicolon needed after })
637
+
638
+ #[test]
639
+ fn parses_if_followed_by_stmt_without_separator() {
640
+ // if statement followed by expression without semicolon
641
+ let source = "if x { y = 1 } z";
642
+ let program = parse_ok(source);
643
+ assert_eq!(program.stmts.len(), 2);
644
+ assert!(matches!(&program.stmts[0], Stmt::If { .. }));
645
+ assert!(matches!(&program.stmts[1], Stmt::Expr { .. }));
646
+ }
647
+
648
+ #[test]
649
+ fn parses_while_followed_by_stmt_without_separator() {
650
+ let source = "while x { y = 1 } z";
651
+ let program = parse_ok(source);
652
+ assert_eq!(program.stmts.len(), 2);
653
+ assert!(matches!(&program.stmts[0], Stmt::While { .. }));
654
+ assert!(matches!(&program.stmts[1], Stmt::Expr { .. }));
655
+ }
656
+
657
+ #[test]
658
+ fn parses_for_followed_by_stmt_without_separator() {
659
+ let source = "for i in x { y = 1 } z";
660
+ let program = parse_ok(source);
661
+ assert_eq!(program.stmts.len(), 2);
662
+ assert!(matches!(&program.stmts[0], Stmt::For { .. }));
663
+ assert!(matches!(&program.stmts[1], Stmt::Expr { .. }));
664
+ }
665
+
666
+ #[test]
667
+ fn parses_def_followed_by_stmt_without_separator() {
668
+ let source = "def f() { pass } f()";
669
+ let program = parse_ok(source);
670
+ assert_eq!(program.stmts.len(), 2);
671
+ assert!(matches!(&program.stmts[0], Stmt::Def { .. }));
672
+ assert!(matches!(&program.stmts[1], Stmt::Expr { .. }));
673
+ }
674
+
675
+ #[test]
676
+ fn parses_class_followed_by_stmt_without_separator() {
677
+ let source = "class C { pass } C()";
678
+ let program = parse_ok(source);
679
+ assert_eq!(program.stmts.len(), 2);
680
+ assert!(matches!(&program.stmts[0], Stmt::Class { .. }));
681
+ assert!(matches!(&program.stmts[1], Stmt::Expr { .. }));
682
+ }
683
+
684
+ #[test]
685
+ fn parses_try_followed_by_stmt_without_separator() {
686
+ // Note: using explicit exception type since bare `except { }` is ambiguous with set literals
687
+ let source = "try { x } except Exception { y } z";
688
+ let program = parse_ok(source);
689
+ assert_eq!(program.stmts.len(), 2);
690
+ assert!(matches!(&program.stmts[0], Stmt::Try { .. }));
691
+ assert!(matches!(&program.stmts[1], Stmt::Expr { .. }));
692
+ }
693
+
694
+ #[test]
695
+ fn parses_with_followed_by_stmt_without_separator() {
696
+ let source = "with x { y } z";
697
+ let program = parse_ok(source);
698
+ assert_eq!(program.stmts.len(), 2);
699
+ assert!(matches!(&program.stmts[0], Stmt::With { .. }));
700
+ assert!(matches!(&program.stmts[1], Stmt::Expr { .. }));
701
+ }
702
+
703
+ #[test]
704
+ fn parses_nested_compound_stmts_without_separators() {
705
+ let source = "if a { if b { c } d } e";
706
+ let program = parse_ok(source);
707
+ assert_eq!(program.stmts.len(), 2);
708
+ assert!(matches!(&program.stmts[0], Stmt::If { .. }));
709
+ assert!(matches!(&program.stmts[1], Stmt::Expr { .. }));
710
+ }
711
+
712
+ #[test]
713
+ fn parses_mixed_compound_and_simple_stmts() {
714
+ let source = "a = 1; if b { c = 2 } d = 3; e = 4";
715
+ let program = parse_ok(source);
716
+ assert_eq!(program.stmts.len(), 4);
717
+ assert!(matches!(&program.stmts[0], Stmt::Assign { .. }));
718
+ assert!(matches!(&program.stmts[1], Stmt::If { .. }));
719
+ assert!(matches!(&program.stmts[2], Stmt::Assign { .. }));
720
+ assert!(matches!(&program.stmts[3], Stmt::Assign { .. }));
721
+ }
722
+
723
+ #[test]
724
+ fn parses_consecutive_compound_stmts_without_separators() {
725
+ let source = "if a { b } if c { d } while e { f }";
726
+ let program = parse_ok(source);
727
+ assert_eq!(program.stmts.len(), 3);
728
+ assert!(matches!(&program.stmts[0], Stmt::If { .. }));
729
+ assert!(matches!(&program.stmts[1], Stmt::If { .. }));
730
+ assert!(matches!(&program.stmts[2], Stmt::While { .. }));
731
+ }
732
+
733
+ #[test]
734
+ fn simple_stmt_still_requires_separator() {
735
+ // Two simple statements without separator should fail
736
+ let source = "a b";
737
+ parse_err(source);
738
+ }
@@ -183,10 +183,10 @@ fn parses_assert_and_del() {
183
183
  }
184
184
 
185
185
  #[test]
186
- fn parses_tuples_sets_and_slices() {
187
- let source = "items = [1, 2, 3, 4]\npair = (1, 2)\nsingle = (1,)\nempty = ()\nflags = {True, False}\nmid = items[1:3]\nhead = items[:2]\ntail = items[2:]\n";
186
+ fn parses_tuples_and_slices() {
187
+ let source = "items = [1, 2, 3, 4]\npair = (1, 2)\nsingle = (1,)\nempty = ()\nmid = items[1:3]\nhead = items[:2]\ntail = items[2:]\n";
188
188
  let program = parse_ok(source);
189
- assert_eq!(program.stmts.len(), 8);
189
+ assert_eq!(program.stmts.len(), 7);
190
190
 
191
191
  let (_, value) = expect_assign(&program.stmts[0]);
192
192
  match value {
@@ -213,18 +213,6 @@ fn parses_tuples_sets_and_slices() {
213
213
  }
214
214
 
215
215
  let (_, value) = expect_assign(&program.stmts[4]);
216
- match value {
217
- Expr::Set { elements, .. } => {
218
- assert_eq!(elements.len(), 2);
219
- match &elements[0] {
220
- Expr::Bool { value, .. } => assert!(*value),
221
- other => panic!("Expected True, got {other:?}"),
222
- }
223
- }
224
- other => panic!("Expected set, got {other:?}"),
225
- }
226
-
227
- let (_, value) = expect_assign(&program.stmts[5]);
228
216
  match value {
229
217
  Expr::Index { value, index, .. } => {
230
218
  expect_name(value.as_ref(), "items");
@@ -239,7 +227,7 @@ fn parses_tuples_sets_and_slices() {
239
227
  other => panic!("Expected index, got {other:?}"),
240
228
  }
241
229
 
242
- let (_, value) = expect_assign(&program.stmts[6]);
230
+ let (_, value) = expect_assign(&program.stmts[5]);
243
231
  match value {
244
232
  Expr::Index { index, .. } => match index.as_ref() {
245
233
  Expr::Slice { start, end, .. } => {
@@ -251,7 +239,7 @@ fn parses_tuples_sets_and_slices() {
251
239
  other => panic!("Expected index, got {other:?}"),
252
240
  }
253
241
 
254
- let (_, value) = expect_assign(&program.stmts[7]);
242
+ let (_, value) = expect_assign(&program.stmts[6]);
255
243
  match value {
256
244
  Expr::Index { index, .. } => match index.as_ref() {
257
245
  Expr::Slice { start, end, .. } => {
@@ -367,3 +367,17 @@ fn parses_call_attribute_index_chain() {
367
367
  other => panic!("Expected index access, got {other:?}"),
368
368
  }
369
369
  }
370
+
371
+ #[test]
372
+ fn parses_numeric_attribute_access() {
373
+ let program = parse_ok("value = match.1");
374
+ let (_, value) = expect_assign(&program.stmts[0]);
375
+
376
+ match value {
377
+ Expr::Attribute { value, attr, .. } => {
378
+ expect_name(value.as_ref(), "match");
379
+ assert_eq!(attr, "1");
380
+ }
381
+ other => panic!("Expected attribute access, got {other:?}"),
382
+ }
383
+ }
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "snail-python"
3
- version = "0.3.7"
3
+ version = "0.3.8"
4
4
  edition.workspace = true
5
5
 
6
6
  [lib]
@@ -4,7 +4,7 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "snail-lang"
7
- version = "0.3.7"
7
+ version = "0.3.8"
8
8
  description = "Snail programming language interpreter"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -9,5 +9,5 @@ def compact_try(expr_fn, fallback_fn=None):
9
9
  fallback_member = getattr(exc, "__fallback__", None)
10
10
  if callable(fallback_member):
11
11
  return fallback_member()
12
- return exc
12
+ return None
13
13
  return fallback_fn(exc)
File without changes
File without changes
File without changes