Files
zeekay a4debdba80 Read a .zap schema the way every other ZAP language reads it
C had no front end for the schema language at all. The compiler directory
holds a code generator plugin, which is handed a request by another tool
and never sees schema text, so a .zap file could not be read here in
either form -- indentation or braces.

zapgen reads it. Whitespace-significant source is rewritten into the brace
form BEFORE the parser, exactly as the Go reference does it: a near
identity that only adds the two tokens the brace grammar requires where
the indented form leaves them out. A file already in braces comes back
byte for byte, so one grammar reads both styles and neither is a dialect
of the other.

The identifier grammar is not restated. Its Unicode ranges are taken from
Go's own tables by gen/table, so a name this parser accepts is a name the
Go parser accepts, and a Go release that moves Unicode moves both.

The generated header gives each struct a view over the message bytes and
one reader per field at a fixed offset. Reading is in place and total: a
field past the end reads zero, a pointer into the head or out of the
message reads null. zap_view.h is the one place that says so.

Checked against the Go front end on every .zap in the estate plus 55
adversarial cases and 15000 random ones: the brace form each writes is
byte-identical, the layout each reads is field-for-field the same, and
they accept and refuse the same files for the same stated reason. A
message built by the Go runtime reads back through these bindings with
every field equal.
2026-09-15 11:32:12 -07:00

746 lines
16 KiB
C

