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

635 lines
16 KiB
C

/* SPDX-License-Identifier: BSD-3-Clause-Eco */
#include "desugar.h"
#include "arena.h"
#include "parse.h"
#include "schema.h"
#include "str.h"
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* A slice of the source: no copies are made until the output is written. */
struct span_str {
const char *p;
size_t n;
};
enum line_kind {
LINE_CLEAR, /* blank or a full-line '#' comment */
LINE_OTHER, /* package, type alias, brace lines */
LINE_HEAD, /* whitespace-form block header */
LINE_FIELD /* whitespace-form field */
};
struct line {
struct span_str raw;
struct span_str body; /* raw, trimmed */
int indent; /* leading spaces; tabs count one column each */
enum line_kind kind;
const char *keyword; /* for LINE_HEAD: the opener that started it */
};
/* maxoff bounds a declared byte offset. A struct's fixed section is
* addressed by these in a zero-copy layout, so an offset far past any real
* message is a typo, not a layout. Rejecting it keeps the read total: no
* overflow, no silent wrap onto offset 0. */
#define MAXOFF 2147483648ULL
struct work {
struct line *line;
size_t n;
struct str out;
size_t written; /* elements joined so far, for the separator */
struct arena *a;
/* aliases, as the text on the right of `type X = ...`, so a field of
* an aliased type can be sized by the one real type parser. */
struct span_str *aname, *aexpr;
size_t aliases;
char *err;
};
/* --- slices ------------------------------------------------------------- */
static struct span_str span(const char *p, size_t n)
{
struct span_str s = {p, n};
return s;
}
static int span_is(struct span_str s, const char *lit)
{
size_t n = strlen(lit);
return s.n == n && memcmp(s.p, lit, n) == 0;
}
static int space(char c) { return c == ' ' || c == '\t'; }
static struct span_str trim(struct span_str s)
{
while (s.n && (space(s.p[0]) || s.p[0] == '\r')) {
s.p++;
s.n--;
}
while (s.n && (space(s.p[s.n - 1]) || s.p[s.n - 1] == '\r'))
s.n--;
return s;
}
/* cut removes a trailing '#' comment, and the space before it. */
static struct span_str cut(struct span_str s)
{
size_t i;
for (i = 0; i < s.n; i++)
if (s.p[i] == '#')
return trim(span(s.p, i));
return s;
}
/* word splits the leading whitespace-delimited token off s. */
static struct span_str word(struct span_str s, struct span_str *rest)
{
size_t i = 0, j;
while (i < s.n && space(s.p[i]))
i++;
for (j = i; j < s.n; j++) {
if (space(s.p[j])) {
size_t k = j;
while (k < s.n && space(s.p[k]))
k++;
*rest = span(s.p + k, s.n - k);
return span(s.p + i, j - i);
}
}
*rest = span(s.p + s.n, 0);
return span(s.p + i, s.n - i);
}
/* after returns the text following kw, with the separating space consumed,
* and whether s started with that keyword as a whole token. */
static int after(struct span_str s, const char *kw, struct span_str *rest)
{
size_t n = strlen(kw);
if (s.n < n || memcmp(s.p, kw, n) != 0)
return 0;
if (s.n == n)
return 0; /* a bare keyword names nothing */
if (!space(s.p[n]))
return 0; /* `structFoo` is one identifier, not the keyword */
*rest = trim(span(s.p + n, s.n - n));
return 1;
}
/* an identifier here is ASCII: `[A-Za-z_]\w*`. The desugar decides shape,
* not meaning; the parser holds the full grammar. */
static int is_ident(struct span_str s)
{
size_t i;
if (!s.n)
return 0;
for (i = 0; i < s.n; i++) {
char c = s.p[i];
if (c == '_' || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z'))
continue;
if (c >= '0' && c <= '9') {
if (i == 0)
return 0;
continue;
}
return 0;
}
return 1;
}
/* --- line facts --------------------------------------------------------- */
/* blockers are the keywords that open a braceless block. A struct body holds
* fields, whose offsets are assigned; an interface body holds methods, which
* carry no offset and pass through as written. */
static const char *const blockers[] = {"struct", "interface"};
/* opener reports whether body is a braceless block header and which keyword
* opened it. A header is EXACTLY `<opener>` + one identifier + end of line,
* once a trailing comment is cut. It deliberately does not match a field
* whose NAME is `struct` or `interface` and that carries a type or an
* offset after it (`struct u8 @0`), nor `structFoo`, nor `struct S {`. Those
* fall through and are read as ordinary fields. */
static const char *opener(struct span_str body)
{
struct span_str rest, id, tail;
size_t i;
body = cut(body);
if (body.n && body.p[body.n - 1] == '{')
return NULL;
for (i = 0; i < sizeof(blockers) / sizeof(blockers[0]); i++) {
if (!after(body, blockers[i], &rest))
continue;
id = word(rest, &tail);
if (is_ident(id) && tail.n == 0)
return blockers[i];
}
return NULL;
}
/* `package` is the one token that begins a file-scope construct and so can
* never name a field. A real header is already claimed by opener, and a
* top-level alias is excluded by the '=' test below - so `struct`,
* `interface`, `type` and the rest ARE valid field names. */
static int reserved(struct span_str s) { return span_is(s, "package"); }
static int is_field(struct span_str body)
{
struct span_str b = cut(body), name, rest, type;
size_t i;
for (i = 0; i < b.n; i++)
if (b.p[i] == '{' || b.p[i] == '}' || b.p[i] == '=')
return 0;
name = word(b, &rest);
if (!name.n || !rest.n || reserved(name))
return 0;
type = word(rest, &rest);
return type.n != 0;
}
/* delta is the net '{' minus '}' on a line, ignoring anything commented. */
static int delta(struct span_str raw)
{
int d = 0;
size_t i;
for (i = 0; i < raw.n; i++) {
if (raw.p[i] == '#')
return d;
if (raw.p[i] == '{')
d++;
else if (raw.p[i] == '}')
d--;
}
return d;
}
static struct line classify(struct span_str raw)
{
struct line ln;
size_t i = 0;
memset(&ln, 0, sizeof(ln));
ln.raw = raw;
while (i < raw.n && space(raw.p[i]))
i++;
ln.indent = (int)i;
ln.body = trim(raw);
if (!ln.body.n || ln.body.p[0] == '#') {
ln.kind = LINE_CLEAR;
return ln;
}
ln.keyword = opener(ln.body);
if (ln.keyword)
ln.kind = LINE_HEAD;
else if (is_field(ln.body))
ln.kind = LINE_FIELD;
else
ln.kind = LINE_OTHER;
return ln;
}
/* --- writing ------------------------------------------------------------ */
static void put(struct work *w, struct span_str s)
{
if (w->written++)
str_add(&w->out, "\n", 1);
str_add(&w->out, s.p, (int)s.n);
}
static void indent(struct str *s, int n)
{
while (n-- > 0)
str_add(s, " ", 1);
}
/* --- offsets ------------------------------------------------------------ */
static int fail(struct work *w, const char *fmt, ...)
{
va_list ap;
va_start(ap, fmt);
vsnprintf(w->err, ERRLEN, fmt, ap);
va_end(ap);
return -1;
}
/* number reads an unsigned decimal, rejecting anything else. An offset past
* MAXOFF, or one that would overflow, is refused outright: an unchecked
* accumulator turns @18446744073709551616 into 0, aliasing field offset 0. */
static int number(struct work *w, struct span_str s, long long *out)
{
unsigned long long n = 0;
size_t i;
if (!s.n)
return fail(w, "empty integer");
for (i = 0; i < s.n; i++) {
unsigned d;
if (s.p[i] < '0' || s.p[i] > '9')
return fail(w, "not a number: \"%.*s\"", (int)s.n, s.p);
d = (unsigned)(s.p[i] - '0');
if (n > (~0ULL - d) / 10)
return fail(w, "offset \"%.*s\" out of range", (int)s.n, s.p);
n = n * 10 + d;
}
if (n > MAXOFF)
return fail(w, "offset \"%.*s\" out of range (max %llu)",
(int)s.n, s.p, MAXOFF);
*out = (long long)n;
return 0;
}
/* width is the fixed-section width of the type text, read by the real type
* parser so layout is sized in exactly one place. A named alias is sized by
* the text it stands for; a list or a nested struct is a fixed-width
* pointer, which slot_size already says. */
static int width(struct work *w, struct span_str type, long long *out)
{
char expr[256], err[ERRLEN];
struct span_str e = type;
struct zap_type t;
size_t i;
for (i = 0; i < w->aliases; i++) {
if (w->aname[i].n == type.n &&
memcmp(w->aname[i].p, type.p, type.n) == 0) {
e = w->aexpr[i];
break;
}
}
if (e.n >= sizeof(expr))
return fail(w, "type \"%.*s\" is too long", (int)e.n, e.p);
memcpy(expr, e.p, e.n);
expr[e.n] = '\0';
if (type_text(w->a, NULL, expr, &t, err))
return fail(w, "%s", err[0] ? err : "bad type");
*out = slot_size(t);
if (!*out)
return fail(w, "type \"%.*s\" has zero slot width",
(int)type.n, type.p);
return 0;
}
/* field writes one whitespace-form field line, keeping or assigning its
* '@N' and advancing the cursor. */
static int field(struct work *w, struct line ln, long long *cursor)
{
struct span_str b = cut(ln.body), name, type, rest;
long long off = 0, sz;
int has = 0;
char buf[64];
name = word(b, &rest);
if (!name.n || !rest.n)
return fail(w, "malformed field \"%.*s\"", (int)b.n, b.p);
type = word(rest, &rest);
if (!type.n)
return fail(w, "field \"%.*s\" missing type", (int)name.n, name.p);
rest = trim(rest);
if (rest.n) {
if (rest.p[0] != '@')
return fail(w, "field \"%.*s\": unexpected trailing \"%.*s\"",
(int)name.n, name.p, (int)rest.n, rest.p);
if (number(w, trim(span(rest.p + 1, rest.n - 1)), &off)) {
char msg[ERRLEN];
snprintf(msg, sizeof(msg), "field \"%.*s\": bad @offset: %s",
(int)name.n, name.p, w->err);
snprintf(w->err, ERRLEN, "%s", msg);
return -1;
}
has = 1;
}
if (width(w, type, &sz)) {
char msg[ERRLEN];
snprintf(msg, sizeof(msg), "field \"%.*s\": %s",
(int)name.n, name.p, w->err);
snprintf(w->err, ERRLEN, "%s", msg);
return -1;
}
if (!has)
off = *cursor;
*cursor = off + sz;
if (w->written++)
str_add(&w->out, "\n", 1);
indent(&w->out, ln.indent);
str_add(&w->out, name.p, (int)name.n);
str_add(&w->out, " ", 1);
str_add(&w->out, type.p, (int)type.n);
snprintf(buf, sizeof(buf), " @%lld", off);
str_add(&w->out, buf, -1);
return 0;
}
/* --- the walk ----------------------------------------------------------- */
/* how a block's body lines are read. */
enum mode {
AT_FILE, /* file scope: no enclosing block */
AT_STRUCT, /* fields, offsets assigned */
AT_IFACE /* methods, written through as they stand */
};
static int walk(struct work *w, size_t start, size_t end, enum mode mode);
/* extent returns the index one past the last line of a block whose header
* sits at head: the run of following lines indented deeper, with blank and
* comment lines absorbed so they neither open nor close a block. */
static size_t extent(struct work *w, size_t start, size_t end, int head)
{
size_t i = start;
while (i < end) {
if (w->line[i].kind == LINE_CLEAR) {
i++;
continue;
}
if (w->line[i].indent <= head)
break;
i++;
}
/* Trim trailing blank lines back out, so a blank line between two
* top-level structs closes the first cleanly. */
while (i > start && w->line[i - 1].kind == LINE_CLEAR)
i--;
return i;
}
/* header opens a braceless block, walks its indented body in the matching
* mode, closes the brace, and returns the index one past the body. */
static size_t header(struct work *w, size_t i, size_t end, int *bad)
{
struct line ln = w->line[i];
struct span_str h = cut(ln.body);
size_t body_end;
if (w->written++)
str_add(&w->out, "\n", 1);
indent(&w->out, ln.indent);
str_add(&w->out, h.p, (int)h.n);
str_add(&w->out, " {", 2);
body_end = extent(w, i + 1, end, ln.indent);
if (walk(w, i + 1, body_end,
strcmp(ln.keyword, "interface") == 0 ? AT_IFACE : AT_STRUCT)) {
*bad = 1;
return body_end;
}
if (w->written++)
str_add(&w->out, "\n", 1);
indent(&w->out, ln.indent);
str_add(&w->out, "}", 1);
return body_end;
}
/* brace copies lines from i verbatim - the line opens at least one '{' -
* until the running depth is back to zero. */
static size_t brace(struct work *w, size_t i, size_t end)
{
int depth = 0;
while (i < end) {
put(w, w->line[i].raw);
depth += delta(w->line[i].raw);
i++;
if (depth <= 0)
break;
}
return i;
}
static int walk(struct work *w, size_t start, size_t end, enum mode mode)
{
long long cursor = 0; /* the struct's running field offset */
size_t i = start;
while (i < end) {
struct line ln = w->line[i];
if (ln.kind == LINE_CLEAR) {
put(w, ln.raw);
i++;
continue;
}
/* A line that opens a literal brace block is brace syntax:
* copy it and everything to its matching '}' as written, so
* brace declarations stay byte-identical. */
if (delta(ln.raw) > 0) {
i = brace(w, i, end);
continue;
}
/* A whitespace header opens a sub-block only at file scope.
* Inside a struct the same line is a field named `interface`,
* not a service; inside an interface it is a method. */
if (ln.kind == LINE_HEAD && mode == AT_FILE) {
int bad = 0;
i = header(w, i, end, &bad);
if (bad)
return -1;
continue;
}
if (mode == AT_STRUCT) {
if (field(w, ln, &cursor))
return -1;
} else {
/* File scope or interface body: written through as it
* stands. At file scope an un-headered field-shaped
* line is left for the parser to name precisely. */
put(w, ln.raw);
}
i++;
}
return 0;
}
/* aliases records every top-level `type X = <expr>` so a field's offset can
* be sized through the name. An alias is ONLY a top-level construct, so a
* field literally named `type` - under a header or inside braces - is not
* read as one; it stays a field and is sized on the field path. */
static int aliases(struct work *w)
{
int depth = 0;
size_t i;
w->aname = arena_get(w->a, w->n * sizeof(*w->aname) + 1);
w->aexpr = arena_get(w->a, w->n * sizeof(*w->aexpr) + 1);
if (!w->aname || !w->aexpr)
return fail(w, "out of memory");
for (i = 0; i < w->n; i++) {
struct line ln = w->line[i];
int top = depth == 0 && ln.indent == 0;
struct span_str body, rest, name, expr;
size_t j, eq;
depth += delta(ln.raw);
if (!top)
continue;
body = cut(ln.body);
if (!after(body, "type", &rest))
continue;
eq = rest.n;
for (j = 0; j < rest.n; j++) {
if (rest.p[j] == '=') {
eq = j;
break;
}
}
if (eq == rest.n)
return fail(w, "alias \"%.*s\" missing '='",
(int)body.n, body.p);
name = trim(span(rest.p, eq));
expr = trim(span(rest.p + eq + 1, rest.n - eq - 1));
if (!name.n || !expr.n)
return fail(w, "malformed alias \"%.*s\"",
(int)body.n, body.p);
w->aname[w->aliases] = name;
w->aexpr[w->aliases] = expr;
w->aliases++;
}
return 0;
}
/* split cuts src into lines, dropping one trailing empty element so a file
* ending in a newline comes back the same length. */
static size_t split(const char *src, size_t len, struct line **out,
struct arena *a)
{
size_t n = 0, i, start = 0;
struct line *ln;
for (i = 0; i < len; i++)
if (src[i] == '\n')
n++;
if (!len || src[len - 1] != '\n')
n++; /* the last, unterminated line */
if (!len)
n = 0;
ln = arena_get(a, (n + 1) * sizeof(*ln));
if (!ln) {
*out = NULL;
return 0;
}
n = 0;
for (i = 0; i < len; i++) {
if (src[i] == '\n') {
ln[n++] = classify(span(src + start, i - start));
start = i + 1;
}
}
if (start < len)
ln[n++] = classify(span(src + start, len - start));
*out = ln;
return n;
}
int desugar(const char *src, size_t len, char **out, size_t *outlen,
char err[ERRLEN])
{
struct work w;
int rc = -1;
memset(&w, 0, sizeof(w));
str_init(&w.out, 0);
w.err = err;
err[0] = '\0';
w.a = arena_new();
if (!w.a) {
snprintf(err, ERRLEN, "out of memory");
goto done;
}
w.n = split(src, len, &w.line, w.a);
if (w.n && !w.line) {
snprintf(err, ERRLEN, "out of memory");
goto done;
}
/* Aliases first: the offset cursor sizes an aliased field through
* the real type parser, so there is one way to size a type. */
if (aliases(&w))
goto done;
if (walk(&w, 0, w.n, AT_FILE))
goto done;
/* Keep the source's final-newline state, so a pure-brace file comes
* back byte for byte. */
if (len && src[len - 1] == '\n')
str_add(&w.out, "\n", 1);
*out = malloc((size_t)w.out.len + 1);
if (!*out) {
snprintf(err, ERRLEN, "out of memory");
goto done;
}
memcpy(*out, w.out.str, (size_t)w.out.len);
(*out)[w.out.len] = '\0';
*outlen = (size_t)w.out.len;
rc = 0;
done:
str_release(&w.out);
arena_free(w.a);
return rc;
}