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.
- package/.gitattributes +24 -0
- package/LICENSE +21 -0
- package/README.md +91 -6
- package/adapters/browserAdapter.js +86 -0
- package/adapters/nodeAdapter.js +101 -0
- package/bin/cli.js +80 -0
- package/bin/commands/convert.js +27 -0
- package/bin/commands/doctor.js +139 -0
- package/bin/commands/eval.js +39 -0
- package/bin/commands/fmt.js +109 -0
- package/bin/commands/help.js +72 -0
- package/bin/commands/lint.js +117 -0
- package/bin/commands/repl.js +24 -0
- package/bin/commands/run.js +64 -0
- package/bin/commands/test.js +126 -0
- package/bin/utils/colors.js +38 -0
- package/bin/utils/formatError.js +47 -0
- package/bin/utils/fs.js +57 -0
- package/bin/utils/version.js +8 -0
- package/build.js +18 -0
- package/bun.lock +74 -0
- package/index.js +49 -39
- package/index.web.js +364 -0
- package/interpreter/BuiltinFunction.js +32 -0
- package/interpreter/ErrorHandler.js +120 -0
- package/interpreter/ExpressionEvaluator.js +106 -0
- package/interpreter/Interpreter.js +172 -0
- package/interpreter/MimoError.js +112 -0
- package/interpreter/ModuleLoader.js +236 -0
- package/interpreter/StatementExecutor.js +107 -0
- package/interpreter/Utils.js +82 -0
- package/interpreter/Values.js +87 -0
- package/interpreter/coreBuiltins.js +490 -0
- package/interpreter/environment.js +99 -0
- package/interpreter/evaluators/binaryExpressionEvaluator.js +111 -0
- package/interpreter/evaluators/collectionEvaluator.js +151 -0
- package/interpreter/evaluators/functionCallEvaluator.js +76 -0
- package/interpreter/evaluators/literalEvaluator.js +27 -0
- package/interpreter/evaluators/moduleAccessEvaluator.js +25 -0
- package/interpreter/evaluators/templateLiteralEvaluator.js +20 -0
- package/interpreter/executors/BaseExecutor.js +37 -0
- package/interpreter/executors/ControlFlowExecutor.js +206 -0
- package/interpreter/executors/FunctionExecutor.js +126 -0
- package/interpreter/executors/PatternMatchExecutor.js +93 -0
- package/interpreter/executors/VariableExecutor.js +144 -0
- package/interpreter/index.js +8 -0
- package/interpreter/stdlib/array/accessFunctions.js +61 -0
- package/interpreter/stdlib/array/arrayUtils.js +36 -0
- package/interpreter/stdlib/array/higherOrderFunctions.js +285 -0
- package/interpreter/stdlib/array/searchFunctions.js +77 -0
- package/interpreter/stdlib/array/setFunctions.js +49 -0
- package/interpreter/stdlib/array/transformationFunctions.js +68 -0
- package/interpreter/stdlib/array.js +85 -0
- package/interpreter/stdlib/assert.js +143 -0
- package/interpreter/stdlib/datetime.js +170 -0
- package/interpreter/stdlib/env.js +54 -0
- package/interpreter/stdlib/fs.js +161 -0
- package/interpreter/stdlib/http.js +92 -0
- package/interpreter/stdlib/json.js +70 -0
- package/interpreter/stdlib/math.js +309 -0
- package/interpreter/stdlib/object.js +142 -0
- package/interpreter/stdlib/path.js +69 -0
- package/interpreter/stdlib/regex.js +134 -0
- package/interpreter/stdlib/string.js +260 -0
- package/interpreter/suggestions.js +46 -0
- package/lexer/Lexer.js +245 -0
- package/lexer/TokenTypes.js +131 -0
- package/lexer/createToken.js +11 -0
- package/lexer/tokenizers/commentTokenizer.js +45 -0
- package/lexer/tokenizers/literalTokenizer.js +163 -0
- package/lexer/tokenizers/symbolTokenizer.js +69 -0
- package/lexer/tokenizers/whitespaceTokenizer.js +36 -0
- package/package.json +29 -11
- package/parser/ASTNodes.js +448 -0
- package/parser/Parser.js +188 -0
- package/parser/expressions/atomicExpressions.js +165 -0
- package/parser/expressions/conditionalExpressions.js +0 -0
- package/parser/expressions/operatorExpressions.js +79 -0
- package/parser/expressions/primaryExpressions.js +77 -0
- package/parser/parseStatement.js +184 -0
- package/parser/parserExpressions.js +115 -0
- package/parser/parserUtils.js +19 -0
- package/parser/statements/controlFlowParsers.js +106 -0
- package/parser/statements/functionParsers.js +314 -0
- package/parser/statements/moduleParsers.js +57 -0
- package/parser/statements/patternMatchParsers.js +124 -0
- package/parser/statements/variableParsers.js +155 -0
- package/repl.js +325 -0
- package/test.js +47 -1
- package/tools/PrettyPrinter.js +3 -0
- package/tools/convert/Args.js +46 -0
- package/tools/convert/Registry.js +91 -0
- package/tools/convert/Transpiler.js +78 -0
- package/tools/convert/plugins/README.md +66 -0
- package/tools/convert/plugins/alya/index.js +10 -0
- package/tools/convert/plugins/alya/to_alya.js +289 -0
- package/tools/convert/plugins/alya/visitors/expressions.js +257 -0
- package/tools/convert/plugins/alya/visitors/statements.js +403 -0
- package/tools/convert/plugins/base_converter.js +228 -0
- package/tools/convert/plugins/javascript/index.js +10 -0
- package/tools/convert/plugins/javascript/mimo_runtime.js +265 -0
- package/tools/convert/plugins/javascript/to_js.js +155 -0
- package/tools/convert/plugins/javascript/visitors/expressions.js +197 -0
- package/tools/convert/plugins/javascript/visitors/patterns.js +102 -0
- package/tools/convert/plugins/javascript/visitors/statements.js +236 -0
- package/tools/convert/plugins/python/index.js +10 -0
- package/tools/convert/plugins/python/mimo_runtime.py +811 -0
- package/tools/convert/plugins/python/to_py.js +329 -0
- package/tools/convert/plugins/python/visitors/expressions.js +272 -0
- package/tools/convert/plugins/python/visitors/patterns.js +100 -0
- package/tools/convert/plugins/python/visitors/statements.js +257 -0
- package/tools/convert.js +102 -0
- package/tools/format/CommentAttacher.js +190 -0
- package/tools/format/CommentLexer.js +152 -0
- package/tools/format/Printer.js +849 -0
- package/tools/format/config.js +107 -0
- package/tools/formatter.js +169 -0
- package/tools/lint/Linter.js +391 -0
- package/tools/lint/config.js +114 -0
- package/tools/lint/rules/consistent-return.js +62 -0
- package/tools/lint/rules/max-depth.js +56 -0
- package/tools/lint/rules/no-empty-function.js +45 -0
- package/tools/lint/rules/no-magic-numbers.js +46 -0
- package/tools/lint/rules/no-shadow.js +113 -0
- package/tools/lint/rules/no-unused-vars.js +26 -0
- package/tools/lint/rules/prefer-const.js +19 -0
- package/tools/linter.js +261 -0
- package/tools/replFormatter.js +93 -0
- package/tools/stamp-version.js +32 -0
- package/web/index.js +9 -0
- package/bun.lockb +0 -0
- package/cli.js +0 -84
- package/compiler/execute/interpreter.js +0 -68
- package/compiler/execute/interpreters/binary.js +0 -12
- package/compiler/execute/interpreters/call.js +0 -10
- package/compiler/execute/interpreters/if.js +0 -10
- package/compiler/execute/interpreters/try-catch.js +0 -10
- package/compiler/execute/interpreters/while.js +0 -8
- package/compiler/execute/utils/createfunction.js +0 -11
- package/compiler/execute/utils/evaluate.js +0 -20
- package/compiler/execute/utils/operate.js +0 -23
- package/compiler/lexer/processToken.js +0 -40
- package/compiler/lexer/tokenTypes.js +0 -4
- package/compiler/lexer/tokenizer.js +0 -63
- package/compiler/parser/expression/comparison.js +0 -18
- package/compiler/parser/expression/identifier.js +0 -29
- package/compiler/parser/expression/number.js +0 -10
- package/compiler/parser/expression/operator.js +0 -21
- package/compiler/parser/expression/punctuation.js +0 -31
- package/compiler/parser/expression/string.js +0 -6
- package/compiler/parser/parseExpression.js +0 -27
- package/compiler/parser/parseStatement.js +0 -35
- package/compiler/parser/parser.js +0 -16
- package/compiler/parser/statement/call.js +0 -26
- package/compiler/parser/statement/function.js +0 -29
- package/compiler/parser/statement/if.js +0 -34
- package/compiler/parser/statement/return.js +0 -10
- package/compiler/parser/statement/set.js +0 -11
- package/compiler/parser/statement/show.js +0 -10
- package/compiler/parser/statement/try-catch.js +0 -25
- package/compiler/parser/statement/while.js +0 -22
- package/converter/go/convert.js +0 -110
- package/converter/js/convert.js +0 -107
- package/i.js +0 -30
- package/jsconfig.json +0 -27
- 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
|
+
}
|
package/bin/utils/fs.js
ADDED
|
@@ -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));
|