Skip to content

Commit 881a41d

Browse files
Support ui lowering
1 parent 2328fad commit 881a41d

3 files changed

Lines changed: 127 additions & 10 deletions

File tree

multilingualprogramming/__main__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ def visit(ir_program, current_file: Path):
151151
continue
152152
visited.add(resolved)
153153
try:
154-
imported_ir = _parse_ir_from_file(resolved, None)
154+
imported_ir = _parse_ir_from_file(resolved, lang)
155155
except Exception as exc: # pylint: disable=broad-exception-caught
156156
warnings.append(f"Skipped UI import {node.module}: {exc}")
157157
continue

multilingualprogramming/codegen/ui_lowering.py

Lines changed: 123 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,27 +21,34 @@
2121
IRBooleanOp,
2222
IRCallExpr,
2323
IRCanvasBlock,
24+
IRClassDecl,
2425
IRCompareOp,
2526
IRConditionalExpr,
27+
IRDelStatement,
2628
IRDictLiteral,
2729
IRExprStatement,
2830
IRForLoop,
2931
IRFunction,
3032
IRIdentifier,
3133
IRIfStatement,
34+
IRImportStatement,
3235
IRIndexAccess,
3336
IRLiteral,
3437
IRListLiteral,
3538
IRNode,
3639
IRObserveBinding,
3740
IROnChange,
3841
IRProgram,
42+
IRRaiseStatement,
3943
IRRenderBlock,
4044
IRReturnStatement,
45+
IRSetLiteral,
46+
IRSliceExpr,
4147
IRTryStatement,
4248
IRUIElement,
4349
IRUnaryOp,
4450
IRViewBinding,
51+
IRWhileLoop,
4552
)
4653

4754
_USM_DIR = Path(__file__).parent.parent / "resources" / "usm"
@@ -75,6 +82,12 @@ def _keyword_aliases_for(category: str, concept: str) -> frozenset[str]:
7582

7683
_RANGE_NAMES = _builtin_aliases_for("range")
7784
_STR_NAMES = _builtin_aliases_for("str")
85+
_LIST_NAMES = _builtin_aliases_for("list")
86+
_NUMBER_NAMES = (
87+
_builtin_aliases_for("number")
88+
| _builtin_aliases_for("int")
89+
| _builtin_aliases_for("float")
90+
)
7891
_TRUE_NAMES = _keyword_aliases_for("logical", "TRUE")
7992
_FALSE_NAMES = _keyword_aliases_for("logical", "FALSE")
8093

