basic parser, get that squared away
authorcassowarii <cassowary@cassowary.me>
Sat, 27 Jun 2026 04:26:02 +0000 (21:26 -0700)
committercassowarii <cassowary@cassowary.me>
Sat, 27 Jun 2026 04:26:02 +0000 (21:26 -0700)
the error reporting could use some work

src/parse/ast.h
src/parse/parser.c
src/parse/parser.h
src/parse/scanner.c
src/parse/token.h

index 89db788..2ed4424 100644 (file)
@@ -11,23 +11,29 @@ typedef enum sbAstType {
   AST_VAL_FLOAT,
   AST_VAL_SYMBOL,
   AST_VAL_BOOLEAN,
+  AST_VAL_FUNC,
+  AST_VAL_OBJ,
   AST_NODE_NAME,
   AST_NODE_SEQ,
   AST_NODE_OP,
   AST_NODE_DEF,
   AST_NODE_LET,
+  AST_NODE_ASSIGN,
   AST_NODE_IF,
   AST_NODE_THENELSE,
   AST_NODE_WHILE,
+  AST_NODE_REPEAT,
+  AST_NODE_CASE,
+  AST_NODE_MATCH,
   AST_NODE_LIST,
   AST_NODE_HASH,
   AST_NODE_HASHENTRY,
   AST_NODE_NEXT,
-  AST_NODE_GROUPING,
   AST_NODE_MULTIVAL,
   AST_NODE_FUNCCALL,
   AST_NODE_METHODCALL,
   AST_NODE_SEND,
+  AST_NODE_RETURN,
   AST_NODE_ELLIPSIS,
 } sbAstType;
 
@@ -56,6 +62,8 @@ typedef enum sbAstOp {
   AST_OP_OR,
   AST_OP_AND,
   AST_OP_NOT,
+  AST_OP_IN,
+  AST_OP_SPLAT,
 } sbAstOp;
 
 typedef struct sbAstNode {
@@ -66,15 +74,20 @@ typedef struct sbAstNode {
     hInteger i;
     double fl;
     struct {
-      const struct sbAstNode *left;
-      const struct sbAstNode *right;
+      struct sbAstNode *left;
+      struct sbAstNode *right;
     } seq;
     struct {
       sbAstOp type;
-      const struct sbAstNode *left;
-      const struct sbAstNode *right;
+      struct sbAstNode *left;
+      struct sbAstNode *right;
     } op;
+    struct {
+      struct sbAstNode *left;
+      struct sbAstNode *center;
+      struct sbAstNode *right;
+    } tri;
   };
 } sbAstNode;
 
-typedef const sbAstNode *sbAst;
+typedef sbAstNode *sbAst;
index 578aa52..83c09ee 100644 (file)
@@ -52,7 +52,7 @@ typedef struct opspelling {
   const char *name;
 } opspelling;
 
