machine-dialect 0.1.0a1__py3-none-any.whl

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 (268) hide show
  1. machine_dialect/__main__.py +667 -0
  2. machine_dialect/agent/__init__.py +5 -0
  3. machine_dialect/agent/agent.py +360 -0
  4. machine_dialect/ast/__init__.py +95 -0
  5. machine_dialect/ast/ast_node.py +35 -0
  6. machine_dialect/ast/call_expression.py +82 -0
  7. machine_dialect/ast/dict_extraction.py +60 -0
  8. machine_dialect/ast/expressions.py +439 -0
  9. machine_dialect/ast/literals.py +309 -0
  10. machine_dialect/ast/program.py +35 -0
  11. machine_dialect/ast/statements.py +1433 -0
  12. machine_dialect/ast/tests/test_ast_string_representation.py +62 -0
  13. machine_dialect/ast/tests/test_boolean_literal.py +29 -0
  14. machine_dialect/ast/tests/test_collection_hir.py +138 -0
  15. machine_dialect/ast/tests/test_define_statement.py +142 -0
  16. machine_dialect/ast/tests/test_desugar.py +541 -0
  17. machine_dialect/ast/tests/test_foreach_desugar.py +245 -0
  18. machine_dialect/cfg/__init__.py +6 -0
  19. machine_dialect/cfg/config.py +156 -0
  20. machine_dialect/cfg/examples.py +221 -0
  21. machine_dialect/cfg/generate_with_ai.py +187 -0
  22. machine_dialect/cfg/openai_generation.py +200 -0
  23. machine_dialect/cfg/parser.py +94 -0
  24. machine_dialect/cfg/tests/__init__.py +1 -0
  25. machine_dialect/cfg/tests/test_cfg_parser.py +252 -0
  26. machine_dialect/cfg/tests/test_config.py +188 -0
  27. machine_dialect/cfg/tests/test_examples.py +391 -0
  28. machine_dialect/cfg/tests/test_generate_with_ai.py +354 -0
  29. machine_dialect/cfg/tests/test_openai_generation.py +256 -0
  30. machine_dialect/codegen/__init__.py +5 -0
  31. machine_dialect/codegen/bytecode_module.py +89 -0
  32. machine_dialect/codegen/bytecode_serializer.py +300 -0
  33. machine_dialect/codegen/opcodes.py +101 -0
  34. machine_dialect/codegen/register_codegen.py +1996 -0
  35. machine_dialect/codegen/symtab.py +208 -0
  36. machine_dialect/codegen/tests/__init__.py +1 -0
  37. machine_dialect/codegen/tests/test_array_operations_codegen.py +295 -0
  38. machine_dialect/codegen/tests/test_bytecode_serializer.py +185 -0
  39. machine_dialect/codegen/tests/test_register_codegen_ssa.py +324 -0
  40. machine_dialect/codegen/tests/test_symtab.py +418 -0
  41. machine_dialect/codegen/vm_serializer.py +621 -0
  42. machine_dialect/compiler/__init__.py +18 -0
  43. machine_dialect/compiler/compiler.py +197 -0
  44. machine_dialect/compiler/config.py +149 -0
  45. machine_dialect/compiler/context.py +149 -0
  46. machine_dialect/compiler/phases/__init__.py +19 -0
  47. machine_dialect/compiler/phases/bytecode_optimization.py +90 -0
  48. machine_dialect/compiler/phases/codegen.py +40 -0
  49. machine_dialect/compiler/phases/hir_generation.py +39 -0
  50. machine_dialect/compiler/phases/mir_generation.py +86 -0
  51. machine_dialect/compiler/phases/optimization.py +110 -0
  52. machine_dialect/compiler/phases/parsing.py +39 -0
  53. machine_dialect/compiler/pipeline.py +143 -0
  54. machine_dialect/compiler/tests/__init__.py +1 -0
  55. machine_dialect/compiler/tests/test_compiler.py +568 -0
  56. machine_dialect/compiler/vm_runner.py +173 -0
  57. machine_dialect/errors/__init__.py +32 -0
  58. machine_dialect/errors/exceptions.py +369 -0
  59. machine_dialect/errors/messages.py +82 -0
  60. machine_dialect/errors/tests/__init__.py +0 -0
  61. machine_dialect/errors/tests/test_expected_token_errors.py +188 -0
  62. machine_dialect/errors/tests/test_name_errors.py +118 -0
  63. machine_dialect/helpers/__init__.py +0 -0
  64. machine_dialect/helpers/stopwords.py +225 -0
  65. machine_dialect/helpers/validators.py +30 -0
  66. machine_dialect/lexer/__init__.py +9 -0
  67. machine_dialect/lexer/constants.py +23 -0
  68. machine_dialect/lexer/lexer.py +907 -0
  69. machine_dialect/lexer/tests/__init__.py +0 -0
  70. machine_dialect/lexer/tests/helpers.py +86 -0
  71. machine_dialect/lexer/tests/test_apostrophe_identifiers.py +122 -0
  72. machine_dialect/lexer/tests/test_backtick_identifiers.py +140 -0
  73. machine_dialect/lexer/tests/test_boolean_literals.py +108 -0
  74. machine_dialect/lexer/tests/test_case_insensitive_keywords.py +188 -0
  75. machine_dialect/lexer/tests/test_comments.py +200 -0
  76. machine_dialect/lexer/tests/test_double_asterisk_keywords.py +127 -0
  77. machine_dialect/lexer/tests/test_lexer_position.py +113 -0
  78. machine_dialect/lexer/tests/test_list_tokens.py +282 -0
  79. machine_dialect/lexer/tests/test_stopwords.py +80 -0
  80. machine_dialect/lexer/tests/test_strict_equality.py +129 -0
  81. machine_dialect/lexer/tests/test_token.py +41 -0
  82. machine_dialect/lexer/tests/test_tokenization.py +294 -0
  83. machine_dialect/lexer/tests/test_underscore_literals.py +343 -0
  84. machine_dialect/lexer/tests/test_url_literals.py +169 -0
  85. machine_dialect/lexer/tokens.py +487 -0
  86. machine_dialect/linter/__init__.py +10 -0
  87. machine_dialect/linter/__main__.py +144 -0
  88. machine_dialect/linter/linter.py +154 -0
  89. machine_dialect/linter/rules/__init__.py +8 -0
  90. machine_dialect/linter/rules/base.py +112 -0
  91. machine_dialect/linter/rules/statement_termination.py +99 -0
  92. machine_dialect/linter/tests/__init__.py +1 -0
  93. machine_dialect/linter/tests/mdrules/__init__.py +0 -0
  94. machine_dialect/linter/tests/mdrules/test_md101_statement_termination.py +181 -0
  95. machine_dialect/linter/tests/test_linter.py +81 -0
  96. machine_dialect/linter/tests/test_rules.py +110 -0
  97. machine_dialect/linter/tests/test_violations.py +71 -0
  98. machine_dialect/linter/violations.py +51 -0
  99. machine_dialect/mir/__init__.py +69 -0
  100. machine_dialect/mir/analyses/__init__.py +20 -0
  101. machine_dialect/mir/analyses/alias_analysis.py +315 -0
  102. machine_dialect/mir/analyses/dominance_analysis.py +49 -0
  103. machine_dialect/mir/analyses/escape_analysis.py +286 -0
  104. machine_dialect/mir/analyses/loop_analysis.py +272 -0
  105. machine_dialect/mir/analyses/tests/test_type_analysis.py +736 -0
  106. machine_dialect/mir/analyses/type_analysis.py +448 -0
  107. machine_dialect/mir/analyses/use_def_chains.py +232 -0
  108. machine_dialect/mir/basic_block.py +385 -0
  109. machine_dialect/mir/dataflow.py +445 -0
  110. machine_dialect/mir/debug_info.py +208 -0
  111. machine_dialect/mir/hir_to_mir.py +1738 -0
  112. machine_dialect/mir/mir_dumper.py +366 -0
  113. machine_dialect/mir/mir_function.py +167 -0
  114. machine_dialect/mir/mir_instructions.py +1877 -0
  115. machine_dialect/mir/mir_interpreter.py +556 -0
  116. machine_dialect/mir/mir_module.py +225 -0
  117. machine_dialect/mir/mir_printer.py +480 -0
  118. machine_dialect/mir/mir_transformer.py +410 -0
  119. machine_dialect/mir/mir_types.py +367 -0
  120. machine_dialect/mir/mir_validation.py +455 -0
  121. machine_dialect/mir/mir_values.py +268 -0
  122. machine_dialect/mir/optimization_config.py +233 -0
  123. machine_dialect/mir/optimization_pass.py +251 -0
  124. machine_dialect/mir/optimization_pipeline.py +355 -0
  125. machine_dialect/mir/optimizations/__init__.py +84 -0
  126. machine_dialect/mir/optimizations/algebraic_simplification.py +733 -0
  127. machine_dialect/mir/optimizations/branch_prediction.py +372 -0
  128. machine_dialect/mir/optimizations/constant_propagation.py +634 -0
  129. machine_dialect/mir/optimizations/cse.py +398 -0
  130. machine_dialect/mir/optimizations/dce.py +288 -0
  131. machine_dialect/mir/optimizations/inlining.py +551 -0
  132. machine_dialect/mir/optimizations/jump_threading.py +487 -0
  133. machine_dialect/mir/optimizations/licm.py +405 -0
  134. machine_dialect/mir/optimizations/loop_unrolling.py +366 -0
  135. machine_dialect/mir/optimizations/strength_reduction.py +422 -0
  136. machine_dialect/mir/optimizations/tail_call.py +207 -0
  137. machine_dialect/mir/optimizations/tests/test_loop_unrolling.py +483 -0
  138. machine_dialect/mir/optimizations/type_narrowing.py +397 -0
  139. machine_dialect/mir/optimizations/type_specialization.py +447 -0
  140. machine_dialect/mir/optimizations/type_specific.py +906 -0
  141. machine_dialect/mir/optimize_mir.py +89 -0
  142. machine_dialect/mir/pass_manager.py +391 -0
  143. machine_dialect/mir/profiling/__init__.py +26 -0
  144. machine_dialect/mir/profiling/profile_collector.py +318 -0
  145. machine_dialect/mir/profiling/profile_data.py +372 -0
  146. machine_dialect/mir/profiling/profile_reader.py +272 -0
  147. machine_dialect/mir/profiling/profile_writer.py +226 -0
  148. machine_dialect/mir/register_allocation.py +302 -0
  149. machine_dialect/mir/reporting/__init__.py +17 -0
  150. machine_dialect/mir/reporting/optimization_reporter.py +314 -0
  151. machine_dialect/mir/reporting/report_formatter.py +289 -0
  152. machine_dialect/mir/ssa_construction.py +342 -0
  153. machine_dialect/mir/tests/__init__.py +1 -0
  154. machine_dialect/mir/tests/test_algebraic_associativity.py +204 -0
  155. machine_dialect/mir/tests/test_algebraic_complex_patterns.py +221 -0
  156. machine_dialect/mir/tests/test_algebraic_division.py +126 -0
  157. machine_dialect/mir/tests/test_algebraic_simplification.py +863 -0
  158. machine_dialect/mir/tests/test_basic_block.py +425 -0
  159. machine_dialect/mir/tests/test_branch_prediction.py +459 -0
  160. machine_dialect/mir/tests/test_call_lowering.py +168 -0
  161. machine_dialect/mir/tests/test_collection_lowering.py +604 -0
  162. machine_dialect/mir/tests/test_cross_block_constant_propagation.py +255 -0
  163. machine_dialect/mir/tests/test_custom_passes.py +166 -0
  164. machine_dialect/mir/tests/test_debug_info.py +285 -0
  165. machine_dialect/mir/tests/test_dict_extraction_lowering.py +192 -0
  166. machine_dialect/mir/tests/test_dictionary_lowering.py +299 -0
  167. machine_dialect/mir/tests/test_double_negation.py +231 -0
  168. machine_dialect/mir/tests/test_escape_analysis.py +233 -0
  169. machine_dialect/mir/tests/test_hir_to_mir.py +465 -0
  170. machine_dialect/mir/tests/test_hir_to_mir_complete.py +389 -0
  171. machine_dialect/mir/tests/test_hir_to_mir_simple.py +130 -0
  172. machine_dialect/mir/tests/test_inlining.py +435 -0
  173. machine_dialect/mir/tests/test_licm.py +472 -0
  174. machine_dialect/mir/tests/test_mir_dumper.py +313 -0
  175. machine_dialect/mir/tests/test_mir_instructions.py +445 -0
  176. machine_dialect/mir/tests/test_mir_module.py +860 -0
  177. machine_dialect/mir/tests/test_mir_printer.py +387 -0
  178. machine_dialect/mir/tests/test_mir_types.py +123 -0
  179. machine_dialect/mir/tests/test_mir_types_enhanced.py +132 -0
  180. machine_dialect/mir/tests/test_mir_validation.py +378 -0
  181. machine_dialect/mir/tests/test_mir_values.py +168 -0
  182. machine_dialect/mir/tests/test_one_based_indexing.py +202 -0
  183. machine_dialect/mir/tests/test_optimization_helpers.py +60 -0
  184. machine_dialect/mir/tests/test_optimization_pipeline.py +554 -0
  185. machine_dialect/mir/tests/test_optimization_reporter.py +318 -0
  186. machine_dialect/mir/tests/test_pass_manager.py +294 -0
  187. machine_dialect/mir/tests/test_pass_registration.py +64 -0
  188. machine_dialect/mir/tests/test_profiling.py +356 -0
  189. machine_dialect/mir/tests/test_register_allocation.py +307 -0
  190. machine_dialect/mir/tests/test_report_formatters.py +372 -0
  191. machine_dialect/mir/tests/test_ssa_construction.py +433 -0
  192. machine_dialect/mir/tests/test_tail_call.py +236 -0
  193. machine_dialect/mir/tests/test_type_annotated_instructions.py +192 -0
  194. machine_dialect/mir/tests/test_type_narrowing.py +277 -0
  195. machine_dialect/mir/tests/test_type_specialization.py +421 -0
  196. machine_dialect/mir/tests/test_type_specific_optimization.py +545 -0
  197. machine_dialect/mir/tests/test_type_specific_optimization_advanced.py +382 -0
  198. machine_dialect/mir/type_inference.py +368 -0
  199. machine_dialect/parser/__init__.py +12 -0
  200. machine_dialect/parser/enums.py +45 -0
  201. machine_dialect/parser/parser.py +3655 -0
  202. machine_dialect/parser/protocols.py +11 -0
  203. machine_dialect/parser/symbol_table.py +169 -0
  204. machine_dialect/parser/tests/__init__.py +0 -0
  205. machine_dialect/parser/tests/helper_functions.py +193 -0
  206. machine_dialect/parser/tests/test_action_statements.py +334 -0
  207. machine_dialect/parser/tests/test_boolean_literal_expressions.py +152 -0
  208. machine_dialect/parser/tests/test_call_statements.py +154 -0
  209. machine_dialect/parser/tests/test_call_statements_errors.py +187 -0
  210. machine_dialect/parser/tests/test_collection_mutations.py +264 -0
  211. machine_dialect/parser/tests/test_conditional_expressions.py +343 -0
  212. machine_dialect/parser/tests/test_define_integration.py +468 -0
  213. machine_dialect/parser/tests/test_define_statements.py +311 -0
  214. machine_dialect/parser/tests/test_dict_extraction.py +115 -0
  215. machine_dialect/parser/tests/test_empty_literal.py +155 -0
  216. machine_dialect/parser/tests/test_float_literal_expressions.py +163 -0
  217. machine_dialect/parser/tests/test_identifier_expressions.py +57 -0
  218. machine_dialect/parser/tests/test_if_empty_block.py +61 -0
  219. machine_dialect/parser/tests/test_if_statements.py +299 -0
  220. machine_dialect/parser/tests/test_illegal_tokens.py +86 -0
  221. machine_dialect/parser/tests/test_infix_expressions.py +680 -0
  222. machine_dialect/parser/tests/test_integer_literal_expressions.py +137 -0
  223. machine_dialect/parser/tests/test_interaction_statements.py +269 -0
  224. machine_dialect/parser/tests/test_list_literals.py +277 -0
  225. machine_dialect/parser/tests/test_no_none_in_ast.py +94 -0
  226. machine_dialect/parser/tests/test_panic_mode_recovery.py +171 -0
  227. machine_dialect/parser/tests/test_parse_errors.py +114 -0
  228. machine_dialect/parser/tests/test_possessive_syntax.py +182 -0
  229. machine_dialect/parser/tests/test_prefix_expressions.py +415 -0
  230. machine_dialect/parser/tests/test_program.py +13 -0
  231. machine_dialect/parser/tests/test_return_statements.py +89 -0
  232. machine_dialect/parser/tests/test_set_statements.py +152 -0
  233. machine_dialect/parser/tests/test_strict_equality.py +258 -0
  234. machine_dialect/parser/tests/test_symbol_table.py +217 -0
  235. machine_dialect/parser/tests/test_url_literal_expressions.py +209 -0
  236. machine_dialect/parser/tests/test_utility_statements.py +423 -0
  237. machine_dialect/parser/token_buffer.py +159 -0
  238. machine_dialect/repl/__init__.py +3 -0
  239. machine_dialect/repl/repl.py +426 -0
  240. machine_dialect/repl/tests/__init__.py +0 -0
  241. machine_dialect/repl/tests/test_repl.py +606 -0
  242. machine_dialect/semantic/__init__.py +12 -0
  243. machine_dialect/semantic/analyzer.py +906 -0
  244. machine_dialect/semantic/error_messages.py +189 -0
  245. machine_dialect/semantic/tests/__init__.py +1 -0
  246. machine_dialect/semantic/tests/test_analyzer.py +364 -0
  247. machine_dialect/semantic/tests/test_error_messages.py +104 -0
  248. machine_dialect/tests/edge_cases/__init__.py +10 -0
  249. machine_dialect/tests/edge_cases/test_boundary_access.py +256 -0
  250. machine_dialect/tests/edge_cases/test_empty_collections.py +166 -0
  251. machine_dialect/tests/edge_cases/test_invalid_operations.py +243 -0
  252. machine_dialect/tests/edge_cases/test_named_list_edge_cases.py +295 -0
  253. machine_dialect/tests/edge_cases/test_nested_structures.py +313 -0
  254. machine_dialect/tests/edge_cases/test_type_mixing.py +277 -0
  255. machine_dialect/tests/integration/test_array_operations_emulation.py +248 -0
  256. machine_dialect/tests/integration/test_list_compilation.py +395 -0
  257. machine_dialect/tests/integration/test_lists_and_dictionaries.py +322 -0
  258. machine_dialect/type_checking/__init__.py +21 -0
  259. machine_dialect/type_checking/tests/__init__.py +1 -0
  260. machine_dialect/type_checking/tests/test_type_system.py +230 -0
  261. machine_dialect/type_checking/type_system.py +270 -0
  262. machine_dialect-0.1.0a1.dist-info/METADATA +128 -0
  263. machine_dialect-0.1.0a1.dist-info/RECORD +268 -0
  264. machine_dialect-0.1.0a1.dist-info/WHEEL +5 -0
  265. machine_dialect-0.1.0a1.dist-info/entry_points.txt +3 -0
  266. machine_dialect-0.1.0a1.dist-info/licenses/LICENSE +201 -0
  267. machine_dialect-0.1.0a1.dist-info/top_level.txt +2 -0
  268. machine_dialect_vm/__init__.pyi +15 -0
