From 3229598f4783fc3c40089607a4c557795aa70d58 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 20 Jan 2026 01:40:00 +0000 Subject: [PATCH 1/4] Add Tricycle: memory safety checker for Hemlock Tricycle is an opt-in static analyzer that proves memory safety properties for Hemlock programs. It provides: - Double-free detection (TRIC001) - Use-after-free detection (TRIC002) - Use-after-move detection (TRIC003) - Memory leak detection (TRIC004) - Async lifetime safety (TRIC006) - Ownership tracking Features: - CLI tool: `tricycle check program.hml` - Supports --strict, --all, --json output modes - Integrates with existing type checker for type-aware analysis - Proper diagnostic formatting with related locations and hints New files: - include/tricycle/tricycle.h - Public API and data structures - src/backends/tricycle/tricycle.c - Context and diagnostics - src/backends/tricycle/ownership.c - Ownership analysis engine - src/backends/tricycle/main.c - CLI entry point - tests/tricycle/ - Test suite with 7 ownership tests Build: `make tricycle` Test: `make test-tricycle` --- .gitignore | 1 + Makefile | 36 + include/tricycle/tricycle.h | 339 +++++++ src/backends/tricycle/main.c | 305 +++++++ src/backends/tricycle/ownership.c | 857 ++++++++++++++++++ src/backends/tricycle/tricycle.c | 677 ++++++++++++++ tests/tricycle/ownership/double_free.expected | 2 + tests/tricycle/ownership/double_free.hml | 6 + .../ownership/free_after_move.expected | 2 + tests/tricycle/ownership/free_after_move.hml | 7 + tests/tricycle/ownership/memory_leak.expected | 3 + tests/tricycle/ownership/memory_leak.hml | 9 + .../tricycle/ownership/ownership_transfer.hml | 7 + .../ownership/use_after_free.expected | 2 + tests/tricycle/ownership/use_after_free.hml | 6 + .../ownership/use_after_move.expected | 2 + tests/tricycle/ownership/use_after_move.hml | 7 + tests/tricycle/ownership/valid_alloc_free.hml | 8 + tests/tricycle/run_tricycle_tests.sh | 115 +++ 19 files changed, 2391 insertions(+) create mode 100644 include/tricycle/tricycle.h create mode 100644 src/backends/tricycle/main.c create mode 100644 src/backends/tricycle/ownership.c create mode 100644 src/backends/tricycle/tricycle.c create mode 100644 tests/tricycle/ownership/double_free.expected create mode 100644 tests/tricycle/ownership/double_free.hml create mode 100644 tests/tricycle/ownership/free_after_move.expected create mode 100644 tests/tricycle/ownership/free_after_move.hml create mode 100644 tests/tricycle/ownership/memory_leak.expected create mode 100644 tests/tricycle/ownership/memory_leak.hml create mode 100644 tests/tricycle/ownership/ownership_transfer.hml create mode 100644 tests/tricycle/ownership/use_after_free.expected create mode 100644 tests/tricycle/ownership/use_after_free.hml create mode 100644 tests/tricycle/ownership/use_after_move.expected create mode 100644 tests/tricycle/ownership/use_after_move.hml create mode 100644 tests/tricycle/ownership/valid_alloc_free.hml create mode 100755 tests/tricycle/run_tricycle_tests.sh diff --git a/.gitignore b/.gitignore index 781ae7ff..99db295b 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ # Compiled executables /hemlock /hemlockc +/tricycle hemlock.exe *.out *.app diff --git a/Makefile b/Makefile index dc1159a6..d3a7c03c 100644 --- a/Makefile +++ b/Makefile @@ -114,6 +114,7 @@ BUILD_DIRS = $(BUILD_DIR) \ $(BUILD_DIR)/backends/interpreter/runtime \ $(BUILD_DIR)/backends/interpreter/profiler \ $(BUILD_DIR)/backends/compiler \ + $(BUILD_DIR)/backends/tricycle \ $(BUILD_DIR)/tools \ $(BUILD_DIR)/tools/lsp \ $(BUILD_DIR)/tools/bundler \ @@ -816,3 +817,38 @@ uninstall: rm -f $(DESTDIR)$(BINDIR)/$(COMPILER_TARGET) rm -rf $(DESTDIR)$(LIBDIR) @echo "✓ Hemlock uninstalled" + +# ========== TRICYCLE MEMORY SAFETY CHECKER ========== + +# Tricycle source files +TRICYCLE_SRCS = $(SRC_DIR)/backends/tricycle/main.c \ + $(SRC_DIR)/backends/tricycle/tricycle.c \ + $(SRC_DIR)/backends/tricycle/ownership.c + +TRICYCLE_OBJS = $(BUILD_DIR)/backends/tricycle/main.o \ + $(BUILD_DIR)/backends/tricycle/tricycle.o \ + $(BUILD_DIR)/backends/tricycle/ownership.o + +TRICYCLE_TARGET = tricycle + +.PHONY: build-tricycle tricycle-clean test-tricycle + +# Build tricycle - use build-tricycle target to avoid circular dependency +tricycle: $(BUILD_DIRS) build-tricycle + +build-tricycle: $(TRICYCLE_OBJS) $(LIBCOMMON) + $(CC) $(TRICYCLE_OBJS) $(LIBCOMMON) -o $(TRICYCLE_TARGET) -lm + +$(BUILD_DIR)/backends/tricycle/%.o: $(SRC_DIR)/backends/tricycle/%.c | $(BUILD_DIRS) + $(CC) $(CFLAGS) -c $< -o $@ + +tricycle-clean: + rm -f $(TRICYCLE_TARGET) $(TRICYCLE_OBJS) + +# Run tricycle test suite +test-tricycle: tricycle + @echo "Running Tricycle memory safety tests..." + @bash tests/tricycle/run_tricycle_tests.sh + +# Add tricycle to full clean +fullclean: clean compiler-clean runtime-clean release-clean release-static-clean analyze-clean tricycle-clean diff --git a/include/tricycle/tricycle.h b/include/tricycle/tricycle.h new file mode 100644 index 00000000..94f963f2 --- /dev/null +++ b/include/tricycle/tricycle.h @@ -0,0 +1,339 @@ +/* + * Tricycle - Hemlock's Memory Safety Checker + * + * "Training wheels for the unsafe. Take them off when you're ready." + * + * Tricycle is an opt-in static analyzer that proves memory safety properties + * for Hemlock programs. It provides: + * - Double-free detection + * - Use-after-free detection + * - Memory leak detection + * - Null pointer safety + * - Async lifetime safety + * - Ownership tracking + * + * Tricycle does NOT change Hemlock's semantics. It's a separate tool that + * rejects programs it can't prove safe while letting unsafe programs compile + * normally with hemlock/hemlockc. + */ + +#ifndef HEMLOCK_TRICYCLE_H +#define HEMLOCK_TRICYCLE_H + +#include "frontend/ast.h" +#include "compiler/type_check.h" + +// ========== VERSION ========== + +#define TRICYCLE_VERSION_MAJOR 0 +#define TRICYCLE_VERSION_MINOR 1 +#define TRICYCLE_VERSION_PATCH 0 +#define TRICYCLE_VERSION "0.1.0" + +// ========== FORWARD DECLARATIONS ========== + +typedef struct TricycleContext TricycleContext; +typedef struct OwnershipEnv OwnershipEnv; +typedef struct OwnershipBinding OwnershipBinding; +typedef struct MemoryState MemoryState; +typedef struct BorrowInfo BorrowInfo; +typedef struct TricycleDiagnostic TricycleDiagnostic; +typedef struct RelatedLocation RelatedLocation; + +// ========== MEMORY STATE ========== + +// Represents the allocation state of a pointer-typed variable +typedef enum { + MEM_UNALLOCATED, // Not allocated (or after free) + MEM_ALLOCATED, // Valid, owned - after alloc()/buffer() + MEM_MOVED, // Ownership transferred elsewhere + MEM_BORROWED, // Reference to owned memory (Phase 2) + MEM_UNKNOWN, // External/unknown origin - conservatively unsafe +} MemoryStateKind; + +// Full memory state information for a pointer +struct MemoryState { + MemoryStateKind kind; + int allocation_line; // Where allocated (for diagnostics) + int allocation_column; // Column of allocation + int free_line; // Where freed (if applicable) + int free_column; // Column of free + int move_line; // Where moved (if applicable) + int move_column; // Column of move + char *owner_name; // Variable that owns this memory + char *moved_to; // If MOVED, which variable now owns it + int borrow_count; // Number of active borrows (Phase 2) +}; + +// ========== OWNERSHIP ENVIRONMENT ========== + +// Parameter ownership modifiers (Phase 2) +typedef enum { + OWNER_DEFAULT, // Default: borrow for pointers + OWNER_OWN, // Takes ownership (own keyword) + OWNER_BORROW, // Borrows (borrow keyword) + OWNER_BORROW_MUT, // Mutable borrow (borrow mut keyword) +} OwnershipKind; + +// Scope kind for tracking context +typedef enum { + SCOPE_GLOBAL, // Top-level scope + SCOPE_FUNCTION, // Function body + SCOPE_BLOCK, // Regular block {} + SCOPE_LOOP, // Loop body (while, for, loop) + SCOPE_IF, // If/else branch + SCOPE_TRY, // Try block + SCOPE_ASYNC, // Spawned task body +} ScopeKind; + +// Variable binding in the ownership environment +struct OwnershipBinding { + char *name; // Variable name + MemoryState state; // Current memory state + int is_parameter; // Came from function parameter? + OwnershipKind param_kind; // OWN, BORROW, BORROW_MUT (for params) + int declaration_line; // Line where declared + int declaration_column; // Column where declared + int is_ptr_type; // Is this a pointer type? + struct OwnershipBinding *next; // Linked list +}; + +// Scoped ownership environment +struct OwnershipEnv { + OwnershipBinding *bindings; // Linked list of bindings in this scope + struct OwnershipEnv *parent; // Enclosing scope + ScopeKind scope_kind; // What kind of scope is this? + char *scope_label; // Optional label (for labeled loops) + int scope_start_line; // Line where scope started +}; + +// ========== ASYNC BORROW TRACKING ========== + +// Track pointers passed to spawned tasks +typedef struct AsyncBorrow { + char *var_name; // Variable name borrowed + int spawn_line; // Line of spawn() call + int spawn_column; // Column of spawn() call + int task_id; // Internal task identifier + int is_joined; // Whether join() has been called + struct AsyncBorrow *next; // Linked list +} AsyncBorrow; + +// ========== DIAGNOSTICS ========== + +// Diagnostic severity levels +typedef enum { + TRIC_ERROR, // Definite memory error - must fix + TRIC_WARNING, // Potential issue - should review + TRIC_NOTE, // Additional context information +} TricycleSeverity; + +// Error categories +typedef enum { + TRIC_DOUBLE_FREE, // Memory freed twice + TRIC_USE_AFTER_FREE, // Use of freed memory + TRIC_USE_AFTER_MOVE, // Use of moved value + TRIC_MEMORY_LEAK, // Allocated memory not freed + TRIC_NULL_DEREF, // Possible null dereference + TRIC_DANGLING_ASYNC, // Pointer freed while task running + TRIC_BORROW_OUTLIVES_OWNER, // Borrow outlives owner (Phase 2) + TRIC_MOVE_OF_BORROWED, // Moving borrowed value (Phase 2) + TRIC_FREE_OF_BORROWED, // Freeing borrowed value (Phase 2) + TRIC_UNKNOWN_LIFETIME, // Cannot determine lifetime + TRIC_INVALID_OWNERSHIP, // Invalid ownership operation +} TricycleErrorKind; + +// Related location for multi-location diagnostics +struct RelatedLocation { + int line; + int column; + char *message; // e.g., "allocated here", "freed here" +}; + +// Full diagnostic information +struct TricycleDiagnostic { + TricycleSeverity severity; + TricycleErrorKind kind; + int line; + int column; + int end_column; // For range highlighting + char *message; // Main error message + char *hint; // Suggested fix + RelatedLocation *related; // Related locations (allocated here, freed here, etc.) + int related_count; + struct TricycleDiagnostic *next; // Linked list +}; + +// ========== TRICYCLE CONTEXT ========== + +struct TricycleContext { + // Input + Stmt **program; // AST from parser + int stmt_count; // Number of top-level statements + TypeCheckContext *type_ctx; // Type information (from type checker) + const char *filename; // Source filename + const char *source; // Source code (for column calculation) + + // Analysis state + OwnershipEnv *current_env; // Current ownership scope + AsyncBorrow *async_borrows; // Pointers borrowed by async tasks + + // Output + TricycleDiagnostic *diagnostics; // Head of diagnostics list + TricycleDiagnostic *diagnostics_tail; // Tail for O(1) append + int error_count; // Number of errors + int warning_count; // Number of warnings + + // Configuration + int strict_mode; // Treat warnings as errors + int infer_ownership; // Auto-infer vs require annotations + int verbose; // Verbose output mode + int collect_all; // Collect all diagnostics vs stop at first error +}; + +// ========== CONTEXT MANAGEMENT ========== + +// Create a new Tricycle context +TricycleContext* tricycle_new(const char *filename); + +// Free a Tricycle context and all associated memory +void tricycle_free(TricycleContext *ctx); + +// Set the source code (for column calculation in diagnostics) +void tricycle_set_source(TricycleContext *ctx, const char *source); + +// Set the type checking context (optional, enables type-aware analysis) +void tricycle_set_type_ctx(TricycleContext *ctx, TypeCheckContext *type_ctx); + +// ========== ENVIRONMENT OPERATIONS ========== + +// Push a new scope onto the ownership environment +void tricycle_push_scope(TricycleContext *ctx, ScopeKind kind); + +// Push a labeled scope (for labeled loops) +void tricycle_push_scope_labeled(TricycleContext *ctx, ScopeKind kind, const char *label); + +// Pop the current scope and check for leaks +void tricycle_pop_scope(TricycleContext *ctx); + +// Bind a variable in the current scope +void tricycle_bind(TricycleContext *ctx, const char *name, MemoryStateKind state, + int line, int column, int is_ptr_type); + +// Bind a function parameter with ownership kind +void tricycle_bind_param(TricycleContext *ctx, const char *name, OwnershipKind kind, + int line, int column, int is_ptr_type); + +// Look up a variable's ownership binding (searches parent scopes) +OwnershipBinding* tricycle_lookup(TricycleContext *ctx, const char *name); + +// Update a variable's memory state +void tricycle_update_state(TricycleContext *ctx, const char *name, + MemoryStateKind new_state, int line, int column); + +// Mark a variable as moved to another variable +void tricycle_mark_moved(TricycleContext *ctx, const char *from, const char *to, + int line, int column); + +// ========== ANALYSIS ENTRY POINTS ========== + +// Analyze a complete program +// Returns the number of memory safety errors found +int tricycle_analyze_program(TricycleContext *ctx, Stmt **stmts, int stmt_count); + +// Analyze a single statement +void tricycle_analyze_stmt(TricycleContext *ctx, Stmt *stmt); + +// Analyze an expression and check for memory safety issues +void tricycle_analyze_expr(TricycleContext *ctx, Expr *expr); + +// ========== SPECIFIC ANALYSIS FUNCTIONS ========== + +// Check for double-free +void tricycle_check_free(TricycleContext *ctx, Expr *ptr_expr, int line, int column); + +// Check for use-after-free/move +void tricycle_check_use(TricycleContext *ctx, const char *var_name, int line, int column); + +// Check for memory leaks at scope exit +void tricycle_check_leaks(TricycleContext *ctx); + +// Check async safety (spawn/join) +void tricycle_check_spawn(TricycleContext *ctx, Expr *call_expr, int line, int column); +void tricycle_check_join(TricycleContext *ctx, Expr *task_expr, int line, int column); + +// ========== DIAGNOSTICS ========== + +// Add an error diagnostic +void tricycle_error(TricycleContext *ctx, TricycleErrorKind kind, + int line, int column, const char *fmt, ...); + +// Add a warning diagnostic +void tricycle_warning(TricycleContext *ctx, TricycleErrorKind kind, + int line, int column, const char *fmt, ...); + +// Add a note (additional context) +void tricycle_note(TricycleContext *ctx, int line, int column, const char *fmt, ...); + +// Add a related location to the most recent diagnostic +void tricycle_add_related(TricycleContext *ctx, int line, int column, const char *message); + +// Add a hint to the most recent diagnostic +void tricycle_add_hint(TricycleContext *ctx, const char *hint); + +// Get the diagnostic list +TricycleDiagnostic* tricycle_get_diagnostics(TricycleContext *ctx); + +// Print diagnostics to stderr +void tricycle_print_diagnostics(TricycleContext *ctx); + +// Print diagnostics as JSON (for IDE integration) +void tricycle_print_diagnostics_json(TricycleContext *ctx); + +// Free all diagnostics +void tricycle_free_diagnostics(TricycleContext *ctx); + +// ========== ERROR CODE HELPERS ========== + +// Get error code string (e.g., "TRIC001") +const char* tricycle_error_code(TricycleErrorKind kind); + +// Get error name string (e.g., "DOUBLE_FREE") +const char* tricycle_error_name(TricycleErrorKind kind); + +// Get severity string (e.g., "error", "warning", "note") +const char* tricycle_severity_str(TricycleSeverity severity); + +// ========== UTILITY FUNCTIONS ========== + +// Check if an expression is a pointer type +int tricycle_is_ptr_expr(TricycleContext *ctx, Expr *expr); + +// Check if a type is a pointer type +int tricycle_is_ptr_type(Type *type); + +// Get the variable name from an expression (if it's a simple identifier) +const char* tricycle_get_var_name(Expr *expr); + +// Check if a function call is an allocation function (alloc, buffer) +int tricycle_is_alloc_call(Expr *call_expr); + +// Check if a function call is a deallocation function (free) +int tricycle_is_free_call(Expr *call_expr); + +// Check if a function call is spawn +int tricycle_is_spawn_call(Expr *call_expr); + +// Check if a function call is join +int tricycle_is_join_call(Expr *call_expr); + +// Check if a function call is a pointer operation (ptr_read_*, ptr_write_*, etc.) +int tricycle_is_ptr_op(Expr *call_expr); + +// Clone a memory state +MemoryState tricycle_clone_state(const MemoryState *state); + +// Free memory state resources (owner_name, moved_to) +void tricycle_free_state(MemoryState *state); + +#endif // HEMLOCK_TRICYCLE_H diff --git a/src/backends/tricycle/main.c b/src/backends/tricycle/main.c new file mode 100644 index 00000000..7ce5bf93 --- /dev/null +++ b/src/backends/tricycle/main.c @@ -0,0 +1,305 @@ +/* + * Tricycle - Hemlock's Memory Safety Checker + * + * Command-line interface for running memory safety analysis. + * + * Usage: + * tricycle check program.hml Check a file for memory safety issues + * tricycle check --strict program.hml Treat warnings as errors + * tricycle check --all program.hml Show all diagnostics (not just first) + * tricycle check --json program.hml Output diagnostics as JSON + */ + +#include +#include +#include +#include + +#include "tricycle/tricycle.h" +#include "frontend.h" +#include "compiler/type_check.h" +#include "version.h" + +// ========== COMMAND-LINE OPTIONS ========== + +typedef struct { + const char *input_file; + int strict_mode; // Treat warnings as errors + int collect_all; // Show all diagnostics + int json_output; // Output as JSON + int verbose; // Verbose output + int help; // Show help + int version; // Show version + int type_check; // Enable type checking first +} Options; + +static void print_usage(const char *progname) { + fprintf(stderr, "Tricycle v%s - Hemlock Memory Safety Checker\n\n", TRICYCLE_VERSION); + fprintf(stderr, "\"Training wheels for the unsafe. Take them off when you're ready.\"\n\n"); + fprintf(stderr, "Usage: %s [command] [options] \n\n", progname); + fprintf(stderr, "Commands:\n"); + fprintf(stderr, " check Analyze a file for memory safety issues\n\n"); + fprintf(stderr, "Options:\n"); + fprintf(stderr, " --strict Treat warnings as errors\n"); + fprintf(stderr, " --all Show all diagnostics (not just first error)\n"); + fprintf(stderr, " --json Output diagnostics as JSON (for IDE integration)\n"); + fprintf(stderr, " --no-type-check Skip type checking phase\n"); + fprintf(stderr, " -v, --verbose Verbose output\n"); + fprintf(stderr, " -h, --help Show this help message\n"); + fprintf(stderr, " --version Show version\n"); + fprintf(stderr, "\nExamples:\n"); + fprintf(stderr, " %s check program.hml\n", progname); + fprintf(stderr, " %s check --strict --all program.hml\n", progname); + fprintf(stderr, " %s check --json program.hml > diagnostics.json\n", progname); + fprintf(stderr, "\nExit codes:\n"); + fprintf(stderr, " 0 No errors (warnings may exist)\n"); + fprintf(stderr, " 1 Memory safety errors found\n"); + fprintf(stderr, " 2 Invalid arguments or file not found\n"); +} + +static Options parse_args(int argc, char **argv) { + Options opts = { + .input_file = NULL, + .strict_mode = 0, + .collect_all = 0, + .json_output = 0, + .verbose = 0, + .help = 0, + .version = 0, + .type_check = 1, + }; + + static struct option long_options[] = { + {"strict", no_argument, 0, 's'}, + {"all", no_argument, 0, 'a'}, + {"json", no_argument, 0, 'j'}, + {"no-type-check", no_argument, 0, 't'}, + {"verbose", no_argument, 0, 'v'}, + {"help", no_argument, 0, 'h'}, + {"version", no_argument, 0, 'V'}, + {0, 0, 0, 0} + }; + + int opt; + int option_index = 0; + + // Skip "check" command if present + int start_index = 1; + if (argc > 1 && strcmp(argv[1], "check") == 0) { + start_index = 2; + } + + // Reset getopt + optind = start_index; + + while ((opt = getopt_long(argc, argv, "savhjV", long_options, &option_index)) != -1) { + switch (opt) { + case 's': + opts.strict_mode = 1; + break; + case 'a': + opts.collect_all = 1; + break; + case 'j': + opts.json_output = 1; + break; + case 't': + opts.type_check = 0; + break; + case 'v': + opts.verbose = 1; + break; + case 'h': + opts.help = 1; + break; + case 'V': + opts.version = 1; + break; + default: + break; + } + } + + // Get input file + if (optind < argc) { + opts.input_file = argv[optind]; + } + + return opts; +} + +// Read entire file into string +static char* read_file(const char *path) { + FILE *file = fopen(path, "rb"); + if (!file) { + fprintf(stderr, "error: Could not open file '%s'\n", path); + return NULL; + } + + fseek(file, 0, SEEK_END); + long size = ftell(file); + rewind(file); + + char *buffer = malloc(size + 1); + if (!buffer) { + fprintf(stderr, "error: Could not allocate memory for file\n"); + fclose(file); + return NULL; + } + + size_t bytes_read = fread(buffer, 1, size, file); + if ((long)bytes_read < size) { + fprintf(stderr, "error: Could not read file\n"); + free(buffer); + fclose(file); + return NULL; + } + + buffer[size] = '\0'; + fclose(file); + return buffer; +} + +// ========== MAIN ========== + +int main(int argc, char **argv) { + Options opts = parse_args(argc, argv); + + // Handle help and version + if (opts.help) { + print_usage(argv[0]); + return 0; + } + + if (opts.version) { + printf("tricycle %s (Hemlock %s)\n", TRICYCLE_VERSION, HEMLOCK_VERSION); + return 0; + } + + // Check for input file + if (!opts.input_file) { + fprintf(stderr, "error: No input file specified\n\n"); + print_usage(argv[0]); + return 2; + } + + // Read source file + char *source = read_file(opts.input_file); + if (!source) { + return 2; + } + + if (opts.verbose) { + fprintf(stderr, "Analyzing: %s\n", opts.input_file); + } + + // Lex and parse + Lexer lexer; + lexer_init(&lexer, source); + + Parser parser; + parser_init(&parser, &lexer); + + int stmt_count = 0; + Stmt **statements = parse_program(&parser, &stmt_count); + if (parser.had_error || !statements) { + fprintf(stderr, "error: Failed to parse '%s'\n", opts.input_file); + free(source); + return 2; + } + + if (opts.verbose) { + fprintf(stderr, "Parsed %d statements\n", stmt_count); + } + + // Resolve variables + resolve_program(statements, stmt_count); + + // Optional: Run type checker first + TypeCheckContext *type_ctx = NULL; + if (opts.type_check) { + type_ctx = type_check_new(opts.input_file); + type_check_program(type_ctx, statements, stmt_count); + + if (opts.verbose) { + fprintf(stderr, "Type check: %d errors, %d warnings\n", + type_ctx->error_count, type_ctx->warning_count); + } + } + + // Create tricycle context + TricycleContext *ctx = tricycle_new(opts.input_file); + if (!ctx) { + fprintf(stderr, "error: Failed to create analysis context\n"); + if (type_ctx) type_check_free(type_ctx); + free(source); + return 2; + } + + tricycle_set_source(ctx, source); + if (type_ctx) { + tricycle_set_type_ctx(ctx, type_ctx); + } + ctx->strict_mode = opts.strict_mode; + ctx->collect_all = opts.collect_all; + ctx->verbose = opts.verbose; + + // Run analysis + (void)tricycle_analyze_program(ctx, statements, stmt_count); + + if (opts.verbose) { + fprintf(stderr, "Analysis complete: %d errors, %d warnings\n", + ctx->error_count, ctx->warning_count); + } + + // Output diagnostics + if (opts.json_output) { + tricycle_print_diagnostics_json(ctx); + } else { + tricycle_print_diagnostics(ctx); + } + + // Summary + if (!opts.json_output) { + if (ctx->error_count > 0 || ctx->warning_count > 0) { + fprintf(stderr, "\n"); + } + if (ctx->error_count > 0) { + fprintf(stderr, "error: aborting due to %d memory safety error%s\n", + ctx->error_count, ctx->error_count == 1 ? "" : "s"); + } else if (ctx->warning_count > 0 && opts.strict_mode) { + fprintf(stderr, "error: %d warning%s (--strict mode)\n", + ctx->warning_count, ctx->warning_count == 1 ? "" : "s"); + } else if (ctx->warning_count > 0) { + fprintf(stderr, "warning: %d warning%s generated\n", + ctx->warning_count, ctx->warning_count == 1 ? "" : "s"); + } else { + if (opts.verbose) { + fprintf(stderr, "No memory safety issues found.\n"); + } + } + } + + // Determine exit code + int exit_code = 0; + if (ctx->error_count > 0) { + exit_code = 1; + } else if (ctx->warning_count > 0 && opts.strict_mode) { + exit_code = 1; + } + + // Cleanup + tricycle_free(ctx); + if (type_ctx) type_check_free(type_ctx); + + // Free AST + for (int i = 0; i < stmt_count; i++) { + if (statements[i]) { + stmt_free(statements[i]); + } + } + free(statements); + free(source); + + return exit_code; +} diff --git a/src/backends/tricycle/ownership.c b/src/backends/tricycle/ownership.c new file mode 100644 index 00000000..f6be4105 --- /dev/null +++ b/src/backends/tricycle/ownership.c @@ -0,0 +1,857 @@ +/* + * Tricycle - Ownership Analysis + * + * Implements ownership tracking and memory safety analysis. + * This is the core analysis engine that walks the AST and + * detects memory safety violations. + */ + +#include +#include +#include +#include "tricycle/tricycle.h" + +// ========== FORWARD DECLARATIONS ========== + +static void analyze_let_stmt(TricycleContext *ctx, Stmt *stmt); +static void analyze_expr_stmt(TricycleContext *ctx, Stmt *stmt); +static void analyze_if_stmt(TricycleContext *ctx, Stmt *stmt); +static void analyze_while_stmt(TricycleContext *ctx, Stmt *stmt); +static void analyze_for_stmt(TricycleContext *ctx, Stmt *stmt); +static void analyze_block_stmt(TricycleContext *ctx, Stmt *stmt); +static void analyze_return_stmt(TricycleContext *ctx, Stmt *stmt); +static void analyze_function_expr(TricycleContext *ctx, Expr *expr); +static void analyze_call_expr(TricycleContext *ctx, Expr *expr); +static void analyze_assign_expr(TricycleContext *ctx, Expr *expr); + +// ========== STATEMENT ANALYSIS ========== + +int tricycle_analyze_program(TricycleContext *ctx, Stmt **stmts, int stmt_count) { + if (!ctx || !stmts) return 0; + + ctx->program = stmts; + ctx->stmt_count = stmt_count; + + // Analyze each top-level statement + for (int i = 0; i < stmt_count; i++) { + if (stmts[i]) { + tricycle_analyze_stmt(ctx, stmts[i]); + } + + // Stop if we hit an error and not in collect_all mode + if (ctx->error_count > 0 && !ctx->collect_all) { + break; + } + } + + // Check for leaks at program end + tricycle_check_leaks(ctx); + + return ctx->error_count; +} + +void tricycle_analyze_stmt(TricycleContext *ctx, Stmt *stmt) { + if (!ctx || !stmt) return; + + switch (stmt->type) { + case STMT_LET: + case STMT_CONST: + analyze_let_stmt(ctx, stmt); + break; + + case STMT_EXPR: + analyze_expr_stmt(ctx, stmt); + break; + + case STMT_IF: + analyze_if_stmt(ctx, stmt); + break; + + case STMT_WHILE: + case STMT_LOOP: + analyze_while_stmt(ctx, stmt); + break; + + case STMT_FOR: + case STMT_FOR_IN: + analyze_for_stmt(ctx, stmt); + break; + + case STMT_BLOCK: + analyze_block_stmt(ctx, stmt); + break; + + case STMT_RETURN: + analyze_return_stmt(ctx, stmt); + break; + + case STMT_TRY: + // Analyze try block + if (stmt->as.try_stmt.try_block) { + tricycle_push_scope(ctx, SCOPE_TRY); + tricycle_analyze_stmt(ctx, stmt->as.try_stmt.try_block); + tricycle_pop_scope(ctx); + } + // Analyze catch block + if (stmt->as.try_stmt.catch_block) { + tricycle_push_scope(ctx, SCOPE_BLOCK); + if (stmt->as.try_stmt.catch_param) { + // Bind catch parameter as unknown (exception value) + tricycle_bind(ctx, stmt->as.try_stmt.catch_param, + MEM_UNKNOWN, stmt->line, stmt->column, 0); + } + tricycle_analyze_stmt(ctx, stmt->as.try_stmt.catch_block); + tricycle_pop_scope(ctx); + } + // Analyze finally block + if (stmt->as.try_stmt.finally_block) { + tricycle_push_scope(ctx, SCOPE_BLOCK); + tricycle_analyze_stmt(ctx, stmt->as.try_stmt.finally_block); + tricycle_pop_scope(ctx); + } + break; + + case STMT_DEFER: + // Analyze the deferred expression + if (stmt->as.defer_stmt.call) { + tricycle_analyze_expr(ctx, stmt->as.defer_stmt.call); + } + break; + + case STMT_SWITCH: + // Analyze switch expression + if (stmt->as.switch_stmt.expr) { + tricycle_analyze_expr(ctx, stmt->as.switch_stmt.expr); + } + // Analyze each case + for (int i = 0; i < stmt->as.switch_stmt.num_cases; i++) { + if (stmt->as.switch_stmt.case_values[i]) { + tricycle_analyze_expr(ctx, stmt->as.switch_stmt.case_values[i]); + } + if (stmt->as.switch_stmt.case_bodies[i]) { + tricycle_push_scope(ctx, SCOPE_BLOCK); + tricycle_analyze_stmt(ctx, stmt->as.switch_stmt.case_bodies[i]); + tricycle_pop_scope(ctx); + } + } + break; + + case STMT_BREAK: + case STMT_CONTINUE: + // No memory analysis needed for control flow + break; + + case STMT_THROW: + if (stmt->as.throw_stmt.value) { + tricycle_analyze_expr(ctx, stmt->as.throw_stmt.value); + } + break; + + case STMT_DEFINE_OBJECT: + case STMT_ENUM: + case STMT_IMPORT: + case STMT_EXPORT: + case STMT_IMPORT_FFI: + case STMT_EXTERN_FN: + case STMT_TYPE_ALIAS: + // No runtime memory analysis needed for declarations + break; + + default: + break; + } +} + +static void analyze_let_stmt(TricycleContext *ctx, Stmt *stmt) { + if (!ctx || !stmt) return; + + const char *name = (stmt->type == STMT_LET) ? + stmt->as.let.name : stmt->as.const_stmt.name; + Expr *value = (stmt->type == STMT_LET) ? + stmt->as.let.value : stmt->as.const_stmt.value; + Type *type_annotation = (stmt->type == STMT_LET) ? + stmt->as.let.type_annotation : stmt->as.const_stmt.type_annotation; + + int is_ptr = tricycle_is_ptr_type(type_annotation); + + if (value) { + // Analyze the initialization expression + tricycle_analyze_expr(ctx, value); + + if (tricycle_is_alloc_call(value)) { + // Direct allocation: let p = alloc(64); + tricycle_bind(ctx, name, MEM_ALLOCATED, stmt->line, stmt->column, 1); + } else if (value->type == EXPR_IDENT) { + // Assignment from another variable: let q = p; + const char *source_name = value->as.ident.name; + OwnershipBinding *source = tricycle_lookup(ctx, source_name); + + if (source && source->is_ptr_type) { + // This is ownership transfer + if (source->state.kind == MEM_ALLOCATED) { + // Transfer ownership from source to this variable + tricycle_bind(ctx, name, MEM_ALLOCATED, stmt->line, stmt->column, 1); + // Copy allocation info to new owner + OwnershipBinding *new_binding = tricycle_lookup(ctx, name); + if (new_binding) { + new_binding->state.allocation_line = source->state.allocation_line; + new_binding->state.allocation_column = source->state.allocation_column; + } + // Mark source as moved + tricycle_mark_moved(ctx, source_name, name, stmt->line, stmt->column); + } else if (source->state.kind == MEM_UNALLOCATED) { + // Use after free + tricycle_error(ctx, TRIC_USE_AFTER_FREE, stmt->line, stmt->column, + "use of freed memory '%s'", source_name); + tricycle_add_related(ctx, source->state.free_line, source->state.free_column, + "freed here"); + tricycle_add_hint(ctx, "do not use memory after it has been freed"); + tricycle_bind(ctx, name, MEM_UNKNOWN, stmt->line, stmt->column, 1); + } else if (source->state.kind == MEM_MOVED) { + // Use after move + tricycle_error(ctx, TRIC_USE_AFTER_MOVE, stmt->line, stmt->column, + "use of moved value '%s'", source_name); + tricycle_add_related(ctx, source->state.move_line, source->state.move_column, + "moved here"); + if (source->state.moved_to) { + char hint[256]; + snprintf(hint, sizeof(hint), "use '%s' instead, which now owns the memory", + source->state.moved_to); + tricycle_add_hint(ctx, hint); + } + tricycle_bind(ctx, name, MEM_UNKNOWN, stmt->line, stmt->column, 1); + } else { + // Unknown or borrowed - bind as unknown + tricycle_bind(ctx, name, source->state.kind, stmt->line, stmt->column, 1); + } + } else { + // Non-pointer type + tricycle_bind(ctx, name, MEM_UNKNOWN, stmt->line, stmt->column, is_ptr); + } + } else if (value->type == EXPR_NULL) { + // let p = null; + tricycle_bind(ctx, name, MEM_UNALLOCATED, stmt->line, stmt->column, is_ptr); + } else { + // Other expressions + tricycle_bind(ctx, name, MEM_UNKNOWN, stmt->line, stmt->column, + is_ptr || tricycle_is_ptr_expr(ctx, value)); + } + } else { + // Uninitialized (shouldn't happen in valid Hemlock, but handle it) + tricycle_bind(ctx, name, MEM_UNKNOWN, stmt->line, stmt->column, is_ptr); + } +} + +static void analyze_expr_stmt(TricycleContext *ctx, Stmt *stmt) { + if (!ctx || !stmt || !stmt->as.expr) return; + tricycle_analyze_expr(ctx, stmt->as.expr); +} + +static void analyze_if_stmt(TricycleContext *ctx, Stmt *stmt) { + if (!ctx || !stmt) return; + + // Analyze condition + if (stmt->as.if_stmt.condition) { + tricycle_analyze_expr(ctx, stmt->as.if_stmt.condition); + } + + // For control flow sensitivity, we'd need to track state snapshots + // For now, analyze both branches independently + + // Analyze then branch + if (stmt->as.if_stmt.then_branch) { + tricycle_push_scope(ctx, SCOPE_IF); + tricycle_analyze_stmt(ctx, stmt->as.if_stmt.then_branch); + tricycle_pop_scope(ctx); + } + + // Analyze else branch + if (stmt->as.if_stmt.else_branch) { + tricycle_push_scope(ctx, SCOPE_IF); + tricycle_analyze_stmt(ctx, stmt->as.if_stmt.else_branch); + tricycle_pop_scope(ctx); + } +} + +static void analyze_while_stmt(TricycleContext *ctx, Stmt *stmt) { + if (!ctx || !stmt) return; + + // For loop/while statements + if (stmt->type == STMT_WHILE) { + if (stmt->as.while_stmt.condition) { + tricycle_analyze_expr(ctx, stmt->as.while_stmt.condition); + } + + tricycle_push_scope_labeled(ctx, SCOPE_LOOP, stmt->as.while_stmt.label); + if (stmt->as.while_stmt.body) { + tricycle_analyze_stmt(ctx, stmt->as.while_stmt.body); + } + tricycle_pop_scope(ctx); + } else if (stmt->type == STMT_LOOP) { + tricycle_push_scope_labeled(ctx, SCOPE_LOOP, stmt->as.loop_stmt.label); + if (stmt->as.loop_stmt.body) { + tricycle_analyze_stmt(ctx, stmt->as.loop_stmt.body); + } + tricycle_pop_scope(ctx); + } +} + +static void analyze_for_stmt(TricycleContext *ctx, Stmt *stmt) { + if (!ctx || !stmt) return; + + if (stmt->type == STMT_FOR) { + tricycle_push_scope_labeled(ctx, SCOPE_LOOP, stmt->as.for_loop.label); + + // Analyze initializer + if (stmt->as.for_loop.initializer) { + tricycle_analyze_stmt(ctx, stmt->as.for_loop.initializer); + } + + // Analyze condition + if (stmt->as.for_loop.condition) { + tricycle_analyze_expr(ctx, stmt->as.for_loop.condition); + } + + // Analyze increment + if (stmt->as.for_loop.increment) { + tricycle_analyze_expr(ctx, stmt->as.for_loop.increment); + } + + // Analyze body + if (stmt->as.for_loop.body) { + tricycle_analyze_stmt(ctx, stmt->as.for_loop.body); + } + + tricycle_pop_scope(ctx); + } else if (stmt->type == STMT_FOR_IN) { + // Analyze iterable + if (stmt->as.for_in.iterable) { + tricycle_analyze_expr(ctx, stmt->as.for_in.iterable); + } + + tricycle_push_scope_labeled(ctx, SCOPE_LOOP, stmt->as.for_in.label); + + // Bind iteration variables + if (stmt->as.for_in.key_var) { + tricycle_bind(ctx, stmt->as.for_in.key_var, MEM_UNKNOWN, + stmt->line, stmt->column, 0); + } + if (stmt->as.for_in.value_var) { + tricycle_bind(ctx, stmt->as.for_in.value_var, MEM_UNKNOWN, + stmt->line, stmt->column, 0); + } + + // Analyze body + if (stmt->as.for_in.body) { + tricycle_analyze_stmt(ctx, stmt->as.for_in.body); + } + + tricycle_pop_scope(ctx); + } +} + +static void analyze_block_stmt(TricycleContext *ctx, Stmt *stmt) { + if (!ctx || !stmt) return; + + tricycle_push_scope(ctx, SCOPE_BLOCK); + + for (int i = 0; i < stmt->as.block.count; i++) { + if (stmt->as.block.statements[i]) { + tricycle_analyze_stmt(ctx, stmt->as.block.statements[i]); + } + } + + tricycle_pop_scope(ctx); +} + +static void analyze_return_stmt(TricycleContext *ctx, Stmt *stmt) { + if (!ctx || !stmt) return; + + if (stmt->as.return_stmt.value) { + tricycle_analyze_expr(ctx, stmt->as.return_stmt.value); + + // If returning a pointer, ownership transfers to caller + Expr *value = stmt->as.return_stmt.value; + if (value->type == EXPR_IDENT) { + const char *name = value->as.ident.name; + OwnershipBinding *binding = tricycle_lookup(ctx, name); + if (binding && binding->is_ptr_type && binding->state.kind == MEM_ALLOCATED) { + // Mark as moved (ownership to caller) + tricycle_update_state(ctx, name, MEM_MOVED, stmt->line, stmt->column); + } + } + } +} + +// ========== EXPRESSION ANALYSIS ========== + +void tricycle_analyze_expr(TricycleContext *ctx, Expr *expr) { + if (!ctx || !expr) return; + + switch (expr->type) { + case EXPR_IDENT: + // Check for use of freed/moved memory + tricycle_check_use(ctx, expr->as.ident.name, expr->line, expr->column); + break; + + case EXPR_CALL: + analyze_call_expr(ctx, expr); + break; + + case EXPR_ASSIGN: + analyze_assign_expr(ctx, expr); + break; + + case EXPR_FUNCTION: + analyze_function_expr(ctx, expr); + break; + + case EXPR_BINARY: + tricycle_analyze_expr(ctx, expr->as.binary.left); + tricycle_analyze_expr(ctx, expr->as.binary.right); + break; + + case EXPR_UNARY: + tricycle_analyze_expr(ctx, expr->as.unary.operand); + break; + + case EXPR_TERNARY: + tricycle_analyze_expr(ctx, expr->as.ternary.condition); + tricycle_analyze_expr(ctx, expr->as.ternary.true_expr); + tricycle_analyze_expr(ctx, expr->as.ternary.false_expr); + break; + + case EXPR_INDEX: + tricycle_analyze_expr(ctx, expr->as.index.object); + tricycle_analyze_expr(ctx, expr->as.index.index); + break; + + case EXPR_INDEX_ASSIGN: + tricycle_analyze_expr(ctx, expr->as.index_assign.object); + tricycle_analyze_expr(ctx, expr->as.index_assign.index); + tricycle_analyze_expr(ctx, expr->as.index_assign.value); + break; + + case EXPR_GET_PROPERTY: + tricycle_analyze_expr(ctx, expr->as.get_property.object); + break; + + case EXPR_SET_PROPERTY: + tricycle_analyze_expr(ctx, expr->as.set_property.object); + tricycle_analyze_expr(ctx, expr->as.set_property.value); + break; + + case EXPR_ARRAY_LITERAL: + for (int i = 0; i < expr->as.array_literal.num_elements; i++) { + if (expr->as.array_literal.elements[i]) { + tricycle_analyze_expr(ctx, expr->as.array_literal.elements[i]); + } + } + break; + + case EXPR_OBJECT_LITERAL: + for (int i = 0; i < expr->as.object_literal.num_fields; i++) { + if (expr->as.object_literal.field_values[i]) { + tricycle_analyze_expr(ctx, expr->as.object_literal.field_values[i]); + } + } + break; + + case EXPR_PREFIX_INC: + case EXPR_PREFIX_DEC: + tricycle_analyze_expr(ctx, expr->as.prefix_inc.operand); + break; + + case EXPR_POSTFIX_INC: + case EXPR_POSTFIX_DEC: + tricycle_analyze_expr(ctx, expr->as.postfix_inc.operand); + break; + + case EXPR_AWAIT: + tricycle_analyze_expr(ctx, expr->as.await_expr.awaited_expr); + break; + + case EXPR_STRING_INTERPOLATION: + for (int i = 0; i < expr->as.string_interpolation.num_parts; i++) { + if (expr->as.string_interpolation.expr_parts[i]) { + tricycle_analyze_expr(ctx, expr->as.string_interpolation.expr_parts[i]); + } + } + break; + + case EXPR_OPTIONAL_CHAIN: + if (expr->as.optional_chain.object) { + tricycle_analyze_expr(ctx, expr->as.optional_chain.object); + } + if (expr->as.optional_chain.index) { + tricycle_analyze_expr(ctx, expr->as.optional_chain.index); + } + for (int i = 0; i < expr->as.optional_chain.num_args; i++) { + if (expr->as.optional_chain.args[i]) { + tricycle_analyze_expr(ctx, expr->as.optional_chain.args[i]); + } + } + break; + + case EXPR_NULL_COALESCE: + tricycle_analyze_expr(ctx, expr->as.null_coalesce.left); + tricycle_analyze_expr(ctx, expr->as.null_coalesce.right); + break; + + case EXPR_MATCH: + if (expr->as.match_expr.scrutinee) { + tricycle_analyze_expr(ctx, expr->as.match_expr.scrutinee); + } + for (int i = 0; i < expr->as.match_expr.num_arms; i++) { + MatchArm *arm = &expr->as.match_expr.arms[i]; + if (arm->guard) { + tricycle_analyze_expr(ctx, arm->guard); + } + if (arm->body) { + tricycle_analyze_expr(ctx, arm->body); + } + } + break; + + case EXPR_NUMBER: + case EXPR_BOOL: + case EXPR_STRING: + case EXPR_RUNE: + case EXPR_NULL: + // Literals don't need memory analysis + break; + + default: + break; + } +} + +static void analyze_call_expr(TricycleContext *ctx, Expr *expr) { + if (!ctx || !expr || expr->type != EXPR_CALL) return; + + // Analyze function expression + if (expr->as.call.func) { + tricycle_analyze_expr(ctx, expr->as.call.func); + } + + // Check for special calls + if (tricycle_is_free_call(expr)) { + // Check for double-free + tricycle_check_free(ctx, expr, expr->line, expr->column); + return; + } + + if (tricycle_is_spawn_call(expr)) { + // Check async safety + tricycle_check_spawn(ctx, expr, expr->line, expr->column); + } + + if (tricycle_is_join_call(expr)) { + // Handle join - releases async borrows + tricycle_check_join(ctx, expr, expr->line, expr->column); + } + + if (tricycle_is_ptr_op(expr)) { + // Pointer operation - check the pointer argument is valid + if (expr->as.call.num_args > 0 && expr->as.call.args[0]) { + Expr *ptr_arg = expr->as.call.args[0]; + if (ptr_arg->type == EXPR_IDENT) { + tricycle_check_use(ctx, ptr_arg->as.ident.name, expr->line, expr->column); + } + } + } + + // Analyze all arguments + for (int i = 0; i < expr->as.call.num_args; i++) { + if (expr->as.call.args[i]) { + tricycle_analyze_expr(ctx, expr->as.call.args[i]); + } + } +} + +static void analyze_assign_expr(TricycleContext *ctx, Expr *expr) { + if (!ctx || !expr || expr->type != EXPR_ASSIGN) return; + + const char *name = expr->as.assign.name; + Expr *value = expr->as.assign.value; + + // Analyze the value first + if (value) { + tricycle_analyze_expr(ctx, value); + } + + OwnershipBinding *binding = tricycle_lookup(ctx, name); + if (!binding) { + // New variable in this scope - bind it + if (tricycle_is_alloc_call(value)) { + tricycle_bind(ctx, name, MEM_ALLOCATED, expr->line, expr->column, 1); + } else if (value && value->type == EXPR_IDENT) { + const char *source_name = value->as.ident.name; + OwnershipBinding *source = tricycle_lookup(ctx, source_name); + if (source && source->is_ptr_type && source->state.kind == MEM_ALLOCATED) { + tricycle_bind(ctx, name, MEM_ALLOCATED, expr->line, expr->column, 1); + tricycle_mark_moved(ctx, source_name, name, expr->line, expr->column); + } else { + tricycle_bind(ctx, name, MEM_UNKNOWN, expr->line, expr->column, 0); + } + } else { + tricycle_bind(ctx, name, MEM_UNKNOWN, expr->line, expr->column, + tricycle_is_ptr_expr(ctx, value)); + } + return; + } + + // Existing variable - check for memory leak if overwriting allocated memory + if (binding->is_ptr_type && binding->state.kind == MEM_ALLOCATED) { + tricycle_warning(ctx, TRIC_MEMORY_LEAK, expr->line, expr->column, + "overwriting '%s' without freeing previously allocated memory", name); + tricycle_add_related(ctx, binding->state.allocation_line, + binding->state.allocation_column, "allocated here"); + tricycle_add_hint(ctx, "call free() before reassigning"); + } + + // Update state based on new value + if (tricycle_is_alloc_call(value)) { + tricycle_update_state(ctx, name, MEM_ALLOCATED, expr->line, expr->column); + } else if (value && value->type == EXPR_IDENT) { + const char *source_name = value->as.ident.name; + OwnershipBinding *source = tricycle_lookup(ctx, source_name); + if (source && source->is_ptr_type && source->state.kind == MEM_ALLOCATED) { + tricycle_update_state(ctx, name, MEM_ALLOCATED, expr->line, expr->column); + tricycle_mark_moved(ctx, source_name, name, expr->line, expr->column); + } + } else if (value && value->type == EXPR_NULL) { + tricycle_update_state(ctx, name, MEM_UNALLOCATED, expr->line, expr->column); + } +} + +static void analyze_function_expr(TricycleContext *ctx, Expr *expr) { + if (!ctx || !expr || expr->type != EXPR_FUNCTION) return; + + // Push function scope + tricycle_push_scope(ctx, SCOPE_FUNCTION); + + // Bind parameters + for (int i = 0; i < expr->as.function.num_params; i++) { + const char *param_name = expr->as.function.param_names[i]; + Type *param_type = expr->as.function.param_types[i]; + int is_ptr = tricycle_is_ptr_type(param_type); + + // For now, treat all params as borrowed by default + tricycle_bind_param(ctx, param_name, OWNER_BORROW, expr->line, expr->column, is_ptr); + } + + // Analyze function body + if (expr->as.function.body) { + tricycle_analyze_stmt(ctx, expr->as.function.body); + } + + // Pop function scope + tricycle_pop_scope(ctx); +} + +// ========== SPECIFIC CHECKS ========== + +void tricycle_check_free(TricycleContext *ctx, Expr *call_expr, int line, int column) { + if (!ctx || !call_expr || call_expr->type != EXPR_CALL) return; + if (call_expr->as.call.num_args < 1) return; + + Expr *ptr_arg = call_expr->as.call.args[0]; + const char *var_name = tricycle_get_var_name(ptr_arg); + if (!var_name) { + // Complex expression - analyze it + tricycle_analyze_expr(ctx, ptr_arg); + return; + } + + OwnershipBinding *binding = tricycle_lookup(ctx, var_name); + if (!binding) { + // Unknown variable - might be okay + return; + } + + if (!binding->is_ptr_type) { + // Not a pointer - freeing non-pointer is an error + tricycle_error(ctx, TRIC_INVALID_OWNERSHIP, line, column, + "freeing non-pointer value '%s'", var_name); + return; + } + + switch (binding->state.kind) { + case MEM_UNALLOCATED: + // Double free! + tricycle_error(ctx, TRIC_DOUBLE_FREE, line, column, + "double free detected for '%s'", var_name); + tricycle_add_related(ctx, binding->state.allocation_line, + binding->state.allocation_column, "allocated here"); + tricycle_add_related(ctx, binding->state.free_line, + binding->state.free_column, "first freed here"); + tricycle_add_hint(ctx, "remove this free() call - memory was already freed"); + break; + + case MEM_MOVED: + // Use after move + tricycle_error(ctx, TRIC_USE_AFTER_MOVE, line, column, + "freeing moved value '%s'", var_name); + tricycle_add_related(ctx, binding->state.move_line, + binding->state.move_column, "moved here"); + if (binding->state.moved_to) { + char hint[256]; + snprintf(hint, sizeof(hint), "free '%s' instead, which now owns the memory", + binding->state.moved_to); + tricycle_add_hint(ctx, hint); + } + break; + + case MEM_BORROWED: + // Freeing borrowed memory + tricycle_error(ctx, TRIC_FREE_OF_BORROWED, line, column, + "freeing borrowed value '%s'", var_name); + tricycle_add_hint(ctx, "borrowed values should not be freed - the owner is responsible"); + break; + + case MEM_ALLOCATED: + // Valid free - mark as unallocated + tricycle_update_state(ctx, var_name, MEM_UNALLOCATED, line, column); + + // Check if this memory is borrowed by an async task + AsyncBorrow *async = ctx->async_borrows; + while (async) { + if (strcmp(async->var_name, var_name) == 0 && !async->is_joined) { + tricycle_error(ctx, TRIC_DANGLING_ASYNC, line, column, + "freeing '%s' while async task may still use it", var_name); + tricycle_add_related(ctx, async->spawn_line, async->spawn_column, + "passed to spawned task here"); + tricycle_add_hint(ctx, "call join() on the task before freeing"); + break; + } + async = async->next; + } + break; + + case MEM_UNKNOWN: + // Unknown state - warn + tricycle_warning(ctx, TRIC_UNKNOWN_LIFETIME, line, column, + "freeing '%s' with unknown ownership", var_name); + tricycle_add_hint(ctx, "ensure this memory was allocated and is owned by this code"); + tricycle_update_state(ctx, var_name, MEM_UNALLOCATED, line, column); + break; + } +} + +void tricycle_check_use(TricycleContext *ctx, const char *var_name, int line, int column) { + if (!ctx || !var_name) return; + + OwnershipBinding *binding = tricycle_lookup(ctx, var_name); + if (!binding || !binding->is_ptr_type) return; + + switch (binding->state.kind) { + case MEM_UNALLOCATED: + tricycle_error(ctx, TRIC_USE_AFTER_FREE, line, column, + "use of freed memory '%s'", var_name); + tricycle_add_related(ctx, binding->state.allocation_line, + binding->state.allocation_column, "allocated here"); + tricycle_add_related(ctx, binding->state.free_line, + binding->state.free_column, "freed here"); + tricycle_add_hint(ctx, "do not use memory after it has been freed"); + break; + + case MEM_MOVED: + tricycle_error(ctx, TRIC_USE_AFTER_MOVE, line, column, + "use of moved value '%s'", var_name); + tricycle_add_related(ctx, binding->state.move_line, + binding->state.move_column, "moved here"); + if (binding->state.moved_to) { + char hint[256]; + snprintf(hint, sizeof(hint), "use '%s' instead, which now owns the memory", + binding->state.moved_to); + tricycle_add_hint(ctx, hint); + } + break; + + default: + // Valid use + break; + } +} + +void tricycle_check_leaks(TricycleContext *ctx) { + if (!ctx || !ctx->current_env) return; + + // Check for leaks in any scope - memory should be freed before scope ends + // This handles variables declared in blocks, loops, etc. + // Note: We only warn (not error) since the memory will eventually be freed + // when the program exits, but it's still a resource management issue. + + OwnershipBinding *binding = ctx->current_env->bindings; + while (binding) { + if (binding->is_ptr_type && binding->state.kind == MEM_ALLOCATED) { + // Memory allocated but not freed before scope ends + tricycle_warning(ctx, TRIC_MEMORY_LEAK, binding->declaration_line, + binding->declaration_column, + "memory leak: '%s' is never freed", binding->name); + tricycle_add_related(ctx, binding->state.allocation_line, + binding->state.allocation_column, "allocated here"); + tricycle_add_hint(ctx, "add free() before the scope ends, or return the pointer to transfer ownership"); + } + binding = binding->next; + } +} + +void tricycle_check_spawn(TricycleContext *ctx, Expr *call_expr, int line, int column) { + if (!ctx || !call_expr) return; + + // Check pointer arguments passed to spawn + static int task_counter = 0; + task_counter++; + + for (int i = 1; i < call_expr->as.call.num_args; i++) { + Expr *arg = call_expr->as.call.args[i]; + const char *var_name = tricycle_get_var_name(arg); + if (!var_name) continue; + + OwnershipBinding *binding = tricycle_lookup(ctx, var_name); + if (!binding || !binding->is_ptr_type) continue; + + if (binding->state.kind != MEM_ALLOCATED) { + tricycle_error(ctx, TRIC_DANGLING_ASYNC, line, column, + "passing invalid pointer '%s' to spawned task", var_name); + if (binding->state.kind == MEM_UNALLOCATED) { + tricycle_add_related(ctx, binding->state.free_line, + binding->state.free_column, "freed here"); + } else if (binding->state.kind == MEM_MOVED) { + tricycle_add_related(ctx, binding->state.move_line, + binding->state.move_column, "moved here"); + } + continue; + } + + // Add async borrow record + AsyncBorrow *borrow = calloc(1, sizeof(AsyncBorrow)); + if (borrow) { + borrow->var_name = strdup(var_name); + borrow->spawn_line = line; + borrow->spawn_column = column; + borrow->task_id = task_counter; + borrow->is_joined = 0; + borrow->next = ctx->async_borrows; + ctx->async_borrows = borrow; + } + } +} + +void tricycle_check_join(TricycleContext *ctx, Expr *task_expr, int line, int column) { + (void)task_expr; + (void)line; + (void)column; + + if (!ctx) return; + + // Mark all async borrows as joined + // (In a more sophisticated analysis, we'd track which task is being joined) + AsyncBorrow *borrow = ctx->async_borrows; + while (borrow) { + borrow->is_joined = 1; + borrow = borrow->next; + } +} diff --git a/src/backends/tricycle/tricycle.c b/src/backends/tricycle/tricycle.c new file mode 100644 index 00000000..c0ce1d33 --- /dev/null +++ b/src/backends/tricycle/tricycle.c @@ -0,0 +1,677 @@ +/* + * Tricycle - Hemlock's Memory Safety Checker + * + * Main implementation file containing context management, + * environment operations, and analysis entry points. + */ + +#include +#include +#include +#include +#include "tricycle/tricycle.h" +#include "hemlock_limits.h" + +// ========== ERROR CODE STRINGS ========== + +static const char* error_codes[] = { + "TRIC001", // TRIC_DOUBLE_FREE + "TRIC002", // TRIC_USE_AFTER_FREE + "TRIC003", // TRIC_USE_AFTER_MOVE + "TRIC004", // TRIC_MEMORY_LEAK + "TRIC005", // TRIC_NULL_DEREF + "TRIC006", // TRIC_DANGLING_ASYNC + "TRIC007", // TRIC_BORROW_OUTLIVES_OWNER + "TRIC008", // TRIC_MOVE_OF_BORROWED + "TRIC009", // TRIC_FREE_OF_BORROWED + "TRIC010", // TRIC_UNKNOWN_LIFETIME + "TRIC011", // TRIC_INVALID_OWNERSHIP +}; + +static const char* error_names[] = { + "DOUBLE_FREE", + "USE_AFTER_FREE", + "USE_AFTER_MOVE", + "MEMORY_LEAK", + "NULL_DEREF", + "DANGLING_ASYNC", + "BORROW_OUTLIVES_OWNER", + "MOVE_OF_BORROWED", + "FREE_OF_BORROWED", + "UNKNOWN_LIFETIME", + "INVALID_OWNERSHIP", +}; + +// ========== CONTEXT MANAGEMENT ========== + +TricycleContext* tricycle_new(const char *filename) { + TricycleContext *ctx = calloc(1, sizeof(TricycleContext)); + if (!ctx) return NULL; + + ctx->filename = filename ? strdup(filename) : NULL; + ctx->source = NULL; + ctx->program = NULL; + ctx->stmt_count = 0; + ctx->type_ctx = NULL; + + // Initialize ownership environment with global scope + ctx->current_env = calloc(1, sizeof(OwnershipEnv)); + if (ctx->current_env) { + ctx->current_env->bindings = NULL; + ctx->current_env->parent = NULL; + ctx->current_env->scope_kind = SCOPE_GLOBAL; + ctx->current_env->scope_label = NULL; + ctx->current_env->scope_start_line = 0; + } + + ctx->async_borrows = NULL; + ctx->diagnostics = NULL; + ctx->diagnostics_tail = NULL; + ctx->error_count = 0; + ctx->warning_count = 0; + ctx->strict_mode = 0; + ctx->infer_ownership = 1; + ctx->verbose = 0; + ctx->collect_all = 0; + + return ctx; +} + +void tricycle_free(TricycleContext *ctx) { + if (!ctx) return; + + // Free filename + if (ctx->filename) { + free((char*)ctx->filename); + } + + // Free all ownership environments + while (ctx->current_env) { + OwnershipEnv *parent = ctx->current_env->parent; + + // Free bindings in current scope + OwnershipBinding *binding = ctx->current_env->bindings; + while (binding) { + OwnershipBinding *next = binding->next; + if (binding->name) free(binding->name); + tricycle_free_state(&binding->state); + free(binding); + binding = next; + } + + if (ctx->current_env->scope_label) { + free(ctx->current_env->scope_label); + } + + free(ctx->current_env); + ctx->current_env = parent; + } + + // Free async borrows + AsyncBorrow *borrow = ctx->async_borrows; + while (borrow) { + AsyncBorrow *next = borrow->next; + if (borrow->var_name) free(borrow->var_name); + free(borrow); + borrow = next; + } + + // Free diagnostics + tricycle_free_diagnostics(ctx); + + free(ctx); +} + +void tricycle_set_source(TricycleContext *ctx, const char *source) { + if (ctx) { + ctx->source = source; + } +} + +void tricycle_set_type_ctx(TricycleContext *ctx, TypeCheckContext *type_ctx) { + if (ctx) { + ctx->type_ctx = type_ctx; + } +} + +// ========== ENVIRONMENT OPERATIONS ========== + +void tricycle_push_scope(TricycleContext *ctx, ScopeKind kind) { + tricycle_push_scope_labeled(ctx, kind, NULL); +} + +void tricycle_push_scope_labeled(TricycleContext *ctx, ScopeKind kind, const char *label) { + if (!ctx) return; + + OwnershipEnv *new_env = calloc(1, sizeof(OwnershipEnv)); + if (!new_env) return; + + new_env->bindings = NULL; + new_env->parent = ctx->current_env; + new_env->scope_kind = kind; + new_env->scope_label = label ? strdup(label) : NULL; + new_env->scope_start_line = 0; + + ctx->current_env = new_env; +} + +void tricycle_pop_scope(TricycleContext *ctx) { + if (!ctx || !ctx->current_env) return; + + // Check for memory leaks before popping + tricycle_check_leaks(ctx); + + OwnershipEnv *old_env = ctx->current_env; + ctx->current_env = old_env->parent; + + // Free bindings + OwnershipBinding *binding = old_env->bindings; + while (binding) { + OwnershipBinding *next = binding->next; + if (binding->name) free(binding->name); + tricycle_free_state(&binding->state); + free(binding); + binding = next; + } + + if (old_env->scope_label) { + free(old_env->scope_label); + } + + free(old_env); +} + +void tricycle_bind(TricycleContext *ctx, const char *name, MemoryStateKind state, + int line, int column, int is_ptr_type) { + if (!ctx || !ctx->current_env || !name) return; + + OwnershipBinding *binding = calloc(1, sizeof(OwnershipBinding)); + if (!binding) return; + + binding->name = strdup(name); + binding->state.kind = state; + binding->state.allocation_line = (state == MEM_ALLOCATED) ? line : 0; + binding->state.allocation_column = (state == MEM_ALLOCATED) ? column : 0; + binding->state.free_line = 0; + binding->state.free_column = 0; + binding->state.move_line = 0; + binding->state.move_column = 0; + binding->state.owner_name = strdup(name); + binding->state.moved_to = NULL; + binding->state.borrow_count = 0; + binding->is_parameter = 0; + binding->param_kind = OWNER_DEFAULT; + binding->declaration_line = line; + binding->declaration_column = column; + binding->is_ptr_type = is_ptr_type; + + // Add to front of binding list + binding->next = ctx->current_env->bindings; + ctx->current_env->bindings = binding; +} + +void tricycle_bind_param(TricycleContext *ctx, const char *name, OwnershipKind kind, + int line, int column, int is_ptr_type) { + if (!ctx || !ctx->current_env || !name) return; + + OwnershipBinding *binding = calloc(1, sizeof(OwnershipBinding)); + if (!binding) return; + + binding->name = strdup(name); + + // Parameters with OWN take ownership, BORROW means borrowed + if (kind == OWNER_OWN) { + binding->state.kind = MEM_ALLOCATED; + } else if (kind == OWNER_BORROW || kind == OWNER_BORROW_MUT) { + binding->state.kind = MEM_BORROWED; + } else { + // Default for pointer params: borrow + binding->state.kind = is_ptr_type ? MEM_BORROWED : MEM_UNKNOWN; + } + + binding->state.allocation_line = 0; + binding->state.allocation_column = 0; + binding->state.owner_name = NULL; + binding->state.moved_to = NULL; + binding->state.borrow_count = 0; + binding->is_parameter = 1; + binding->param_kind = kind; + binding->declaration_line = line; + binding->declaration_column = column; + binding->is_ptr_type = is_ptr_type; + + binding->next = ctx->current_env->bindings; + ctx->current_env->bindings = binding; +} + +OwnershipBinding* tricycle_lookup(TricycleContext *ctx, const char *name) { + if (!ctx || !name) return NULL; + + OwnershipEnv *env = ctx->current_env; + while (env) { + OwnershipBinding *binding = env->bindings; + while (binding) { + if (strcmp(binding->name, name) == 0) { + return binding; + } + binding = binding->next; + } + env = env->parent; + } + + return NULL; +} + +void tricycle_update_state(TricycleContext *ctx, const char *name, + MemoryStateKind new_state, int line, int column) { + OwnershipBinding *binding = tricycle_lookup(ctx, name); + if (!binding) return; + + MemoryStateKind old_state = binding->state.kind; + binding->state.kind = new_state; + + if (new_state == MEM_UNALLOCATED) { + binding->state.free_line = line; + binding->state.free_column = column; + } else if (new_state == MEM_MOVED) { + binding->state.move_line = line; + binding->state.move_column = column; + } else if (new_state == MEM_ALLOCATED && old_state != MEM_ALLOCATED) { + binding->state.allocation_line = line; + binding->state.allocation_column = column; + } +} + +void tricycle_mark_moved(TricycleContext *ctx, const char *from, const char *to, + int line, int column) { + OwnershipBinding *from_binding = tricycle_lookup(ctx, from); + if (!from_binding) return; + + from_binding->state.kind = MEM_MOVED; + from_binding->state.move_line = line; + from_binding->state.move_column = column; + if (from_binding->state.moved_to) { + free(from_binding->state.moved_to); + } + from_binding->state.moved_to = strdup(to); +} + +// ========== MEMORY STATE UTILITIES ========== + +MemoryState tricycle_clone_state(const MemoryState *state) { + MemoryState clone = {0}; + if (!state) return clone; + + clone.kind = state->kind; + clone.allocation_line = state->allocation_line; + clone.allocation_column = state->allocation_column; + clone.free_line = state->free_line; + clone.free_column = state->free_column; + clone.move_line = state->move_line; + clone.move_column = state->move_column; + clone.owner_name = state->owner_name ? strdup(state->owner_name) : NULL; + clone.moved_to = state->moved_to ? strdup(state->moved_to) : NULL; + clone.borrow_count = state->borrow_count; + + return clone; +} + +void tricycle_free_state(MemoryState *state) { + if (!state) return; + if (state->owner_name) { + free(state->owner_name); + state->owner_name = NULL; + } + if (state->moved_to) { + free(state->moved_to); + state->moved_to = NULL; + } +} + +// ========== ERROR CODE HELPERS ========== + +const char* tricycle_error_code(TricycleErrorKind kind) { + if (kind >= 0 && kind < (int)(sizeof(error_codes) / sizeof(error_codes[0]))) { + return error_codes[kind]; + } + return "TRIC???"; +} + +const char* tricycle_error_name(TricycleErrorKind kind) { + if (kind >= 0 && kind < (int)(sizeof(error_names) / sizeof(error_names[0]))) { + return error_names[kind]; + } + return "UNKNOWN"; +} + +const char* tricycle_severity_str(TricycleSeverity severity) { + switch (severity) { + case TRIC_ERROR: return "error"; + case TRIC_WARNING: return "warning"; + case TRIC_NOTE: return "note"; + default: return "unknown"; + } +} + +// ========== DIAGNOSTICS ========== + +static TricycleDiagnostic* create_diagnostic(TricycleSeverity severity, + TricycleErrorKind kind, + int line, int column, + const char *message) { + TricycleDiagnostic *diag = calloc(1, sizeof(TricycleDiagnostic)); + if (!diag) return NULL; + + diag->severity = severity; + diag->kind = kind; + diag->line = line; + diag->column = column; + diag->end_column = column; + diag->message = message ? strdup(message) : NULL; + diag->hint = NULL; + diag->related = NULL; + diag->related_count = 0; + diag->next = NULL; + + return diag; +} + +static void add_diagnostic(TricycleContext *ctx, TricycleDiagnostic *diag) { + if (!ctx || !diag) return; + + if (ctx->diagnostics_tail) { + ctx->diagnostics_tail->next = diag; + } else { + ctx->diagnostics = diag; + } + ctx->diagnostics_tail = diag; + + if (diag->severity == TRIC_ERROR) { + ctx->error_count++; + } else if (diag->severity == TRIC_WARNING) { + ctx->warning_count++; + } +} + +void tricycle_error(TricycleContext *ctx, TricycleErrorKind kind, + int line, int column, const char *fmt, ...) { + if (!ctx) return; + + char message[1024]; + va_list args; + va_start(args, fmt); + vsnprintf(message, sizeof(message), fmt, args); + va_end(args); + + TricycleDiagnostic *diag = create_diagnostic(TRIC_ERROR, kind, line, column, message); + add_diagnostic(ctx, diag); +} + +void tricycle_warning(TricycleContext *ctx, TricycleErrorKind kind, + int line, int column, const char *fmt, ...) { + if (!ctx) return; + + char message[1024]; + va_list args; + va_start(args, fmt); + vsnprintf(message, sizeof(message), fmt, args); + va_end(args); + + TricycleDiagnostic *diag = create_diagnostic(TRIC_WARNING, kind, line, column, message); + add_diagnostic(ctx, diag); +} + +void tricycle_note(TricycleContext *ctx, int line, int column, const char *fmt, ...) { + if (!ctx) return; + + char message[1024]; + va_list args; + va_start(args, fmt); + vsnprintf(message, sizeof(message), fmt, args); + va_end(args); + + // Notes are attached as additional diagnostics + TricycleDiagnostic *diag = create_diagnostic(TRIC_NOTE, TRIC_UNKNOWN_LIFETIME, line, column, message); + add_diagnostic(ctx, diag); +} + +void tricycle_add_related(TricycleContext *ctx, int line, int column, const char *message) { + if (!ctx || !ctx->diagnostics_tail) return; + + TricycleDiagnostic *diag = ctx->diagnostics_tail; + + // Grow related locations array + int new_count = diag->related_count + 1; + RelatedLocation *new_related = realloc(diag->related, new_count * sizeof(RelatedLocation)); + if (!new_related) return; + + diag->related = new_related; + diag->related[diag->related_count].line = line; + diag->related[diag->related_count].column = column; + diag->related[diag->related_count].message = message ? strdup(message) : NULL; + diag->related_count = new_count; +} + +void tricycle_add_hint(TricycleContext *ctx, const char *hint) { + if (!ctx || !ctx->diagnostics_tail || !hint) return; + + if (ctx->diagnostics_tail->hint) { + free(ctx->diagnostics_tail->hint); + } + ctx->diagnostics_tail->hint = strdup(hint); +} + +TricycleDiagnostic* tricycle_get_diagnostics(TricycleContext *ctx) { + return ctx ? ctx->diagnostics : NULL; +} + +void tricycle_print_diagnostics(TricycleContext *ctx) { + if (!ctx) return; + + TricycleDiagnostic *diag = ctx->diagnostics; + while (diag) { + // Print main diagnostic + fprintf(stderr, "%s[%s]: %s\n", + tricycle_severity_str(diag->severity), + tricycle_error_code(diag->kind), + diag->message ? diag->message : ""); + + // Print location + fprintf(stderr, " --> %s:%d:%d\n", + ctx->filename ? ctx->filename : "", + diag->line, diag->column); + + // Print related locations + for (int i = 0; i < diag->related_count; i++) { + fprintf(stderr, " |\n"); + fprintf(stderr, "%3d| %s\n", + diag->related[i].line, + diag->related[i].message ? diag->related[i].message : ""); + } + + // Print hint + if (diag->hint) { + fprintf(stderr, " |\n"); + fprintf(stderr, " = help: %s\n", diag->hint); + } + + fprintf(stderr, "\n"); + diag = diag->next; + } +} + +void tricycle_print_diagnostics_json(TricycleContext *ctx) { + if (!ctx) return; + + printf("{\n"); + printf(" \"uri\": \"file://%s\",\n", ctx->filename ? ctx->filename : ""); + printf(" \"diagnostics\": [\n"); + + TricycleDiagnostic *diag = ctx->diagnostics; + int first = 1; + while (diag) { + if (!first) printf(",\n"); + first = 0; + + printf(" {\n"); + printf(" \"range\": {\n"); + printf(" \"start\": {\"line\": %d, \"character\": %d},\n", + diag->line - 1, diag->column); + printf(" \"end\": {\"line\": %d, \"character\": %d}\n", + diag->line - 1, diag->end_column); + printf(" },\n"); + printf(" \"severity\": %d,\n", diag->severity == TRIC_ERROR ? 1 : 2); + printf(" \"code\": \"%s\",\n", tricycle_error_code(diag->kind)); + printf(" \"source\": \"tricycle\",\n"); + printf(" \"message\": \"%s\"", diag->message ? diag->message : ""); + + if (diag->related_count > 0) { + printf(",\n \"relatedInformation\": [\n"); + for (int i = 0; i < diag->related_count; i++) { + if (i > 0) printf(",\n"); + printf(" {\n"); + printf(" \"location\": {\n"); + printf(" \"uri\": \"file://%s\",\n", ctx->filename ? ctx->filename : ""); + printf(" \"range\": {\n"); + printf(" \"start\": {\"line\": %d, \"character\": %d},\n", + diag->related[i].line - 1, diag->related[i].column); + printf(" \"end\": {\"line\": %d, \"character\": %d}\n", + diag->related[i].line - 1, diag->related[i].column); + printf(" }\n"); + printf(" },\n"); + printf(" \"message\": \"%s\"\n", + diag->related[i].message ? diag->related[i].message : ""); + printf(" }"); + } + printf("\n ]"); + } + + printf("\n }"); + diag = diag->next; + } + + printf("\n ]\n"); + printf("}\n"); +} + +void tricycle_free_diagnostics(TricycleContext *ctx) { + if (!ctx) return; + + TricycleDiagnostic *diag = ctx->diagnostics; + while (diag) { + TricycleDiagnostic *next = diag->next; + + if (diag->message) free(diag->message); + if (diag->hint) free(diag->hint); + + for (int i = 0; i < diag->related_count; i++) { + if (diag->related[i].message) { + free(diag->related[i].message); + } + } + if (diag->related) free(diag->related); + + free(diag); + diag = next; + } + + ctx->diagnostics = NULL; + ctx->diagnostics_tail = NULL; + ctx->error_count = 0; + ctx->warning_count = 0; +} + +// ========== UTILITY FUNCTIONS ========== + +int tricycle_is_ptr_type(Type *type) { + if (!type) return 0; + return type->kind == TYPE_PTR || type->kind == TYPE_BUFFER; +} + +const char* tricycle_get_var_name(Expr *expr) { + if (!expr) return NULL; + if (expr->type == EXPR_IDENT) { + return expr->as.ident.name; + } + return NULL; +} + +int tricycle_is_alloc_call(Expr *call_expr) { + if (!call_expr || call_expr->type != EXPR_CALL) return 0; + + Expr *func = call_expr->as.call.func; + if (!func || func->type != EXPR_IDENT) return 0; + + const char *name = func->as.ident.name; + return (strcmp(name, "alloc") == 0 || + strcmp(name, "buffer") == 0 || + strcmp(name, "talloc") == 0); +} + +int tricycle_is_free_call(Expr *call_expr) { + if (!call_expr || call_expr->type != EXPR_CALL) return 0; + + Expr *func = call_expr->as.call.func; + if (!func || func->type != EXPR_IDENT) return 0; + + const char *name = func->as.ident.name; + return strcmp(name, "free") == 0; +} + +int tricycle_is_spawn_call(Expr *call_expr) { + if (!call_expr || call_expr->type != EXPR_CALL) return 0; + + Expr *func = call_expr->as.call.func; + if (!func || func->type != EXPR_IDENT) return 0; + + const char *name = func->as.ident.name; + return strcmp(name, "spawn") == 0; +} + +int tricycle_is_join_call(Expr *call_expr) { + if (!call_expr || call_expr->type != EXPR_CALL) return 0; + + Expr *func = call_expr->as.call.func; + if (!func || func->type != EXPR_IDENT) return 0; + + const char *name = func->as.ident.name; + return (strcmp(name, "join") == 0 || strcmp(name, "await") == 0); +} + +int tricycle_is_ptr_op(Expr *call_expr) { + if (!call_expr || call_expr->type != EXPR_CALL) return 0; + + Expr *func = call_expr->as.call.func; + if (!func || func->type != EXPR_IDENT) return 0; + + const char *name = func->as.ident.name; + return (strncmp(name, "ptr_read_", 9) == 0 || + strncmp(name, "ptr_write_", 10) == 0 || + strcmp(name, "ptr_offset") == 0 || + strcmp(name, "memcpy") == 0 || + strcmp(name, "memset") == 0 || + strcmp(name, "memmove") == 0); +} + +int tricycle_is_ptr_expr(TricycleContext *ctx, Expr *expr) { + if (!expr) return 0; + + // Check if it's a call to alloc/buffer + if (tricycle_is_alloc_call(expr)) return 1; + + // Check if it's an identifier bound as a pointer + if (expr->type == EXPR_IDENT) { + OwnershipBinding *binding = tricycle_lookup(ctx, expr->as.ident.name); + if (binding && binding->is_ptr_type) return 1; + } + + // If we have type context, use it + if (ctx->type_ctx) { + CheckedType *type = type_check_infer_expr(ctx->type_ctx, expr); + if (type && (type->kind == CHECKED_PTR || type->kind == CHECKED_BUFFER)) { + return 1; + } + } + + return 0; +} diff --git a/tests/tricycle/ownership/double_free.expected b/tests/tricycle/ownership/double_free.expected new file mode 100644 index 00000000..541cc714 --- /dev/null +++ b/tests/tricycle/ownership/double_free.expected @@ -0,0 +1,2 @@ +TRIC001 +double free diff --git a/tests/tricycle/ownership/double_free.hml b/tests/tricycle/ownership/double_free.hml new file mode 100644 index 00000000..10b3475c --- /dev/null +++ b/tests/tricycle/ownership/double_free.hml @@ -0,0 +1,6 @@ +// Test: Double free detection +// This should report a TRIC001 error + +let p = alloc(64); +free(p); +free(p); // ERROR: double free diff --git a/tests/tricycle/ownership/free_after_move.expected b/tests/tricycle/ownership/free_after_move.expected new file mode 100644 index 00000000..f2253431 --- /dev/null +++ b/tests/tricycle/ownership/free_after_move.expected @@ -0,0 +1,2 @@ +TRIC003 +freeing moved value diff --git a/tests/tricycle/ownership/free_after_move.hml b/tests/tricycle/ownership/free_after_move.hml new file mode 100644 index 00000000..acd9085f --- /dev/null +++ b/tests/tricycle/ownership/free_after_move.hml @@ -0,0 +1,7 @@ +// Test: Freeing a moved value +// This should report a TRIC003 error + +let p: ptr = alloc(64); +let q = p; // Ownership moves to q +free(p); // ERROR: p was moved +free(q); // This is the correct free diff --git a/tests/tricycle/ownership/memory_leak.expected b/tests/tricycle/ownership/memory_leak.expected new file mode 100644 index 00000000..f8357ca5 --- /dev/null +++ b/tests/tricycle/ownership/memory_leak.expected @@ -0,0 +1,3 @@ +TRIC004 +memory leak +never freed diff --git a/tests/tricycle/ownership/memory_leak.hml b/tests/tricycle/ownership/memory_leak.hml new file mode 100644 index 00000000..9e9f60b5 --- /dev/null +++ b/tests/tricycle/ownership/memory_leak.hml @@ -0,0 +1,9 @@ +// Test: Memory leak detection +// This should report a TRIC004 warning + +fn leaky() { + let p = alloc(64); + // ERROR: p is never freed +} + +leaky(); diff --git a/tests/tricycle/ownership/ownership_transfer.hml b/tests/tricycle/ownership/ownership_transfer.hml new file mode 100644 index 00000000..82c71ab6 --- /dev/null +++ b/tests/tricycle/ownership/ownership_transfer.hml @@ -0,0 +1,7 @@ +// Test: Valid ownership transfer +// This should NOT report any errors + +let p: ptr = alloc(64); +let q = p; // Ownership transfers to q +ptr_write_i32(q, 42); +free(q); diff --git a/tests/tricycle/ownership/use_after_free.expected b/tests/tricycle/ownership/use_after_free.expected new file mode 100644 index 00000000..b9fa1fae --- /dev/null +++ b/tests/tricycle/ownership/use_after_free.expected @@ -0,0 +1,2 @@ +TRIC002 +use of freed memory diff --git a/tests/tricycle/ownership/use_after_free.hml b/tests/tricycle/ownership/use_after_free.hml new file mode 100644 index 00000000..b0d9fb90 --- /dev/null +++ b/tests/tricycle/ownership/use_after_free.hml @@ -0,0 +1,6 @@ +// Test: Use after free detection +// This should report a TRIC002 error + +let p = alloc(64); +free(p); +ptr_write_i32(p, 42); // ERROR: use after free diff --git a/tests/tricycle/ownership/use_after_move.expected b/tests/tricycle/ownership/use_after_move.expected new file mode 100644 index 00000000..b7b3574b --- /dev/null +++ b/tests/tricycle/ownership/use_after_move.expected @@ -0,0 +1,2 @@ +TRIC003 +use of moved value diff --git a/tests/tricycle/ownership/use_after_move.hml b/tests/tricycle/ownership/use_after_move.hml new file mode 100644 index 00000000..b5a43cca --- /dev/null +++ b/tests/tricycle/ownership/use_after_move.hml @@ -0,0 +1,7 @@ +// Test: Use after move detection +// This should report a TRIC003 error + +let p: ptr = alloc(64); +let q = p; // Ownership moves to q +ptr_write_i32(p, 42); // ERROR: use after move +free(q); diff --git a/tests/tricycle/ownership/valid_alloc_free.hml b/tests/tricycle/ownership/valid_alloc_free.hml new file mode 100644 index 00000000..3631ffdc --- /dev/null +++ b/tests/tricycle/ownership/valid_alloc_free.hml @@ -0,0 +1,8 @@ +// Test: Valid allocation and free +// This should NOT report any errors + +let p = alloc(64); +ptr_write_i32(p, 42); +let val = ptr_read_i32(p); +print(val); +free(p); diff --git a/tests/tricycle/run_tricycle_tests.sh b/tests/tricycle/run_tricycle_tests.sh new file mode 100755 index 00000000..95b30f32 --- /dev/null +++ b/tests/tricycle/run_tricycle_tests.sh @@ -0,0 +1,115 @@ +#!/bin/bash + +# Tricycle Memory Safety Test Runner +# +# This script runs all Tricycle tests and reports results. + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +HEMLOCK_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" +TRICYCLE="$HEMLOCK_ROOT/tricycle" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Counters +PASSED=0 +FAILED=0 +SKIPPED=0 + +# Test a single file +run_test() { + local test_file="$1" + local expected_file="${test_file%.hml}.expected" + local test_name=$(basename "$test_file" .hml) + + # Check if tricycle exists + if [ ! -x "$TRICYCLE" ]; then + echo -e "${YELLOW}SKIP${NC} $test_name (tricycle not built)" + ((SKIPPED++)) + return + fi + + # Run tricycle and capture output + local output + local exit_code + output=$("$TRICYCLE" check --all "$test_file" 2>&1) || exit_code=$? + exit_code=${exit_code:-0} + + # Check if expected file exists + if [ -f "$expected_file" ]; then + local expected + expected=$(cat "$expected_file") + + # Check for expected error codes in output + local all_found=true + while IFS= read -r line; do + # Skip empty lines and comments + [[ -z "$line" || "$line" =~ ^# ]] && continue + + # Check if the expected pattern is in the output + if ! echo "$output" | grep -q "$line"; then + all_found=false + break + fi + done < "$expected_file" + + if $all_found; then + echo -e "${GREEN}PASS${NC} $test_name" + ((PASSED++)) + else + echo -e "${RED}FAIL${NC} $test_name" + echo " Expected patterns from: $expected_file" + echo " Got output:" + echo "$output" | sed 's/^/ /' + ((FAILED++)) + fi + else + # No expected file - just check if it runs without crashing + if [ $exit_code -eq 2 ]; then + echo -e "${RED}FAIL${NC} $test_name (crashed or file not found)" + ((FAILED++)) + else + echo -e "${GREEN}PASS${NC} $test_name (no expected file, ran successfully)" + ((PASSED++)) + fi + fi +} + +# Main +echo "=== Tricycle Memory Safety Tests ===" +echo "" + +# Find all test files +TEST_DIRS=( + "$SCRIPT_DIR/ownership" +) + +for dir in "${TEST_DIRS[@]}"; do + if [ -d "$dir" ]; then + category=$(basename "$dir") + echo "--- $category ---" + + for test_file in "$dir"/*.hml; do + [ -f "$test_file" ] && run_test "$test_file" + done + echo "" + fi +done + +# Summary +echo "=== Summary ===" +echo -e "${GREEN}Passed:${NC} $PASSED" +echo -e "${RED}Failed:${NC} $FAILED" +echo -e "${YELLOW}Skipped:${NC} $SKIPPED" + +# Exit with failure if any tests failed +if [ $FAILED -gt 0 ]; then + exit 1 +fi + +exit 0 From dabe077bf31b3e706f778ad8b9ef0b72f9894248 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 20 Jan 2026 01:49:34 +0000 Subject: [PATCH 2/4] Fix memory leak in tricycle_is_ptr_expr The CheckedType returned by type_check_infer_expr was not being freed, causing a 96-byte leak per call. Now properly call checked_type_free() after checking the type kind. --- src/backends/tricycle/tricycle.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/backends/tricycle/tricycle.c b/src/backends/tricycle/tricycle.c index c0ce1d33..eff542ca 100644 --- a/src/backends/tricycle/tricycle.c +++ b/src/backends/tricycle/tricycle.c @@ -668,8 +668,10 @@ int tricycle_is_ptr_expr(TricycleContext *ctx, Expr *expr) { // If we have type context, use it if (ctx->type_ctx) { CheckedType *type = type_check_infer_expr(ctx->type_ctx, expr); - if (type && (type->kind == CHECKED_PTR || type->kind == CHECKED_BUFFER)) { - return 1; + if (type) { + int is_ptr = (type->kind == CHECKED_PTR || type->kind == CHECKED_BUFFER); + checked_type_free(type); + return is_ptr; } } From 0759ac2a040e3ed66dd8e546c1fcde4bb19720d5 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 20 Jan 2026 02:01:16 +0000 Subject: [PATCH 3/4] Add EXTRA_LDFLAGS support to tricycle build target This allows tricycle to be built with ASAN and other linker flags passed via EXTRA_LDFLAGS, consistent with other build targets. --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index d3a7c03c..8a276f19 100644 --- a/Makefile +++ b/Makefile @@ -837,7 +837,7 @@ TRICYCLE_TARGET = tricycle tricycle: $(BUILD_DIRS) build-tricycle build-tricycle: $(TRICYCLE_OBJS) $(LIBCOMMON) - $(CC) $(TRICYCLE_OBJS) $(LIBCOMMON) -o $(TRICYCLE_TARGET) -lm + $(CC) $(TRICYCLE_OBJS) $(LIBCOMMON) -o $(TRICYCLE_TARGET) -lm $(EXTRA_LDFLAGS) $(BUILD_DIR)/backends/tricycle/%.o: $(SRC_DIR)/backends/tricycle/%.c | $(BUILD_DIRS) $(CC) $(CFLAGS) -c $< -o $@ From aa746f521180439d91f18c8d5ba02c8b428cc8b8 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 20 Jan 2026 13:08:29 +0000 Subject: [PATCH 4/4] Exclude tricycle tests from main test runner Tricycle test files contain intentional memory bugs (double-free, use-after-free, etc.) that are meant to be analyzed by the tricycle static analyzer, not executed by the hemlock interpreter. Changes: - Exclude tests/tricycle/ from the main test runner (run_tests.sh) - Fix tricycle test runner by removing 'set -e' which caused premature exit due to (( PASSED++ )) returning 1 when PASSED=0 --- tests/run_tests.sh | 4 ++-- tests/tricycle/run_tricycle_tests.sh | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/run_tests.sh b/tests/run_tests.sh index 800df8e3..66d8828d 100755 --- a/tests/run_tests.sh +++ b/tests/run_tests.sh @@ -105,8 +105,8 @@ format_time() { echo -e "${BLUE}Running tests...${NC}" echo "" -# Find all test files (excluding compiler, parity, and formatter directories which have their own test runners) -TEST_FILES=$(find "$TEST_DIR" -name "*.hml" -not -path "*/compiler/*" -not -path "*/parity/*" -not -path "*/formatter/*" | sort) +# Find all test files (excluding compiler, parity, formatter, and tricycle directories which have their own test runners) +TEST_FILES=$(find "$TEST_DIR" -name "*.hml" -not -path "*/compiler/*" -not -path "*/parity/*" -not -path "*/formatter/*" -not -path "*/tricycle/*" | sort) CURRENT_CATEGORY="" for test_file in $TEST_FILES; do diff --git a/tests/tricycle/run_tricycle_tests.sh b/tests/tricycle/run_tricycle_tests.sh index 95b30f32..561476e3 100755 --- a/tests/tricycle/run_tricycle_tests.sh +++ b/tests/tricycle/run_tricycle_tests.sh @@ -4,7 +4,7 @@ # # This script runs all Tricycle tests and reports results. -set -e +# Note: Don't use 'set -e' because (( PASSED++ )) returns 1 when PASSED=0 SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" HEMLOCK_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"