-static const sbAstNode SENTINEL_VALUE = {0};
+static sbAstNode SENTINEL_VALUE = {0};
 static sbAst NO_NODE = &SENTINEL_VALUE;
 
 static sbLexToken peek_ahead(hParser pr, usize count) {
@@ -71,17 +71,6 @@ static sbLexToken next_token(hParser pr) {
   return sbTokenQueue_at(&pr->input_queue, 0);
 }
 
-static flag accept(hParser pr, sbTokenType type, sbLexToken *tok_out) {
-  sbLexToken up_next = peek_ahead(pr, 0);
-  if (up_next.type == type) {
-    next_token(pr);
-    if (tok_out) *tok_out = up_next;
-    return TRUE;
-  } else {
-    return FALSE;
-  }
-}
-
 static flag expect(hParser pr, sbTokenType type, sbLexToken *tok_out) {
   sbLexToken up_next = peek_ahead(pr, 0);
   if (up_next.type == type) {
@@ -89,7 +78,6 @@ static flag expect(hParser pr, sbTokenType type, sbLexToken *tok_out) {
     if (tok_out) *tok_out = up_next;
     return TRUE;
   } else {
-    fprintf(stderr, "syntax error ! unexpected type %d\n", up_next.type); // TODO add line information, etc.
     return FALSE;
   }
 }
@@ -128,6 +116,31 @@ static sbAst seq_node(hParser pr, sbAstType type, sbAst left, sbAst right) {
   return new_node(pr, &n);
 }
 
+static sbAst tri_node(hParser pr, sbAstType type, sbAst left, sbAst center, sbAst right) {
+  sbAstNode n = (sbAstNode) {
+    .type = type,
+    .tri.left = left,
+    .tri.center = center,
+    .tri.right = right,
+  };
+  return new_node(pr, &n);
+}
+
+static sbAst wrap_node(hParser pr, sbAstType type, sbAst left) {
+  sbAstNode n = (sbAstNode) {
+    .type = type,
+    .seq.left = left,
+  };
+  return new_node(pr, &n);
+}
+
+static sbAst atomic_node(hParser pr, sbAstType type) {
+  sbAstNode n = {
+    .type = type,
+  };
+  return new_node(pr, &n);
+}
+
 static sbAst name_node(hParser pr, sbLexToken token) {
   if (token.type != T_IDENTIFIER) {
     PANIC("can't create name node with token of type %d\n", token.type);
@@ -144,6 +157,7 @@ static binop binops[] = {
   { T_PIPE, 6, 7, AST_OP_PIPE },
   { T_rOR, 10, 11, AST_OP_OR },
   { T_rAND, 20, 21, AST_OP_AND },
+  { T_rIN, 25, 26, AST_OP_IN },
   { T_DOUBLEEQUALS, 30, 31, AST_OP_EQ },
   { T_GREATER, 30, 31, AST_OP_GT },
   { T_LESS, 30, 31, AST_OP_LT },
@@ -175,6 +189,7 @@ static opspelling op_spellings[] = {
   { AST_OP_OR, "or" },
   { AST_OP_AND, "and" },
   { AST_OP_NOT, "not" },
+  { AST_OP_IN, "in" },
   { AST_OP_EQ, "==" },
   { AST_OP_NE, "!=" },
   { AST_OP_GT, ">" },
@@ -200,14 +215,101 @@ const int NUM_BINOPS = sizeof(binops) / sizeof(binops[0]);
 const int NUM_UNOPS = sizeof(unops) / sizeof(unops[0]);
 const int NUM_SPELLINGS = sizeof(op_spellings) / sizeof(op_spellings[0]);
 
+static sbAst parse_expr(hParser pr, u8 min_precedence);
+static sbAst parse_comma_exprs(hParser pr, sbAst after) {
+  sbAst result = NO_NODE;
+  sbAst *put_here = &result;
+  sbAst expr;
+
+  if (after) {
+    *put_here = seq_node(pr, AST_NODE_MULTIVAL, after, NO_NODE);
+    put_here = &(*put_here)->seq.right;
+  }
+
+  do {
+    if (expect(pr, T_ELLIPSIS, NULL)) {
+      sbAst splatted_expr = parse_expr(pr, 0);
+      if (splatted_expr != NO_NODE) {
+        /* "...something" */
+        expr = unop_node(pr, AST_OP_SPLAT, splatted_expr);
+      } else {
+        /* plain "..." */
+        expr = atomic_node(pr, AST_NODE_ELLIPSIS);
+      }
+    } else {
+      expr = parse_expr(pr, 0);
+
+      /* this 'break' allows empty () and trailing comma */
+      if (expr == NO_NODE) break;
+    }
+
+    *put_here = seq_node(pr, AST_NODE_MULTIVAL, expr, NO_NODE);
+    put_here = &(*put_here)->seq.right;
+  } while (expect(pr, T_COMMA, NULL));
+
+  return result;
+}
+
+static sbAst parse_name(hParser pr) {
+  sbLexToken t = peek_ahead(pr, 0);
+  if (t.type == T_IDENTIFIER) {
+    return name_node(pr, next_token(pr));
+  }
+  return NO_NODE;
+}
+
+static sbAst parse_block(hParser pr);
+
 /* https://matklad.github.io/2020/04/13/simple-but-powerful-pratt-parsing.html */
 static sbAst parse_expr(hParser pr, u8 min_precedence) {
   sbLexToken t = peek_ahead(pr, 0);
   sbAst lhs = NO_NODE;
-  if (t.type == T_IDENTIFIER) {
-    expect(pr, T_IDENTIFIER, NULL);
-    sbAstNode n = { .type = AST_NODE_NAME, .symb = t.symb };
+  if (t.type == T_rNIL) {
+    sbAstNode n = { .type = AST_VAL_NIL };
+    lhs = new_node(pr, &n);
+  } else if (t.type == T_rTRUE || t.type == T_rFALSE) {
+    sbAstNode n = { .type = AST_VAL_BOOLEAN, .i = (t.type == T_rTRUE) };
+    lhs = new_node(pr, &n);
+  } else if (t.type == T_INTEGER) {
+    sbAstNode n = { .type = AST_VAL_INT, .i = t.i };
     lhs = new_node(pr, &n);
+  } else if (t.type == T_FLOAT) {
+    sbAstNode n = { .type = AST_VAL_FLOAT, .fl = t.fl };
+    lhs = new_node(pr, &n);
+  } else if (t.type == T_SYMBOL) {
+    sbAstNode n = { .type = AST_VAL_SYMBOL, .symb = t.symb };
+    lhs = new_node(pr, &n);
+  } else if (t.type == T_STRING) {
+    sbAstNode n = { .type = AST_VAL_STRING, .str = t.hstr };
+    lhs = new_node(pr, &n);
+  } else if (t.type == T_IDENTIFIER) {
+    lhs = parse_name(pr);
+  } else if (t.type == T_FATARROW) {
+    next_token(pr);
+    if (!expect(pr, T_LPAREN, NULL)) {
+      fprintf(stderr, "expected '(' after '=>' token\n");
+      return NO_NODE;
+    }
+    sbAst params = parse_comma_exprs(pr, NULL);
+    if (!expect(pr, T_RPAREN, NULL)) {
+      fprintf(stderr, "expected ')' after function parameters\n");
+      return NO_NODE;
+    }
+    sbAst body = parse_block(pr);
+    return seq_node(pr, AST_VAL_FUNC, params, body);
+  } else if (t.type == T_SQUIGARROW) {
+    next_token(pr);
+    if (!expect(pr, T_LPAREN, NULL)) {
+      fprintf(stderr, "expected '(' after '~>' token\n");
+      return NO_NODE;
+    }
+    sbAst params = parse_comma_exprs(pr, NULL);
+    if (!expect(pr, T_RPAREN, NULL)) {
+      fprintf(stderr, "expected ')' after function parameters\n");
+      return NO_NODE;
+    }
+    sbAst body = parse_block(pr);
+    return seq_node(pr, AST_VAL_OBJ, params, body);
   } else if (t.type == T_LPAREN) {
     next_token(pr);
     lhs = parse_expr(pr, 0);
@@ -226,7 +328,7 @@ static sbAst parse_expr(hParser pr, u8 min_precedence) {
     }
 
     if (!prefix) {
-      fprintf(stderr, "expected identifier, '(', or operator!\n");
+      /* thing we're looking at isn't an expression, but this might be ok */
       return NO_NODE;
     }
 
@@ -263,11 +365,10 @@ static sbAst parse_expr(hParser pr, u8 min_precedence) {
     sbAst rhs;
     if (op.type == T_LPAREN) {
       /* function call */
-      rhs = parse_expr(pr, 0);
+      rhs = parse_comma_exprs(pr, NULL);
       ast_type = AST_NODE_FUNCCALL;
       if (!expect(pr, T_RPAREN, NULL)) {
-        /* TODO handle commas: we should have a "parse comma separated expr" thing */
-        fprintf(stderr, "expected ')'\n");
+        fprintf(stderr, "expected ')' after function parameter list\n");
         return NO_NODE;
       }
     } else if (op.type == T_LBRACKET) {
@@ -284,13 +385,11 @@ static sbAst parse_expr(hParser pr, u8 min_precedence) {
         return NO_NODE;
       }
       sbAst name = name_node(pr, method_name);
-      printf("method name: %s\n", sbSymbol_name(method_name.symb));
       if (!expect(pr, T_LPAREN, NULL)) {
         fprintf(stderr, "expected opening-parenthesis after '.%s'\n", sbSymbol_name(method_name.symb));
         return NO_NODE;
       }
-      // TODO handle commas
-      sbAst params = parse_expr(pr, 0);
+      sbAst params = parse_comma_exprs(pr, NULL);
       if (!expect(pr, T_RPAREN, NULL)) {
         fprintf(stderr, "expected closing-parenthesis after '.%s(...'\n", sbSymbol_name(method_name.symb));
         return NO_NODE;
@@ -311,7 +410,178 @@ static sbAst parse_expr(hParser pr, u8 min_precedence) {
   return lhs;
 }
 
+static sbAst parse_stmt(hParser pr);
+static sbAst parse_stmtseq(hParser pr);
+
+static sbAst parse_block(hParser pr) {
+  if (!expect(pr, '{', NULL)) {
+    fprintf(stderr, "syntax error! expecting '{'\n");
+    return NO_NODE;
+  }
+
+  sbAst result = parse_stmtseq(pr);
+
+  if (!expect(pr, '}', NULL)) {
+    fprintf(stderr, "syntax error! expecting '}'\n");
+    return NO_NODE;
+  }
+
+  return result;
+}
+
+static sbAst parse_stmtseq(hParser pr) {
+  sbAst result = NO_NODE;
+  sbAst *put_here = &result;
+
+  do {
+    sbAst stmt = parse_stmt(pr);
+    if (stmt == NO_NODE) {
+      /* if failed to parse a statement, try eating an
+       * additional semicolon first */
+      if (expect(pr, ';', NULL)) continue;
+      break;
+    }
+    *put_here = seq_node(pr, AST_NODE_SEQ, stmt, NO_NODE);
+    put_here = &(*put_here)->seq.right;
+  } while (expect(pr, ';', NULL));
+
+  return result;
+}
+
+static sbAst with_trailing_conditional(hParser pr, sbAst stmt) {
+  sbAst result = stmt;
+
+  if (expect(pr, T_rIF, NULL)) {
+    sbAst condition = parse_expr(pr, 0);
+    result = tri_node(pr, AST_NODE_IF, condition, stmt, NO_NODE);
+  } else if (expect(pr, T_rUNLESS, NULL)) {
+    sbAst condition = parse_expr(pr, 0);
+    result = tri_node(pr, AST_NODE_IF, unop_node(pr, AST_OP_NOT, condition), stmt, NO_NODE);
+  }
+
+  return result;
+}
+
+static sbAst parse_stmt(hParser pr) {
+  sbLexToken t = peek_ahead(pr, 0);
+  if (t.type == T_rIF) {
+    next_token(pr);
+    sbAst condition = parse_expr(pr, 0);
+    sbAst then_body = parse_block(pr);
+    sbAst else_body = NO_NODE;
+    if (expect(pr, T_rELSE, NULL)) {
+      sbLexToken t = peek_ahead(pr, 0);
+      if (t.type == T_rIF) {
+        /* else if ...as above... */
+        else_body = parse_stmt(pr);
+      } else {
+        else_body = parse_block(pr);
+      }
+    }
+    return tri_node(pr, AST_NODE_IF, condition, then_body, else_body);
+  } else if (t.type == T_rUNLESS) {
+    next_token(pr);
+    sbAst condition = parse_expr(pr, 0);
+    /* won't permit unless..else. it is too confusing. unless can only have 1 block */
+    sbAst unless_body = parse_block(pr);
+    return tri_node(pr, AST_NODE_IF, unop_node(pr, AST_OP_NOT, condition), unless_body, NO_NODE);
+  } else if (t.type == T_rWHILE) {
+    next_token(pr);
+    sbAst condition = parse_expr(pr, 0);
+    sbAst body = parse_block(pr);
+    return seq_node(pr, AST_NODE_WHILE, condition, body);
+  } else if (t.type == T_rUNTIL) {
+    next_token(pr);
+    sbAst condition = parse_expr(pr, 0);
+    sbAst body = parse_block(pr);
+    return seq_node(pr, AST_NODE_WHILE, unop_node(pr, AST_OP_NOT, condition), body);
+  } else if (t.type == T_rREPEAT) {
+    next_token(pr);
+    sbAst body = parse_block(pr);
+    if (expect(pr, T_rWHILE, NULL)) {
+      /* repeat..while */
+      sbAst condition = parse_expr(pr, 0);
+      return seq_node(pr, AST_NODE_REPEAT, body, condition);
+    } else if (expect(pr, T_rUNTIL, NULL)) {
+      /* repeat..until */
+      sbAst condition = parse_expr(pr, 0);
+      return seq_node(pr, AST_NODE_REPEAT, body, condition);
+    } else {
+      fprintf(stderr, "syntax error! 'while' or 'until' required after 'repeat { ... }'\n");
+      return NO_NODE;
+    }
+  } else if (t.type == T_rCASE) {
+    next_token(pr);
+    sbAst values = parse_comma_exprs(pr, NULL);
+    sbAst body = parse_block(pr);
+    return seq_node(pr, AST_NODE_CASE, values, body);
+  } else if (t.type == T_rMATCH) {
+    next_token(pr);
+    sbAst pattern = parse_comma_exprs(pr, NULL);
+    sbAst guard_clause = NO_NODE;
+    if (expect(pr, T_rIF, NULL)) {
+      next_token(pr);
+      guard_clause = parse_expr(pr, 0);
+    } else if (expect(pr, T_rUNLESS, NULL)) {
+      next_token(pr);
+      guard_clause = parse_expr(pr, 0);
+      guard_clause = unop_node(pr, AST_OP_NOT, guard_clause);
+    }
+    sbAst body = parse_block(pr);
+    return tri_node(pr, AST_NODE_MATCH, pattern, guard_clause, body);
+  } else if (t.type == T_rLET) {
+    next_token(pr);
+    sbAst bindings = parse_comma_exprs(pr, NULL);
+    if (!expect(pr, T_EQUALS, NULL)) {
+      fprintf(stderr, "expected '=' after let ...\n");
+      return NO_NODE;
+    }
+    sbAst values = parse_comma_exprs(pr, NULL);
+    return seq_node(pr, AST_NODE_LET, bindings, values);
+  } else if (t.type == T_rDEF) {
+    next_token(pr);
+    sbAst name = parse_name(pr);
+    if (!expect(pr, T_LPAREN, NULL)) {
+      fprintf(stderr, "expected '(' after 'def' and name\n");
+      return NO_NODE;
+    }
+    sbAst params = parse_comma_exprs(pr, NULL);
+    if (!expect(pr, T_RPAREN, NULL)) {
+      fprintf(stderr, "expected ')' after function parameters\n");
+      return NO_NODE;
+    }
+    sbAst body = parse_block(pr);
+    sbAst func_node = seq_node(pr, AST_VAL_FUNC, params, body);
+    return seq_node(pr, AST_NODE_DEF, name, func_node);
+  } else if (t.type == T_rRETURN) {
+    next_token(pr);
+    sbAst returned_val = parse_comma_exprs(pr, 0);
+    sbAst return_node = wrap_node(pr, AST_NODE_RETURN, returned_val);
+    return_node = with_trailing_conditional(pr, return_node);
+    return return_node;
+  } else {
+    sbAst expr = parse_expr(pr, 0);
+    if (peek_ahead(pr, 0).type == ',') {
+      next_token(pr);
+      expr = parse_comma_exprs(pr, expr);
+    }
+    if (peek_ahead(pr, 0).type == '=') {
+      next_token(pr);
+      sbAst assigned_values = parse_comma_exprs(pr, 0);
+      expr = seq_node(pr, AST_NODE_ASSIGN, expr, assigned_values);
+    }
+    if (expr != NO_NODE) {
+      expr = with_trailing_conditional(pr, expr);
+      return expr;
+    }
+  }
+
+  return NO_NODE;
+}
+
 static void print_ast_node(sbAst n, int indent) {
+  if (n == NULL) return;
+
   printf("\n");
   for (int i = 0; i < indent; i++) {
     printf("  ");
@@ -319,6 +589,10 @@ static void print_ast_node(sbAst n, int indent) {
 
   if (n->type == AST_NODE_NAME) {
     printf("%s", sbSymbol_name(n->symb));
+  } else if (n->type == AST_NODE_MULTIVAL) {
+    print_ast_node(n->seq.left, indent + 1);
+    printf(",");
+    print_ast_node(n->seq.right, indent + 1);
   } else {
     printf("(");
     if (n->type == AST_NODE_OP) {
index 5cbf389..3847ef9 100644 (file)
@@ -11,7 +11,7 @@ typedef struct sbParser {
 
 typedef sbParser *hParser;
 
-const sbAstNode *sbParser_parse_file(hParser pr, const char *filename);
+sbAstNode *sbParser_parse_file(hParser pr, const char *filename);
 
 void sbParser_initialize(hParser pr);
 
index fb6e50f..f444e10 100644 (file)
@@ -33,6 +33,7 @@ static struct ReservedWord reserved_words[] = {
     { "in", T_rIN },
     { "match", T_rMATCH },
     { "not", T_rNOT },
+    { "nil", T_rNIL },
     { "or", T_rOR },
     { "repeat", T_rREPEAT },
     { "return", T_rRETURN },
index 24e2c42..7fa61ed 100644 (file)
@@ -72,6 +72,7 @@ typedef enum sbTokenType {
     T_rIN,                  // in
     T_rLET,                 // let
     T_rMATCH,               // match
+    T_rNIL,                 // nil
     T_rNOT,                 // not
     T_rOR,                  // or
     T_rREPEAT,              // repeat