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,144 @@
1
+ """Command-line interface for the Machine Dialect™ linter.
2
+
3
+ This module provides the CLI for running the linter on Machine Dialect™ files.
4
+ """
5
+
6
+ import argparse
7
+ import json
8
+ import sys
9
+ from pathlib import Path
10
+ from typing import Any
11
+
12
+ from machine_dialect.linter import Linter, Violation
13
+
14
+
15
+ def load_config(config_path: Path | None) -> dict[str, Any]:
16
+ """Load linter configuration from a file.
17
+
18
+ Args:
19
+ config_path: Path to the configuration file.
20
+
21
+ Returns:
22
+ Configuration dictionary.
23
+ """
24
+ if not config_path or not config_path.exists():
25
+ return {}
26
+
27
+ with open(config_path) as f:
28
+ if config_path.suffix == ".json":
29
+ return json.load(f) # type: ignore[no-any-return]
30
+ else:
31
+ # For now, only support JSON
32
+ print(f"Warning: Unsupported config format {config_path.suffix}, using defaults")
33
+ return {}
34
+
35
+
36
+ def format_violation(violation: Violation, filename: str) -> str:
37
+ """Format a violation for display.
38
+
39
+ Args:
40
+ violation: The violation to format.
41
+ filename: The filename where the violation occurred.
42
+
43
+ Returns:
44
+ A formatted string for display.
45
+ """
46
+ return f"{filename}:{violation}"
47
+
48
+
49
+ def main() -> None:
50
+ """Main entry point for the linter CLI."""
51
+ parser = argparse.ArgumentParser(description="Lint Machine Dialect™ code for style and errors")
52
+
53
+ parser.add_argument(
54
+ "files",
55
+ nargs="*", # Changed from "+" to "*" to make it optional
56
+ type=Path,
57
+ help="Machine Dialect™ files to lint",
58
+ )
59
+
60
+ parser.add_argument(
61
+ "-c",
62
+ "--config",
63
+ type=Path,
64
+ help="Path to configuration file",
65
+ )
66
+
67
+ parser.add_argument(
68
+ "-q",
69
+ "--quiet",
70
+ action="store_true",
71
+ help="Only show errors, not warnings",
72
+ )
73
+
74
+ parser.add_argument(
75
+ "--list-rules",
76
+ action="store_true",
77
+ help="List all available rules and exit",
78
+ )
79
+
80
+ args = parser.parse_args()
81
+
82
+ # Handle --list-rules
83
+ if args.list_rules:
84
+ linter = Linter()
85
+ print("Available linting rules:")
86
+ for rule in linter.rules:
87
+ print(f" {rule.rule_id}: {rule.description}")
88
+ sys.exit(0)
89
+
90
+ # Check if files were provided
91
+ if not args.files:
92
+ parser.error("No files specified to lint")
93
+
94
+ # Load configuration
95
+ config = load_config(args.config)
96
+ linter = Linter(config)
97
+
98
+ # Lint files
99
+ total_violations = 0
100
+ has_errors = False
101
+
102
+ for filepath in args.files:
103
+ if not filepath.exists():
104
+ print(f"Error: File not found: {filepath}", file=sys.stderr)
105
+ has_errors = True
106
+ continue
107
+
108
+ try:
109
+ violations = linter.lint_file(str(filepath))
110
+
111
+ # Filter violations if --quiet
112
+ if args.quiet:
113
+ from machine_dialect.linter.violations import ViolationSeverity
114
+
115
+ violations = [v for v in violations if v.severity == ViolationSeverity.ERROR]
116
+
117
+ # Display violations
118
+ for violation in violations:
119
+ print(format_violation(violation, str(filepath)))
120
+ if violation.fix_suggestion:
121
+ print(f" Suggestion: {violation.fix_suggestion}")
122
+
123
+ total_violations += len(violations)
124
+
125
+ # Check for errors
126
+ from machine_dialect.linter.violations import ViolationSeverity
127
+
128
+ if any(v.severity == ViolationSeverity.ERROR for v in violations):
129
+ has_errors = True
130
+
131
+ except Exception as e:
132
+ print(f"Error linting {filepath}: {e}", file=sys.stderr)
133
+ has_errors = True
134
+
135
+ # Summary
136
+ if not args.quiet and total_violations > 0:
137
+ print(f"\nFound {total_violations} issue(s)")
138
+
139
+ # Exit with error code if there were errors
140
+ sys.exit(1 if has_errors or total_violations > 0 else 0)
141
+
142
+
143
+ if __name__ == "__main__":
144
+ main()
@@ -0,0 +1,154 @@
1
+ """Main linter class for Machine Dialect™.
2
+
3
+ This module provides the main Linter class that orchestrates the
4
+ linting process by running rules against an AST.
5
+ """
6
+
7
+ from typing import Any
8
+
9
+ from machine_dialect.ast import (
10
+ ASTNode,
11
+ ExpressionStatement,
12
+ PrefixExpression,
13
+ Program,
14
+ ReturnStatement,
15
+ SetStatement,
16
+ )
17
+ from machine_dialect.linter.rules import Rule
18
+ from machine_dialect.linter.rules.base import Context
19
+ from machine_dialect.linter.violations import Violation, ViolationSeverity
20
+ from machine_dialect.parser import Parser
21
+
22
+
23
+ class Linter:
24
+ """Main linter class that runs rules against Machine Dialect™ code.
25
+
26
+ The linter uses a visitor pattern to traverse the AST and apply
27
+ registered rules to each node.
28
+ """
29
+
30
+ def __init__(self, config: dict[str, Any] | None = None) -> None:
31
+ """Initialize the linter with optional configuration.
32
+
33
+ Args:
34
+ config: Configuration dictionary for the linter.
35
+ """
36
+ self.config = config or {}
37
+ self.rules: list[Rule] = []
38
+ self._register_default_rules()
39
+
40
+ def _register_default_rules(self) -> None:
41
+ """Register the default set of linting rules."""
42
+ # Import rules here to avoid circular imports
43
+ from machine_dialect.linter.rules.statement_termination import StatementTerminationRule
44
+
45
+ # Add default rules
46
+ self.add_rule(StatementTerminationRule())
47
+
48
+ def add_rule(self, rule: Rule) -> None:
49
+ """Add a linting rule to the linter.
50
+
51
+ Args:
52
+ rule: The rule instance to add.
53
+ """
54
+ if rule.is_enabled(self.config):
55
+ self.rules.append(rule)
56
+
57
+ def lint(self, source_code: str, filename: str = "<stdin>") -> list[Violation]:
58
+ """Lint the given source code.
59
+
60
+ Args:
61
+ source_code: The Machine Dialect™ source code to lint.
62
+ filename: The filename for error reporting.
63
+
64
+ Returns:
65
+ A list of violations found in the code.
66
+ """
67
+ # Parse the source code
68
+ parser = Parser()
69
+ program = parser.parse(source_code)
70
+
71
+ # Include parse errors as violations
72
+ violations: list[Violation] = []
73
+ for error in parser.errors:
74
+ violations.append(
75
+ Violation(
76
+ rule_id="parse-error",
77
+ message=str(error),
78
+ severity=ViolationSeverity.ERROR,
79
+ line=error._line,
80
+ column=error._column,
81
+ )
82
+ )
83
+
84
+ # If there are parse errors, don't run other rules
85
+ if violations:
86
+ return violations
87
+
88
+ # Create context
89
+ context = Context(filename, source_code)
90
+
91
+ # Visit the AST and collect violations
92
+ violations.extend(self._visit_node(program, context))
93
+
94
+ return violations
95
+
96
+ def _visit_node(self, node: ASTNode | None, context: Context) -> list[Violation]:
97
+ """Visit an AST node and its children, applying rules.
98
+
99
+ Args:
100
+ node: The AST node to visit.
101
+ context: The linting context.
102
+
103
+ Returns:
104
+ A list of violations found.
105
+ """
106
+ if node is None:
107
+ return []
108
+
109
+ violations: list[Violation] = []
110
+
111
+ # Apply all rules to this node
112
+ for rule in self.rules:
113
+ violations.extend(rule.check(node, context))
114
+
115
+ # Visit children based on node type
116
+ context.push_parent(node)
117
+
118
+ if isinstance(node, Program):
119
+ for statement in node.statements:
120
+ violations.extend(self._visit_node(statement, context))
121
+
122
+ elif isinstance(node, SetStatement):
123
+ violations.extend(self._visit_node(node.name, context))
124
+ violations.extend(self._visit_node(node.value, context))
125
+
126
+ elif isinstance(node, ReturnStatement):
127
+ # ReturnStatement doesn't have a value attribute yet (TODO)
128
+ # violations.extend(self._visit_node(node.value, context))
129
+ pass
130
+
131
+ elif isinstance(node, ExpressionStatement):
132
+ violations.extend(self._visit_node(node.expression, context))
133
+
134
+ elif isinstance(node, PrefixExpression):
135
+ violations.extend(self._visit_node(node.right, context))
136
+
137
+ # Literals and identifiers have no children to visit
138
+
139
+ context.pop_parent()
140
+ return violations
141
+
142
+ def lint_file(self, filepath: str) -> list[Violation]:
143
+ """Lint a file by reading its contents and running the linter.
144
+
145
+ Args:
146
+ filepath: Path to the file to lint.
147
+
148
+ Returns:
149
+ A list of violations found in the file.
150
+ """
151
+ with open(filepath, encoding="utf-8") as f:
152
+ source_code = f.read()
153
+
154
+ return self.lint(source_code, filepath)
@@ -0,0 +1,8 @@
1
+ """Linting rules for Machine Dialect™.
2
+
3
+ This module manages the registration and execution of linting rules.
4
+ """
5
+
6
+ from .base import Rule
7
+
8
+ __all__ = ["Rule"]
@@ -0,0 +1,112 @@
1
+ """Base rule class for Machine Dialect™ linting rules.
2
+
3
+ This module defines the abstract base class that all linting rules
4
+ must inherit from.
5
+ """
6
+
7
+ from abc import ABC, abstractmethod
8
+ from typing import Any
9
+
10
+ from machine_dialect.ast import ASTNode
11
+ from machine_dialect.linter.violations import Violation
12
+
13
+
14
+ class Context:
15
+ """Context information passed to rules during linting.
16
+
17
+ Attributes:
18
+ filename: The name of the file being linted.
19
+ source_lines: The source code split into lines.
20
+ parent_stack: Stack of parent nodes for context.
21
+ """
22
+
23
+ def __init__(self, filename: str, source_code: str) -> None:
24
+ """Initialize the linting context.
25
+
26
+ Args:
27
+ filename: The name of the file being linted.
28
+ source_code: The complete source code.
29
+ """
30
+ self.filename = filename
31
+ self.source_code = source_code
32
+ self.source_lines = source_code.splitlines()
33
+ self.parent_stack: list[ASTNode] = []
34
+
35
+ def push_parent(self, node: ASTNode) -> None:
36
+ """Push a parent node onto the stack.
37
+
38
+ Args:
39
+ node: The parent node to push.
40
+ """
41
+ self.parent_stack.append(node)
42
+
43
+ def pop_parent(self) -> ASTNode | None:
44
+ """Pop a parent node from the stack.
45
+
46
+ Returns:
47
+ The popped parent node, or None if stack is empty.
48
+ """
49
+ return self.parent_stack.pop() if self.parent_stack else None
50
+
51
+ @property
52
+ def current_parent(self) -> ASTNode | None:
53
+ """Get the current parent node without removing it.
54
+
55
+ Returns:
56
+ The current parent node, or None if stack is empty.
57
+ """
58
+ return self.parent_stack[-1] if self.parent_stack else None
59
+
60
+
61
+ class Rule(ABC):
62
+ """Abstract base class for linting rules.
63
+
64
+ All linting rules must inherit from this class and implement
65
+ the required methods.
66
+ """
67
+
68
+ @property
69
+ @abstractmethod
70
+ def rule_id(self) -> str:
71
+ """Return the unique identifier for this rule.
72
+
73
+ Returns:
74
+ A string identifier like "MD001" or "style-naming".
75
+ """
76
+ pass
77
+
78
+ @property
79
+ @abstractmethod
80
+ def description(self) -> str:
81
+ """Return a human-readable description of what this rule checks.
82
+
83
+ Returns:
84
+ A description of the rule's purpose.
85
+ """
86
+ pass
87
+
88
+ @abstractmethod
89
+ def check(self, node: ASTNode, context: Context) -> list[Violation]:
90
+ """Check the given AST node for violations of this rule.
91
+
92
+ Args:
93
+ node: The AST node to check.
94
+ context: The linting context with additional information.
95
+
96
+ Returns:
97
+ A list of violations found, or empty list if none.
98
+ """
99
+ pass
100
+
101
+ def is_enabled(self, config: dict[str, Any]) -> bool:
102
+ """Check if this rule is enabled in the configuration.
103
+
104
+ Args:
105
+ config: The linter configuration dictionary.
106
+
107
+ Returns:
108
+ True if the rule is enabled, False otherwise.
109
+ """
110
+ # By default, rules are enabled unless explicitly disabled
111
+ rules_config = config.get("rules", {})
112
+ return rules_config.get(self.rule_id, True) is not False
@@ -0,0 +1,99 @@
1
+ """Statement termination rule for Machine Dialect™.
2
+
3
+ This rule checks that all statements are properly terminated with periods.
4
+ """
5
+
6
+ from machine_dialect.ast import (
7
+ ASTNode,
8
+ ExpressionStatement,
9
+ ReturnStatement,
10
+ SetStatement,
11
+ )
12
+ from machine_dialect.linter.rules.base import Context, Rule
13
+ from machine_dialect.linter.violations import Violation, ViolationSeverity
14
+
15
+
16
+ class StatementTerminationRule(Rule):
17
+ """Check that all statements end with periods.
18
+
19
+ Machine Dialect™ requires statements to be terminated with periods.
20
+ This rule checks that the source code follows this convention.
21
+ """
22
+
23
+ @property
24
+ def rule_id(self) -> str:
25
+ """Return the rule identifier."""
26
+ return "MD101"
27
+
28
+ @property
29
+ def description(self) -> str:
30
+ """Return the rule description."""
31
+ return "Statements must end with periods"
32
+
33
+ def check(self, node: ASTNode, context: Context) -> list[Violation]:
34
+ """Check if statements are properly terminated.
35
+
36
+ Args:
37
+ node: The AST node to check.
38
+ context: The linting context.
39
+
40
+ Returns:
41
+ A list of violations found.
42
+ """
43
+ violations: list[Violation] = []
44
+
45
+ # Only check statement nodes
46
+ if not isinstance(node, SetStatement | ReturnStatement | ExpressionStatement):
47
+ return violations
48
+
49
+ # Get the token that represents this statement
50
+ token = node.token
51
+ if not token:
52
+ return violations
53
+
54
+ # Find the line in the source code
55
+ if token.line <= len(context.source_lines):
56
+ line = context.source_lines[token.line - 1]
57
+
58
+ # Find where this statement likely ends
59
+ # This is a simplified check - in reality we'd need more sophisticated logic
60
+ # For now, check if there's a period after the statement
61
+
62
+ # Skip if this is not the last statement on the line
63
+ # (simplified check - just look for period somewhere after the token position)
64
+ remaining_line = line[token.position :]
65
+
66
+ # Check if there's any non-whitespace after the statement before a period
67
+ found_period = False
68
+ found_content = False
69
+
70
+ for char in remaining_line:
71
+ if char == ".":
72
+ found_period = True
73
+ break
74
+ elif not char.isspace():
75
+ found_content = True
76
+
77
+ # If we found content but no period, it's likely missing termination
78
+ # This is a heuristic and may have false positives
79
+ # Exception: statements at EOF don't require periods
80
+ is_last_line = token.line == len(context.source_lines)
81
+ is_last_statement_on_line = not any(
82
+ line[i:].strip() for i in range(token.position + len(remaining_line.rstrip()), len(line))
83
+ )
84
+ is_at_eof = is_last_line and is_last_statement_on_line
85
+
86
+ if found_content and not found_period and not is_at_eof:
87
+ violations.append(
88
+ Violation(
89
+ rule_id=self.rule_id,
90
+ message="Statement should end with a period",
91
+ severity=ViolationSeverity.STYLE,
92
+ line=token.line,
93
+ column=token.position + len(remaining_line.rstrip()),
94
+ node=node,
95
+ fix_suggestion="Add a period at the end of the statement",
96
+ )
97
+ )
98
+
99
+ return violations
@@ -0,0 +1 @@
1
+ """Tests for the Machine Dialect™ linter."""
File without changes