Skip to content

Commit 95d8db9

Browse files
committed
Phase 13: Deep modularization of Mimo converter
1 parent 26ba97c commit 95d8db9

12 files changed

Lines changed: 231 additions & 293 deletions

File tree

tools/convert.js

Lines changed: 16 additions & 293 deletions
Original file line numberDiff line numberDiff line change
@@ -1,267 +1,17 @@
11
#!/usr/bin/env node
22
/**
3-
* Mimo Language Converter
4-
*
5-
* A flexible transpiler that converts Mimo source code to various target languages.
6-
*
7-
* Features:
8-
* - Modular converter system with language registration
9-
* - Auto-detection of target language from output file extension
10-
* - Support for both single file and directory output
11-
* - Runtime file copying for languages that need it
12-
*
13-
* Usage:
14-
* node tools/convert.js --in <source.mimo> --out <output.ext> [--to <language>]
15-
* node tools/convert.js --in <source.mimo> --out <output_dir> --to <language>
16-
*
17-
* Directory Structure:
18-
* tools/converters/<language>/
19-
* ├── to_<lang>.js # Converter implementation
20-
* └── mimo_runtime.<ext> # Runtime library for target language
21-
*
22-
* Adding New Languages:
23-
* 1. Create directory: tools/converters/<language>/
24-
* 2. Create converter: tools/converters/<language>/to_<lang>.js
25-
* 3. Create runtime: tools/converters/<language>/mimo_runtime.<ext>
26-
* 4. Import and register in setupDefaultConverters()
27-
*
28-
* Example:
29-
* import { MimoToGoConverter } from './converters/go/to_go.js';
30-
* converterRegistry.register('go', '.go', MimoToGoConverter, 'go/mimo_runtime.go');
3+
* Mimo Language Converter - Standardized Entry Point
314
*/
325
import fs from 'node:fs';
336
import path from 'node:path';
347
import { fileURLToPath } from 'node:url';
35-
import { Parser } from '../parser/Parser.js';
36-
import { Lexer } from '../lexer/Lexer.js';
37-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
388

39-
// Converter registry system
40-
class ConverterRegistry {
41-
constructor() {
42-
this.converters = new Map();
43-
}
44-
45-
/**
46-
* Register a converter for a target language
47-
* @param {string} language - Target language identifier (e.g., 'js', 'py', 'go')
48-
* @param {string} extension - File extension for the target language (e.g., '.js', '.py', '.go')
49-
* @param {class} ConverterClass - The converter class
50-
* @param {string} [runtimeFile] - Optional runtime file to copy
51-
*/
52-
register(language, extension, ConverterClass, runtimeFile = null) {
53-
this.converters.set(language, {
54-
extension,
55-
ConverterClass,
56-
runtimeFile
57-
});
58-
}
59-
60-
/**
61-
* Get converter info for a language
62-
* @param {string} language - Target language identifier
63-
* @returns {Object|null} Converter info or null if not found
64-
*/
65-
get(language) {
66-
return this.converters.get(language) || null;
67-
}
68-
69-
/**
70-
* Get all registered languages
71-
* @returns {Array<string>} Array of language identifiers
72-
*/
73-
getLanguages() {
74-
return Array.from(this.converters.keys());
75-
}
76-
77-
/**
78-
* Detect target language from file extension
79-
* @param {string} filePath - Output file path
80-
* @returns {string|null} Language identifier or null if not detected
81-
*/
82-
detectLanguageFromExtension(filePath) {
83-
const ext = path.extname(filePath);
84-
for (const [lang, info] of this.converters) {
85-
if (info.extension === ext) {
86-
return lang;
87-
}
88-
}
89-
return null;
90-
}
91-
92-
/**
93-
* Dynamically scan the converters directory and load all plugins
94-
*/
95-
async discoverConverters() {
96-
const convertersDir = path.join(__dirname, 'converters');
97-
if (!fs.existsSync(convertersDir)) return;
98-
99-
const entries = fs.readdirSync(convertersDir, { withFileTypes: true });
100-
101-
for (const entry of entries) {
102-
if (entry.isDirectory()) {
103-
const langDir = path.join(convertersDir, entry.name);
104-
const indexPath = path.join(langDir, 'index.js');
105-
106-
if (fs.existsSync(indexPath)) {
107-
try {
108-
// Use dynamic import with file URL for Windows compatibility
109-
const modulePath = `file://${indexPath}`;
110-
const { config, Converter } = await import(modulePath);
111-
112-
if (config && Converter) {
113-
const { name, aliases, extension, runtimeFile } = config;
114-
115-
// Register main name and all aliases
116-
const regName = name || entry.name;
117-
const runtimePath = runtimeFile ? path.join(entry.name, runtimeFile) : null;
118-
119-
this.register(regName, extension, Converter, runtimePath);
120-
121-
if (aliases && Array.isArray(aliases)) {
122-
for (const alias of aliases) {
123-
if (alias !== regName) {
124-
this.register(alias, extension, Converter, runtimePath);
125-
}
126-
}
127-
}
128-
}
129-
} catch (error) {
130-
console.warn(`Warning: Failed to load converter in ${entry.name}: ${error.message}`);
131-
}
132-
}
133-
}
134-
}
135-
}
136-
}
9+
import { ConverterRegistry } from './convert/Registry.js';
10+
import { parseArgs, determineTarget } from './convert/Args.js';
11+
import { Transpiler } from './convert/Transpiler.js';
13712

