Lexical analysis
Juniper Beatitudes [email protected]
Sun, 26 Jul 2026 13:55:59 -0500
8 files changed,
323 insertions(+),
61 deletions(-)
A
example.pba
@@ -0,0 +1,67 @@
+protocol proto_circe +mtu 1472 + +version 1 { + header { + u8[64] tunnel_id; + } + + message open_tunnel { + clear { + u8[32] ecdh_fragment + u8[1184] mlkem_pubkey + } + digest sha512 + } + + message ack_open (u8[32] k_hs_e) { + clear { + u8[32] ecdh_fragment + u8[1088] mlkem_encaps + } + cc20_p1305 (nonce:0, key:@k_hs_e, aad:$preceding) { + u8[64] identity + u8[2420] signature + } + digest sha512 + } + + message identify (u8[32] k_hs_p) { + cc20_p1305 (nonce:0, key:@k_hs_p, aad:$preceding) { + u8[64] identity + u8[2420] signature + } + digest sha512 + } + + message data (u8[32] k_d) { + clear { + u64 sequence_number + } + cc20_p1305 (nonce:@sequence_number, key:@k_d, aad:$preceding) { + u8 n_random_bytes + u8[] payload + u8[@n_random_bytes] padding + } + } + + message close_tunnel (u8[32] k_d) { + clear { + u64 sequence_number + } + cc20_p1305 (nonce:@sequence_number, key:@k_d, aad:$preceding) { + u8[64] identity + u8[2420] signature + } + } + + message path_challenge (u8[32] k_d) { + clear { + u64 sequence_number + } + cc20_p1305 (nonce:@sequence_number, key:@k_d, aad:$preceding) { + u8[64] identity + u8[2420] signature + } + } +}
A
src/fail.zig
@@ -0,0 +1,7 @@
+const std = @import("std"); +const lex = @import("lex.zig"); + +pub fn fail(pos: lex.Position, msg: []const u8) noreturn { + std.log.err("PBA compilation failed at line {d}, column {d} with message {s}", .{ pos.line, pos.column, msg }); + std.process.exit(1); +}
A
src/ingest.zig
@@ -0,0 +1,125 @@
+const lex = @import("lex.zig"); + +pub const Scalar = enum { + U8, + U16, + U32, + U64, + + pub fn width(self: Scalar) usize { + return switch (self) { + Scalar.U8 => 1, + Scalar.U16 => 2, + Scalar.U32 => 4, + Scalar.U64 => 8, + }; + } + + pub fn pba_name(self: Scalar) []const u8 { + return switch (self) { + Scalar.U8 => "u8", + Scalar.U16 => "u16", + Scalar.U32 => "u32", + Scalar.U64 => "u64", + }; + } + + pub fn c_type(self: Scalar) []const u8 { + return switch (self) { + Scalar.U8 => "uint8_t", + Scalar.U16 => "uint16_t", + Scalar.U32 => "uint32_t", + Scalar.U64 => "uint64_t", + }; + } +}; + +pub const ArrayLength = union(enum) { + fixed: usize, + ref: []const u8, + open: void, +}; + +pub const FieldType = union(enum) { + single: Scalar, + array: struct { + scalar_type: Scalar, + length: ArrayLength, + }, +}; + +pub const Field = struct { + name: []const u8, + field_type: FieldType, + position: lex.Position, +}; + +pub const Builtin = enum { + PRECEDING, +}; + +pub const Value = union(enum) { + literal: usize, + field_ref: []const u8, + builtin: Builtin, +}; + +pub const AeadAlgorithm = union(enum) { + chacha20_poly1305: struct { + nonce: Value, + key: Value, + aad: Value, + }, + + pub fn tag_len(self: AeadAlgorithm) usize { + return switch (self) { + .chacha20_poly1305 => 16, + }; + } +}; + +pub const DigestAlgorithm = enum { + SHA256, + SHA512, + + pub fn digest_len(self: DigestAlgorithm) usize { + return switch (self) { + DigestAlgorithm.SHA256 => 32, + DigestAlgorithm.SHA512 => 64, + }; + } +}; + +pub const Block = struct { + contents: union(enum) { + clear: []Field, + aead: []struct { + algorithm: AeadAlgorithm, + }, + }, + pos: lex.Position, +}; + +pub const Message = struct { + name: []const u8, + params: []Field, + blocks: []Block, + pos: lex.Position, + digest: ?DigestAlgorithm, + fragment_key: ?Value, +}; + +pub const ProtocolInstance = struct { + version: usize, + header: []Field, + messages: []Message, + pos: lex.Position, +}; + +pub const Protocol = struct { + name: []const u8, + mtu: usize, + instances: []ProtocolInstance, +}; + +const auto_header_len = 6;
A
src/lex.zig
@@ -0,0 +1,113 @@
+const std = @import("std"); +const fail = @import("fail.zig"); + +pub const Position = struct { + line: usize = 0, + column: usize = 0, +}; + +pub const Token = struct { + pos: Position, + tok: union(enum) { + identifier: []const u8, + scalar: usize, + lbrace: void, + rbrace: void, + lparen: void, + rparen: void, + lbracket: void, + rbracket: void, + semicolon: void, + colon: void, + comma: void, + at: void, + dollar: void, + eof: void, + }, + + pub fn describe(self: Token) []const u8 { + return switch (self.tok) { + .identifier => "identifier", + .scalar => "scalar", + .lbrace => "left brace", + .rbrace => "right brace", + .lparen => "left parenthesis", + .rparen => "right parenthesis", + .lbracket => "left bracket", + .rbracket => "right bracket", + .semicolon => "semicolon", + .colon => "colon", + .at => "at sign (@)", + .dollar => "dollar sign", + .eof => "end of file", + }; + } +}; + +fn is_ident_start(c: u8) bool { + return std.ascii.isAlphabetic(c) or (c == '_'); +} + +fn is_ident_continue(c: u8) bool { + return std.ascii.isAlphanumeric(c) or (c == '_'); +} + +pub fn tokenize(buf: []const u8, alloc: std.mem.Allocator) !std.ArrayList(Token) { + var res = std.ArrayList(Token).empty; + var current_pos: Position = .{ .line = 1, .column = 0 }; + var current_idx: usize = 0; + while (current_idx < buf.len) { + current_pos.column += 1; + if (is_ident_start(buf[current_idx])) { + const start = current_idx; + while (current_idx < buf.len and is_ident_continue(buf[current_idx])) { + current_idx += 1; + } + try res.append(alloc, .{ + .tok = .{ + .identifier = buf[start..current_idx], + }, + .pos = current_pos, + }); + current_pos.column += current_idx - start; + } else if (std.ascii.isDigit(buf[current_idx])) { + const start = current_idx; + while (current_idx < buf.len and std.ascii.isDigit(buf[current_idx])) { + current_idx += 1; + } + try res.append(alloc, .{ + .tok = .{ + .scalar = try std.fmt.parseInt(usize, buf[start..current_idx], 10), + }, + .pos = current_pos, + }); + current_pos.column += current_idx - start; + } else if (buf[current_idx] == '\n') { + current_idx += 1; + current_pos.line += 1; + current_pos.column = 0; + } else if (std.ascii.isWhitespace(buf[current_idx])) { + current_idx += 1; + current_pos.column += 1; + } else { + switch (buf[current_idx]) { + '{' => try res.append(alloc, .{ .tok = .lbrace, .pos = current_pos }), + '}' => try res.append(alloc, .{ .tok = .rbrace, .pos = current_pos }), + '(' => try res.append(alloc, .{ .tok = .lparen, .pos = current_pos }), + ')' => try res.append(alloc, .{ .tok = .rparen, .pos = current_pos }), + '[' => try res.append(alloc, .{ .tok = .lbracket, .pos = current_pos }), + ']' => try res.append(alloc, .{ .tok = .rbracket, .pos = current_pos }), + ';' => try res.append(alloc, .{ .tok = .semicolon, .pos = current_pos }), + ':' => try res.append(alloc, .{ .tok = .colon, .pos = current_pos }), + ',' => try res.append(alloc, .{ .tok = .comma, .pos = current_pos }), + '@' => try res.append(alloc, .{ .tok = .at, .pos = current_pos }), + '$' => try res.append(alloc, .{ .tok = .dollar, .pos = current_pos }), + else => fail.fail(current_pos, "invalid token"), + } + current_idx += 1; + current_pos.column += 1; + } + } + try res.append(alloc, .{ .tok = .eof, .pos = current_pos }); + return res; +}
M
src/main.zig
→
src/main.zig
@@ -2,70 +2,18 @@ const std = @import("std");
const Io = std.Io; const pba_zig = @import("pba_zig"); +const ingest = @import("ingest.zig"); +const lex = @import("lex.zig"); pub fn main(init: std.process.Init) !void { - // Prints to stderr, unbuffered, ignoring potential errors. - std.debug.print("All your {s} are belong to us.\n", .{"codebase"}); - - // This is appropriate for anything that lives as long as the process. - const arena: std.mem.Allocator = init.arena.allocator(); - - // Accessing command line arguments: - const args = try init.minimal.args.toSlice(arena); - for (args) |arg| { - std.log.info("arg: {s}", .{arg}); - } - - // In order to do I/O operations need an `Io` instance. - const io = init.io; - - // Stdout is for the actual output of your application, for example if you - // are implementing gzip, then only the compressed bytes should be sent to - // stdout, not any debugging messages. - var stdout_buffer: [1024]u8 = undefined; - var stdout_file_writer: Io.File.Writer = .init(.stdout(), io, &stdout_buffer); - const stdout_writer = &stdout_file_writer.interface; - - try pba_zig.printAnotherMessage(stdout_writer); - - try stdout_writer.flush(); // Don't forget to flush! -} - -test "simple test" { - const gpa = std.testing.allocator; - var list: std.ArrayList(i32) = .empty; - defer list.deinit(gpa); // Try commenting this out and see if zig detects the memory leak! - try list.append(gpa, 42); - try std.testing.expectEqual(@as(i32, 42), list.pop()); -} + const arena = init.arena.allocator(); -test "fuzz example" { - try std.testing.fuzz({}, testOne, .{}); -} + const cwd = std.Io.Dir.cwd(); -fn testOne(context: void, smith: *std.testing.Smith) !void { - _ = context; - // Try passing `--fuzz` to `zig build test` and see if it manages to fail this test case! + const max_bytes = 10 * 1024 * 1024; + const buf = try cwd.readFileAlloc(init.io, "example.pba", arena, .limited(max_bytes)); + defer arena.free(buf); - const gpa = std.testing.allocator; - var list: std.ArrayList(u8) = .empty; - defer list.deinit(gpa); - while (!smith.eos()) switch (smith.value(enum { add_data, dup_data })) { - .add_data => { - const slice = try list.addManyAsSlice(gpa, smith.value(u4)); - smith.bytes(slice); - }, - .dup_data => { - if (list.items.len == 0) continue; - if (list.items.len > std.math.maxInt(u32)) return error.SkipZigTest; - const len = smith.valueRangeAtMost(u32, 1, @min(32, list.items.len)); - const off = smith.valueRangeAtMost(u32, 0, @intCast(list.items.len - len)); - try list.appendSlice(gpa, list.items[off..][0..len]); - try std.testing.expectEqualSlices( - u8, - list.items[off..][0..len], - list.items[list.items.len - len ..], - ); - }, - }; + const tokens = try lex.tokenize(buf, arena); + std.log.info("{d}", .{tokens.items.len}); }