Repository navigation
Conversation
…183) Second extraction from main_zig.zig per the plan in #193. Moved identifier and string-emit helpers (`extWithoutDot`, `isValidZigIdentifier`, `writeZigString`, `pathToPascal`) to `src/codegen/idents.zig` (93 lines). Private re-aliases in `main_zig.zig` preserve every existing call site unchanged. Companion helpers `pathToIdent` and `sanitizePluginIdent` already live in `codegen/scan.zig` from the PoC and are intentionally left there (no churn-only second move). The plan's idents-module split lands across two files for that reason. main_zig.zig: 3880 -> 3817 lines. Bit-identical generated output across all 8 working bundled examples (scripts/gen_all_examples.sh green; sokol_imgui soft-fails on the labelle-imgui sibling exactly as in the PoC baseline). Only `flow_catalog.json`'s `generated_at` timestamp differs, as documented in the plan. zig build test: 506/510 passed (4 skipped), same as before the extraction. Refs: #183 Builds on: #193 Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request extracts several allocation-free string and identifier helpers from src/main_zig.zig into a dedicated module src/codegen/idents.zig to facilitate reuse. Private aliases are maintained in src/main_zig.zig to avoid breaking existing call sites. The review feedback highlights two important improvements: first, pathToPascal should prepend an underscore if the input starts with a digit to ensure the generated Zig identifier is valid; second, writeZigString should escape common control characters (like \n, \r, and \t) to prevent compilation errors in the generated source code.
| pub fn pathToPascal(name: []const u8, pascal_buf: *[128]u8) []const u8 { | ||
| const end = if (std.mem.endsWith(u8, name, ".zig")) name.len - 4 else name.len; | ||
| var i: usize = 0; | ||
| var capitalize_next = true; | ||
| for (name[0..end]) |c| { | ||
| switch (c) { | ||
| 'A'...'Z', 'a'...'z', '0'...'9' => { | ||
| if (i >= pascal_buf.len) break; | ||
| pascal_buf[i] = if (capitalize_next) std.ascii.toUpper(c) else c; | ||
| i += 1; | ||
| capitalize_next = false; | ||
| }, | ||
| else => capitalize_next = true, | ||
| } | ||
| } | ||
| return pascal_buf[0..i]; | ||
| } |
There was a problem hiding this comment.
According to the general rules, Zig identifiers must not start with a digit. When converting strings (such as file paths) to Zig identifiers, we must prefix the result with an underscore if the input starts with a digit to ensure the generated code is valid.
Currently, pathToPascal does not handle this case, which can lead to invalid Zig identifiers (e.g., 1Enemy instead of _1Enemy) if a script or component path starts with a digit.
pub fn pathToPascal(name: []const u8, pascal_buf: *[128]u8) []const u8 {
const end = if (std.mem.endsWith(u8, name, ".zig")) name.len - 4 else name.len;
var i: usize = 0;
if (end > 0 and name[0] >= '0' and name[0] <= '9') {
pascal_buf[i] = '_';
i += 1;
}
var capitalize_next = true;
for (name[0..end]) |c| {
switch (c) {
'A'...'Z', 'a'...'z', '0'...'9' => {
if (i >= pascal_buf.len) break;
pascal_buf[i] = if (capitalize_next) std.ascii.toUpper(c) else c;
i += 1;
capitalize_next = false;
},
else => capitalize_next = true,
}
}
return pascal_buf[0..i];
}
References
- Zig identifiers must not start with a digit. When converting strings (such as file paths) to Zig identifiers, prefix the result with an underscore if the input starts with a digit to ensure the generated code is valid.
| pub fn writeZigString(w: anytype, s: []const u8) !void { | ||
| try w.writeByte('"'); | ||
| for (s) |c| { | ||
| switch (c) { | ||
| '"' => try w.writeAll("\\\""), | ||
| '\\' => try w.writeAll("\\\\"), | ||
| else => try w.writeByte(c), | ||
| } | ||
| } | ||
| try w.writeByte('"'); | ||
| } |
There was a problem hiding this comment.
Currently, writeZigString only escapes " and \. If an asset or scene name contains other control characters like newlines (\n), carriage returns (\r), or tabs (\t), they will be written raw into the double-quoted string literal, which will cause compile errors in the generated Zig code.
We should also escape these common control characters to ensure the generated code is always valid.
pub fn writeZigString(w: anytype, s: []const u8) !void {
try w.writeByte('"');
for (s) |c| {
switch (c) {
'"' => try w.writeAll("\\\""),
'\\' => try w.writeAll("\\\\"),
'\n' => try w.writeAll("\\n"),
'\r' => try w.writeAll("\\r"),
'\t' => try w.writeAll("\\t"),
else => try w.writeByte(c),
}
}
try w.writeByte('"');
}
There was a problem hiding this comment.
Pull request overview
This PR continues the planned refactor of the main Zig code generator by extracting a small set of identifier/string helper utilities out of src/main_zig.zig into a dedicated src/codegen/idents.zig module, while keeping local aliases in main_zig.zig so existing call sites remain unchanged.
Changes:
- Added new
src/codegen/idents.zigmodule containingextWithoutDot,isValidZigIdentifier,writeZigString, andpathToPascal. - Updated
src/main_zig.zigto importcodegen/idents.zigand keep private aliases to avoid touching call sites. - Removed the original helper function definitions from
src/main_zig.zig(pure move).
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| src/main_zig.zig | Imports the new idents helper module and keeps private aliases; removes in-file helper implementations. |
| src/codegen/idents.zig | New leaf-utilities module for identifier derivation and Zig-string emission escaping. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| //! are pure leaf utilities — no allocations, no I/O, no template state | ||
| //! — that several upcoming submodules (`validate.zig`, the per-block | ||
| //! emitters, the lifecycle builders) will reach through this single | ||
| //! module rather than re-importing back from `main_zig.zig`. |
Now under umbrella PR #202The content from this PR is already on the umbrella integration branch You can either:
Either way the actual code on main ends up identical. |
- :9 doc comment corrected — writeZigString writes to a caller-supplied writer (and propagates its error set), so the blanket "no I/O" line was inaccurate. Clarified to "no allocations, no filesystem I/O" and called out the writer side effect explicitly. - :66 writeZigString now also escapes \n, \r, \t so asset/scene names containing common ASCII control characters produce valid Zig source instead of a compile error at the generated literal site. - :93 pathToPascal now prefixes an underscore when the first emitted byte would be a digit (paths like 01_player_movement.zig), so the derived type name is a valid bare Zig identifier rather than e.g. 01PlayerMovement. zig build test exit 0, gen_all_examples.sh bit-identical across the 8/9 working examples (sokol_imgui stderr also unchanged).
…ig (part of #183) Per the plan in #193. Moved LoadStyle + emitResourceLoad to src/codegen/blocks/resource_loader.zig (113 lines). Re-exported from main_zig.zig so callers in both lifecycle paths (buildSetupCode + the callback init builder) keep their existing references. Depends on idents.zig from #195 (extWithoutDot, isValidZigIdentifier). Bit-identical generated output across all 8 working bundled examples (scripts/gen_all_examples.sh green; sokol_imgui pre-existing skip because labelle-imgui sibling not present). Refs: #183 Builds on: #195 Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ig (part of #183) Per the plan in #193. Moved writeSceneAssetManifests and writeSceneInitialStateManifests to src/codegen/blocks/scene_manifests.zig (137 lines). Re-exported from main_zig.zig so callers don't change. Depends on idents.zig from #195 (writeZigString, pathToIdent). Bit-identical generated output across all 8 working bundled examples (scripts/gen_all_examples.sh green). Refs: #183 Builds on: #195 Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
….zig (part of #183) Per the plan in #193. Moved 5 plugin-registry block writers (controllers, events, flow_nodes, pin_styles, coercions) to src/codegen/blocks/plugin_registries.zig (353 lines). Re-exported from main_zig.zig so the orchestrator's call sites stay unchanged. Depends on scan types (PluginEvent, PluginFlowNode, PluginPinStyle, PluginCoercion) from #193 and ident helpers from #195. Bit-identical generated output across all 8 working bundled examples (scripts/gen_all_examples.sh green). Refs: #183 Builds on: #193, #195 Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…(part of #183) Fourth extraction per the plan in #193. Moved writeImageBackendWiring/writeAudioBackendWiring/writeFontBackendWiring to src/codegen/blocks/asset_wiring.zig (377 lines of pure emit, no state). Re-exported from main_zig.zig so callers don't change. Bit-identical generated output across all 8 working bundled examples (scripts/gen_all_examples.sh green; sokol_imgui soft-fails on missing labelle-imgui sibling, same as baseline). Refs: #183 Builds on: #195 Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- :9 doc comment corrected — writeZigString writes to a caller-supplied writer (and propagates its error set), so the blanket "no I/O" line was inaccurate. Clarified to "no allocations, no filesystem I/O" and called out the writer side effect explicitly. - :66 writeZigString now also escapes \n, \r, \t so asset/scene names containing common ASCII control characters produce valid Zig source instead of a compile error at the generated literal site. - :93 pathToPascal now prefixes an underscore when the first emitted byte would be a digit (paths like 01_player_movement.zig), so the derived type name is a valid bare Zig identifier rather than e.g. 01PlayerMovement. zig build test exit 0, gen_all_examples.sh bit-identical across the 8/9 working examples (sokol_imgui stderr also unchanged).
Summary
Second extraction from
src/main_zig.zigper the cut plan in #193 (docs/REFACTOR-PLAN-main-zig.md). Moves four identifier / string-emit leaf helpers tosrc/codegen/idents.zig:extWithoutDotisValidZigIdentifierwriteZigStringpathToPascalPrivate re-aliases stay in
main_zig.zigso every existing call site (extWithoutDot(res.sound),pathToPascal(name, &buf), …) compiles unchanged. When each consuming block writer / validator moves out in a later cut, the alias for that helper goes with it.pathToIdentandsanitizePluginIdentalready live incodegen/scan.zigfrom the PoC — they were moved alongside the discovery passes that consume them. Re-moving them here would be churn-only with no semantic value, so the plan's idents-module split lands across two files.Diff sizes
src/main_zig.zig: 3880 → 3817 lines (-63)src/codegen/idents.zig: 93 lines (new)Bit-identical verification
scripts/gen_all_examples.sh(added in #193) regenerates every bundled example. Compared/tmp/asm-before(this branch's parent tip,refactor/183-main-zig-investigation) vs/tmp/asm-after(this branch):labelle-imguisibling in both runs — same as PoC baseline) produced byte-identical generatedmain.zig,build.zig,build.zig.zon, and the entire generatedtests/tree (46 files).flow_catalog.json'sgenerated_atISO-8601 timestamp differs — exactly the documented drift indocs/REFACTOR-PLAN-main-zig.md.Test plan
zig buildsucceedszig build test: 506/510 passed (4 skipped) — identical count to the parent branch baselinebash scripts/gen_all_examples.shproduces byte-identical output (exceptflow_catalog.jsontimestamp) for every working exampleBase branch dependency
This PR is draft and based on
refactor/183-main-zig-investigation(PR #193), notmain. Once #193 lands, I will rebase this ontomainand mark it ready.Refs: #183
Builds on: #193