diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 993472a..9f37d0a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,13 +7,17 @@ on: jobs: build: runs-on: ubuntu-24.04 + strategy: + fail-fast: false + matrix: + zig-version: ['0.16.0', '0.17.0'] permissions: contents: read steps: - uses: actions/checkout@v4 - uses: mlugg/setup-zig@v2 with: - version: 0.16.0 + version: ${{ matrix.zig-version }} - run: zig fmt --check . - run: zig build test --summary all - run: zig build conformance diff --git a/CHANGELOG.md b/CHANGELOG.md index 36d8ba2..8c80ad0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Added +- Support for Zig 0.17, alongside 0.16 + ## [0.1.0] - 2026-09-16 Initial release. diff --git a/README.md b/README.md index bf3bc08..baf46cb 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ const json = b.dependency("json", .{ exe.root_module.addImport("json", json.module("json")); ``` -Requires Zig 0.16.0. +Requires Zig 0.16 or 0.17. ## Usage diff --git a/build.zig b/build.zig index 267d6be..c937ce4 100644 --- a/build.zig +++ b/build.zig @@ -35,11 +35,20 @@ pub fn build(b: *std.Build) !void { conformance.root_module.addImport("json", json_module); const options = b.addOptions(); - options.addOptionPath("suite_path", b.path("test/JSONTestSuite/test_parsing")); + const suite_path = b.path("test/JSONTestSuite/test_parsing"); + if (comptime @hasDecl(std.Build.Step.Options, "addOptionPathDirectory")) { + options.addOptionPathDirectory("suite_path", suite_path); + } else { + options.addOptionPath("suite_path", suite_path); + } conformance.root_module.addOptions("build_options", options); const run_conformance = b.addRunArtifact(conformance); - if (b.args) |args| run_conformance.addArgs(args); + if (comptime @hasDecl(std.Build.Step.Run, "addPassthruArgs")) { + run_conformance.addPassthruArgs(); + } else if (b.args) |args| { + run_conformance.addArgs(args); + } const conformance_step = b.step("conformance", "Run the JSONTestSuite conformance suite"); conformance_step.dependOn(&run_conformance.step); diff --git a/src/compat.zig b/src/compat.zig new file mode 100644 index 0000000..8ec67db --- /dev/null +++ b/src/compat.zig @@ -0,0 +1,60 @@ +//! Type reflection helpers that work on both Zig 0.16 and 0.17. +//! +//! Zig 0.17 changed `@typeInfo` for containers from a slice of field structs +//! (`fields`) to parallel slices (`field_names`, `field_types`, +//! `field_attrs`). These helpers present the 0.16 shape on both. + +const std = @import("std"); + +const new_type_info = @hasField(std.builtin.Type.Struct, "field_names"); + +pub const StructField = if (new_type_info) struct { + name: [:0]const u8, + type: type, + default_value_ptr: ?*const anyopaque, + is_comptime: bool, + alignment: ?usize, + + pub inline fn defaultValue(comptime sf: StructField) ?sf.type { + const dp: *const sf.type = @ptrCast(@alignCast(sf.default_value_ptr orelse return null)); + return dp.*; + } +} else std.builtin.Type.StructField; + +pub const UnionField = if (new_type_info) struct { + name: [:0]const u8, + type: type, + alignment: ?usize, +} else std.builtin.Type.UnionField; + +pub fn structFields(comptime T: type) []const StructField { + if (!new_type_info) return @typeInfo(T).@"struct".fields; + return comptime blk: { + const info = @typeInfo(T).@"struct"; + var fields: [info.field_names.len]StructField = undefined; + for (&fields, info.field_names, info.field_types, info.field_attrs) |*field, name, Field, attrs| { + field.* = .{ + .name = name, + .type = Field, + .default_value_ptr = attrs.default_value_ptr, + .is_comptime = attrs.@"comptime", + .alignment = attrs.@"align", + }; + } + const final = fields; + break :blk &final; + }; +} + +pub fn unionFields(comptime T: type) []const UnionField { + if (!new_type_info) return @typeInfo(T).@"union".fields; + return comptime blk: { + const info = @typeInfo(T).@"union"; + var fields: [info.field_names.len]UnionField = undefined; + for (&fields, info.field_names, info.field_types, info.field_attrs) |*field, name, Field, attrs| { + field.* = .{ .name = name, .type = Field, .alignment = attrs.@"align" }; + } + const final = fields; + break :blk &final; + }; +} diff --git a/src/decode.zig b/src/decode.zig index 2a5259a..cc0c95e 100644 --- a/src/decode.zig +++ b/src/decode.zig @@ -13,6 +13,7 @@ const std = @import("std"); const Reader = std.Io.Reader; const Allocator = std.mem.Allocator; const json = @import("json.zig"); +const compat = @import("compat.zig"); const escapedLiteral = @import("encode.zig").escapedLiteral; pub const DecodeError = error{ @@ -858,9 +859,9 @@ pub const Decoder = struct { fn enumValue(d: *Decoder, comptime T: type) DecodeError!T { var buf: [maxTagLen(T)]u8 = undefined; const name = (try d.boundedString(&buf)) orelse return error.InvalidEnumTag; - inline for (@typeInfo(T).@"enum".fields) |f| { - if (name.len == f.name.len and std.mem.eql(u8, name, f.name)) { - return @field(T, f.name); + inline for (comptime std.meta.fieldNames(T)) |tag_name| { + if (name.len == tag_name.len and std.mem.eql(u8, name, tag_name)) { + return @field(T, tag_name); } } return error.InvalidEnumTag; @@ -882,7 +883,7 @@ pub const Decoder = struct { try d.expectByte(':'); var result: ?T = null; - inline for (info.fields) |f| { + inline for (comptime compat.unionFields(T)) |f| { if (result == null and name.len == f.name.len and std.mem.eql(u8, name, f.name)) { result = @unionInit(T, f.name, try d.value(f.type)); } @@ -904,7 +905,7 @@ pub const Decoder = struct { const name = (try d.boundedString(&name_buf)) orelse return error.UnknownUnionVariant; var result: ?T = null; - inline for (info.fields) |f| { + inline for (comptime compat.unionFields(T)) |f| { if (result == null and name.len == f.name.len and std.mem.eql(u8, name, f.name)) { if (f.type == void) { // Nothing to hoist, so run the member loop over an @@ -958,10 +959,10 @@ pub const Decoder = struct { comptime options: json.StructOptions, comptime continuing: bool, ) DecodeError!T { - const fields = @typeInfo(T).@"struct".fields; + const fields = comptime compat.structFields(T); var result: T = undefined; - var seen = std.bit_set.StaticBitSet(fields.len).initEmpty(); + var seen = std.bit_set.StaticBitSet(fields.len).empty; const has_members = if (continuing) switch (try d.peekByte()) { ',' => blk: { @@ -1224,7 +1225,7 @@ fn jsonKey(comptime T: type, comptime field_name: []const u8) []const u8 { fn maxKeyLen(comptime T: type) usize { comptime { var m: usize = 1; - for (@typeInfo(T).@"struct".fields) |f| m = @max(m, jsonKey(T, f.name).len); + for (compat.structFields(T)) |f| m = @max(m, jsonKey(T, f.name).len); return m; } } @@ -1232,15 +1233,7 @@ fn maxKeyLen(comptime T: type) usize { fn maxTagLen(comptime T: type) usize { comptime { var m: usize = 1; - switch (@typeInfo(T)) { - .@"enum" => |info| for (info.fields) |f| { - m = @max(m, f.name.len); - }, - .@"union" => |info| for (info.fields) |f| { - m = @max(m, f.name.len); - }, - else => unreachable, - } + for (std.meta.fieldNames(T)) |name| m = @max(m, name.len); return m; } } diff --git a/src/encode.zig b/src/encode.zig index 8996be4..fdb97dd 100644 --- a/src/encode.zig +++ b/src/encode.zig @@ -8,6 +8,7 @@ const std = @import("std"); const Writer = std.Io.Writer; const json = @import("json.zig"); +const compat = @import("compat.zig"); pub const EncodeError = Writer.Error || error{NonFiniteFloat}; @@ -151,7 +152,7 @@ fn encodeUnion(comptime T: type, value: T, w: *Writer, opts: EncodeOptions) Enco /// has at least one member. fn encodeStructFieldsAfter(comptime T: type, value: T, w: *Writer, opts: EncodeOptions) EncodeError!void { const options = comptime json.structOptions(T); - inline for (@typeInfo(T).@"struct".fields) |f| { + inline for (comptime compat.structFields(T)) |f| { const omit = options.omit_null_fields and @typeInfo(f.type) == .optional and @field(value, f.name) == null; @@ -201,14 +202,14 @@ fn fieldKey(comptime T: type, comptime field_name: []const u8) []const u8 { fn canOmitFields(comptime T: type) bool { const options = comptime json.structOptions(T); if (!options.omit_null_fields) return false; - for (@typeInfo(T).@"struct".fields) |f| { + for (compat.structFields(T)) |f| { if (@typeInfo(f.type) == .optional) return true; } return false; } fn encodeStruct(comptime T: type, value: T, w: *Writer, opts: EncodeOptions) EncodeError!void { - const fields = @typeInfo(T).@"struct".fields; + const fields = comptime compat.structFields(T); const options = comptime json.structOptions(T); try w.writeByte('{'); diff --git a/src/test.zig b/src/test.zig index e16ece8..cc3b510 100644 --- a/src/test.zig +++ b/src/test.zig @@ -376,7 +376,7 @@ test "decode from a trickling stream at every buffer size" { test "a string far larger than the reader buffer" { const a = std.testing.allocator; - const long = "x" ** 500; + const long = &@as([500]u8, @splat('x')); const doc = "{\"s\":\"" ++ long ++ "\"}"; const T = struct { s: []const u8 }; @@ -392,7 +392,7 @@ test "a string far larger than the reader buffer" { test "a skipped value far larger than the reader buffer" { const a = std.testing.allocator; - const long = "y" ** 400; + const long = &@as([400]u8, @splat('y')); const doc = "{\"skipped\":{\"deep\":[\"" ++ long ++ "\",1,2]},\"keep\":5}"; const T = struct { keep: u8, @@ -416,7 +416,7 @@ test "an object key longer than any field name is handled without allocating" { return .{ .skip_unknown_fields = true }; } }; - const doc = "{\"" ++ ("k" ** 300) ++ "\":1,\"id\":2}"; + const doc = "{\"" ++ @as([300]u8, @splat('k')) ++ "\":1,\"id\":2}"; var buffer: [16]u8 = undefined; var trickle = TrickleReader.init(&buffer, doc); const v = try json.decodeLeaky(T, a, &trickle.reader, .{});