/* SPDX-License-Identifier: BSD-3-Clause-Eco */
#include "parse.h"
#include "desugar.h"
#include "rune.h"
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
long long slot_size(struct zap_type t)
{
switch (t.kind) {
case KIND_BOOL:
case KIND_U8:
case KIND_I8:
return 1;
case KIND_U16:
case KIND_I16:
return 2;
case KIND_U32:
case KIND_I32:
case KIND_F32:
return 4;
case KIND_U64:
case KIND_I64:
case KIND_F64:
return 8;
case KIND_FIXED:
return t.fixed;
case KIND_BYTES:
case KIND_TEXT:
case KIND_LIST:
return 8;
case KIND_STRUCT:
return 4;
default:
return 0;
}
}
const char *kind_name(enum kind k)
{
switch (k) {
case KIND_BOOL: return "bool";
case KIND_U8: return "u8";
case KIND_U16: return "u16";
case KIND_U32: return "u32";
case KIND_U64: return "u64";
case KIND_I8: return "i8";
case KIND_I16: return "i16";
case KIND_I32: return "i32";
case KIND_I64: return "i64";
case KIND_F32: return "f32";
case KIND_F64: return "f64";
case KIND_BYTES: return "bytes";
case KIND_FIXED: return "bytes_fixed";
case KIND_TEXT: return "text";
case KIND_LIST: return "list";
case KIND_STRUCT: return "struct";
default: return "invalid";
}
}
/* --- the reader ---------------------------------------------------------
*
* One cursor over the desugared source. `doc` holds the comment lines read
* since the last thing that was not a comment; a declaration takes them, a
* blank line drops them.
*/
struct reader {
const char *src;
size_t len, pos;
int line;
const char *name; /* file name, for messages */
struct arena *a;
struct zap_file *f;
const char **doc;
size_t docn, doccap;
char *err; /* ERRLEN bytes, owned by the caller */
int failed;
};
static void fail(struct reader *r, const char *fmt, ...)
{
char msg[ERRLEN - 64];
va_list ap;
if (r->failed)
return;
r->failed = 1;
va_start(ap, fmt);
vsnprintf(msg, sizeof(msg), fmt, ap);
va_end(ap);
snprintf(r->err, ERRLEN, "%s:%d: %s", r->name, r->line, msg);
}
/* grow returns room for cap items, holding the n items already in old. An
* empty array is NULL, so the copy is only made when there is something to
* copy - reading zero bytes from nowhere is not reading nothing. */
static void *grow(struct arena *a, const void *old, size_t n, size_t cap,
size_t item)
{
void *p = arena_get(a, cap * item);
if (p && n)
memcpy(p, old, n * item);
return p;
}
/* push_doc appends one comment line to the block being accumulated. */
static void push_doc(struct reader *r, const char *s, size_t n)
{
if (r->docn == r->doccap) {
size_t cap = r->doccap ? r->doccap * 2 : 8;
const char **p = realloc(r->doc, cap * sizeof(*p));
if (!p)
return;
r->doc = p;
r->doccap = cap;
}
r->doc[r->docn++] = arena_str(r->a, s, n);
}
/* take_doc hands the accumulated block to the declaration being parsed and
* clears it. An empty block stays empty, so "undocumented" is not the same
* as "documented with nothing". */
static struct doc take_doc(struct reader *r)
{
struct doc d = {NULL, 0};
size_t i;
if (!r->docn)
return d;
d.line = arena_get(r->a, r->docn * sizeof(*d.line));
if (!d.line)
return d;
for (i = 0; i < r->docn; i++)
d.line[i] = r->doc[i];
d.n = r->docn;
r->docn = 0;
return d;
}
static void trim(const char **s, size_t *n)
{
while (*n && (**s == ' ' || **s == '\t' || **s == '\r')) {
(*s)++;
(*n)--;
}
while (*n && ((*s)[*n - 1] == ' ' || (*s)[*n - 1] == '\t' ||
(*s)[*n - 1] == '\r'))
(*n)--;
}
/* at_line_start reports whether only whitespace separates pos from the
* start of its line. A '#' that fails this test remarks on the code to its
* left rather than documenting what follows. */
static int at_line_start(struct reader *r)
{
size_t i = r->pos;
while (i > 0) {
char c = r->src[--i];
if (c == '\n')
return 1;
if (c != ' ' && c != '\t' && c != '\r')
return 0;
}
return 1;
}
static void skip_space(struct reader *r)
{
int blank = 1;
while (r->pos < r->len) {
char c = r->src[r->pos];
if (c == '\n') {
if (blank)
r->docn = 0;
blank = 1;
r->line++;
r->pos++;
} else if (c == ' ' || c == '\t' || c == '\r') {
r->pos++;
} else if (c == '#') {
int doc = at_line_start(r);
size_t start = r->pos + 1, n;
const char *s;
while (r->pos < r->len && r->src[r->pos] != '\n')
r->pos++;
if (doc) {
s = r->src + start;
n = r->pos - start;
trim(&s, &n);
push_doc(r, s, n);
}
blank = 0;
} else {
return;
}
}
}
static int ident_start(uint32_t c) { return c == '_' || rune_letter(c); }
static int ident_rune(uint32_t c)
{
return c == '_' || rune_letter(c) || rune_digit(c);
}
/* peek_word reports whether the upcoming bytes are word followed by a rune
* that cannot continue an identifier. It does not advance. */
static int peek_word(struct reader *r, const char *word)
{
size_t n = strlen(word), size;
if (r->pos + n > r->len)
return 0;
if (memcmp(r->src + r->pos, word, n) != 0)
return 0;
if (r->pos + n == r->len)
return 1;
return !ident_rune(rune_read(r->src + r->pos + n, r->len - r->pos - n,
&size));
}
static const char *read_ident(struct reader *r)
{
size_t start = r->pos, size;
uint32_t c;
if (r->pos >= r->len)
return NULL;
c = rune_read(r->src + r->pos, r->len - r->pos, &size);
if (!ident_start(c))
return NULL;
r->pos += size;
while (r->pos < r->len) {
c = rune_read(r->src + r->pos, r->len - r->pos, &size);
if (!ident_rune(c))
break;
r->pos += size;
}
return arena_str(r->a, r->src + start, r->pos - start);
}
/* A name that could not be held is a name the reader does not have, and
* every caller already treats a missing name as a failure to read. */
/* read_int reads an unsigned decimal. A value too large to hold is an
* error, never a wrap: an offset that wrapped would alias another field in
* the zero-copy layout. */
static int read_int(struct reader *r, long long *out)
{
size_t start = r->pos;
unsigned long long n = 0;
while (r->pos < r->len && r->src[r->pos] >= '0' && r->src[r->pos] <= '9') {
unsigned d = (unsigned)(r->src[r->pos] - '0');
if (n > (0x7FFFFFFFFFFFFFFFULL - d) / 10) {
fail(r, "bad integer: value out of range");
return -1;
}
n = n * 10 + d;
r->pos++;
}
if (r->pos == start) {
fail(r, "expected integer");
return -1;
}
*out = (long long)n;
return 0;
}
static int expect(struct reader *r, const char *lit)
{
size_t n = strlen(lit), i;
if (r->pos + n > r->len || memcmp(r->src + r->pos, lit, n) != 0) {
fail(r, "expected \"%s\"", lit);
return -1;
}
for (i = 0; i < n; i++)
if (lit[i] == '\n')
r->line++;
r->pos += n;
return 0;
}
/* --- grammar ----------------------------------------------------------- */
static int parse_type(struct reader *r, struct zap_type *out);
static const struct {
const char *word;
enum kind kind;
} primitives[] = {
{"bool", KIND_BOOL},
{"u8", KIND_U8}, {"u16", KIND_U16}, {"u32", KIND_U32}, {"u64", KIND_U64},
{"i8", KIND_I8}, {"i16", KIND_I16}, {"i32", KIND_I32}, {"i64", KIND_I64},
{"f32", KIND_F32}, {"f64", KIND_F64},
{"bytes", KIND_BYTES}, {"text", KIND_TEXT},
};
static int parse_type(struct reader *r, struct zap_type *out)
{
const char *name;
size_t i;
memset(out, 0, sizeof(*out));
if (peek_word(r, "list")) {
struct zap_type *elem = arena_get(r->a, sizeof(*elem));
r->pos += 4;
skip_space(r);
if (expect(r, "<"))
return -1;
skip_space(r);
if (parse_type(r, elem))
return -1;
skip_space(r);
if (expect(r, ">"))
return -1;
out->kind = KIND_LIST;
out->elem = elem;
return 0;
}
if (peek_word(r, "bytes_fixed")) {
long long n;
r->pos += 11;
skip_space(r);
if (expect(r, "["))
return -1;
skip_space(r);
if (read_int(r, &n))
return -1;
if (n <= 0) {
fail(r, "bytes_fixed[N] must have N > 0");
return -1;
}
skip_space(r);
if (expect(r, "]"))
return -1;
out->kind = KIND_FIXED;
out->fixed = n;
return 0;
}
for (i = 0; i < sizeof(primitives) / sizeof(primitives[0]); i++) {
if (peek_word(r, primitives[i].word)) {
r->pos += strlen(primitives[i].word);
out->kind = primitives[i].kind;
return 0;
}
}
name = read_ident(r);
if (!name) {
fail(r, "expected type");
return -1;
}
if (r->f) {
for (i = 0; i < r->f->aliases; i++) {
if (strcmp(r->f->alias[i].name, name) == 0) {
*out = r->f->alias[i].type;
return 0;
}
}
}
out->kind = KIND_STRUCT;
out->name = name;
return 0;
}
/* alias := 'type' Ident '=' Type */
static int parse_alias(struct reader *r)
{
const char *name;
struct zap_type t;
struct alias *a;
size_t i;
r->pos += 4;
skip_space(r);
name = read_ident(r);
if (!name) {
fail(r, "expected alias name after `type`");
return -1;
}
skip_space(r);
if (expect(r, "="))
return -1;
skip_space(r);
if (parse_type(r, &t))
return -1;
for (i = 0; i < r->f->aliases; i++) {
if (strcmp(r->f->alias[i].name, name) == 0) {
fail(r, "duplicate type alias \"%s\"", name);
return -1;
}
}
a = grow(r->a, r->f->alias, r->f->aliases, r->f->aliases + 1, sizeof(*a));
if (!a) {
fail(r, "out of memory");
return -1;
}
a[r->f->aliases].name = name;
a[r->f->aliases].type = t;
r->f->alias = a;
r->f->aliases++;
return 0;
}
/* field := Ident Type '@' Int */
static int parse_field(struct reader *r, struct zap_field *out)
{
memset(out, 0, sizeof(*out));
out->doc = take_doc(r);
out->name = read_ident(r);
if (!out->name) {
fail(r, "expected field name");
return -1;
}
skip_space(r);
if (parse_type(r, &out->type))
return -1;
skip_space(r);
if (expect(r, "@"))
return -1;
skip_space(r);
return read_int(r, &out->offset);
}
/* strct := 'struct' Ident '{' Field* '}' */
static int parse_struct(struct reader *r, struct zap_struct *out)
{
size_t cap = 0;
memset(out, 0, sizeof(*out));
out->doc = take_doc(r);
r->pos += 6;
skip_space(r);
out->name = read_ident(r);
if (!out->name) {
fail(r, "expected struct name");
return -1;
}
skip_space(r);
if (expect(r, "{"))
return -1;
for (;;) {
skip_space(r);
if (r->pos >= r->len) {
fail(r, "unterminated struct \"%s\"", out->name);
return -1;
}
if (r->src[r->pos] == '}') {
r->pos++;
return 0;
}
if (out->n == cap) {
struct zap_field *p;
cap = cap ? cap * 2 : 8;
p = grow(r->a, out->field, out->n, cap, sizeof(*p));
if (!p) {
fail(r, "out of memory");
return -1;
}
out->field = p;
}
if (parse_field(r, &out->field[out->n]))
return -1;
out->n++;
}
}
/* params := '(' (Ident ':' Ident)? ')' -- at most one struct payload per
* direction, because a ZAP method carries one. */
static int parse_params(struct reader *r, int *has, struct param *out)
{
*has = 0;
if (expect(r, "("))
return -1;
skip_space(r);
if (r->pos < r->len && r->src[r->pos] == ')') {
r->pos++;
return 0;
}
out->name = read_ident(r);
if (!out->name) {
fail(r, "expected parameter name");
return -1;
}
skip_space(r);
if (expect(r, ":"))
return -1;
skip_space(r);
out->type = read_ident(r);
if (!out->type) {
fail(r, "expected parameter type");
return -1;
}
skip_space(r);
if (r->pos < r->len && r->src[r->pos] == ',') {
fail(r, "method params carry exactly one struct payload per direction");
return -1;
}
if (expect(r, ")"))
return -1;
*has = 1;
return 0;
}
/* method := Ident '(' Param? ')' ('returns' '(' Param? ')')? */
static int parse_method(struct reader *r, int ordinal, struct zap_method *out)
{
memset(out, 0, sizeof(*out));
out->doc = take_doc(r);
out->ordinal = ordinal;
out->name = read_ident(r);
if (!out->name) {
fail(r, "expected method name");
return -1;
}
skip_space(r);
if (parse_params(r, &out->has_request, &out->request))
return -1;
skip_space(r);
if (peek_word(r, "returns")) {
r->pos += 7;
skip_space(r);
if (parse_params(r, &out->has_response, &out->response))
return -1;
}
return 0;
}
/* iface := 'interface' Ident '{' Method* '}' -- ordinals are 1, 2, 3, ...
* in declaration order, so appending a method never renumbers an older one. */
static int parse_iface(struct reader *r, struct zap_iface *out)
{
size_t cap = 0;
int ordinal = 1;
memset(out, 0, sizeof(*out));
out->doc = take_doc(r);
r->pos += 9;
skip_space(r);
out->name = read_ident(r);
if (!out->name) {
fail(r, "expected interface name");
return -1;
}
skip_space(r);
if (expect(r, "{"))
return -1;
for (;;) {
skip_space(r);
if (r->pos >= r->len) {
fail(r, "unterminated interface \"%s\"", out->name);
return -1;
}
if (r->src[r->pos] == '}') {
r->pos++;
return 0;
}
if (out->n == cap) {
struct zap_method *p;
cap = cap ? cap * 2 : 8;
p = grow(r->a, out->method, out->n, cap, sizeof(*p));
if (!p) {
fail(r, "out of memory");
return -1;
}
out->method = p;
}
if (parse_method(r, ordinal, &out->method[out->n]))
return -1;
out->n++;
ordinal++;
}
}
/* file := 'package' Ident (Alias | Struct | Interface)* */
static int parse_file(struct reader *r)
{
size_t scap = 0, icap = 0;
skip_space(r);
if (!peek_word(r, "package")) {
fail(r, "expected `package` declaration");
return -1;
}
r->f->doc = take_doc(r);
r->pos += 7;
skip_space(r);
r->f->package = read_ident(r);
if (!r->f->package) {
fail(r, "expected package name after `package`");
return -1;
}
for (;;) {
skip_space(r);
if (r->pos >= r->len)
return 0;
if (peek_word(r, "struct")) {
if (r->f->structs == scap) {
struct zap_struct *p;
scap = scap ? scap * 2 : 8;
p = grow(r->a, r->f->strct, r->f->structs, scap,
sizeof(*p));
if (!p) {
fail(r, "out of memory");
return -1;
}
r->f->strct = p;
}
if (parse_struct(r, &r->f->strct[r->f->structs]))
return -1;
r->f->structs++;
} else if (peek_word(r, "interface")) {
if (r->f->ifaces == icap) {
struct zap_iface *p;
icap = icap ? icap * 2 : 8;
p = grow(r->a, r->f->iface, r->f->ifaces, icap,
sizeof(*p));
if (!p) {
fail(r, "out of memory");
return -1;
}
r->f->iface = p;
}
if (parse_iface(r, &r->f->iface[r->f->ifaces]))
return -1;
r->f->ifaces++;
} else if (peek_word(r, "type")) {
if (parse_alias(r))
return -1;
} else {
fail(r, "expected `struct`, `interface`, or `type` at top level");
return -1;
}
}
}
static const char *basename_of(const char *name)
{
const char *p;
for (p = name + strlen(name); p > name; p--)
if (p[-1] == '/' || p[-1] == '\\')
return p;
return name;
}
struct zap_file *parse(const char *name, const char *src, size_t len,
char err[ERRLEN])
{
struct arena *a = arena_new();
struct reader r;
struct zap_file *f;
char *sugar = NULL;
size_t sugarlen = 0;
err[0] = '\0';
if (!a) {
snprintf(err, ERRLEN, "out of memory");
return NULL;
}
if (desugar(src, len, &sugar, &sugarlen, err)) {
char msg[ERRLEN];
snprintf(msg, sizeof(msg), "%s: %s", basename_of(name), err);
snprintf(err, ERRLEN, "%s", msg);
arena_free(a);
return NULL;
}
f = arena_get(a, sizeof(*f));
if (!f) {
snprintf(err, ERRLEN, "out of memory");
free(sugar);
arena_free(a);
return NULL;
}
f->arena = a;
f->source = arena_str(a, basename_of(name), strlen(basename_of(name)));
memset(&r, 0, sizeof(r));
r.src = sugar;
r.len = sugarlen;
r.line = 1;
r.name = name;
r.a = a;
r.f = f;
r.err = err;
if (parse_file(&r)) {
free(r.doc);
free(sugar);
arena_free(a);
return NULL;
}
free(r.doc);
free(sugar);
return f;
}
void file_free(struct zap_file *f)
{
if (f)
arena_free(f->arena);
}
int type_text(struct arena *a, const struct zap_file *f, const char *expr,
struct zap_type *out, char err[ERRLEN])
{
struct reader r;
memset(&r, 0, sizeof(r));
r.src = expr;
r.len = strlen(expr);
/* No file and no line: this reader is sizing a type expression lifted
* out of a field, so there is no position in the source to name. */
r.line = 0;
r.name = "";
r.a = a;
r.f = (struct zap_file *)f;
r.err = err;
err[0] = '\0';
return parse_type(&r, out);
}