diff options
| -rw-r--r-- | README.md | 59 | ||||
| -rw-r--r-- | src/analysis/typecheck.c | 138 | ||||
| -rw-r--r-- | src/codegen/codegen_decl.c | 7 | ||||
| -rw-r--r-- | src/parser/parser_core.c | 6 | ||||
| -rw-r--r-- | src/parser/parser_stmt.c | 31 |
5 files changed, 206 insertions, 35 deletions
@@ -433,35 +433,42 @@ Automatically free the variable when scope exits. autofree var types = malloc(1024); ``` -#### Move Semantics & Copy Safety -Zen C prevents double-free errors by enforcing **Move Semantics** for non-trivial types (structs) by default. +#### Resource Semantics (Move by Default) +Zen C treats types with destructors (like `File`, `Vec`, or malloc'd pointers) as **Resources**. To prevent double-free errors, resources cannot be implicitly duplicated. -- **Move by Default**: Assigning a struct variable transfers ownership. The original variable becomes invalid. -- **Management Strategies**: - - **`Copy` Trait**: Opt-out of move semantics for simple types. - - **`ref` Binding**: Use `match val { Some(ref x) => ... }` to inspect without moving. - - **Smart Derives**: `@derive(Eq)` uses references for comparison to avoid moves. +- **Move by Default**: Assigning a resource variable transfers ownership. The original variable becomes invalid (Moved). +- **Copy Types**: Simple Plain Old Data (int, Point, etc.) can opt-in to `Copy` behavior, making assignment a duplication. + +**Diagnostics & Philosophy**: +If you see an error "Use of moved value", the compiler is telling you: *"This type owns a resource (like memory or a handle) and blindly copying it is unsafe."* + +> **Contrast:** Unlike C/C++, Zen C does not implicitly duplicate resource-owning values. + +**Function Arguments**: +Passing a value to a function follows the same rules as assignment: resources are moved unless passed by reference. ```zc -struct Mover { val: int; } +fn process(r: Resource) { ... } // 'r' is moved into function +fn peek(r: Resource*) { ... } // 'r' is borrowed (reference) +``` -fn main() { - var a = Mover { val: 1 }; - var b = a; // 'a' moved to 'b' - // print(a.val); // Error: Use of moved value 'a' -} +**Explicit Cloning**: +If you *do* want two copies of a resource, make it explicit: + +```zc +var b = a.clone(); // Deep copy: New allocation, safe. ``` -**Opt-in Copy**: +**Opt-in Copy (Value Types)**: +For small types without destructors: ```zc struct Point { x: int; y: int; } -impl Copy for Point {} +impl Copy for Point {} // Opt-in to implicit duplication fn main() { var p1 = Point { x: 1, y: 2 }; - var p2 = p1; // p1 copied to p2 - // p1 is still valid + var p2 = p1; // Copied. p1 stays valid. } ``` @@ -569,7 +576,23 @@ impl Drop for Resource { } ``` -> **Note:** If a variable is moved, `drop` is NOT called on the original variable. It adheres to [Move Semantics](#move-semantics--copy-safety). +> **Note:** If a variable is moved, `drop` is NOT called on the original variable. It adheres to [Resource Semantics](#resource-semantics-move-by-default). + +**Copy** + +Marker trait to opt-in to `Copy` behavior (implicit duplication) instead of Move semantics. Used via `@derive(Copy)`. + +> **Rule:** Types that implement `Copy` must not define a destructor (`Drop`). + +```zc +@derive(Copy) +struct Point { x: int; y: int; } + +fn main() { + var p1 = Point{x: 1, y: 2}; + var p2 = p1; // Copied! p1 remains valid. +} +``` #### Composition Use `use` to embed other structs. You can either mix them in (flatten fields) or name them (nest fields). diff --git a/src/analysis/typecheck.c b/src/analysis/typecheck.c index e3abf10..2b06bb4 100644 --- a/src/analysis/typecheck.c +++ b/src/analysis/typecheck.c @@ -69,10 +69,114 @@ static Symbol *tc_lookup(TypeChecker *tc, const char *name) return NULL; } +// ** Move Semantics Helpers ** + +static int is_safe_to_copy(TypeChecker *tc, Type *t) +{ + // Use parser's helper if available, or simple heuristic + return is_type_copy(tc->pctx, t); +} + +static void check_use_validity(TypeChecker *tc, ASTNode *var_node, Symbol *sym) +{ + if (!sym || !var_node) + { + return; + } + + if (sym->is_moved) + { + char msg[256]; + snprintf( + msg, 255, + "Use of moved value '%s'. This type owns resources and cannot be implicitly copied.", + sym->name); + tc_error(tc, var_node->token, msg); + } +} + +static void mark_symbol_moved(TypeChecker *tc, Symbol *sym, ASTNode *context_node) +{ + if (!sym) + { + return; + } + + // Only move if type is NOT Copy + Type *t = sym->type_info; + if (t && !is_safe_to_copy(tc, t)) + { + sym->is_moved = 1; + } +} + +static void mark_symbol_valid(TypeChecker *tc, Symbol *sym) +{ + if (sym) + { + sym->is_moved = 0; + } +} + // ** Node Checkers ** static void check_node(TypeChecker *tc, ASTNode *node); +static void check_expr_binary(TypeChecker *tc, ASTNode *node) +{ + check_node(tc, node->binary.left); + check_node(tc, node->binary.right); + + // Assignment Logic for Moves + if (strcmp(node->binary.op, "=") == 0) + { + // If RHS is a var, it might Move + if (node->binary.right->type == NODE_EXPR_VAR) + { + Symbol *rhs_sym = tc_lookup(tc, node->binary.right->var_ref.name); + if (rhs_sym) + { + mark_symbol_moved(tc, rhs_sym, node); + } + } + + // LHS is being (re-)initialized, so it becomes Valid. + if (node->binary.left->type == NODE_EXPR_VAR) + { + Symbol *lhs_sym = tc_lookup(tc, node->binary.left->var_ref.name); + if (lhs_sym) + { + mark_symbol_valid(tc, lhs_sym); + } + } + } +} + +static void check_expr_call(TypeChecker *tc, ASTNode *node) +{ + check_node(tc, node->call.callee); + + // Check arguments + ASTNode *arg = node->call.args; + while (arg) + { + check_node(tc, arg); + + // If argument is passed by VALUE, and it's a variable, it MOVES. + // If passed by ref (UNARY '&'), the child was checked but Is Not A Var Node itself. + if (arg->type == NODE_EXPR_VAR) + { + Symbol *sym = tc_lookup(tc, arg->var_ref.name); + if (sym) + { + mark_symbol_moved(tc, sym, node); + } + } + + arg = arg->next; + } +} + static void check_block(TypeChecker *tc, ASTNode *block) { tc_enter_scope(tc); @@ -140,6 +244,16 @@ static void check_var_decl(TypeChecker *tc, ASTNode *node) { check_type_compatibility(tc, decl_type, init_type, node->token); } + + // Move Analysis: If initializing from another variable, it moves. + if (node->var_decl.init_expr->type == NODE_EXPR_VAR) + { + Symbol *init_sym = tc_lookup(tc, node->var_decl.init_expr->var_ref.name); + if (init_sym) + { + mark_symbol_moved(tc, init_sym, node); + } + } } // If type is not explicit, we should ideally infer it from init_expr. @@ -187,6 +301,9 @@ static void check_expr_var(TypeChecker *tc, ASTNode *node) { node->type_info = sym->type_info; } + + // Check for Use-After-Move + check_use_validity(tc, node, sym); } static void check_node(TypeChecker *tc, ASTNode *node) @@ -240,15 +357,26 @@ static void check_node(TypeChecker *tc, ASTNode *node) tc_exit_scope(tc); break; case NODE_EXPR_BINARY: - check_node(tc, node->binary.left); - check_node(tc, node->binary.right); + check_expr_binary(tc, node); break; case NODE_EXPR_CALL: - check_node(tc, node->call.callee); - check_node(tc, node->call.args); + check_expr_call(tc, node); break; default: - // Generic recursion for lists. + // Generic recursion for lists and other nodes. + // Special case for Return to trigger move? + if (node->type == NODE_RETURN && node->ret.value) + { + // If returning a variable by value, it is moved. + if (node->ret.value->type == NODE_EXPR_VAR) + { + Symbol *sym = tc_lookup(tc, node->ret.value->var_ref.name); + if (sym) + { + mark_symbol_moved(tc, sym, node); + } + } + } break; } diff --git a/src/codegen/codegen_decl.c b/src/codegen/codegen_decl.c index 8ada7d6..932420f 100644 --- a/src/codegen/codegen_decl.c +++ b/src/codegen/codegen_decl.c @@ -853,6 +853,13 @@ void emit_impl_vtables(ParserContext *ctx, FILE *out) emitted[count].strct = strct; count++; + if (0 == strcmp(trait, "Copy")) + { + // Marker trait, no runtime vtable needed + ref = ref->next; + continue; + } + fprintf(out, "%s_VTable %s_%s_VTable = {", trait, strct, trait); ASTNode *m = node->impl_trait.methods; diff --git a/src/parser/parser_core.c b/src/parser/parser_core.c index 3c2805c..464c905 100644 --- a/src/parser/parser_core.c +++ b/src/parser/parser_core.c @@ -622,6 +622,12 @@ static ASTNode *generate_derive_impls(ParserContext *ctx, ASTNode *strct, char * sprintf(code, "impl %s { fn to_string(self) -> char* { return \"%s { ... }\"; } }", name, name); } + else if (0 == strcmp(trait, "Copy")) + { + // Marker trait for Copy/Move semantics + code = xmalloc(1024); + sprintf(code, "impl Copy for %s {}", name); + } if (code) { diff --git a/src/parser/parser_stmt.c b/src/parser/parser_stmt.c index c5efe8a..a8c8df5 100644 --- a/src/parser/parser_stmt.c +++ b/src/parser/parser_stmt.c @@ -4269,30 +4269,37 @@ ASTNode *parse_import(ParserContext *ctx, Lexer *l) strncpy(fn, t.start + 1, ln); fn[ln] = 0; - // Resolve relative paths (if starts with ./ or ../ + // Resolve paths relative to current file char resolved_path[1024]; - if (fn[0] == '.' && (fn[1] == '/' || (fn[1] == '.' && fn[2] == '/'))) + int is_explicit_relative = (fn[0] == '.' && (fn[1] == '/' || (fn[1] == '.' && fn[2] == '/'))); + + // Try to resolve relative to current file if not absolute + if (fn[0] != '/') { - // Relative import - resolve relative to current file char *current_dir = xstrdup(g_current_filename); char *last_slash = strrchr(current_dir, '/'); if (last_slash) { *last_slash = 0; // Truncate to directory + + // Handle explicit relative differently? + // Existing logic enforced it. Let's try to verify existence first. + + // Construct candidate path const char *leaf = fn; - if (leaf[0] == '.' && leaf[1] == '/') + // Clean up ./ prefix for cleaner path construction if we want + // but keeping it is fine too, /path/to/./file works. + + snprintf(resolved_path, sizeof(resolved_path), "%s/%s", current_dir, leaf); + + // If it's an explicit relative path, OR if the file exists at this relative location + if (is_explicit_relative || access(resolved_path, R_OK) == 0) { - leaf += 2; + free(fn); + fn = xstrdup(resolved_path); } - snprintf(resolved_path, sizeof(resolved_path), "%s/%s", current_dir, leaf); - } - else - { - snprintf(resolved_path, sizeof(resolved_path), "%s", fn); } free(current_dir); - free(fn); - fn = xstrdup(resolved_path); } // Check if file exists, if not try system-wide paths |
