From d70decab14f459337aa51c5e4743d4a0f9146d5c Mon Sep 17 00:00:00 2001 From: Bill Hails Date: Sat, 23 May 2026 18:40:57 +0100 Subject: [PATCH 1/2] stage 1 complete --- docs/agent/language-syntax.md | 13 +++ src/pratt_parser.c | 176 +++++++++++++++++++++++------ tests/fn/test_typedef_shorthand.fn | 143 +++++++++++++++++++++++ 3 files changed, 295 insertions(+), 37 deletions(-) create mode 100644 tests/fn/test_typedef_shorthand.fn diff --git a/docs/agent/language-syntax.md b/docs/agent/language-syntax.md index c0f47a0c..d498ffba 100644 --- a/docs/agent/language-syntax.md +++ b/docs/agent/language-syntax.md @@ -38,6 +38,19 @@ typedef typename(#generic) { constructor1(types) | constructor2 } - Named fields: `constructor{ fieldName: type }` - Typedefs at `namespace` level are global; typedefs inside `let` blocks are scoped to that block +#### Shorthand typedef forms + +When a typedef has exactly one constructor that shares the typedef's name, the body can be omitted: + +| Shorthand | Equivalent full form | +|-----------|----------------------| +| `typedef t;` | `typedef t { t }` | +| `typedef t(#a);` | `typedef t(#a) { t(#a) }` | +| `typedef t(char);` | `typedef t { t(char) }` | +| `typedef t(#a, char);` | `typedef t(#a) { t(#a, char) }` | + +The rewrite rule: generic type variables (`#name`) appearing in the argument list are lifted to the typedef head in first-appearance order; non-generic types remain only in the synthesised constructor. Duplicate generic variables in the arg list (`typedef t(#a, #a);`) contribute only one entry on the head. + ### Namespaces Files start with `namespace` keyword (like `let` without `in` - mutually recursive declarations). diff --git a/src/pratt_parser.c b/src/pratt_parser.c index 28a671a5..b1ca282c 100644 --- a/src/pratt_parser.c +++ b/src/pratt_parser.c @@ -191,7 +191,6 @@ static AstTypeFunction *typeFunction(PrattParser *); static AstTypeList *typeList(PrattParser *); static AstTypeList *typeTuple(PrattParser *); static AstTypeMap *typeMap(PrattParser *); -static AstTypeSymbols *typeVariables(PrattParser *); static AstType *typeType(PrattParser *); static HashSymbol *symbol(PrattParser *); static HashSymbol *typeVariable(PrattParser *); @@ -4613,8 +4612,111 @@ static AstDefinition *multiDefinition(PrattParser *parser) { return res; } +/** + * @brief Returns true if `type` is a plain type variable with no arrow + * continuation (i.e. a bare `#name`). + */ +static bool isSimpleTypeVar(AstType *type) { + return type->next == NULL && + type->typeClause->type == AST_TYPECLAUSE_TYPE_VAR; +} + +/** + * @brief Converts an AstTypeList to AstTypeSymbols for an explicit typedef + * head, validating that every entry is a plain type variable. + * Emits a parser error for any non-variable entry and substitutes a + * wildcard so that parsing can continue. + */ +static AstTypeSymbols *typeListToHeadSymbols(PrattParser *parser, + AstTypeList *list, ParserInfo pi) { + if (list == NULL) + return NULL; + AstType *type = list->type; + if (!isSimpleTypeVar(type)) { + parserErrorAt(pi, parser, + "typedef head can only contain type variables (#name)"); + } + HashSymbol *typeVar = + isSimpleTypeVar(type) ? type->typeClause->val.var : TOK_WILDCARD(); + AstTypeSymbols *rest = typeListToHeadSymbols(parser, list->next, pi); + int save = PROTECT(rest); + AstTypeSymbols *result = newAstTypeSymbols(pi, typeVar, rest); + UNPROTECT(save); + return result; +} + +/** + * @brief Collects distinct type-variable (#var) entries from an AstTypeList + * in first-appearance order and returns them as an AstTypeSymbols list. + * Non-variable entries are silently skipped; they remain only in the + * synthesised constructor's argument list. + */ +static AstTypeSymbols *liftGenericVarsFromTypeList(AstTypeList *list, + ParserInfo pi) { + HashSymbol *vars[64]; + int count = 0; + for (AstTypeList *cur = list; cur != NULL; cur = cur->next) { + AstType *type = cur->type; + if (isSimpleTypeVar(type)) { + HashSymbol *sym = type->typeClause->val.var; + bool seen = false; + for (int i = 0; i < count; i++) { + if (vars[i] == sym) { + seen = true; + break; + } + } + if (!seen && count < 64) { + vars[count++] = sym; + } + } + } + AstTypeSymbols *result = NULL; + for (int i = count - 1; i >= 0; i--) { + int save = PROTECT(result); + result = newAstTypeSymbols(pi, vars[i], result); + UNPROTECT(save); + } + return result; +} + +/** + * @brief Synthesises a single-constructor AstTypeBody for the shorthand + * typedef forms. The constructor shares the typedef's name. When args is + * non-NULL it becomes the constructor's argument list; otherwise the + * constructor is zero-argument. + */ +static AstTypeBody *synthesizeShorthandTypeBody(ParserInfo pi, HashSymbol *name, + AstTypeList *args) { + int save = STARTPROTECT(); + AstTypeConstructorArgs *ctorArgs = NULL; + if (args != NULL) { + ctorArgs = newAstTypeConstructorArgs_List(pi, args); + PROTECT(ctorArgs); + } + AstTypeConstructor *ctor = newAstTypeConstructor(pi, name, ctorArgs); + PROTECT(ctor); + AstTypeBody *body = newAstTypeBody(pi, ctor, NULL); + UNPROTECT(save); + return body; +} + /** * @brief parses a typedef + * + * Accepts both the full explicit form and three shorthand forms: + * + * typedef name; + * => typedef name { name } + * + * typedef name(types...); + * => lift distinct #vars to head; synthesise single constructor + * e.g. typedef box(#t); => typedef box(#t) { box(#t) } + * typedef w(char); => typedef w { w(char) } + * typedef m(#a,char); => typedef m(#a) { m(#a, char) } + * + * typedef name(types...) { body } + * => explicit form; types must all be plain type variables */ static AstDefinition *typeDefinition(PrattParser *parser) { ENTER(typeDefinition); @@ -4622,21 +4724,49 @@ static AstDefinition *typeDefinition(PrattParser *parser) { int save = PROTECT(tok); HashSymbol *s = symbol(parser); AstTypeSig *typeSig = NULL; + AstTypeBody *type_body = NULL; if (check(parser, TOK_OPEN())) { next(parser); validateLastAlloc(); - AstTypeSymbols *variables = typeVariables(parser); - PROTECT(variables); + AstTypeList *arg_list = typeList(parser); + PROTECT(arg_list); consume(parser, TOK_CLOSE()); - typeSig = newAstTypeSig(TOKPI(tok), s, variables); + if (check(parser, TOK_LCURLY())) { + // Explicit typedef with body: all args must be type variables. + AstTypeSymbols *variables = + typeListToHeadSymbols(parser, arg_list, TOKPI(tok)); + PROTECT(variables); + typeSig = newAstTypeSig(TOKPI(tok), s, variables); + PROTECT(typeSig); + consume(parser, TOK_LCURLY()); + type_body = typeBody(parser); + PROTECT(type_body); + consume(parser, TOK_RCURLY()); + } else { + // Shorthand: typedef name(types...); + AstTypeSymbols *variables = + liftGenericVarsFromTypeList(arg_list, TOKPI(tok)); + PROTECT(variables); + typeSig = newAstTypeSig(TOKPI(tok), s, variables); + PROTECT(typeSig); + type_body = synthesizeShorthandTypeBody(CPI(typeSig), s, arg_list); + PROTECT(type_body); + } } else { typeSig = newAstTypeSig(TOKPI(tok), s, NULL); + PROTECT(typeSig); + if (check(parser, TOK_LCURLY())) { + // Explicit typedef with no type parameters. + consume(parser, TOK_LCURLY()); + type_body = typeBody(parser); + PROTECT(type_body); + consume(parser, TOK_RCURLY()); + } else { + // Shorthand: typedef name; + type_body = synthesizeShorthandTypeBody(CPI(typeSig), s, NULL); + PROTECT(type_body); + } } - PROTECT(typeSig); - consume(parser, TOK_LCURLY()); - AstTypeBody *type_body = typeBody(parser); - PROTECT(type_body); - consume(parser, TOK_RCURLY()); AstDefinition *res = makeAstDefinition_TypeDef(CPI(typeSig), typeSig, type_body); LEAVE(typeDefinition); @@ -4692,34 +4822,6 @@ static AstTypeConstructor *typeConstructor(PrattParser *parser) { return res; } -/** - * @brief parses the type variable arguments to the type signature of a - * typedef - */ -static AstTypeSymbols *typeVariables(PrattParser *parser) { - ENTER(typeVariables); - PrattToken *tok = peek(parser); - int save = PROTECT(tok); - HashSymbol *s = typeVariable(parser); - AstTypeSymbols *t = NULL; - if (check(parser, TOK_CLOSE())) { - t = newAstTypeSymbols(TOKPI(tok), s, NULL); - } else { - consume(parser, TOK_COMMA()); - // Allow trailing comma: only continue if not at closing paren - if (!check(parser, TOK_CLOSE())) { - AstTypeSymbols *rest = typeVariables(parser); - PROTECT(rest); - t = newAstTypeSymbols(TOKPI(tok), s, rest); - } else { - t = newAstTypeSymbols(TOKPI(tok), s, NULL); - } - } - LEAVE(typeVariables); - UNPROTECT(save); - return t; -} - /** * @brief parses a link (nameSpace import) directive. */ diff --git a/tests/fn/test_typedef_shorthand.fn b/tests/fn/test_typedef_shorthand.fn new file mode 100644 index 00000000..2bde9083 --- /dev/null +++ b/tests/fn/test_typedef_shorthand.fn @@ -0,0 +1,143 @@ +// Typedef shorthand forms +// typedef t; => typedef t { t } +// typedef t(types...); => lift #vars to head, single constructor with full arg list +// e.g. typedef box(#t); => typedef box(#t) { box(#t) } +// typedef w(char); => typedef w { w(char) } +// typedef m(#a, char); => typedef m(#a) { m(#a, char) } + +let + // bare shorthand: typedef colour; => typedef colour { colour } + fn test_bare_shorthand() { + let + typedef tag + x = tag + in + switch (x) { + (tag) { true } + } + } + + // all-generic shorthand: typedef box(#t); => typedef box(#t) { box(#t) } + fn test_generic_shorthand() { + let + typedef box(#t) + int_box = box(42); + str_box = box("hello") + in { + assert(switch (int_box) { (box(v)) { v } } == 42); + assert(switch (str_box) { (box(v)) { v } } == "hello"); + true + } + } + + // concrete-type shorthand: typedef wrapper(char); => typedef wrapper { wrapper(char) } + fn test_concrete_shorthand() { + let + typedef wrapped(char) + w = wrapped('z') + in + switch (w) { + (wrapped(c)) { + assert(c == 'z'); + true + } + } + } + + // number-type shorthand + fn test_number_shorthand() { + let + typedef counted(number) + c = counted(99) + in + switch (c) { + (counted(n)) { + assert(n == 99); + true + } + } + } + + // mixed shorthand: typedef pair(#a, char); => typedef pair(#a) { pair(#a, char) } + fn test_mixed_shorthand() { + let + typedef tagged(#a, char) + t = tagged(42, 'x') + in + switch (t) { + (tagged(n, c)) { + assert(n == 42); + assert(c == 'x'); + true + } + } + } + + // two generic params shorthand: typedef pair(#a, #b); => typedef pair(#a, #b) { pair(#a, #b) } + fn test_two_param_shorthand() { + let + typedef kv(#k, #v) + p = kv("name", 42) + in + switch (p) { + (kv(k, v)) { + assert(k == "name"); + assert(v == 42); + true + } + } + } + + // duplicate generic var: typedef dup(#a, #a); => typedef dup(#a) { dup(#a, #a) } + fn test_duplicate_var_shorthand() { + let + typedef dup(#a, #a) + d = dup(7, 7) + in + switch (d) { + (dup(x, y)) { + assert(x == y); + true + } + } + } + + // no-trailing-semicolon shorthand (same as test_parser_semi_3 style) + fn test_shorthand_no_semicolon() { + let + typedef token(#t) + x = token(true) + in + switch (x) { + (token(b)) { b } + } + } + + // shorthand inside a nested let block + fn test_shorthand_nested() { + let + outer = true + in { + let + typedef inner(char) + v = inner('q') + in + switch (v) { + (inner(c)) { + assert(c == 'q'); + true + } + } + } + } + +in + test_bare_shorthand(); + test_generic_shorthand(); + test_concrete_shorthand(); + test_number_shorthand(); + test_mixed_shorthand(); + test_two_param_shorthand(); + test_duplicate_var_shorthand(); + test_shorthand_no_semicolon(); + test_shorthand_nested() From 94727821adf90b07736b71ed8710b0a5f0920a23 Mon Sep 17 00:00:00 2001 From: Bill Hails Date: Sat, 23 May 2026 19:10:15 +0100 Subject: [PATCH 2/2] typedef shorthand complete --- docs/agent/language-syntax.md | 10 ++- src/pratt_parser.c | 104 ++++++++++++++++++++++++++--- src/pratt_scanner.c | 16 ++++- tests/fn/test_typedef_shorthand.fn | 84 ++++++++++++++++++++++- 4 files changed, 201 insertions(+), 13 deletions(-) diff --git a/docs/agent/language-syntax.md b/docs/agent/language-syntax.md index d498ffba..a126dbd8 100644 --- a/docs/agent/language-syntax.md +++ b/docs/agent/language-syntax.md @@ -48,8 +48,16 @@ When a typedef has exactly one constructor that shares the typedef's name, the b | `typedef t(#a);` | `typedef t(#a) { t(#a) }` | | `typedef t(char);` | `typedef t { t(char) }` | | `typedef t(#a, char);` | `typedef t(#a) { t(#a, char) }` | +| `typedef t{field: type, ...};` | `typedef t(...) { t{field: type, ...} }` | -The rewrite rule: generic type variables (`#name`) appearing in the argument list are lifted to the typedef head in first-appearance order; non-generic types remain only in the synthesised constructor. Duplicate generic variables in the arg list (`typedef t(#a, #a);`) contribute only one entry on the head. +The rewrite rule for positional shorthands: generic type variables (`#name`) appearing in the argument list are lifted to the typedef head in first-appearance order; non-generic types remain only in the synthesised constructor. Duplicate generic variables contribute only one entry on the head. + +The rewrite rule for tagged (named-field) shorthands: generic type variables that appear as the complete type of a field (i.e. a plain `#name`, not inside `list(#a)` etc.) are lifted to the typedef head in left-to-right first-appearance order. Concrete and nested field types are left unchanged. Example: + +``` +typedef person{name: string, meta: #a} + => typedef person(#a) { person{name: string, meta: #a} } +``` ### Namespaces diff --git a/src/pratt_parser.c b/src/pratt_parser.c index b1ca282c..efc2dedd 100644 --- a/src/pratt_parser.c +++ b/src/pratt_parser.c @@ -4701,10 +4701,62 @@ static AstTypeBody *synthesizeShorthandTypeBody(ParserInfo pi, HashSymbol *name, return body; } +/** + * @brief Collects distinct type-variable (#var) entries from an AstTypeMap + * in first-appearance order, checking only plain field types (shallow scan). + * Field types that are not bare type variables are silently skipped. + */ +static AstTypeSymbols *liftGenericVarsFromTypeMap(AstTypeMap *map, + ParserInfo pi) { + HashSymbol *vars[64]; + int count = 0; + for (AstTypeMap *cur = map; cur != NULL; cur = cur->next) { + AstType *type = cur->type; + if (isSimpleTypeVar(type)) { + HashSymbol *sym = type->typeClause->val.var; + bool seen = false; + for (int i = 0; i < count; i++) { + if (vars[i] == sym) { + seen = true; + break; + } + } + if (!seen && count < 64) { + vars[count++] = sym; + } + } + } + AstTypeSymbols *result = NULL; + for (int i = count - 1; i >= 0; i--) { + int save = PROTECT(result); + result = newAstTypeSymbols(pi, vars[i], result); + UNPROTECT(save); + } + return result; +} + +/** + * @brief Synthesises a single-constructor AstTypeBody for the tagged + * typedef shorthand form. The constructor shares the typedef's name and + * uses the supplied field map as its record-style arguments. + */ +static AstTypeBody *synthesizeShorthandTypeBodyFromMap(ParserInfo pi, + HashSymbol *name, + AstTypeMap *args) { + int save = STARTPROTECT(); + AstTypeConstructorArgs *ctorArgs = newAstTypeConstructorArgs_Map(pi, args); + PROTECT(ctorArgs); + AstTypeConstructor *ctor = newAstTypeConstructor(pi, name, ctorArgs); + PROTECT(ctor); + AstTypeBody *body = newAstTypeBody(pi, ctor, NULL); + UNPROTECT(save); + return body; +} + /** * @brief parses a typedef * - * Accepts both the full explicit form and three shorthand forms: + * Accepts both the full explicit form and four shorthand forms: * * typedef name; * => typedef name { name } @@ -4715,6 +4767,12 @@ static AstTypeBody *synthesizeShorthandTypeBody(ParserInfo pi, HashSymbol *name, * typedef w(char); => typedef w { w(char) } * typedef m(#a,char); => typedef m(#a) { m(#a, char) } * + * typedef name{field: type, ...}; + * => lift distinct #vars from plain field types to head; synthesise + * single tagged constructor sharing the typedef name + * e.g. typedef person{name: string, meta: #a}; + * => typedef person(#a) { person{name: string, meta: #a} } + * * typedef name(types...) { body } * => explicit form; types must all be plain type variables */ @@ -4753,15 +4811,45 @@ static AstDefinition *typeDefinition(PrattParser *parser) { PROTECT(type_body); } } else { - typeSig = newAstTypeSig(TOKPI(tok), s, NULL); - PROTECT(typeSig); if (check(parser, TOK_LCURLY())) { - // Explicit typedef with no type parameters. - consume(parser, TOK_LCURLY()); - type_body = typeBody(parser); - PROTECT(type_body); - consume(parser, TOK_RCURLY()); + // Peek inside '{' to distinguish: + // explicit body: typedef name { constructor... } + // tagged shorthand: typedef name{field: type, ...} + PrattToken *lcurly = next(parser); + int save2 = PROTECT(lcurly); + PrattToken *firstTok = next(parser); + PROTECT(firstTok); + if (firstTok->type == TOK_ATOM() && check(parser, TOK_COLON())) { + // Tagged shorthand: push back first field name for typeMap(). + poke(parser, firstTok); + UNPROTECT(save2); // firstTok now in queue; lcurly unneeded + AstTypeMap *field_map = typeMap(parser); + PROTECT(field_map); + consume(parser, TOK_RCURLY()); + AstTypeSymbols *variables = + liftGenericVarsFromTypeMap(field_map, TOKPI(tok)); + PROTECT(variables); + typeSig = newAstTypeSig(TOKPI(tok), s, variables); + PROTECT(typeSig); + type_body = synthesizeShorthandTypeBodyFromMap(CPI(typeSig), s, + field_map); + PROTECT(type_body); + } else { + // Explicit typedef body: push back in reverse order so + // next() returns them as: { firstTok ... + poke(parser, firstTok); + poke(parser, lcurly); + UNPROTECT(save2); // both now in queue (GC-safe) + typeSig = newAstTypeSig(TOKPI(tok), s, NULL); + PROTECT(typeSig); + consume(parser, TOK_LCURLY()); + type_body = typeBody(parser); + PROTECT(type_body); + consume(parser, TOK_RCURLY()); + } } else { + typeSig = newAstTypeSig(TOKPI(tok), s, NULL); + PROTECT(typeSig); // Shorthand: typedef name; type_body = synthesizeShorthandTypeBody(CPI(typeSig), s, NULL); PROTECT(type_body); diff --git a/src/pratt_scanner.c b/src/pratt_scanner.c index 5f4ec169..f70ef733 100644 --- a/src/pratt_scanner.c +++ b/src/pratt_scanner.c @@ -1344,13 +1344,23 @@ static PrattBuffer *prattBufferFromFileName(char *path) { } /** - * @brief Re-enqueues a token back into the lexer. + * @brief Pushes a token back to the front of the lexer queue. + * + * Unlike enqueueToken() which appends to the tail, poke() prepends to the + * head so that multiple successive poke() calls produce LIFO (stack) order. + * This enables arbitrary pushback: the last token poked is the next one + * returned by next(). * * @param parser The PrattParser instance containing the lexer. - * @param token The PrattToken to re-enqueue. + * @param token The PrattToken to push back. */ void poke(PrattParser *parser, PrattToken *token) { - enqueueToken(parser->lexer, token); + PrattLexer *lexer = parser->lexer; + token->next = lexer->tokenHead; + lexer->tokenHead = token; + if (lexer->tokenTail == NULL) { + lexer->tokenTail = token; + } } /** diff --git a/tests/fn/test_typedef_shorthand.fn b/tests/fn/test_typedef_shorthand.fn index 2bde9083..878b385f 100644 --- a/tests/fn/test_typedef_shorthand.fn +++ b/tests/fn/test_typedef_shorthand.fn @@ -131,6 +131,83 @@ let } } + // tagged shorthand: no generics + // typedef point{x: number, y: number} + // => typedef point { point{x: number, y: number} } + fn test_tagged_no_generics() { + let + typedef point{x: number, y: number} + p = point{x: 3, y: 4} + in + switch (p) { + (point{x: a, y: b}) { + assert(a == 3); + assert(b == 4); + true + } + } + } + + // tagged shorthand: one lifted generic field + // typedef box{value: #t} + // => typedef box(#t) { box{value: #t} } + fn test_tagged_one_generic() { + let + typedef box{value: #t} + b1 = box{value: 42} + b2 = box{value: "hello"} + in { + assert(switch (b1) { (box{value: v}) { v } } == 42); + assert(switch (b2) { (box{value: v}) { v } } == "hello"); + true + } + } + + // tagged shorthand: mixed concrete and generic fields + // typedef labelled{meta: #a, tag: char} + // => typedef labelled(#a) { labelled{meta: #a, tag: char} } + fn test_tagged_mixed() { + let + typedef labelled{meta: #a, tag: char} + l = labelled{meta: 99, tag: 'z'} + in + switch (l) { + (labelled{meta: m, tag: t}) { + assert(m == 99); + assert(t == 'z'); + true + } + } + } + + // tagged shorthand: two generics, distinct fields + // typedef pair{first: #a, second: #b} + // => typedef pair(#a, #b) { pair{first: #a, second: #b} } + fn test_tagged_two_generics() { + let + typedef pair{first: #a, second: #b} + p = pair{first: "key", second: 100} + in + switch (p) { + (pair{first: f, second: s}) { + assert(f == "key"); + assert(s == 100); + true + } + } + } + + // tagged shorthand: no trailing semicolon (mirrors test_shorthand_no_semicolon) + fn test_tagged_no_semicolon() { + let + typedef record{id: number} + r = record{id: 7} + in + switch (r) { + (record{id: n}) { n == 7 } + } + } + in test_bare_shorthand(); test_generic_shorthand(); @@ -140,4 +217,9 @@ in test_two_param_shorthand(); test_duplicate_var_shorthand(); test_shorthand_no_semicolon(); - test_shorthand_nested() + test_shorthand_nested(); + test_tagged_no_generics(); + test_tagged_one_generic(); + test_tagged_mixed(); + test_tagged_two_generics(); + test_tagged_no_semicolon()