Backlog/lib/sdl3/parser/AGENTS.md

9.4 KiB

Agent Solutions Guide: Zig 0.15 Issues

This document catalogs common issues encountered when working with Zig 0.15 and their solutions. Written for AI coding assistants to avoid repeating mistakes.

Critical: ArrayList API Changed in Zig 0.15

Problem

std.ArrayList is now an alias to std.ArrayListUnmanaged in Zig 0.15. The managed version has been removed.

Old (Pre-0.15) Code - DOES NOT WORK

var list = std.ArrayList(u8).init(allocator);
defer list.deinit();
try list.append(item);

New (0.15+) Code - CORRECT

// Empty initialization
var list = std.ArrayList(u8){};
defer list.deinit(allocator);
try list.append(allocator, item);

// Or with capacity
var list = try std.ArrayList(u8).initCapacity(allocator, 100);
defer list.deinit(allocator);
try list.append(allocator, item);

Key Changes

  1. Initialization: Use {} or initCapacity(), not init()
  2. All methods take allocator: append(allocator, item) not append(item)
  3. Deinit takes allocator: deinit(allocator) not deinit()

AST Rendering API Changed

Problem

The ast.render() function signature changed in Zig 0.15.

Old Code - DOES NOT WORK

var ast = try std.zig.Ast.parse(allocator, source, .zig);
const output = try ast.render(allocator);

New Code - CORRECT

var ast = try std.zig.Ast.parse(allocator, source, .zig);
const output = try ast.renderAlloc(allocator);
defer allocator.free(output);

The API

  • renderAlloc(allocator) - Returns allocated string
  • render(tree, gpa, writer, fixups) - Low-level version for custom output

Type Conversion: SDL Types to Zig

Pointer Types

C Type Zig Type Notes
const char * [*c]const u8 C string
void * ?*anyopaque Nullable any pointer
const void * ?*const anyopaque Const version
SDL_Type * ?*Type Nullable pointer to opaque/struct
const SDL_Type * *const Type Non-null const pointer
SDL_Type ** ?*?*Type Output parameter (double pointer)
SDL_Type *const * [*c]*const Type Array of const pointers
Uint32 * *u32 Output parameter (primitive)

Key Principles

  1. Non-nullable by default for const pointers to structs
  2. Nullable (?*) for pointers that can be NULL
  3. Use * not [*c] when you know it's not a C-style array
  4. Double pointers: ?*?*Type for output parameters

Function Signature Formatting

Trailing Commas

Only use trailing commas for functions with more than 3 parameters. This triggers multi-line formatting.

// 1-3 parameters: single line, no trailing comma
pub fn foo(a: i32, b: i32, c: i32) void {}

// 4+ parameters: multi-line with trailing comma
pub fn bar(
    a: i32,
    b: i32,
    c: i32,
    d: i32,
) void {}

Why?

  • Trailing comma with no parameters: (,) is syntax error
  • Trailing comma with 1-3 params: unnecessary, wastes vertical space
  • Trailing comma with 4+ params: makes diffs cleaner, easier to read

Method Organization

Place Methods Inside Opaque Types

Functions where the first parameter is a pointer to an opaque type should be methods:

// Good - method syntax
pub const GPUDevice = opaque {
    pub fn destroy(device: *GPUDevice) void {
        c.SDL_DestroyGPUDevice(device);
    }
};

// Usage: device.destroy()

// Bad - standalone function
pub fn destroyGPUDevice(device: ?*GPUDevice) void {
    c.SDL_DestroyGPUDevice(device);
}

// Usage: destroyGPUDevice(device)

Benefits

  1. Cleaner API: device.create() vs createGPUDevice(device)
  2. IDE autocomplete works better
  3. Namespacing prevents naming conflicts
  4. More idiomatic Zig

Casting Guidelines

When to Cast

Scenario Cast Example
Opaque pointer @ptrCast @ptrCast(device)
Flags (packed struct) @bitCast @bitCast(flags)
Enum to int @intFromEnum @intFromEnum(enum_val)
Struct passed by value None Just pass it
Const pointer to struct @ptrCast @ptrCast(info)

Don't Over-Cast

// Bad - unnecessary cast for value type
fn setColor(color: FColor) void {
    c.SDL_SetColor(@bitCast(color)); // Wrong!
}

// Good - no cast needed
fn setColor(color: FColor) void {
    c.SDL_SetColor(color); // Correct
}

StringHashMap Usage

Correct Pattern

var map = std.StringHashMap(ValueType).init(allocator);
defer map.deinit(); // No allocator needed for deinit

try map.put("key", value);
const val = map.get("key");

Iteration

var it = map.keyIterator();
while (it.next()) |key| {
    // Use key.*
}