@@ -0,0 +1,680 @@
1
+ """Tests for parsing infix expressions.
2
+
3
+ This module tests the parser's ability to handle infix expressions including:
4
+ - Arithmetic operators: +, -, *, /
5
+ - Comparison operators: ==, !=, <, >
6
+ - Operator precedence and associativity
7
+ - Complex expressions with mixed operators
8
+ """
9
+
10
+ import pytest
11
+
12
+ from machine_dialect.ast import (
13
+ ExpressionStatement,
14
+ InfixExpression,
15
+ WholeNumberLiteral,
16
+ YesNoLiteral,
17
+ )
18
+ from machine_dialect.parser import Parser
19
+ from machine_dialect.parser.tests.helper_functions import (
20
+ assert_infix_expression,
21
+ assert_program_statements,
22
+ )
23
+
24
+
25
+ class TestInfixExpressions:
26
+ """Test parsing of infix expressions."""
27
+
28
+ @pytest.mark.parametrize(
29
+ "source,left,operator,right",
30
+ [
31
+ # Addition
32
+ ("5 + 5", 5, "+", 5),
33
+ ("10 + 20", 10, "+", 20),
34
+ ("42 + 123", 42, "+", 123),
35
+ ("0 + 0", 0, "+", 0),
36
+ # Addition with underscores
37
+ ("_5_ + _5_", 5, "+", 5),
38
+ ("_10_ + _20_", 10, "+", 20),
39
+ # Subtraction
40
+ ("5 - 5", 5, "-", 5),
41
+ ("20 - 10", 20, "-", 10),
42
+ ("100 - 50", 100, "-", 50),
43
+ ("0 - 0", 0, "-", 0),
44
+ # Subtraction with underscores
45
+ ("_5_ - _5_", 5, "-", 5),
46
+ ("_20_ - _10_", 20, "-", 10),
47
+ # Multiplication
48
+ ("5 * 5", 5, "*", 5),
49
+ ("10 * 20", 10, "*", 20),
50
+ ("7 * 8", 7, "*", 8),
51
+ ("0 * 100", 0, "*", 100),
52
+ # Multiplication with underscores
53
+ ("_5_ * _5_", 5, "*", 5),
54
+ ("_10_ * _20_", 10, "*", 20),
55
+ # Division
56
+ ("10 / 5", 10, "/", 5),
57
+ ("20 / 4", 20, "/", 4),
58
+ ("100 / 10", 100, "/", 10),
59
+ ("0 / 1", 0, "/", 1),
60
+ # Division with underscores
61
+ ("_10_ / _5_", 10, "/", 5),
62
+ ("_20_ / _4_", 20, "/", 4),
63
+ ],
64
+ )
65
+ def test_integer_arithmetic_expressions(self, source: str, left: int, operator: str, right: int) -> None:
66
+ """Test parsing integer arithmetic infix expressions.
67
+
68
+ Args:
69
+ source: The source code containing an infix expression.
70
+ left: Expected left operand value.
71
+ operator: Expected operator string.
72
+ right: Expected right operand value.
73
+ """
74
+ parser = Parser()
75
+
76
+ program = parser.parse(source)
77
+
78
+ assert len(parser.errors) == 0, f"Parser errors: {parser.errors}"
79
+ assert_program_statements(parser, program)
80
+
81
+ statement = program.statements[0]
82
+ assert isinstance(statement, ExpressionStatement)
83
+ assert statement.expression is not None
84
+
85
+ assert_infix_expression(statement.expression, left, operator, right)
86
+
87
+ @pytest.mark.parametrize(
88
+ "source,left,operator,right",
89
+ [
90
+ # Float addition
91
+ ("3.14 + 2.86", 3.14, "+", 2.86),
92
+ ("0.5 + 0.5", 0.5, "+", 0.5),
93
+ ("10.0 + 20.0", 10.0, "+", 20.0),
94
+ # Float subtraction
95
+ ("5.5 - 2.5", 5.5, "-", 2.5),
96
+ ("10.0 - 5.0", 10.0, "-", 5.0),
97
+ ("3.14 - 1.14", 3.14, "-", 1.14),
98
+ # Float multiplication
99
+ ("2.5 * 2.0", 2.5, "*", 2.0),
100
+ ("3.14 * 2.0", 3.14, "*", 2.0),
101
+ ("0.5 * 0.5", 0.5, "*", 0.5),
102
+ # Float division
103
+ ("10.0 / 2.0", 10.0, "/", 2.0),
104
+ ("7.5 / 2.5", 7.5, "/", 2.5),
105
+ ("3.14 / 2.0", 3.14, "/", 2.0),
106
+ # Mixed integer and float
107
+ ("5 + 2.5", 5, "+", 2.5),
108
+ ("10.0 - 5", 10.0, "-", 5),
109
+ ("3 * 2.5", 3, "*", 2.5),
110
+ ("10.0 / 2", 10.0, "/", 2),
111
+ ],
112
+ )
113
+ def test_float_arithmetic_expressions(
114
+ self, source: str, left: int | float, operator: str, right: int | float
115
+ ) -> None:
116
+ """Test parsing float and mixed arithmetic infix expressions.
117
+
118
+ Args:
119
+ source: The source code containing an infix expression.
120
+ left: Expected left operand value.
121
+ operator: Expected operator string.
122
+ right: Expected right operand value.
123
+ """
124
+ parser = Parser()
125
+
126
+ program = parser.parse(source)
127
+
128
+ assert len(parser.errors) == 0, f"Parser errors: {parser.errors}"
129
+ assert_program_statements(parser, program)
130
+
131
+ statement = program.statements[0]
132
+ assert isinstance(statement, ExpressionStatement)
133
+ assert statement.expression is not None
134
+
135
+ assert_infix_expression(statement.expression, left, operator, right)
136
+
137
+ @pytest.mark.parametrize(
138
+ "source,left,operator,right",
139
+ [
140
+ # Integer comparisons
141
+ ("5 equals 5", 5, "equals", 5),
142
+ ("10 equals 20", 10, "equals", 20),
143
+ ("5 is not 5", 5, "is not", 5),
144
+ ("10 is not 20", 10, "is not", 20),
145
+ ("5 < 10", 5, "<", 10),
146
+ ("20 < 10", 20, "<", 10),
147
+ ("5 > 10", 5, ">", 10),
148
+ ("20 > 10", 20, ">", 10),
149
+ ("5 <= 10", 5, "<=", 10),
150
+ ("10 <= 10", 10, "<=", 10),
151
+ ("20 <= 10", 20, "<=", 10),
152
+ ("5 >= 10", 5, ">=", 10),
153
+ ("10 >= 10", 10, ">=", 10),
154
+ ("20 >= 10", 20, ">=", 10),
155
+ # Float comparisons
156
+ ("3.14 equals 3.14", 3.14, "equals", 3.14),
157
+ ("2.5 is not 3.5", 2.5, "is not", 3.5),
158
+ ("1.5 < 2.5", 1.5, "<", 2.5),
159
+ ("3.5 > 2.5", 3.5, ">", 2.5),
160
+ # Boolean comparisons
161
+ ("Yes equals Yes", True, "equals", True),
162
+ ("Yes equals No", True, "equals", False),
163
+ ("No is not Yes", False, "is not", True),
164
+ ("No is not No", False, "is not", False),
165
+ # Mixed type comparisons (will be type-checked at runtime)
166
+ ("5 equals 5.0", 5, "equals", 5.0),
167
+ ("10 is not 10.5", 10, "is not", 10.5),
168
+ ("3 < 3.14", 3, "<", 3.14),
169
+ ("5.0 > 4", 5.0, ">", 4),
170
+ ],
171
+ )
172
+ def test_comparison_expressions(
173
+ self, source: str, left: int | float | bool, operator: str, right: int | float | bool
174
+ ) -> None:
175
+ """Test parsing comparison infix expressions.
176
+
177
+ Args:
178
+ source: The source code containing a comparison expression.
179
+ left: Expected left operand value.
180
+ operator: Expected comparison operator.
181
+ right: Expected right operand value.
182
+ """
183
+ parser = Parser()
184
+
185
+ program = parser.parse(source)
186
+
187
+ assert len(parser.errors) == 0, f"Parser errors: {parser.errors}"
188
+ assert_program_statements(parser, program)
189
+
190
+ statement = program.statements[0]
191
+ assert isinstance(statement, ExpressionStatement)
192
+ assert statement.expression is not None
193
+
194
+ assert_infix_expression(statement.expression, left, operator, right)
195
+
196
+ @pytest.mark.parametrize(
197
+ "source,left,operator,right",
198
+ [
199
+ # Identifier arithmetic
200
+ ("x + z", "x", "+", "z"),
201
+ ("foo - bar", "foo", "-", "bar"),
202
+ ("p * q", "p", "*", "q"),
203
+ ("width / height", "width", "/", "height"),
204
+ # Identifier comparisons
205
+ ("x equals z", "x", "equals", "z"),
206
+ ("foo is not bar", "foo", "is not", "bar"),
207
+ ("p < q", "p", "<", "q"),
208
+ ("width > height", "width", ">", "height"),
209
+ # Mixed identifier and literal
210
+ ("x + 5", "x", "+", 5),
211
+ ("10 - z", 10, "-", "z"),
212
+ ("pi * 2", "pi", "*", 2),
213
+ ("total / 100.0", "total", "/", 100.0),
214
+ # Backtick identifiers
215
+ ("`first name` + `last name`", "first name", "+", "last name"),
216
+ ("`total cost` * `tax rate`", "total cost", "*", "tax rate"),
217
+ ("`is valid` equals Yes", "is valid", "equals", True),
218
+ ],
219
+ )
220
+ def test_identifier_expressions(
221
+ self, source: str, left: str | int | float, operator: str, right: str | int | float | bool
222
+ ) -> None:
223
+ """Test parsing infix expressions with identifiers.
224
+
225
+ Args:
226
+ source: The source code containing an infix expression with identifiers.
227
+ left: Expected left operand value.
228
+ operator: Expected operator string.
229
+ right: Expected right operand value.
230
+ """
231
+ parser = Parser()
232
+
233
+ program = parser.parse(source)
234
+
235
+ assert len(parser.errors) == 0, f"Parser errors: {parser.errors}"
236
+ assert_program_statements(parser, program)
237
+
238
+ statement = program.statements[0]
239
+ assert isinstance(statement, ExpressionStatement)
240
+ assert statement.expression is not None
241
+
242
+ assert_infix_expression(statement.expression, left, operator, right)
243
+
244
+ @pytest.mark.parametrize(
245
+ "source,left,operator,right",
246
+ [
247
+ # Logical AND
248
+ ("Yes and Yes", True, "and", True),
249
+ ("Yes and No", True, "and", False),
250
+ ("No and Yes", False, "and", True),
251
+ ("No and No", False, "and", False),
252
+ # Logical OR
253
+ ("Yes or Yes", True, "or", True),
254
+ ("Yes or No", True, "or", False),
255
+ ("No or Yes", False, "or", True),
256
+ ("No or No", False, "or", False),
257
+ # Case variations
258
+ ("yes AND no", True, "and", False),
259
+ ("YES And NO", True, "and", False),
260
+ ("yes OR no", True, "or", False),
261
+ ("YES Or NO", True, "or", False),
262
+ # With identifiers
263
+ ("x and z", "x", "and", "z"),
264
+ ("foo or bar", "foo", "or", "bar"),
265
+ ("`is valid` and `has permission`", "is valid", "and", "has permission"),
266
+ # Mixed with literals
267
+ ("x and Yes", "x", "and", True),
268
+ ("No or z", False, "or", "z"),
269
+ # With underscores
270
+ ("_Yes_ and _No_", True, "and", False),
271
+ ("_x_ or _y_", "_x_", "or", "_y_"),
272
+ ],
273
+ )
274
+ def test_logical_operators(self, source: str, left: bool | str, operator: str, right: bool | str) -> None:
275
+ """Test parsing logical operator expressions (and, or).
276
+
277
+ Args:
278
+ source: The source code containing a logical expression.
279
+ left: Expected left operand value.
280
+ operator: Expected logical operator ('and' or 'or').
281
+ right: Expected right operand value.
282
+ """
283
+ parser = Parser()
284
+
285
+ program = parser.parse(source)
286
+
287
+ assert len(parser.errors) == 0, f"Parser errors: {parser.errors}"
288
+ assert_program_statements(parser, program)
289
+
290
+ statement = program.statements[0]
291
+ assert isinstance(statement, ExpressionStatement)
292
+ assert statement.expression is not None
293
+
294
+ assert_infix_expression(statement.expression, left, operator.lower(), right)
295
+
296
+ def test_natural_language_comparison_operators(self) -> None:
297
+ """Test parsing natural language comparison operators."""
298
+ test_cases = [
299
+ # Equality variations
300
+ ("5 is equal to 5", 5, "equals", 5),
301
+ ("x is equal to z", "x", "equals", "z"),
302
+ ("10 is the same as 10", 10, "equals", 10),
303
+ ("foo is the same as bar", "foo", "equals", "bar"),
304
+ ("3.14 equals 3.14", 3.14, "equals", 3.14),
305
+ ("`value` equals 42", "value", "equals", 42),
306
+ # Inequality variations
307
+ ("5 is not 10", 5, "is not", 10),
308
+ ("x is not z", "x", "is not", "z"),
309
+ ("5 isn't 10", 5, "is not", 10),
310
+ ("`value` isn't 0", "value", "is not", 0),
311
+ ("10 is not equal to 20", 10, "is not", 20),
312
+ ("foo is not equal to bar", "foo", "is not", "bar"),
313
+ ("5 doesn't equal 10", 5, "is not", 10),
314
+ ("result doesn't equal expected", "result", "is not", "expected"),
315
+ ("7 is different from 8", 7, "is not", 8),
316
+ ("actual is different from expected", "actual", "is not", "expected"),
317
+ # Greater than variations
318
+ ("10 is greater than 5", 10, ">", 5),
319
+ ("x is greater than 0", "x", ">", 0),
320
+ ("20 is more than 10", 20, ">", 10),
321
+ ("total is more than limit", "total", ">", "limit"),
322
+ # Less than variations
323
+ ("5 is less than 10", 5, "<", 10),
324
+ ("`value` is less than max", "value", "<", "max"),
325
+ ("3 is under 10", 3, "<", 10),
326
+ ("price is under budget", "price", "<", "budget"),
327
+ ("2 is fewer than 5", 2, "<", 5),
328
+ ("errors is fewer than threshold", "errors", "<", "threshold"),
329
+ # Greater than or equal variations
330
+ ("10 is greater than or equal to 10", 10, ">=", 10),
331
+ ("x is greater than or equal to min", "x", ">=", "min"),
332
+ ("5 is at least 5", 5, ">=", 5),
333
+ ("score is at least passing", "score", ">=", "passing"),
334
+ ("10 is no less than 5", 10, ">=", 5),
335
+ ("`value` is no less than minimum", "value", ">=", "minimum"),
336
+ # Less than or equal variations
337
+ ("5 is less than or equal to 10", 5, "<=", 10),
338
+ ("x is less than or equal to max", "x", "<=", "max"),
339
+ ("10 is at most 10", 10, "<=", 10),
340
+ ("cost is at most budget", "cost", "<=", "budget"),
341
+ ("5 is no more than 10", 5, "<=", 10),
342
+ ("usage is no more than limit", "usage", "<=", "limit"),
343
+ # Mixed with identifiers and literals
344
+ ("`total cost` is equal to 100.50", "total cost", "equals", 100.50),
345
+ ("`error count` is less than 5", "error count", "<", 5),
346
+ ("Yes is not No", True, "is not", False),
347
+ ("_42_ equals _42_", 42, "equals", 42),
348
+ ]
349
+
350
+ for source, left_value, expected_operator, right_value in test_cases:
351
+ parser = Parser()
352
+
353
+ program = parser.parse(source)
354
+
355
+ assert len(parser.errors) == 0, f"Parser errors for '{source}': {parser.errors}"
356
+ assert_program_statements(parser, program)
357
+
358
+ statement = program.statements[0]
359
+ assert isinstance(statement, ExpressionStatement)
360
+ assert statement.expression is not None
361
+
362
+ assert_infix_expression(statement.expression, left_value, expected_operator, right_value)
363
+
364
+ def test_natural_language_operators_in_complex_expressions(self) -> None:
365
+ """Test natural language operators in complex expressions with precedence."""
366
+ test_cases = [
367
+ # With logical operators
368
+ ("x is equal to 5 and y is greater than 10", "((x equals 5) and (y > 10))"),
369
+ ("foo is not bar or baz is less than qux", "((foo is not bar) or (baz < qux))"),
370
+ ("`value` is at least 0 and `value` is at most 100", "((value >= 0) and (value <= 100))"),
371
+ # With arithmetic
372
+ ("x + 5 is equal to 10", "((x + 5) equals 10)"),
373
+ ("2 * y is greater than 20", "((2 * y) > 20)"),
374
+ ("total / count is less than average", "((total / count) < average)"),
375
+ # With parentheses
376
+ ("(x is equal to 5) and (y is not 10)", "((x equals 5) and (y is not 10))"),
377
+ ("not (x is greater than 10)", "(not (x > 10))"),
378
+ # Nested comparisons
379
+ ("x is greater than y and y is greater than z", "((x > y) and (y > z))"),
380
+ ("score is at least passing or retake is equal to True", "((score >= passing) or (retake equals True))"),
381
+ ]
382
+
383
+ for source, _ in test_cases:
384
+ parser = Parser()
385
+
386
+ program = parser.parse(source)
387
+
388
+ assert len(parser.errors) == 0, f"Parser errors for '{source}': {parser.errors}"
389
+ assert len(program.statements) == 1
390
+
391
+ statement = program.statements[0]
392
+ assert isinstance(statement, ExpressionStatement)
393
+ assert statement.expression is not None
394
+
395
+ # For now, just ensure it parses without errors
396
+ # The exact string representation would depend on how we format natural language operators
397
+
398
+ def test_operator_precedence(self) -> None:
399
+ """Test that operators follow correct precedence rules."""
400
+ # Test cases with expected parsing based on precedence
401
+ test_cases = [
402
+ # Multiplication before addition
403
+ ("5 + 2 * 3", "(_5_ + (_2_ * _3_))"),
404
+ ("2 * 3 + 5", "((_2_ * _3_) + _5_)"),
405
+ # Division before subtraction
406
+ ("10 - 6 / 2", "(_10_ - (_6_ / _2_))"),
407
+ ("6 / 2 - 1", "((_6_ / _2_) - _1_)"),
408
+ # Same precedence operators are left-associative
409
+ ("5 - 3 - 1", "((_5_ - _3_) - _1_)"),
410
+ ("10 / 5 / 2", "((_10_ / _5_) / _2_)"),
411
+ # Complex expressions
412
+ ("1 + 2 * 3 + 4", "((_1_ + (_2_ * _3_)) + _4_)"),
413
+ ("5 + 6 * 7 - 8 / 2", "((_5_ + (_6_ * _7_)) - (_8_ / _2_))"),
414
+ # Comparison operators have lower precedence than arithmetic
415
+ ("5 + 3 equals 8", "((_5_ + _3_) equals _8_)"),
416
+ ("2 * 3 < 10", "((_2_ * _3_) < _10_)"),
417
+ ("10 / 2 > 4", "((_10_ / _2_) > _4_)"),
418
+ ("3 + 2 <= 5", "((_3_ + _2_) <= _5_)"),
419
+ ("8 - 3 >= 5", "((_8_ - _3_) >= _5_)"),
420
+ # Logical operators have lowest precedence
421
+ ("Yes and No or Yes", "((_Yes_ and _No_) or _Yes_)"),
422
+ ("Yes or No and Yes", "(_Yes_ or (_No_ and _Yes_))"),
423
+ ("5 > 3 and 10 < 20", "((_5_ > _3_) and (_10_ < _20_))"),
424
+ ("x equals z or p is not q", "((`x` equals `z`) or (`p` is not `q`))"),
425
+ # Mixed precedence with logical operators
426
+ ("5 + 3 > 7 and 2 * 3 equals 6", "(((_5_ + _3_) > _7_) and ((_2_ * _3_) equals _6_))"),
427
+ ("not x equals z and w > 0", "(((not `x`) equals `z`) and (`w` > _0_))"),
428
+ ]
429
+
430
+ for source, expected in test_cases:
431
+ parser = Parser()
432
+ program = parser.parse(source)
433
+
434
+ assert len(parser.errors) == 0, f"Parser errors for '{source}': {parser.errors}"
435
+ assert len(program.statements) == 1
436
+
437
+ statement = program.statements[0]
438
+ assert isinstance(statement, ExpressionStatement)
439
+ assert statement.expression is not None
440
+
441
+ # Check string representation matches expected precedence
442
+ assert str(statement.expression) == expected, (
443
+ f"For '{source}': expected {expected}, got {statement.expression!s}"
444
+ )
445
+
446
+ def test_grouped_expressions(self) -> None:
447
+ """Test parsing expressions with parentheses for grouping."""
448
+ test_cases = [
449
+ # Parentheses override precedence
450
+ ("(5 + 2) * 3", "((_5_ + _2_) * _3_)"),
451
+ ("3 * (5 + 2)", "(_3_ * (_5_ + _2_))"),
452
+ ("(10 - 6) / 2", "((_10_ - _6_) / _2_)"),
453
+ ("2 / (10 - 6)", "(_2_ / (_10_ - _6_))"),
454
+ # Nested parentheses
455
+ ("((5 + 2) * 3) + 4", "(((_5_ + _2_) * _3_) + _4_)"),
456
+ ("5 + ((2 * 3) + 4)", "(_5_ + ((_2_ * _3_) + _4_))"),
457
+ # Complex grouped expressions
458
+ ("(2 + 3) * (4 + 5)", "((_2_ + _3_) * (_4_ + _5_))"),
459
+ ("((1 + 2) * 3) / (4 - 2)", "(((_1_ + _2_) * _3_) / (_4_ - _2_))"),
460
+ # Logical operators with parentheses
461
+ ("(Yes or No) and Yes", "((_Yes_ or _No_) and _Yes_)"),
462
+ ("Yes and (No or Yes)", "(_Yes_ and (_No_ or _Yes_))"),
463
+ ("(No and Yes) or No", "((_No_ and _Yes_) or _No_)"),
464
+ ("No or (Yes and No)", "(_No_ or (_Yes_ and _No_))"),
465
+ # Complex logical expressions with parentheses
466
+ ("(x or z) and (p or q)", "((`x` or `z`) and (`p` or `q`))"),
467
+ ("(foo and bar) or (baz and qux)", "((`foo` and `bar`) or (`baz` and `qux`))"),
468
+ ("not (x and z)", "(not (`x` and `z`))"),
469
+ ("not (x or z)", "(not (`x` or `z`))"),
470
+ # Mixed logical and comparison with parentheses
471
+ ("(x > 5) and (y < 10)", "((`x` > _5_) and (`y` < _10_))"),
472
+ ("(foo equals bar) or (baz is not qux)", "((`foo` equals `bar`) or (`baz` is not `qux`))"),
473
+ ("(5 > 3) and (10 < 20 or 15 equals 15)", "((_5_ > _3_) and ((_10_ < _20_) or (_15_ equals _15_)))"),
474
+ # Deeply nested logical expressions
475
+ ("((x or z) and p) or q", "(((`x` or `z`) and `p`) or `q`)"),
476
+ ("x or (z and (p or q))", "(`x` or (`z` and (`p` or `q`)))"),
477
+ (
478
+ "((Yes or No) and (No or Yes)) or No",
479
+ "(((_Yes_ or _No_) and (_No_ or _Yes_)) or _No_)",
480
+ ),
481
+ ]
482
+
483
+ for source, expected in test_cases:
484
+ parser = Parser()
485
+ program = parser.parse(source)
486
+
487
+ assert len(parser.errors) == 0, f"Parser errors for '{source}': {parser.errors}"
488
+ assert len(program.statements) == 1
489
+
490
+ statement = program.statements[0]
491
+ assert isinstance(statement, ExpressionStatement)
492
+ assert statement.expression is not None
493
+
494
+ assert str(statement.expression) == expected, (
495
+ f"For '{source}': expected {expected}, got {statement.expression!s}"
496
+ )
497
+
498
+ def test_complex_logical_with_comparison(self) -> None:
499
+ """Test parsing complex expressions with comparison and logical operators."""
500
+ source = "5 > 3 and Yes"
501
+ parser = Parser()
502
+
503
+ program = parser.parse(source)
504
+
505
+ assert len(parser.errors) == 0, f"Parser errors: {parser.errors}"
506
+ assert len(program.statements) == 1
507
+
508
+ statement = program.statements[0]
509
+ assert isinstance(statement, ExpressionStatement)
510
+ assert statement.expression is not None
511
+
512
+ # The expression should be an InfixExpression with 'and' operator
513
+ expr = statement.expression
514
+ assert isinstance(expr, InfixExpression)
515
+ assert expr.operator == "and"
516
+
517
+ # The left side should be the comparison (5 > 3)
518
+ assert expr.left is not None
519
+ assert isinstance(expr.left, InfixExpression)
520
+ left_expr = expr.left
521
+ assert left_expr.operator == ">"
522
+
523
+ # Check the comparison operands
524
+ assert left_expr.left is not None
525
+ assert isinstance(left_expr.left, WholeNumberLiteral)
526
+ assert left_expr.left.value == 5
527
+
528
+ assert left_expr.right is not None
529
+ assert isinstance(left_expr.right, WholeNumberLiteral)
530
+ assert left_expr.right.value == 3
531
+
532
+ # The right side should be True
533
+ assert expr.right is not None
534
+ assert isinstance(expr.right, YesNoLiteral)
535
+ assert expr.right.value is True
536
+
537
+ # Check the string representation
538
+ assert str(statement.expression) == "((_5_ > _3_) and _Yes_)"
539
+
540
+ def test_mixed_prefix_and_infix_expressions(self) -> None:
541
+ """Test parsing expressions that combine prefix and infix operators."""
542
+ test_cases = [
543
+ # Negative numbers in arithmetic
544
+ ("-5 + 10", "((-_5_) + _10_)"),
545
+ ("10 + -5", "(_10_ + (-_5_))"),
546
+ ("-5 * -5", "((-_5_) * (-_5_))"),
547
+ ("-10 / 2", "((-_10_) / _2_)"),
548
+ # Boolean negation with comparisons
549
+ ("not x equals z", "((not `x`) equals `z`)"),
550
+ ("not 5 < 10", "((not _5_) < _10_)"),
551
+ ("not Yes equals No", "((not _Yes_) equals _No_)"),
552
+ # Complex mixed expressions
553
+ ("-x + z * -w", "((-`x`) + (`z` * (-`w`)))"),
554
+ ("not p equals q and r > v", "(((not `p`) equals `q`) and (`r` > `v`))"),
555
+ ]
556
+
557
+ for source, expected in test_cases:
558
+ parser = Parser()
559
+ program = parser.parse(source)
560
+
561
+ assert len(parser.errors) == 0, f"Parser errors for '{source}': {parser.errors}"
562
+ assert len(program.statements) == 1
563
+
564
+ statement = program.statements[0]
565
+ assert isinstance(statement, ExpressionStatement)
566
+ assert statement.expression is not None
567
+
568
+ assert str(statement.expression) == expected, (
569
+ f"For '{source}': expected {expected}, got {statement.expression!s}"
570
+ )
571
+
572
+ def test_multiple_infix_expressions(self) -> None:
573
+ """Test parsing multiple infix expressions in sequence."""
574
+ source = "5 + 5. 10 - 2. 3 * 4. 8 / 2."
575
+ parser = Parser()
576
+
577
+ program = parser.parse(source)
578
+
579
+ assert len(parser.errors) == 0
580
+ assert len(program.statements) == 4
581
+
582
+ # First: 5 + 5
583
+ statement = program.statements[0]
584
+ assert isinstance(statement, ExpressionStatement)
585
+ assert statement.expression is not None
586
+ assert_infix_expression(statement.expression, 5, "+", 5)
587
+
588
+ # Second: 10 - 2
589
+ statement = program.statements[1]
590
+ assert isinstance(statement, ExpressionStatement)
591
+ assert statement.expression is not None
592
+ assert_infix_expression(statement.expression, 10, "-", 2)
593
+
594
+ # Third: 3 * 4
595
+ statement = program.statements[2]
596
+ assert isinstance(statement, ExpressionStatement)
597
+ assert statement.expression is not None
598
+ assert_infix_expression(statement.expression, 3, "*", 4)
599
+
600
+ # Fourth: 8 / 2
601
+ statement = program.statements[3]
602
+ assert isinstance(statement, ExpressionStatement)
603
+ assert statement.expression is not None
604
+ assert_infix_expression(statement.expression, 8, "/", 2)
605
+
606
+ def test_infix_expression_string_representation(self) -> None:
607
+ """Test the string representation of infix expressions."""
608
+ test_cases = [
609
+ # Basic arithmetic
610
+ ("5 + 5", "(_5_ + _5_)"),
611
+ ("10 - 5", "(_10_ - _5_)"),
612
+ ("3 * 4", "(_3_ * _4_)"),
613
+ ("10 / 2", "(_10_ / _2_)"),
614
+ # Comparisons
615
+ ("5 equals 5", "(_5_ equals _5_)"),
616
+ ("10 is not 5", "(_10_ is not _5_)"),
617
+ ("3 < 4", "(_3_ < _4_)"),
618
+ ("10 > 2", "(_10_ > _2_)"),
619
+ # With identifiers
620
+ ("x + z", "(`x` + `z`)"),
621
+ ("foo equals bar", "(`foo` equals `bar`)"),
622
+ # Complex expressions
623
+ ("5 + 2 * 3", "(_5_ + (_2_ * _3_))"),
624
+ ("-5 + 10", "((-_5_) + _10_)"),
625
+ ]
626
+
627
+ for source, expected in test_cases:
628
+ parser = Parser()
629
+ program = parser.parse(source)
630
+
631
+ assert len(parser.errors) == 0
632
+ assert len(program.statements) == 1
633
+
634
+ statement = program.statements[0]
635
+ assert isinstance(statement, ExpressionStatement)
636
+ assert statement.expression is not None
637
+
638
+ assert str(statement.expression) == expected
639
+
640
+ @pytest.mark.parametrize(
641
+ "source,expected_error",
642
+ [
643
+ # Missing right operand
644
+ ("5 +", "expected expression, got <end-of-file>"),
645
+ ("10 -", "expected expression, got <end-of-file>"),
646
+ ("x *", "expected expression, got <end-of-file>"),
647
+ # Missing left operand (these would be parsed as prefix expressions or cause errors)
648
+ ("+ 5", "unexpected token '+' at start of expression"),
649
+ ("* 10", "unexpected token '*' at start of expression"),
650
+ ("/ 2", "unexpected token '/' at start of expression"),
651
+ # Invalid operator combinations
652
+ ("5 ++ 5", "unexpected token '+' at start of expression"),
653
+ # Missing operands in complex expressions
654
+ ("5 + * 3", "unexpected token '*' at start of expression"),
655
+ ("(5 + ) * 3", "No suitable parse function was found to handle ')'"),
656
+ # Natural language operator errors
657
+ ("x is equal to", "expected expression, got <end-of-file>"),
658
+ ("is greater than 5", "unexpected token 'is greater than' at start of expression"),
659
+ ("5 is", "No suitable parse function was found to handle 'is'"),
660
+ ],
661
+ )
662
+ def test_invalid_infix_expressions(self, source: str, expected_error: str) -> None:
663
+ """Test that invalid infix expressions produce appropriate errors.
664
+
665
+ Args:
666
+ source: The invalid source code.
667
+ expected_error: Expected error message substring.
668
+ """
669
+ parser = Parser()
670
+
671
+ parser.parse(source)
672
+
673
+ # Should have at least one error
674
+ assert len(parser.errors) > 0, f"Expected errors for '{source}', but got none"
675
+
676
+ # Check that at least one error contains the expected message
677
+ error_messages = [str(error) for error in parser.errors]
678
+ assert any(expected_error in msg for msg in error_messages), (
679
+ f"Expected error containing '{expected_error}' for '{source}', but got: {error_messages}"
680
+ )