summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md59
-rw-r--r--src/analysis/typecheck.c138
-rw-r--r--src/codegen/codegen_decl.c7
-rw-r--r--src/parser/parser_core.c6
-rw-r--r--src/parser/parser_stmt.c31
5 files changed, 206 insertions, 35 deletions
diff --git a/README.md b/README.md
index e9e383d..0ce0665 100644
--- a/README.md
+++ b/README.md
@@ -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