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,252 @@
|
|
1
|
+
"""Tests for the CFG parser."""
|
2
|
+
|
3
|
+
import pytest
|
4
|
+
|
5
|
+
from machine_dialect.cfg import CFGParser
|
6
|
+
|
7
|
+
|
8
|
+
class TestCFGParser:
|
9
|
+
"""Test the CFG parser for Machine Dialect™."""
|
10
|
+
|
11
|
+
def setup_method(self) -> None:
|
12
|
+
"""Set up test fixtures."""
|
13
|
+
self.parser = CFGParser()
|
14
|
+
|
15
|
+
def test_parse_set_statement(self) -> None:
|
16
|
+
"""Test parsing Set statements."""
|
17
|
+
code = "Set `x` to _10_."
|
18
|
+
tree = self.parser.parse(code)
|
19
|
+
assert tree is not None
|
20
|
+
assert tree.data == "start"
|
21
|
+
|
22
|
+
def test_parse_give_back_statement(self) -> None:
|
23
|
+
"""Test parsing Give back statements."""
|
24
|
+
code = 'Give back _"Hello, World!"_.'
|
25
|
+
tree = self.parser.parse(code)
|
26
|
+
assert tree is not None
|
27
|
+
|
28
|
+
def test_parse_arithmetic_expression(self) -> None:
|
29
|
+
"""Test parsing arithmetic expressions."""
|
30
|
+
code = "Set `result` to _5_ + _3_ * _2_."
|
31
|
+
tree = self.parser.parse(code)
|
32
|
+
assert tree is not None
|
33
|
+
|
34
|
+
def test_parse_logical_expression(self) -> None:
|
35
|
+
"""Test parsing logical expressions."""
|
36
|
+
code = "Set `flag` to _yes_ and not _yes_ or _yes_."
|
37
|
+
tree = self.parser.parse(code)
|
38
|
+
assert tree is not None
|
39
|
+
|
40
|
+
def test_parse_comparison(self) -> None:
|
41
|
+
"""Test parsing comparison expressions."""
|
42
|
+
test_cases = [
|
43
|
+
"Set `check` to `x` > _5_.",
|
44
|
+
"Set `check` to `y` is greater than _10_.",
|
45
|
+
"Set `check` to `z` equals _0_.",
|
46
|
+
"Set `check` to `a` is not equal to `b`.",
|
47
|
+
]
|
48
|
+
|
49
|
+
for code in test_cases:
|
50
|
+
tree = self.parser.parse(code)
|
51
|
+
assert tree is not None
|
52
|
+
|
53
|
+
def test_parse_if_statement(self) -> None:
|
54
|
+
"""Test parsing if statements."""
|
55
|
+
code = """If `x` > _0_ then:
|
56
|
+
> Give back _"Positive"_."""
|
57
|
+
tree = self.parser.parse(code)
|
58
|
+
assert tree is not None
|
59
|
+
|
60
|
+
def test_parse_if_else_statement(self) -> None:
|
61
|
+
"""Test parsing if-else statements."""
|
62
|
+
code = """If `age` >= _18_ then:
|
63
|
+
> Give back _"Adult"_.
|
64
|
+
Else:
|
65
|
+
> Give back _"Minor"_."""
|
66
|
+
tree = self.parser.parse(code)
|
67
|
+
assert tree is not None
|
68
|
+
|
69
|
+
def test_parse_multiple_statements(self) -> None:
|
70
|
+
"""Test parsing multiple statements."""
|
71
|
+
code = """Set `x` to _10_.
|
72
|
+
Set `y` to _20_.
|
73
|
+
Set `sum` to `x` + `y`.
|
74
|
+
Give back `sum`."""
|
75
|
+
tree = self.parser.parse(code)
|
76
|
+
assert tree is not None
|
77
|
+
|
78
|
+
def test_parse_nested_expressions(self) -> None:
|
79
|
+
"""Test parsing nested expressions."""
|
80
|
+
code = "Set `result` to (_5_ + _3_) * (_10_ - _2_)."
|
81
|
+
tree = self.parser.parse(code)
|
82
|
+
assert tree is not None
|
83
|
+
|
84
|
+
def test_parse_string_literals(self) -> None:
|
85
|
+
"""Test parsing string literals."""
|
86
|
+
test_cases = [
|
87
|
+
'Give back _"Hello"_.',
|
88
|
+
"Give back _'World'_.",
|
89
|
+
'Give back _"Machine Dialect™!"_.',
|
90
|
+
]
|
91
|
+
|
92
|
+
for code in test_cases:
|
93
|
+
tree = self.parser.parse(code)
|
94
|
+
assert tree is not None
|
95
|
+
|
96
|
+
def test_parse_boolean_literals(self) -> None:
|
97
|
+
"""Test parsing boolean literals."""
|
98
|
+
test_cases = [
|
99
|
+
"Set `flag` to _yes_.",
|
100
|
+
"Set `flag` to _no_.",
|
101
|
+
]
|
102
|
+
|
103
|
+
for code in test_cases:
|
104
|
+
tree = self.parser.parse(code)
|
105
|
+
assert tree is not None
|
106
|
+
|
107
|
+
def test_parse_empty_literal(self) -> None:
|
108
|
+
"""Test parsing empty literal."""
|
109
|
+
code = "Set `value` to _empty_."
|
110
|
+
tree = self.parser.parse(code)
|
111
|
+
assert tree is not None
|
112
|
+
|
113
|
+
def test_validate_valid_code(self) -> None:
|
114
|
+
"""Test validation of valid code."""
|
115
|
+
code = "Set `x` to _5_."
|
116
|
+
assert self.parser.validate(code) is True
|
117
|
+
|
118
|
+
def test_validate_invalid_code(self) -> None:
|
119
|
+
"""Test validation of invalid code."""
|
120
|
+
code = "Set `x` to 5" # Missing underscores and period
|
121
|
+
assert self.parser.validate(code) is False
|
122
|
+
|
123
|
+
def test_case_insensitive_keywords(self) -> None:
|
124
|
+
"""Test that keywords are case-insensitive."""
|
125
|
+
test_cases = [
|
126
|
+
"set `x` to _5_.",
|
127
|
+
"SET `x` to _5_.",
|
128
|
+
"Set `x` TO _5_.",
|
129
|
+
'give back _"Hello"_.',
|
130
|
+
'GIVE BACK _"Hello"_.',
|
131
|
+
'If `x` > _0_ then:\n> Give back _"Yes"_.',
|
132
|
+
]
|
133
|
+
|
134
|
+
for code in test_cases:
|
135
|
+
tree = self.parser.parse(code)
|
136
|
+
assert tree is not None
|
137
|
+
|
138
|
+
def test_complex_program(self) -> None:
|
139
|
+
"""Test parsing a complex program."""
|
140
|
+
code = """Set `score` to _85_.
|
141
|
+
Set `passing_grade` to _60_.
|
142
|
+
Set `is_excellent` to `score` >= _90_.
|
143
|
+
If `score` >= `passing_grade` then:
|
144
|
+
> If `is_excellent` then:
|
145
|
+
> > Give back _"Excellent work!"_.
|
146
|
+
> Else:
|
147
|
+
> > Give back _"Good job, you passed."_.
|
148
|
+
Else:
|
149
|
+
> Give back _"Please try again."_."""
|
150
|
+
tree = self.parser.parse(code)
|
151
|
+
assert tree is not None
|
152
|
+
|
153
|
+
def test_identifier_with_underscores(self) -> None:
|
154
|
+
"""Test parsing identifiers with underscores."""
|
155
|
+
code = 'Set `user_name` to _"Alice"_.'
|
156
|
+
tree = self.parser.parse(code)
|
157
|
+
assert tree is not None
|
158
|
+
|
159
|
+
def test_natural_language_operators(self) -> None:
|
160
|
+
"""Test parsing natural language operators."""
|
161
|
+
test_cases = [
|
162
|
+
"Set `check` to `x` is equal to `y`.",
|
163
|
+
"Set `check` to `a` is not equal to `b`.",
|
164
|
+
"Set `check` to `m` is less than `n`.",
|
165
|
+
"Set `check` to `p` is greater than `q`.",
|
166
|
+
]
|
167
|
+
|
168
|
+
for code in test_cases:
|
169
|
+
tree = self.parser.parse(code)
|
170
|
+
assert tree is not None
|
171
|
+
|
172
|
+
def test_strict_equality_operators(self) -> None:
|
173
|
+
"""Test parsing strict equality operators."""
|
174
|
+
test_cases = [
|
175
|
+
"Give back _5_ is strictly equal to _5_.",
|
176
|
+
"Give back _5_ is not strictly equal to _5.0_.",
|
177
|
+
"Give back _5_ is exactly equal to _5_.",
|
178
|
+
"Give back _5_ is not exactly equal to _5.0_.",
|
179
|
+
"Give back _5_ is identical to _5_.",
|
180
|
+
"Give back _5_ is not identical to _5.0_.",
|
181
|
+
]
|
182
|
+
|
183
|
+
for code in test_cases:
|
184
|
+
tree = self.parser.parse(code)
|
185
|
+
assert tree is not None
|
186
|
+
|
187
|
+
def test_float_literals(self) -> None:
|
188
|
+
"""Test parsing float literals."""
|
189
|
+
test_cases = [
|
190
|
+
"Set `pi` to _3.14_.",
|
191
|
+
"Give back _2.5_ + _1.5_.",
|
192
|
+
"Set `result` to _10.0_ / _3.0_.",
|
193
|
+
]
|
194
|
+
|
195
|
+
for code in test_cases:
|
196
|
+
tree = self.parser.parse(code)
|
197
|
+
assert tree is not None
|
198
|
+
|
199
|
+
def test_use_statement(self) -> None:
|
200
|
+
"""Test parsing Use statements."""
|
201
|
+
code = 'Use `print` with _"Hello"_, _42_.'
|
202
|
+
tree = self.parser.parse(code)
|
203
|
+
assert tree is not None
|
204
|
+
|
205
|
+
@pytest.mark.skip(reason="Actions not yet implemented in main parser")
|
206
|
+
def test_action_statement(self) -> None:
|
207
|
+
"""Test parsing Action statements with inputs, outputs, and body."""
|
208
|
+
# Action with parameters and Say statement
|
209
|
+
code = """Action make_noise with `sound` as Text, `volume` as Number = _60_:
|
210
|
+
> Set `noise` to `sound`.
|
211
|
+
> Say `noise`."""
|
212
|
+
tree = self.parser.parse(code)
|
213
|
+
assert tree is not None
|
214
|
+
|
215
|
+
@pytest.mark.skip(reason="Actions with complex syntax not yet implemented")
|
216
|
+
def test_action_with_markdown_format(self) -> None:
|
217
|
+
"""Test parsing Action in markdown documentation format."""
|
218
|
+
# This represents the full markdown format with details tags
|
219
|
+
# The parser would need to handle this documentation-style format
|
220
|
+
code = """Action `make noise`:
|
221
|
+
<details>
|
222
|
+
<summary>Emits the sound of the alarm.</summary>
|
223
|
+
|
224
|
+
> Set `noise` to _"WEE-OO WEE-OO WEE-OO"_.
|
225
|
+
> Say `noise`.
|
226
|
+
|
227
|
+
</details>"""
|
228
|
+
tree = self.parser.parse(code)
|
229
|
+
assert tree is not None
|
230
|
+
|
231
|
+
@pytest.mark.skip(reason="Interactions not yet implemented in main parser")
|
232
|
+
def test_interaction_statement(self) -> None:
|
233
|
+
"""Test parsing Interaction statements."""
|
234
|
+
code = """Interaction turn_alarm_off:
|
235
|
+
> If `alarm_is_on` then:
|
236
|
+
> > Set `alarm_is_on` to _no_.
|
237
|
+
> > Say _"Alarm has been turned off"_."""
|
238
|
+
tree = self.parser.parse(code)
|
239
|
+
assert tree is not None
|
240
|
+
|
241
|
+
@pytest.mark.skip(reason="Say statements not yet implemented in main parser")
|
242
|
+
def test_say_statement(self) -> None:
|
243
|
+
"""Test parsing Say statements (used in actions/interactions)."""
|
244
|
+
test_cases = [
|
245
|
+
'Say _"Hello, World!"_.',
|
246
|
+
"Say `noise`.",
|
247
|
+
'Say _"Alarm is "_ + `status` + _"."_.',
|
248
|
+
]
|
249
|
+
|
250
|
+
for code in test_cases:
|
251
|
+
tree = self.parser.parse(code)
|
252
|
+
assert tree is not None
|
@@ -0,0 +1,188 @@
|
|
1
|
+
"""Tests for the configuration module."""
|
2
|
+
|
3
|
+
import os
|
4
|
+
import tempfile
|
5
|
+
from pathlib import Path
|
6
|
+
from unittest.mock import patch
|
7
|
+
|
8
|
+
from machine_dialect.cfg.config import AIAPIConfig, ConfigLoader, get_ai_config
|
9
|
+
|
10
|
+
|
11
|
+
class TestAIAPIConfig:
|
12
|
+
"""Test AIAPIConfig dataclass."""
|
13
|
+
|
14
|
+
def test_is_valid_with_both_fields(self) -> None:
|
15
|
+
"""Test is_valid returns True when both fields are set."""
|
16
|
+
config = AIAPIConfig(model="gpt-4o", key="test-key")
|
17
|
+
assert config.is_valid() is True
|
18
|
+
|
19
|
+
def test_is_valid_with_missing_model(self) -> None:
|
20
|
+
"""Test is_valid returns False when model is missing."""
|
21
|
+
config = AIAPIConfig(key="test-key")
|
22
|
+
assert config.is_valid() is False
|
23
|
+
|
24
|
+
def test_is_valid_with_missing_key(self) -> None:
|
25
|
+
"""Test is_valid returns False when key is missing."""
|
26
|
+
config = AIAPIConfig(model="gpt-4o")
|
27
|
+
assert config.is_valid() is False
|
28
|
+
|
29
|
+
def test_is_valid_with_both_missing(self) -> None:
|
30
|
+
"""Test is_valid returns False when both fields are missing."""
|
31
|
+
config = AIAPIConfig()
|
32
|
+
assert config.is_valid() is False
|
33
|
+
|
34
|
+
|
35
|
+
class TestConfigLoader:
|
36
|
+
"""Test ConfigLoader class."""
|
37
|
+
|
38
|
+
def test_load_from_config_file(self) -> None:
|
39
|
+
"""Test loading configuration from .mdconfig file."""
|
40
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
41
|
+
config_file = Path(tmpdir) / ".mdconfig"
|
42
|
+
config_file.write_text(
|
43
|
+
"""[ai-api]
|
44
|
+
model = gpt-4o-mini
|
45
|
+
key = test-api-key-123
|
46
|
+
"""
|
47
|
+
)
|
48
|
+
|
49
|
+
with patch.object(Path, "home", return_value=Path(tmpdir)):
|
50
|
+
loader = ConfigLoader()
|
51
|
+
config = loader.load()
|
52
|
+
|
53
|
+
assert config.model == "gpt-4o-mini"
|
54
|
+
assert config.key == "test-api-key-123"
|
55
|
+
|
56
|
+
def test_load_from_environment_variables(self) -> None:
|
57
|
+
"""Test loading configuration from environment variables."""
|
58
|
+
with patch.dict(os.environ, {"MD_AI_API_MODEL": "gpt-4-turbo", "MD_AI_API_KEY": "env-key-456"}):
|
59
|
+
with patch.object(Path, "exists", return_value=False):
|
60
|
+
loader = ConfigLoader()
|
61
|
+
config = loader.load()
|
62
|
+
|
63
|
+
assert config.model == "gpt-4-turbo"
|
64
|
+
assert config.key == "env-key-456"
|
65
|
+
|
66
|
+
def test_environment_overrides_file_partially(self) -> None:
|
67
|
+
"""Test that environment variables can override file config partially."""
|
68
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
69
|
+
config_file = Path(tmpdir) / ".mdconfig"
|
70
|
+
config_file.write_text(
|
71
|
+
"""[ai-api]
|
72
|
+
model = gpt-3.5-turbo
|
73
|
+
key = file-key
|
74
|
+
"""
|
75
|
+
)
|
76
|
+
|
77
|
+
with patch.object(Path, "home", return_value=Path(tmpdir)):
|
78
|
+
with patch.dict(os.environ, {"MD_AI_API_MODEL": "gpt-4o"}):
|
79
|
+
loader = ConfigLoader()
|
80
|
+
config = loader.load()
|
81
|
+
|
82
|
+
# Model from env, key from file
|
83
|
+
assert config.model == "gpt-4o"
|
84
|
+
assert config.key == "file-key"
|
85
|
+
|
86
|
+
def test_legacy_openai_key_fallback(self) -> None:
|
87
|
+
"""Test fallback to OPENAI_API_KEY for backward compatibility."""
|
88
|
+
with patch.dict(os.environ, {"OPENAI_API_KEY": "legacy-key-789"}):
|
89
|
+
with patch.object(Path, "exists", return_value=False):
|
90
|
+
loader = ConfigLoader()
|
91
|
+
config = loader.load()
|
92
|
+
|
93
|
+
assert config.key == "legacy-key-789"
|
94
|
+
assert config.model is None # No model specified
|
95
|
+
|
96
|
+
def test_md_key_overrides_openai_key(self) -> None:
|
97
|
+
"""Test that MD_AI_API_KEY takes precedence over OPENAI_API_KEY."""
|
98
|
+
with patch.dict(os.environ, {"MD_AI_API_KEY": "new-key", "OPENAI_API_KEY": "old-key"}):
|
99
|
+
with patch.object(Path, "exists", return_value=False):
|
100
|
+
loader = ConfigLoader()
|
101
|
+
config = loader.load()
|
102
|
+
|
103
|
+
assert config.key == "new-key"
|
104
|
+
|
105
|
+
def test_missing_config_section(self) -> None:
|
106
|
+
"""Test handling of missing [ai-api] section in config file."""
|
107
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
108
|
+
config_file = Path(tmpdir) / ".mdconfig"
|
109
|
+
config_file.write_text(
|
110
|
+
"""[other-section]
|
111
|
+
some = value
|
112
|
+
"""
|
113
|
+
)
|
114
|
+
|
115
|
+
with patch.object(Path, "home", return_value=Path(tmpdir)):
|
116
|
+
loader = ConfigLoader()
|
117
|
+
config = loader.load()
|
118
|
+
|
119
|
+
assert config.model is None
|
120
|
+
assert config.key is None
|
121
|
+
|
122
|
+
def test_empty_config_file(self) -> None:
|
123
|
+
"""Test handling of empty config file."""
|
124
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
125
|
+
config_file = Path(tmpdir) / ".mdconfig"
|
126
|
+
config_file.write_text("")
|
127
|
+
|
128
|
+
with patch.object(Path, "home", return_value=Path(tmpdir)):
|
129
|
+
loader = ConfigLoader()
|
130
|
+
config = loader.load()
|
131
|
+
|
132
|
+
assert config.model is None
|
133
|
+
assert config.key is None
|
134
|
+
|
135
|
+
def test_get_error_message(self) -> None:
|
136
|
+
"""Test error message generation."""
|
137
|
+
loader = ConfigLoader()
|
138
|
+
error_msg = loader.get_error_message()
|
139
|
+
|
140
|
+
assert "AI API configuration not found" in error_msg
|
141
|
+
assert ".mdconfig" in error_msg
|
142
|
+
assert "MD_AI_API_MODEL" in error_msg
|
143
|
+
assert "MD_AI_API_KEY" in error_msg
|
144
|
+
assert "OPENAI_API_KEY" in error_msg
|
145
|
+
|
146
|
+
def test_config_caching(self) -> None:
|
147
|
+
"""Test that configuration is cached after first load."""
|
148
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
149
|
+
config_file = Path(tmpdir) / ".mdconfig"
|
150
|
+
config_file.write_text(
|
151
|
+
"""[ai-api]
|
152
|
+
model = gpt-4o
|
153
|
+
key = test-key
|
154
|
+
"""
|
155
|
+
)
|
156
|
+
|
157
|
+
with patch.object(Path, "home", return_value=Path(tmpdir)):
|
158
|
+
loader = ConfigLoader()
|
159
|
+
config1 = loader.load()
|
160
|
+
|
161
|
+
# Modify the file
|
162
|
+
config_file.write_text(
|
163
|
+
"""[ai-api]
|
164
|
+
model = different-model
|
165
|
+
key = different-key
|
166
|
+
"""
|
167
|
+
)
|
168
|
+
|
169
|
+
# Should still get cached config
|
170
|
+
config2 = loader.load()
|
171
|
+
|
172
|
+
assert config1.model == config2.model
|
173
|
+
assert config1.key == config2.key
|
174
|
+
assert config2.model == "gpt-4o"
|
175
|
+
|
176
|
+
|
177
|
+
class TestGetAIConfig:
|
178
|
+
"""Test the convenience function get_ai_config."""
|
179
|
+
|
180
|
+
def test_get_ai_config(self) -> None:
|
181
|
+
"""Test that get_ai_config returns a valid config."""
|
182
|
+
with patch.dict(os.environ, {"MD_AI_API_MODEL": "test-model", "MD_AI_API_KEY": "test-key"}):
|
183
|
+
with patch.object(Path, "exists", return_value=False):
|
184
|
+
config = get_ai_config()
|
185
|
+
|
186
|
+
assert isinstance(config, AIAPIConfig)
|
187
|
+
assert config.model == "test-model"
|
188
|
+
assert config.key == "test-key"
|