mimo-lang 1.1.0 → 2.0.6

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 (166) hide show
  1. package/.gitattributes +24 -0
  2. package/LICENSE +21 -0
  3. package/README.md +91 -6
  4. package/adapters/browserAdapter.js +86 -0
  5. package/adapters/nodeAdapter.js +101 -0
  6. package/bin/cli.js +80 -0
  7. package/bin/commands/convert.js +27 -0
  8. package/bin/commands/doctor.js +139 -0
  9. package/bin/commands/eval.js +39 -0
  10. package/bin/commands/fmt.js +109 -0
  11. package/bin/commands/help.js +72 -0
  12. package/bin/commands/lint.js +117 -0
  13. package/bin/commands/repl.js +24 -0
  14. package/bin/commands/run.js +64 -0
  15. package/bin/commands/test.js +126 -0
  16. package/bin/utils/colors.js +38 -0
  17. package/bin/utils/formatError.js +47 -0
  18. package/bin/utils/fs.js +57 -0
  19. package/bin/utils/version.js +8 -0
  20. package/build.js +18 -0
  21. package/bun.lock +74 -0
  22. package/index.js +49 -39
  23. package/index.web.js +364 -0
  24. package/interpreter/BuiltinFunction.js +32 -0
  25. package/interpreter/ErrorHandler.js +120 -0
  26. package/interpreter/ExpressionEvaluator.js +106 -0
  27. package/interpreter/Interpreter.js +172 -0
  28. package/interpreter/MimoError.js +112 -0
  29. package/interpreter/ModuleLoader.js +236 -0
  30. package/interpreter/StatementExecutor.js +107 -0
  31. package/interpreter/Utils.js +82 -0
  32. package/interpreter/Values.js +87 -0
  33. package/interpreter/coreBuiltins.js +490 -0
  34. package/interpreter/environment.js +99 -0
  35. package/interpreter/evaluators/binaryExpressionEvaluator.js +111 -0
  36. package/interpreter/evaluators/collectionEvaluator.js +151 -0
  37. package/interpreter/evaluators/functionCallEvaluator.js +76 -0
  38. package/interpreter/evaluators/literalEvaluator.js +27 -0
  39. package/interpreter/evaluators/moduleAccessEvaluator.js +25 -0
  40. package/interpreter/evaluators/templateLiteralEvaluator.js +20 -0
  41. package/interpreter/executors/BaseExecutor.js +37 -0
  42. package/interpreter/executors/ControlFlowExecutor.js +206 -0
  43. package/interpreter/executors/FunctionExecutor.js +126 -0
  44. package/interpreter/executors/PatternMatchExecutor.js +93 -0
  45. package/interpreter/executors/VariableExecutor.js +144 -0
  46. package/interpreter/index.js +8 -0
  47. package/interpreter/stdlib/array/accessFunctions.js +61 -0
  48. package/interpreter/stdlib/array/arrayUtils.js +36 -0
  49. package/interpreter/stdlib/array/higherOrderFunctions.js +285 -0
  50. package/interpreter/stdlib/array/searchFunctions.js +77 -0
  51. package/interpreter/stdlib/array/setFunctions.js +49 -0
  52. package/interpreter/stdlib/array/transformationFunctions.js +68 -0
  53. package/interpreter/stdlib/array.js +85 -0
  54. package/interpreter/stdlib/assert.js +143 -0
  55. package/interpreter/stdlib/datetime.js +170 -0
  56. package/interpreter/stdlib/env.js +54 -0
  57. package/interpreter/stdlib/fs.js +161 -0
  58. package/interpreter/stdlib/http.js +92 -0
  59. package/interpreter/stdlib/json.js +70 -0
  60. package/interpreter/stdlib/math.js +309 -0
  61. package/interpreter/stdlib/object.js +142 -0
  62. package/interpreter/stdlib/path.js +69 -0
  63. package/interpreter/stdlib/regex.js +134 -0
  64. package/interpreter/stdlib/string.js +260 -0
  65. package/interpreter/suggestions.js +46 -0
  66. package/lexer/Lexer.js +245 -0
  67. package/lexer/TokenTypes.js +131 -0
  68. package/lexer/createToken.js +11 -0
  69. package/lexer/tokenizers/commentTokenizer.js +45 -0
  70. package/lexer/tokenizers/literalTokenizer.js +163 -0
  71. package/lexer/tokenizers/symbolTokenizer.js +69 -0
  72. package/lexer/tokenizers/whitespaceTokenizer.js +36 -0
  73. package/package.json +29 -11
  74. package/parser/ASTNodes.js +448 -0
  75. package/parser/Parser.js +188 -0
  76. package/parser/expressions/atomicExpressions.js +165 -0
  77. package/parser/expressions/conditionalExpressions.js +0 -0
  78. package/parser/expressions/operatorExpressions.js +79 -0
  79. package/parser/expressions/primaryExpressions.js +77 -0
  80. package/parser/parseStatement.js +184 -0
  81. package/parser/parserExpressions.js +115 -0
  82. package/parser/parserUtils.js +19 -0
  83. package/parser/statements/controlFlowParsers.js +106 -0
  84. package/parser/statements/functionParsers.js +314 -0
  85. package/parser/statements/moduleParsers.js +57 -0
  86. package/parser/statements/patternMatchParsers.js +124 -0
  87. package/parser/statements/variableParsers.js +155 -0
  88. package/repl.js +325 -0
  89. package/test.js +47 -1
  90. package/tools/PrettyPrinter.js +3 -0
  91. package/tools/convert/Args.js +46 -0
  92. package/tools/convert/Registry.js +91 -0
  93. package/tools/convert/Transpiler.js +78 -0
  94. package/tools/convert/plugins/README.md +66 -0
  95. package/tools/convert/plugins/alya/index.js +10 -0
  96. package/tools/convert/plugins/alya/to_alya.js +289 -0
  97. package/tools/convert/plugins/alya/visitors/expressions.js +257 -0
  98. package/tools/convert/plugins/alya/visitors/statements.js +403 -0
  99. package/tools/convert/plugins/base_converter.js +228 -0
  100. package/tools/convert/plugins/javascript/index.js +10 -0
  101. package/tools/convert/plugins/javascript/mimo_runtime.js +265 -0
  102. package/tools/convert/plugins/javascript/to_js.js +155 -0
  103. package/tools/convert/plugins/javascript/visitors/expressions.js +197 -0
  104. package/tools/convert/plugins/javascript/visitors/patterns.js +102 -0
  105. package/tools/convert/plugins/javascript/visitors/statements.js +236 -0
  106. package/tools/convert/plugins/python/index.js +10 -0
  107. package/tools/convert/plugins/python/mimo_runtime.py +811 -0
  108. package/tools/convert/plugins/python/to_py.js +329 -0
  109. package/tools/convert/plugins/python/visitors/expressions.js +272 -0
  110. package/tools/convert/plugins/python/visitors/patterns.js +100 -0
  111. package/tools/convert/plugins/python/visitors/statements.js +257 -0
  112. package/tools/convert.js +102 -0
  113. package/tools/format/CommentAttacher.js +190 -0
  114. package/tools/format/CommentLexer.js +152 -0
  115. package/tools/format/Printer.js +849 -0
  116. package/tools/format/config.js +107 -0
  117. package/tools/formatter.js +169 -0
  118. package/tools/lint/Linter.js +391 -0
  119. package/tools/lint/config.js +114 -0
  120. package/tools/lint/rules/consistent-return.js +62 -0
  121. package/tools/lint/rules/max-depth.js +56 -0
  122. package/tools/lint/rules/no-empty-function.js +45 -0
  123. package/tools/lint/rules/no-magic-numbers.js +46 -0
  124. package/tools/lint/rules/no-shadow.js +113 -0
  125. package/tools/lint/rules/no-unused-vars.js +26 -0
  126. package/tools/lint/rules/prefer-const.js +19 -0
  127. package/tools/linter.js +261 -0
  128. package/tools/replFormatter.js +93 -0
  129. package/tools/stamp-version.js +32 -0
  130. package/web/index.js +9 -0
  131. package/bun.lockb +0 -0
  132. package/cli.js +0 -84
  133. package/compiler/execute/interpreter.js +0 -68
  134. package/compiler/execute/interpreters/binary.js +0 -12
  135. package/compiler/execute/interpreters/call.js +0 -10
  136. package/compiler/execute/interpreters/if.js +0 -10
  137. package/compiler/execute/interpreters/try-catch.js +0 -10
  138. package/compiler/execute/interpreters/while.js +0 -8
  139. package/compiler/execute/utils/createfunction.js +0 -11
  140. package/compiler/execute/utils/evaluate.js +0 -20
  141. package/compiler/execute/utils/operate.js +0 -23
  142. package/compiler/lexer/processToken.js +0 -40
  143. package/compiler/lexer/tokenTypes.js +0 -4
  144. package/compiler/lexer/tokenizer.js +0 -63
  145. package/compiler/parser/expression/comparison.js +0 -18
  146. package/compiler/parser/expression/identifier.js +0 -29
  147. package/compiler/parser/expression/number.js +0 -10
  148. package/compiler/parser/expression/operator.js +0 -21
  149. package/compiler/parser/expression/punctuation.js +0 -31
  150. package/compiler/parser/expression/string.js +0 -6
  151. package/compiler/parser/parseExpression.js +0 -27
  152. package/compiler/parser/parseStatement.js +0 -35
  153. package/compiler/parser/parser.js +0 -16
  154. package/compiler/parser/statement/call.js +0 -26
  155. package/compiler/parser/statement/function.js +0 -29
  156. package/compiler/parser/statement/if.js +0 -34
  157. package/compiler/parser/statement/return.js +0 -10
  158. package/compiler/parser/statement/set.js +0 -11
  159. package/compiler/parser/statement/show.js +0 -10
  160. package/compiler/parser/statement/try-catch.js +0 -25
  161. package/compiler/parser/statement/while.js +0 -22
  162. package/converter/go/convert.js +0 -110
  163. package/converter/js/convert.js +0 -107
  164. package/i.js +0 -30
  165. package/jsconfig.json +0 -27
  166. package/webpack.config.js +0 -9
