From afd00b93f01ca4a2699fc439e78f977f68d02c78 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 3 Jan 2026 22:21:12 +0000 Subject: [PATCH] Implement tuples with (i32, string) syntax Add first-class tuple support across interpreter and compiler: - AST: Add TYPE_TUPLE and EXPR_TUPLE_LITERAL with element_types array - Parser: Parse tuple type syntax (type1, type2) and literal syntax (val1, val2) - Parser: Handle .0, .1 numeric property access for tuple indexing - Interpreter: Add VAL_TUPLE, Tuple struct, and tuple operations - Interpreter: Support tuple type coercion for function params/returns - Compiler: Add CHECKED_TUPLE, code generation for tuple literals and access - Runtime: Add HML_VAL_TUPLE, HmlTuple struct, and hml_tuple_get - Bundler: Handle EXPR_TUPLE_LITERAL in dependency collection - Parity test: tests/parity/language/tuples.hml covering all tuple features Usage: let pair = (42, "answer"); let typed: (i32, string) = (100, "hello"); print(pair.0); // 42 print(pair[1]); // answer print(pair.length); // 2 --- include/ast.h | 9 ++ include/interpreter.h | 17 +++ runtime/include/hemlock_value.h | 18 ++- runtime/src/value.c | 61 ++++++++ src/backends/compiler/codegen_expr.c | 54 ++++++- src/backends/compiler/type_check.c | 47 ++++++ src/backends/compiler/type_check.h | 1 + src/backends/interpreter/builtins/debugging.c | 3 + .../interpreter/runtime/expressions.c | 80 +++++++++- src/backends/interpreter/types.c | 35 +++++ src/backends/interpreter/values.c | 144 +++++++++++++++++- src/bundler/bundler.c | 6 + src/frontend/ast.c | 39 +++++ src/frontend/parser/expressions.c | 99 +++++++++++- tests/parity/language/tuples.expected | 27 ++++ tests/parity/language/tuples.hml | 65 ++++++++ 16 files changed, 694 insertions(+), 11 deletions(-) create mode 100644 tests/parity/language/tuples.expected create mode 100644 tests/parity/language/tuples.hml diff --git a/include/ast.h b/include/ast.h index 26f92cd1..0ea83370 100644 --- a/include/ast.h +++ b/include/ast.h @@ -29,6 +29,7 @@ typedef enum { EXPR_FUNCTION, EXPR_ARRAY_LITERAL, EXPR_OBJECT_LITERAL, + EXPR_TUPLE_LITERAL, // Tuple literal: (expr1, expr2, ...) EXPR_PREFIX_INC, EXPR_PREFIX_DEC, EXPR_POSTFIX_INC, @@ -156,6 +157,10 @@ struct Expr { Expr **field_values; int num_fields; } object_literal; + struct { + Expr **elements; + int num_elements; + } tuple_literal; struct { Expr *operand; } prefix_inc; @@ -211,6 +216,7 @@ typedef enum { TYPE_PTR, TYPE_BUFFER, TYPE_ARRAY, // Typed array (e.g., array) + TYPE_TUPLE, // Tuple type (e.g., (i32, string)) TYPE_NULL, TYPE_INFER, // No annotation, infer from value TYPE_CUSTOM_OBJECT, // Custom object type (Person, User, etc.) @@ -223,6 +229,8 @@ struct Type { TypeKind kind; char *type_name; // For TYPE_CUSTOM_OBJECT (e.g., "Person") struct Type *element_type; // For TYPE_ARRAY (element type) + struct Type **element_types; // For TYPE_TUPLE (array of element types) + int num_element_types; // For TYPE_TUPLE (number of elements) int nullable; // If true, type allows null (e.g., string?) }; @@ -382,6 +390,7 @@ Expr* expr_index_assign(Expr *object, Expr *index, Expr *value); Expr* expr_function(int is_async, char **param_names, Type **param_types, Expr **param_defaults, int *param_is_ref, int num_params, char *rest_param, Type *rest_param_type, Type *return_type, Stmt *body); Expr* expr_array_literal(Expr **elements, int num_elements); Expr* expr_object_literal(char **field_names, Expr **field_values, int num_fields); +Expr* expr_tuple_literal(Expr **elements, int num_elements); Expr* expr_prefix_inc(Expr *operand); Expr* expr_prefix_dec(Expr *operand); Expr* expr_postfix_inc(Expr *operand); diff --git a/include/interpreter.h b/include/interpreter.h index e3d9d2e0..3a9bec52 100644 --- a/include/interpreter.h +++ b/include/interpreter.h @@ -25,6 +25,7 @@ typedef enum { VAL_PTR, VAL_BUFFER, VAL_ARRAY, // Dynamic array + VAL_TUPLE, // Fixed-size heterogeneous tuple VAL_OBJECT, // JavaScript-style object VAL_FILE, // File handle VAL_SOCKET, // Socket handle @@ -72,6 +73,13 @@ typedef struct { _Atomic int freed; // Atomic flag: 1 if freed via free(), 0 otherwise } Array; +// Tuple struct (fixed-size heterogeneous container) +typedef struct { + Value *elements; // Array of values + int length; // Number of elements (fixed at creation) + int ref_count; // Reference count for memory management +} Tuple; + // File handle struct typedef struct { FILE *fp; // C file pointer @@ -227,6 +235,7 @@ typedef struct Value { void *as_ptr; Buffer *as_buffer; Array *as_array; + Tuple *as_tuple; FileHandle *as_file; SocketHandle *as_socket; WebSocketHandle *as_websocket; @@ -329,6 +338,14 @@ Value array_pop(Array *arr); Value array_get(Array *arr, int index, ExecutionContext *ctx); void array_set(Array *arr, int index, Value val, ExecutionContext *ctx); +// Tuple operations +Tuple* tuple_new(int length); +void tuple_free(Tuple *tuple); +void tuple_retain(Tuple *tuple); +void tuple_release(Tuple *tuple); +Value tuple_get(Tuple *tuple, int index, ExecutionContext *ctx); +Value val_tuple(Tuple *tuple); + // File operations void file_free(FileHandle *file); diff --git a/runtime/include/hemlock_value.h b/runtime/include/hemlock_value.h index 201b3deb..6b0cd026 100644 --- a/runtime/include/hemlock_value.h +++ b/runtime/include/hemlock_value.h @@ -15,6 +15,7 @@ // Forward declarations for heap-allocated types typedef struct HmlString HmlString; typedef struct HmlArray HmlArray; +typedef struct HmlTuple HmlTuple; typedef struct HmlObject HmlObject; typedef struct HmlBuffer HmlBuffer; typedef struct HmlFunction HmlFunction; @@ -48,6 +49,7 @@ typedef enum { HML_VAL_PTR, HML_VAL_BUFFER, HML_VAL_ARRAY, + HML_VAL_TUPLE, HML_VAL_OBJECT, HML_VAL_FILE, HML_VAL_FUNCTION, @@ -82,6 +84,7 @@ typedef struct HmlValue { void *as_ptr; HmlBuffer *as_buffer; HmlArray *as_array; + HmlTuple *as_tuple; HmlObject *as_object; HmlFileHandle *as_file; HmlFunction *as_function; @@ -120,6 +123,13 @@ struct HmlArray { _Atomic int freed; // Atomic flag: 1 if freed via free(), 0 otherwise }; +// Tuple struct (fixed-size heterogeneous container) +struct HmlTuple { + HmlValue *elements; + int length; // Fixed at creation + int ref_count; +}; + // Object struct (JavaScript-style) struct HmlObject { char *type_name; // NULL for anonymous @@ -232,6 +242,8 @@ HmlValue hml_val_rune(uint32_t codepoint); HmlValue hml_val_ptr(void *ptr); HmlValue hml_val_buffer(int size); HmlValue hml_val_array(void); +HmlValue hml_val_tuple(int length); +HmlValue hml_tuple_get(HmlValue tuple, int index); HmlValue hml_val_object(void); HmlValue hml_val_null(void); HmlValue hml_val_function(void *fn_ptr, int num_params, int num_required, int is_async); @@ -459,9 +471,9 @@ static inline HmlValue hml_i64_rshift(HmlValue left, HmlValue right) { // Defined early so it can be used by array_get_i32_fast static inline int hml_needs_refcount(HmlValue val) { return val.type == HML_VAL_STRING || val.type == HML_VAL_BUFFER || - val.type == HML_VAL_ARRAY || val.type == HML_VAL_OBJECT || - val.type == HML_VAL_FUNCTION || val.type == HML_VAL_TASK || - val.type == HML_VAL_CHANNEL; + val.type == HML_VAL_ARRAY || val.type == HML_VAL_TUPLE || + val.type == HML_VAL_OBJECT || val.type == HML_VAL_FUNCTION || + val.type == HML_VAL_TASK || val.type == HML_VAL_CHANNEL; } // Fast path: array[i32] access (bounds checked, skip retain for primitives) diff --git a/runtime/src/value.c b/runtime/src/value.c index 900be565..aa0fa6b5 100644 --- a/runtime/src/value.c +++ b/runtime/src/value.c @@ -206,6 +206,43 @@ HmlValue hml_val_array(void) { return v; } +HmlValue hml_val_tuple(int length) { + if (length < 2) { + hml_runtime_error("Tuple must have at least 2 elements"); + } + + HmlValue v; + v.type = HML_VAL_TUPLE; + + HmlTuple *t = malloc(sizeof(HmlTuple)); + t->elements = malloc(sizeof(HmlValue) * length); + t->length = length; + t->ref_count = 1; + + // Initialize all elements to null + for (int i = 0; i < length; i++) { + t->elements[i] = hml_val_null(); + } + + v.as.as_tuple = t; + return v; +} + +HmlValue hml_tuple_get(HmlValue tuple, int index) { + if (tuple.type != HML_VAL_TUPLE) { + hml_runtime_error("Cannot get element from non-tuple"); + } + HmlTuple *t = tuple.as.as_tuple; + if (index < 0 || index >= t->length) { + hml_runtime_error("Tuple index %d out of bounds (length %d)", index, t->length); + } + HmlValue result = t->elements[index]; + if (hml_needs_refcount(result)) { + hml_retain(&result); + } + return result; +} + HmlValue hml_val_object(void) { HmlValue v; v.type = HML_VAL_OBJECT; @@ -356,6 +393,9 @@ void hml_retain(HmlValue *val) { case HML_VAL_ARRAY: if (val->as.as_array) val->as.as_array->ref_count++; break; + case HML_VAL_TUPLE: + if (val->as.as_tuple) val->as.as_tuple->ref_count++; + break; case HML_VAL_OBJECT: if (val->as.as_object) val->as.as_object->ref_count++; break; @@ -398,6 +438,17 @@ static void array_free(HmlArray *arr) { } } +static void tuple_free(HmlTuple *tuple) { + if (tuple) { + // Release all elements + for (int i = 0; i < tuple->length; i++) { + hml_release(&tuple->elements[i]); + } + free(tuple->elements); + free(tuple); + } +} + static void object_free(HmlObject *obj) { if (obj) { // Free field names and release field values @@ -452,6 +503,15 @@ void hml_release(HmlValue *val) { val->as.as_array = NULL; } break; + case HML_VAL_TUPLE: + if (val->as.as_tuple) { + val->as.as_tuple->ref_count--; + if (val->as.as_tuple->ref_count <= 0) { + tuple_free(val->as.as_tuple); + } + val->as.as_tuple = NULL; + } + break; case HML_VAL_OBJECT: if (val->as.as_object) { val->as.as_object->ref_count--; @@ -704,6 +764,7 @@ const char* hml_type_name(HmlValueType type) { case HML_VAL_PTR: return "ptr"; case HML_VAL_BUFFER: return "buffer"; case HML_VAL_ARRAY: return "array"; + case HML_VAL_TUPLE: return "tuple"; case HML_VAL_OBJECT: return "object"; case HML_VAL_FILE: return "file"; case HML_VAL_FUNCTION: return "function"; diff --git a/src/backends/compiler/codegen_expr.c b/src/backends/compiler/codegen_expr.c index 5f748e2e..c485a7d6 100644 --- a/src/backends/compiler/codegen_expr.c +++ b/src/backends/compiler/codegen_expr.c @@ -1085,6 +1085,10 @@ char* codegen_expr(CodegenContext *ctx, Expr *expr) { codegen_indent_inc(ctx); codegen_writeln(ctx, "%s = hml_array_length(%s);", result, obj); codegen_indent_dec(ctx); + codegen_writeln(ctx, "} else if (%s.type == HML_VAL_TUPLE) {", obj); + codegen_indent_inc(ctx); + codegen_writeln(ctx, "%s = hml_val_i32(%s.as.as_tuple->length);", result, obj); + codegen_indent_dec(ctx); codegen_writeln(ctx, "} else if (%s.type == HML_VAL_STRING) {", obj); codegen_indent_inc(ctx); codegen_writeln(ctx, "%s = hml_string_length(%s);", result, obj); @@ -1168,9 +1172,36 @@ char* codegen_expr(CodegenContext *ctx, Expr *expr) { codegen_indent_dec(ctx); codegen_writeln(ctx, "}"); } else { - // Regular property access - throws error if field not found (parity with interpreter) - codegen_writeln(ctx, "HmlValue %s = hml_object_get_field_required(%s, \"%s\");", - result, obj, expr->as.get_property.property); + // Check if property is a numeric index (for tuples: .0, .1, .2, etc.) + const char *prop = expr->as.get_property.property; + int is_numeric = (prop[0] >= '0' && prop[0] <= '9'); + if (is_numeric) { + // Check that entire property is numeric + for (const char *p = prop; *p; p++) { + if (*p < '0' || *p > '9') { + is_numeric = 0; + break; + } + } + } + + if (is_numeric) { + int index = atoi(prop); + codegen_writeln(ctx, "HmlValue %s;", result); + codegen_writeln(ctx, "if (%s.type == HML_VAL_TUPLE) {", obj); + codegen_indent_inc(ctx); + codegen_writeln(ctx, "%s = hml_tuple_get(%s, %d);", result, obj, index); + codegen_indent_dec(ctx); + codegen_writeln(ctx, "} else {"); + codegen_indent_inc(ctx); + codegen_writeln(ctx, "hml_runtime_error(\"Cannot access .%s on non-tuple type\");", prop); + codegen_indent_dec(ctx); + codegen_writeln(ctx, "}"); + } else { + // Regular property access - throws error if field not found (parity with interpreter) + codegen_writeln(ctx, "HmlValue %s = hml_object_get_field_required(%s, \"%s\");", + result, obj, expr->as.get_property.property); + } } codegen_writeln(ctx, "hml_release(&%s);", obj); free(obj); @@ -1237,6 +1268,10 @@ char* codegen_expr(CodegenContext *ctx, Expr *expr) { codegen_indent_inc(ctx); codegen_writeln(ctx, "%s = hml_array_get(%s, %s);", result, obj, idx); codegen_indent_dec(ctx); + codegen_writeln(ctx, "} else if (%s.type == HML_VAL_TUPLE) {", obj); + codegen_indent_inc(ctx); + codegen_writeln(ctx, "%s = hml_tuple_get(%s, hml_to_i32(%s));", result, obj, idx); + codegen_indent_dec(ctx); codegen_writeln(ctx, "} else if (%s.type == HML_VAL_STRING) {", obj); codegen_indent_inc(ctx); codegen_writeln(ctx, "%s = hml_string_index(%s, %s);", result, obj, idx); @@ -1352,6 +1387,19 @@ char* codegen_expr(CodegenContext *ctx, Expr *expr) { break; } + case EXPR_TUPLE_LITERAL: { + int num_elements = expr->as.tuple_literal.num_elements; + codegen_writeln(ctx, "HmlValue %s = hml_val_tuple(%d);", result, num_elements); + for (int i = 0; i < num_elements; i++) { + char *elem = codegen_expr(ctx, expr->as.tuple_literal.elements[i]); + codegen_writeln(ctx, "%s.as.as_tuple->elements[%d] = %s;", result, i, elem); + codegen_writeln(ctx, "hml_retain(&%s.as.as_tuple->elements[%d]);", result, i); + codegen_writeln(ctx, "hml_release(&%s);", elem); + free(elem); + } + break; + } + case EXPR_OBJECT_LITERAL: { codegen_writeln(ctx, "HmlValue %s = hml_val_object();", result); for (int i = 0; i < expr->as.object_literal.num_fields; i++) { diff --git a/src/backends/compiler/type_check.c b/src/backends/compiler/type_check.c index 29a5c023..b9e91b71 100644 --- a/src/backends/compiler/type_check.c +++ b/src/backends/compiler/type_check.c @@ -126,6 +126,17 @@ CheckedType* checked_type_from_ast(Type *ast_type) { type->element_type = checked_type_from_ast(ast_type->element_type); } break; + case TYPE_TUPLE: + type->kind = CHECKED_TUPLE; + // Store element types using param_types field + if (ast_type->num_element_types > 0) { + type->num_params = ast_type->num_element_types; + type->param_types = calloc(type->num_params, sizeof(CheckedType*)); + for (int i = 0; i < type->num_params; i++) { + type->param_types[i] = checked_type_from_ast(ast_type->element_types[i]); + } + } + break; case TYPE_CUSTOM_OBJECT: type->kind = CHECKED_CUSTOM; if (ast_type->type_name) { @@ -172,6 +183,7 @@ const char* checked_type_kind_name(CheckedTypeKind kind) { case CHECKED_PTR: return "ptr"; case CHECKED_BUFFER: return "buffer"; case CHECKED_ARRAY: return "array"; + case CHECKED_TUPLE: return "tuple"; case CHECKED_OBJECT: return "object"; case CHECKED_CUSTOM: return "object"; case CHECKED_FUNCTION: return "function"; @@ -1015,6 +1027,19 @@ CheckedType* type_check_infer_expr(TypeCheckContext *ctx, Expr *expr) { return checked_type_array(elem); } + case EXPR_TUPLE_LITERAL: { + // Create tuple type with inferred element types + int num = expr->as.tuple_literal.num_elements; + CheckedType *type = calloc(1, sizeof(CheckedType)); + type->kind = CHECKED_TUPLE; + type->num_params = num; + type->param_types = calloc(num, sizeof(CheckedType*)); + for (int i = 0; i < num; i++) { + type->param_types[i] = type_check_infer_expr(ctx, expr->as.tuple_literal.elements[i]); + } + return type; + } + case EXPR_OBJECT_LITERAL: return checked_type_primitive(CHECKED_OBJECT); @@ -1590,6 +1615,12 @@ void type_check_expr(TypeCheckContext *ctx, Expr *expr) { } break; + case EXPR_TUPLE_LITERAL: + for (int i = 0; i < expr->as.tuple_literal.num_elements; i++) { + type_check_expr(ctx, expr->as.tuple_literal.elements[i]); + } + break; + case EXPR_OBJECT_LITERAL: for (int i = 0; i < expr->as.object_literal.num_fields; i++) { type_check_expr(ctx, expr->as.object_literal.field_values[i]); @@ -2246,6 +2277,16 @@ static int variable_escapes_in_expr_internal(Expr *expr, const char *var_name) { } return 0; + case EXPR_TUPLE_LITERAL: + for (int i = 0; i < expr->as.tuple_literal.num_elements; i++) { + Expr *elem = expr->as.tuple_literal.elements[i]; + if (elem->type == EXPR_IDENT && strcmp(elem->as.ident.name, var_name) == 0) { + return 1; + } + if (variable_escapes_in_expr_internal(elem, var_name)) return 1; + } + return 0; + case EXPR_OBJECT_LITERAL: for (int i = 0; i < expr->as.object_literal.num_fields; i++) { Expr *val = expr->as.object_literal.field_values[i]; @@ -2748,6 +2789,12 @@ static int contains_recursive_call(Expr *expr, const char *func_name) { } return 0; + case EXPR_TUPLE_LITERAL: + for (int i = 0; i < expr->as.tuple_literal.num_elements; i++) { + if (contains_recursive_call(expr->as.tuple_literal.elements[i], func_name)) return 1; + } + return 0; + case EXPR_OBJECT_LITERAL: for (int i = 0; i < expr->as.object_literal.num_fields; i++) { if (contains_recursive_call(expr->as.object_literal.field_values[i], func_name)) return 1; diff --git a/src/backends/compiler/type_check.h b/src/backends/compiler/type_check.h index ad082999..29324f9a 100644 --- a/src/backends/compiler/type_check.h +++ b/src/backends/compiler/type_check.h @@ -33,6 +33,7 @@ typedef enum { CHECKED_PTR, CHECKED_BUFFER, CHECKED_ARRAY, // Array with optional element type + CHECKED_TUPLE, // Tuple with element types CHECKED_OBJECT, // Generic object CHECKED_CUSTOM, // Custom object type (Person, User, etc.) CHECKED_FUNCTION, // Function type diff --git a/src/backends/interpreter/builtins/debugging.c b/src/backends/interpreter/builtins/debugging.c index d7930c07..22c350a6 100644 --- a/src/backends/interpreter/builtins/debugging.c +++ b/src/backends/interpreter/builtins/debugging.c @@ -57,6 +57,9 @@ Value builtin_typeof(Value *args, int num_args, ExecutionContext *ctx) { case VAL_ARRAY: type_name = "array"; break; + case VAL_TUPLE: + type_name = "tuple"; + break; case VAL_FILE: type_name = "file"; break; diff --git a/src/backends/interpreter/runtime/expressions.c b/src/backends/interpreter/runtime/expressions.c index b18d369d..268261c3 100644 --- a/src/backends/interpreter/runtime/expressions.c +++ b/src/backends/interpreter/runtime/expressions.c @@ -810,6 +810,26 @@ Value eval_expr(Expr *expr, Environment *env, ExecutionContext *ctx) { } else { runtime_error(ctx, "Array has no property '%s'", property); } + } else if (object.type == VAL_TUPLE) { + // Tuple element access via numeric property (e.g., tuple.0, tuple.1) + Tuple *tuple = object.as.as_tuple; + if (strcmp(property, "length") == 0) { + result = val_i32(tuple->length); + } else { + // Try to parse as numeric index + char *endptr; + long index = strtol(property, &endptr, 10); + if (*endptr == '\0' && property[0] >= '0' && property[0] <= '9') { + // Valid numeric property + if (index < 0 || index >= tuple->length) { + runtime_error(ctx, "Tuple index %ld out of bounds (length %d)", index, tuple->length); + } + result = tuple->elements[index]; + VALUE_RETAIN(result); + } else { + runtime_error(ctx, "Tuple has no property '%s' (use .0, .1, etc. for element access)", property); + } + } } else if (object.type == VAL_OBJECT) { // Look up field in object using hash table Object *obj = object.as.as_object; @@ -884,6 +904,19 @@ Value eval_expr(Expr *expr, Environment *env, ExecutionContext *ctx) { // Fall through to normal path for bounds error } + // FAST PATH: tuple[i32] + if (object.type == VAL_TUPLE && index_val.type == VAL_I32) { + Tuple *tuple = object.as.as_tuple; + int32_t index = index_val.as.as_i32; + if (index >= 0 && index < tuple->length) { + result = tuple->elements[index]; + VALUE_RETAIN(result); + VALUE_RELEASE(object); + return result; + } + // Fall through to normal path for bounds error + } + // Object property access with string key if (object.type == VAL_OBJECT && index_val.type == VAL_STRING) { Object *obj = object.as.as_object; @@ -946,6 +979,11 @@ Value eval_expr(Expr *expr, Environment *env, ExecutionContext *ctx) { result = array_get(object.as.as_array, index, ctx); // Retain the element so it survives array release VALUE_RETAIN(result); + } else if (object.type == VAL_TUPLE) { + // Tuple indexing + result = tuple_get(object.as.as_tuple, index, ctx); + // Retain the element so it survives tuple release + VALUE_RETAIN(result); } else if (object.type == VAL_PTR) { // Raw pointer indexing - no bounds checking (unsafe!) void *ptr = object.as.as_ptr; @@ -958,7 +996,7 @@ Value eval_expr(Expr *expr, Environment *env, ExecutionContext *ctx) { // Return the byte as u8 result = val_u8(((unsigned char *)ptr)[index]); } else { - runtime_error(ctx, "Only strings, buffers, arrays, pointers, and objects can be indexed"); + runtime_error(ctx, "Only strings, buffers, arrays, tuples, pointers, and objects can be indexed"); VALUE_RELEASE(object); VALUE_RELEASE(index_val); return val_null(); @@ -1183,6 +1221,19 @@ Value eval_expr(Expr *expr, Environment *env, ExecutionContext *ctx) { fn->param_types[i]->element_type->type_name = strdup(expr->as.function.param_types[i]->element_type->type_name); } } + // Copy element_types for tuples + if (expr->as.function.param_types[i]->num_element_types > 0) { + fn->param_types[i]->num_element_types = expr->as.function.param_types[i]->num_element_types; + fn->param_types[i]->element_types = malloc(sizeof(Type*) * fn->param_types[i]->num_element_types); + for (int ti = 0; ti < fn->param_types[i]->num_element_types; ti++) { + Type *src_elem = expr->as.function.param_types[i]->element_types[ti]; + fn->param_types[i]->element_types[ti] = type_new(src_elem->kind); + fn->param_types[i]->element_types[ti]->nullable = src_elem->nullable; + if (src_elem->type_name) { + fn->param_types[i]->element_types[ti]->type_name = strdup(src_elem->type_name); + } + } + } } else { fn->param_types[i] = NULL; } @@ -1255,6 +1306,19 @@ Value eval_expr(Expr *expr, Environment *env, ExecutionContext *ctx) { fn->return_type->element_type->type_name = strdup(expr->as.function.return_type->element_type->type_name); } } + // Copy element_types for tuples + if (expr->as.function.return_type->num_element_types > 0) { + fn->return_type->num_element_types = expr->as.function.return_type->num_element_types; + fn->return_type->element_types = malloc(sizeof(Type*) * fn->return_type->num_element_types); + for (int ti = 0; ti < fn->return_type->num_element_types; ti++) { + Type *src_elem = expr->as.function.return_type->element_types[ti]; + fn->return_type->element_types[ti] = type_new(src_elem->kind); + fn->return_type->element_types[ti]->nullable = src_elem->nullable; + if (src_elem->type_name) { + fn->return_type->element_types[ti]->type_name = strdup(src_elem->type_name); + } + } + } } else { fn->return_type = NULL; } @@ -1288,6 +1352,20 @@ Value eval_expr(Expr *expr, Environment *env, ExecutionContext *ctx) { return val_array(arr); } + case EXPR_TUPLE_LITERAL: { + // Create tuple and evaluate elements + int num_elements = expr->as.tuple_literal.num_elements; + Tuple *tuple = tuple_new(num_elements); + + for (int i = 0; i < num_elements; i++) { + Value element = eval_expr(expr->as.tuple_literal.elements[i], env, ctx); + VALUE_RETAIN(element); + tuple->elements[i] = element; + } + + return val_tuple(tuple); + } + case EXPR_OBJECT_LITERAL: { // Create anonymous object Object *obj = object_new(NULL, expr->as.object_literal.num_fields); diff --git a/src/backends/interpreter/types.c b/src/backends/interpreter/types.c index 55f270ca..f26e3d63 100644 --- a/src/backends/interpreter/types.c +++ b/src/backends/interpreter/types.c @@ -574,6 +574,35 @@ Value convert_to_type(Value value, Type *target_type, Environment *env, Executio return value; } + // Handle tuples + if (kind == TYPE_TUPLE) { + if (value.type != VAL_TUPLE) { + fprintf(stderr, "Runtime error: Expected tuple, got non-tuple\n"); + exit(1); + } + + Tuple *tuple = value.as.as_tuple; + + // Check length matches + if (target_type->num_element_types != tuple->length) { + fprintf(stderr, "Runtime error: Tuple length mismatch: expected %d, got %d\n", + target_type->num_element_types, tuple->length); + exit(1); + } + + // Coerce each element to its target type + for (int i = 0; i < tuple->length; i++) { + Value elem = tuple->elements[i]; + Value coerced = convert_to_type(elem, target_type->element_types[i], env, ctx); + // Release old value and retain new + VALUE_RELEASE(tuple->elements[i]); + VALUE_RETAIN(coerced); + tuple->elements[i] = coerced; + } + + return value; + } + // Original function continues with TypeKind TypeKind target_kind = kind; // Get the source value as the widest type for range checking @@ -808,6 +837,12 @@ Value convert_to_type(Value value, Type *target_type, Environment *env, Executio fprintf(stderr, "Runtime error: Cannot convert to void type\n"); exit(1); + case TYPE_TUPLE: + // Tuple type is already handled earlier in this function + // If we reach here, something went wrong + fprintf(stderr, "Runtime error: Tuple type should be handled earlier\n"); + exit(1); + case TYPE_CUSTOM_OBJECT: case TYPE_GENERIC_OBJECT: // These should have been handled above in the early return diff --git a/src/backends/interpreter/values.c b/src/backends/interpreter/values.c index be591995..1eb17869 100644 --- a/src/backends/interpreter/values.c +++ b/src/backends/interpreter/values.c @@ -428,6 +428,72 @@ Value val_array(Array *arr) { return v; } +// ========== TUPLE OPERATIONS ========== + +Tuple* tuple_new(int length) { + if (length < 2) { + fprintf(stderr, "Runtime error: Tuple must have at least 2 elements\n"); + exit(1); + } + Tuple *tuple = malloc(sizeof(Tuple)); + if (!tuple) { + fprintf(stderr, "Runtime error: Memory allocation failed\n"); + exit(1); + } + tuple->length = length; + tuple->ref_count = 1; // Start with 1 - caller owns the first reference + tuple->elements = malloc(sizeof(Value) * length); + if (!tuple->elements) { + free(tuple); + fprintf(stderr, "Runtime error: Memory allocation failed\n"); + exit(1); + } + // Initialize all elements to null + for (int i = 0; i < length; i++) { + tuple->elements[i] = val_null(); + } + return tuple; +} + +void tuple_free(Tuple *tuple) { + if (!tuple) return; + + // Release each element (decrements ref_counts) + for (int i = 0; i < tuple->length; i++) { + value_release(tuple->elements[i]); + } + free(tuple->elements); + free(tuple); +} + +void tuple_retain(Tuple *tuple) { + if (tuple) { + __atomic_add_fetch(&tuple->ref_count, 1, __ATOMIC_SEQ_CST); + } +} + +void tuple_release(Tuple *tuple) { + if (!tuple) return; + int old_count = __atomic_sub_fetch(&tuple->ref_count, 1, __ATOMIC_SEQ_CST); + if (old_count == 0) { + tuple_free(tuple); + } +} + +Value tuple_get(Tuple *tuple, int index, ExecutionContext *ctx) { + if (index < 0 || index >= tuple->length) { + runtime_error(ctx, "Tuple index %d out of bounds (length %d)", index, tuple->length); + } + return tuple->elements[index]; +} + +Value val_tuple(Tuple *tuple) { + Value v = {0}; // Zero-initialize entire struct + v.type = VAL_TUPLE; + v.as.as_tuple = tuple; + return v; +} + // ========== FILE OPERATIONS ========== void file_free(FileHandle *file) { @@ -1033,6 +1099,16 @@ void print_value(Value val) { printf("]"); break; } + case VAL_TUPLE: { + Tuple *tuple = val.as.as_tuple; + printf("("); + for (int i = 0; i < tuple->length; i++) { + if (i > 0) printf(", "); + print_value(tuple->elements[i]); + } + printf(")"); + break; + } case VAL_FILE: { FileHandle *file = val.as.as_file; if (file->closed) { @@ -1206,6 +1282,38 @@ char* value_to_string(Value val) { free(part_lens); return result; } + case VAL_TUPLE: { + // Tuple representation with parentheses + Tuple *tuple = val.as.as_tuple; + size_t total_len = 2; // ( and ) + char **parts = malloc(sizeof(char*) * tuple->length); + size_t *part_lens = malloc(sizeof(size_t) * tuple->length); + for (int i = 0; i < tuple->length; i++) { + parts[i] = value_to_string(tuple->elements[i]); + part_lens[i] = strlen(parts[i]); + total_len += part_lens[i]; + if (i > 0) total_len += 2; // ", " + } + + // SECURITY: Use memcpy with explicit position tracking instead of strcat + char *result = malloc(total_len + 1); + size_t pos = 0; + result[pos++] = '('; + for (int i = 0; i < tuple->length; i++) { + if (i > 0) { + memcpy(result + pos, ", ", 2); + pos += 2; + } + memcpy(result + pos, parts[i], part_lens[i]); + pos += part_lens[i]; + free(parts[i]); + } + result[pos++] = ')'; + result[pos] = '\0'; + free(parts); + free(part_lens); + return result; + } case VAL_FILE: { FileHandle *file = val.as.as_file; if (file->closed) { @@ -1416,6 +1524,11 @@ static void value_free_internal(Value val, VisitedSet *visited) { array_free_internal(val.as.as_array, visited); } break; + case VAL_TUPLE: + if (val.as.as_tuple) { + tuple_free(val.as.as_tuple); + } + break; case VAL_FILE: if (val.as.as_file) { file_free(val.as.as_file); @@ -1501,8 +1614,8 @@ void value_free(Value val) { static inline int value_needs_refcount(ValueType type) { // Only heap-allocated types need refcounting return type == VAL_STRING || type == VAL_BUFFER || type == VAL_ARRAY || - type == VAL_OBJECT || type == VAL_FUNCTION || type == VAL_TASK || - type == VAL_CHANNEL || type == VAL_REF; + type == VAL_TUPLE || type == VAL_OBJECT || type == VAL_FUNCTION || + type == VAL_TASK || type == VAL_CHANNEL || type == VAL_REF; } // Public API - increment reference count for heap-allocated values @@ -1526,6 +1639,11 @@ void value_retain(Value val) { array_retain(val.as.as_array); } break; + case VAL_TUPLE: + if (val.as.as_tuple) { + tuple_retain(val.as.as_tuple); + } + break; case VAL_OBJECT: if (val.as.as_object) { object_retain(val.as.as_object); @@ -1578,6 +1696,11 @@ void value_release(Value val) { array_release(val.as.as_array); } break; + case VAL_TUPLE: + if (val.as.as_tuple) { + tuple_release(val.as.as_tuple); + } + break; case VAL_OBJECT: if (val.as.as_object) { object_release(val.as.as_object); @@ -1679,6 +1802,23 @@ Value value_deep_copy(Value val) { } break; + case VAL_TUPLE: + if (val.as.as_tuple) { + Tuple *src = val.as.as_tuple; + Tuple *dst = tuple_new(src->length); + // Deep copy each element + for (int i = 0; i < src->length; i++) { + Value elem_copy = value_deep_copy(src->elements[i]); + value_retain(elem_copy); + dst->elements[i] = elem_copy; + } + result.type = VAL_TUPLE; + result.as.as_tuple = dst; + } else { + result = val_null(); + } + break; + case VAL_OBJECT: if (val.as.as_object) { Object *src = val.as.as_object; diff --git a/src/bundler/bundler.c b/src/bundler/bundler.c index a5e58c69..3bfc7843 100644 --- a/src/bundler/bundler.c +++ b/src/bundler/bundler.c @@ -332,6 +332,12 @@ static void collect_expr_deps(Expr *expr, Symbol *sym) { } break; + case EXPR_TUPLE_LITERAL: + for (int i = 0; i < expr->as.tuple_literal.num_elements; i++) { + collect_expr_deps(expr->as.tuple_literal.elements[i], sym); + } + break; + case EXPR_PREFIX_INC: case EXPR_PREFIX_DEC: collect_expr_deps(expr->as.prefix_inc.operand, sym); diff --git a/src/frontend/ast.c b/src/frontend/ast.c index f6f61630..c7ede172 100644 --- a/src/frontend/ast.c +++ b/src/frontend/ast.c @@ -212,6 +212,16 @@ Expr* expr_object_literal(char **field_names, Expr **field_values, int num_field return expr; } +Expr* expr_tuple_literal(Expr **elements, int num_elements) { + Expr *expr = malloc(sizeof(Expr)); + expr->type = EXPR_TUPLE_LITERAL; + expr->line = 0; + expr->column = 0; + expr->as.tuple_literal.elements = elements; + expr->as.tuple_literal.num_elements = num_elements; + return expr; +} + Expr* expr_prefix_inc(Expr *operand) { Expr *expr = malloc(sizeof(Expr)); expr->type = EXPR_PREFIX_INC; @@ -330,6 +340,8 @@ Type* type_new(TypeKind kind) { type->kind = kind; type->type_name = NULL; type->element_type = NULL; + type->element_types = NULL; + type->num_element_types = 0; type->nullable = 0; return type; } @@ -342,6 +354,15 @@ void type_free(Type *type) { if (type->element_type) { type_free(type->element_type); } + // Free tuple element types + if (type->element_types) { + for (int i = 0; i < type->num_element_types; i++) { + if (type->element_types[i]) { + type_free(type->element_types[i]); + } + } + free(type->element_types); + } free(type); } } @@ -772,6 +793,17 @@ Expr* expr_clone(const Expr *expr) { ); } + case EXPR_TUPLE_LITERAL: { + Expr **elements_copy = malloc(sizeof(Expr*) * expr->as.tuple_literal.num_elements); + for (int i = 0; i < expr->as.tuple_literal.num_elements; i++) { + elements_copy[i] = expr_clone(expr->as.tuple_literal.elements[i]); + } + return expr_tuple_literal( + elements_copy, + expr->as.tuple_literal.num_elements + ); + } + case EXPR_PREFIX_INC: return expr_prefix_inc(expr_clone(expr->as.prefix_inc.operand)); @@ -932,6 +964,13 @@ void expr_free(Expr *expr) { } free(expr->as.array_literal.elements); break; + case EXPR_TUPLE_LITERAL: + // Free tuple elements + for (int i = 0; i < expr->as.tuple_literal.num_elements; i++) { + expr_free(expr->as.tuple_literal.elements[i]); + } + free(expr->as.tuple_literal.elements); + break; case EXPR_OBJECT_LITERAL: // Free field names and values for (int i = 0; i < expr->as.object_literal.num_fields; i++) { diff --git a/src/frontend/parser/expressions.c b/src/frontend/parser/expressions.c index 1f815f18..7a55fe61 100644 --- a/src/frontend/parser/expressions.c +++ b/src/frontend/parser/expressions.c @@ -176,9 +176,37 @@ Expr* primary(Parser *p) { } if (match(p, TOK_LPAREN)) { - Expr *expr = expression(p); + // Could be a grouped expression (expr) or a tuple literal (expr1, expr2, ...) + Expr *first = expression(p); + + // Check for comma - if present, this is a tuple + if (match(p, TOK_COMMA)) { + // It's a tuple literal + int capacity = 8; + Expr **elements = malloc(sizeof(Expr*) * capacity); + elements[0] = first; + int num_elements = 1; + + do { + if (num_elements >= capacity) { + capacity *= 2; + elements = realloc(elements, sizeof(Expr*) * capacity); + } + elements[num_elements++] = expression(p); + } while (match(p, TOK_COMMA)); + + consume(p, TOK_RPAREN, "Expect ')' after tuple elements"); + + // Must have at least 2 elements (we already have at least 2 because we saw a comma) + Expr *tuple = expr_tuple_literal(elements, num_elements); + tuple->line = p->previous.line; + tuple->column = p->previous.column; + return tuple; + } + + // Just a grouped expression consume(p, TOK_RPAREN, "Expect ')' after expression"); - return expr; + return first; } // Object literal: { field: value, ... } @@ -444,6 +472,28 @@ Expr* postfix(Parser *p) { } else if (match(p, TOK_MINUS_MINUS)) { // Postfix decrement: x-- expr = expr_postfix_dec(expr); + } else if (p->current.type == TOK_NUMBER && p->current.is_float && + p->current.start[0] == '.') { + // Tuple index access: tuple.0, tuple.1, etc. + // The lexer tokenizes ".0" as a float, so we need to handle it here + advance(p); // consume the .N token + + // Extract the integer index from the source text (skip the leading '.') + // The float value may have precision issues, so parse from source + char idx_str[32]; + int idx_len = 0; + const char *src = p->previous.start + 1; // skip the '.' + while (idx_len < 31 && src < p->previous.start + p->previous.length) { + if (*src >= '0' && *src <= '9') { + idx_str[idx_len++] = *src; + } + src++; + } + idx_str[idx_len] = '\0'; + + char *property = strdup(idx_str); + expr = expr_get_property(expr, property); + free(property); } else { break; } @@ -841,6 +891,51 @@ Type* parse_type(Parser *p) { TypeKind kind; Type *type = NULL; + // Check for tuple type syntax: (type1, type2, ...) + if (p->current.type == TOK_LPAREN) { + advance(p); // consume '(' + + // Parse element types + int capacity = 8; + Type **element_types = malloc(sizeof(Type*) * capacity); + int num_elements = 0; + + if (!check(p, TOK_RPAREN)) { + do { + if (num_elements >= capacity) { + capacity *= 2; + element_types = realloc(element_types, sizeof(Type*) * capacity); + } + element_types[num_elements++] = parse_type(p); + } while (match(p, TOK_COMMA)); + } + + consume(p, TOK_RPAREN, "Expect ')' after tuple type elements"); + + // Must have at least 2 elements to be a tuple type + if (num_elements < 2) { + error(p, "Tuple type must have at least 2 elements"); + // Clean up and return infer type + for (int i = 0; i < num_elements; i++) { + type_free(element_types[i]); + } + free(element_types); + return type_new(TYPE_INFER); + } + + type = type_new(TYPE_TUPLE); + type->element_types = element_types; + type->num_element_types = num_elements; + + // Check for nullable type syntax: (type1, type2)? + if (p->current.type == TOK_QUESTION) { + advance(p); + type->nullable = 1; + } + + return type; + } + // Check for 'array' or 'array' syntax if (p->current.type == TOK_TYPE_ARRAY) { advance(p); diff --git a/tests/parity/language/tuples.expected b/tests/parity/language/tuples.expected new file mode 100644 index 00000000..4b644542 --- /dev/null +++ b/tests/parity/language/tuples.expected @@ -0,0 +1,27 @@ +42 +answer +2 +100 +hello +1 +2 +3 +3 +true +3.14 +text +null +10 +20 +30 +1 +2 +3 +4 +999 +result +12 +2 +arr +tuple +done diff --git a/tests/parity/language/tuples.hml b/tests/parity/language/tuples.hml new file mode 100644 index 00000000..6e4ceb26 --- /dev/null +++ b/tests/parity/language/tuples.hml @@ -0,0 +1,65 @@ +// Test tuples + +// Basic tuple creation +let pair = (42, "answer"); +print(pair.0); +print(pair.1); +print(pair.length); + +// Typed tuple +let typed: (i32, string) = (100, "hello"); +print(typed.0); +print(typed.1); + +// Tuple with more elements +let triple = (1, 2, 3); +print(triple.0); +print(triple.1); +print(triple.2); +print(triple.length); + +// Mixed types +let mixed = (true, 3.14, "text", null); +print(mixed.0); +print(mixed.1); +print(mixed.2); +print(mixed.3); + +// Bracket indexing +let indexed = (10, 20, 30); +print(indexed[0]); +print(indexed[1]); +print(indexed[2]); + +// Nested tuples +let nested = ((1, 2), (3, 4)); +print(nested.0.0); +print(nested.0.1); +print(nested.1.0); +print(nested.1.1); + +// Tuple in function +fn get_pair(): (i32, string) { + return (999, "result"); +} + +let result = get_pair(); +print(result.0); +print(result.1); + +// Tuple as function parameter +fn sum_pair(p: (i32, i32)): i32 { + return p.0 + p.1; +} + +print(sum_pair((5, 7))); + +// Tuple with array +let with_array = ([1, 2, 3], "arr"); +print(with_array.0[1]); +print(with_array.1); + +// typeof tuple +print(typeof((1, 2))); + +print("done");