Skip to content

Commit b825981

Browse files
Add comprehensive tests for array, set, string, and dictionary operations
- Implement tests for array slicing, range generation, and array comprehension in `test_pythonic_arrays.py`. - Add tests for set creation, addition, removal, and various set operations in `test_sets_try_with.py`. - Introduce tests for string formatting, string methods, and dictionary methods in `test_str_format_dict_methods.py`. - Ensure coverage for error handling with try/catch/else/finally constructs and context management using the with statement. - Include integration tests that combine multiple features to validate functionality across different components.
1 parent bd870ac commit b825981

13 files changed

Lines changed: 4760 additions & 24 deletions

techlang.db

8 KB
Binary file not shown.

techlang/basic_commands.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,19 +16,35 @@ class BasicCommandHandler:
1616
"boot", "ping", "crash", "reboot", "print", "upload",
1717
"download", "debug", "hack", "lag", "sleep", "yield", "fork", "set", "add",
1818
"mul", "sub", "div", "loop", "while", "switch", "match", "try", "catch", "default", "case", "end", "if", "def", "call", "input", "alias", "import", "package", "struct", "macro", "inline", "do",
19+
"else", "finally", "with", # Extended try/catch and context managers
1920
"db_create", "db_insert", "db_select", "db_update", "db_delete", "db_execute", "db_close",
2021
# Advanced DB
2122
"db_begin", "db_commit", "db_rollback", "db_tables", "db_schema", "db_indexes", "db_connect", "db_disconnect",
2223
# Array commands - for working with lists
2324
"array_create", "array_set", "array_get", "array_push", "array_pop",
2425
"array_map", "array_filter", "array_sort", "array_reverse", "array_find", "array_unique", "array_join",
26+
"array_slice", "array_comprehend", "range", "enumerate", "array_zip", "array_apply",
27+
"any", "all", "array_min", "array_max", "array_sorted",
28+
# Lambda/anonymous functions
29+
"lambda", "lambda_call",
2530
# String commands - for working with text
2631
"str_create", "str_concat", "str_length", "str_substring", "string_interpolate", "string_match",
2732
"str_split", "str_replace", "str_trim", "str_upper", "str_lower", "str_contains", "str_reverse",
33+
"str_format", "str_startswith", "str_endswith", "str_count", "str_find", "str_rfind",
34+
"str_isdigit", "str_isalpha", "str_isalnum",
2835
# Dictionary commands - for working with key-value pairs
2936
"dict_create", "dict_set", "dict_get", "dict_keys",
37+
"dict_values", "dict_items", "dict_update", "dict_pop", "dict_get_default",
38+
"dict_has_key", "dict_clear", "dict_len",
39+
# Set commands - for working with unique collections
40+
"set_create", "set_add", "set_remove", "set_contains", "set_len", "set_clear",
41+
"set_union", "set_intersection", "set_difference", "set_symmetric_difference",
42+
"set_issubset", "set_issuperset", "set_to_array", "array_to_set",
3043
# Type checking commands - for runtime introspection
31-
"type_of", "is_number", "is_string", "is_array", "is_dict", "is_struct",
44+
"type_of", "is_number", "is_string", "is_array", "is_dict", "is_struct", "is_set", "is_generator",
45+
# Generator commands - for lazy iteration
46+
"generator_create", "generator_next", "generator_reset", "generator_to_array",
47+
"generator_from_range", "generator_take",
3248
# Regex commands - for pattern matching
3349
"regex_match", "regex_find", "regex_replace", "regex_split",
3450
# Crypto/encoding commands

techlang/control_flow.py

Lines changed: 205 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -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+

techlang/core.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,12 @@ class InterpreterState:
6060
# Dictionaries that store key-value pairs (like a phone book)
6161
dictionaries: Dict[str, Dict[str, Union[int, str]]] = None
6262

63+
# Sets that store unique values (like a bag of unique items)
64+
sets: Dict[str, Set[Union[int, str]]] = None
65+
66+
# Generators - stateful iterators that yield values one at a time
67+
generators: Dict[str, Dict[str, object]] = None # name -> {func, index, values, exhausted}
68+
6369
# Struct type definitions (field name -> type) and instances (instance -> {type, fields})
6470
struct_defs: Dict[str, Dict[str, str]] = None
6571
structs: Dict[str, Dict[str, object]] = None
@@ -154,6 +160,10 @@ def __post_init__(self):
154160
self.strings = {}
155161
if self.dictionaries is None:
156162
self.dictionaries = {}
163+
if self.sets is None:
164+
self.sets = {}
165+
if self.generators is None:
166+
self.generators = {}
157167
if self.struct_defs is None:
158168
self.struct_defs = {}
159169
if self.structs is None:

0 commit comments

Comments
 (0)