@@ -155,7 +168,7 @@ def _lower_imported_modules(self, modules: dict[str, IRProgram]) -> None:
155168
exported_names = [
156169
node.name
157170
for node in module_program.body
158-
if isinstance(node, IRFunction) and node.is_async
171+
if isinstance(node, (IRClassDecl, IRFunction))
159172
]
160173
if not exported_names:
161174
continue
@@ -185,16 +198,18 @@ def _lower_node(self, node: IRNode) -> None:
185198
if isinstance(node, IRCanvasBlock):
186199
self._lower_canvas(node)
187200
return
201+
if isinstance(node, IRClassDecl):
202+
self._lower_class(node)
203+
return
188204
if isinstance(node, IRViewBinding):
189205
self._lower_view_binding(node)
190206
return
191207
if isinstance(node, IRRenderBlock):
192208
self._lower_render_block(node)
193209
return
194210
if isinstance(node, IRFunction):
195-
if node.is_async:
196-
self._lower_function(node)
197-
else:
211+
self._lower_function(node)
212+
if not node.is_async:
198213
self._ui_function_names.add(node.name)
199214
for child in node.body:
200215
self._lower_node(child)
@@ -304,6 +319,42 @@ class ReactiveEngine {
304319
return result;
305320
}
306321
322+
function __ml_contains(container, item) {
323+
if (container instanceof Set) {
324+
return container.has(item);
325+
}
326+
if (Array.isArray(container) || typeof container === 'string') {
327+
return container.includes(item);
328+
}
329+
if (container && typeof container === 'object') {
330+
return item in container;
331+
}
332+
return false;
333+
}
334+
335+
function __ml_add(container, item) {
336+
if (container instanceof Set) {
337+
container.add(item);
338+
return container;
339+
}
340+
if (Array.isArray(container)) {
341+
container.push(item);
342+
return container;
343+
}
344+
return container;
345+
}
346+
347+
function __ml_extend(container, values) {
348+
for (const value of values || []) {
349+
__ml_add(container, value);
350+
}
351+
return container;
352+
}
353+
354+
function __ml_slice(start, stop, step) {
355+
return { start, stop, step };
356+
}
357+
307358
const _engine = new ReactiveEngine();
308359
const __ml_signals = _engine.signals;"""
309360

@@ -342,6 +393,22 @@ def _lower_function(self, node: IRFunction) -> None:
342393
body = "\n".join(self._stmt_to_js(stmt, 1) for stmt in (node.body or []))
343394
self._functions.append(f"{keyword} {node.name}({params}) {{\n{body}\n}}")
344395

396+
def _lower_class(self, node: IRClassDecl) -> None:
397+
lines = [f"class {node.name} {{"]
398+
for child in node.body or []:
399+
if not isinstance(child, IRFunction):
400+
continue
401+
name = "constructor" if child.name == "__init__" else child.name
402+
keyword = "async " if child.is_async else ""
403+
params = ", ".join(param.name for param in (child.parameters or []))
404+
body = "\n".join(self._stmt_to_js(stmt, 2) for stmt in (child.body or []))
405+
lines.append(f" {keyword}{name}({params}) {{")
406+
if body:
407+
lines.append(body)
408+
lines.append(" }")
409+
lines.append("}")
410+
self._functions.append("\n".join(lines))
411+
345412
def _lower_render_block(self, node: IRRenderBlock) -> None:
346413
self._has_render_root = True
347414
lines = [
@@ -496,10 +563,20 @@ def _stmt_to_js(self, stmt: IRNode, indent: int) -> str:
496563
if stmt.value is None:
497564
return f"{pad}return;"
498565
return f"{pad}return {self._expr_to_js(stmt.value)};"
566+
if isinstance(stmt, IRRaiseStatement):
567+
if stmt.value is None:
568+
return f"{pad}throw new Error();"
569+
return f"{pad}throw {self._expr_to_js(stmt.value)};"
570+
if isinstance(stmt, IRImportStatement):
571+
return ""
572+
if isinstance(stmt, IRDelStatement):
573+
return f"{pad}delete {self._expr_to_js(stmt.target)};"
499574
if isinstance(stmt, IRIfStatement):
500575
return self._if_to_js(stmt, indent)
501576
if isinstance(stmt, IRForLoop):
502577
return self._for_to_js(stmt, indent)
578+
if isinstance(stmt, IRWhileLoop):
579+
return self._while_to_js(stmt, indent)
503580
if isinstance(stmt, IRTryStatement):
504581
return self._try_to_js(stmt, indent)
505582
if isinstance(stmt, IRExprStatement):
@@ -520,7 +597,7 @@ def _assignment_to_js(self, stmt: IRNode, indent: int) -> str:
520597
signal_name = self._signal_name(target.obj)
521598
index = self._expr_to_js(target.index)
522599
rendered = self._expr_to_js(value)
523-
if signal_name:
600+
if signal_name and signal_name in self._signal_names:
524601
return (
525602
f"{pad}_engine.get('{signal_name}').setIndex({index}, {rendered});"
526603
)
@@ -603,6 +680,13 @@ def _for_to_js(self, node: IRForLoop, indent: int) -> str:
603680
lines.append(f"{pad}}}")
604681
return "\n".join(lines)
605682

683+
def _while_to_js(self, node: IRWhileLoop, indent: int) -> str:
684+
pad = " " * indent
685+
lines = [f"{pad}while ({self._expr_to_js(node.condition)}) {{"]
686+
lines.extend(self._stmt_to_js(stmt, indent + 1) for stmt in (node.body or []))
687+
lines.append(f"{pad}}}")
688+
return "\n".join(lines)
689+
606690
def _element_to_js(self, elem: IRUIElement, parent_var: str, indent: int) -> list[str]:
607691
pad = " " * indent
608692
lines: list[str] = []
@@ -725,6 +809,8 @@ def _expr_to_js(self, node: IRNode | None) -> str:
725809
return str(node.value)
726810
if isinstance(node, IRListLiteral):
727811
return "[" + ", ".join(self._expr_to_js(item) for item in node.elements) + "]"
812+
if isinstance(node, IRSetLiteral):
813+
return "new Set([" + ", ".join(self._expr_to_js(item) for item in node.elements) + "])"
728814
if isinstance(node, IRDictLiteral):
729815
entries = []
730816
for entry in node.entries:
@@ -735,6 +821,8 @@ def _expr_to_js(self, node: IRNode | None) -> str:
735821
entries.append(f"[{rendered_key}]: {rendered_value}")
736822
return "{" + ", ".join(entries) + "}"
737823
if isinstance(node, IRIdentifier):
824+
if node.name == "self":
825+
return "this"
738826
if node.name in _TRUE_NAMES:
739827
return "true"
740828
if node.name in _FALSE_NAMES:
@@ -743,21 +831,36 @@ def _expr_to_js(self, node: IRNode | None) -> str:
743831
return f"_engine.get('{node.name}').get()"
744832
return node.name
745833
if isinstance(node, IRIndexAccess):
834+
if isinstance(node.index, IRSliceExpr):
835+
start = self._expr_to_js(node.index.start) if node.index.start is not None else "undefined"
836+
stop = self._expr_to_js(node.index.stop) if node.index.stop is not None else "undefined"
837+
return f"{self._expr_to_js(node.obj)}.slice({start}, {stop})"
746838
return f"{self._expr_to_js(node.obj)}[{self._expr_to_js(node.index)}]"
839+
if isinstance(node, IRSliceExpr):
840+
start = self._expr_to_js(node.start) if node.start is not None else "undefined"
841+
stop = self._expr_to_js(node.stop) if node.stop is not None else "undefined"
842+
if node.step is not None:
843+
return f"__ml_slice({start}, {stop}, {self._expr_to_js(node.step)})"
844+
return f"__ml_slice({start}, {stop})"
747845
if isinstance(node, IRAttributeAccess):
748846
return f"{self._expr_to_js(node.obj)}.{node.attr}"
749847
if isinstance(node, IRBinaryOp):
750848
return f"({self._expr_to_js(node.left)} {node.op} {self._expr_to_js(node.right)})"
751849
if isinstance(node, IRBooleanOp):
752-
op = " && " if node.op == "and" else " || "
850+
op = " && " if node.op in ("and", "et", "&&") else " || "
753851
return "(" + op.join(self._expr_to_js(value) for value in node.values) + ")"
754852
if isinstance(node, IRCompareOp):
755853
left = self._expr_to_js(node.left)
756854
parts: list[str] = []
757855
current_left = left
758856
for op, right in node.comparators:
759857
right_js = self._expr_to_js(right)
760-
parts.append(f"({current_left} {op} {right_js})")
858+
if op in ("in", "dans"):
859+
parts.append(f"__ml_contains({right_js}, {current_left})")
860+
elif op in ("not in", "non dans"):
861+
parts.append(f"(!__ml_contains({right_js}, {current_left}))")
862+
else:
863+
parts.append(f"({current_left} {op} {right_js})")
761864
current_left = right_js
762865
return " && ".join(parts) if parts else left
763866
if isinstance(node, IRUnaryOp):
@@ -778,6 +881,10 @@ def _expr_to_js(self, node: IRNode | None) -> str:
778881
return localized_method
779882
if call_name in _STR_NAMES:
780883
return f"String({args})"
884+
if call_name in _LIST_NAMES:
885+
return f"Array.from({args})" if args else "[]"
886+
if call_name in _NUMBER_NAMES:
887+
return f"Number({args})"
781888
if call_name in _RANGE_NAMES:
782889
return f"intervalle({args})"
783890
if call_name == "len":
@@ -804,14 +911,21 @@ def _localized_method_call_to_js(self, node: IRCallExpr) -> str | None:
804911
default = args[1] if len(args) > 1 else "undefined"
805912
return f"(({obj})?.[{key}] ?? {default})"
806913
if attr in {"ajouter", "append"}:
807-
return f"{obj}.push({', '.join(args)})"
914+
value = args[0] if args else "undefined"
915+
return f"__ml_add({obj}, {value})"
808916
if attr in {"etendre", "extend"}:
809917
values = args[0] if args else "[]"
810-
return f"{obj}.push(...({values} || []))"
918+
return f"__ml_extend({obj}, {values})"
811919
if attr in {"minuscule", "lower"}:
812920
return f"String({obj}).toLowerCase()"
813921
if attr in {"remplacer", "replace"}:
814922
return f"{obj}.replace({', '.join(args)})"
923+
if attr == "items":
924+
return f"Object.entries({obj})"
925+
if attr == "keys":
926+
return f"Object.keys({obj})"
927+
if attr == "pop" and args and args[0] == "0":
928+
return f"{obj}.shift()"
815929
return None
816930

817931
def _identifier_name(self, node: IRNode | None) -> str | None:

multilingualprogramming/resources/usm/builtins_aliases.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -795,6 +795,9 @@
795795
"da": ["flydende"],
796796
"fi": ["liukuluku"]
797797
},
798+
"number": {
799+
"fr": ["nombre"]
800+
},
798801
"str": {
799802
"fr": ["chaine", "chaîne"],
800803
"es": ["cadena"],

0 commit comments

Comments
 (0)