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,163 @@
|
|
1
|
+
"""Tests for parsing float literal expressions."""
|
2
|
+
|
3
|
+
import pytest
|
4
|
+
|
5
|
+
from machine_dialect.ast import ExpressionStatement
|
6
|
+
from machine_dialect.parser import Parser
|
7
|
+
from machine_dialect.parser.tests.helper_functions import (
|
8
|
+
assert_literal_expression,
|
9
|
+
assert_program_statements,
|
10
|
+
)
|
11
|
+
|
12
|
+
|
13
|
+
class TestFloatLiteralExpressions:
|
14
|
+
"""Test cases for parsing float literal expressions."""
|
15
|
+
|
16
|
+
@pytest.mark.parametrize(
|
17
|
+
"source,expected_value",
|
18
|
+
[
|
19
|
+
# Simple floats
|
20
|
+
("0.0", 0.0),
|
21
|
+
("1.0", 1.0),
|
22
|
+
("3.14", 3.14),
|
23
|
+
("2.71", 2.71),
|
24
|
+
# Floats without leading zero
|
25
|
+
(".5", 0.5),
|
26
|
+
(".25", 0.25),
|
27
|
+
(".125", 0.125),
|
28
|
+
# Floats with many decimal places
|
29
|
+
("3.14159", 3.14159),
|
30
|
+
("2.71828", 2.71828),
|
31
|
+
("0.123456789", 0.123456789),
|
32
|
+
# Large floats
|
33
|
+
("123.456", 123.456),
|
34
|
+
("9999.9999", 9999.9999),
|
35
|
+
("1000000.0", 1000000.0),
|
36
|
+
# Small floats
|
37
|
+
("0.001", 0.001),
|
38
|
+
("0.0001", 0.0001),
|
39
|
+
("0.00001", 0.00001),
|
40
|
+
# Edge cases
|
41
|
+
("999999999.999999999", 999999999.999999999),
|
42
|
+
# Floats with underscores (one on each side)
|
43
|
+
("_3.14_", 3.14),
|
44
|
+
("_0.5_", 0.5),
|
45
|
+
("_1.0_", 1.0),
|
46
|
+
("_123.456_", 123.456),
|
47
|
+
("_.25_", 0.25),
|
48
|
+
("_0.001_", 0.001),
|
49
|
+
("_0.0_", 0.0),
|
50
|
+
],
|
51
|
+
)
|
52
|
+
def test_float_literal_expression(self, source: str, expected_value: float) -> None:
|
53
|
+
"""Test parsing various float literal expressions.
|
54
|
+
|
55
|
+
Args:
|
56
|
+
source: The source code to parse.
|
57
|
+
expected_value: The expected float value.
|
58
|
+
"""
|
59
|
+
parser = Parser()
|
60
|
+
|
61
|
+
program = parser.parse(source)
|
62
|
+
|
63
|
+
assert_program_statements(parser, program)
|
64
|
+
|
65
|
+
statement = program.statements[0]
|
66
|
+
assert isinstance(statement, ExpressionStatement)
|
67
|
+
assert statement.expression is not None
|
68
|
+
|
69
|
+
assert_literal_expression(statement.expression, expected_value)
|
70
|
+
|
71
|
+
def test_float_with_period(self) -> None:
|
72
|
+
"""Test parsing float literal followed by period."""
|
73
|
+
source = "3.14."
|
74
|
+
parser = Parser()
|
75
|
+
|
76
|
+
program = parser.parse(source)
|
77
|
+
|
78
|
+
assert_program_statements(parser, program)
|
79
|
+
|
80
|
+
statement = program.statements[0]
|
81
|
+
assert isinstance(statement, ExpressionStatement)
|
82
|
+
assert statement.expression is not None
|
83
|
+
|
84
|
+
assert_literal_expression(statement.expression, 3.14)
|
85
|
+
|
86
|
+
def test_multiple_float_statements(self) -> None:
|
87
|
+
"""Test parsing multiple float literal statements."""
|
88
|
+
source = "1.1. 2.2. 3.3."
|
89
|
+
parser = Parser()
|
90
|
+
|
91
|
+
program = parser.parse(source)
|
92
|
+
|
93
|
+
assert len(parser.errors) == 0
|
94
|
+
assert len(program.statements) == 3
|
95
|
+
|
96
|
+
# Check each statement
|
97
|
+
for i, expected_value in enumerate([1.1, 2.2, 3.3]):
|
98
|
+
statement = program.statements[i]
|
99
|
+
assert isinstance(statement, ExpressionStatement)
|
100
|
+
assert statement.expression is not None
|
101
|
+
assert_literal_expression(statement.expression, expected_value)
|
102
|
+
|
103
|
+
@pytest.mark.parametrize(
|
104
|
+
"source,error_substring",
|
105
|
+
[
|
106
|
+
# Underscore only on left side
|
107
|
+
("_3.14", "_3.14"),
|
108
|
+
("_0.5", "_0.5"),
|
109
|
+
("_123.456", "_123.456"),
|
110
|
+
("_.25", "_.25"),
|
111
|
+
# Underscore only on right side
|
112
|
+
("3.14_", "3.14_"),
|
113
|
+
("0.5_", "0.5_"),
|
114
|
+
("123.456_", "123.456_"),
|
115
|
+
(".25_", ".25_"),
|
116
|
+
# Multiple underscores
|
117
|
+
("__3.14__", "__3.14__"),
|
118
|
+
("___0.5___", "___0.5___"),
|
119
|
+
("__123.456__", "__123.456__"),
|
120
|
+
# Mixed multiple and single
|
121
|
+
("__3.14_", "__3.14_"),
|
122
|
+
("_3.14__", "_3.14__"),
|
123
|
+
],
|
124
|
+
)
|
125
|
+
def test_invalid_underscore_formats_produce_errors(self, source: str, error_substring: str) -> None:
|
126
|
+
"""Test that invalid underscore formats produce lexer errors.
|
127
|
+
|
128
|
+
Args:
|
129
|
+
source: The source code with invalid underscore format.
|
130
|
+
error_substring: Expected substring in the error message.
|
131
|
+
"""
|
132
|
+
# Lexer instantiation moved to Parser.parse()
|
133
|
+
parser = Parser()
|
134
|
+
|
135
|
+
# Parse the program (parser collects lexer errors)
|
136
|
+
_ = parser.parse(source)
|
137
|
+
|
138
|
+
# Should have at least one error
|
139
|
+
assert len(parser.errors) >= 1, f"Expected error for invalid format: {source}"
|
140
|
+
|
141
|
+
# Check that the error mentions the invalid token
|
142
|
+
error_messages = [str(error) for error in parser.errors]
|
143
|
+
assert any(error_substring in msg for msg in error_messages), (
|
144
|
+
f"Expected error to mention '{error_substring}', got errors: {error_messages}"
|
145
|
+
)
|
146
|
+
|
147
|
+
def test_mixed_integer_and_float_statements(self) -> None:
|
148
|
+
"""Test parsing mixed integer and float literal statements."""
|
149
|
+
source = "42. 3.14. 100. 0.5."
|
150
|
+
parser = Parser()
|
151
|
+
|
152
|
+
program = parser.parse(source)
|
153
|
+
|
154
|
+
assert len(parser.errors) == 0
|
155
|
+
assert len(program.statements) == 4
|
156
|
+
|
157
|
+
# Check each statement with expected values and types
|
158
|
+
expected_values = [42, 3.14, 100, 0.5]
|
159
|
+
for i, expected_value in enumerate(expected_values):
|
160
|
+
statement = program.statements[i]
|
161
|
+
assert isinstance(statement, ExpressionStatement)
|
162
|
+
assert statement.expression is not None
|
163
|
+
assert_literal_expression(statement.expression, expected_value)
|
@@ -0,0 +1,57 @@
|
|
1
|
+
"""Tests for parsing identifier expressions."""
|
2
|
+
|
3
|
+
import pytest
|
4
|
+
|
5
|
+
from machine_dialect.ast import ExpressionStatement
|
6
|
+
from machine_dialect.parser import Parser
|
7
|
+
from machine_dialect.parser.tests.helper_functions import (
|
8
|
+
assert_literal_expression,
|
9
|
+
assert_program_statements,
|
10
|
+
)
|
11
|
+
|
12
|
+
|
13
|
+
class TestIdentifierExpressions:
|
14
|
+
"""Test cases for parsing identifier expressions."""
|
15
|
+
|
16
|
+
@pytest.mark.parametrize(
|
17
|
+
"source,expected_value",
|
18
|
+
[
|
19
|
+
# Simple identifiers
|
20
|
+
("foobar", "foobar"),
|
21
|
+
("x", "x"),
|
22
|
+
("myVariableName", "myVariableName"),
|
23
|
+
("test_variable_name", "test_variable_name"),
|
24
|
+
# Backtick identifiers
|
25
|
+
("`myVariable`", "myVariable"),
|
26
|
+
("`x`", "x"),
|
27
|
+
# Multi-word identifiers
|
28
|
+
("`email address`", "email address"),
|
29
|
+
("`user name`", "user name"),
|
30
|
+
("`first name`", "first name"),
|
31
|
+
("`shopping cart total`", "shopping cart total"),
|
32
|
+
("`is logged in`", "is logged in"),
|
33
|
+
("`has been processed`", "has been processed"),
|
34
|
+
# Complex multi-word identifiers
|
35
|
+
("`customer email address`", "customer email address"),
|
36
|
+
("`total order amount`", "total order amount"),
|
37
|
+
("`user account status`", "user account status"),
|
38
|
+
],
|
39
|
+
)
|
40
|
+
def test_identifier_expression(self, source: str, expected_value: str) -> None:
|
41
|
+
"""Test parsing various identifier expressions.
|
42
|
+
|
43
|
+
Args:
|
44
|
+
source: The source code to parse.
|
45
|
+
expected_value: The expected identifier value.
|
46
|
+
"""
|
47
|
+
parser = Parser()
|
48
|
+
|
49
|
+
program = parser.parse(source, check_semantics=False)
|
50
|
+
|
51
|
+
assert_program_statements(parser, program)
|
52
|
+
|
53
|
+
statement = program.statements[0]
|
54
|
+
assert isinstance(statement, ExpressionStatement)
|
55
|
+
assert statement.expression is not None
|
56
|
+
|
57
|
+
assert_literal_expression(statement.expression, expected_value)
|
@@ -0,0 +1,61 @@
|
|
1
|
+
"""Tests for if statements with empty blocks."""
|
2
|
+
|
3
|
+
from machine_dialect.parser import Parser
|
4
|
+
|
5
|
+
|
6
|
+
class TestIfEmptyBlock:
|
7
|
+
"""Test that if statements with empty blocks generate appropriate errors."""
|
8
|
+
|
9
|
+
def test_if_with_empty_consequence_block(self) -> None:
|
10
|
+
"""Test that an if statement with an empty consequence block generates an error."""
|
11
|
+
source = """if True then:
|
12
|
+
>
|
13
|
+
"""
|
14
|
+
parser = Parser()
|
15
|
+
_ = parser.parse(source)
|
16
|
+
|
17
|
+
# Should have an error about empty consequence block
|
18
|
+
assert len(parser.errors) > 0
|
19
|
+
assert any("must have a non-empty consequence block" in str(error) for error in parser.errors)
|
20
|
+
|
21
|
+
def test_if_with_empty_else_block(self) -> None:
|
22
|
+
"""Test that an else block that is empty generates an error."""
|
23
|
+
source = """if True then:
|
24
|
+
> Set x to 1.
|
25
|
+
else:
|
26
|
+
>
|
27
|
+
"""
|
28
|
+
parser = Parser()
|
29
|
+
_ = parser.parse(source)
|
30
|
+
|
31
|
+
# Should have an error about empty else block
|
32
|
+
assert len(parser.errors) > 0
|
33
|
+
assert any("must not be empty" in str(error) for error in parser.errors)
|
34
|
+
|
35
|
+
def test_nested_if_with_empty_block(self) -> None:
|
36
|
+
"""Test that nested if statements also require non-empty blocks."""
|
37
|
+
source = """if True then:
|
38
|
+
> if False then:
|
39
|
+
> >
|
40
|
+
> Set x to 1."""
|
41
|
+
|
42
|
+
parser = Parser()
|
43
|
+
_ = parser.parse(source)
|
44
|
+
|
45
|
+
# Should have an error about the nested if's empty block
|
46
|
+
assert len(parser.errors) > 0
|
47
|
+
assert any("must have a non-empty consequence block" in str(error) for error in parser.errors)
|
48
|
+
|
49
|
+
def test_if_with_only_empty_lines_in_block(self) -> None:
|
50
|
+
"""Test that a block with only empty lines is still considered empty."""
|
51
|
+
source = """if True then:
|
52
|
+
>
|
53
|
+
>
|
54
|
+
>
|
55
|
+
"""
|
56
|
+
parser = Parser()
|
57
|
+
_ = parser.parse(source)
|
58
|
+
|
59
|
+
# Should have an error about empty consequence block
|
60
|
+
assert len(parser.errors) > 0
|
61
|
+
assert any("must have a non-empty consequence block" in str(error) for error in parser.errors)
|
@@ -0,0 +1,299 @@
|
|
1
|
+
"""Tests for if statements with block statements in the parser.
|
2
|
+
|
3
|
+
This module tests the parsing of if statements which support blocks of statements
|
4
|
+
marked by '>' symbols for depth tracking.
|
5
|
+
"""
|
6
|
+
|
7
|
+
import pytest
|
8
|
+
|
9
|
+
from machine_dialect.ast import (
|
10
|
+
BlockStatement,
|
11
|
+
Identifier,
|
12
|
+
IfStatement,
|
13
|
+
InfixExpression,
|
14
|
+
SetStatement,
|
15
|
+
WholeNumberLiteral,
|
16
|
+
YesNoLiteral,
|
17
|
+
)
|
18
|
+
from machine_dialect.parser import Parser
|
19
|
+
|
20
|
+
|
21
|
+
class TestIfStatements:
|
22
|
+
"""Test parsing of if statements with block statements."""
|
23
|
+
|
24
|
+
def test_parse_basic_if_statement(self) -> None:
|
25
|
+
"""Test parsing of basic if statement with single statement in block."""
|
26
|
+
source = """
|
27
|
+
if Yes then:
|
28
|
+
> Set x to 1.
|
29
|
+
"""
|
30
|
+
parser = Parser()
|
31
|
+
program = parser.parse(source)
|
32
|
+
|
33
|
+
assert len(program.statements) == 1
|
34
|
+
assert isinstance(program.statements[0], IfStatement)
|
35
|
+
|
36
|
+
if_stmt = program.statements[0]
|
37
|
+
assert isinstance(if_stmt.condition, YesNoLiteral)
|
38
|
+
assert if_stmt.condition.value is True
|
39
|
+
|
40
|
+
assert isinstance(if_stmt.consequence, BlockStatement)
|
41
|
+
assert if_stmt.consequence.depth == 1
|
42
|
+
assert len(if_stmt.consequence.statements) == 1
|
43
|
+
|
44
|
+
set_stmt = if_stmt.consequence.statements[0]
|
45
|
+
assert isinstance(set_stmt, SetStatement)
|
46
|
+
assert isinstance(set_stmt.name, Identifier)
|
47
|
+
assert set_stmt.name.value == "x"
|
48
|
+
assert isinstance(set_stmt.value, WholeNumberLiteral)
|
49
|
+
assert set_stmt.value.value == 1
|
50
|
+
|
51
|
+
assert if_stmt.alternative is None
|
52
|
+
|
53
|
+
def test_parse_if_else_statement(self) -> None:
|
54
|
+
"""Test parsing of if-else statement with blocks."""
|
55
|
+
source = """
|
56
|
+
if Yes then:
|
57
|
+
> Set x to 1.
|
58
|
+
> Set y to 2.
|
59
|
+
else:
|
60
|
+
> Set x to 3.
|
61
|
+
> Set y to 4.
|
62
|
+
"""
|
63
|
+
parser = Parser()
|
64
|
+
program = parser.parse(source)
|
65
|
+
|
66
|
+
assert len(program.statements) == 1
|
67
|
+
assert isinstance(program.statements[0], IfStatement)
|
68
|
+
|
69
|
+
if_stmt = program.statements[0]
|
70
|
+
assert isinstance(if_stmt.condition, YesNoLiteral)
|
71
|
+
assert if_stmt.condition.value is True
|
72
|
+
|
73
|
+
# Check consequence block
|
74
|
+
assert isinstance(if_stmt.consequence, BlockStatement)
|
75
|
+
assert if_stmt.consequence.depth == 1
|
76
|
+
assert len(if_stmt.consequence.statements) == 2
|
77
|
+
|
78
|
+
# Check alternative block
|
79
|
+
assert if_stmt.alternative is not None
|
80
|
+
assert isinstance(if_stmt.alternative, BlockStatement)
|
81
|
+
assert if_stmt.alternative.depth == 1
|
82
|
+
assert len(if_stmt.alternative.statements) == 2
|
83
|
+
|
84
|
+
def test_parse_nested_if_statements(self) -> None:
|
85
|
+
"""Test parsing of nested if statements with proper depth."""
|
86
|
+
source = """
|
87
|
+
if Yes then:
|
88
|
+
>
|
89
|
+
> Set foo to 1.
|
90
|
+
>
|
91
|
+
> if No then:
|
92
|
+
> >
|
93
|
+
> > Set bar to 2.
|
94
|
+
> > Set baz to 3.
|
95
|
+
>
|
96
|
+
> Set bax to 4.
|
97
|
+
"""
|
98
|
+
parser = Parser()
|
99
|
+
program = parser.parse(source)
|
100
|
+
|
101
|
+
assert len(program.statements) == 1
|
102
|
+
outer_if = program.statements[0]
|
103
|
+
assert isinstance(outer_if, IfStatement)
|
104
|
+
|
105
|
+
assert isinstance(outer_if.consequence, BlockStatement)
|
106
|
+
assert outer_if.consequence.depth == 1
|
107
|
+
assert len(outer_if.consequence.statements) == 3
|
108
|
+
|
109
|
+
# First statement in outer block
|
110
|
+
assert isinstance(outer_if.consequence.statements[0], SetStatement)
|
111
|
+
|
112
|
+
# Nested if statement
|
113
|
+
inner_if = outer_if.consequence.statements[1]
|
114
|
+
assert isinstance(inner_if, IfStatement)
|
115
|
+
assert isinstance(inner_if.consequence, BlockStatement)
|
116
|
+
assert inner_if.consequence.depth == 2
|
117
|
+
assert len(inner_if.consequence.statements) == 2
|
118
|
+
|
119
|
+
# Last statement in outer block
|
120
|
+
assert isinstance(outer_if.consequence.statements[2], SetStatement)
|
121
|
+
|
122
|
+
@pytest.mark.parametrize(
|
123
|
+
"keyword,else_keyword",
|
124
|
+
[
|
125
|
+
("if", "else"),
|
126
|
+
("if", "otherwise"),
|
127
|
+
("when", "else"),
|
128
|
+
("when", "otherwise"),
|
129
|
+
("whenever", "else"),
|
130
|
+
("whenever", "otherwise"),
|
131
|
+
],
|
132
|
+
)
|
133
|
+
def test_parse_if_keywords_variations(self, keyword: str, else_keyword: str) -> None:
|
134
|
+
"""Test parsing of if statements with different keyword variations."""
|
135
|
+
source = f"""
|
136
|
+
{keyword} Yes then:
|
137
|
+
> Set x to 1.
|
138
|
+
{else_keyword}:
|
139
|
+
> Set x to 2.
|
140
|
+
"""
|
141
|
+
parser = Parser()
|
142
|
+
program = parser.parse(source)
|
143
|
+
|
144
|
+
assert len(program.statements) == 1
|
145
|
+
assert isinstance(program.statements[0], IfStatement)
|
146
|
+
|
147
|
+
if_stmt = program.statements[0]
|
148
|
+
assert isinstance(if_stmt.condition, YesNoLiteral)
|
149
|
+
assert if_stmt.condition.value is True
|
150
|
+
|
151
|
+
assert isinstance(if_stmt.consequence, BlockStatement)
|
152
|
+
assert len(if_stmt.consequence.statements) == 1
|
153
|
+
|
154
|
+
assert if_stmt.alternative is not None
|
155
|
+
assert isinstance(if_stmt.alternative, BlockStatement)
|
156
|
+
assert len(if_stmt.alternative.statements) == 1
|
157
|
+
|
158
|
+
def test_parse_if_with_complex_condition(self) -> None:
|
159
|
+
"""Test parsing of if statement with complex boolean expression."""
|
160
|
+
source = """
|
161
|
+
if x > 5 and y < 10 then:
|
162
|
+
> Set result to True.
|
163
|
+
"""
|
164
|
+
parser = Parser()
|
165
|
+
program = parser.parse(source)
|
166
|
+
|
167
|
+
assert len(program.statements) == 1
|
168
|
+
assert isinstance(program.statements[0], IfStatement)
|
169
|
+
|
170
|
+
if_stmt = program.statements[0]
|
171
|
+
assert isinstance(if_stmt.condition, InfixExpression)
|
172
|
+
assert if_stmt.condition.operator == "and"
|
173
|
+
|
174
|
+
assert isinstance(if_stmt.consequence, BlockStatement)
|
175
|
+
assert len(if_stmt.consequence.statements) == 1
|
176
|
+
|
177
|
+
def test_parse_empty_blocks(self) -> None:
|
178
|
+
"""Test parsing of if statement with empty blocks."""
|
179
|
+
source = """
|
180
|
+
if Yes then:
|
181
|
+
>
|
182
|
+
else:
|
183
|
+
>
|
184
|
+
"""
|
185
|
+
parser = Parser()
|
186
|
+
program = parser.parse(source)
|
187
|
+
|
188
|
+
assert len(program.statements) == 1
|
189
|
+
assert isinstance(program.statements[0], IfStatement)
|
190
|
+
|
191
|
+
if_stmt = program.statements[0]
|
192
|
+
assert isinstance(if_stmt.consequence, BlockStatement)
|
193
|
+
assert len(if_stmt.consequence.statements) == 0
|
194
|
+
|
195
|
+
assert if_stmt.alternative is not None
|
196
|
+
assert isinstance(if_stmt.alternative, BlockStatement)
|
197
|
+
assert len(if_stmt.alternative.statements) == 0
|
198
|
+
|
199
|
+
def test_parse_if_without_then_keyword(self) -> None:
|
200
|
+
"""Test parsing of if statement with colon directly after condition."""
|
201
|
+
source = """
|
202
|
+
if Yes:
|
203
|
+
> Set x to 1.
|
204
|
+
"""
|
205
|
+
parser = Parser()
|
206
|
+
program = parser.parse(source)
|
207
|
+
|
208
|
+
assert len(program.statements) == 1
|
209
|
+
assert isinstance(program.statements[0], IfStatement)
|
210
|
+
|
211
|
+
if_stmt = program.statements[0]
|
212
|
+
assert isinstance(if_stmt.condition, YesNoLiteral)
|
213
|
+
assert isinstance(if_stmt.consequence, BlockStatement)
|
214
|
+
assert len(if_stmt.consequence.statements) == 1
|
215
|
+
|
216
|
+
def test_block_depth_tracking(self) -> None:
|
217
|
+
"""Test that block depth is properly tracked and validated."""
|
218
|
+
source = """
|
219
|
+
if Yes then:
|
220
|
+
> Set x to 1.
|
221
|
+
> > Set y to 2.
|
222
|
+
"""
|
223
|
+
parser = Parser()
|
224
|
+
parser.parse(source)
|
225
|
+
|
226
|
+
# This should produce an error - depth suddenly increases
|
227
|
+
assert len(parser.errors) > 0
|
228
|
+
assert any("depth" in str(error).lower() for error in parser.errors)
|
229
|
+
|
230
|
+
def test_missing_period_error(self) -> None:
|
231
|
+
"""Test that missing periods generate appropriate errors."""
|
232
|
+
source = """
|
233
|
+
if Yes then:
|
234
|
+
> Set x to 1
|
235
|
+
|
236
|
+
x
|
237
|
+
"""
|
238
|
+
parser = Parser()
|
239
|
+
parser.parse(source)
|
240
|
+
|
241
|
+
# Should have errors about missing period
|
242
|
+
assert len(parser.errors) > 0
|
243
|
+
assert any("period" in str(error).lower() or "TokenType.PUNCT_PERIOD" in str(error) for error in parser.errors)
|
244
|
+
|
245
|
+
def test_multiple_if_statements(self) -> None:
|
246
|
+
"""Test parsing multiple if statements in sequence."""
|
247
|
+
source = """
|
248
|
+
if Yes then:
|
249
|
+
> Set x to 1.
|
250
|
+
|
251
|
+
if No then:
|
252
|
+
> Set y to 2.
|
253
|
+
else:
|
254
|
+
> Set y to 3.
|
255
|
+
"""
|
256
|
+
parser = Parser()
|
257
|
+
program = parser.parse(source)
|
258
|
+
|
259
|
+
assert len(program.statements) == 2
|
260
|
+
assert all(isinstance(stmt, IfStatement) for stmt in program.statements)
|
261
|
+
|
262
|
+
# First if statement
|
263
|
+
if_stmt1 = program.statements[0]
|
264
|
+
assert isinstance(if_stmt1, IfStatement)
|
265
|
+
assert isinstance(if_stmt1.condition, YesNoLiteral)
|
266
|
+
assert if_stmt1.condition.value is True
|
267
|
+
assert if_stmt1.alternative is None
|
268
|
+
|
269
|
+
# Second if statement
|
270
|
+
if_stmt2 = program.statements[1]
|
271
|
+
assert isinstance(if_stmt2, IfStatement)
|
272
|
+
assert isinstance(if_stmt2.condition, YesNoLiteral)
|
273
|
+
assert if_stmt2.condition.value is False
|
274
|
+
assert if_stmt2.alternative is not None
|
275
|
+
|
276
|
+
def test_if_statements_with_empty_lines(self) -> None:
|
277
|
+
"""Test parsing multiple if statements in sequence."""
|
278
|
+
source = """
|
279
|
+
if No then:
|
280
|
+
>
|
281
|
+
> Set y to 2.
|
282
|
+
>
|
283
|
+
else:
|
284
|
+
>
|
285
|
+
> Set y to 3.
|
286
|
+
>
|
287
|
+
"""
|
288
|
+
parser = Parser()
|
289
|
+
program = parser.parse(source)
|
290
|
+
|
291
|
+
assert len(program.statements) == 1
|
292
|
+
assert all(isinstance(stmt, IfStatement) for stmt in program.statements)
|
293
|
+
|
294
|
+
# If statement
|
295
|
+
if_stmt = program.statements[0]
|
296
|
+
assert isinstance(if_stmt, IfStatement)
|
297
|
+
assert isinstance(if_stmt.condition, YesNoLiteral)
|
298
|
+
assert if_stmt.condition.value is False
|
299
|
+
assert if_stmt.alternative is not None
|
@@ -0,0 +1,86 @@
|
|
1
|
+
"""Tests for parser handling of illegal tokens.
|
2
|
+
|
3
|
+
This module tests that the parser correctly reports MDSyntaxError
|
4
|
+
(not MDNameError) when encountering MISC_ILLEGAL tokens from the lexer.
|
5
|
+
"""
|
6
|
+
|
7
|
+
from machine_dialect.errors.exceptions import MDSyntaxError
|
8
|
+
from machine_dialect.parser import Parser
|
9
|
+
|
10
|
+
|
11
|
+
class TestIllegalTokenHandling:
|
12
|
+
"""Test that illegal tokens are reported as syntax errors."""
|
13
|
+
|
14
|
+
def test_malformed_underscore_literal_syntax_error(self) -> None:
|
15
|
+
"""Test that malformed underscore literals produce syntax errors."""
|
16
|
+
source = 'Define `x` as Whole Number. Set `x` to _"unclosed.'
|
17
|
+
parser = Parser()
|
18
|
+
|
19
|
+
parser.parse(source)
|
20
|
+
|
21
|
+
# Should have one syntax error for the illegal token
|
22
|
+
assert len(parser.errors) == 1
|
23
|
+
assert isinstance(parser.errors[0], MDSyntaxError)
|
24
|
+
assert "Illegal token" in str(parser.errors[0])
|
25
|
+
assert '_"unclosed.' in str(parser.errors[0])
|
26
|
+
|
27
|
+
def test_multiple_underscores_with_number(self) -> None:
|
28
|
+
"""Test that invalid underscore patterns produce syntax errors."""
|
29
|
+
source = "Define `x` as Whole Number. Set `x` to __42."
|
30
|
+
parser = Parser()
|
31
|
+
|
32
|
+
parser.parse(source)
|
33
|
+
|
34
|
+
# Should have one syntax error for the illegal token
|
35
|
+
assert len(parser.errors) == 1
|
36
|
+
assert isinstance(parser.errors[0], MDSyntaxError)
|
37
|
+
assert "Illegal token" in str(parser.errors[0])
|
38
|
+
assert "__42" in str(parser.errors[0])
|
39
|
+
|
40
|
+
def test_trailing_underscore_after_number(self) -> None:
|
41
|
+
"""Test that numbers with trailing underscores produce syntax errors."""
|
42
|
+
source = "Define `x` as Whole Number. Set `x` to 42_."
|
43
|
+
parser = Parser()
|
44
|
+
|
45
|
+
parser.parse(source)
|
46
|
+
|
47
|
+
# Should have one syntax error for the illegal token
|
48
|
+
assert len(parser.errors) == 1
|
49
|
+
assert isinstance(parser.errors[0], MDSyntaxError)
|
50
|
+
assert "Illegal token" in str(parser.errors[0])
|
51
|
+
assert "42_" in str(parser.errors[0])
|
52
|
+
|
53
|
+
def test_incomplete_underscore_wrapped_number(self) -> None:
|
54
|
+
"""Test that incomplete underscore-wrapped numbers produce syntax errors."""
|
55
|
+
source = "Define `x` as Whole Number. Set `x` to _42."
|
56
|
+
parser = Parser()
|
57
|
+
|
58
|
+
parser.parse(source)
|
59
|
+
|
60
|
+
# Should have one syntax error for the illegal token
|
61
|
+
assert len(parser.errors) == 1
|
62
|
+
assert isinstance(parser.errors[0], MDSyntaxError)
|
63
|
+
assert "Illegal token" in str(parser.errors[0])
|
64
|
+
assert "_42" in str(parser.errors[0])
|
65
|
+
|
66
|
+
def test_recovery_after_illegal_token(self) -> None:
|
67
|
+
"""Test that parser recovers and continues after illegal tokens."""
|
68
|
+
source = "Define `x` as Whole Number. Define `y` as Whole Number. Set `x` to _42. Set `y` to _10_."
|
69
|
+
parser = Parser()
|
70
|
+
|
71
|
+
program = parser.parse(source)
|
72
|
+
|
73
|
+
# Should have one syntax error for the first illegal token
|
74
|
+
assert len(parser.errors) == 1
|
75
|
+
assert isinstance(parser.errors[0], MDSyntaxError)
|
76
|
+
assert "_42" in str(parser.errors[0])
|
77
|
+
|
78
|
+
# Should have parsed four statements (2 Define + 2 Set)
|
79
|
+
assert len(program.statements) == 4
|
80
|
+
# First two are Define statements
|
81
|
+
assert program.statements[0] is not None
|
82
|
+
assert program.statements[1] is not None
|
83
|
+
# Third statement (Set with _42) has an error
|
84
|
+
assert program.statements[2] is not None
|
85
|
+
# Fourth statement should be complete (10 is properly wrapped)
|
86
|
+
assert program.statements[3] is not None
|