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.
- machine_dialect/__main__.py +667 -0
- machine_dialect/agent/__init__.py +5 -0
- machine_dialect/agent/agent.py +360 -0
- machine_dialect/ast/__init__.py +95 -0
- machine_dialect/ast/ast_node.py +35 -0
- machine_dialect/ast/call_expression.py +82 -0
- machine_dialect/ast/dict_extraction.py +60 -0
- machine_dialect/ast/expressions.py +439 -0
- machine_dialect/ast/literals.py +309 -0
- machine_dialect/ast/program.py +35 -0
- machine_dialect/ast/statements.py +1433 -0
- machine_dialect/ast/tests/test_ast_string_representation.py +62 -0
- machine_dialect/ast/tests/test_boolean_literal.py +29 -0
- machine_dialect/ast/tests/test_collection_hir.py +138 -0
- machine_dialect/ast/tests/test_define_statement.py +142 -0
- machine_dialect/ast/tests/test_desugar.py +541 -0
- machine_dialect/ast/tests/test_foreach_desugar.py +245 -0
- machine_dialect/cfg/__init__.py +6 -0
- machine_dialect/cfg/config.py +156 -0
- machine_dialect/cfg/examples.py +221 -0
- machine_dialect/cfg/generate_with_ai.py +187 -0
- machine_dialect/cfg/openai_generation.py +200 -0
- machine_dialect/cfg/parser.py +94 -0
- machine_dialect/cfg/tests/__init__.py +1 -0
- machine_dialect/cfg/tests/test_cfg_parser.py +252 -0
- machine_dialect/cfg/tests/test_config.py +188 -0
- machine_dialect/cfg/tests/test_examples.py +391 -0
- machine_dialect/cfg/tests/test_generate_with_ai.py +354 -0
- machine_dialect/cfg/tests/test_openai_generation.py +256 -0
- machine_dialect/codegen/__init__.py +5 -0
- machine_dialect/codegen/bytecode_module.py +89 -0
- machine_dialect/codegen/bytecode_serializer.py +300 -0
- machine_dialect/codegen/opcodes.py +101 -0
- machine_dialect/codegen/register_codegen.py +1996 -0
- machine_dialect/codegen/symtab.py +208 -0
- machine_dialect/codegen/tests/__init__.py +1 -0
- machine_dialect/codegen/tests/test_array_operations_codegen.py +295 -0
- machine_dialect/codegen/tests/test_bytecode_serializer.py +185 -0
- machine_dialect/codegen/tests/test_register_codegen_ssa.py +324 -0
- machine_dialect/codegen/tests/test_symtab.py +418 -0
- machine_dialect/codegen/vm_serializer.py +621 -0
- machine_dialect/compiler/__init__.py +18 -0
- machine_dialect/compiler/compiler.py +197 -0
- machine_dialect/compiler/config.py +149 -0
- machine_dialect/compiler/context.py +149 -0
- machine_dialect/compiler/phases/__init__.py +19 -0
- machine_dialect/compiler/phases/bytecode_optimization.py +90 -0
- machine_dialect/compiler/phases/codegen.py +40 -0
- machine_dialect/compiler/phases/hir_generation.py +39 -0
- machine_dialect/compiler/phases/mir_generation.py +86 -0
- machine_dialect/compiler/phases/optimization.py +110 -0
- machine_dialect/compiler/phases/parsing.py +39 -0
- machine_dialect/compiler/pipeline.py +143 -0
- machine_dialect/compiler/tests/__init__.py +1 -0
- machine_dialect/compiler/tests/test_compiler.py +568 -0
- machine_dialect/compiler/vm_runner.py +173 -0
- machine_dialect/errors/__init__.py +32 -0
- machine_dialect/errors/exceptions.py +369 -0
- machine_dialect/errors/messages.py +82 -0
- machine_dialect/errors/tests/__init__.py +0 -0
- machine_dialect/errors/tests/test_expected_token_errors.py +188 -0
- machine_dialect/errors/tests/test_name_errors.py +118 -0
- machine_dialect/helpers/__init__.py +0 -0
- machine_dialect/helpers/stopwords.py +225 -0
- machine_dialect/helpers/validators.py +30 -0
- machine_dialect/lexer/__init__.py +9 -0
- machine_dialect/lexer/constants.py +23 -0
- machine_dialect/lexer/lexer.py +907 -0
- machine_dialect/lexer/tests/__init__.py +0 -0
- machine_dialect/lexer/tests/helpers.py +86 -0
- machine_dialect/lexer/tests/test_apostrophe_identifiers.py +122 -0
- machine_dialect/lexer/tests/test_backtick_identifiers.py +140 -0
- machine_dialect/lexer/tests/test_boolean_literals.py +108 -0
- machine_dialect/lexer/tests/test_case_insensitive_keywords.py +188 -0
- machine_dialect/lexer/tests/test_comments.py +200 -0
- machine_dialect/lexer/tests/test_double_asterisk_keywords.py +127 -0
- machine_dialect/lexer/tests/test_lexer_position.py +113 -0
- machine_dialect/lexer/tests/test_list_tokens.py +282 -0
- machine_dialect/lexer/tests/test_stopwords.py +80 -0
- machine_dialect/lexer/tests/test_strict_equality.py +129 -0
- machine_dialect/lexer/tests/test_token.py +41 -0
- machine_dialect/lexer/tests/test_tokenization.py +294 -0
- machine_dialect/lexer/tests/test_underscore_literals.py +343 -0
- machine_dialect/lexer/tests/test_url_literals.py +169 -0
- machine_dialect/lexer/tokens.py +487 -0
- machine_dialect/linter/__init__.py +10 -0
- machine_dialect/linter/__main__.py +144 -0
- machine_dialect/linter/linter.py +154 -0
- machine_dialect/linter/rules/__init__.py +8 -0
- machine_dialect/linter/rules/base.py +112 -0
- machine_dialect/linter/rules/statement_termination.py +99 -0
- machine_dialect/linter/tests/__init__.py +1 -0
- machine_dialect/linter/tests/mdrules/__init__.py +0 -0
- machine_dialect/linter/tests/mdrules/test_md101_statement_termination.py +181 -0
- machine_dialect/linter/tests/test_linter.py +81 -0
- machine_dialect/linter/tests/test_rules.py +110 -0
- machine_dialect/linter/tests/test_violations.py +71 -0
- machine_dialect/linter/violations.py +51 -0
- machine_dialect/mir/__init__.py +69 -0
- machine_dialect/mir/analyses/__init__.py +20 -0
- machine_dialect/mir/analyses/alias_analysis.py +315 -0
- machine_dialect/mir/analyses/dominance_analysis.py +49 -0
- machine_dialect/mir/analyses/escape_analysis.py +286 -0
- machine_dialect/mir/analyses/loop_analysis.py +272 -0
- machine_dialect/mir/analyses/tests/test_type_analysis.py +736 -0
- machine_dialect/mir/analyses/type_analysis.py +448 -0
- machine_dialect/mir/analyses/use_def_chains.py +232 -0
- machine_dialect/mir/basic_block.py +385 -0
- machine_dialect/mir/dataflow.py +445 -0
- machine_dialect/mir/debug_info.py +208 -0
- machine_dialect/mir/hir_to_mir.py +1738 -0
- machine_dialect/mir/mir_dumper.py +366 -0
- machine_dialect/mir/mir_function.py +167 -0
- machine_dialect/mir/mir_instructions.py +1877 -0
- machine_dialect/mir/mir_interpreter.py +556 -0
- machine_dialect/mir/mir_module.py +225 -0
- machine_dialect/mir/mir_printer.py +480 -0
- machine_dialect/mir/mir_transformer.py +410 -0
- machine_dialect/mir/mir_types.py +367 -0
- machine_dialect/mir/mir_validation.py +455 -0
- machine_dialect/mir/mir_values.py +268 -0
- machine_dialect/mir/optimization_config.py +233 -0
- machine_dialect/mir/optimization_pass.py +251 -0
- machine_dialect/mir/optimization_pipeline.py +355 -0
- machine_dialect/mir/optimizations/__init__.py +84 -0
- machine_dialect/mir/optimizations/algebraic_simplification.py +733 -0
- machine_dialect/mir/optimizations/branch_prediction.py +372 -0
- machine_dialect/mir/optimizations/constant_propagation.py +634 -0
- machine_dialect/mir/optimizations/cse.py +398 -0
- machine_dialect/mir/optimizations/dce.py +288 -0
- machine_dialect/mir/optimizations/inlining.py +551 -0
- machine_dialect/mir/optimizations/jump_threading.py +487 -0
- machine_dialect/mir/optimizations/licm.py +405 -0
- machine_dialect/mir/optimizations/loop_unrolling.py +366 -0
- machine_dialect/mir/optimizations/strength_reduction.py +422 -0
- machine_dialect/mir/optimizations/tail_call.py +207 -0
- machine_dialect/mir/optimizations/tests/test_loop_unrolling.py +483 -0
- machine_dialect/mir/optimizations/type_narrowing.py +397 -0
- machine_dialect/mir/optimizations/type_specialization.py +447 -0
- machine_dialect/mir/optimizations/type_specific.py +906 -0
- machine_dialect/mir/optimize_mir.py +89 -0
- machine_dialect/mir/pass_manager.py +391 -0
- machine_dialect/mir/profiling/__init__.py +26 -0
- machine_dialect/mir/profiling/profile_collector.py +318 -0
- machine_dialect/mir/profiling/profile_data.py +372 -0
- machine_dialect/mir/profiling/profile_reader.py +272 -0
- machine_dialect/mir/profiling/profile_writer.py +226 -0
- machine_dialect/mir/register_allocation.py +302 -0
- machine_dialect/mir/reporting/__init__.py +17 -0
- machine_dialect/mir/reporting/optimization_reporter.py +314 -0
- machine_dialect/mir/reporting/report_formatter.py +289 -0
- machine_dialect/mir/ssa_construction.py +342 -0
- machine_dialect/mir/tests/__init__.py +1 -0
- machine_dialect/mir/tests/test_algebraic_associativity.py +204 -0
- machine_dialect/mir/tests/test_algebraic_complex_patterns.py +221 -0
- machine_dialect/mir/tests/test_algebraic_division.py +126 -0
- machine_dialect/mir/tests/test_algebraic_simplification.py +863 -0
- machine_dialect/mir/tests/test_basic_block.py +425 -0
- machine_dialect/mir/tests/test_branch_prediction.py +459 -0
- machine_dialect/mir/tests/test_call_lowering.py +168 -0
- machine_dialect/mir/tests/test_collection_lowering.py +604 -0
- machine_dialect/mir/tests/test_cross_block_constant_propagation.py +255 -0
- machine_dialect/mir/tests/test_custom_passes.py +166 -0
- machine_dialect/mir/tests/test_debug_info.py +285 -0
- machine_dialect/mir/tests/test_dict_extraction_lowering.py +192 -0
- machine_dialect/mir/tests/test_dictionary_lowering.py +299 -0
- machine_dialect/mir/tests/test_double_negation.py +231 -0
- machine_dialect/mir/tests/test_escape_analysis.py +233 -0
- machine_dialect/mir/tests/test_hir_to_mir.py +465 -0
- machine_dialect/mir/tests/test_hir_to_mir_complete.py +389 -0
- machine_dialect/mir/tests/test_hir_to_mir_simple.py +130 -0
- machine_dialect/mir/tests/test_inlining.py +435 -0
- machine_dialect/mir/tests/test_licm.py +472 -0
- machine_dialect/mir/tests/test_mir_dumper.py +313 -0
- machine_dialect/mir/tests/test_mir_instructions.py +445 -0
- machine_dialect/mir/tests/test_mir_module.py +860 -0
- machine_dialect/mir/tests/test_mir_printer.py +387 -0
- machine_dialect/mir/tests/test_mir_types.py +123 -0
- machine_dialect/mir/tests/test_mir_types_enhanced.py +132 -0
- machine_dialect/mir/tests/test_mir_validation.py +378 -0
- machine_dialect/mir/tests/test_mir_values.py +168 -0
- machine_dialect/mir/tests/test_one_based_indexing.py +202 -0
- machine_dialect/mir/tests/test_optimization_helpers.py +60 -0
- machine_dialect/mir/tests/test_optimization_pipeline.py +554 -0
- machine_dialect/mir/tests/test_optimization_reporter.py +318 -0
- machine_dialect/mir/tests/test_pass_manager.py +294 -0
- machine_dialect/mir/tests/test_pass_registration.py +64 -0
- machine_dialect/mir/tests/test_profiling.py +356 -0
- machine_dialect/mir/tests/test_register_allocation.py +307 -0
- machine_dialect/mir/tests/test_report_formatters.py +372 -0
- machine_dialect/mir/tests/test_ssa_construction.py +433 -0
- machine_dialect/mir/tests/test_tail_call.py +236 -0
- machine_dialect/mir/tests/test_type_annotated_instructions.py +192 -0
- machine_dialect/mir/tests/test_type_narrowing.py +277 -0
- machine_dialect/mir/tests/test_type_specialization.py +421 -0
- machine_dialect/mir/tests/test_type_specific_optimization.py +545 -0
- machine_dialect/mir/tests/test_type_specific_optimization_advanced.py +382 -0
- machine_dialect/mir/type_inference.py +368 -0
- machine_dialect/parser/__init__.py +12 -0
- machine_dialect/parser/enums.py +45 -0
- machine_dialect/parser/parser.py +3655 -0
- machine_dialect/parser/protocols.py +11 -0
- machine_dialect/parser/symbol_table.py +169 -0
- machine_dialect/parser/tests/__init__.py +0 -0
- machine_dialect/parser/tests/helper_functions.py +193 -0
- machine_dialect/parser/tests/test_action_statements.py +334 -0
- machine_dialect/parser/tests/test_boolean_literal_expressions.py +152 -0
- machine_dialect/parser/tests/test_call_statements.py +154 -0
- machine_dialect/parser/tests/test_call_statements_errors.py +187 -0
- machine_dialect/parser/tests/test_collection_mutations.py +264 -0
- machine_dialect/parser/tests/test_conditional_expressions.py +343 -0
- machine_dialect/parser/tests/test_define_integration.py +468 -0
- machine_dialect/parser/tests/test_define_statements.py +311 -0
- machine_dialect/parser/tests/test_dict_extraction.py +115 -0
- machine_dialect/parser/tests/test_empty_literal.py +155 -0
- machine_dialect/parser/tests/test_float_literal_expressions.py +163 -0
- machine_dialect/parser/tests/test_identifier_expressions.py +57 -0
- machine_dialect/parser/tests/test_if_empty_block.py +61 -0
- machine_dialect/parser/tests/test_if_statements.py +299 -0
- machine_dialect/parser/tests/test_illegal_tokens.py +86 -0
- machine_dialect/parser/tests/test_infix_expressions.py +680 -0
- machine_dialect/parser/tests/test_integer_literal_expressions.py +137 -0
- machine_dialect/parser/tests/test_interaction_statements.py +269 -0
- machine_dialect/parser/tests/test_list_literals.py +277 -0
- machine_dialect/parser/tests/test_no_none_in_ast.py +94 -0
- machine_dialect/parser/tests/test_panic_mode_recovery.py +171 -0
- machine_dialect/parser/tests/test_parse_errors.py +114 -0
- machine_dialect/parser/tests/test_possessive_syntax.py +182 -0
- machine_dialect/parser/tests/test_prefix_expressions.py +415 -0
- machine_dialect/parser/tests/test_program.py +13 -0
- machine_dialect/parser/tests/test_return_statements.py +89 -0
- machine_dialect/parser/tests/test_set_statements.py +152 -0
- machine_dialect/parser/tests/test_strict_equality.py +258 -0
- machine_dialect/parser/tests/test_symbol_table.py +217 -0
- machine_dialect/parser/tests/test_url_literal_expressions.py +209 -0
- machine_dialect/parser/tests/test_utility_statements.py +423 -0
- machine_dialect/parser/token_buffer.py +159 -0
- machine_dialect/repl/__init__.py +3 -0
- machine_dialect/repl/repl.py +426 -0
- machine_dialect/repl/tests/__init__.py +0 -0
- machine_dialect/repl/tests/test_repl.py +606 -0
- machine_dialect/semantic/__init__.py +12 -0
- machine_dialect/semantic/analyzer.py +906 -0
- machine_dialect/semantic/error_messages.py +189 -0
- machine_dialect/semantic/tests/__init__.py +1 -0
- machine_dialect/semantic/tests/test_analyzer.py +364 -0
- machine_dialect/semantic/tests/test_error_messages.py +104 -0
- machine_dialect/tests/edge_cases/__init__.py +10 -0
- machine_dialect/tests/edge_cases/test_boundary_access.py +256 -0
- machine_dialect/tests/edge_cases/test_empty_collections.py +166 -0
- machine_dialect/tests/edge_cases/test_invalid_operations.py +243 -0
- machine_dialect/tests/edge_cases/test_named_list_edge_cases.py +295 -0
- machine_dialect/tests/edge_cases/test_nested_structures.py +313 -0
- machine_dialect/tests/edge_cases/test_type_mixing.py +277 -0
- machine_dialect/tests/integration/test_array_operations_emulation.py +248 -0
- machine_dialect/tests/integration/test_list_compilation.py +395 -0
- machine_dialect/tests/integration/test_lists_and_dictionaries.py +322 -0
- machine_dialect/type_checking/__init__.py +21 -0
- machine_dialect/type_checking/tests/__init__.py +1 -0
- machine_dialect/type_checking/tests/test_type_system.py +230 -0
- machine_dialect/type_checking/type_system.py +270 -0
- machine_dialect-0.1.0a1.dist-info/METADATA +128 -0
- machine_dialect-0.1.0a1.dist-info/RECORD +268 -0
- machine_dialect-0.1.0a1.dist-info/WHEEL +5 -0
- machine_dialect-0.1.0a1.dist-info/entry_points.txt +3 -0
- machine_dialect-0.1.0a1.dist-info/licenses/LICENSE +201 -0
- machine_dialect-0.1.0a1.dist-info/top_level.txt +2 -0
- 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"
|