machine-dialect 0.1.0a1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (268) hide show
  1. machine_dialect/__main__.py +667 -0
  2. machine_dialect/agent/__init__.py +5 -0
  3. machine_dialect/agent/agent.py +360 -0
  4. machine_dialect/ast/__init__.py +95 -0
  5. machine_dialect/ast/ast_node.py +35 -0
  6. machine_dialect/ast/call_expression.py +82 -0
  7. machine_dialect/ast/dict_extraction.py +60 -0
  8. machine_dialect/ast/expressions.py +439 -0
  9. machine_dialect/ast/literals.py +309 -0
  10. machine_dialect/ast/program.py +35 -0
  11. machine_dialect/ast/statements.py +1433 -0
  12. machine_dialect/ast/tests/test_ast_string_representation.py +62 -0
  13. machine_dialect/ast/tests/test_boolean_literal.py +29 -0
  14. machine_dialect/ast/tests/test_collection_hir.py +138 -0
  15. machine_dialect/ast/tests/test_define_statement.py +142 -0
  16. machine_dialect/ast/tests/test_desugar.py +541 -0
  17. machine_dialect/ast/tests/test_foreach_desugar.py +245 -0
  18. machine_dialect/cfg/__init__.py +6 -0
  19. machine_dialect/cfg/config.py +156 -0
  20. machine_dialect/cfg/examples.py +221 -0
  21. machine_dialect/cfg/generate_with_ai.py +187 -0
  22. machine_dialect/cfg/openai_generation.py +200 -0
  23. machine_dialect/cfg/parser.py +94 -0
  24. machine_dialect/cfg/tests/__init__.py +1 -0
  25. machine_dialect/cfg/tests/test_cfg_parser.py +252 -0
  26. machine_dialect/cfg/tests/test_config.py +188 -0
  27. machine_dialect/cfg/tests/test_examples.py +391 -0
  28. machine_dialect/cfg/tests/test_generate_with_ai.py +354 -0
  29. machine_dialect/cfg/tests/test_openai_generation.py +256 -0
  30. machine_dialect/codegen/__init__.py +5 -0
  31. machine_dialect/codegen/bytecode_module.py +89 -0
  32. machine_dialect/codegen/bytecode_serializer.py +300 -0
  33. machine_dialect/codegen/opcodes.py +101 -0
  34. machine_dialect/codegen/register_codegen.py +1996 -0
  35. machine_dialect/codegen/symtab.py +208 -0
  36. machine_dialect/codegen/tests/__init__.py +1 -0
  37. machine_dialect/codegen/tests/test_array_operations_codegen.py +295 -0
  38. machine_dialect/codegen/tests/test_bytecode_serializer.py +185 -0
  39. machine_dialect/codegen/tests/test_register_codegen_ssa.py +324 -0
  40. machine_dialect/codegen/tests/test_symtab.py +418 -0
  41. machine_dialect/codegen/vm_serializer.py +621 -0
  42. machine_dialect/compiler/__init__.py +18 -0
  43. machine_dialect/compiler/compiler.py +197 -0
  44. machine_dialect/compiler/config.py +149 -0
  45. machine_dialect/compiler/context.py +149 -0
  46. machine_dialect/compiler/phases/__init__.py +19 -0
  47. machine_dialect/compiler/phases/bytecode_optimization.py +90 -0
  48. machine_dialect/compiler/phases/codegen.py +40 -0
  49. machine_dialect/compiler/phases/hir_generation.py +39 -0
  50. machine_dialect/compiler/phases/mir_generation.py +86 -0
  51. machine_dialect/compiler/phases/optimization.py +110 -0
  52. machine_dialect/compiler/phases/parsing.py +39 -0
  53. machine_dialect/compiler/pipeline.py +143 -0
  54. machine_dialect/compiler/tests/__init__.py +1 -0
  55. machine_dialect/compiler/tests/test_compiler.py +568 -0
  56. machine_dialect/compiler/vm_runner.py +173 -0
  57. machine_dialect/errors/__init__.py +32 -0
  58. machine_dialect/errors/exceptions.py +369 -0
  59. machine_dialect/errors/messages.py +82 -0
  60. machine_dialect/errors/tests/__init__.py +0 -0
  61. machine_dialect/errors/tests/test_expected_token_errors.py +188 -0
  62. machine_dialect/errors/tests/test_name_errors.py +118 -0
  63. machine_dialect/helpers/__init__.py +0 -0
  64. machine_dialect/helpers/stopwords.py +225 -0
  65. machine_dialect/helpers/validators.py +30 -0
  66. machine_dialect/lexer/__init__.py +9 -0
  67. machine_dialect/lexer/constants.py +23 -0
  68. machine_dialect/lexer/lexer.py +907 -0
  69. machine_dialect/lexer/tests/__init__.py +0 -0
  70. machine_dialect/lexer/tests/helpers.py +86 -0
  71. machine_dialect/lexer/tests/test_apostrophe_identifiers.py +122 -0
  72. machine_dialect/lexer/tests/test_backtick_identifiers.py +140 -0
  73. machine_dialect/lexer/tests/test_boolean_literals.py +108 -0
  74. machine_dialect/lexer/tests/test_case_insensitive_keywords.py +188 -0
  75. machine_dialect/lexer/tests/test_comments.py +200 -0
  76. machine_dialect/lexer/tests/test_double_asterisk_keywords.py +127 -0
  77. machine_dialect/lexer/tests/test_lexer_position.py +113 -0
  78. machine_dialect/lexer/tests/test_list_tokens.py +282 -0
  79. machine_dialect/lexer/tests/test_stopwords.py +80 -0
  80. machine_dialect/lexer/tests/test_strict_equality.py +129 -0
  81. machine_dialect/lexer/tests/test_token.py +41 -0
  82. machine_dialect/lexer/tests/test_tokenization.py +294 -0
  83. machine_dialect/lexer/tests/test_underscore_literals.py +343 -0
  84. machine_dialect/lexer/tests/test_url_literals.py +169 -0
  85. machine_dialect/lexer/tokens.py +487 -0
  86. machine_dialect/linter/__init__.py +10 -0
  87. machine_dialect/linter/__main__.py +144 -0
  88. machine_dialect/linter/linter.py +154 -0
  89. machine_dialect/linter/rules/__init__.py +8 -0
  90. machine_dialect/linter/rules/base.py +112 -0
  91. machine_dialect/linter/rules/statement_termination.py +99 -0
  92. machine_dialect/linter/tests/__init__.py +1 -0
  93. machine_dialect/linter/tests/mdrules/__init__.py +0 -0
  94. machine_dialect/linter/tests/mdrules/test_md101_statement_termination.py +181 -0
  95. machine_dialect/linter/tests/test_linter.py +81 -0
  96. machine_dialect/linter/tests/test_rules.py +110 -0
  97. machine_dialect/linter/tests/test_violations.py +71 -0
  98. machine_dialect/linter/violations.py +51 -0
  99. machine_dialect/mir/__init__.py +69 -0
  100. machine_dialect/mir/analyses/__init__.py +20 -0
  101. machine_dialect/mir/analyses/alias_analysis.py +315 -0
  102. machine_dialect/mir/analyses/dominance_analysis.py +49 -0
  103. machine_dialect/mir/analyses/escape_analysis.py +286 -0
  104. machine_dialect/mir/analyses/loop_analysis.py +272 -0
  105. machine_dialect/mir/analyses/tests/test_type_analysis.py +736 -0
  106. machine_dialect/mir/analyses/type_analysis.py +448 -0
  107. machine_dialect/mir/analyses/use_def_chains.py +232 -0
  108. machine_dialect/mir/basic_block.py +385 -0
  109. machine_dialect/mir/dataflow.py +445 -0
  110. machine_dialect/mir/debug_info.py +208 -0
  111. machine_dialect/mir/hir_to_mir.py +1738 -0
  112. machine_dialect/mir/mir_dumper.py +366 -0
  113. machine_dialect/mir/mir_function.py +167 -0
  114. machine_dialect/mir/mir_instructions.py +1877 -0
  115. machine_dialect/mir/mir_interpreter.py +556 -0
  116. machine_dialect/mir/mir_module.py +225 -0
  117. machine_dialect/mir/mir_printer.py +480 -0
  118. machine_dialect/mir/mir_transformer.py +410 -0
  119. machine_dialect/mir/mir_types.py +367 -0
  120. machine_dialect/mir/mir_validation.py +455 -0
  121. machine_dialect/mir/mir_values.py +268 -0
  122. machine_dialect/mir/optimization_config.py +233 -0
  123. machine_dialect/mir/optimization_pass.py +251 -0
  124. machine_dialect/mir/optimization_pipeline.py +355 -0
  125. machine_dialect/mir/optimizations/__init__.py +84 -0
  126. machine_dialect/mir/optimizations/algebraic_simplification.py +733 -0
  127. machine_dialect/mir/optimizations/branch_prediction.py +372 -0
  128. machine_dialect/mir/optimizations/constant_propagation.py +634 -0
  129. machine_dialect/mir/optimizations/cse.py +398 -0
  130. machine_dialect/mir/optimizations/dce.py +288 -0
  131. machine_dialect/mir/optimizations/inlining.py +551 -0
  132. machine_dialect/mir/optimizations/jump_threading.py +487 -0
  133. machine_dialect/mir/optimizations/licm.py +405 -0
  134. machine_dialect/mir/optimizations/loop_unrolling.py +366 -0
  135. machine_dialect/mir/optimizations/strength_reduction.py +422 -0
  136. machine_dialect/mir/optimizations/tail_call.py +207 -0
  137. machine_dialect/mir/optimizations/tests/test_loop_unrolling.py +483 -0
  138. machine_dialect/mir/optimizations/type_narrowing.py +397 -0
  139. machine_dialect/mir/optimizations/type_specialization.py +447 -0
  140. machine_dialect/mir/optimizations/type_specific.py +906 -0
  141. machine_dialect/mir/optimize_mir.py +89 -0
  142. machine_dialect/mir/pass_manager.py +391 -0
  143. machine_dialect/mir/profiling/__init__.py +26 -0
  144. machine_dialect/mir/profiling/profile_collector.py +318 -0
  145. machine_dialect/mir/profiling/profile_data.py +372 -0
  146. machine_dialect/mir/profiling/profile_reader.py +272 -0
  147. machine_dialect/mir/profiling/profile_writer.py +226 -0
  148. machine_dialect/mir/register_allocation.py +302 -0
  149. machine_dialect/mir/reporting/__init__.py +17 -0
  150. machine_dialect/mir/reporting/optimization_reporter.py +314 -0
  151. machine_dialect/mir/reporting/report_formatter.py +289 -0
  152. machine_dialect/mir/ssa_construction.py +342 -0
  153. machine_dialect/mir/tests/__init__.py +1 -0
  154. machine_dialect/mir/tests/test_algebraic_associativity.py +204 -0
  155. machine_dialect/mir/tests/test_algebraic_complex_patterns.py +221 -0
  156. machine_dialect/mir/tests/test_algebraic_division.py +126 -0
  157. machine_dialect/mir/tests/test_algebraic_simplification.py +863 -0
  158. machine_dialect/mir/tests/test_basic_block.py +425 -0
  159. machine_dialect/mir/tests/test_branch_prediction.py +459 -0
  160. machine_dialect/mir/tests/test_call_lowering.py +168 -0
  161. machine_dialect/mir/tests/test_collection_lowering.py +604 -0
  162. machine_dialect/mir/tests/test_cross_block_constant_propagation.py +255 -0
  163. machine_dialect/mir/tests/test_custom_passes.py +166 -0
  164. machine_dialect/mir/tests/test_debug_info.py +285 -0
  165. machine_dialect/mir/tests/test_dict_extraction_lowering.py +192 -0
  166. machine_dialect/mir/tests/test_dictionary_lowering.py +299 -0
  167. machine_dialect/mir/tests/test_double_negation.py +231 -0
  168. machine_dialect/mir/tests/test_escape_analysis.py +233 -0
  169. machine_dialect/mir/tests/test_hir_to_mir.py +465 -0
  170. machine_dialect/mir/tests/test_hir_to_mir_complete.py +389 -0
  171. machine_dialect/mir/tests/test_hir_to_mir_simple.py +130 -0
  172. machine_dialect/mir/tests/test_inlining.py +435 -0
  173. machine_dialect/mir/tests/test_licm.py +472 -0
  174. machine_dialect/mir/tests/test_mir_dumper.py +313 -0
  175. machine_dialect/mir/tests/test_mir_instructions.py +445 -0
  176. machine_dialect/mir/tests/test_mir_module.py +860 -0
  177. machine_dialect/mir/tests/test_mir_printer.py +387 -0
  178. machine_dialect/mir/tests/test_mir_types.py +123 -0
  179. machine_dialect/mir/tests/test_mir_types_enhanced.py +132 -0
  180. machine_dialect/mir/tests/test_mir_validation.py +378 -0
  181. machine_dialect/mir/tests/test_mir_values.py +168 -0
  182. machine_dialect/mir/tests/test_one_based_indexing.py +202 -0
  183. machine_dialect/mir/tests/test_optimization_helpers.py +60 -0
  184. machine_dialect/mir/tests/test_optimization_pipeline.py +554 -0
  185. machine_dialect/mir/tests/test_optimization_reporter.py +318 -0
  186. machine_dialect/mir/tests/test_pass_manager.py +294 -0
  187. machine_dialect/mir/tests/test_pass_registration.py +64 -0
  188. machine_dialect/mir/tests/test_profiling.py +356 -0
  189. machine_dialect/mir/tests/test_register_allocation.py +307 -0
  190. machine_dialect/mir/tests/test_report_formatters.py +372 -0
  191. machine_dialect/mir/tests/test_ssa_construction.py +433 -0
  192. machine_dialect/mir/tests/test_tail_call.py +236 -0
  193. machine_dialect/mir/tests/test_type_annotated_instructions.py +192 -0
  194. machine_dialect/mir/tests/test_type_narrowing.py +277 -0
  195. machine_dialect/mir/tests/test_type_specialization.py +421 -0
  196. machine_dialect/mir/tests/test_type_specific_optimization.py +545 -0
  197. machine_dialect/mir/tests/test_type_specific_optimization_advanced.py +382 -0
  198. machine_dialect/mir/type_inference.py +368 -0
  199. machine_dialect/parser/__init__.py +12 -0
  200. machine_dialect/parser/enums.py +45 -0
  201. machine_dialect/parser/parser.py +3655 -0
  202. machine_dialect/parser/protocols.py +11 -0
  203. machine_dialect/parser/symbol_table.py +169 -0
  204. machine_dialect/parser/tests/__init__.py +0 -0
  205. machine_dialect/parser/tests/helper_functions.py +193 -0
  206. machine_dialect/parser/tests/test_action_statements.py +334 -0
  207. machine_dialect/parser/tests/test_boolean_literal_expressions.py +152 -0
  208. machine_dialect/parser/tests/test_call_statements.py +154 -0
  209. machine_dialect/parser/tests/test_call_statements_errors.py +187 -0
  210. machine_dialect/parser/tests/test_collection_mutations.py +264 -0
  211. machine_dialect/parser/tests/test_conditional_expressions.py +343 -0
  212. machine_dialect/parser/tests/test_define_integration.py +468 -0
  213. machine_dialect/parser/tests/test_define_statements.py +311 -0
  214. machine_dialect/parser/tests/test_dict_extraction.py +115 -0
  215. machine_dialect/parser/tests/test_empty_literal.py +155 -0
  216. machine_dialect/parser/tests/test_float_literal_expressions.py +163 -0
  217. machine_dialect/parser/tests/test_identifier_expressions.py +57 -0
  218. machine_dialect/parser/tests/test_if_empty_block.py +61 -0
  219. machine_dialect/parser/tests/test_if_statements.py +299 -0
  220. machine_dialect/parser/tests/test_illegal_tokens.py +86 -0
  221. machine_dialect/parser/tests/test_infix_expressions.py +680 -0
  222. machine_dialect/parser/tests/test_integer_literal_expressions.py +137 -0
  223. machine_dialect/parser/tests/test_interaction_statements.py +269 -0
  224. machine_dialect/parser/tests/test_list_literals.py +277 -0
  225. machine_dialect/parser/tests/test_no_none_in_ast.py +94 -0
  226. machine_dialect/parser/tests/test_panic_mode_recovery.py +171 -0
  227. machine_dialect/parser/tests/test_parse_errors.py +114 -0
  228. machine_dialect/parser/tests/test_possessive_syntax.py +182 -0
  229. machine_dialect/parser/tests/test_prefix_expressions.py +415 -0
  230. machine_dialect/parser/tests/test_program.py +13 -0
  231. machine_dialect/parser/tests/test_return_statements.py +89 -0
  232. machine_dialect/parser/tests/test_set_statements.py +152 -0
  233. machine_dialect/parser/tests/test_strict_equality.py +258 -0
  234. machine_dialect/parser/tests/test_symbol_table.py +217 -0
  235. machine_dialect/parser/tests/test_url_literal_expressions.py +209 -0
  236. machine_dialect/parser/tests/test_utility_statements.py +423 -0
  237. machine_dialect/parser/token_buffer.py +159 -0
  238. machine_dialect/repl/__init__.py +3 -0
  239. machine_dialect/repl/repl.py +426 -0
  240. machine_dialect/repl/tests/__init__.py +0 -0
  241. machine_dialect/repl/tests/test_repl.py +606 -0
  242. machine_dialect/semantic/__init__.py +12 -0
  243. machine_dialect/semantic/analyzer.py +906 -0
  244. machine_dialect/semantic/error_messages.py +189 -0
  245. machine_dialect/semantic/tests/__init__.py +1 -0
  246. machine_dialect/semantic/tests/test_analyzer.py +364 -0
  247. machine_dialect/semantic/tests/test_error_messages.py +104 -0
  248. machine_dialect/tests/edge_cases/__init__.py +10 -0
  249. machine_dialect/tests/edge_cases/test_boundary_access.py +256 -0
  250. machine_dialect/tests/edge_cases/test_empty_collections.py +166 -0
  251. machine_dialect/tests/edge_cases/test_invalid_operations.py +243 -0
  252. machine_dialect/tests/edge_cases/test_named_list_edge_cases.py +295 -0
  253. machine_dialect/tests/edge_cases/test_nested_structures.py +313 -0
  254. machine_dialect/tests/edge_cases/test_type_mixing.py +277 -0
  255. machine_dialect/tests/integration/test_array_operations_emulation.py +248 -0
  256. machine_dialect/tests/integration/test_list_compilation.py +395 -0
  257. machine_dialect/tests/integration/test_lists_and_dictionaries.py +322 -0
  258. machine_dialect/type_checking/__init__.py +21 -0
  259. machine_dialect/type_checking/tests/__init__.py +1 -0
  260. machine_dialect/type_checking/tests/test_type_system.py +230 -0
  261. machine_dialect/type_checking/type_system.py +270 -0
  262. machine_dialect-0.1.0a1.dist-info/METADATA +128 -0
  263. machine_dialect-0.1.0a1.dist-info/RECORD +268 -0
  264. machine_dialect-0.1.0a1.dist-info/WHEEL +5 -0
  265. machine_dialect-0.1.0a1.dist-info/entry_points.txt +3 -0
  266. machine_dialect-0.1.0a1.dist-info/licenses/LICENSE +201 -0
  267. machine_dialect-0.1.0a1.dist-info/top_level.txt +2 -0
  268. machine_dialect_vm/__init__.pyi +15 -0
@@ -0,0 +1,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"