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+
307358const _engine = new ReactiveEngine();
308359const __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 :
0 commit comments