Skip to content

Commit 4a019ad

Browse files
committed
Refactor converters: modularize visitors, add Alya plugin, preserve source spacing
- Modularize JS and Python converters into visitors/ sub-modules (statements, expressions, patterns) - Promote BaseConverter to full shared infrastructure: write/writeLine, indent/dedent, visitBlock, emitArgs, buildParamNames, CORE_BUILTINS, STDLIB_MODULES - Add emitLineGap() to BaseConverter; apply in visitProgram/visitBlock of all three converters to mirror blank-line spacing from source - Add Alya assembly converter plugin (integers, arithmetic, variables, if/while/for, functions, break/continue) - Expand JS and Python runtimes with full stdlib coverage (string, array, object, path, env, regex, http, assert modules) - Extend Transpiler stdlib allowlist to cover all STDLIB_MODULES - Ignore alya_vm symlink via .gitignore
1 parent 95d8db9 commit 4a019ad

17 files changed

Lines changed: 3310 additions & 1090 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,3 +180,4 @@ guides/
180180

181181
test/output
182182
.mimo_history
183+
alya_vm

tools/convert/Transpiler.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ export class Transpiler {
5252
ast.body.forEach(stmt => {
5353
if (stmt.type === 'ImportStatement') {
5454
const modulePath = stmt.path;
55-
if (!['fs', 'math', 'string', 'array', 'json', 'datetime'].includes(modulePath)) {
55+
if (!['fs', 'math', 'string', 'array', 'json', 'datetime', 'path', 'env', 'regex', 'http', 'object', 'assert'].includes(modulePath)) {
5656
let nextFilePath = path.resolve(path.dirname(filePath), modulePath);
5757
if (!nextFilePath.endsWith('.mimo')) {
5858
nextFilePath += '.mimo';
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { MimoToAlyaConverter } from './to_alya.js';
2+
3+
export const config = {
4+
name: 'alya',
5+
aliases: ['alya'],
6+
extension: '.alya',
7+
runtimeFile: null,
8+
};
9+
10+
export { MimoToAlyaConverter as Converter };
Lines changed: 289 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,289 @@
1+
/**
2+
* Mimo → Alya assembly converter.
3+
*
4+
* Targets the Alya VM assembler syntax (.alya files).
5+
*
6+
* Scope (core subset only):
7+
* ✅ Integer literals, arithmetic, boolean logic
8+
* ✅ Variables via named @registers
9+
* ✅ `show` (integer / boolean values) via `print`
10+
* ✅ Variable declarations (`set`, `let`, `const`)
11+
* ✅ Assignment operators (+= -= *= /= etc.)
12+
* ✅ Unary negation / `not`
13+
* ✅ if / else if / else → conditional jumps + labels
14+
* ✅ while loops → loop label + conditional jump
15+
* ✅ for…in range() → counter + loop label
16+
* ✅ break / continue → goto
17+
* ✅ Function declarations → call/return + label
18+
* ✅ return statement
19+
* ✅ Inline if expression → conditional move pattern
20+
* ⚠️ Strings (literal only, via LoadString + syscall 2)
21+
* ❌ Closures, higher-order functions, lambdas
22+
* ❌ Objects, arrays, pattern matching
23+
* ❌ Imports / stdlib
24+
* ❌ try/catch/throw
25+
* ❌ Decorators
26+
*
27+
* Design decisions:
28+
* - Variables use named @registers (Alya allows any @name, not just @r0–@r15).
29+
* - Expressions are evaluated into a temporary "virtual accumulator" register
30+
* (@__t0, @__t1, …) that the generator allocates and recycles.
31+
* - Calling convention:
32+
* • Args in @__a0, @__a1, … (caller sets before `call`)
33+
* • Return value in @__ret
34+
* • Caller saves its temporaries if needed (currently: simple depth-first
35+
* evaluation avoids conflicts within a single expression)
36+
* - Labels are generated as __L<n> (loops, conditionals, etc.)
37+
* - Function labels match function names verbatim.
38+
* - `print @reg` is used for integer/boolean output.
39+
* - `syscall` (ID 2 in @r0, string address in @r1) is used for string output.
40+
*/
41+
42+
import { BaseConverter } from '../base_converter.js';
43+
import { statementVisitors } from './visitors/statements.js';
44+
import { expressionVisitors } from './visitors/expressions.js';
45+
46+
export class MimoToAlyaConverter extends BaseConverter {
47+
constructor() {
48+
super();
49+
this.indentation = ' ';
50+
51+
// Counter for unique labels
52+
this._labelCounter = 0;
53+
54+
// Temporary register depth (for expression evaluation)
55+
this._tempDepth = 0;
56+
this._maxTempDepth = 0;
57+
58+
// Call-context stack: { breakLabel, continueLabel }
59+
this._loopStack = [];
60+
61+
// Deferred function bodies (emit after main code)
62+
this._functions = []; // [{ name, node }]
63+
64+
// Track which functions are being compiled (for recursion)
65+
this._inFunction = false;
66+
this._currentFunctionParams = null;
67+
this._currentFunctionLocals = null;
68+
}
69+
70+
// =========================================================================
71+
// Entry point
72+
// =========================================================================
73+
74+
convert(ast) {
75+
this.output = '';
76+
77+
// Emit a jump to `main` so function bodies defined first don't run.
78+
this.writeLine('; Generated by Mimo → Alya converter');
79+
this.writeLine('; Supported subset: integers, arithmetic, variables, if/while/for, functions');
80+
this.writeLine('');
81+
this.writeLine('goto __main');
82+
this.writeLine('');
83+
84+
// First pass: collect top-level function declarations so forward calls work.
85+
// (They will be emitted later, after the main body.)
86+
const body = ast.body || [];
87+
const mainStmts = [];
88+
for (const node of body) {
89+
if (node.type === 'FunctionDeclaration') {
90+
this._functions.push(node);
91+
} else {
92+
mainStmts.push(node);
93+
}
94+
}
95+
96+
// Emit all function bodies first (before main, after the goto)
97+
for (const fn of this._functions) {
98+
this._emitFunction(fn);
99+
this.writeLine('');
100+
}
101+
102+
// Main body
103+
this.writeLine('__main:');
104+
this.indent();
105+
let prev = null;
106+
for (const stmt of mainStmts) {
107+
this.emitLineGap(stmt, prev);
108+
this.visitNode(stmt);
109+
prev = stmt;
110+
}
111+
this.writeLine('halt');
112+
this.dedent();
113+
114+
return this.output;
115+
}
116+
117+
// =========================================================================
118+
// Label helpers
119+
// =========================================================================
120+
121+
/** Allocate a unique label string. */
122+
newLabel(prefix = 'L') {
123+
return `__${prefix}${this._labelCounter++}`;
124+
}
125+
126+
// =========================================================================
127+
// Temporary register helpers
128+
// =========================================================================
129+
130+
/**
131+
* Allocate the next temporary register name and return it.
132+
* Temporaries are @__t0, @__t1, … — they are sequential and
133+
* the generator never nests two allocations for the same slot
134+
* within a single expression evaluation path.
135+
*/
136+
allocTemp() {
137+
const name = `__t${this._tempDepth}`;
138+
this._tempDepth++;
139+
if (this._tempDepth > this._maxTempDepth) {
140+
this._maxTempDepth = this._tempDepth;
141+
}
142+
return name;
143+
}
144+
145+
freeTemp() {
146+
this._tempDepth--;
147+
}
148+
149+
// =========================================================================
150+
// Expression evaluation → register
151+
//
152+
// Every expression visitor returns the NAME of the Alya register that
153+
// holds the result (without the '@' prefix). The caller is responsible
154+
// for freeing temporaries it allocated.
155+
// =========================================================================
156+
157+
/**
158+
* Evaluate `node` and return the register name that holds its value.
159+
* This is the central dispatcher for expression codegen.
160+
*/
161+
evalExpr(node) {
162+
switch (node.type) {
163+
case 'Literal': return this.evalLiteral(node);
164+
case 'Identifier': return this.evalIdentifier(node);
165+
case 'BinaryExpression': return this.evalBinary(node);
166+
case 'UnaryExpression': return this.evalUnary(node);
167+
case 'InlineIfExpression': return this.evalInlineIf(node);
168+
case 'CallExpression': return this.evalCall(node);
169+
case 'ModuleAccess': return this._unsupported(node, 'ModuleAccess');
170+
case 'PropertyAccess': return this._unsupported(node, 'PropertyAccess');
171+
case 'ArrayAccess': return this._unsupported(node, 'ArrayAccess');
172+
default:
173+
return this._unsupported(node, node.type);
174+
}
175+
}
176+
177+
_unsupported(node, label) {
178+
const tmp = this.allocTemp();
179+
this.writeLine(`; UNSUPPORTED: ${label} — emitting 0`);
180+
this.writeLine(`@${tmp} := 0`);
181+
return tmp;
182+
}
183+
184+
// =========================================================================
185+
// Function emission
186+
// =========================================================================
187+
188+
_emitFunction(node) {
189+
const savedIn = this._inFunction;
190+
const savedParams = this._currentFunctionParams;
191+
const savedLocals = this._currentFunctionLocals;
192+
193+
this._inFunction = true;
194+
this._currentFunctionParams = new Set((node.params || []).map((p) => p.name));
195+
this._currentFunctionLocals = new Set();
196+
197+
const params = node.params || [];
198+
this.writeLine(`${node.name}:`);
199+
this.indent();
200+
201+
// Move arg registers to named param registers
202+
params.forEach((p, i) => {
203+
this.writeLine(`@${p.name} := @__a${i}`);
204+
});
205+
206+
// Emit body
207+
let prev = null;
208+
for (const stmt of node.body || []) {
209+
this.emitLineGap(stmt, prev);
210+
this.visitNode(stmt);
211+
prev = stmt;
212+
}
213+
214+
// Implicit return 0
215+
this.writeLine(`@__ret := 0`);
216+
this.writeLine(`return`);
217+
this.dedent();
218+
219+
this._inFunction = savedIn;
220+
this._currentFunctionParams = savedParams;
221+
this._currentFunctionLocals = savedLocals;
222+
}
223+
224+
/**
225+
* Track a local variable name declared inside the current function.
226+
* Used to determine what to push/pop around recursive calls.
227+
*/
228+
_trackLocal(name) {
229+
if (this._currentFunctionLocals) {
230+
this._currentFunctionLocals.add(name);
231+
}
232+
}
233+
234+
/**
235+
* Emit push/pop of all live registers (params + locals) around a call.
236+
* Returns the list of registers pushed (in order) so the caller can pop.
237+
*/
238+
_emitCallWithSave(calleeName) {
239+
// Collect everything that must be preserved
240+
const toSave = [
241+
...(this._currentFunctionParams || []),
242+
...(this._currentFunctionLocals || []),
243+
];
244+
245+
// Push in order
246+
for (const reg of toSave) {
247+
this.writeLine(`push @${reg}`);
248+
}
249+
250+
this.writeLine(`call ${calleeName}`);
251+
252+
// Pop in reverse order
253+
for (const reg of [...toSave].reverse()) {
254+
this.writeLine(`@${reg} := pop`);
255+
}
256+
}
257+
258+
// =========================================================================
259+
// Loop stack helpers
260+
// =========================================================================
261+
262+
pushLoop(breakLabel, continueLabel) {
263+
this._loopStack.push({ breakLabel, continueLabel });
264+
}
265+
266+
popLoop() {
267+
this._loopStack.pop();
268+
}
269+
270+
currentLoop() {
271+
return this._loopStack[this._loopStack.length - 1] || null;
272+
}
273+
274+
// =========================================================================
275+
// Overrides
276+
// =========================================================================
277+
278+
onUndefinedVisitor(node) {
279+
this.writeLine(`; UNSUPPORTED NODE: ${node.type}`);
280+
}
281+
282+
/** Alya has no notion of blocks — we just emit flat instructions. */
283+
visitBlock(statements) {
284+
(statements || []).forEach((stmt) => this.visitNode(stmt));
285+
}
286+
}
287+
288+
// Mix in visitors
289+
Object.assign(MimoToAlyaConverter.prototype, statementVisitors, expressionVisitors);

0 commit comments

Comments
 (0)