@@ -0,0 +1,109 @@
1
+ // bin/commands/fmt.js
2
+ // Formats .mimo files using the Mimo pretty-printer.
3
+
4
+ import { formatFile, formatSource } from '../../tools/formatter.js';
5
+ import { collectMimoFiles, readStdin } from '../utils/fs.js';
6
+ import { c } from '../utils/colors.js';
7
+
8
+ export function help() {
9
+ console.log(`
10
+ ${c.bold('mimo fmt')} — Format Mimo source files
11
+
12
+ ${c.bold('Usage:')}
13
+ mimo fmt [options] [paths...]
14
+ echo "code" | mimo fmt
15
+
16
+ ${c.bold('Options:')}
17
+ --write Write formatted output back to files (default: preview)
18
+ --check Exit with code 1 if any file is not formatted (CI mode)
19
+ --quiet Suppress all output except errors
20
+ --verbose Show each file's status even when unchanged
21
+
22
+ ${c.bold('Examples:')}
23
+ mimo fmt src/ Preview formatting for all files in src/
24
+ mimo fmt --write src/ Format files in place
25
+ mimo fmt --check src/ CI check (non-zero exit if unformatted)
26
+ echo 'show + 1 2' | mimo fmt Format from STDIN
27
+ `);
28
+ }
29
+
30
+ export async function run(args) {
31
+ const shouldWrite = args.includes('--write');
32
+ const shouldCheck = args.includes('--check');
33
+ const quiet = args.includes('--quiet');
34
+ const verbose = args.includes('--verbose');
35
+ const targets = args.filter((a) => !a.startsWith('--'));
36
+
37
+ // STDIN mode
38
+ if (targets.includes('-') || (targets.length === 0 && !process.stdin.isTTY)) {
39
+ const source = await readStdin();
40
+ try {
41
+ process.stdout.write(formatSource(source, 'stdin'));
42
+ } catch (err) {
43
+ console.error(c.error(err.message ?? String(err)));
44
+ process.exit(1);
45
+ }
46
+ return;
47
+ }
48
+
49
+ const files = collectMimoFiles(targets);
50
+ if (files.length === 0) {
51
+ console.error(c.error('Error: No .mimo files found for formatting.'));
52
+ process.exit(1);
53
+ }
54
+
55
+ const t0 = Date.now();
56
+ let hadErrors = false;
57
+ let hadUnformatted = false;
58
+ let changedCount = 0;
59
+
60
+ for (const file of files) {
61
+ const result = formatFile(file, { write: shouldWrite, check: shouldCheck, quiet: true });
62
+ if (!result.ok) {
63
+ hadErrors = true;
64
+ // formatFile already printed its own error; nothing extra needed
65
+ continue;
66
+ }
67
+
68
+ if (shouldCheck) {
69
+ if (result.changed) {
70
+ hadUnformatted = true;
71
+ if (!quiet) console.log(`${c.red('✗')} ${file}`);
72
+ } else if (verbose) {
73
+ console.log(`${c.green('✓')} ${file}`);
74
+ }
75
+ } else if (shouldWrite) {
76
+ if (result.changed) {
77
+ changedCount++;
78
+ if (!quiet) console.log(`${c.green('formatted')} ${file}`);
79
+ } else if (verbose) {
80
+ console.log(`${c.dim('unchanged')} ${file}`);
81
+ }
82
+ } else {
83
+ // Preview mode — print formatted output (formatFile already did it in non-quiet mode)
84
+ // Re-invoke in non-quiet for preview only when verbose or single file
85
+ if (!quiet) {
86
+ formatFile(file, { write: false, check: false, quiet: false });
87
+ }
88
+ }
89
+ }
90
+
91
+ const ms = Date.now() - t0;
92
+
93
+ if (!quiet && !shouldCheck && shouldWrite) {
94
+ const summary = changedCount > 0
95
+ ? c.green(`${changedCount} file${changedCount !== 1 ? 's' : ''} formatted`)
96
+ : c.dim('All files already formatted');
97
+ console.log(`\n${summary} ${c.dim(`(${ms}ms)`)}`);
98
+ }
99
+
100
+ if (!quiet && shouldCheck) {
101
+ if (hadUnformatted) {
102
+ console.log(c.red(`\nSome files are not formatted. Run \`mimo fmt --write\` to fix.`));
103
+ } else {
104
+ console.log(c.green(`\nAll files are properly formatted. ${c.dim(`(${ms}ms)`)}`));
105
+ }
106
+ }
107
+
108
+ if (hadErrors || hadUnformatted) process.exit(1);
109
+ }
@@ -0,0 +1,72 @@
1
+ // bin/commands/help.js
2
+ // Shows global help, or per-command help when a command name is provided.
3
+
4
+ import { c } from '../utils/colors.js';
5
+ import { getVersion } from '../utils/version.js';
6
+
7
+ // Lazy loaders for each command — mirrors cli.js COMMANDS registry.
8
+ const COMMAND_LOADERS = {
9
+ run: () => import('./run.js'),
10
+ repl: () => import('./repl.js'),
11
+ fmt: () => import('./fmt.js'),
12
+ lint: () => import('./lint.js'),
13
+ test: () => import('./test.js'),
14
+ convert: () => import('./convert.js'),
15
+ doctor: () => import('./doctor.js'),
16
+ eval: () => import('./eval.js'),
17
+ };
18
+
19
+ export function help() {
20
+ // `mimo help --help` is a no-op — just show global help
21
+ run([]);
22
+ }
23
+
24
+ export async function run(args) {
25
+ const target = args[0];
26
+
27
+ // `mimo help <command>` → delegate to that command's help()
28
+ if (target && target in COMMAND_LOADERS) {
29
+ const mod = await COMMAND_LOADERS[target]();
30
+ mod.help();
31
+ return;
32
+ }
33
+
34
+ // Unknown sub-command
35
+ if (target) {
36
+ console.error(c.error(`Unknown command: ${target}`));
37
+ console.error(c.dim(`Run ${c.cyan('mimo help')} to see all commands.`));
38
+ process.exit(1);
39
+ }
40
+
41
+ // Global help
42
+ console.log(`
43
+ ${c.bold('Mimo Language Toolkit')} ${c.dim(`v${getVersion()}`)}
44
+
45
+ ${c.bold('Usage:')}
46
+ mimo <command> [options]
47
+ mimo <file>
48
+
49
+ ${c.bold('Commands:')}
50
+ ${c.cyan('run')} <file> Execute a Mimo file (also: mimo <file>)
51
+ ${c.cyan('repl')} Start the interactive Read-Eval-Print Loop
52
+ ${c.cyan('fmt')} [paths...] Format .mimo files
53
+ ${c.dim('--write --check --quiet --verbose')}
54
+ ${c.cyan('lint')} [paths...] Statically analyse .mimo files
55
+ ${c.dim('--fail-on-warning --quiet --verbose --json')}
56
+ ${c.cyan('test')} [path] Run test files
57
+ ${c.dim('--quiet --verbose')}
58
+ ${c.cyan('convert')} Transpile Mimo to another language
59
+ ${c.dim('--in --out --to')}
60
+ ${c.cyan('doctor')} Validate runtime/tooling environment
61
+
62
+ ${c.bold('Global options:')}
63
+ ${c.cyan('--version')}, ${c.cyan('-v')} Show version
64
+ ${c.cyan('--help')}, ${c.cyan('-h')} Show this help
65
+ ${c.cyan('--eval')}, ${c.cyan('-e')} <code> Evaluate a string of Mimo code
66
+ ${c.cyan('-')} Read and execute Mimo code from STDIN
67
+
68
+ ${c.bold('Per-command help:')}
69
+ mimo help <command>
70
+ mimo <command> --help
71
+ `);
72
+ }
@@ -0,0 +1,117 @@
1
+ // bin/commands/lint.js
2
+ // Statically analyses .mimo files using the Mimo linter.
3
+
4
+ import { lintFile, lintFileJson, parseRuleFlags } from '../../tools/linter.js';
5
+ import { collectMimoFiles } from '../utils/fs.js';
6
+ import { c } from '../utils/colors.js';
7
+
8
+ export function help() {
9
+ console.log(`
10
+ ${c.bold('mimo lint')} — Statically analyse Mimo source files
11
+
12
+ ${c.bold('Usage:')}
13
+ mimo lint [options] [paths...]
14
+
15
+ ${c.bold('Options:')}
16
+ --fail-on-warning Exit 1 on warnings (not just errors)
17
+ --quiet Suppress per-file output; print only summary
18
+ --verbose Show rule IDs alongside messages
19
+ --json Output results as JSON (for tooling)
20
+ --rule:<name>=<bool> Override a specific rule (e.g. --rule:no-magic-numbers=true)
21
+
22
+ ${c.bold('Available rules:')}
23
+ no-unused-vars (on by default)
24
+ prefer-const (on by default)
25
+ no-magic-numbers
26
+ no-empty-function
27
+ max-depth
28
+ no-shadow
29
+ consistent-return
30
+
31
+ ${c.bold('Examples:')}
32
+ mimo lint src/
33
+ mimo lint --fail-on-warning src/
34
+ mimo lint --rule:no-magic-numbers=true src/
35
+ mimo lint --json src/ | jq .
36
+ `);
37
+ }
38
+
39
+ export async function run(args) {
40
+ const quiet = args.includes('--quiet');
41
+ const verbose = args.includes('--verbose');
42
+ const json = args.includes('--json');
43
+ const failOnWarning = args.includes('--fail-on-warning');
44
+ const rules = parseRuleFlags(args);
45
+ const targets = args.filter((a) => !a.startsWith('--'));
46
+ const files = collectMimoFiles(targets);
47
+
48
+ if (files.length === 0) {
49
+ console.error(c.error('Error: No .mimo files found for linting.'));
50
+ process.exit(1);
51
+ }
52
+
53
+ const t0 = Date.now();
54
+
55
+ if (json) {
56
+ const results = files.map((f) => lintFileJson(f, { rules }));
57
+ console.log(JSON.stringify(results.length === 1 ? results[0] : results, null, 2));
58
+ const hasParseErrors = results.some((r) => !r.ok);
59
+ const errorCount = results.reduce((s, r) => s + r.messages.filter((m) => m.severity === 'error').length, 0);
60
+ const warningCount = results.reduce((s, r) => s + r.messages.filter((m) => m.severity !== 'error').length, 0);
61
+ if (hasParseErrors || errorCount > 0 || (failOnWarning && warningCount > 0)) process.exit(1);
62
+ return;
63
+ }
64
+
65
+ let hasParseErrors = false;
66
+ let totalErrors = 0;
67
+ let totalWarnings = 0;
68
+ let fileCount = 0;
69
+
70
+ for (const file of files) {
71
+ const result = lintFile(file, { quiet: true, rules, verbose });
72
+ if (!result.ok) {
73
+ hasParseErrors = true;
74
+ if (!quiet) console.error(`${c.red('parse error')} ${file}`);
75
+ continue;
76
+ }
77
+
78
+ fileCount++;
79
+ const errors = result.messages.filter((m) => m.severity === 'error');
80
+ const warnings = result.messages.filter((m) => m.severity !== 'error');
81
+ totalErrors += errors.length;
82
+ totalWarnings += warnings.length;
83
+
84
+ if (quiet) continue;
85
+
86
+ const hasIssues = errors.length > 0 || warnings.length > 0;
87
+ if (!hasIssues) {
88
+ if (verbose) console.log(`${c.green('✓')} ${file}`);
89
+ continue;
90
+ }
91
+
92
+ console.log(`\n${c.bold(file)}`);
93
+ for (const msg of result.messages) {
94
+ const loc = msg.line ? `${c.dim(`${msg.line}:${msg.column ?? 1}`)}` : '';
95
+ const badge = msg.severity === 'error'
96
+ ? c.red('error')
97
+ : c.yellow('warn ');
98
+ const ruleTag = verbose ? c.dim(` [${msg.ruleId ?? 'unknown'}]`) : '';
99
+ console.log(` ${badge} ${loc.padEnd(8)} ${msg.message}${ruleTag}`);
100
+ }
101
+ }
102
+
103
+ const ms = Date.now() - t0;
104
+ const errorLabel = totalErrors > 0 ? c.red(`${totalErrors} error${totalErrors !== 1 ? 's' : ''}`) : null;
105
+ const warningLabel = totalWarnings > 0 ? c.yellow(`${totalWarnings} warning${totalWarnings !== 1 ? 's' : ''}`) : null;
106
+ const parts = [errorLabel, warningLabel].filter(Boolean);
107
+
108
+ if (!quiet) {
109
+ if (parts.length > 0) {
110
+ console.log(`\n${parts.join(c.dim(', '))} ${c.dim(`in ${fileCount} file${fileCount !== 1 ? 's' : ''} (${ms}ms)`)}`);
111
+ } else if (!hasParseErrors) {
112
+ console.log(c.green(`\nNo issues found`) + c.dim(` in ${fileCount} file${fileCount !== 1 ? 's' : ''} (${ms}ms)`));
113
+ }
114
+ }
115
+
116
+ if (hasParseErrors || totalErrors > 0 || (failOnWarning && totalWarnings > 0)) process.exit(1);
117
+ }
@@ -0,0 +1,24 @@
1
+ // bin/commands/repl.js
2
+ // Starts the interactive Mimo REPL.
3
+
4
+ import { runRepl } from '../../repl.js';
5
+ import { c } from '../utils/colors.js';
6
+
7
+ export function help() {
8
+ console.log(`
9
+ ${c.bold('mimo repl')} — Start the interactive Read-Eval-Print Loop
10
+
11
+ ${c.bold('Usage:')}
12
+ mimo repl
13
+
14
+ ${c.bold('REPL shortcuts:')}
15
+ .help Show REPL help
16
+ .exit Exit the REPL
17
+ Ctrl+C Abort current input
18
+ Ctrl+D Exit
19
+ `);
20
+ }
21
+
22
+ export async function run(_args) {
23
+ runRepl();
24
+ }
@@ -0,0 +1,64 @@
1
+ // bin/commands/run.js
2
+ // Executes a .mimo file (or STDIN) through the Mimo interpreter.
3
+
4
+ import fs from 'node:fs';
5
+ import path from 'node:path';
6
+ import { Mimo } from '../../index.js';
7
+ import { nodeAdapter } from '../../adapters/nodeAdapter.js';
8
+ import { formatError } from '../utils/formatError.js';
9
+ import { readStdin } from '../utils/fs.js';
10
+ import { c } from '../utils/colors.js';
11
+
12
+ export function help() {
13
+ console.log(`
14
+ ${c.bold('mimo run')} — Execute a Mimo file
15
+
16
+ ${c.bold('Usage:')}
17
+ mimo run <file>
18
+ mimo <file>
19
+ mimo - Read from STDIN
20
+ echo "code" | mimo
21
+
22
+ ${c.bold('Examples:')}
23
+ mimo hello.mimo
24
+ mimo run examples/hello.mimo
25
+ echo 'show + 1 2' | mimo
26
+ `);
27
+ }
28
+
29
+ export async function run(args) {
30
+ // args[0] is either the file path or already stripped by cli.js
31
+ const filePath = args[0];
32
+
33
+ // No file given → try STDIN
34
+ if (!filePath || filePath === '-') {
35
+ if (process.stdin.isTTY && filePath !== '-') {
36
+ console.error(c.error('Error: No file specified. Use `mimo --help` for usage.'));
37
+ process.exit(1);
38
+ }
39
+ const source = await readStdin();
40
+ const mimo = new Mimo(nodeAdapter);
41
+ try {
42
+ mimo.run(source, '/stdin');
43
+ } catch (err) {
44
+ console.error(formatError(err, source));
45
+ process.exit(1);
46
+ }
47
+ return;
48
+ }
49
+
50
+ const absolutePath = path.resolve(process.cwd(), filePath);
51
+ if (!fs.existsSync(absolutePath)) {
52
+ console.error(c.error(`Error: File not found: ${absolutePath}`));
53
+ process.exit(1);
54
+ }
55
+
56
+ const source = fs.readFileSync(absolutePath, 'utf-8');
57
+ const mimo = new Mimo(nodeAdapter);
58
+ try {
59
+ mimo.run(source, absolutePath);
60
+ } catch (err) {
61
+ console.error(formatError(err, source));
62
+ process.exit(1);
63
+ }
64
+ }
@@ -0,0 +1,126 @@
1
+ // bin/commands/test.js
2
+ // Runs .mimo test files and reports results.
3
+
4
+ import fs from 'node:fs';
5
+ import path from 'node:path';
6
+ import { Mimo } from '../../index.js';
7
+ import { nodeAdapter } from '../../adapters/nodeAdapter.js';
8
+ import { formatError } from '../utils/formatError.js';
9
+ import { c } from '../utils/colors.js';
10
+
11
+ function collectTestFiles(dir, out = []) {
12
+ for (const entry of fs.readdirSync(dir)) {
13
+ const full = path.join(dir, entry);
14
+ if (fs.statSync(full).isDirectory()) {
15
+ collectTestFiles(full, out);
16
+ } else if (
17
+ entry.endsWith('.mimo') &&
18
+ (entry.includes('.test.') || dir.includes('test'))
19
+ ) {
20
+ out.push(full);
21
+ }
22
+ }
23
+ return out;
24
+ }
25
+
26
+ export function help() {
27
+ console.log(`
28
+ ${c.bold('mimo test')} — Run Mimo test files
29
+
30
+ ${c.bold('Usage:')}
31
+ mimo test [path]
32
+
33
+ ${c.bold('Options:')}
34
+ --verbose Show captured output even for passing tests
35
+ --quiet Only print the final summary line
36
+
37
+ ${c.bold('Arguments:')}
38
+ path File or directory to test. Defaults to current directory.
39
+ Discovers all .mimo files inside directories named 'test'
40
+ or files with '.test.' in their name.
41
+
42
+ ${c.bold('Examples:')}
43
+ mimo test
44
+ mimo test test/source/
45
+ mimo test test/source/strings.mimo
46
+ `);
47
+ }
48
+
49
+ export async function run(args) {
50
+ const verbose = args.includes('--verbose');
51
+ const quiet = args.includes('--quiet');
52
+ const targets = args.filter((a) => !a.startsWith('--'));
53
+ const target = targets[0] ?? '.';
54
+
55
+ const absoluteTarget = path.resolve(process.cwd(), target);
56
+ let filesToTest = [];
57
+
58
+ if (!fs.existsSync(absoluteTarget)) {
59
+ console.error(c.error(`Error: Path not found: ${target}`));
60
+ process.exit(1);
61
+ }
62
+
63
+ if (fs.statSync(absoluteTarget).isFile()) {
64
+ filesToTest.push(absoluteTarget);
65
+ } else {
66
+ filesToTest = collectTestFiles(absoluteTarget);
67
+ }
68
+
69
+ if (filesToTest.length === 0) {
70
+ console.log(c.yellow('No test files found.'));
71
+ return;
72
+ }
73
+
74
+ if (!quiet) console.log(`\n${c.bold('Mimo Test Runner')}\n`);
75
+
76
+ let passed = 0;
77
+ let failed = 0;
78
+ const t0 = Date.now();
79
+
80
+ for (const file of filesToTest) {
81
+ const relName = path.relative(process.cwd(), file);
82
+ if (!quiet) process.stdout.write(` ${c.dim(relName)} ... `);
83
+
84
+ const source = fs.readFileSync(file, 'utf-8');
85
+ const logs = [];
86
+
87
+ const testAdapter = {
88
+ ...nodeAdapter,
89
+ log: (...a) => logs.push(a.join(' ')),
90
+ error: (...a) => logs.push(c.dim('[stderr] ') + a.join(' ')),
91
+ };
92
+
93
+ const mimo = new Mimo(testAdapter);
94
+
95
+ try {
96
+ mimo.run(source, file);
97
+ if (!quiet) console.log(c.green('PASS'));
98
+ passed++;
99
+ if (verbose && logs.length > 0) {
100
+ console.log(c.dim(' --- output ---'));
101
+ for (const l of logs) console.log(' ' + l);
102
+ }
103
+ } catch (err) {
104
+ if (!quiet) console.log(c.red('FAIL'));
105
+ failed++;
106
+ if (!quiet) {
107
+ if (logs.length > 0) {
108
+ console.log(c.dim(' --- output ---'));
109
+ for (const l of logs) console.log(' ' + l);
110
+ }
111
+ console.log(c.dim(' --- error ---'));
112
+ console.log(' ' + formatError(err, source).replace(/\n/g, '\n '));
113
+ console.log();
114
+ }
115
+ }
116
+ }
117
+
118
+ const ms = ((Date.now() - t0) / 1000).toFixed(2);
119
+ const summary = failed > 0
120
+ ? `${c.green(`${passed} passed`)}, ${c.red(`${failed} failed`)}`
121
+ : c.green(`${passed} passed`);
122
+
123
+ console.log(`\nTest Result: ${summary}. ${c.dim(`(Time: ${ms}s)`)}`);
124
+
125
+ if (failed > 0) process.exit(1);
126
+ }
@@ -0,0 +1,38 @@
1
+ // bin/utils/colors.js
2
+ // Tiny zero-dependency ANSI color helper.
3
+ // Colors are suppressed automatically when:
4
+ // - stdout is not a TTY (pipe / redirect)
5
+ // - NO_COLOR env var is set (https://no-color.org)
6
+
7
+ const enabled = process.stdout.isTTY && !process.env.NO_COLOR;
8
+
9
+ const code = (n) => enabled ? `\x1b[${n}m` : '';
10
+ const reset = code(0);
11
+ const wrap = (n, s) => `${code(n)}${s}${reset}`;
12
+
13
+ export const c = {
14
+ // Foreground colours
15
+ green: (s) => wrap(32, s),
16
+ red: (s) => wrap(31, s),
17
+ yellow: (s) => wrap(33, s),
18
+ blue: (s) => wrap(34, s),
19
+ cyan: (s) => wrap(36, s),
20
+ magenta: (s) => wrap(35, s),
21
+ white: (s) => wrap(37, s),
22
+
23
+ // Styles
24
+ bold: (s) => wrap(1, s),
25
+ dim: (s) => wrap(2, s),
26
+ italic: (s) => wrap(3, s),
27
+
28
+ // Semantic aliases
29
+ success: (s) => wrap(32, s), // green
30
+ error: (s) => wrap(31, s), // red
31
+ warn: (s) => wrap(33, s), // yellow
32
+ info: (s) => wrap(36, s), // cyan
33
+ muted: (s) => wrap(2, s), // dim
34
+
35
+ // Raw reset (useful for building multi-segment strings)
36
+ reset,
37
+ enabled,
38
+ };
@@ -0,0 +1,47 @@
1
+ // bin/utils/formatError.js
2
+ // Renders a Mimo parse error as a source snippet with a caret pointer.
3
+ // Only structured errors (those with a `.location` object) get the snippet;
4
+ // plain strings / runtime errors are printed as-is.
5
+
6
+ import { c } from './colors.js';
7
+
8
+ /**
9
+ * Format a Mimo error for terminal output.
10
+ *
11
+ * @param {unknown} err The caught value (MimoError, Error, or string)
12
+ * @param {string} [src] Raw source text (used to extract the snippet line)
13
+ * @returns {string} The formatted string, ready for console.error()
14
+ */
15
+ export function formatError(err, src) {
16
+ // Structured Mimo error with location info
17
+ if (err && typeof err === 'object' && err.location) {
18
+ const { line, column } = err.location;
19
+ const tag = c.bold(c.red(`[${err.code ?? 'Error'}]`));
20
+ const label = c.bold(err.message ?? String(err));
21
+ let out = `${tag} ${label}`;
22
+
23
+ if (src && line) {
24
+ const lines = src.split('\n');
25
+ const srcLine = lines[line - 1] ?? '';
26
+ const lineNo = String(line).padStart(4);
27
+ const col = Math.max(0, (column ?? 1) - 1);
28
+
29
+ out += `\n${c.dim(`${lineNo} │`)} ${srcLine}`;
30
+ out += `\n ${' '.repeat(col)}${c.red('^')}`;
31
+ }
32
+
33
+ if (err.file) {
34
+ out += `\n${c.dim(` at ${err.file}:${line ?? '?'}:${column ?? '?'}`)}`;
35
+ }
36
+
37
+ return out;
38
+ }
39
+
40
+ // Plain string (Mimo.run() sometimes throws formatted strings)
41
+ if (typeof err === 'string') {
42
+ return c.red(err);
43
+ }
44
+
45
+ // Standard Error or anything else
46
+ return c.red(err?.message ?? String(err));
47
+ }
@@ -0,0 +1,57 @@
1
+ // bin/utils/fs.js
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+
5
+ /**
6
+ * Recursively collect all .mimo files from a list of file/directory targets.
7
+ * Deduplicates by absolute path. Warns on missing paths.
8
+ * @param {string[]} targets
9
+ * @returns {string[]} Absolute paths to .mimo files, in discovery order
10
+ */
11
+ export function collectMimoFiles(targets) {
12
+ const resolvedTargets = targets.length === 0 ? ['.'] : targets;
13
+ const seen = new Set();
14
+ const files = [];
15
+
16
+ function visit(targetPath) {
17
+ const absolutePath = path.resolve(process.cwd(), targetPath);
18
+ if (!fs.existsSync(absolutePath)) {
19
+ console.error(`Warning: Path not found: ${targetPath}`);
20
+ return;
21
+ }
22
+
23
+ const stat = fs.statSync(absolutePath);
24
+ if (stat.isDirectory()) {
25
+ for (const entry of fs.readdirSync(absolutePath)) {
26
+ visit(path.join(absolutePath, entry));
27
+ }
28
+ return;
29
+ }
30
+
31
+ if (!absolutePath.endsWith('.mimo')) return;
32
+
33
+ if (!seen.has(absolutePath)) {
34
+ seen.add(absolutePath);
35
+ files.push(absolutePath);
36
+ }
37
+ }
38
+
39
+ for (const target of resolvedTargets) {
40
+ visit(target);
41
+ }
42
+
43
+ return files;
44
+ }
45
+
46
+ /**
47
+ * Read all of STDIN as a UTF-8 string.
48
+ * @returns {Promise<string>}
49
+ */
50
+ export function readStdin() {
51
+ return new Promise((resolve) => {
52
+ let data = '';
53
+ process.stdin.setEncoding('utf-8');
54
+ process.stdin.on('data', (chunk) => { data += chunk; });
55
+ process.stdin.on('end', () => { resolve(data); });
56
+ });
57
+ }
@@ -0,0 +1,8 @@
1
+ // bin/utils/version.js
2
+ // This string is stamped by the `prebuild:mimo` script (tools/stamp-version.js).
3
+ // It is a static literal so it survives `bun build --compile` and global installs.
4
+ export const VERSION = '2.0.6';
5
+
6
+ export function getVersion() {
7
+ return VERSION;
8
+ }
package/build.js ADDED
@@ -0,0 +1,18 @@
1
+ import * as esbuild from 'esbuild';
2
+ import fs from 'node:fs';
3
+
4
+ console.log("Bundling Mimo for the web...");
5
+
6
+ // Create the 'dist' directory if it doesn't exist
7
+ if (!fs.existsSync('./dist')) {
8
+ fs.mkdirSync('./dist');
9
+ }
10
+
11
+ esbuild.build({
12
+ entryPoints: ['web/index.js'],
13
+ bundle: true,
14
+ outfile: 'dist/mimo.web.js',
15
+ format: 'iife',
16
+ globalName: 'mimoBundle',
17
+ minify: true,
18
+ }).catch(() => process.exit(1));