diff --git a/include/runtime/types.h b/include/runtime/types.h index 55897425..6e08ca83 100644 --- a/include/runtime/types.h +++ b/include/runtime/types.h @@ -130,6 +130,7 @@ typedef struct { Type **param_types; Expr **param_defaults; // Default value expressions (NULL for required params) int *param_is_ref; // 1 if parameter is pass-by-reference (ref keyword) + int *param_is_const; // 1 if parameter is const (immutable) uint32_t *param_hashes; // Pre-computed hashes of param names (optimization) int num_params; char *rest_param; // Name of rest parameter (...args), NULL if none diff --git a/runtime/include/hemlock_runtime.h b/runtime/include/hemlock_runtime.h index 1014df24..a977cce3 100644 --- a/runtime/include/hemlock_runtime.h +++ b/runtime/include/hemlock_runtime.h @@ -506,6 +506,8 @@ void hml_defer_push_call(HmlValue fn); void hml_defer_push_call_with_args(HmlValue fn, HmlValue *args, int num_args); void hml_defer_pop_and_execute(void); void hml_defer_execute_all(void); +int hml_defer_save_depth(void); +void hml_defer_execute_to_depth(int saved_depth); // ========== ASYNC/CONCURRENCY ========== diff --git a/runtime/src/builtins_func.c b/runtime/src/builtins_func.c index 0954f8a2..e52fa9b7 100644 --- a/runtime/src/builtins_func.c +++ b/runtime/src/builtins_func.c @@ -90,6 +90,25 @@ void hml_defer_execute_all(void) { } } +int hml_defer_save_depth(void) { + int depth = 0; + DeferEntry *e = g_defer_stack; + while (e) { depth++; e = e->next; } + return depth; +} + +void hml_defer_execute_to_depth(int saved_depth) { + // Count current depth + int current = 0; + DeferEntry *e = g_defer_stack; + while (e) { current++; e = e->next; } + // Execute only the entries added since saved_depth + while (current > saved_depth && g_defer_stack) { + hml_defer_pop_and_execute(); + current--; + } +} + // Helper for deferring HmlValue function calls static void hml_defer_call_wrapper(void *arg) { HmlValue *fn_ptr = (HmlValue *)arg; diff --git a/src/backends/compiler/codegen_expr_match.c b/src/backends/compiler/codegen_expr_match.c index 2dbf8e50..be380e81 100644 --- a/src/backends/compiler/codegen_expr_match.c +++ b/src/backends/compiler/codegen_expr_match.c @@ -38,6 +38,45 @@ static const char* type_kind_to_hml_type_enum(TypeKind kind) { } } +// Helper: collect all binding names from a pattern (for pre-declaration in OR patterns) +static void collect_pattern_bindings(Pattern *p, char ***names, int *count, int *cap) { + if (!p) return; + switch (p->type) { + case PATTERN_BINDING: + if (*count >= *cap) { + *cap = *cap ? *cap * 2 : 4; + *names = realloc(*names, (size_t)*cap * sizeof(char*)); + } + (*names)[(*count)++] = p->as.binding.name; + break; + case PATTERN_TYPED: + if (p->as.typed.name) { + if (*count >= *cap) { + *cap = *cap ? *cap * 2 : 4; + *names = realloc(*names, (size_t)*cap * sizeof(char*)); + } + (*names)[(*count)++] = p->as.typed.name; + } + break; + case PATTERN_OR: + // Only collect from first alternative (all should bind the same names) + if (p->as.or_pattern.num_alternatives > 0) + collect_pattern_bindings(p->as.or_pattern.alternatives[0], names, count, cap); + break; + case PATTERN_OBJECT: + for (int i = 0; i < p->as.object.num_fields; i++) + if (p->as.object.fields[i].pattern) + collect_pattern_bindings(p->as.object.fields[i].pattern, names, count, cap); + break; + case PATTERN_ARRAY: + for (int i = 0; i < p->as.array.num_elements; i++) + collect_pattern_bindings(p->as.array.elements[i].pattern, names, count, cap); + break; + default: + break; + } +} + // Generate code to match a pattern against a value void codegen_pattern_match(CodegenContext *ctx, Pattern *pattern, const char *scrutinee, const char *fail_label) { if (!pattern) { @@ -67,11 +106,17 @@ void codegen_pattern_match(CodegenContext *ctx, Pattern *pattern, const char *sc case PATTERN_BINDING: { // Bind the value to a local variable // Use the actual variable name so identifier lookup works - codegen_add_local(ctx, pattern->as.binding.name); - if (ctx->current_scope) { - scope_add_var(ctx->current_scope, pattern->as.binding.name); + if (codegen_is_local(ctx, pattern->as.binding.name)) { + // Already pre-declared (e.g., from OR pattern), use assignment + codegen_writeln(ctx, "hml_release(&%s);", pattern->as.binding.name); + codegen_writeln(ctx, "%s = %s;", pattern->as.binding.name, scrutinee); + } else { + codegen_add_local(ctx, pattern->as.binding.name); + if (ctx->current_scope) { + scope_add_var(ctx->current_scope, pattern->as.binding.name); + } + codegen_writeln(ctx, "HmlValue %s = %s;", pattern->as.binding.name, scrutinee); } - codegen_writeln(ctx, "HmlValue %s = %s;", pattern->as.binding.name, scrutinee); codegen_writeln(ctx, "hml_retain(&%s);", pattern->as.binding.name); break; } @@ -90,11 +135,17 @@ void codegen_pattern_match(CodegenContext *ctx, Pattern *pattern, const char *sc } // Bind if name is provided if (pattern->as.typed.name) { - codegen_add_local(ctx, pattern->as.typed.name); - if (ctx->current_scope) { - scope_add_var(ctx->current_scope, pattern->as.typed.name); + if (codegen_is_local(ctx, pattern->as.typed.name)) { + // Already pre-declared (e.g., from OR pattern) + codegen_writeln(ctx, "hml_release(&%s);", pattern->as.typed.name); + codegen_writeln(ctx, "%s = %s;", pattern->as.typed.name, scrutinee); + } else { + codegen_add_local(ctx, pattern->as.typed.name); + if (ctx->current_scope) { + scope_add_var(ctx->current_scope, pattern->as.typed.name); + } + codegen_writeln(ctx, "HmlValue %s = %s;", pattern->as.typed.name, scrutinee); } - codegen_writeln(ctx, "HmlValue %s = %s;", pattern->as.typed.name, scrutinee); codegen_writeln(ctx, "hml_retain(&%s);", pattern->as.typed.name); } break; @@ -104,6 +155,19 @@ void codegen_pattern_match(CodegenContext *ctx, Pattern *pattern, const char *sc // Try each alternative - if any matches, continue; if all fail, go to fail_label char *success_label = codegen_label(ctx); + // Pre-declare binding variables so they survive the inner {} blocks + char **binding_names = NULL; + int binding_count = 0, binding_cap = 0; + collect_pattern_bindings(pattern, &binding_names, &binding_count, &binding_cap); + for (int b = 0; b < binding_count; b++) { + codegen_add_local(ctx, binding_names[b]); + if (ctx->current_scope) { + scope_add_var(ctx->current_scope, binding_names[b]); + } + codegen_writeln(ctx, "HmlValue %s = hml_val_null();", binding_names[b]); + } + free(binding_names); + for (int i = 0; i < pattern->as.or_pattern.num_alternatives; i++) { char *next_alt_label = (i + 1 < pattern->as.or_pattern.num_alternatives) ? codegen_label(ctx) : NULL; diff --git a/src/backends/compiler/codegen_program.c b/src/backends/compiler/codegen_program.c index 7fe43e68..0045526a 100644 --- a/src/backends/compiler/codegen_program.c +++ b/src/backends/compiler/codegen_program.c @@ -143,6 +143,9 @@ void codegen_function_decl(CodegenContext *ctx, Expr *func, const char *name, An codegen_writeln(ctx, "HML_CALL_ENTER();"); } + // Save defer stack depth so we only execute this function's defers on return + codegen_writeln(ctx, "int _defer_depth = hml_defer_save_depth();"); + // OPTIMIZATION: Tail call elimination // Check if function is tail recursive and set up for tail call optimization // Tail call optimization converts: return func(args) -> reassign params; goto start @@ -165,10 +168,10 @@ void codegen_function_decl(CodegenContext *ctx, Expr *func, const char *name, An // Generate body funcgen_generate_body(ctx, func); - // Execute defers before implicit return + // Execute defers before implicit return (only this function's defers) codegen_defer_execute_all(ctx); if (ctx->has_defers) { - codegen_writeln(ctx, "hml_defer_execute_all();"); + codegen_writeln(ctx, "hml_defer_execute_to_depth(_defer_depth);"); } // Release body-local variables and shared env before returning @@ -303,16 +306,19 @@ void codegen_closure_impl(CodegenContext *ctx, ClosureInfo *closure) { codegen_writeln(ctx, "HML_CALL_ENTER();"); } + // Save defer stack depth so we only execute this function's defers on return + codegen_writeln(ctx, "int _defer_depth = hml_defer_save_depth();"); + // Set up shared environment for nested closures funcgen_setup_shared_env(ctx, func, closure); // Generate body funcgen_generate_body(ctx, func); - // Execute defers before implicit return + // Execute defers before implicit return (only this function's defers) codegen_defer_execute_all(ctx); if (ctx->has_defers) { - codegen_writeln(ctx, "hml_defer_execute_all();"); + codegen_writeln(ctx, "hml_defer_execute_to_depth(_defer_depth);"); } // Release body-local variables before returning diff --git a/src/backends/compiler/codegen_stmt.c b/src/backends/compiler/codegen_stmt.c index 95d0213a..d8180dde 100644 --- a/src/backends/compiler/codegen_stmt.c +++ b/src/backends/compiler/codegen_stmt.c @@ -969,7 +969,7 @@ void codegen_stmt(CodegenContext *ctx, Stmt *stmt) { codegen_defer_execute_all(ctx); // Execute any runtime defers (from loops) - only if this function has defers if (ctx->has_defers) { - codegen_writeln(ctx, "hml_defer_execute_all();"); + codegen_writeln(ctx, "hml_defer_execute_to_depth(_defer_depth);"); } // Release body-local variables codegen_emit_local_cleanup(ctx, NULL); @@ -1068,7 +1068,7 @@ void codegen_stmt(CodegenContext *ctx, Stmt *stmt) { char *value = codegen_expr(ctx, stmt->as.return_stmt.value); // Execute any runtime defers (from loops) - only if this function has defers if (ctx->has_defers) { - codegen_writeln(ctx, "hml_defer_execute_all();"); + codegen_writeln(ctx, "hml_defer_execute_to_depth(_defer_depth);"); } // Release body-local variables codegen_emit_local_cleanup(ctx, NULL); @@ -1080,7 +1080,7 @@ void codegen_stmt(CodegenContext *ctx, Stmt *stmt) { } else { // Execute any runtime defers (from loops) - only if this function has defers if (ctx->has_defers) { - codegen_writeln(ctx, "hml_defer_execute_all();"); + codegen_writeln(ctx, "hml_defer_execute_to_depth(_defer_depth);"); } // Release body-local variables codegen_emit_local_cleanup(ctx, NULL); @@ -1267,7 +1267,7 @@ void codegen_stmt(CodegenContext *ctx, Stmt *stmt) { codegen_indent_inc(ctx); // Execute any runtime defers (from loops) - only if this function has defers if (ctx->has_defers) { - codegen_writeln(ctx, "hml_defer_execute_all();"); + codegen_writeln(ctx, "hml_defer_execute_to_depth(_defer_depth);"); } // Release body-local variables before returning codegen_emit_local_cleanup(ctx, NULL); diff --git a/src/backends/interpreter/builtins/concurrency.c b/src/backends/interpreter/builtins/concurrency.c index 9e49edd2..a23836e2 100644 --- a/src/backends/interpreter/builtins/concurrency.c +++ b/src/backends/interpreter/builtins/concurrency.c @@ -359,6 +359,7 @@ Value builtin_join(Value *args, int num_args, ExecutionContext *ctx) { if (task->ctx->exception_state.is_throwing) { // Re-throw the exception in the current context ctx->exception_state = task->ctx->exception_state; + VALUE_RETAIN(ctx->exception_state.exception_value); pthread_mutex_unlock((pthread_mutex_t*)task->task_mutex); return val_null(); } diff --git a/src/backends/interpreter/environment.c b/src/backends/interpreter/environment.c index 601de98e..0cacf2ec 100644 --- a/src/backends/interpreter/environment.c +++ b/src/backends/interpreter/environment.c @@ -596,7 +596,7 @@ void env_define_borrowed(Environment *env, const char *name, Value value, int is // This is safe because we know params are unique (enforced by parser) and env is freshly created // Note: This is typically called on newly created environments that aren't shared yet, // but we still lock for consistency if the env happens to be shared. -void env_define_param(Environment *env, const char *name, uint32_t hash, Value value) { +void env_define_param(Environment *env, const char *name, uint32_t hash, Value value, int is_const) { // Lock environment for thread-safe access pthread_mutex_t *mutex = (pthread_mutex_t*)env->mutex; if (mutex) pthread_mutex_lock(mutex); @@ -613,7 +613,7 @@ void env_define_param(Environment *env, const char *name, uint32_t hash, Value v } VALUE_RETAIN(value); env->values[index] = value; - env->is_const[index] = 0; // Params are never const + env->is_const[index] = is_const; env->count++; // Insert with pre-computed hash diff --git a/src/backends/interpreter/internal.h b/src/backends/interpreter/internal.h index 81cefff5..aa0f4722 100644 --- a/src/backends/interpreter/internal.h +++ b/src/backends/interpreter/internal.h @@ -177,7 +177,7 @@ void env_define(Environment *env, const char *name, Value value, int is_const, E // Fast variant that borrows the name string without strdup - caller must ensure name outlives env void env_define_borrowed(Environment *env, const char *name, Value value, int is_const, ExecutionContext *ctx); // Fastest variant for function params - uses pre-computed hash, skips "already defined" check -void env_define_param(Environment *env, const char *name, uint32_t hash, Value value); +void env_define_param(Environment *env, const char *name, uint32_t hash, Value value, int is_const); void env_set(Environment *env, const char *name, Value value, ExecutionContext *ctx); Value env_get(Environment *env, const char *name, ExecutionContext *ctx); diff --git a/src/backends/interpreter/runtime/eval_calls.c b/src/backends/interpreter/runtime/eval_calls.c index 130ba2a5..604e4d17 100644 --- a/src/backends/interpreter/runtime/eval_calls.c +++ b/src/backends/interpreter/runtime/eval_calls.c @@ -1023,7 +1023,8 @@ Value eval_call_expr(Expr *expr, Environment *env, ExecutionContext *ctx) { } // Use fast param binding with pre-computed hash (skips redundant checks) - env_define_param(call_env, fn->param_names[i], fn->param_hashes[i], arg_value); + int is_const_param = fn->param_is_const && fn->param_is_const[i]; + env_define_param(call_env, fn->param_names[i], fn->param_hashes[i], arg_value, is_const_param); // Release default param value if we created it (not from args array) // For default params or ref params, arg_value was created locally and needs release diff --git a/src/backends/interpreter/runtime/eval_pattern_match.c b/src/backends/interpreter/runtime/eval_pattern_match.c index 452b5699..37c346bf 100644 --- a/src/backends/interpreter/runtime/eval_pattern_match.c +++ b/src/backends/interpreter/runtime/eval_pattern_match.c @@ -91,15 +91,15 @@ int match_pattern(Pattern *pattern, Value value, Environment *env, ExecutionCont Environment *temp_env = env_new(env); int matched = match_pattern(pattern->as.or_pattern.alternatives[i], value, temp_env, ctx); if (matched) { - // Copy bindings from temp_env into the real env + // Transfer bindings from temp_env to the real env for (int j = 0; j < temp_env->count; j++) { VALUE_RETAIN(temp_env->values[j]); env_define(env, temp_env->names[j], temp_env->values[j], 0, ctx); } - env_free(temp_env); + env_release(temp_env); return 1; } - env_free(temp_env); + env_release(temp_env); } return 0; } diff --git a/src/backends/interpreter/runtime/expressions.c b/src/backends/interpreter/runtime/expressions.c index 424503aa..a0eebd58 100644 --- a/src/backends/interpreter/runtime/expressions.c +++ b/src/backends/interpreter/runtime/expressions.c @@ -361,6 +361,7 @@ Value eval_expr(Expr *expr, Environment *env, ExecutionContext *ctx) { bound_fn->param_types = orig_fn->param_types; bound_fn->param_defaults = orig_fn->param_defaults; bound_fn->param_is_ref = orig_fn->param_is_ref; // Share ref flags + bound_fn->param_is_const = orig_fn->param_is_const; // Share const flags bound_fn->param_hashes = orig_fn->param_hashes; // Share pre-computed hashes bound_fn->num_params = orig_fn->num_params; bound_fn->rest_param = orig_fn->rest_param; // Share rest param name @@ -773,6 +774,7 @@ Value eval_expr(Expr *expr, Environment *env, ExecutionContext *ctx) { fn->param_types = expr->as.function.param_types; fn->param_defaults = expr->as.function.param_defaults; fn->param_is_ref = expr->as.function.param_is_ref; + fn->param_is_const = expr->as.function.param_is_const; // Lazily compute and cache param hashes on the AST node. // First closure from this AST node pays the cost; subsequent ones reuse. diff --git a/src/backends/interpreter/runtime/statements.c b/src/backends/interpreter/runtime/statements.c index 4997c24f..032887bf 100644 --- a/src/backends/interpreter/runtime/statements.c +++ b/src/backends/interpreter/runtime/statements.c @@ -749,12 +749,14 @@ void eval_stmt(Stmt *stmt, Environment *env, ExecutionContext *ctx) { Value saved_exception = ctx->exception_state.exception_value; int was_breaking = ctx->loop_state.is_breaking; int was_continuing = ctx->loop_state.is_continuing; + char *saved_target_label = ctx->loop_state.target_label; // Clear states before finally ctx->return_state.is_returning = 0; ctx->exception_state.is_throwing = 0; ctx->loop_state.is_breaking = 0; ctx->loop_state.is_continuing = 0; + ctx->loop_state.target_label = NULL; // Execute finally block eval_stmt(stmt->as.try_stmt.finally_block, env, ctx); @@ -768,6 +770,11 @@ void eval_stmt(Stmt *stmt, Environment *env, ExecutionContext *ctx) { ctx->exception_state.exception_value = saved_exception; ctx->loop_state.is_breaking = was_breaking; ctx->loop_state.is_continuing = was_continuing; + ctx->loop_state.target_label = saved_target_label; + } else { + // Finally introduced its own break/continue/return/throw; + // the saved label is no longer needed + if (saved_target_label) free(saved_target_label); } } break; diff --git a/src/backends/interpreter/values.c b/src/backends/interpreter/values.c index fb8ccbbd..66a81b28 100644 --- a/src/backends/interpreter/values.c +++ b/src/backends/interpreter/values.c @@ -696,6 +696,11 @@ void function_free(Function *fn) { free(fn->param_is_ref); } + // Free param_is_const array + if (fn->param_is_const) { + free(fn->param_is_const); + } + // Free pre-computed param hashes if (fn->param_hashes) { free(fn->param_hashes); diff --git a/src/frontend/lexer/lexer.c b/src/frontend/lexer/lexer.c index 5fae5294..294d0cad 100644 --- a/src/frontend/lexer/lexer.c +++ b/src/frontend/lexer/lexer.c @@ -1064,10 +1064,10 @@ Token lexer_next(Lexer *lex) { advance(lex); // consume third dot return make_token(lex, TOK_DOT_DOT_DOT); } - // Check for float literal without leading zero (e.g., .5) + // Check for float literal without leading zero (e.g., .5, .1_2_3) if (isdigit(peek(lex))) { - // Consume the fractional digits - while (isdigit(peek(lex))) { + // Consume the fractional digits (with underscore separators) + while (isdigit(peek(lex)) || peek(lex) == '_') { advance(lex); } @@ -1089,7 +1089,7 @@ Token lexer_next(Lexer *lex) { if (peek(lex) == '+' || peek(lex) == '-') { advance(lex); // consume sign } - while (isdigit(peek(lex))) { + while (isdigit(peek(lex)) || peek(lex) == '_') { advance(lex); } } @@ -1097,7 +1097,9 @@ Token lexer_next(Lexer *lex) { Token token = make_token(lex, TOK_NUMBER); token.is_float = 1; - token.float_value = strtod(lex->start, NULL); + char *clean = strip_underscores(lex->start, (int)(lex->current - lex->start)); + token.float_value = strtod(clean, NULL); + free(clean); return token; } return make_token(lex, TOK_DOT); diff --git a/tests/parity/language/defer_nested_functions.expected b/tests/parity/language/defer_nested_functions.expected new file mode 100644 index 00000000..20424635 --- /dev/null +++ b/tests/parity/language/defer_nested_functions.expected @@ -0,0 +1,7 @@ +outer before inner +inner body +inner defer 2 +inner defer 1 +outer after inner +outer defer 2 +outer defer 1 diff --git a/tests/parity/language/defer_nested_functions.hml b/tests/parity/language/defer_nested_functions.hml new file mode 100644 index 00000000..1c1ca33c --- /dev/null +++ b/tests/parity/language/defer_nested_functions.hml @@ -0,0 +1,17 @@ +// Test: defer execution scoped to function calls (not global) + +fn inner() { + defer print("inner defer 1"); + defer print("inner defer 2"); + print("inner body"); +} + +fn outer() { + defer print("outer defer 1"); + print("outer before inner"); + inner(); + print("outer after inner"); + defer print("outer defer 2"); +} + +outer(); diff --git a/tests/parity/language/labeled_break_finally.expected b/tests/parity/language/labeled_break_finally.expected new file mode 100644 index 00000000..34267e5a --- /dev/null +++ b/tests/parity/language/labeled_break_finally.expected @@ -0,0 +1,2 @@ +finally ran +0 diff --git a/tests/parity/language/labeled_break_finally.hml b/tests/parity/language/labeled_break_finally.hml new file mode 100644 index 00000000..ff2c0bdc --- /dev/null +++ b/tests/parity/language/labeled_break_finally.hml @@ -0,0 +1,32 @@ +// Test: labeled break/continue inside try-finally preserves target label + +// Labeled break through finally +let result1 = "not set"; +outer: for (let i = 0; i < 3; i++) { + for (let j = 0; j < 3; j++) { + try { + if (j == 1) { + break outer; + } + } finally { + result1 = "finally ran"; + } + } +} +print(result1); + +// Labeled continue through finally +let count = 0; +outer2: for (let i = 0; i < 3; i++) { + for (let j = 0; j < 3; j++) { + try { + if (j == 0) { + continue outer2; + } + count++; + } finally { + // finally should execute on continue too + } + } +} +print(count); diff --git a/tests/parity/language/leading_dot_float_underscore.expected b/tests/parity/language/leading_dot_float_underscore.expected new file mode 100644 index 00000000..90300b2a --- /dev/null +++ b/tests/parity/language/leading_dot_float_underscore.expected @@ -0,0 +1,4 @@ +0.5 +0.123 +0.15 +1000.5 diff --git a/tests/parity/language/leading_dot_float_underscore.hml b/tests/parity/language/leading_dot_float_underscore.hml new file mode 100644 index 00000000..ab8406c9 --- /dev/null +++ b/tests/parity/language/leading_dot_float_underscore.hml @@ -0,0 +1,15 @@ +// Test: leading-dot floats with underscore separators + +let a = .5; +print(a); + +let b = .123; +print(b); + +// Leading dot with underscore separator +let c = .1_5; +print(c); + +// Regular float with underscores (for comparison) +let d = 1_000.5; +print(d); diff --git a/tests/parity/language/or_pattern_binding.expected b/tests/parity/language/or_pattern_binding.expected new file mode 100644 index 00000000..e930b3be --- /dev/null +++ b/tests/parity/language/or_pattern_binding.expected @@ -0,0 +1,3 @@ +number: 42 +small +medium: 15 diff --git a/tests/parity/language/or_pattern_binding.hml b/tests/parity/language/or_pattern_binding.hml new file mode 100644 index 00000000..293ee616 --- /dev/null +++ b/tests/parity/language/or_pattern_binding.hml @@ -0,0 +1,23 @@ +// Test: OR patterns with variable bindings + +// Simple OR pattern with binding +let val1 = match (42) { + n: i32 | n: f64 => "number: " + n, + _ => "other" +}; +print(val1); + +// OR pattern with literal alternatives (no bindings) +let val2 = match (2) { + 1 | 2 | 3 => "small", + _ => "big" +}; +print(val2); + +// OR pattern binding with guard +let val3 = match (15) { + n if n < 10 => "small", + n if n < 20 => "medium: " + n, + n => "large" +}; +print(val3);