var it = map.valueIterator();
while (it.next()) |value| {
    // Use value.* if needed
}

Common Pitfalls

1. Forgetting Allocator in Unmanaged Collections

// Wrong
list.append(item);

// Right
list.append(allocator, item);

2. Using .init() on ArrayList

// Wrong
var list = std.ArrayList(u8).init(allocator);

// Right
var list = std.ArrayList(u8){};
// or
var list = try std.ArrayList(u8).initCapacity(allocator, size);

3. Not Checking AST Errors Before Rendering

// Wrong - will panic if there are errors
const output = try ast.renderAlloc(allocator);

// Right - check first
if (ast.errors.len > 0) {
    // Handle errors
    return error.ParseError;
}
const output = try ast.renderAlloc(allocator);

4. Incorrect Double Pointer Types

// Wrong - C-style for output params
texture: [*c]*GPUTexture

// Right - Zig optional pointers
texture: ?*?*GPUTexture

Testing Patterns

Simple Test

test "description" {
    const result = try someFunction();
    try std.testing.expectEqual(expected, result);
}

Test with Allocator

test "with allocator" {
    const allocator = std.testing.allocator;
    const result = try allocateAndDoSomething(allocator);
    defer allocator.free(result);
    
    try std.testing.expectEqualStrings("expected", result);
}

Build System Integration

Adding Parser to Dependencies

// build.zig.zon
.dependencies = .{
    .sdl3_parser = .{ .path = "parser/" },
},

// build.zig
const parser_dep = b.dependency("sdl3_parser", .{
    .target = target,
    .optimize = optimize,
});
const parser_exe = parser_dep.artifact("sdl-parser");

Run Step

const run_parser = b.addRunArtifact(parser_exe);
run_parser.addFileArg(b.path("input.h"));
run_parser.addArg("--output=output.zig");

const step = b.step("generate", "Generate bindings");
step.dependOn(&run_parser.step);

Quick Reference Card

// Collections
var list = std.ArrayList(T){};
defer list.deinit(allocator);
try list.append(allocator, item);

var map = std.StringHashMap(V).init(allocator);
defer map.deinit();
try map.put("key", value);

// AST
var ast = try std.zig.Ast.parse(allocator, source, .zig);
defer ast.deinit(allocator);
const formatted = try ast.renderAlloc(allocator);
defer allocator.free(formatted);

// Type Patterns
?*Type           // Nullable pointer
*const Type      // Non-null const pointer
?*?*Type         // Output parameter
[*c]*const Type  // C array of const pointers

// Casts
@ptrCast(ptr)      // Pointers
@bitCast(value)    // Packed structs, flags
@intFromEnum(e)    // Enum to int
// No cast for value types!

Version Info

  • Zig Version: 0.15.2
  • Date: 2025-01-22
  • SDL Version: 3.2.0

Issues Encountered During Mock Testing Implementation

Issue 1: Build.addStaticLibrary Removed

Problem: Zig 0.15 removed b.addStaticLibrary() method.

Error:

error: no field or member function named 'addStaticLibrary' in 'Build'

Solution: Use b.addLibrary() with .linkage = .static:

// OLD - Does not work
const lib = b.addStaticLibrary(.{
    .name = "mylib",
    .target = target,
    .optimize = optimize,
});

// NEW - Correct for Zig 0.15
const lib = b.addLibrary(.{
    .name = "mylib",
    .linkage = .static,
    .root_module = b.createModule(.{
        .target = target,
        .optimize = optimize,
    }),
});

Key change: Must create root_module explicitly with target/optimize.

Issue 2: C Mock Type Definitions

Problem: Generated C mocks referenced SDL types like Uint32, SDL_Window, FColor that weren't defined when using only stdint.h/stdbool.h.

Error:

error: unknown type name 'Uint32'
error: unknown type name 'SDL_GPUColorTargetInfo'

Solution: Include actual SDL headers in generated mocks:

// OLD - Missing types
#include <stdint.h>
#include <stdbool.h>
#include <stddef.h>

// NEW - Proper type definitions
#include <SDL3/SDL_stdinc.h>
#include <SDL3/SDL_gpu.h>

Then add SDL include path to C compilation:

mock_lib.addIncludePath(b.path("SDL/include"));

Key insight: Mocks should compile like real SDL implementation files, with full access to SDL type definitions.

Issue 3: Testing Strategy

Problem: Initial testing with tiny test_small.h (3 declarations) didn't reveal real-world issues.

Solution: Test with full production header (SDL_gpu.h with 169 declarations) to:

  • Verify parser handles large inputs
  • Catch type definition issues
  • Validate all declaration types work together
  • Ensure build system scales

Lesson: Always test with realistic, production-sized inputs, not toy examples.

References