13813
const converterRegistry = new ConverterRegistry();
13914

140-
function parseArgs(args) {
141-
const options = {};
142-
if (args.length === 2 && !args[0].startsWith('--')) {
143-
// Simple positional arguments: mimo_in mimo_out
144-
options.in = args[0];
145-
options.out = args[1];
146-
options.to = 'js'; // Default to JS
147-
return options;
148-
}
149-
for (let i = 0; i < args.length; i += 2) {
150-
const flag = args[i];
151-
const value = args[i + 1];
152-
if (flag.startsWith('--')) {
153-
options[flag.substring(2)] = value;
154-
}
155-
}
156-
return options;
157-
}
158-
159-
/**
160-
* Determine the target language and converter to use
161-
* @param {Object} options - Parsed command line options
162-
* @returns {Object} Object containing language, converterInfo, and targetExtension
163-
*/
164-
function determineTarget(options) {
165-
let targetLanguage = options.to;
166-
167-
// If no target specified, try to detect from output file extension
168-
if (!targetLanguage && options.out) {
169-
targetLanguage = converterRegistry.detectLanguageFromExtension(options.out);
170-
}
171-
172-
// Default to JavaScript if still not determined
173-
if (!targetLanguage) {
174-
targetLanguage = 'js';
175-
}
176-
177-
const converterInfo = converterRegistry.get(targetLanguage);
178-
if (!converterInfo) {
179-
const availableLanguages = converterRegistry.getLanguages().join(', ');
180-
throw new Error(`Unsupported target language: ${targetLanguage}. Available: ${availableLanguages}`);
181-
}
182-
183-
return {
184-
language: targetLanguage,
185-
converterInfo,
186-
targetExtension: converterInfo.extension
187-
};
188-
}
189-
190-
191-
const processedFiles = new Set();
192-
193-
function transpileMainFile(filePath, outPath, converterInfo) {
194-
console.log(` -> Transpiling: ${filePath}`);
195-
196-
const source = fs.readFileSync(filePath, 'utf-8');
197-
const sourcePath = path.resolve(filePath);
198-
199-
const lexer = new Lexer(source, sourcePath);
200-
const tokens = [];
201-
let token;
202-
while ((token = lexer.nextToken()) !== null) tokens.push(token);
203-
204-
const parser = new Parser(tokens, sourcePath);
205-
const ast = parser.parse();
206-
207-
// Convert the AST using the appropriate converter
208-
const converter = new converterInfo.ConverterClass();
209-
const output = converter.convert(ast);
210-
211-
fs.writeFileSync(outPath, output, 'utf-8');
212-
}
213-
214-
function transpileFile(filePath, outDir, converterInfo, targetExtension) {
215-
if (processedFiles.has(filePath)) {
216-
return; // Already processed this file in this run
217-
}
218-
processedFiles.add(filePath);
219-
console.log(` -> Transpiling: ${filePath}`);
220-
221-
const source = fs.readFileSync(filePath, 'utf-8');
222-
const sourcePath = path.resolve(filePath);
223-
224-
const lexer = new Lexer(source, sourcePath);
225-
const tokens = [];
226-
let token;
227-
while ((token = lexer.nextToken()) !== null) tokens.push(token);
228-
229-
const parser = new Parser(tokens, sourcePath);
230-
const ast = parser.parse();
231-
232-
// After parsing, check for more imports to process
233-
ast.body.forEach(stmt => {
234-
if (stmt.type === 'ImportStatement') {
235-
const modulePath = stmt.path;
236-
// Ignore stdlib modules
237-
if (!['fs', 'math', 'string', 'array', 'json', 'datetime'].includes(modulePath)) {
238-
// Construct the path to the Mimo source file to be imported
239-
let nextFilePath = path.resolve(path.dirname(filePath), modulePath);
240-
if (!nextFilePath.endsWith('.mimo')) {
241-
nextFilePath += '.mimo';
242-
}
243-
244-
if (fs.existsSync(nextFilePath)) {
245-
// Recursively transpile the dependency
246-
transpileFile(nextFilePath, outDir, converterInfo, targetExtension);
247-
} else {
248-
console.warn(`Warning: Imported file not found, skipping: ${nextFilePath}`);
249-
}
250-
}
251-
}
252-
});
253-
254-
// Now, convert the current file's AST using the appropriate converter
255-
const converter = new converterInfo.ConverterClass();
256-
const output = converter.convert(ast);
257-
258-
// Determine the output path with correct extension
259-
const baseName = path.basename(filePath, '.mimo');
260-
const outPath = path.join(outDir, `${baseName}${targetExtension}`);
261-
262-
fs.writeFileSync(outPath, output, 'utf-8');
263-
}
264-
26515
async function readStdin() {
26616
return new Promise((resolve) => {
26717
let data = '';
@@ -280,100 +30,73 @@ async function main(providedArgs) {
28030

28131
if ((!options.in && process.stdin.isTTY) || !options.out) {
28232
const availableLanguages = converterRegistry.getLanguages().join(', ');
283-
console.error('Usage: node tools/convert.js --in <infile> --out <outfile|outdir> [--to <language>]');
284-
console.error('Or pipe to stdin: echo "..." | node tools/convert.js --out <outfile> [--to <language>]');
33+
console.error('Usage: mimo convert --in <infile> --out <outfile|outdir> [--to <language>]');
34+
console.error('Or pipe to stdin: echo "..." | mimo convert --out <outfile> [--to <language>]');
28535
console.error(`Available target languages: ${availableLanguages}`);
28636
process.exit(1);
28737
}
28838

28939
let targetConfig;
29040
try {
291-
targetConfig = determineTarget(options);
41+
targetConfig = determineTarget(options, converterRegistry);
29242
} catch (error) {
29343
console.error(`Error: ${error.message}`);
29444
process.exit(1);
29545
}
29646

29747
const { language, converterInfo, targetExtension } = targetConfig;
48+
const transpiler = new Transpiler();
29849

29950
let outDir;
30051
let isFileOutput = false;
30152

302-
// Check if the output is a file or a directory
303-
// If it has an extension that matches our target, treat it as a file
30453
if (path.extname(options.out) === targetExtension) {
305-
// Output is a file, so extract the directory
30654
outDir = path.dirname(options.out);
30755
isFileOutput = true;
308-
if (options.in) {
309-
console.log(`Converting '${options.in}' to ${language.toUpperCase()} file '${options.out}'...`);
310-
} else {
311-
console.log(`Converting STDIN to ${language.toUpperCase()} file '${options.out}'...`);
312-
}
56+
console.log(`Converting ${options.in ? `'${options.in}'` : 'STDIN'} to ${language.toUpperCase()} file '${options.out}'...`);
31357
} else {
314-
// Output is a directory
31558
outDir = options.out;
316-
if (options.in) {
317-
console.log(`Converting '${options.in}' to ${language.toUpperCase()} in directory '${options.out}'...`);
318-
} else {
319-
console.log(`Converting STDIN to ${language.toUpperCase()} in directory '${options.out}'...`);
320-
}
59+
console.log(`Converting ${options.in ? `'${options.in}'` : 'STDIN'} to ${language.toUpperCase()} in directory '${options.out}'...`);
32160
}
32261

32362
if (!fs.existsSync(outDir)) {
32463
fs.mkdirSync(outDir, { recursive: true });
32564
}
32665

327-
// Clear processed files set for this run
328-
processedFiles.clear();
329-
330-
// Start the transpilation process
33166
if (isFileOutput) {
332-
// For single file output, only transpile the main file
33367
if (options.in) {
334-
transpileMainFile(options.in, options.out, converterInfo);
68+
transpiler.transpileMainFile(options.in, options.out, converterInfo);
33569
} else {
33670
const source = await readStdin();
337-
const lexer = new Lexer(source, 'stdin');
338-
const tokens = [];
339-
let token;
340-
while ((token = lexer.nextToken()) !== null) tokens.push(token);
341-
const parser = new Parser(tokens, 'stdin');
342-
const ast = parser.parse();
343-
const converter = new converterInfo.ConverterClass();
344-
const output = converter.convert(ast);
71+
const output = transpiler.transpileSource(source, 'stdin', converterInfo);
34572
fs.writeFileSync(options.out, output, 'utf-8');
34673
}
34774
} else {
348-
// For directory output, transpile all files recursively
34975
if (options.in) {
350-
transpileFile(options.in, outDir, converterInfo, targetExtension);
76+
transpiler.transpileFile(options.in, outDir, converterInfo, targetExtension);
35177
} else {
35278
console.error('Error: Directory output requires an input file to resolve dependencies.');
35379
process.exit(1);
35480
}
35581

356-
// Copy the runtime file if specified for directory output
82+
// Copy runtime if exists
35783
if (converterInfo.runtimeFile) {
35884
const __dirname = path.dirname(fileURLToPath(import.meta.url));
359-
const runtimeSourcePath = path.join(__dirname, 'converters', converterInfo.runtimeFile);
85+
const runtimeSourcePath = path.join(__dirname, 'convert', 'plugins', converterInfo.runtimeFile);
36086
const runtimeFileName = path.basename(converterInfo.runtimeFile);
87+
36188
if (fs.existsSync(runtimeSourcePath)) {
36289
fs.copyFileSync(runtimeSourcePath, path.join(outDir, runtimeFileName));
36390
console.log(` -> Copied runtime: ${runtimeFileName}`);
364-
} else {
365-
console.warn(`Warning: Runtime file not found: ${runtimeSourcePath}`);
36691
}
36792
}
36893
}
36994

37095
console.log(`✅ Conversion to ${language.toUpperCase()} successful!`);
37196
}
37297

373-
// Export for use in CLI
37498
export { main as runConverter };
37599

376-
// Only run if this file is the main module
377100
if (process.argv[1] === fileURLToPath(import.meta.url)) {
378101
main();
379102
}

0 commit comments

Comments
 (0)