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,94 @@
1
+ """Tests to ensure parser never returns None in AST nodes.
2
+
3
+ When errors occur, the parser should always return ErrorExpression or
4
+ ErrorStatement nodes to preserve the AST structure, never None.
5
+ """
6
+
7
+ import pytest
8
+
9
+ from machine_dialect.ast import ErrorStatement, ExpressionStatement
10
+ from machine_dialect.parser import Parser
11
+
12
+
13
+ class TestNoNoneInAST:
14
+ """Test that parser always creates AST nodes, never None."""
15
+
16
+ @pytest.mark.parametrize(
17
+ "source",
18
+ [
19
+ "(42", # Missing closing parenthesis
20
+ "((42)", # Missing one closing parenthesis
21
+ "()", # Empty parentheses
22
+ "( + 42)", # Operator at start inside parens
23
+ "Set x 42.", # Missing 'to' keyword
24
+ "Set @ to 42.", # Illegal character as identifier
25
+ "42 + @ + 5.", # Illegal character in expression
26
+ ],
27
+ )
28
+ def test_errors_produce_error_nodes_not_none(self, source: str) -> None:
29
+ """Test that parsing errors result in Error nodes, not None."""
30
+ parser = Parser()
31
+ program = parser.parse(source)
32
+
33
+ # Should have at least one error
34
+ assert parser.has_errors()
35
+
36
+ # Should have at least one statement
37
+ assert len(program.statements) > 0
38
+
39
+ # Check all statements are not None
40
+ for stmt in program.statements:
41
+ assert stmt is not None
42
+
43
+ # If it's an expression statement, check the expression
44
+ if isinstance(stmt, ExpressionStatement):
45
+ assert stmt.expression is not None
46
+ # Many of these should be ErrorExpressions
47
+ if parser.has_errors() and not isinstance(stmt, ErrorStatement):
48
+ # If there were errors and it's not an ErrorStatement,
49
+ # the expression might be an ErrorExpression
50
+ pass # This is fine, we just care it's not None
51
+
52
+ def test_invalid_float_produces_error_expression(self) -> None:
53
+ """Test that invalid float literals produce ErrorExpression."""
54
+ # This would need to bypass the lexer somehow to test the parser directly
55
+ # Since the lexer validates floats, this is hard to test in integration
56
+ # We trust that our changes to _parse_float_literal work
57
+ pass
58
+
59
+ def test_invalid_integer_produces_error_expression(self) -> None:
60
+ """Test that invalid integer literals produce ErrorExpression."""
61
+ # This would need to bypass the lexer somehow to test the parser directly
62
+ # Since the lexer validates integers, this is hard to test in integration
63
+ # We trust that our changes to _parse_integer_literal work
64
+ pass
65
+
66
+ def test_nested_errors_still_create_nodes(self) -> None:
67
+ """Test that nested parsing errors still create AST nodes."""
68
+ source = "Set x to (42 + (5 * @))."
69
+ parser = Parser()
70
+ program = parser.parse(source)
71
+
72
+ # Should have errors
73
+ assert parser.has_errors()
74
+
75
+ # Should have a statement
76
+ assert len(program.statements) == 1
77
+ stmt = program.statements[0]
78
+ assert stmt is not None
79
+
80
+ def test_multiple_grouped_expression_errors(self) -> None:
81
+ """Test multiple grouped expression errors."""
82
+ source = "(42 + (5 * 3" # Missing two closing parens
83
+ parser = Parser()
84
+ program = parser.parse(source)
85
+
86
+ # Should have error(s)
87
+ assert parser.has_errors()
88
+
89
+ # Should have a statement with an expression
90
+ assert len(program.statements) == 1
91
+ stmt = program.statements[0]
92
+ assert stmt is not None
93
+ if isinstance(stmt, ExpressionStatement):
94
+ assert stmt.expression is not None
@@ -0,0 +1,171 @@
1
+ """Tests for parser panic mode recovery.
2
+
3
+ This module tests the parser's panic mode recovery mechanism which allows
4
+ the parser to recover from errors and continue parsing to find more errors.
5
+ """
6
+
7
+ from machine_dialect.errors.exceptions import MDSyntaxError
8
+ from machine_dialect.parser import Parser
9
+
10
+
11
+ class TestPanicModeRecovery:
12
+ """Test panic mode recovery in the parser."""
13
+
14
+ def test_recovery_at_period_boundary(self) -> None:
15
+ """Test that panic mode stops at period boundaries."""
16
+ source = "Define `x` as Whole Number. Set @ to 42. Set `x` to 5."
17
+ parser = Parser()
18
+
19
+ program = parser.parse(source)
20
+
21
+ # Should have one error for the illegal @ character
22
+ assert len(parser.errors) == 1
23
+ assert isinstance(parser.errors[0], MDSyntaxError)
24
+ assert "@" in str(parser.errors[0])
25
+
26
+ # Should have parsed the statements successfully
27
+ assert len(program.statements) == 3 # Define + 2 Sets
28
+ # First statement should be Define
29
+ assert program.statements[0] is not None
30
+ # Second statement should be incomplete (error due to @)
31
+ assert program.statements[1] is not None
32
+ # Third statement should be complete
33
+ assert program.statements[2] is not None
34
+
35
+ def test_recovery_at_eof_boundary(self) -> None:
36
+ """Test that panic mode stops at EOF."""
37
+ source = "Set @ to 42" # No period at end
38
+ parser = Parser()
39
+
40
+ program = parser.parse(source)
41
+
42
+ # Should have one error for the illegal @ character
43
+ assert len(parser.errors) == 1
44
+ assert isinstance(parser.errors[0], MDSyntaxError)
45
+
46
+ # Should have one statement (incomplete due to error)
47
+ assert len(program.statements) == 1
48
+
49
+ def test_multiple_errors_with_recovery(self) -> None:
50
+ """Test that multiple errors are collected with recovery between them."""
51
+ source = "Define `x` as Whole Number. Set @ to 42. Set # to 10. Set `x` to 5."
52
+ parser = Parser()
53
+
54
+ program = parser.parse(source)
55
+
56
+ # Should have two errors for the illegal characters
57
+ assert len(parser.errors) == 2
58
+ assert "@" in str(parser.errors[0])
59
+ assert "#" in str(parser.errors[1])
60
+ # @ is now MDSyntaxError (illegal token), # remains MDNameError (unexpected identifier)
61
+
62
+ # Should have four statements (Define + 3 Sets)
63
+ assert len(program.statements) == 4
64
+
65
+ def test_recovery_in_expression_context(self) -> None:
66
+ """Test panic recovery when error occurs in expression parsing."""
67
+ source = "Define `x` as Whole Number. Define `y` as Whole Number. Set `x` to @ + 5. Set `y` to 10."
68
+ parser = Parser()
69
+
70
+ program = parser.parse(source)
71
+
72
+ # Should have one error for the illegal @ in expression
73
+ assert len(parser.errors) == 1
74
+ assert isinstance(parser.errors[0], MDSyntaxError)
75
+
76
+ # Should have four statements (2 defines + 2 sets)
77
+ assert len(program.statements) == 4
78
+
79
+ def test_recovery_with_missing_keyword(self) -> None:
80
+ """Test panic recovery when 'to' keyword is missing."""
81
+ source = "Define `x` as Whole Number. Define `y` as Whole Number. Set `x` 42. Set `y` to 10."
82
+ parser = Parser()
83
+
84
+ program = parser.parse(source)
85
+
86
+ # Should have one error for missing 'to'
87
+ assert len(parser.errors) == 1
88
+ assert isinstance(parser.errors[0], MDSyntaxError)
89
+
90
+ # Should have four statements (2 defines + 2 sets)
91
+ assert len(program.statements) == 4
92
+
93
+ def test_recovery_with_consecutive_errors(self) -> None:
94
+ """Test panic recovery with errors in consecutive statements."""
95
+ source = "Set @ to 42. Set # to 10."
96
+ parser = Parser()
97
+
98
+ program = parser.parse(source)
99
+
100
+ # Should have two errors
101
+ assert len(parser.errors) == 2
102
+
103
+ # Should have two statements (both incomplete)
104
+ assert len(program.statements) == 2
105
+
106
+ def test_panic_counter_limit(self) -> None:
107
+ """Test that panic counter prevents infinite loops."""
108
+ # Create a source with many errors (more than 20)
109
+ statements = [f"Set @{i} to {i}." for i in range(25)]
110
+ source = " ".join(statements)
111
+
112
+ parser = Parser()
113
+
114
+ program = parser.parse(source)
115
+
116
+ # Parser should stop after 20 panic recoveries
117
+ # We might have fewer errors if parser stops early
118
+ assert len(parser.errors) <= 20
119
+ assert len(program.statements) <= 20
120
+
121
+ def test_recovery_preserves_valid_parts(self) -> None:
122
+ """Test that valid parts of statements are preserved during recovery."""
123
+ source = "give back @. give back 42."
124
+ parser = Parser()
125
+
126
+ program = parser.parse(source)
127
+
128
+ # Should have one error for illegal @
129
+ assert len(parser.errors) == 1
130
+ assert isinstance(parser.errors[0], MDSyntaxError)
131
+
132
+ # Should have two return statements
133
+ assert len(program.statements) == 2
134
+ # First should be incomplete, second should be complete
135
+ from machine_dialect.ast import ReturnStatement
136
+
137
+ if isinstance(program.statements[1], ReturnStatement):
138
+ assert program.statements[1].return_value is not None
139
+
140
+ def test_no_recovery_for_valid_code(self) -> None:
141
+ """Test that valid code doesn't trigger panic recovery."""
142
+ source = """Define `x` as Whole Number.
143
+ Define `y` as Whole Number.
144
+ Set `x` to _42_.
145
+ Set `y` to _10_.
146
+ give back `x`."""
147
+ parser = Parser()
148
+
149
+ program = parser.parse(source)
150
+
151
+ # Should have no errors
152
+ assert len(parser.errors) == 0
153
+
154
+ # Should have five complete statements (2 defines + 2 sets + 1 return)
155
+ assert len(program.statements) == 5
156
+
157
+ def test_recovery_with_mixed_statement_types(self) -> None:
158
+ """Test panic recovery across different statement types."""
159
+ source = """Define `x` as Whole Number.
160
+ Set @ to 42.
161
+ give back #.
162
+ Set `x` to _5_."""
163
+ parser = Parser()
164
+
165
+ program = parser.parse(source)
166
+
167
+ # Should have two errors (@ is illegal, # is illegal)
168
+ assert len(parser.errors) == 2
169
+
170
+ # Should have four statements (1 define + 3 attempts)
171
+ assert len(program.statements) == 4
@@ -0,0 +1,114 @@
1
+ """Tests for parser error handling.
2
+
3
+ This module tests the parser's ability to properly report errors
4
+ when encountering invalid syntax or missing parse functions.
5
+ """
6
+
7
+ import pytest
8
+
9
+ from machine_dialect.errors.exceptions import MDSyntaxError
10
+ from machine_dialect.parser import Parser
11
+
12
+
13
+ class TestParseErrors:
14
+ """Test parsing error handling."""
15
+
16
+ @pytest.mark.parametrize(
17
+ "source,expected_literal,expected_message",
18
+ [
19
+ ("* 42", "*", "unexpected token '*' at start of expression"), # Multiplication operator at start
20
+ ("+ 5", "+", "unexpected token '+' at start of expression"), # Plus operator at start
21
+ ("/ 10", "/", "unexpected token '/' at start of expression"), # Division operator at start
22
+ (") x", ")", "No suitable parse function was found to handle ')'"), # Right parenthesis at start
23
+ ("} x", "}", "No suitable parse function was found to handle '}'"), # Right brace at start
24
+ (", x", ",", "No suitable parse function was found to handle ','"), # Comma at start
25
+ ("; x", ";", "No suitable parse function was found to handle ';'"), # Semicolon at start
26
+ ],
27
+ )
28
+ def test_no_prefix_parse_function_error(self, source: str, expected_literal: str, expected_message: str) -> None:
29
+ """Test error reporting when no prefix parse function exists.
30
+
31
+ Args:
32
+ source: The source code that should trigger an error.
33
+ expected_literal: The literal that should appear in the error message.
34
+ expected_message: The expected error message.
35
+ """
36
+ parser = Parser()
37
+
38
+ program = parser.parse(source, check_semantics=False)
39
+
40
+ # Should have exactly one error
41
+ assert len(parser.errors) == 1
42
+
43
+ # Check the error type and message
44
+ error = parser.errors[0]
45
+ assert isinstance(error, MDSyntaxError)
46
+ assert expected_message in str(error)
47
+
48
+ # The program should still be created
49
+ assert program is not None
50
+ # Parser continues after errors, so we'll have statements with ErrorExpression
51
+ # Check that the first statement has an error expression
52
+ if program.statements:
53
+ from machine_dialect.ast import ErrorExpression, ExpressionStatement
54
+
55
+ stmt = program.statements[0]
56
+ assert isinstance(stmt, ExpressionStatement)
57
+ assert isinstance(stmt.expression, ErrorExpression)
58
+
59
+ def test_multiple_parse_errors(self) -> None:
60
+ """Test that multiple parse errors are collected."""
61
+ source = "* 42. + 5. / 10."
62
+ parser = Parser()
63
+
64
+ parser.parse(source, check_semantics=False)
65
+
66
+ # Should have three errors (one for each invalid prefix)
67
+ assert len(parser.errors) == 3
68
+
69
+ # Check the error messages
70
+ expected_messages = [
71
+ "unexpected token '*' at start of expression",
72
+ "unexpected token '+' at start of expression",
73
+ "unexpected token '/' at start of expression",
74
+ ]
75
+
76
+ for error, expected_msg in zip(parser.errors, expected_messages, strict=True):
77
+ assert isinstance(error, MDSyntaxError)
78
+ assert expected_msg in str(error), f"Expected '{expected_msg}' in '{error!s}'"
79
+
80
+ def test_error_location_tracking(self) -> None:
81
+ """Test that errors track correct line and column positions."""
82
+ source = " * 42" # 3 spaces before *
83
+ parser = Parser()
84
+
85
+ _ = parser.parse(source, check_semantics=False)
86
+
87
+ assert len(parser.errors) == 1
88
+ error = parser.errors[0]
89
+
90
+ # The * should be at column 4 (1-indexed)
91
+ assert "column 4" in str(error)
92
+ assert "line 1" in str(error)
93
+
94
+ def test_valid_expression_no_errors(self) -> None:
95
+ """Test that valid expressions don't produce parse errors."""
96
+ valid_sources = [
97
+ "42",
98
+ "-42",
99
+ "not Yes",
100
+ "x",
101
+ "`my variable`",
102
+ "_123_",
103
+ "Yes",
104
+ "No",
105
+ ]
106
+
107
+ for source in valid_sources:
108
+ parser = Parser()
109
+
110
+ program = parser.parse(source, check_semantics=False)
111
+
112
+ # Should have no errors
113
+ assert len(parser.errors) == 0, f"Unexpected error for source: {source}"
114
+ assert len(program.statements) == 1
@@ -0,0 +1,182 @@
1
+ """Tests for possessive syntax parsing."""
2
+
3
+ from machine_dialect.ast.expressions import CollectionAccessExpression, ErrorExpression, Identifier
4
+ from machine_dialect.ast.statements import IfStatement, SetStatement
5
+ from machine_dialect.lexer.lexer import Lexer
6
+ from machine_dialect.lexer.tokens import TokenType
7
+ from machine_dialect.parser.parser import Parser
8
+
9
+
10
+ class TestPossessiveSyntax:
11
+ """Test parsing of possessive property access syntax."""
12
+
13
+ def test_lexer_recognizes_possessive(self) -> None:
14
+ """Test that lexer properly tokenizes `person`'s."""
15
+ lexer = Lexer('`person`\'s "name"')
16
+
17
+ # First token should be the possessive token
18
+ token = lexer.next_token()
19
+ assert token.type == TokenType.PUNCT_APOSTROPHE_S
20
+ assert token.literal == "person"
21
+
22
+ # Next should be the property name as a string literal
23
+ token = lexer.next_token()
24
+ assert token.type == TokenType.LIT_TEXT
25
+ assert token.literal == '"name"'
26
+
27
+ def test_parse_possessive_property_access(self) -> None:
28
+ """Test parsing `person`'s _"name"_."""
29
+ source = 'Set `result` to `person`\'s _"name"_.'
30
+ parser = Parser()
31
+ program = parser.parse(source)
32
+
33
+ assert len(program.statements) == 1
34
+ stmt = program.statements[0]
35
+ assert stmt.__class__.__name__ == "SetStatement"
36
+ assert isinstance(stmt, SetStatement) # Type assertion for MyPy
37
+
38
+ # Check the value is a CollectionAccessExpression
39
+ expr = stmt.value
40
+ assert isinstance(expr, CollectionAccessExpression)
41
+ assert expr.access_type == "property"
42
+ assert isinstance(expr.collection, Identifier)
43
+ assert expr.collection.value == "person"
44
+ assert expr.accessor == "name"
45
+
46
+ def test_parse_possessive_in_set_statement(self) -> None:
47
+ """Test parsing Set `user` to `person`'s _"name"_."""
48
+ source = 'Set `user` to `person`\'s _"name"_.'
49
+ parser = Parser()
50
+ program = parser.parse(source)
51
+
52
+ assert len(program.statements) == 1
53
+ stmt = program.statements[0]
54
+ assert stmt.__class__.__name__ == "SetStatement"
55
+ assert isinstance(stmt, SetStatement) # Type assertion for MyPy
56
+
57
+ # Check the value is a CollectionAccessExpression
58
+ assert isinstance(stmt.value, CollectionAccessExpression)
59
+ assert stmt.value.access_type == "property"
60
+ coll = stmt.value.collection
61
+ assert isinstance(coll, Identifier)
62
+ assert coll.value == "person"
63
+ assert stmt.value.accessor == "name"
64
+
65
+ def test_parse_possessive_mutation(self) -> None:
66
+ """Test parsing Set `person`'s _"age"_ to _31_."""
67
+ source = 'Set `person`\'s _"age"_ to _31_.'
68
+ parser = Parser()
69
+ program = parser.parse(source)
70
+
71
+ assert len(program.statements) == 1
72
+ stmt = program.statements[0]
73
+
74
+ # This should parse as a collection mutation statement
75
+ # The parser should recognize this pattern and create appropriate AST
76
+ assert stmt is not None
77
+ # The implementation might vary - could be SetStatement with special handling
78
+ # or CollectionMutationStatement
79
+
80
+ def test_parse_multiple_possessive_access(self) -> None:
81
+ """Test parsing chained possessive access."""
82
+ source = 'Set `result` to `user`\'s _"email"_.'
83
+ parser = Parser()
84
+ program = parser.parse(source)
85
+
86
+ assert len(program.statements) == 1
87
+ stmt = program.statements[0]
88
+ assert isinstance(stmt, SetStatement) # Type assertion for MyPy
89
+ expr = stmt.value
90
+
91
+ assert isinstance(expr, CollectionAccessExpression)
92
+ assert expr.access_type == "property"
93
+ coll = expr.collection
94
+ assert isinstance(coll, Identifier)
95
+ assert coll.value == "user"
96
+ assert expr.accessor == "email"
97
+
98
+ def test_possessive_with_different_properties(self) -> None:
99
+ """Test possessive with various property names."""
100
+ test_cases = [
101
+ ('`config`\'s _"host"_', "config", "host"),
102
+ ('`user`\'s _"premium"_', "user", "premium"),
103
+ ('`settings`\'s _"timeout"_', "settings", "timeout"),
104
+ ('`person`\'s _"phone"_', "person", "phone"),
105
+ ]
106
+
107
+ parser = Parser()
108
+
109
+ for possessive_expr, dict_name, prop_name in test_cases:
110
+ source = f"Set `result` to {possessive_expr}."
111
+ program = parser.parse(source)
112
+
113
+ assert len(program.statements) == 1
114
+ stmt = program.statements[0]
115
+ assert isinstance(stmt, SetStatement) # Type assertion for MyPy
116
+ expr = stmt.value
117
+
118
+ assert isinstance(expr, CollectionAccessExpression)
119
+ assert expr.access_type == "property"
120
+ coll = expr.collection
121
+ assert isinstance(coll, Identifier)
122
+ assert coll.value == dict_name
123
+ assert expr.accessor == prop_name
124
+
125
+ def test_lexer_apostrophe_without_s(self) -> None:
126
+ """Test that regular apostrophes don't trigger possessive."""
127
+ lexer = Lexer("`don't`")
128
+
129
+ # Should be parsed as a regular identifier
130
+ token = lexer.next_token()
131
+ assert token.type == TokenType.MISC_IDENT
132
+ assert token.literal == "don't"
133
+
134
+ def test_lexer_apostrophe_s_without_backticks(self) -> None:
135
+ """Test that 's without backticks doesn't trigger possessive."""
136
+ lexer = Lexer('person\'s "name"')
137
+
138
+ # Should parse as regular identifier with contraction
139
+ token = lexer.next_token()
140
+ assert token.type == TokenType.MISC_IDENT
141
+ assert token.literal == "person's"
142
+
143
+ token = lexer.next_token()
144
+ # Now expecting a string literal
145
+ assert token.type == TokenType.LIT_TEXT
146
+ assert token.literal == '"name"'
147
+
148
+ def test_possessive_missing_property_name(self) -> None:
149
+ """Test error when property name is missing after possessive."""
150
+ source = "Set `result` to `person`'s."
151
+ parser = Parser()
152
+ program = parser.parse(source)
153
+
154
+ # Should have an error in parsing
155
+ assert len(program.statements) == 1
156
+ stmt = program.statements[0]
157
+ assert isinstance(stmt, SetStatement) # Type assertion for MyPy
158
+ expr = stmt.value
159
+
160
+ # The value should be an error expression
161
+ assert isinstance(expr, ErrorExpression)
162
+ assert "Expected string literal" in expr.message
163
+
164
+ def test_possessive_in_conditional(self) -> None:
165
+ """Test possessive in if statement condition."""
166
+ source = 'If `user`\'s _"premium"_ then:\n> Say _"Premium user"_.'
167
+ parser = Parser()
168
+ program = parser.parse(source)
169
+
170
+ assert len(program.statements) == 1
171
+ stmt = program.statements[0]
172
+ assert stmt.__class__.__name__ == "IfStatement"
173
+ assert isinstance(stmt, IfStatement) # Type assertion for MyPy
174
+
175
+ # Check the condition contains possessive access
176
+ condition = stmt.condition
177
+ assert isinstance(condition, CollectionAccessExpression)
178
+ assert condition.access_type == "property"
179
+ coll = condition.collection
180
+ assert isinstance(coll, Identifier)
181
+ assert coll.value == "user"
182
+ assert condition.accessor == "premium"