Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions docs/agent/language-syntax.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,27 @@ 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) }` |
| `typedef t{field: type, ...};` | `typedef t(...) { t{field: type, ...} }` |

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

Files start with `namespace` keyword (like `let` without `in` - mutually recursive declarations).
Expand Down
266 changes: 228 additions & 38 deletions src/pratt_parser.c
Original file line number Diff line number Diff line change
Expand Up @@ -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 *);
Expand Down Expand Up @@ -4613,30 +4612,249 @@ 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 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 four 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{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
*/
static AstDefinition *typeDefinition(PrattParser *parser) {
ENTER(typeDefinition);
PrattToken *tok = peek(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);
if (check(parser, TOK_LCURLY())) {
// 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);
}
}
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);
Expand Down Expand Up @@ -4692,34 +4910,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.
*/
Expand Down
16 changes: 13 additions & 3 deletions src/pratt_scanner.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}

/**
Expand Down
Loading
Loading