@@ -327,45 +327,68 @@ def handle_match(state: InterpreterState, tokens: List[str], index: int, execute
327327 @staticmethod
328328 def handle_try (state : InterpreterState , tokens : List [str ], index : int , execute_block : Callable ) -> int :
329329 """
330- try ... catch ... end
330+ try ... catch [error_var] ... else ... finally ... end
331331 Execute try block; if any error is emitted inside, suppress it and run catch.
332+ If no error, run else block (if present).
333+ Finally block always runs (if present).
332334 We detect errors by inspecting output lines appended during try.
333335 """
334336 start_index = index + 1
335337 try_body , end_index = BlockCollector .collect_block (start_index , tokens )
336338
337- # Split try_body by top-level 'catch'
339+ # Split try_body by top-level 'catch', 'else', 'finally'
338340 i = 0
339341 nested_depth = 0
340342 try_block : List [str ] = []
341343 catch_block : List [str ] = []
342- found_catch = False
344+ else_block : List [str ] = []
345+ finally_block : List [str ] = []
346+ current_section = "try"
343347 catch_error_var : Optional [str ] = None
344348 catch_stack_var : Optional [str ] = None
345349 from .basic_commands import BasicCommandHandler # local import to avoid circular dependency
346350
347- reserved_tokens = {"case" , "catch" , "default" , "def" , "end" , "if" , "loop" , "match" , "switch" , "try" , "while" }
351+ reserved_tokens = {"case" , "catch" , "default" , "def" , "end" , "if" , "loop" , "match" , "switch" , "try" , "while" , "else" , "finally" }
348352 reserved_tokens .update (BasicCommandHandler .KNOWN_COMMANDS )
353+
349354 while i < len (try_body ):
350355 t = try_body [i ]
351356 if t in {"def" , "if" , "loop" , "while" , "switch" , "try" }:
352357 nested_depth += 1
353358 elif t == "end" and nested_depth > 0 :
354359 nested_depth -= 1
355- if t == "catch" and nested_depth == 0 and not found_catch :
356- found_catch = True
357- if i + 1 < len (try_body ) and try_body [i + 1 ] not in reserved_tokens :
358- catch_error_var = try_body [i + 1 ]
359- i += 1
360+
361+ # Check for section keywords at top level
362+ if nested_depth == 0 :
363+ if t == "catch" and current_section == "try" :
364+ current_section = "catch"
365+ # Check for optional error variable
360366 if i + 1 < len (try_body ) and try_body [i + 1 ] not in reserved_tokens :
361- catch_stack_var = try_body [i + 1 ]
367+ catch_error_var = try_body [i + 1 ]
362368 i += 1
363- i += 1
364- continue
365- if not found_catch :
369+ if i + 1 < len (try_body ) and try_body [i + 1 ] not in reserved_tokens :
370+ catch_stack_var = try_body [i + 1 ]
371+ i += 1
372+ i += 1
373+ continue
374+ elif t == "else" and current_section in ("try" , "catch" ):
375+ current_section = "else"
376+ i += 1
377+ continue
378+ elif t == "finally" and current_section in ("try" , "catch" , "else" ):
379+ current_section = "finally"
380+ i += 1
381+ continue
382+
383+ # Add token to current section
384+ if current_section == "try" :
366385 try_block .append (t )
367- else :
386+ elif current_section == "catch" :
368387 catch_block .append (t )
388+ elif current_section == "else" :
389+ else_block .append (t )
390+ elif current_section == "finally" :
391+ finally_block .append (t )
369392 i += 1
370393
371394 # Snapshot output length; if an [Error: ...] is added during try, run catch
@@ -374,14 +397,28 @@ def handle_try(state: InterpreterState, tokens: List[str], index: int, execute_b
374397 new_lines = state .output [before_len :]
375398 error_lines = [line for line in new_lines if line .startswith ("[Error:" )]
376399 error_emitted = bool (error_lines )
377- if error_emitted and catch_block :
378- if catch_error_var :
379- message = ControlFlowHandler ._extract_error_message (error_lines [0 ])
380- state .set_variable (catch_error_var , message )
381- if catch_stack_var :
382- state .set_variable (catch_stack_var , str (state .stack ))
383- execute_block (catch_block )
400+
401+ if error_emitted :
402+ # Error occurred - run catch block
403+ if catch_block :
404+ if catch_error_var :
405+ message = ControlFlowHandler ._extract_error_message (error_lines [0 ])
406+ state .set_variable (catch_error_var , message )
407+ if catch_stack_var :
408+ state .set_variable (catch_stack_var , str (state .stack ))
409+ execute_block (catch_block )
410+ else :
411+ # No error - run else block
412+ if else_block :
413+ execute_block (else_block )
414+
415+ # Finally always runs
416+ if finally_block :
417+ execute_block (finally_block )
418+
384419 return end_index - index
420+
421+ @staticmethod
385422 def handle_def (state : InterpreterState , tokens : List [str ], index : int ) -> int :
386423 if index + 1 >= len (tokens ):
387424 state .add_error ("Invalid 'def' command. Use: def <function_name> [params...] ... end" )
@@ -721,3 +758,150 @@ def _evaluate_condition(var_value: Union[int, str], op: str, compare_val: Union[
721758 return var_value <= compare_val
722759
723760 return False # Unknown operator
761+
762+ @staticmethod
763+ def handle_with (state : InterpreterState , tokens : List [str ], index : int , execute_block : Callable ) -> int :
764+ """
765+ with <resource_type> <name> [args...] do ... end
766+ Context manager that ensures cleanup happens even if errors occur.
767+
768+ Supported resource types:
769+ - file: with file "path" "mode" as f do ... end
770+ - timer: with timer as t do ... end (stores elapsed time in t)
771+ - suppress: with suppress do ... end (suppresses all errors in block)
772+ - transaction: with transaction do ... end (auto-commits or rolls back DB)
773+ """
774+ import time
775+
776+ if index + 2 >= len (tokens ):
777+ state .add_error ("with requires resource type and body. Use: with <type> [args] do ... end" )
778+ return 0
779+
780+ resource_type = tokens [index + 1 ]
781+
782+ # Find 'do' keyword to determine args and start of body
783+ do_index = - 1
784+ for i in range (index + 2 , len (tokens )):
785+ if tokens [i ] == "do" :
786+ do_index = i
787+ break
788+
789+ if do_index == - 1 :
790+ state .add_error ("with block requires 'do' keyword. Use: with <type> [args] do ... end" )
791+ return 0
792+
793+ # Collect args between resource_type and 'do'
794+ args = tokens [index + 2 :do_index ]
795+
796+ # Collect block body
797+ body_start = do_index + 1
798+ body_block , end_index = BlockCollector .collect_block (body_start , tokens )
799+
800+ # Handle different resource types
801+ if resource_type == "file" :
802+ return ControlFlowHandler ._with_file (state , args , body_block , execute_block , end_index - index )
803+ elif resource_type == "timer" :
804+ return ControlFlowHandler ._with_timer (state , args , body_block , execute_block , end_index - index )
805+ elif resource_type == "suppress" :
806+ return ControlFlowHandler ._with_suppress (state , args , body_block , execute_block , end_index - index )
807+ elif resource_type == "transaction" :
808+ return ControlFlowHandler ._with_transaction (state , args , body_block , execute_block , end_index - index )
809+ else :
810+ state .add_error (f"Unknown resource type '{ resource_type } '. Supported: file, timer, suppress, transaction" )
811+ return end_index - index
812+
813+ @staticmethod
814+ def _with_file (state : InterpreterState , args : List [str ], body : List [str ], execute_block : Callable , consumed : int ) -> int :
815+ """Handle with file ... do ... end - auto-closes file after block."""
816+ import os
817+ from pathlib import Path
818+
819+ if len (args ) < 2 :
820+ state .add_error ("with file requires path and variable. Use: with file \" path\" as <var> do ... end" )
821+ return consumed
822+
823+ path_token = args [0 ]
824+ # Find 'as' keyword
825+ if len (args ) >= 3 and args [1 ] == "as" :
826+ var_name = args [2 ]
827+ else :
828+ var_name = args [1 ] # Fallback if no 'as'
829+
830+ # Remove quotes from path
831+ if path_token .startswith ('"' ) and path_token .endswith ('"' ):
832+ path_token = path_token [1 :- 1 ]
833+
834+ # Resolve path relative to base_dir
835+ base_dir = getattr (state , 'base_dir' , '.' )
836+ full_path = str (Path (base_dir ) / path_token )
837+
838+ # Store path in a string variable for the block to use
839+ state .strings [var_name ] = full_path
840+
841+ # Execute body (file operations will use the path)
842+ execute_block (body )
843+
844+ # Cleanup: remove the variable (simulates file close)
845+ if var_name in state .strings :
846+ del state .strings [var_name ]
847+
848+ return consumed
849+
850+ @staticmethod
851+ def _with_timer (state : InterpreterState , args : List [str ], body : List [str ], execute_block : Callable , consumed : int ) -> int :
852+ """Handle with timer as <var> do ... end - stores elapsed time."""
853+ import time
854+
855+ var_name = None
856+ if len (args ) >= 2 and args [0 ] == "as" :
857+ var_name = args [1 ]
858+ elif len (args ) >= 1 :
859+ var_name = args [0 ]
860+
861+ start_time = time .time ()
862+ execute_block (body )
863+ elapsed = time .time () - start_time
864+
865+ if var_name :
866+ # Store elapsed time in milliseconds
867+ state .set_variable (var_name , int (elapsed * 1000 ))
868+
869+ return consumed
870+
871+ @staticmethod
872+ def _with_suppress (state : InterpreterState , args : List [str ], body : List [str ], execute_block : Callable , consumed : int ) -> int :
873+ """Handle with suppress do ... end - suppresses all errors in block."""
874+ before_len = len (state .output )
875+ execute_block (body )
876+
877+ # Remove any error lines that were added
878+ new_output = []
879+ for i , line in enumerate (state .output ):
880+ if i < before_len or not line .startswith ("[Error:" ):
881+ new_output .append (line )
882+ state .output [:] = new_output
883+
884+ return consumed
885+
886+ @staticmethod
887+ def _with_transaction (state : InterpreterState , args : List [str ], body : List [str ], execute_block : Callable , consumed : int ) -> int :
888+ """Handle with transaction do ... end - auto-commits or rolls back database."""
889+ from .database import DatabaseHandler
890+
891+ # Begin transaction
892+ DatabaseHandler .handle_begin (state , ["db_begin" ], 0 )
893+
894+ before_len = len (state .output )
895+ execute_block (body )
896+ new_lines = state .output [before_len :]
897+ error_lines = [line for line in new_lines if line .startswith ("[Error:" )]
898+
899+ if error_lines :
900+ # Rollback on error
901+ DatabaseHandler .handle_rollback (state , ["db_rollback" ], 0 )
902+ else :
903+ # Commit on success
904+ DatabaseHandler .handle_commit (state , ["db_commit" ], 0 )
905+
906+ return consumed
907+
0 commit comments