# Tooling and Ecosystem Standards for Zig
This document outlines the recommended tooling and ecosystem standards for Zig development. It aims to promote consistency, maintainability, performance, and security across Zig projects. It serves as a guide for developers and a source of truth for AI coding assistants.
## I. Build Systems and Package Management
### 1.1 Standard: Using "zig build"
* **Do This:** Use "zig build" for project setup, dependency management, building, testing, and running your Zig applications.
* **Don't Do This:** Rely on external build systems unless absolutely necessary. Embrace the simplicity and integration of the Zig build system.
* **Why:** "zig build" is the officially supported build system, ensuring compatibility and leveraging Zig's unique features (e.g., comptime). It simplifies cross-compilation and reproducible builds.
"""zig
// build.zig
const std = @import("std");
pub fn build(b: *std.Build) !void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const exe = b.addExecutable(.{
.name = "my-app",
.root_source_file = .{ .path = "src/main.zig" },
.target = target,
.optimize = optimize,
});
b.installArtifact(exe);
const run_cmd = b.addRunArtifact(exe);
run_cmd.step.dependOn(b.getInstallStep());
if (b.args) |args| {
run_cmd.addArgs(args);
}
const run_step = b.step("run", "Run the app");
run_step.dependOn(&run_cmd.step);
const exe_tests = b.addExecutable(.{
.name = "my-app-tests",
.root_source_file = .{ .path = "src/test.zig" },
.target = target,
.optimize = optimize,
});
exe_tests.addPackagePath("my-app", .{ .path = "src/main.zig" });
const test_step = b.addRunArtifact(exe_tests);
const test_step_obj = b.step("test", "Run unit tests");
test_step_obj.dependOn(&test_step.step);
const format_step = b.addFormatStep(.{
.source_files = &.{
"src/main.zig",
"src/test.zig",
"build.zig",
},
.mode = .check,
});
const format_check_step = b.step("format", "Check format");
format_check_step.dependOn(&format_step.step);
}
"""
### 1.2 Standard: Dependency Management with "zig build"
* **Do This:** Define dependencies using "addModule" or "addPackagePath" within "build.zig". Use versioning to track dependency updates.
* **Don't Do This:** Manually download and manage dependencies or commit them directly into the repository.
* **Why:** Centralized dependency management ensures that the project builds correctly and consistently across different environments.
"""zig
// build.zig (Example: Adding a module)
const std = @import("std");
pub fn build(b: *std.Build) !void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const exe = b.addExecutable(.{
.name = "my-app",
.root_source_file = .{ .path = "src/main.zig" },
.target = target,
.optimize = optimize,
});
// Add a dependency to another Zig package
exe.addModule("my-package", .{ .source_file = .{ .path = "lib/my_package.zig" } });
b.installArtifact(exe);
}
"""
"""zig
// lib/my_package.zig
pub fn add(a: i32, b: i32) i32 {
return a + b;
}
"""
"""zig
// src/main.zig
const std = @import("std");
const my_package = @import("my-package");
pub fn main() !void {
const result = my_package.add(5, 3);
std.debug.print("Result: {}\n", .{result});
}
"""
### 1.3 Standard: Versioning Dependencies
* **Do This:** Utilize semantic versioning for your own packages and specify version constraints when including external packages.
* **Don't Do This:** Use vague version ranges or rely on the latest version without testing.
* **Why:** Versioning provides stability and prevents unexpected breakages due to dependency updates. Zig itself doesn't enforce semantic versioning directly but you are still responsible to your users for tracking changes and breaking changes.
"""zig
// build.zig (Example: Versioning - hypothetically using a package from an external source)
const std = @import("std");
pub fn build(b: *std.Build) !void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const exe = b.addExecutable(.{
.name = "my-app",
.root_source_file = .{ .path = "src/main.zig" },
.target = target,
.optimize = optimize,
});
// Hypothetical external package (using a git repo in this example)
const my_ext_package = b.dependency("my-ext-library", .{
.source = .{ .git = .{
.url = "https://github.com/someuser/my-ext-library.git",
.tag = "v1.2.3", // Pin to a specific version
}},
});
exe.addModule("my-ext-library", my_ext_package.module("my-ext-library"));// adjust for actual library name
b.installArtifact(exe);
}
"""
### 1.4 Standard: Utilizing "zig fmt"
* **Do This:** Use "zig fmt" to automatically format your code. Configure your editor/IDE to run "zig fmt" on save.
* **Don't Do This:** Manually format code or ignore formatting inconsistencies. Commit code with formatting issues.
* **Why:** Consistent formatting improves readability and reduces cognitive load when reviewing code. "zig fmt" is deterministic and ensures a uniform style across the codebase. Run zig fmt --check build.zig to check.
"""bash
zig fmt src/main.zig
"""
## II. Testing
### 2.1 Standard: Unit Testing with "@test"
* **Do This:** Write comprehensive unit tests using the "@test" block within Zig files. Ensure each function and module has adequate test coverage.
* **Don't Do This:** Neglect writing tests or rely solely on manual testing.
* **Why:** Unit tests ensure the correctness of individual components, making it easier to identify and fix bugs.
"""zig
// src/main.zig
pub fn add(a: i32, b: i32) i32 {
return a + b;
}
test "add function" {
const assert = @import("std").debug.assert;
assert(add(2, 3) == 5);
assert(add(-1, 1) == 0);
assert(add(0, 0) == 0);
}
"""
### 2.2 Standard: Test Organization
* **Do This:** Organize tests logically within the source files they are testing. Create a "test.zig" file (shown in build.zig example) or a "tests/" directory for more complex test suites.
* **Don't Do This:** Mix production code and tests in a confusing manner.
* **Why:** Proper organization improves the maintainability and readability of test suites.
"""zig
// tests/add_test.zig (Example)
const std = @import("std");
const add = @import("../src/main.zig").add; // Assuming the main module is one directory up
test "add function" {
const assert = std.debug.assert;
assert(add(2, 3) == 5);
assert(add(-1, 1) == 0);
assert(add(0, 0) == 0);
}
"""
### 2.3 Standard: Assertions
* **Do This:** Utilize "std.debug.assert" and other assertion functions from the standard library for test conditions.
* **Don't Do This:** Use print statements and manual checks instead of proper assertions.
* **Why:** Assertions provide clear and concise feedback when tests fail, simplifying debugging.
"""zig
// Example using std.testing.expect
const expect = @import("std").testing.expect;
test "example with expect" {
try expect(1 + 1 == 2);
try expect(1 + 2 == 3);
}
"""
## III. Logging and Debugging
### 3.1 Standard: Using "std.debug.print" for Development
* **Do This:** Use "std.debug.print" for logging debug information during development.
* **Don't Do This:** Use print statements in production code.
* **Why:** "std.debug.print" is automatically disabled in release builds, preventing unnecessary overhead.
"""zig
const std = @import("std");
pub fn main() !void {
const x: i32 = 10;
std.debug.print("Value of x: {}\n", .{x}); // Only prints in debug builds
}
"""
### 3.2 Standard: Structured Logging
* **Do This:** For production systems, consider using a structured logging library with different severity levels (e.g., info, warning, error).
* **Don't Do This:** Rely on basic print statements in production, which are harder to filter and analyze.
* **Why:** Structured logging enables efficient debugging and monitoring in production environments.
*(Note: At the time of writing, structured logging libraries are still evolving in the Zig ecosystem.)*
### 3.3 Standard: Debugging Tools
* **Do This:** Utilize debuggers such as LLDB or GDB with Zig for in-depth analysis and troubleshooting. Learn to use breakpoints, stepping, and variable inspection.
* **Don't Do This:** Resort to guesswork or excessive print statements for complex debugging tasks.
* **Why:** Debuggers are powerful tools for understanding program behavior and identifying the root cause of errors.
## IV. Error Handling and Panics
### 4.1 Standard: Explicit Error Handling
* **Do This:** Use "error{}" and the "try" keyword for explicit error handling. Propagate errors up the call stack when appropriate.
* **Don't Do This:** Ignore errors or discard them without proper handling.
* **Why:** Explicit error handling makes your code more robust and prevents unexpected crashes. Zig mandates this.
"""zig
const std = @import("std");
fn divide(a: i32, b: i32) !i32 {
if (b == 0) {
return error.DivisionByZero;
}
return a / b;
}
pub fn main() !void {
const result = try divide(10, 2);
std.debug.print("Result: {}\n", .{result});
const error_result = divide(10, 0) catch |err| {
std.debug.print("Error occurred: {}\n", .{err});
return 0;
};
std.debug.print("Error Result: {}\n", .{error_result});
}
"""
### 4.2 Standard: Recoverable Errors vs Panics
* **Do This:** Use recoverable errors ("!Type") for situations where the program can reasonably recover from an error. Use "unreachable" or "std.debug.panic" ONLY in truly exceptional circumstances that should never occur in production. Prefer "std.debug.assert" for runtime checks during development.
* **Don't Do This:** Use panics as a general-purpose error-handling mechanism. Overuse "unreachable".
* **Why:** Panics are intended for situations that indicate a serious bug or unrecoverable condition and will cause the program to exit. Recoverable errors provide a mechanism for graceful degradation.
"""zig
const std = @import("std");
// Example of using "unreachable" when a condition should *never* be met.
// Use sparingly! It's best to have runtime errors for all expected failure cases
fn getStatusCodeName(code: u16) []const u8 {
return switch (code) {
200 => "OK",
404 => "Not Found",
500 => "Internal Server Error",
else => unreachable, // Only use if you're absolutely sure the code will always be 200, 404, or 500
};
}
pub fn main() !void {
std.debug.print("Status: {}\n", .{getStatusCodeName(200)});
}
"""
## V. Code Analysis and Linting
### 5.1 Standard: Static Analysis
* **Do This:** Integrate static analysis tools into your workflow. Zig itself doesn't ship with a comprehensive linter; however, keep an eye on community developed tools as they emerge.
* **Don't Do This:** Rely solely on the compiler for catching errors.
* **Why:** Static analysis can detect potential bugs, security vulnerabilities, and style violations before runtime.
### 5.2 Standard: Code Reviews
* **Do This:** Conduct thorough code reviews for all changes to the codebase.
* **Don't Do This:** Skip code reviews or rush through them.
* **Why:** Code reviews are an effective way to catch errors, improve code quality, and share knowledge among team members.
## VI. Library Selection and Usage
### 6.1 Standard: Favor the Standard Library
* **Do This:** Utilize the Zig standard library ("std") whenever possible. It is well-maintained, optimized, and guaranteed to be compatible with the language.
* **Don't Do This:** Introduce external dependencies unless they provide significant value or functionality not available in the standard library.
* **Why:** Reduces the number of dependencies and ensures long-term compatibility.
### 6.2 Standard: Evaluate External Libraries Carefully
* **Do This:** Thoroughly evaluate the quality, maintainability, and security of external libraries before incorporating them into your project. Check for active development, test coverage, and security audits.
* **Don't Do This:** Blindly trust external libraries without proper scrutiny.
* **Why:** Prevents the introduction of buggy, unmaintained, or insecure code into your project.
### 6.3 Standard: Contributing to the Ecosystem
* **Do This:** Contribute back to the Zig ecosystem by sharing reusable libraries, tools, and examples. Report bugs and submit pull requests to improve existing projects.
* **Don't Do This:** Hoard knowledge or keep useful code private.
* **Why:** A strong and vibrant ecosystem benefits everyone.
### 6.4 Standard: Asynchronous Programming
* **Do This:** Employ "async" and "await" (when fully supported and available) for concurrent operations when appropriate, creating a more scalable solution than manual thread management.
## VII. Documentation
### 7.1 Standard: Documenting Code
* **Do This:** Write clear and concise documentation for all public functions, types, and modules using doc comments ("///"). Use examples to illustrate how to use the code. The Zig compiler can generate documentation from these comments.
* **Don't Do This:** Skip documentation or write vague or incomplete documentation.
* **Why:** Documentation makes it easier for other developers (including your future self) to understand and use your code.
"""zig
/// Adds two integers together.
///
/// Example:
/// """zig
/// const result = add(2, 3);
/// assert(result == 5);
/// """
pub fn add(a: i32, b: i32) i32 {
return a + b;
}
"""
### 7.2 Standard: Updating Documentation
* **Do This:** Keep your documentation up-to-date with the latest code changes.
* **Don't Do This:** Allow documentation to become stale or inaccurate.
* **Why:** Outdated documentation is worse than no documentation at all.
### 7.3 Standard: Project README
* **Do This:** Include a comprehensive README file at the root of your project that explains:
* The purpose of the project
* How to build and run the project
* How to contribute to the project
* License information
* **Don't Do This:** Omit the README file or provide only minimal information.
* **Why:** A well-written README file is the first thing that many developers will see when they encounter your project.
## VIII. Tooling
### 8.1 Standard: Editor Configuration
* **Do This:** Configure your code editor (e.g., VS Code, Neovim) with syntax highlighting, code completion, and linting support for Zig. Use extensions like "zig-lsp" (or similar LSP implementations) for improved development experience.
* **Don't Do This:** Use a plain text editor without any Zig-specific tooling.
* **Why:** Editor tooling significantly improves productivity and reduces errors.
### 8.2 Standard: Continuous Integration (CI)
* **Do This:** Set up a CI system (e.g., GitHub Actions, GitLab CI) to automatically build, test, and lint your code on every commit.
* **Don't Do This:** Rely solely on manual builds and tests.
* **Why:** CI helps to catch errors early and ensure that the codebase remains in a working state.
"""yaml
# .github/workflows/ci.yml (Example using GitHub Actions)
name: CI
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Zig
uses: goto-bus-stop/setup-zig@v2
with:
version: master # Or a specific version, e.g., 0.11.0
- name: Build
run: zig build
- name: Test
run: zig build test
- name: Format Check
run: zig fmt --check ./src/*.zig ./build.zig
"""
### 8.3 Standard: Code Generation
* **Do This:** Utilize code generation techniques (e.g., using "comptime") to automate repetitive tasks and reduce boilerplate code.
* **Don't Do This:** Manually write large amounts of repetitive code.
* **Why:** Code generation can improve maintainability and reduce the risk of errors.
## IX. Security
### 9.1 Standard: Input Validation
* **Do This:** Validate all external inputs to prevent injection attacks and other vulnerabilities.
* **Don't Do This:** Trust external inputs without proper validation.
* **Why:** Input validation is a critical security measure for preventing malicious code from entering your system.
### 9.2 Standard: Memory Safety
* **Do This:** Design your code with memory safety in mind. Use Zig's memory management features carefully and avoid common pitfalls such as buffer overflows and use-after-free errors. Leverage features like bounds checking and address sanitizers during development to detect memory errors.
* **Don't Do This:** Neglect memory safety considerations, especially when working with pointers and manual memory management.
* **Why:** Memory safety vulnerabilities can lead to serious security breaches.
### 9.3 Standard: Dependency Security
* **Do This:** Regularly audit your dependencies for known security vulnerabilities. Use dependency management tools that provide security scanning capabilities.
* **Don't Do This:** Ignore security alerts from your dependency manager.
* **Why:** External libraries can contain security vulnerabilities that can compromise your application.
danielsogl
Created Mar 6, 2025
This guide explains how to effectively use .clinerules
with Cline, the AI-powered coding assistant.
The .clinerules
file is a powerful configuration file that helps Cline understand your project's requirements, coding standards, and constraints. When placed in your project's root directory, it automatically guides Cline's behavior and ensures consistency across your codebase.
Place the .clinerules
file in your project's root directory. Cline automatically detects and follows these rules for all files within the project.
# Project Overview project: name: 'Your Project Name' description: 'Brief project description' stack: - technology: 'Framework/Language' version: 'X.Y.Z' - technology: 'Database' version: 'X.Y.Z'
# Code Standards standards: style: - 'Use consistent indentation (2 spaces)' - 'Follow language-specific naming conventions' documentation: - 'Include JSDoc comments for all functions' - 'Maintain up-to-date README files' testing: - 'Write unit tests for all new features' - 'Maintain minimum 80% code coverage'
# Security Guidelines security: authentication: - 'Implement proper token validation' - 'Use environment variables for secrets' dataProtection: - 'Sanitize all user inputs' - 'Implement proper error handling'
Be Specific
Maintain Organization
Regular Updates
# Common Patterns Example patterns: components: - pattern: 'Use functional components by default' - pattern: 'Implement error boundaries for component trees' stateManagement: - pattern: 'Use React Query for server state' - pattern: 'Implement proper loading states'
Commit the Rules
.clinerules
in version controlTeam Collaboration
Rules Not Being Applied
Conflicting Rules
Performance Considerations
# Basic .clinerules Example project: name: 'Web Application' type: 'Next.js Frontend' standards: - 'Use TypeScript for all new code' - 'Follow React best practices' - 'Implement proper error handling' testing: unit: - 'Jest for unit tests' - 'React Testing Library for components' e2e: - 'Cypress for end-to-end testing' documentation: required: - 'README.md in each major directory' - 'JSDoc comments for public APIs' - 'Changelog updates for all changes'
# Advanced .clinerules Example project: name: 'Enterprise Application' compliance: - 'GDPR requirements' - 'WCAG 2.1 AA accessibility' architecture: patterns: - 'Clean Architecture principles' - 'Domain-Driven Design concepts' security: requirements: - 'OAuth 2.0 authentication' - 'Rate limiting on all APIs' - 'Input validation with Zod'
# Component Design Standards for Zig This document outlines the component design standards for Zig, focusing on creating reusable, maintainable, and performant components. These guidelines are tailored for modern Zig development and emphasize utilizing the language's unique features effectively. ## 1. Component Architecture ### 1.1. Principles of Component Design * **Standard:** Adhere to the principles of high cohesion and low coupling. * **Do This:** Design components that perform a single, well-defined task. Ensure that components depend on minimal external dependencies. * **Don't Do This:** Create "god" components that handle multiple unrelated responsibilities or have tight dependencies on numerous other components. * **Why:** High cohesion makes components easier to understand and modify. Low coupling reduces the impact of changes in one component on other parts of the system. * **Standard:** Prefer composition over inheritance. * **Do This:** Use interfaces or function pointers to define component contracts and compose functionalities together. * **Don't Do This:** Rely heavily on inheritance, as it can lead to fragile base class problems and decreased flexibility. Zig does not have class inheritance, so focus on alternatives like composition. * **Why:** Composition promotes flexibility and reduces the risk of creating rigid hierarchies. * **Standard:** Design for testability. * **Do This:** Structure components to be easily testable in isolation. Use dependency injection or similar techniques to provide mock implementations of dependencies during testing. * **Don't Do This:** Create components with hard-coded dependencies that make unit testing difficult or impossible. * **Why:** Testable components lead to more robust and reliable software. ### 1.2. Component Modularity * **Standard:** Organize components into well-defined modules. * **Do This:** Group related components into modules based on functionality. Use Zig's "pub" keyword to control component visibility and enforce encapsulation. * **Don't Do This:** Place all components in a single monolithic module or expose internal implementation details unnecessarily. * **Why:** Modularity enhances code organization and maintainability while clearly defining public APIs. **Example:** """zig // my_module.zig pub const MyComponent = struct { pub fn doSomething(self: *MyComponent, input: i32) i32 { return self.internalFunction(input) * 2; } fn internalFunction(self: *MyComponent, value: i32) i32 { // Implementation detail, not exposed publicly return value + 1; } }; """ """zig // main.zig const std = @import("std"); const my_module = @import("my_module"); pub fn main() !void { var component = my_module.MyComponent {}; const result = component.doSomething(&component, 5); std.debug.print("Result: {}\n", .{result}); // Output: Result: 12 } """ * **Standard:** Use abstract interfaces or data types to define component interactions. * **Do This:** Define interfaces or data types that express the contracts between components. Implement these interfaces within each component. This is especially important when the concrete types are determined at runtime. * **Don't Do This:** Hardcode specific component implementations as dependencies, making it difficult to swap implementations or test independently. * **Why:** Abstract interfaces promote loose coupling and enable polymorphism. **Example:** """zig // interfaces.zig pub const DataReader = interface { pub fn readData() ![]const u8; }; pub const DataWriter = interface { pub fn writeData(data: []const u8) !void; }; """ """zig // component_a.zig const interfaces = @import("interfaces.zig"); const std = @import("std"); pub const ComponentA = struct { impl: *ComAImpl, }; const ComAImpl = struct { reader: anytype, writer: anytype, pub fn readData(self: *ComAImpl) ![]const u8 { return self.reader.readData(); } pub fn writeData(self: *ComAImpl, data: []const u8) !void { try self.writer.writeData(data); } }; pub fn init(reader: anytype, writer: anytype) ComponentA { return ComponentA{ .impl = &ComAImpl{ .reader = reader, .writer = writer, }, }; } """ * **Standard:** Manage dependencies explicitly. * **Do This:** Use dependency injection or a similar mechanism to provide components with their dependencies. Prefer passing dependencies as arguments to component constructors or functions. * **Don't Do This:** Rely on global state or hard-coded dependencies, as these make testing and maintenance difficult. * **Why:** Explicit dependency management makes components more modular and testable. ## 2. Component Implementation ### 2.1. Data Structures * **Standard:** Choose appropriate data structures for performance and memory efficiency. * **Do This:** Consider the specific requirements of your component when selecting data structures. Use "std.ArrayList" for dynamically sized arrays, "std.AutoHashMap" for key-value lookups, and structs for fixed-size data. Utilize comptime introspection to automatically optimize data structures. * **Don't Do This:** Use inefficient data structures without considering performance implications or introduce unnecessary copies or allocations. * **Why:** Efficient data structures are essential for achieving optimal performance. * **Standard:** Consider alignment and packing. * **Do This:** Understand Zig's data layout and use "packed struct" when appropriate to minimize memory usage. Be aware of alignment requirements on different architectures. * **Don't Do This:** Ignore alignment and packing, leading to potential memory waste or performance degradation. * **Why:** Efficient memory usage is crucial in resource-constrained environments. **Example:** """zig const std = @import("std"); pub fn main() !void { const UnpackedStruct = struct { a: u8, b: u32, c: u8, }; const PackedStruct = packed struct { a: u8, b: u32, c: u8, }; std.debug.print("Size of UnpackedStruct: {}\n", .{@sizeOf(UnpackedStruct)}); // Output: Size of UnpackedStruct: 12 (due to alignment) std.debug.print("Size of PackedStruct: {}\n", .{@sizeOf(PackedStruct)}); // Output: Size of PackedStruct: 6 } """ ### 2.2. Error Handling * **Standard:** Use Zig's error union and "try" syntax for robust error handling. * **Do This:** Define error sets that are specific to your component. Use "try" to propagate errors to the caller. Consider using "catch" to handle specific errors or provide fallback behavior. * **Don't Do This:** Ignore errors or use generic error types that don't provide sufficient context, or rely on panics for non-exceptional situations. * **Why:** Proper error handling is essential for building reliable software. **Example:** """zig const std = @import("std"); const ComponentError = error { InvalidInput, FileNotFound, }; fn processData(input: i32) !i32 { if (input < 0) { return ComponentError.InvalidInput; } // Simulate file not found error if (input == 13) { return ComponentError.FileNotFound; } return input * 2; } pub fn main() !void { const input_value: i32 = 13; // Try different values (e.g., -1 for InvalidInput) const result = processData(input_value) catch |err| { std.debug.print("Error processing data: {}\n", .{err}); return 0; // Fallback value }; std.debug.print("Result: {}\n", .{result}); // If error, prints 0 } """ ### 2.3. Memory Management * **Standard:** Be explicit about memory allocation and deallocation. * **Do This:** Use Zig's allocator interface for memory management. Pass allocators explicitly to components that need to allocate memory. Ensure that all allocated memory is properly deallocated. Leverage "defer" for automatic cleanup and use arenas for managing temporary allocations. * **Don't Do This:** Rely on implicit memory management or leak memory. Avoid using global allocators without careful consideration. * **Why:** Zig offers precise control over memory, which is crucial for performance-sensitive applications. **Example:** """zig const std = @import("std"); pub fn main() !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = arena.allocator(); const data = try allocator.alloc(u8, 10); defer allocator.free(data); for (data, 0..) |*byte, i| { byte.* = @intCast(u8, i); } std.debug.print("Data: {}\n", .{data}); } """ ### 2.4. Concurrency * **Standard:** Use Zig's concurrency features to improve performance, but avoid data races. * **Do This:** Employ "std.Thread" and "std.Mutex" for concurrent execution and synchronization. Use atomics for lock-free data sharing when appropriate. * **Don't Do This:** Introduce data races by accessing shared mutable state without proper synchronization. Overuse locks, which can lead to performance bottlenecks. * **Why:** Concurrency can significantly improve performance, but requires careful consideration of synchronization and memory safety. ## 3. Component API Design ### 3.1. Naming Conventions * **Standard:** Follow consistent naming conventions for components, functions, and variables. * **Do This:** Use PascalCase for component names, camelCase for function and variable names, SNAKE_CASE for constants. Always name things to easily understand their function. * **Don't Do This:** Use inconsistent or ambiguous names that make code difficult to understand. * **Why:** Consistent naming improves code readability and maintainability. ### 3.2. Function Design * **Standard:** Design functions to be small, focused, and easy to understand. * **Do This:** Limit the size of functions to a manageable number of lines of code (e.g., less than 50 lines per function). Extract complex logic into smaller, reusable helper functions. Return errors instead of panicking for recoverable situations. * **Don't Do This:** Create large, monolithic functions that are difficult to understand and test, or mix too much unrelated logic together. * **Why:** Small, focused functions are easier to test, debug, and maintain. ### 3.3. Interoperability * **Standard:** Consider interoperability with C and other languages, where needed. * **Do This:** Use "extern" keyword to create C-compatible interfaces. Ensure that data structures are compatible with C data types. Carefully manage memory allocation and deallocation when crossing language boundaries. Pass allocators through the component API so allocation rules are obvious. * **Don't Do This:** Expose Zig-specific data structures directly to C code or create memory leaks when passing data between languages. * **Why:** Zig excels at providing low-level access and interfaces with other languages. **Example:** """zig // zig_component.zig export fn add(a: i32, b: i32) i32 { return a + b; } """ """c // main.c #include <stdio.h> extern int add(int a, int b); int main() { int result = add(5, 3); printf("Result from Zig: %d\n", result); return 0; } """ Compile with "zig cc main.c zig_component.zig -o main" ## 4. Advanced Component Design Patterns ### 4.1. Compile-Time Reflection and Code Generation * **Standard:** Utilize compile-time reflection to generate code and optimize data structures. * **Do This:** Use "@typeInfo" and other comptime functions to inspect data types and generate code dynamically at compile time. * **Don't Do This:** Overuse compile-time reflection, which can increase compilation time. * **Why:** Compile-time reflection allows for powerful metaprogramming and optimization. **Example:** """zig const std = @import("std"); pub fn main() !void { const MyStruct = struct { a: i32, b: f32, }; const type_info = @typeInfo(MyStruct); if (type_info == .Struct) { std.debug.print("MyStruct is a struct with {} fields.\n", .{type_info.Struct.fields.len}); for (type_info.Struct.fields) |field| { std.debug.print("Field name: {}, type id: {}\n", .{field.name, field.type}); } } } """ ### 4.2. Custom Allocators * **Standard:** Create custom allocators for specialized memory management needs. * **Do This:** Implement the "std.mem.Allocator" interface to create custom allocators, such as stack allocators or pool allocators. * **Don't Do This:** Use standard allocators for all memory allocation needs, which may not be the most efficient solution for specific use cases. * **Why:** Custom allocators can provide significant performance improvements in certain scenarios. ### 4.3. Tagged Unions for Polymorphism * **Standard:** Employ tagged unions for implementing polymorphic behavior. * **Do This:** Define a tagged union type with different variants, each representing a specific implementation of an interface. Use a tag field to indicate the active variant. * **Don't Do This:** Rely on dynamic dispatch mechanisms, which can introduce runtime overhead and are outside of Zig's core design. * **Why:** Tagged unions provide a type-safe and efficient way to implement polymorphism without runtime dispatch overhead. **Example:** """zig const std = @import("std"); const Shape = union(enum) { circle: Circle, rectangle: Rectangle, }; const Circle = struct { radius: f32, }; const Rectangle = struct { width: f32, height: f32, }; fn area(shape: Shape) f32 { return switch (shape) { .circle => |c| std.math.pi * c.radius * c.radius, .rectangle => |r| r.width * r.height, }; } pub fn main() !void { const circle = Shape{ .circle = Circle{ .radius = 5.0 } }; const rectangle = Shape{ .rectangle = Rectangle{ .width = 4.0, .height = 6.0 } }; std.debug.print("Circle area: {}\n", .{area(circle)}); std.debug.print("Rectangle area: {}\n", .{area(rectangle)}); } """ ## 5. Tooling * **Standard:** Leverage the Zig build system and package manager. * **Do This:** Structure your project according to the Zig build system conventions. Use "zig build" to compile your code and manage dependencies. * **Don't Do This:** Use custom build scripts or ignore the Zig package manager, as this can lead to inconsistencies and maintenance challenges. * **Why:** The Zig build system provides a consistent and reliable way to build and manage Zig projects. ## Summary By adhering to these component design standards, Zig developers can create reusable, maintainable, performant, and secure software. The specific features of Zig, such as its explicit memory management, error handling, and compile-time reflection, should be leveraged to design components that are both efficient and easy to understand. This document provides a solid foundation for building high-quality Zig applications.
# Core Architecture Standards for Zig This document outlines the core architecture standards for Zig projects, focusing on fundamental patterns, project structure, and organization principles. These standards aim to promote maintainability, performance, and security. All examples are written for the currently supported Zig versions (0.11.0 and later). ## 1. Project Structure A well-defined project structure is crucial for finding code, understanding relationships, and scaling projects effectively. ### 1.1. Standard Layout **Do This:** Adopt a standardized project layout from the beginning. **Don't Do This:** Use a flat or haphazard directory structure. **Why:** Consistent structure improves navigation and makes it easier for new developers to understand the codebase. """ my_project/ ├── build.zig # Build script ├── src/ # Source code │ ├── main.zig # Entry point │ ├── core/ # Core logic and data structures │ │ ├── allocator.zig # Custom allocator │ │ └── utils.zig # General utilities │ ├── api/ # API-related code (if applicable) │ │ ├── routes.zig # HTTP route definitions │ │ └── handlers.zig # Route handlers │ └── models/ #Data Models │ └── user.zig ├── lib/ # External dependencies (if not using package manager) ├── test/ # Unit and integration tests │ ├── main.zig # Test entry point │ └── core_test.zig # Tests for core functionality ├── README.md # Project documentation ├── .gitignore # Git ignore file """ **Explanation:** * "build.zig": The central build script for compiling and managing the project. * "src/": Contains all the project's source code. * "main.zig": The application's entry point. * "core/": Houses fundamental data structures, algorithms, or low-level functionalities that are central to the application's logic. Examples: memory management, custom allocators, utility functions. * "api/": Contains code related to external interfaces (e.g., HTTP APIs). This can include route definitions, request handlers, and data serialization/deserialization logic. * "models/": Defines data structures representing domain entities (e.g., users, products, orders) and can include serialization/deserialization logic. * "lib/": Though "build.zig" and Zig's package management are preferred, this can hold externally-managed libraries. Avoid this where possible. Prefer using "build.zig" to manage dependencies. * "test/": Contains unit and integration tests. * "main.zig": Entry point for running tests. * "core_test.zig": Example tests for the 'core' module. ### 1.2. Modules and Packages **Do This:** Break code into logical modules and packages. Use "pub" keyword for exported symbols. Package related modules together in directories. **Don't Do This:** Put all code into a single file or create a dependency cycle between modules. **Why:** Modularity promotes encapsulation, reusability, and reduces compilation times. **Example:** "src/core/allocator.zig": """zig // src/core/allocator.zig const std = @import("std"); pub const FixedBufferAllocatorError = error { OutOfMemory, }; pub fn FixedBufferAllocator(comptime T: type, comptime capacity: usize) type { return struct { buffer: [capacity]T = undefined, index: usize = 0, pub fn init() !@This() { return .{}; } pub fn alloc(self: *@This()) !*T { if (self.index >= capacity) { return FixedBufferAllocatorError.OutOfMemory; } defer self.index += 1; return &self.buffer[self.index]; } pub fn reset(self: *@This()) void { self.index = 0; } }; } """ "src/main.zig": """zig // src/main.zig const std = @import("std"); const Allocator = @import("core/allocator.zig").FixedBufferAllocator(u8, 1024); pub fn main() !void { var allocator = Allocator.init() catch unreachable; const value = try allocator.alloc(); value.* = 42; std.debug.print("Value: {}\n", .{value.*}); } """ **Explanation:** * The "FixedBufferAllocator" is defined as a module in "core/allocator.zig". Its functions and the "FixedBufferAllocatorError" type are explicitly exported using the "pub" keyword. * "src/main.zig" imports the "FixedBufferAllocator" using "@import". * The "catch unreachable" handles the case where the allocator initialization fails. In a release build, this will optimize away, whereas in a debug build it will cause a crash with a helpful message. ### 1.3. Dependency Management **Do This:** Use "build.zig" for dependency management. Leverage semantic versioning with explicit version constraints. **Don't Do This:** Manually download and manage dependencies or rely on system-wide installations. **Why:** Reproducible builds are essential for reliability. **Example:** """zig // build.zig const std = @import("std"); pub fn build(b: *std.build.Builder) void { const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); const exe = b.addExecutable(.{ .name = "my_project", .root_source_file = .{ .path = "src/main.zig" }, .target = target, .optimize = optimize, }); // Add dependencies const http = b.dependency("http", .{ .target = target, .optimize = optimize, }); exe.addModule("http", http.module("http")); // Ensure types are available for import exe.linkLibrary(std.net.lib); // Example of linking a standard library b.installArtifact(exe); } """ **Explanation:** * "b.dependency" declares an external dependency named "http". The exact mechanism for fetching the dependency (e.g., from a registry, a local path, or git) would be specified in the "build.zig.zon" file. * "exe.addModule" creates a module named "http" that makes types and other "pub" symbols from the dependency available for import. This eliminates stringly-typed dependencies. * "b.installArtifact" installs the executable. ### 1.4. Managing Configuration **Do This:** Utilize structured configuration files (e.g., YAML, JSON, TOML) and load them at runtime or compile time. **Don't Do This:** Hardcode configuration values directly in the source code. **Why:** Configuration should be externalized for deployment flexibility **Caveat:** While helpful in other languages, loading configuration files at runtime increases the binary size. For most simple needs, settings can simply be passed into the executable as command-line arguments. For larger needs, consider using a library that pre-processes a TOML or INI file into zig data structures at compilation time using comptime, for instance using the "embed_file" builtin. This avoids adding a costly dependency to your final executable, such as a JSON implementation or a YAML parser. """zig // src/main.zig const std = @import("std"); const Config = struct { port: u16, host: []const u8, }; // For example, configuration can be received through command line parameters. pub fn main() !void { var args = std.process.argsAlloc(std.heap.page_allocator) catch unreachable; defer std.process.argsFree(std.heap.page_allocator, args); // Default values var config = Config { .port = 8080, .host = "127.0.0.1", }; // Parse arguments for --port and --host var i: usize = 1; while (i < args.len) { if (std.mem.eql(u8, args[i], "--port")) { i += 1; if (i < args.len) { config.port =(try std.fmt.parseInt(u16, args[i], 10)); i += 1; } else { std.debug.print("Error: --port requires a value\n", .{}); return error.InvalidCommandLineArgument; } } else if (std.mem.eql(u8, args[i], "--host")) { i += 1; if (i < args.len) { config.host = args[i]; i += 1; } else { std.debug.print("Error: --host requires a value\n", .{}); return error.InvalidCommandLineArgument; } } else { std.debug.print("Error: Unknown argument: {}\n", .{args[i]}); return error.InvalidCommandLineArgument; } } std.debug.print("Listening on {}:{}\n", .{ config.host, config.port }); } """ ## 2. Architectural Patterns Zig, while low-level, benefits from applying well-established architectural patterns to improve structure and testability. ### 2.1. Layered Architecture **Do This:** Organize the application into distinct layers (presentation, business logic, data access) with clear separation of concerns. **Don't Do This:** Create a monolithic application where layers are tightly coupled. **Why:** Layered architecture improves maintainability by isolating changes to specific layers. **Example:** A web application architecture. * **Presentation Layer:** Handles user interface, HTTP requests, and responses (e.g., using a web framework). * **Business Logic Layer:** Implements core application logic and validation rules. * **Data Access Layer:** Manages interactions with data storage (e.g., databases, files). ### 2.2. Dependency Injection **Do This:** Pass dependencies explicitly to functions or structs. **Don't Do This:** Directly create dependencies within functions or structs. **Why:** Promotes testability by allowing mocking of dependencies and reducing global state. **Example:** """zig const std = @import("std"); // Interface for a logger pub const Logger = interface { pub fn log(self: *@This(), message: []const u8) void; }; // Concrete logger implementation pub const ConsoleLogger = struct { pub fn log(self: *@This(), message: []const u8) void { std.debug.print("{s}\n", .{message}); } }; // A service that depends on a logger pub const MyService = struct { logger: *const Logger, pub fn new(logger: *const Logger) @This() { return .{ .logger = logger }; } pub fn doSomething(self: *@This()) void { self.logger.log("MyService is doing something"); } }; pub fn main() !void { var console_logger = ConsoleLogger{}; // Create Logger var service = MyService.new(&console_logger); // We can call console_logger as though it were an Interface service.doSomething(); return; } """ **Explanation:** * "MyService" doesn't directly instantiate a specific logger implementation. Instead, it receives a logger instance through its constructor. * This allows you to inject different logger implementations (e.g., a file logger, a test logger) without modifying "MyService". ### 2.3. Error Handling Strategies **Do This:** Use Zig's error union types for explicit error handling. Centralize error handling logic where appropriate. **Don't Do This:** Ignore errors or rely on panics without proper handling. **Why:** Robust error handling is critical for application stability. **Example:** """zig const std = @import("std"); fn divide(a: i32, b: i32) !f32 { if (b == 0) { return error.DivisionByZero; } return @floatFromInt(a) / @floatFromInt(b); } pub fn main() !void { const result = try divide(10, 2); std.debug.print("Result: {}\n", .{result}); const err = divide(5,0); if (err) |division_error| { std.debug.print("ERROR: {}", .{division_error}); } } """ **Explanation** * The "divide" function returns "!f32", which an error union with a possible error of type "error.DivisionByZero". * The "try" keyword can be used to automatically return the thrown error * Alternatively, errors can be parsed using the "if (err)" keyword. ### 2.4. Custom Allocators **Do This:** Use custom allocators for fine-grained memory management. **Don't Do This:** Rely solely on the default global allocator in performance-critical sections. **Why:** Improves memory efficiency and provides deterministic cleanup. This is PARTICULARLY important in Zig because Zig does NOT offer any garbage collection! """zig const std = @import("std"); const PoolAllocatorError = error { OutOfMemory, }; pub fn PoolAllocator(comptime T: type, comptime capacity: usize) type { return struct { arena: [capacity]T = undefined, // Preallocate used: [capacity] bool = .{false} , // Track Usage pub fn init() @This() { return .{}; } pub fn alloc(self: *@This()) !*T { for (self.used, 0..) |used, i| { // Find a free slot if (!used) { self.used[i] = true; return &self.arena[i]; } } return PoolAllocatorError.OutOfMemory; } pub fn free(self: *@This(), ptr: *T) void { const index = @intFromPtr(ptr) - @intFromPtr(&self.arena); // Calculate Index if (index >= 0 and index < capacity * @sizeOf(T)) { const real_index = index / @sizeOf(T); self.used[real_index] = false; } } }; } pub fn main() !void { var pool = PoolAllocator(u8, 1024).init(); var x = pool.alloc() catch PoolAllocatorError.OutOfMemory => unreachable; x.* = 1; defer pool.free(x); const y = pool.alloc() catch PoolAllocatorError.OutOfMemory => unreachable; y.* = 2; defer pool.free(y); std.debug.print("{}, {}\n", .{x.*, y.*}); } """ ## 3. Implementation Details and Best Practices ### 3.1. Compile-Time Metaprogramming **Do This:** Leverage "comptime" to perform calculations and generate code at compile time. **Don't Do This:** Overuse "comptime" excessively, making the compilation process too slow. **Why:** Generate efficient and specialized code based on compile-time constants. **Example:** """zig const std = @import("std"); // Generate an array of a given size at compile time fn createArray(comptime size: usize, comptime value: i32) [size]i32 { var result: [size]i32 = undefined; inline for (0..size) |i| { result[i] = value; } return result; } pub fn main() !void { const array = createArray(5, 10); // Array size is known at compile time. std.debug.print("{any}\n", .{array}); } """ **Explanation:** * The "createArray" function takes a "comptime" size argument, meaning the array size is determined at compile time. * The array is constructed with a value that's also known at compile time. ### 3.2. Memory Management **Do This:** Be explicit about memory allocation and deallocation. Use "defer" for cleanup to avoid leaks. Consider arena allocators for short-lived objects. **Don't Do This:** Leak memory or double-free memory, leading to crashes. **Why:** Zig relies heavily on manual memory management; careful practices minimizes errors. **Example:** """zig const std = @import("std"); pub fn main() !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = arena.allocator(); const buffer = try allocator.alloc(u8, 1024); defer allocator.free(buffer); std.mem.set(buffer, 0, 1024); std.debug.print("Buffer allocated and initialized.\n", .{}); } """ **Explanation:** * This example uses an "ArenaAllocator". All allocations made with this allocator will be freed when "arena.deinit()" is called. * "defer allocator.free(buffer)" ensures "buffer" is deallocated correctly, even if errors occur before the end of the function. ### 3.3. Concurrency and Asynchronous Programming **Do This:** Use "async" and "await" keywords appropriately. Protect shared resources with mutexes or atomic operations. **Don't Do This:** Create data races or deadlocks. **Why:** Efficient concurrency is vital for resource utilization, especially during I/O. **Example:** """zig const std = @import("std"); // Simulate an Async Task fn someAsyncOperation(value: i32) usize { std.time.sleep(std.time.milliseconds(100)); // Simulate Work return @as(usize, value * 2); } // Async Function Wrapper. Can be used with await fn asyncTask(value: i32) anyerror!usize {@async { return someAsyncOperation(value); }}; pub fn main() anyerror!void { var promise = asyncTask(10); std.debug.print("Result is {}\n", .{@await(promise)}); } """ ### 3.4. Testing Strategies **Do This:** Write unit tests and integration tests. Use Zig's built-in testing framework. **Don't Do This:** Skip testing or write insufficient tests. **Why:** Automated tests safeguard the correctness and maintainability of the code. **Example:** "test/core_test.zig": """zig const std = @import("std"); const allocator = @import("core/allocator.zig"); test "FixedBufferAllocator: Basic Alloc" { var buf = allocator.FixedBufferAllocator(u8, 10).init() catch unreachable; var x = buf.alloc() catch unreachable; x.* = 1; try std.testing.expectEqual(@as(u8, 1), x.*); } """ "test/main.zig": """zig const std = @import("std"); pub fn main() !void { std.testing.runTests() catch |err| { std.debug.print("Tests failed: {}\n", .{err}); return err; }; } """ **Explanation:** * "test/core_test.zig" defines unit tests for the "FixedBufferAllocator" module. * "std.testing.expectEqual" asserts that a value is what you expect it to be. * "test/main.zig" serves as the entry point for running all tests in the project. It calls "std.testing.runTests()" to discover and execute all test functions. ### 3.5. Documentation and Code Comments **Do This:** Write clear, concise, and up-to-date documentation and comments. **Don't Do This:** Write redundant, out-of-date, or misleading documentation. **Why:** Documentation bridges the information gap between the code and the developers. **Example:** """zig /// A struct that represents a point in 2D space. pub const Point = struct { /// The x-coordinate of the point. x: f32, /// The y-coordinate of the point. y: f32, /// Calculates the distance from this point to another point. pub fn distanceTo(self: @This(), other: Point) f32 { const dx = other.x - self.x; const dy = other.y - self.y; return std.math.sqrt(dx * dx + dy * dy); } }; """ ### 3.6. Code Formatting **Do This:** Consistently format code using a standard style (e.g., "zig fmt"). **Don't Do This:** Use inconsistent formatting or violate style guidelines. **Why:** Formatting enhances readability and reduces cognitive load. **Example:** Run "zig fmt" on your code. The most important thing is consistency. ### 3.7. Security Best Practices **Do This:** Always validate untrusted inputs. Use Zig's "std.crypto" package for cryptographic operations. Prevent buffer overflows. **Don't Do This:** Trust user-provided data without validation. Implement custom cryptographic algorithms. Ignore potential overflow errors. **Why:** Secure coding practices are essential to prevent vulnerabilities and protect against attacks. ### 3.8. Asynchronous Programming Best Practices **Do This:** Use "async" and "await" for concurrency. Use "std.Thread" only for parallelism. Avoid blocking operations in "async" functions. Handle errors properly in "async" functions. **Don't Do This:** Use complex threading models without proper synchronization. Perform blocking operations in asynchronous contexts. Ignore potential errors from asynchronous operations. **Why:** Asynchronous programming improves performance for concurrent applications and avoids blocking the main thread. Parallel programming improves CPU Utilization for computationally intensive tasks.
# State Management Standards for Zig This document outlines the recommended standards for managing state in Zig applications. It covers diverse approaches to application state, data flow, and reactivity, all tailored to Zig's unique features and philosophy. The focus is on maintainability, performance, and security, drawing upon the latest Zig versions and ecosystem tools. ## 1. Core Principles ### 1.1 Immutability and Data Flow * **Do This:** Favor immutable data structures and explicit data flow to minimize side effects and improve predictability. This greatly helps reasoning about the program's behavior. * **Don't Do This:** Avoid pervasive mutable global state. It makes debugging and reasoning about the program's behavior extremely difficult. * **Why?:** Immutability reduces the likelihood of unexpected state changes, leading to fewer bugs and easier debugging. Explicit data flow makes the flow of information through the application easier to understand, aiding maintainability. **Example:** """zig const std = @import("std"); const User = struct { id: u32, name: []const u8, }; fn createUser(id: u32, name: []const u8) User { return User{ .id = id, .name = name, // Note: this should typically be a copy, see data ownership section }; } fn updateUser(user: User, newName: []const u8) User { return User{ .id = user.id, .name = newName, // Again, consider a copy for proper ownership }; } pub fn main() !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = arena.allocator(); const initialUser = createUser(123, "Alice"); const updatedUser = updateUser(initialUser, "Alicia"); std.debug.print("Initial User: id={}, name={s}\n", .{initialUser.id, initialUser.name}); std.debug.print("Updated User: id={}, name={s}\n", .{updatedUser.id, updatedUser.name}); } """ ### 1.2 Explicit Memory Management and Ownership * **Do This:** Understand and manage memory ownership explicitly. Use "allocator" parameters judiciously and carefully consider the lifetime of allocated data. Use "defer" statements to ensure resources are released. * **Don't Do This:** Rely on implicit memory management or ignore potential memory leaks. * **Why?:** Zig's lack of garbage collection requires meticulous memory management. Explicit control reduces the risk of memory leaks and dangling pointers. Zig's powerful comptime features can aid with this (e.g. detecting unused resources). **Example:** """zig const std = @import("std"); pub fn main() !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = arena.allocator(); const message: []const u8 = "Hello, Zig!"; // Allocate memory for a copy of the message. Important for ownership! const buffer = try allocator.alloc(u8, message.len); defer allocator.free(buffer); @memcpy(buffer, message); // Copy the message into the allocated buffer std.debug.print("Message: {s}\n", .{buffer}); } """ ### 1.3 Error Handling * **Do This:** Use "try" and "catch" for explicit error handling. Leverage Zig's error union types to propagate errors gracefully. * **Don't Do This:** Ignore errors or use panics for non-exceptional situations. * **Why?:** Explicit error handling allows for graceful recovery and prevents unexpected program termination. Error unions enhance code clarity and guarantee that errors are handled appropriately. **Example:** """zig const std = @import("std"); fn readFile(allocator: std.mem.Allocator, filename: []const u8) ![]u8 { const file = try std.fs.openFile(filename, .{}); defer file.close(); const file_size = try file.getEndPos(); const buffer = try allocator.alloc(u8, file_size); try file.readAll(buffer); return buffer; } pub fn main() !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = arena.allocator(); const filename: []const u8 = "my_file.txt"; const result = readFile(allocator, filename) catch |err| { std.debug.print("Error reading file: {any}\n", .{err}); return; // Exit the function }; if (result != null) { defer allocator.free(result); std.debug.print("File content: {s}\n", .{result}); } } """ ## 2. Architectural Patterns ### 2.1 Data-Oriented Design (DOD) * **Do This:** Organize data in structures of arrays (SoA) format when performance is critical. Separate data from logic to improve cache utilization and SIMD optimization opportunities. * **Don't Do This:** Blindly use object-oriented principles where performance is paramount. Consider the memory layout implications of your data structures. * **Why?:** DOD maximizes data locality, leading to improved cache hit rates and enhanced SIMD parallelization capabilities. Zig's low-level control facilitates effective DOD implementation. **Example:** """zig const std = @import("std"); const MAX_ENTITIES = 1024; const Position = struct { x: [MAX_ENTITIES]f32, y: [MAX_ENTITIES]f32, }; const Velocity = struct { dx: [MAX_ENTITIES]f32, dy: [MAX_ENTITIES]f32, }; var position: Position = undefined; var velocity: Velocity = undefined; fn update(dt: f32) void { for (0..MAX_ENTITIES) |i| { position.x[i] += velocity.dx[i] * dt; position.y[i] += velocity.dy[i] * dt; } } pub fn main() !void { // Initialize data (simplified for demonstration) for (0..MAX_ENTITIES) |i| { position.x[i] = @floatFromInt(i); position.y[i] = @floatFromInt(i); velocity.dx[i] = 1.0; velocity.dy[i] = 0.5; } update(0.1); std.debug.print("Position (first entity): x={}, y={}\n", .{position.x[0], position.y[0]}); } """ ### 2.2 Finite State Machines (FSMs) * **Do This:** Utilize tagged unions or enums to represent distinct states in an FSM. Use switch statements for state transitions. Consider comptime evaluation to validate state transitions at compile time. * **Don't Do This:** Encode state transitions with complex if-else chains or rely on string comparisons. * **Why?:** FSMs provide a structured way to manage complex state transitions, enhancing code readability and maintainability. Tagged unions and switch statements provide type safety and facilitate compile-time checking. **Example:** """zig const std = @import("std"); const State = enum { Idle, Loading, Running, Error, }; var currentState: State = .Idle; fn transitionState(newState: State) void { std.debug.print("Transitioning from {s} to {s}\n", .{ @tagName(currentState), @tagName(newState) }); currentState = newState; } fn processState() void { switch (currentState) { .Idle => { std.debug.print("State: Idle\n", .{}); transitionState(.Loading); }, .Loading => { std.debug.print("State: Loading\n", .{}); // Simulate loading... transitionState(.Running); }, .Running => { std.debug.print("State: Running\n", .{}); // Simulate running... transitionState(.Idle); // loop back to idle }, .Error => { std.debug.print("State: Error\n", .{}); // Handle error... }, } } pub fn main() !void { for (0..3) |_| { processState(); } } """ ### 2.3 Actor Model * **Do This:** Consider the actor model for concurrent and distributed systems. Leverage channels (provided by the standard library or external libraries) for message passing between actors. Each actor should encapsulate its own state. * **Don't Do This:** Use shared mutable state and locks directly for concurrency when the actor model may be more appropriate. * **Why?:** The actor model simplifies concurrent programming by isolating state within actors and facilitating communication through asynchronous message passing. **Example (Conceptual - Zig's concurrency features are still evolving):** """zig // This is a simplified conceptual illustration of the actor model // Zig's concurrency features are under development so this may change in the future. // Placeholder for a channel implementation (using std.async when available, or a custom solution) const Channel = struct { // ... Implementation details hidden ... }; fn createChannel(comptime T: type) Channel { // Placeholder for channel creation logic return Channel{}; } fn send(channel: Channel, message: anytype) void { // Placeholder for sending a message on the channel } fn receive(channel: Channel) anytype { // Placeholder for receiving a message on the channel return undefined; } const ActorA = struct { state: i32, channel: Channel, fn run(self: *@This()) void { while (true) { const message = receive(self.channel); // Process the message and update the state accordingly self.state += 1; // Example update std.debug.print("ActorA - Received message, state = {}\n", .{self.state}); } } }; const ActorB = struct { channel: Channel, fn run(self: *@This()) void { // ... send(self.channel, "Hello from ActorB"); } }; pub fn main() !void { // In a real implementation, would need async/concurrency framework to actually run these concurrently. const channelA = createChannel(anyopaque); // create channel for ActorA const channelB = createChannel(anyopaque); // create channel for ActorB var actorA = ActorA{ .state = 0, .channel = channelA }; var actorB = ActorB{ .channel = channelB }; // Placeholder: Simulate running the actors concurrently (needs suitable Zig concurrency primitives) actorA.run(); actorB.run(); } """ ## 3. State Management Techniques ### 3.1 Function Parameters and Return Values * **Do This:** Pass state as function parameters and return updated state as return values. This promotes pure functions and explicit data flow. * **Don't Do This:** Rely on global variables or mutable function arguments for managing state unless absolutely necessary. * **Why?:** Function parameters and return values establish clear input-output relationships, making functions easier to test and reason about. **Example:** """zig const std = @import("std"); const CounterState = struct { count: u32, }; fn incrementCounter(state: CounterState) CounterState { return CounterState{ .count = state.count + 1 }; } pub fn main() !void { var state = CounterState{ .count = 0 }; state = incrementCounter(state); state = incrementCounter(state); std.debug.print("Counter: {}\n", .{state.count}); // Outputs 2 } """ ### 3.2 Context Objects * **Do This:** Group related state variables into context objects, especially when multiple functions need access to the same set of data. Use "*const" for read-only access and "*" for mutable access. Pass context objects as parameters. * **Don't Do This:** Pass a large number of individual parameters to functions. This reduces readability and increases the risk of errors. * **Why?:** Context objects improve code organization and reduce the number of function parameters. **Example:** """zig const std = @import("std"); const RenderingContext = struct { width: u32, height: u32, clearColor: u32, }; fn render(context: *const RenderingContext) void { std.debug.print("Rendering with width={}, height={}, clearColor={x}\n", .{context.width, context.height, context.clearColor}); } pub fn main() !void { const context = RenderingContext{ .width = 800, .height = 600, .clearColor = 0x0000FF, // Blue }; render(&context); } """ ### 3.3 Dependency Injection * **Do This:** Pass dependencies explicitly as function parameters or through context objects. This promotes testability and modularity. * **Don't Do This:** Hardcode dependencies within functions. * **Why?:** Dependency injection makes it easier to replace dependencies with mock objects for testing or to switch implementations. **Example:** """zig const std = @import("std"); const Logger = struct { log: fn ([]const u8) void, }; fn processData(logger: Logger, data: []const u8) void { logger.log("Processing data..."); logger.log(data); } fn defaultLog(message: []const u8) void { std.debug.print("LOG: {s}\n", .{message}); } pub fn main() !void { const logger = Logger{ .log = defaultLog }; const data: []const u8 = "Some important data"; processData(logger, data); } """ ## 4. Reactivity ### 4.1 Observer Pattern * **Do This:** Implement the observer pattern for decoupling components. Use a central subject that maintains a list of observers and notifies them of state changes. * **Don't Do This:** Tightly couple components that depend on each other's state. * **Why?:** The observer pattern allows components to react to state changes without being directly dependent on the component that owns the state. **Example (Simplified):** """zig const std = @import("std"); const Observer = struct { callback: fn (anyopaque) void, context: anyopaque, }; const Subject = struct { observers: std.ArrayList(Observer), allocator: std.mem.Allocator, fn init(allocator: std.mem.Allocator) Subject { return Subject{ .observers = std.ArrayList(Observer).init(allocator), .allocator = allocator, }; } fn deinit(self: *@This()) void { self.observers.deinit(); } fn attach(self: *@This(), observer: Observer) !void { try self.observers.append(observer); } fn detach(self: *@This(), observer: Observer) void { // ... (Implementation to remove an observer) ... } fn notify(self: *@This(), data: anyopaque) void { for (self.observers.items) |obs| { obs.callback(data); } } }; // Usage: Define a state, a subject, and observers that react to changes. """ ### 4.2 Signals and Slots (Conceptual) * **Do This:** Consider a signals and slots mechanism conceptually for event-driven architectures. This is similar to the observer pattern, but with a more explicit connection between signals (events) and slots (handlers). Create separate signal and slot types for each event. Zig itself doesn't have built-in support, so this would require custom implementation. * **Don't Do This:** Use global event handlers without a clear structure. * **Why?:** Signals and slots offer a structured, type-safe way to manage events and their corresponding handlers. **Example (Conceptual - needs custom implementation):** """zig // Conceptual example of signals and slots // Requires custom implementation in Zig // Define a Signal and Slot type for each specific event type. const ButtonClickSignal = struct { // ... }; const ButtonClickHandler = struct { callback: fn (ButtonClickSignal) void, }; // ... Implementation to connect signals to slots and emit signals ... """ ## 5. Persistent State ### 5.1 Serialization and Deserialization * **Do This:** Use libraries like "zig-json" or "c-ray" for serializing and deserializing state to/from persistent storage. Choose the appropriate format (JSON, MessagePack, etc.) based on requirements. * **Don't Do This:** Manually construct serialization/deserialization logic, especially for complex data structures. * **Why?:** Serialization allows for persisting state to files or databases. Using existing libraries reduces the risk of errors and improves efficiency. **Example (using "zig-json" - example needs confirmation of current library API):** """zig // Note: This example is illustrative and may need adjustments based on the current zig-json API // It's important to consult the library's documentation for accurate usage. const std = @import("std"); const json = @import("json"); // Assuming zig-json is available const User = struct { id: u32, name: []const u8, }; pub fn main() !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = arena.allocator(); const user = User{ .id = 123, .name = "Alice" }; // Serialize to JSON const serialized = try json.stringify(user, .{ .allocator = allocator }); defer allocator.free(serialized); // Important to free! std.debug.print("Serialized JSON: {s}\n", .{serialized}); // Deserialize from JSON (example, adjust according to zig-json API) const deserialized = try json.parse(User,serialized, .{ .allocator = allocator }); defer allocator.free(deserialized.name); // free any allocations made in deserialization std.debug.print("Deserialized User: id={}, name={s}\n", .{deserialized.id, deserialized.name}); } """ ### 5.2 Databases * **Do This:** Use a database library (e.g., SQLite, PostgreSQL) for persisting complex state that requires querying and indexing. Design your database schema carefully. * **Don't Do This:** Store large amounts of structured data in simple files without using a database. * **Why?:** Databases provide efficient storage, querying, and indexing capabilities for structured data. **Example (Conceptual - requires a Zig database library):** """zig // Conceptual example using a database - Requires a Zig database library // Placeholder - Needs integration with a Zig database library like zig-sqlite // const db = @import("zig-sqlite"); // Hypothetical import // ... (Example to connect to database, create tables, insert and query data) ... """ ## 6. Testing State Management ### 6.1 Unit Tests * **Do This:** Write unit tests to verify the correctness of state transitions and data transformations. Mock dependencies to isolate units of code. * **Don't Do This:** Neglect testing state management logic. * **Why?:** Unit tests ensure that state is managed correctly and prevent regressions. **Example:** """zig const std = @import("std"); const testing = std.testing; const CounterState = struct { count: u32, }; fn incrementCounter(state: CounterState) CounterState { return CounterState{ .count = state.count + 1 }; } test "incrementCounter increments the count" { var state = CounterState{ .count = 0 }; state = incrementCounter(state); try testing.expectEqual(state.count, 1); } """ ### 6.2 Integration Tests * **Do This:** Write integration tests to verify the interaction of different components that manage state. * **Don't Do This:** Only write unit tests and neglect integration testing. * **Why?:** Integration tests ensure that different parts of the system work together correctly. ## 7. Security Considerations * **Do This:** Validate and sanitize user input to prevent state corruption. Use appropriate access control mechanisms to protect sensitive state. * **Don't Do This:** Trust user input implicitly or expose sensitive state without proper protection. * **Why?:** Security measures are essential to protect against malicious attacks that could compromise the application's state. Input validation can prevent issues like buffer overflows or malicious code injection.
# Performance Optimization Standards for Zig This document outlines coding standards specifically focused on performance optimization in Zig. These standards are designed to improve application speed, responsiveness, and resource usage while leveraging Zig's unique features. Adherence to these standards will result in code that is not only fast but also maintainable and understandable. ## 1. Data Structures and Memory Layout ### 1.1. Struct Packing and Alignment **Standard:** Minimize struct size by ordering members by size (largest to smallest) to reduce padding. Utilize "packed" structs when memory layout control is paramount and performance outweighs individual member access speed. Consider "@TypeInfo" to analyze data layout. **Why:** Reduces memory footprint and improves cache utilization. "packed" removes padding added by the compiler for alignment, which can be crucial in memory-constrained environments or when interfacing with external data formats. **Do This:** """zig const Point = struct { x: f32, y: f32, }; // Good: Fields are ordered appropriately for minimal padding. const MyStruct = struct { big_array: [1024]u8, // Largest value: u32, // Then u32 flag: bool, // Smallest }; // Packed struct for precise memory layout. Note: Accessing members can be slower. const PackedStruct = packed struct { a: u8, b: u16, c: u32, }; """ **Don't Do This:** """zig // Bad: Potentially unnecessary padding due to field order. const BadStruct = struct { flag: bool, value: u32, big_array: [1024]u8, }; """ **Anti-pattern:** Blindly using "packed" without understanding its implications on access performance. Measure the performance difference. **Zig-specific:** Zig's comptime capabilities are beneficial in calculating optimal struct layouts based on target architecture requirements. """zig const std = @import("std"); pub fn main() !void { const type_info = @typeInfo(MyStruct); if (type_info == .Struct) { std.debug.print("Size: {}\n", .{type_info.Struct.layout.size}); std.debug.print("Alignment: {}\n", .{type_info.Struct.layout.alignment}); } } const MyStruct = struct { a: u8, b: u32, c: u8, }; """ ### 1.2. Choosing the Right Data Structure **Standard:** Use appropriate data structures based on access patterns and data characteristics. Consider "std.ArrayList", "std.AutoHashMap", "std.SinglyLinkedList", and fixed-size arrays. Profile the performance impact of different structures. **Why:** Using the correct data structure significantly affects memory usage and access time. Choosing between an array, linked list, or hash map depends on operations like searching, insertion, deletion, and iteration. **Do This:** """zig const std = @import("std"); // When you know the size: var array: [10]i32 = undefined; // When you need dynamic resizing: var list = std.ArrayList(i32).init(std.heap.page_allocator); defer list.deinit(); try list.append(10); // For key-value pairs with frequent lookups: var map = std.AutoHashMap(i32, f32).init(std.heap.page_allocator); defer map.deinit(); try map.put(5, 3.14); // For efficient string building: var builder = std.StringArrayList.init(std.heap.page_allocator); defer builder.deinit(); try builder.append("Hello"); try builder.append("World"); const result = builder.join(std.heap.page_allocator, " ") catch unreachable; // Properly handle errors. defer std.heap.page_allocator.free(result); std.debug.print("{s}\n", .{result}); """ **Don't Do This:** """zig // Inefficient: Using a linked list when frequent random access is required. var list = std.SinglyLinkedList(i32).init(std.heap.page_allocator); defer list.deinit(); list.append(1); list.append(2); //Problematic: Manually resizing arrays which is error prone and less efficient. """ **Anti-pattern:** Relying on a single data structure for all scenarios. Failing to profile performance under realistic workloads. **Zig-specific:** Zig's allocator-aware data structures ("ArrayList", "HashMap", etc.) are powerful but require careful management of allocation and deallocation via "defer". ### 1.3. Copy-on-Write (Cow) Structures **Standard:** Implement copy-on-write semantics for immutable data structures that are frequently copied to avoid unnecessary memory duplication. **Why:** Reduces memory consumption and allocation overhead, especially when dealing with large, immutable datasets. **Example (Conceptual):** """zig const std = @import("std"); const CowString = struct { ptr: [*]const u8, len: usize, ref_count: *usize, pub fn init(allocator: std.mem.Allocator, str: []const u8) !CowString { const len = str.len; const data = try allocator.alloc(u8, len); @memcpy(data, str); const ref_count = try allocator.create(usize); ref_count.* = 1; return .{ .ptr = data, .len = len, .ref_count = ref_count, }; } pub fn clone(self: CowString, allocator: std.mem.Allocator) !CowString { self.ref_count.* += 1; return .{ .ptr = self.ptr, .len = self.len, .ref_count = self.ref_count, }; } pub fn deinit(self: CowString, allocator: std.mem.Allocator) void { self.ref_count.* -= 1; if (self.ref_count.* == 0) { allocator.free(self.ptr); allocator.destroy(self.ref_count); } } }; pub fn main() !void { const allocator = std.heap.page_allocator; var str1 = try CowString.init(allocator, "Hello, world!"); defer str1.deinit(allocator); var str2 = try str1.clone(allocator); // Clone, increasing ref_count defer str2.deinit(allocator); // str1 and str2 now point to the same memory. std.debug.print("String 1: {s}\n", .{str1.ptr[0..str1.len]}); std.debug.print("String 2: {s}\n", .{str2.ptr[0..str2.len]}); // Modification would trigger a copy (not implemented here for brevity). } """ **Anti-pattern:** Implementing COW without proper atomic reference counting, leading to data races in concurrent environments. Forgetting to deallocate the underlying data when the reference count reaches zero. **Zig-specific:** Zig's memory safety features force you to be explicit with lifetimes and ownership, which is crucial for correct COW implementation. ## 2. Algorithmic Optimization ### 2.1. Choosing Efficient Algorithms **Standard:** Select algorithms with optimal time and space complexity for the task. Understand the Big O notation of different algorithms and choose the most suitable one based on input data size and access patterns. **Why:** Algorithmic choices can have a drastic impact on performance, especially as data scales. **Example:** * **Sorting:** For small datasets, insertion sort or selection sort might be faster due to lower overhead. For larger datasets, use merge sort, quicksort, or heapsort. * **Searching:** Use binary search on sorted data, hash maps for fast lookups, or tree-based structures for ordered data with range queries. **Do This:** """zig const std = @import("std"); // Binary search for sorted arrays. fn binarySearch(array: []const i32, target: i32) ?usize { var low: usize = 0; var high: usize = array.len; while (low < high) { const mid = low + (high - low) / 2; //Prevent overflow if (array[mid] == target) { return mid; } else if (array[mid] < target) { low = mid + 1; } else { high = mid; } } return null; } pub fn main() !void{ const sorted_array = [_]i32{2,4,6,8,10,12}; const target = 8; if(binarySearch(sorted_array[0..], target)){ std.debug.print("Found at index: {}\n",.{binarySearch(sorted_array[0..], target).?}); } else { std.debug.print("Target not found\n", .{}); } } """ **Don't Do This:** """zig // Inefficient: Linear search on a large, sorted array. O(n) fn linearSearch(array: []const i32, target: i32) ?usize { for (array, 0..) |value, i|{ if(value == target){ return i; } } return null; } """ **Anti-pattern:** Implementing naive algorithms without considering their performance limitations. Failing to profile and benchmark different algorithmic approaches. **Zig-specific:** Zig's generic programming features allow you to write algorithms that work with different data types without runtime overhead. ### 2.2. Loop Optimization **Standard:** Minimize computations within loops. Avoid unnecessary memory allocations and deallocations inside loops. Use loop unrolling techniques where appropriate (especially for small loops). Consider vectorization when applicable. **Why:** Loops are often performance bottlenecks. Moving computations outside loops, reducing memory operations, and unrolling loops can significantly improve performance. **Do This:** """zig const std = @import("std"); // Good: Pre-calculate the array length outside the loop. fn processArray(array: []i32) void { const len = array.len; for (0..len) |i| { array[i] *= 2; } } //Unrolling a loop might be beneficial for certain types of workloads fn unrolledLoop(array: []i32) void{ const len = array.len; var i:usize = 0; while(i < len){ array[i] *= 2; if(i + 1 < len){ array[i + 1] *= 2; } i += 2; } } pub fn main() !void { var my_array: [4]i32 = [_]i32{1,2,3,4}; processArray(my_array[0..]); std.debug.print("{any}\n",{my_array}); var my_array2: [4]i32 = [_]i32{1,2,3,4}; unrolledLoop(my_array2[0..]); std.debug.print("{any}\n",{my_array2}); } """ **Don't Do This:** """zig // Bad: Calculating array.len inside the loop on every iteration. fn badProcessArray(array: []i32) void { for (0..array.len) |i| { array[i] *= 2; } } """ **Anti-pattern:** Performing expensive operations (e.g., function calls, memory allocation) within inner loops. Ignoring the potential for loop unrolling or vectorization. **Zig-specific:** Zig's "inline" keyword can be used to force the compiler to inline a function call, potentially eliminating function call overhead within loops. Careful use of "comptime" can precompute loop bounds or other loop invariants. ### 2.3. Avoiding Unnecessary Allocations **Standard:** Minimize memory allocation, especially in performance-critical sections. Use pre-allocated buffers, arena allocators ("std.heap.ArenaAllocator"), and manual memory management where appropriate. Favor stack allocation over heap allocation when the size is known at compile time. **Why:** Memory allocation is an expensive operation. Reducing the frequency of allocations and deallocations significantly improves performance. **Do This:** """zig const std = @import("std"); // Using an arena allocator for temporary allocations. fn processData(allocator: *std.mem.Allocator, data: []const u8) ![]u8 { var arena = std.heap.ArenaAllocator.init(allocator.*); defer arena.deinit(); const arena_allocator = arena.allocator(); const processed = try arena_allocator.alloc(u8, data.len + 1); @memcpy(processed, data); processed[data.len] = 0; // Null terminate return processed; // Ownership is returned to the caller. } pub fn main() !void { const allocator = std.heap.page_allocator; const data = "Hello, world!"; const processed_data = try processData(&allocator, data); defer allocator.free(processed_data); //Deallocate after use std.debug.print("{s}\n", .{processed_data}); } """ **Don't Do This:** """zig const std = @import("std"); //Avoid this: Allocating memory within a frequently called function without reuse. fn badProcessData(data: []const u8) ![]u8 { const allocator = std.heap.page_allocator; const processed = try allocator.alloc(u8, data.len); @memcpy(processed, data); return processed; } """ **Anti-pattern:** Frequent allocations within loops or frequently called functions. Ignoring the potential for memory reuse. **Zig-specific:** Zig's manual memory management capabilities provide fine-grained control over memory allocation and deallocation. Exploit "defer" for resource management. Zig's stage2 compiler is much more efficient at optimizing code and performing escape analysis, allowing for more efficient memory usage. ## 3. Concurrency and Parallelism ### 3.1. Utilizing Multi-threading **Standard:** Utilize multi-threading ("std.Thread") to perform tasks concurrently and leverage multiple CPU cores. Avoid data races by using proper synchronization mechanisms (mutexes, atomics). **Why:** Parallel execution can dramatically improve performance for computationally intensive tasks. **Do This:** """zig const std = @import("std"); const os = std.os; fn workerThread(arg: anyopaque) !void { const id = @ptrToInt(arg); std.debug.print("Worker thread {} started\n", .{id}); // Perform some work var sum: usize = 0; for (0..1000000){|_| sum += id; } std.debug.print("Worker thread {} finished. Sum: {}\n", .{id, sum}); } pub fn main() !void { var threads: [4]?std.Thread = [_]?std.Thread{null} ** 4; // Create and start threads for (threads, 0..) |*thread, i| { thread.* = try std.Thread.spawn(.{}, workerThread, @intToPtr(anyopaque, i)); std.debug.print("Started thread {}\n", .{i}); } // Join threads that have finished executing for (threads) |thread| { if (thread) |t| { t.join(); } } std.debug.print("All threads finished\n", .{}); } """ **Don't Do This:** """zig //Potentially problematic because it lacks proper synchronization, leading to data races var shared_data: i32 = 0; fn badWorkerThread(arg: anyopaque) !void { shared_data += 1; // Race condition! } """ **Anti-pattern:** Using threads without proper synchronization, resulting in data races and unpredictable behavior. Creating too many threads, leading to excessive context-switching overhead. **Zig-specific:** Zig makes it easy to create very small, efficient threads. Zig's memory model requires explicit memory access synchronization when using threads. Consider using "std.atomic" for synchronized primitive access. ### 3.2. Asynchronous Programming **Standard:** Use asynchronous programming techniques (e.g., "async"/"await") for I/O-bound operations to avoid blocking the main thread. **Why:** Asynchronous programming allows you to perform other tasks while waiting for I/O operations to complete, improving responsiveness. **Example:** """zig const std = @import("std"); // Placeholder implementation. Actual implementations may vary // based upon libraries used. const NetworkError = error{ConnectFailed, Timeout}; // Async function to simulate network request fn makeNetworkRequest() !NetworkError { // Simulate a network delay std.time.sleep(100 * std.time.ms); return {}; } // Async function to fetch data async fn fetchData() !void { std.debug.print("Starting network request...\n", .{}); try makeNetworkRequest(); std.debug.print("Network request completed!\n", .{}); } pub fn main() !void { // Run the async task directly in the main function. // More complex applications might use event loops. std.debug.print("Starting fetch data...\n", .{}); if (fetchData()) |result| { // No need for an event loop in this simple example. // In a more advanced use case, an event loop will poll // for completion and schedule the next operation. } catch |err| { std.debug.print("Error: {}\n", .{err}); } std.debug.print("Fetch data started asynchronously, continuing main function...\n", .{}); // Perform other tasks while waiting for the data. std.time.sleep(50 * std.time.ms); // Simulate doing something else std.debug.print("Main function continued execution\n", .{}); } """ **Anti-pattern:** Overusing "async"/"await" for CPU-bound tasks, which does not provide performance benefits and can add overhead. Not handling errors properly in asynchronous code. **Zig-specific:** Zig provides direct support for async functions via the "async" and "await" keywords, but lacks a standard library event loop. User-space event loops or integrations with external libraries are common. Note that async functions in Zig are *not* green threads. ### 3.3. Data Parallelism **Standard:** Use SIMD (Single Instruction, Multiple Data) instructions and GPU programming to perform computations on multiple data elements simultaneously. **Why:** Data parallelism significantly accelerates operations that can be applied independently to multiple data elements. **Note:** Zig provides lower-level support for SIMD than languages like Rust, C++, or specialized GPU languages. Direct SIMD intrinsics and external GPU libraries might be required. **Example (Conceptual):** """zig // Illustration only. Requires platform-specific SIMD intrinsics. fn addArraysSIMD(a: []const f32, b: []const f32, result: []f32) void { // Requires detailed knowledge of the target architecture and SIMD instruction sets. // Use platform-specific intrinsics (e.g., Intel intrinsics, ARM NEON intrinsics). // Typically involves loading data into SIMD registers, performing the addition, and storing the result. // Zig does not have high-level SIMD abstractions in the standard library. } """ **Anti-pattern:** Applying SIMD to inappropriate tasks (e.g., tasks with complex data dependencies). Not aligning data properly for optimal SIMD performance. **Zig-specific:** Zig's low-level nature gives you direct access to hardware features, allowing for fine-grained control over SIMD operations. ## 4. Code Generation and Optimization ### 4.1. Compile-Time Computation **Standard:** Take advantage of Zig's "comptime" keyword to perform computations at compile time. Pre-calculate constants, generate data structures, and optimize code based on compile-time information. **Why:** Eliminates runtime overhead by performing calculations during compilation. **Do This:** """zig const std = @import("std"); // Calculate factorial at compile time. fn factorial(comptime n: u64) u64 { return if (n == 0) 1 else n * factorial(n - 1); } // Generate an array of squares at compile time fn generateSquares(comptime n: usize) [n]u64 { var result: [n]u64 = undefined; for (0..n) |i| { result[i] = @as(u64, @intCast(i * i)); } return result; } pub fn main() !void { // Computed at compile time. const fact_5 = factorial(5); std.debug.print("Factorial of 5: {}\n", .{fact_5}); // Array generated at compile time const squares = generateSquares(5); std.debug.print("Squares: {any}\n", .{squares}); } """ **Don't Do This:** """zig // Performing calculations at runtime that could be done at compile time. fn runtimeFactorial(n: u64) u64 { return if (n == 0) 1 else n * runtimeFactorial(n - 1); } """ **Anti-pattern:** Overusing "comptime", leading to increased compile times. Performing computations at compile time that depend on runtime input. **Zig-specific:** Zig's powerful "comptime" features allow for extensive code generation and optimization based on compile-time knowledge. ### 4.2. Inlining and Link-Time Optimization (LTO) **Standard:** Use the "inline" keyword to encourage the compiler to inline function calls. Enable Link-Time Optimization (LTO) during compilation to allow the compiler to perform cross-module optimizations. **Why:** Inlining eliminates function call overhead and allows for more aggressive optimizations. LTO enables optimizations across translation units, potentially improving performance. **Do This:** """zig // Force function to be inlined (if possible) inline fn add(a: i32, b: i32) i32 { return a + b; } pub fn main() !void { const result = add(5, 3); // Likely inlined. std.debug.print("Result: {}\n", .{result}); } //Compile with: zig build -Doptimize=ReleaseSmall """ **Don't Do This:** """zig // Avoid inlining very large or complex functions, as it can increase code size. """ **Anti-pattern:** Inlining functions excessively, leading to code bloat. Forgetting to enable LTO when building release versions. **Zig-specific:** Zig's LTO implementation can significantly improve performance, especially for larger projects. ### 4.3. Profiling and Benchmarking **Standard:** Use profiling tools (e.g., "perf", "valgrind", custom profiling code) to identify performance bottlenecks. Use benchmarking tools ("std.testing.benchmark") to measure the performance of critical code sections. **Why:** Profiling and benchmarking provide concrete data for optimization efforts. **Do This:** """zig const std = @import("std"); pub fn main() !void { const result = expensiveCalculation(100000); std.debug.print("Result: {}\n", .{result}); } // Function whose performance needs to be measured fn expensiveCalculation(n: i32) i32 { var result: i32 = 0; for (0..n) |i| { result += i; } return result; } test "benchmark expensiveCalculation" { const start = std.time.nanoTimestamp(); const result = expensiveCalculation(100000); const end = std.time.nanoTimestamp(); std.debug.print("Result: {}, Time taken: {} ns\n", .{result, end - start}); } """ **Anti-pattern:** Optimizing code without profiling. Making assumptions about performance without empirical data. **Zig-specific:** Zig's standard library includes basic benchmarking tools, but external profilers are often necessary for detailed analysis. Can use sampling based performance analysis tools. ### 4.4 Specialized Functions **Standard:** If you know something about the arguments being passed to a function, you can specialize a function for those cases. Use Zig's features to switch to optimized versions of code using "if" statments or comptime branching. **Why:** Allows you to cut instructions based on specific data or situations. **Do This:** """zig const std = @import("std"); pub fn main() !void { const result = optimizedCalculation(10); std.debug.print("Result: {}\n", .{result}); const result_comptime = comptime optimizedCalculation(10); std.debug.print("Comptime Result: {}\n", .{result_comptime}); } fn optimizedCalculation(a: i32) i32 { if (@hasDecl(@TypeOf(a), "Float")) { // Float specific return @floatToInt(i32, @intToFloat(f32, a) * 10.0); } else { return a * 10; } } test "benchmark optimizedCalculation" { const start = std.time.nanoTimestamp(); const result = optimizedCalculation(100000); const end = std.time.nanoTimestamp(); std.debug.print("Result: {}, Time taken: {} ns\n", .{result, end - start}); } """ **Anti-pattern:** Writing overly specific specialized function that are hard to maintain. **Zig-specific:** Zig's powerful "comptime" features allow you to generate specialized code which reduces complexity while improving performance. By adhering to these performance optimization standards, you can write Zig code that is not only efficient but also maintainable and understandable. Remember to profile and benchmark your code to identify performance bottlenecks and measure the impact of your optimizations.
# Testing Methodologies Standards for Zig This document outlines the recommended testing methodologies and best practices for Zig projects. Adhering to these guidelines will lead to more robust, maintainable, and reliable software. ## 1. Testing Pyramid and Levels ### 1.1. General Testing Strategy * **Do This:** Follow the testing pyramid concept. Prioritize unit tests, have fewer integration tests, and a minimal number of end-to-end tests. * **Don't Do This:** Rely heavily on end-to-end tests while neglecting unit and integration tests. This approach leads to slow feedback cycles and makes debugging difficult. **Why:** The testing pyramid ensures that the majority of your test cases are fast, isolated, and cheap to run (unit tests), while still providing confidence in the system's overall functionality (integration and end-to-end tests). ### 1.2. Unit Testing * **Do This:** Write focused unit tests that verify the behavior of individual functions, methods, or modules in isolation. * **Don't Do This:** Create unit tests that depend on external systems, databases, or network connections. Use mocks or stubs to isolate the unit under test. **Why:** Unit tests should provide rapid feedback on the correctness of your code. Dependencies on external systems can make tests slow, flaky, and difficult to maintain. **Example:** Testing a simple function. """zig const std = @import("std"); const assert = std.debug.assert; const adder = struct { pub fn add(a: i32, b: i32) i32 { return a + b; } }; test "test adder.add" { assert(adder.add(2, 3) == 5); assert(adder.add(-1, 1) == 0); assert(adder.add(-5, -5) == -10); } """ ### 1.3. Integration Testing * **Do This:** Use integration tests to verify the interaction between different modules or components within your system. * **Don't Do This:** Attempt to test the entire system at once. Focus on specific, important interactions. **Why:** Integration tests ensure that individual modules work correctly together. These tests catch issues that might not be apparent from unit tests alone. **Example:** Testing interaction between two modules. For illustrative purpose, assume both "adder" and "multiplier" are modules defined in separate files. """zig const std = @import("std"); const assert = std.debug.assert; const adder = @import("adder"); const multiplier = @import("multiplier"); test "test adder and multiplier integration" { const sum = adder.add(2, 3); const product = multiplier.multiply(sum, 4); assert(product == 20); } """ ### 1.4. End-to-End (E2E) Testing * **Do This:** Limit E2E tests to critical user flows and scenarios that validate the overall system functionality. * **Don't Do This:** Overuse E2E tests. They are slow, brittle, and expensive to maintain. **Why:** E2E tests simulate real user interaction with the system. Use them sparingly to ensure that the most critical functionality is working as expected. ## 2. Test-Driven Development (TDD) ### 2.1. Red-Green-Refactor * **Do This:** Follow the Red-Green-Refactor cycle: 1. Write a failing test (Red). 2. Write the minimum amount of code to make the test pass (Green). 3. Refactor the code to improve its structure, readability, and maintainability. * **Don't Do This:** Write code without tests or write tests after the code is already complete. **Why:** TDD leads to better code design, reduces defects, and provides comprehensive test coverage. **Example:** TDD implementation 1. **Red (Failing Test):** """zig const std = @import("std"); const assert = std.debug.assert; const StringReverser = struct { // Placeholder - To be implemented }; test "test string reversal" { var reverser = StringReverser{}; const reversed = reverser.reverse("hello"); assert(std.mem.eql(u8, reversed, "olleh")); } """ 2. **Green (Passing Test):** """zig const std = @import("std"); const assert = std.debug.assert; const StringReverser = struct { pub fn reverse(self: *StringReverser, input: []const u8) []const u8 { var reversed = std.ArrayList(u8).init(std.heap.page_allocator); // Use ArrayList for dynamic allocation // Reverse the string for (input) |c| { errdefer reversed.deinit(); try reversed.insert(0, c); } return reversed.items(); } }; test "test string reversal" { var reverser = StringReverser{}; const reversed = reverser.reverse("hello"); defer std.heap.page_allocator.free(reversed); // Important: Free the memory assert(std.mem.eql(u8, reversed , "olleh")); } """ 3. **Refactor:** (Further refactoring, such as error handling and optimizations, can be applied here.) ### 2.2. Boundary Conditions * **Do This:** Test boundary conditions (e.g., empty strings, maximum values, minimum values) to ensure that your code handles edge cases correctly. * **Don't Do This:** Only test typical scenarios. Neglecting boundary conditions can lead to unexpected errors. **Why:** Boundary conditions are often where bugs lurk. Testing these cases explicitly improves the robustness of your code. **Example:** Boundary conditions for a function calculating the average of an array. """zig const std = @import("std"); const assert = std.debug.assert; const calculator = struct { pub fn average(items: []const f64) ?f64 { if (items.len == 0) { return null; // Handle empty array } var sum: f64 = 0; for (items) |item| { sum += item; } return sum / @floatFromInt(items.len); } }; test "average of empty array" { assert(calculator.average([]const f64{}) == null); } test "average of single element array" { const arr = [_]f64{5.0}; const avgOpt = calculator.average(&arr); assert(avgOpt != null); assert(avgOpt.? == 5.0); } """ ## 3. Mocking and Stubbing ### 3.1. Isolating Units * **Do This:** Use mocking and stubbing to isolate the unit under test from its dependencies. * **Don't Do This:** Directly use real dependencies in your unit tests. **Why:** Mocking and stubbing ensure that your unit tests are fast, predictable, and focused on the behavior of the unit itself. ### 3.2. Test Doubles * **Do This:** Use test doubles (mocks, stubs, spies) to control the behavior of dependencies and verify interactions. Zig does not have built-in mocking frameworks, so custom implementations or community libraries (if available) are used. * **Don't Do This:** Create overly complex mocks that replicate the entire behavior of the dependency. Focus on the specific interactions that are relevant to the test. **Why:** Test doubles allow you to simulate different scenarios and verify that the unit under test interacts with its dependencies as expected. **Example:** Custom stub implementation in Zig. """zig const std = @import("std"); const assert = std.debug.assert; // Assume this is an external service we don't want to call directly in tests. const ExternalService = struct { pub fn getData(self: *ExternalService, id: i32) ![]const u8 { _ = id; // Suppress unused variable warning // In a real implementation, this would make a network request or use a database. return error.NotImplemented; } }; // Stub for the ExternalService const StubExternalService = struct { data: []const u8, pub fn getData(self: *StubExternalService, id: i32) ![]const u8 { _ = id; return self.data; } }; const DataProcessor = struct { service: *ExternalService, pub fn processData(self: *DataProcessor, id: i32) ![]const u8 { const data = try self.service.getData(id); // Some kind of data processing... return data; } }; test "test DataProcessor with StubExternalService" { const allocator = std.heap.page_allocator; var stub_service = StubExternalService{ .data = "stubbed data" }; var processor = DataProcessor{ .service = &stub_service }; const processed_data = try processor.processData(123); defer allocator.free(processed_data); assert(std.mem.eql(u8, processed_data, "stubbed data")); } """ ## 4. Asynchronous and Concurrent Testing ### 4.1. Dealing with Async Code * **Do This:** Manage asynchronous operations and concurrency explicitly in tests. Use mechanisms to synchronize test execution with the completion of async tasks using a testing frameworks (if created) or manual synchronization with atomics and mutexes. * **Don't Do This:** Rely on timing or "std.time.sleep" to "wait" for asynchronous operations. Such approach leads to flaky and unreliable tests. **Why:** Asynchronous code presents unique challenges for testing. Proper synchronization is crucial to ensure that tests are deterministic and reliable. ### 4.2. Synchronization Primitives * **Do This:** Use synchronization primitives such as "std.Thread.Mutex" and "std.Thread.Semaphore" when testing concurrent code. * **Don't Do This:** Forget proper teardown of threads and disposal of synchronisation objects causing memory leaks or hangs. **Why:** Synchronisation primitive are critical to correctly test concurrent code. **Example:** Testing basic async operation with manual synchronization (without a testing framework). """zig const std = @import("std"); const assert = std.debug.assert; // A function that simulates an asynchronous operation with a channel fn asyncOperationWithChannel(tx: chan i32, result: i32) void { std.time.sleep(10 * std.time.millisecond); // Simulate some work tx <- result; } // A simple channel type const chan = @Vector(1, i32); test "test async operation with channel" { var channel: chan i32 = undefined; var thread: std.Thread = undefined; var result: i32 = undefined; // Launch the async operation in a separate thread thread = std.Thread.spawn(.{}, asyncOperationWithChannel, .{ &channel, 42 }) catch unreachable; // Read the result from the channel result = <-channel; // Wait for the thread to finish thread.join(); assert(result == 42); } """ ## 5. Error Handling and Failure Injection ### 5.1. Testing Error Paths * **Do This:** Explicitly test error paths and ensure that your code handles errors gracefully. * **Don't Do This:** Only test the happy path. Neglecting error handling can lead to unexpected crashes and data corruption. **Why:** Robust error handling is essential for reliable software. Testing error paths ensures that your code can recover from failures gracefully. ### 5.2. Failure Injection * **Do This:** Use failure injection techniques (e.g., simulating network outages, disk errors, or memory allocation failures) to test the resilience of your code. * **Don't Do This:**. Assume that external resources and dependencies will always be available. **Why:** Failure injection helps you identify and fix potential weaknesses in your code's error handling logic. **Example:** Injecting failure by intercepting allocation requests and returning "error.OutOfMemory". Note: this requires advanced Zig knowledge and should be used with caution. """zig const std = @import("std"); const assert = std.debug.assert; // Custom allocator that can simulate out-of-memory errors pub const FailingAllocator = struct { pub fn alloc(self: *@This(), len: usize, alignment: u29) ?[*]u8 { _ = alignment; // Suppress unused variable warning. if (self.should_fail) { self.should_fail = false; // Only fail once return null; } return self.backing_allocator.alloc(len); } pub fn free(self: *@This(), ptr: [*]u8, len: usize, alignment: u29) void { _ = alignment; // Suppress unused variable warning. self.backing_allocator.free(ptr, len); } backing_allocator: std.mem.Allocator, should_fail: bool, }; test "test with failing allocator" { var backing_allocator = std.heap.page_allocator; var failing_allocator = FailingAllocator{ .backing_allocator = backing_allocator, .should_fail = true, }; // Try to allocate some memory using the failing allocator const result = failing_allocator.alloc(1024, 1); assert(result == null); // Now try again, it should succeed failing_allocator.should_fail = false; const result2 = failing_allocator.alloc(1024,1) catch unreachable; defer failing_allocator.free(result2, 1024, 1); assert(result2 != null); } """ ## 6. Code Coverage ### 6.1. Measuring Coverage * **Do This:** Use code coverage tools to measure the percentage of your code that is executed by tests. Tools like "llvm-cov" can be integrated into the build system. * **Don't Do This:** Rely solely on code coverage metrics to assess the quality of your tests. High coverage does not necessarily mean that your tests are effective. **Why:** Code coverage provides insights into the areas of your code that are not being adequately tested. ### 6.2. Interpreting Results * **Do This:** Use code coverage reports to identify gaps in your test suite and write additional tests to cover those areas. * **Don't Do This:** Aim for 100% code coverage at all costs. Focus on testing the most critical and complex parts of your system. **Why:** Code coverage helps you prioritize your testing efforts and improve the overall quality of your code. ## 7. Property Based Testing ### 7.1 Using Generators * **Do This**: Use property-based testing frameworks to generate various input values to test different scenarios. Since there are no mature frameworks for zig, one might need to be built. * **Don't Do This**: Assume that providing a few fixed parameters covers the entire range of possibilities for the code being tested. Property-based testing helps discover unexpected edge cases. **Why:** Property-based testing automatically generates a large number of test cases based on predefined properties, which can uncover edge cases and unexpected behavior. **Example**: A rough example of property-based testing implementation(concept). """zig const std = @import("std"); const testing = @import("testing"); // Hypothetical testing framework const assert = std.debug.assert; /// Function to be tested fn add(a: i32, b: i32) i32 { return a + b; } test "add function always yields correct sum" { testing.property("Addition is commutative", struct { fn generate() testing.TestCase { const a = testing.random.int(i32); const b = testing.random.int(i32); return .{ .input = .{ .a = a, .b = b } }; } fn check(case: testing.TestCase) bool { const a = case.input.a; const b = case.input.b; return add(a,b) == add(b,a); } }); testing.property("Addition result is always larger", struct { fn generate() testing.TestCase { const a = testing.random.int(i32); const b = testing.random.int(i32); return .{ .input = .{ .a = a, .b = b } }; } fn check(case: testing.TestCase) bool { const a = case.input.a; const b = case.input.b; return add(a,b) >= a; } }); } """ This example demonstrates how using a purely hypothetical library, you'd define properties about the "add" function, such as commutativity. ## 8. Continuous Integration ### 8.1. Automated Testing * **Do This:** Integrate your tests into a CI/CD pipeline to automatically run tests on every commit or pull request. * **Don't Do This:** Rely on manual testing. Automated testing ensures that defects are detected early and often. **Why:** Continuous integration helps you maintain a high level of code quality and prevent regressions. ### 8.2. Reporting * **Do This:** Generate test reports and dashboards to track test results, code coverage, and other relevant metrics. * **Don't Do This:** Ignore failing tests. Failing tests should be addressed immediately to prevent further issues. **Why:** Test reports and dashboards provide visibility into the health of your codebase and help you identify areas that need improvement. ## 9. Performance Testing ### 9.1 Proper Tooling * **Do This:** Utilise Zig's built-in features for benchmarking code and conduct performance testing. Tools such as "zig build test -Drelease-safe" or "zig build test -Drelease-fast" with cycle counters allows you to accurately track the execution time of critical section of code. * **Don't Do This:** Trust premature optimisation results without confirming the true bottleneck in the system. **Why:** Performance testing is essential for optimising and ensuring that code is as efficient as possible. **Example**: """zig const std = @import("std"); const assert = std.debug.assert; // Function to benchmark fn calculateSum(n: usize) usize { var sum: usize = 0; for (0..n) |i| { sum += i; } return sum; } test "benchmark calculateSum" { const start = std.time.nanoTimestamp(); const result = calculateSum(1000000); // 1 million const end = std.time.nanoTimestamp(); const duration = end - start; std.debug.print("calculateSum(1000000) = {}\n", .{result}); std.debug.print("Time taken: {} ns\n", .{duration}); assert(result == 499999500000); // Verify correctness } """ ## 10. Security Testing ### 10.1 Common Vulnerabilities * **Do This:** Check for common vulnerability types, such as buffer overflows, use after free, and format string bugs. Use static analysis tools and fuzzing approaches to find security vulnerabilities. * **Don't Do This:** Assume that the code is secure without conducting security specific tests. ### 10.2 Fuzzing * **Do This:** For security sensitive code, integrating a fuzzer such as "z fuzz" will help uncover inputs that may crash or cause vulnerability within the system. * **Don't Do This:** Avoid fuzzing as part of the development lifecycle. **Example**: (Hypothetical usage with a future "z fuzz" tool) """zig // Example code to be fuzzed (potentially vulnerable to buffer overflow) fn copyString(dest: []u8, source: []const u8) void { for (source, 0..) |char, i| { dest[i] = char; // Potential buffer overflow if source is larger than dest } } // Fuzz test function export fn zig_fuzz(data: []const u8) void { var buffer: [10]u8 = undefined; // Perform test copy - will cause crash if overflow copyString(&buffer, data); } """ Running "z fuzz" on this example, the tool would generate various random inputs for the data parameter and attempt to find inputs that cause the "copyString" function to write past the end of dest (buffer), flagging a buffer overflow.