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 */
325import fs from 'node:fs' ;
336import path from 'node:path' ;
347import { 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
13813const 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-
26515async 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
37498export { main as runConverter } ;
37599
376- // Only run if this file is the main module
377100if ( process . argv [ 1 ] === fileURLToPath ( import . meta. url ) ) {
378101 main ( ) ;
379102}
0 commit comments