|
43 | 43 | from multilingualprogramming.exceptions import UnsupportedLanguageError |
44 | 44 | from multilingualprogramming.lexer.lexer import Lexer |
45 | 45 | from multilingualprogramming.parser.parser import Parser |
46 | | -from multilingualprogramming.parser.ast_nodes import ImportStatement, FromImportStatement, Program |
47 | 46 | from multilingualprogramming.runtime.ai_runtime import AIRuntime, MockProvider |
48 | 47 | from multilingualprogramming.source_extensions import ( |
49 | 48 | find_module_source, |
@@ -107,75 +106,60 @@ def _parse_program_from_file(path: str, lang: str | None): |
107 | 106 | return parser.parse() |
108 | 107 |
|
109 | 108 |
|
110 | | -def _resolve_module_path(module_name: str, base_dir: Path) -> Path | None: |
111 | | - """Resolve a module name (e.g., 'ui.composants.barre_recherche') to a file path.""" |
112 | | - # Convert dot notation to file path |
113 | | - parts = module_name.split(".") |
114 | | - # Try relative to base directory first |
115 | | - candidate = base_dir.joinpath(*parts).with_suffix(".multi") |
116 | | - if candidate.exists(): |
117 | | - return candidate |
118 | | - # Try one level up (in case we're in a subdirectory) |
119 | | - candidate = base_dir.parent.joinpath(*parts).with_suffix(".multi") |
120 | | - if candidate.exists(): |
121 | | - return candidate |
122 | | - return None |
123 | | - |
124 | | - |
125 | | -def _merge_programs(main_program: Program, imported_program: Program) -> Program: |
126 | | - """Merge imported program statements into main program.""" |
127 | | - # Combine statements, removing duplicate imports |
128 | | - seen_imports = set() |
129 | | - merged_statements = [] |
130 | | - |
131 | | - # Add unique imports from both programs |
132 | | - for stmt in main_program.body + imported_program.body: |
133 | | - if isinstance(stmt, (ImportStatement, FromImportStatement)): |
134 | | - # Create a unique key for the import |
135 | | - key = (type(stmt).__name__, getattr(stmt, 'module', '')) |
136 | | - if key not in seen_imports: |
137 | | - seen_imports.add(key) |
138 | | - merged_statements.append(stmt) |
139 | | - else: |
140 | | - merged_statements.append(stmt) |
141 | | - |
142 | | - # Create new program with merged statements |
143 | | - new_program = Program(merged_statements) |
144 | | - return new_program |
145 | | - |
| 109 | +def _parse_ir_from_file(path: str | Path, lang: str | None): |
| 110 | + resolved = Path(path) |
| 111 | + source = _read_source_file(str(resolved)) |
| 112 | + lexer = Lexer(source, language=lang) |
| 113 | + tokens = lexer.tokenize() |
| 114 | + detected_lang = lexer.language or lang or "en" |
| 115 | + parser = Parser(tokens, source_language=detected_lang) |
| 116 | + program = parser.parse() |
| 117 | + return lower_to_semantic_ir(program, detected_lang) |
146 | 118 |
|
147 | | -def _parse_program_with_dependencies(path: str, lang: str | None) -> Program: |
148 | | - """Parse a program and recursively include all imported modules.""" |
149 | | - main_program = _parse_program_from_file(path, lang) |
150 | | - base_dir = Path(path).resolve().parent |
151 | | - processed = set() |
152 | 119 |
|
153 | | - def _collect_imports(program: Program, current_dir: Path) -> Program: |
154 | | - """Recursively collect imports and merge programs.""" |
155 | | - result = program |
| 120 | +def _resolve_absolute_module_source(entry_file: Path, module_name: str) -> Path | None: |
| 121 | + parts = module_name.split(".") |
| 122 | + search_root = entry_file.resolve().parent |
| 123 | + while True: |
| 124 | + base = search_root.joinpath(*parts[:-1]) if len(parts) > 1 else search_root |
| 125 | + candidate = find_module_source(base, parts[-1]) |
| 126 | + if candidate is not None: |
| 127 | + return candidate |
| 128 | + package_dir = search_root.joinpath(*parts) |
| 129 | + package_init = find_package_init(package_dir) |
| 130 | + if package_init is not None: |
| 131 | + return package_init |
| 132 | + if search_root.parent == search_root: |
| 133 | + return None |
| 134 | + search_root = search_root.parent |
156 | 135 |
|
157 | | - for stmt in program.body: |
158 | | - if isinstance(stmt, ImportStatement) and stmt.module: |
159 | | - # Skip if already processed |
160 | | - if stmt.module in processed: |
161 | | - continue |
162 | | - processed.add(stmt.module) |
163 | 136 |
|
164 | | - # Resolve module path |
165 | | - module_path = _resolve_module_path(stmt.module, current_dir) |
166 | | - if module_path: |
167 | | - try: |
168 | | - imported_program = _parse_program_from_file(str(module_path), lang) |
169 | | - # Recursively collect dependencies of imported module |
170 | | - imported_program = _collect_imports(imported_program, module_path.parent) |
171 | | - # Merge into result |
172 | | - result = _merge_programs(result, imported_program) |
173 | | - except Exception as e: |
174 | | - print(f"[WARN] Failed to import {stmt.module}: {e}") |
| 137 | +def _collect_ui_import_modules(entry_file: Path, root_ir, lang: str | None): |
| 138 | + modules = {} |
| 139 | + warnings = [] |
| 140 | + visited = {entry_file.resolve()} |
175 | 141 |
|
176 | | - return result |
| 142 | + def visit(ir_program, current_file: Path): |
| 143 | + for node in ir_program.body: |
| 144 | + if not isinstance(node, IRImportStatement): |
| 145 | + continue |
| 146 | + module_path = _resolve_absolute_module_source(current_file, node.module) |
| 147 | + if module_path is None: |
| 148 | + continue |
| 149 | + resolved = module_path.resolve() |
| 150 | + if resolved in visited: |
| 151 | + continue |
| 152 | + visited.add(resolved) |
| 153 | + try: |
| 154 | + imported_ir = _parse_ir_from_file(resolved, lang) |
| 155 | + except Exception as exc: # pylint: disable=broad-exception-caught |
| 156 | + warnings.append(f"Skipped UI import {node.module}: {exc}") |
| 157 | + continue |
| 158 | + visit(imported_ir, resolved) |
| 159 | + modules[node.module] = imported_ir |
177 | 160 |
|
178 | | - return _collect_imports(main_program, base_dir) |
| 161 | + visit(root_ir, entry_file.resolve()) |
| 162 | + return modules, warnings |
179 | 163 |
|
180 | 164 |
|
181 | 165 | def cmd_run(args): |
@@ -358,9 +342,11 @@ def cmd_build_wasm_bundle(args): |
358 | 342 |
|
359 | 343 | def cmd_build_ui_bundle(args): |
360 | 344 | """Build a self-contained reactive UI bundle (HTML + JS).""" |
361 | | - program = _parse_program_with_dependencies(args.file, args.lang) |
362 | | - ir = lower_to_semantic_ir(program, args.lang or "en") |
363 | | - result = lower_to_ui(ir) |
| 345 | + entry_file = Path(args.file) |
| 346 | + ir = _parse_ir_from_file(entry_file, args.lang) |
| 347 | + modules, import_warnings = _collect_ui_import_modules(entry_file, ir, args.lang) |
| 348 | + result = lower_to_ui(ir, modules=modules) |
| 349 | + result.diagnostics.extend(import_warnings) |
364 | 350 |
|
365 | 351 | # Create output directory |
366 | 352 | out_dir = Path(args.out_dir) |
|
0 commit comments