aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authoreunhhu <[email protected]>2026-09-27 07:30:51 +0100
committereunhhu <[email protected]>2026-09-27 07:30:51 +0100
commit1a72fe031a007323c51f49810dcf6164b8aaaae2 (patch)
treeb170926648e20c8570c137231096313ac4e4fe4a /src
parent49324b0a75549960af0a13b1d884eaac637312ef (diff)
downloadaup-1a72fe031a007323c51f49810dcf6164b8aaaae2.tar.gz
aup-1a72fe031a007323c51f49810dcf6164b8aaaae2.zip
Group message fields: to: { city: text }, read as ship.to.city
A message's field block may hold groups, nested as deep as the 31 characters of a path allow; the compiler flattens them into fields named by their path (to.city), which Pure's module format takes, so groups cost nothing at run time. A member chain on a message names a field by its path; one that stops at a group, or names a field the group lacks, is diagnosed, and a field's own member (m.text.bytes) still reads the field. A module may have 64 message fields. The JSON server check posts 100 random bodies with groups two deep (members shuffled at every level, strings with escapes, control bytes, Korean, emoji and U+2028) to pure-serve, which reads them in place, and compares the answers and the table with a Python model; a group's fixed field from the query and ten malformed group bodies are checked too. Six diagnostics cover groups.
Diffstat (limited to 'src')
-rw-r--r--src/compile.c94
1 files changed, 74 insertions, 20 deletions
diff --git a/src/compile.c b/src/compile.c
index 27176a1..bccc2c3 100644
--- a/src/compile.c
+++ b/src/compile.c
@@ -9,7 +9,7 @@
/* Limits and opcodes of Pure module format v5. */
enum { STATES = 32, INPUTS = 32, VIEWS = 64, HANDLERS = 64, CODE = 65535, STACK = 64, NAME = 32, JOINS = 8,
- DEPTH = 64, FIELDS = 32, COLLECTIONS = 1, CONSTANTS = 4096, CONSTANT_BYTES = 1 << 20,
+ DEPTH = 64, FIELDS = 64, COLLECTIONS = 1, CONSTANTS = 4096, CONSTANT_BYTES = 1 << 20,
RECORDS = 8, RECORD_FIELDS = 64, FIELDS_PER_RECORD = 16, FNS = 64, PARAMS = 8, MODULES = 9 };
enum { CONST, LOAD, DT, ADD, SUB, MUL, DIV, NEG, LT, LE, GT, GE, EQ, NE, AND, OR, NOT,
MIN, MAX, CLAMP, STORE, JZ, JMP, FIELD, DOC, DOC_SIZE, LEN, REPLACE, REPLY, REQUIRE,
@@ -968,19 +968,42 @@ static bool key_name(const char *key) {
return false;
}
+/* Whether a message already has a field or a group of this path. */
+static bool path_taken(const Input *in, const char *path) {
+ size_t length = strlen(path);
+ for (unsigned i = 0; i < in->fields; ++i) {
+ const char *name = in->field_names[i];
+ size_t size = strlen(name);
+ if (!strcmp(name, path) || (size > length && !memcmp(name, path, length) && name[length] == '.') ||
+ (length > size && !memcmp(path, name, size) && path[size] == '.')) return true;
+ }
+ return false;
+}
+
/* { name: type ... }: u32 and number are fixed, bytes and text of any length
- * (Pure lays them out: docs/design/008-module-format.md). */
-static void message_fields(Compiler *c, Input *in) {
+ * (Pure lays them out: docs/design/008-module-format.md). name: { ... } is a
+ * group of fields inside the message, named group.name (a JSON object inside
+ * a request's body). */
+static void message_fields(Compiler *c, Input *in, const char *group) {
if (!is(c, "{")) return; /* the field block opens on the declaration's line */
take(c);
separators(c);
+ unsigned first = in->fields;
while (!is(c, "}")) {
Token name = expect_name(c, "a field name");
if (reserved_name(name.text)) fail_at(c, name.line, name.column, "'%s' is reserved", name.text);
- for (unsigned i = 0; i < in->fields; ++i)
- if (!strcmp(in->field_names[i], name.text)) fail_at(c, name.line, name.column, "duplicate field '%s'", name.text);
- if (c->field_count == FIELDS) fail_at(c, name.line, name.column, "at most 32 message fields are allowed");
+ char path[NAME];
+ int length = snprintf(path, sizeof(path), "%s%s%s", group, *group ? "." : "", name.text);
+ if (length < 0 || length >= NAME)
+ fail_at(c, name.line, name.column, "'%s.%s' is longer than %d characters", group, name.text, NAME - 1);
+ if (path_taken(in, path)) fail_at(c, name.line, name.column, "duplicate field '%s'", path);
expect(c, ":", "':'");
+ if (is(c, "{")) {
+ message_fields(c, in, path);
+ end(c);
+ continue;
+ }
+ if (c->field_count == FIELDS) fail_at(c, name.line, name.column, "at most %d message fields are allowed", FIELDS);
Token type = expect_name(c, "u32, number, bytes or text");
static const struct { const char *name; int type; } types[] = {
{"u32", FIELD_U32}, {"number", FIELD_NUMBER}, {"bytes", FIELD_BYTES}, {"text", FIELD_TEXT}};
@@ -988,14 +1011,16 @@ static void message_fields(Compiler *c, Input *in) {
for (size_t t = 0; t < sizeof(types) / sizeof(*types); ++t)
if (!strcmp(type.text, types[t].name)) field_type = types[t].type;
if (!field_type) fail_at(c, type.line, type.column, "field types are u32, number, bytes and text");
- memcpy(in->field_names[in->fields], name.text, NAME);
+ memcpy(in->field_names[in->fields], path, NAME);
in->field_types[in->fields++] = field_type;
++c->field_count;
end(c);
}
+ if (*group && in->fields == first) fail_at(c, c->token.line, c->token.column, "the group '%s' has no fields", group);
take(c);
}
+
/* A signed number literal: a numeric state or field default. */
static double signed_number(Compiler *c, const char *what) {
double sign = 1;
@@ -1273,7 +1298,7 @@ static void declaration(Compiler *c) {
in->kind = KIND_MESSAGE;
if (word(c, "by")) { take(c); expect_word(c, "key"); in->keyed = true; }
++c->input_count;
- message_fields(c, in);
+ message_fields(c, in, "");
} else if (word(c, "text")) {
take(c);
in->kind = KIND_TEXT;
@@ -1508,6 +1533,28 @@ static int parameter(const Compiler *c, const char *name) {
return -1;
}
+/* A member chain on a message, as order.customer.name: the input and the
+ * path after it (customer.name). False for anything else. */
+static bool message_path(Compiler *c, const Expr *e, int *input, char path[NAME]) {
+ if (e->kind != E_MEMBER) return false;
+ const Expr *base = e->args[0];
+ if (base->kind == E_NAME) {
+ if (parameter(c, base->name) >= 0 || c->module) return false;
+ *input = input_index(c, base->name);
+ if (*input < 0 || c->inputs[*input].kind != KIND_MESSAGE) return false;
+ snprintf(path, NAME, "%s", e->member);
+ return true;
+ }
+ char inner[NAME];
+ if (!message_path(c, base, input, inner)) return false;
+ const Input *in = &c->inputs[*input];
+ for (unsigned i = 0; i < in->fields; ++i)
+ if (!strcmp(in->field_names[i], inner)) return false; /* a field's own member, as m.text.bytes */
+ int length = snprintf(path, NAME, "%s.%s", inner, e->member);
+ if (length < 0 || length >= NAME) snprintf(path, NAME, "%s", inner); /* too long: names nothing */
+ return true;
+}
+
/* A name of the app (a state, input or collection): not visible in a library
* fn, and hidden by a parameter of the same name. */
static void app_name(Compiler *c, const Expr *e, const char *name) {
@@ -1588,20 +1635,27 @@ static Kind value(Compiler *c, const Expr *e, bool handler, int height) {
const Expr *base = e->args[0];
int library = base->kind == E_NAME && parameter(c, base->name) < 0 ? used_library(c, c->module, base->name) : -1;
if (library >= 0) return inline_fn(c, e, library, e->member, handler, height); /* library.constant */
+ int message;
+ char path[NAME];
+ if (message_path(c, e, &message, path)) {
+ const Input *in = &c->inputs[message];
+ if (message != c->message)
+ fail_at(c, e->line, e->column, "fields of '%s' are only available in its handler", in->name);
+ size_t length = strlen(path);
+ for (unsigned i = 0; i < in->fields; ++i)
+ if (!strcmp(in->field_names[i], path)) {
+ emit(c, e->line, e->column, FIELD, i, 0);
+ int type = in->field_types[i];
+ return (Kind){type == FIELD_TEXT ? TEXT : type == FIELD_BYTES ? BYTES : NUM, -1};
+ }
+ for (unsigned i = 0; i < in->fields; ++i)
+ if (!strncmp(in->field_names[i], path, length) && in->field_names[i][length] == '.')
+ fail_at(c, e->line, e->column, "'%s.%s' is a group of fields; read one of them, as in %s.%s",
+ in->name, path, in->name, in->field_names[i]);
+ fail_at(c, e->line, e->column, "message '%s' has no field '%s'", in->name, path);
+ }
if (base->kind == E_NAME && parameter(c, base->name) < 0 && !c->module) {
int input = input_index(c, base->name);
- if (input >= 0 && c->inputs[input].kind == KIND_MESSAGE) {
- if (input != c->message)
- fail_at(c, e->line, e->column, "fields of '%s' are only available in its handler", base->name);
- const Input *in = &c->inputs[input];
- for (unsigned i = 0; i < in->fields; ++i)
- if (!strcmp(in->field_names[i], e->member)) {
- emit(c, e->line, e->column, FIELD, i, 0);
- int type = in->field_types[i];
- return (Kind){type == FIELD_TEXT ? TEXT : type == FIELD_BYTES ? BYTES : NUM, -1};
- }
- fail_at(c, e->line, e->column, "message '%s' has no field '%s'", base->name, e->member);
- }
if (input >= 0 && c->inputs[input].kind == KIND_KEY)
fail_at(c, base->line, base->column, "'%s' is a key input and has no fields", base->name);
int state = state_index(c, base->name);