list splatting
authorcassowarii <cassowary@cassowary.me>
Fri, 3 Jul 2026 00:23:08 +0000 (17:23 -0700)
committercassowarii <cassowary@cassowary.me>
Fri, 3 Jul 2026 00:23:08 +0000 (17:23 -0700)
src/compile/emit.c
src/compile/ir.c
src/mem/pool.c
src/vm/bytecode.h
src/vm/exec.c

index 071edfd..f6fe64d 100644 (file)
@@ -193,13 +193,44 @@ void compile_stmt(sbVmCompiler *cm, sbIrStmt *stmt) {
 void compile_list(sbVmCompiler *cm, sbIrExpr *expr) {
   sbIrExpr *considering = expr;
   usize count = 0;
+  flag was_splat = FALSE;
   while (considering) {
+    sbIrExpr *elem = considering->list.this;
+    if (elem->type == IR_E_OP && elem->op.type == AST_OP_SPLAT) {
+      if (!was_splat) {
+        /* first splatted thing in list: emit non-splat argument count,
+         * then spill list onto it (LIST_SPILL takes a list and a number,
+         * and unpacks the list onto the stack, then adds the length of
+         * the list to the number) */
+        was_splat = TRUE;
+        EMIT(BC_LD_IMM);
+        EARG(count);
+      }
+      /* compile whatever's behind the splat, and splat it */
+      compile_expr(cm, elem->op.left);
+      EMIT(BC_LIST_SPILL);
+    } else if (was_splat) {
+      /* if was splat (but not this one), we can no longer rely on the
+       * number of commas in the list to say how many arguments (or
+       * whatever) we are passing. so we have to start counting one
+       * by one */
+      compile_expr(cm, elem);
+      /* swap to put the number on top, then increment */
+      EMIT(BC_SWAP, BC_OP_INCR);
+    } else {
+      /* blissfully unaware of the splat */
+      compile_expr(cm, elem);
+    }
     count ++;
-    compile_expr(cm, considering->list.this);
     considering = considering->list.next;
   }
-  EMIT(BC_LD_IMM);
-  EARG(count); /* calling convention: store argument count on stack */
+
+  if (!was_splat) {
+    /* if no splat, just put the total count here. if there was a splat,
+     * we counted them already manually */
+    EMIT(BC_LD_IMM);
+    EARG(count);
+  }
 }
 
 void compile_hash(sbVmCompiler *cm, sbIrExpr *expr) {
index d5fea84..c761109 100644 (file)
@@ -466,7 +466,7 @@ static void put_implicit_return(hIrChunk ck) {
 
 static void compile_ast_stmt(hIrChunk ck, sbAst stmtast, flag implicit_return);
 static void compile_ast_stmtseq(hIrChunk ck, sbAst seqast, flag implicit_return);
-static sbIrExpr *compile_ast_expr(hIrChunk ck, sbAst exprast);
+static sbIrExpr *compile_ast_expr(hIrChunk ck, sbAst exprast, flag list_context);
 static sbIrVariable *compile_ast_var(hIrChunk ck, sbAst node);
 
 static sbIrChunk *compile_ast_function(hIrProgram ir, sbAst paramsAst, sbAst seqast) {
@@ -651,7 +651,7 @@ static void compile_ast_stmt(hIrChunk ck, sbAst node, flag implicit_return) {
         E1 = NULL;
       } else {
         /* TODO handle multival here */
-        E1 = compile_ast_expr(ck, node->seq.left->seq.left);
+        E1 = compile_ast_expr(ck, node->seq.left->seq.left, FALSE);
       }
       put_return(ck, E1);
       break;
@@ -685,7 +685,7 @@ static void compile_ast_stmt(hIrChunk ck, sbAst node, flag implicit_return) {
           const char *vname = sbSymbol_name(N1->seq.left->symb);
           V1 = var_name(ck, vname, strlen(vname));
           V1->initialized = BY_LET;
-          E1 = compile_ast_expr(ck, N2->seq.left);
+          E1 = compile_ast_expr(ck, N2->seq.left, TRUE);
           put_assign(ck, V1, E1);
           N1 = N1->seq.right;
           N2 = N2->seq.right;
@@ -720,7 +720,7 @@ static void compile_ast_stmt(hIrChunk ck, sbAst node, flag implicit_return) {
       N2 = node->seq.right; /* values to assign */
       while (N1 != NO_NODE && N2 != NO_NODE) {
         V1 = compile_ast_var(ck, N1->seq.left);
-        E1 = compile_ast_expr(ck, N2->seq.left);
+        E1 = compile_ast_expr(ck, N2->seq.left, TRUE);
         put_assign(ck, V1, E1);
         N1 = N1->seq.right;
         N2 = N2->seq.right;
@@ -758,7 +758,7 @@ static void compile_ast_stmt(hIrChunk ck, sbAst node, flag implicit_return) {
        * context" struct that captures all of these. */
       L1 = new_label(ck);
       L2 = new_label(ck);
-      E1 = compile_ast_expr(ck, node->seq.left);
+      E1 = compile_ast_expr(ck, node->seq.left, FALSE);
       put_jump_unconditional(ck, L2);
       put_label(ck, L1);
       compile_ast_stmtseq(ck, node->seq.right, FALSE);
@@ -787,14 +787,14 @@ static void compile_ast_stmt(hIrChunk ck, sbAst node, flag implicit_return) {
        */
       if (node->tri.right == NO_NODE) {
         /* no else branch */
-        E1 = compile_ast_expr(ck, node->tri.left);
+        E1 = compile_ast_expr(ck, node->tri.left, FALSE);
         L1 = new_label(ck);
         put_jump_if_no(ck, E1, L1);
         compile_ast_stmtseq(ck, node->tri.center, implicit_return);
         put_label(ck, L1);
       } else {
         /* yes else branch */
-        E1 = compile_ast_expr(ck, node->tri.left);
+        E1 = compile_ast_expr(ck, node->tri.left, FALSE);
         L1 = new_label(ck);
         L2 = new_label(ck);
         put_jump_if_no(ck, E1, L1);
@@ -825,7 +825,7 @@ static void compile_ast_stmt(hIrChunk ck, sbAst node, flag implicit_return) {
        */
       L1 = new_label(ck);
       if (node->seq.right) {
-        E1 = compile_ast_expr(ck, node->seq.right);
+        E1 = compile_ast_expr(ck, node->seq.right, FALSE);
         put_label(ck, L1);
         compile_ast_stmtseq(ck, node->seq.left, FALSE);
         put_jump_if_yes(ck, E1, L1);
@@ -838,7 +838,7 @@ static void compile_ast_stmt(hIrChunk ck, sbAst node, flag implicit_return) {
 
     default:
       /* probably an expr node */
-      put_expr(ck, compile_ast_expr(ck, node));
+      put_expr(ck, compile_ast_expr(ck, node, FALSE));
   }
 }
 
@@ -857,7 +857,7 @@ static sbIrExpr *compile_ast_list(hIrChunk ck, sbAst node) {
   sbIrExpr *list = NULL;
   sbIrExpr **place_here = &list;
   while (considering != NO_NODE) {
-    *place_here = expr_list(ck, compile_ast_expr(ck, considering->seq.left));
+    *place_here = expr_list(ck, compile_ast_expr(ck, considering->seq.left, TRUE));
     place_here = &(*place_here)->list.next;
     considering = considering->seq.right;
   }
@@ -874,8 +874,8 @@ static sbIrExpr *compile_ast_hash(hIrChunk ck, sbAst node) {
     if (hash_entry->type != AST_NODE_HASHENTRY) {
       PANIC("Hash table literals should only contain hashentries (%d)", hash_entry->type);
     }
-    sbIrExpr *compiled_entry = expr_list(ck, compile_ast_expr(ck, hash_entry->seq.left)); // key
-    compiled_entry->list.next = compile_ast_expr(ck, hash_entry->seq.right); // value
+    sbIrExpr *compiled_entry = expr_list(ck, compile_ast_expr(ck, hash_entry->seq.left, FALSE)); // key
+    compiled_entry->list.next = compile_ast_expr(ck, hash_entry->seq.right, FALSE); // value
 
     *place_here = expr_hash(ck, compiled_entry);
     place_here = &(*place_here)->list.next;
@@ -884,11 +884,11 @@ static sbIrExpr *compile_ast_hash(hIrChunk ck, sbAst node) {
   return list;
 }
 
-static sbIrExpr *compile_ast_expr(hIrChunk ck, sbAst node) {
+static sbIrExpr *compile_ast_expr(hIrChunk ck, sbAst node, flag list_context) {
   if (node == NO_NODE) return NULL;
 
   if (node->type == AST_NODE_FUNCCALL) {
-    sbIrExpr *called = compile_ast_expr(ck, node->seq.left);
+    sbIrExpr *called = compile_ast_expr(ck, node->seq.left, FALSE);
     sbIrExpr *params = compile_ast_list(ck, node->seq.right);
     return expr_call(ck, called, params);
   } else if (node->type == AST_VAL_FUNC) {
@@ -901,14 +901,23 @@ static sbIrExpr *compile_ast_expr(hIrChunk ck, sbAst node) {
     sbIrExpr *hash = compile_ast_hash(ck, node->seq.left);
     return hash;
   } else if (node->type == AST_NODE_OP) {
-    sbIrExpr *left = NULL, *right = NULL;
-    if (node->op.left != NO_NODE) {
-      left = compile_ast_expr(ck, node->op.left);
-    }
-    if (node->op.right != NO_NODE) {
-      right = compile_ast_expr(ck, node->op.right);
+    if (node->op.type == AST_OP_SPLAT) {
+      if (!list_context) {
+        chunk_error(ck, "'...' not allowed outside a list");
+        return NULL;
+      } else {
+        return expr_op(ck, AST_OP_SPLAT, compile_ast_expr(ck, node->op.left, FALSE), NULL);
+      }
+    } else {
+      sbIrExpr *left = NULL, *right = NULL;
+      if (node->op.left != NO_NODE) {
+        left = compile_ast_expr(ck, node->op.left, FALSE);
+      }
+      if (node->op.right != NO_NODE) {
+        right = compile_ast_expr(ck, node->op.right, FALSE);
+      }
+      return expr_op(ck, node->op.type, left, right);
     }
-    return expr_op(ck, node->op.type, left, right);
   } else if (node->type == AST_VAL_INT) {
     return expr_value(ck, &HVINT(node->i));
   } else if (node->type == AST_VAL_STRING) {
index 7fe1b37..eab380b 100644 (file)
@@ -78,6 +78,9 @@ void *sbPool_alloc(hPool pl, usize *id_out) {
 void *sbPool_get_entry(hPool pl, usize index) {
   usize block_id = index / pl->block_size;
   usize elem_id = index % pl->block_size;
+  if (!BLOCK_ALLOCATION_FLAGS(pl, block_id)[elem_id]) {
+    PANIC("attempt to get unallocated index from pool");
+  }
   return &BLOCK_ALLOCATED_DATA(pl, block_id)[elem_id * pl->elem_size];
 }
 
index d6ae47b..89cb296 100644 (file)
@@ -24,6 +24,7 @@ typedef enum sbOpcode {
   BC_ST_ARG_IND,        // decrement TOS and store NOS in variable indirectly
   BC_POP,               // pop value from stack
   BC_NPOP,              // pop N values from stack given by top number
+  BC_SWAP,              // swap top two values on stack
   BC_CALL,              // push return address and jump to new block
   BC_RET,               // return to calling block
   BC_NUMARG,            // check number of function arguments = this
@@ -53,6 +54,7 @@ typedef enum sbOpcode {
   BC_CLOSURE,           // create closure from top elements of stack
   BC_LIST_GATHER,       // create list from count + list of values on stack
   BC_HASH_GATHER,       // create hash from count + list of pairs of keys/values on stack
+  BC_LIST_SPILL,        // splat a list into a context with a count
   BC_LONG_NUM   = 0xFC, // next two bytes are a 16-bit number
   BC_VLONG_NUM  = 0xFD, // next four bytes are a 32-bit number
   BC_VVLONG_NUM = 0xFE, // next four bytes are a 32-bit number
index bef5fa2..1c4934a 100644 (file)
@@ -1,6 +1,7 @@
 #include "vm/exec.h"
 
 #include "vm/operations.h"
+#include "data/list.h"
 #include "data/reference.h"
 #include "data/closure.h"
 
@@ -101,10 +102,10 @@ void push_stack(hVm vm, const hV *value) {
   vm->sp += sizeof(hV);
 }
 
-hV *pop_stack(hVm vm) {
+hV pop_stack(hVm vm) {
   vm->sp -= sizeof(hV);
   sbV_release((hV*)vm->sp);
-  return (hV*)vm->sp;
+  return *(hV*)vm->sp;
 }
 
 hV *npop_stack(hVm vm, usize count) {
@@ -177,7 +178,9 @@ void execute_instruction(hVm vm) {
   sbOpcode op = get_opcode(vm);
   if (vm->debugmode) debug("op %02X ", op);
   u64 param;
+  usize count;
   hV *v, *w, *x, res;
+  hV vv, ww, xx;
 
   switch (op) {
     case BC_NOP:
@@ -244,7 +247,8 @@ void execute_instruction(hVm vm) {
       break;
     case BC_ST_UPVAL:
       param = get_param(vm);
-      sbClosure_set_var(vm->fp->closure, param, pop_stack(vm));
+      vv = pop_stack(vm);
+      sbClosure_set_var(vm->fp->closure, param, &vv);
       break;
     case BC_ST_IND:
       param = get_param(vm);
@@ -262,24 +266,24 @@ void execute_instruction(hVm vm) {
       break;
     case BC_ST_ARG:
       param = get_param(vm);
-      v = pop_stack(vm); /* argument count */
-      if (v->type != IT_INTEGER) {
+      vv = pop_stack(vm); /* argument count */
+      if (vv.type != IT_INTEGER) {
         /* internal error! this should be generated correctly */
         CHECK("calling convention violation: number of args should be an integer");
       }
-      w = pop_stack(vm); /* argument value */
-      store_local(vm, param, w);
+      ww = pop_stack(vm); /* argument value */
+      store_local(vm, param, &ww);
       /* we track the number of arguments remaining, for variadic functions later */
-      v->integer --;
-      if (v->integer > 0) {
+      vv.integer --;
+      if (vv.integer > 0) {
         /* if last integer, don't put the 0 count back on the stack */
-        push_stack(vm, v);
+        push_stack(vm, &vv);
       }
       break;
     case BC_ST_ARG_IND:
       param = get_param(vm);
-      x = pop_stack(vm); /* argument count */
-      if (x->type != IT_INTEGER) {
+      xx = pop_stack(vm); /* argument count */
+      if (xx.type != IT_INTEGER) {
         /* internal error! this should be generated correctly */
         CHECK("calling convention violation: number of args should be an integer");
       }
@@ -295,42 +299,48 @@ void execute_instruction(hVm vm) {
         CHECK("indirect variables should be of type reference! (%lld)", v->type);
       }
       pop_stack(vm);
-      x->integer --;
-      if (x->integer > 0) {
+      xx.integer --;
+      if (xx.integer > 0) {
         /* if last integer, don't put the 0 count back on the stack */
-        push_stack(vm, x);
+        push_stack(vm, &xx);
       }
       break;
     case BC_POP:
-      v = pop_stack(vm);
+      pop_stack(vm);
       break;
     case BC_NPOP:
       param = get_param(vm);
       npop_stack(vm, param);
       break;
+    case BC_SWAP:
+      vv = pop_stack(vm);
+      ww = pop_stack(vm);
+      push_stack(vm, &vv);
+      push_stack(vm, &ww);
+      break;
     case BC_CALL:
-      v = pop_stack(vm);
-      if (v->type <= 0) {
+      vv = pop_stack(vm);
+      if (vv.type <= 0) {
         /* We need to figure out exception support or some such.
          * User error should not panic. */
         PANIC("attempt to call a non-function value");
       }
-      call_block(vm, v->type, v->closure);
+      call_block(vm, vv.type, vv.closure);
       break;
     case BC_NUMARG:
       param = get_param(vm);
-      v = pop_stack(vm);
-      if (v->type != IT_INTEGER) {
+      vv = pop_stack(vm);
+      if (vv.type != IT_INTEGER) {
         /* internal error! this should be generated correctly */
         CHECK("calling convention violation: number of args should be an integer");
       }
-      if (v->integer != param) {
+      if (vv.integer != param) {
         /* This should be an exception. */
         PANIC("wrong number of arguments passed to function.");
       }
-      if (v->integer > 0) {
+      if (vv.integer > 0) {
         /* when 0 arguments, leave this off */
-        push_stack(vm, v);
+        push_stack(vm, &vv);
       }
       break;
     case BC_JMP:
@@ -339,15 +349,15 @@ void execute_instruction(hVm vm) {
       break;
     case BC_JT:
       param = get_param(vm);
-      v = pop_stack(vm);
-      if (!sbV_c_falsy(v)) {
+      vv = pop_stack(vm);
+      if (!sbV_c_falsy(&vv)) {
         vm->ip = &vm->fp->block->bytecode[param];
       }
       break;
     case BC_JF:
       param = get_param(vm);
-      v = pop_stack(vm);
-      if (sbV_c_falsy(v)) {
+      vv = pop_stack(vm);
+      if (sbV_c_falsy(&vv)) {
         vm->ip = &vm->fp->block->bytecode[param];
       }
       break;
@@ -364,8 +374,8 @@ void execute_instruction(hVm vm) {
       push_stack(vm, &res);
       break;
     case BC_OP_NOT:
-      v = pop_stack(vm);
-      if (sbV_c_falsy(v)) {
+      vv = pop_stack(vm);
+      if (sbV_c_falsy(&vv)) {
         push_stack(vm, &HVBOOL(TRUE));
       } else {
         push_stack(vm, &HVBOOL(FALSE));
@@ -460,8 +470,8 @@ void execute_instruction(hVm vm) {
     case BC_CLOSURE:
       param = get_param(vm);
       hClosure c = sbClosure_create(param);
-      v = pop_stack(vm); /* function to close with */
-      if (v->type <= 0) {
+      vv = pop_stack(vm); /* function to close with */
+      if (vv.type <= 0) {
         CHECK("internal violation: a function is required to create a closure!");
       }
       while (param > 0) {
@@ -475,41 +485,62 @@ void execute_instruction(hVm vm) {
         sbClosure_set_ref(c, param, w);
         pop_stack(vm);
       }
-      v->closure = c;
-      push_stack(vm, v);
+      vv.closure = c;
+      push_stack(vm, &vv);
       break;
     case BC_LIST_GATHER:
-      v = pop_stack(vm);
-      if (v->type != IT_INTEGER) {
+      vv = pop_stack(vm);
+      if (vv.type != IT_INTEGER) {
         /* internal error! this should be generated correctly */
         CHECK("internal violation: LIST_GATHER should receive an integer on top of stack");
       }
-      param = v->integer;
+      param = vv.integer;
+      count = param;
       res = sbV_empty_list(param);
-      while (param > 0) {
-        w = pop_stack(vm);
+      while (count > 0) {
+        /* list gets built from bottom up, because first thing pushed is lower
+         * on the stack */
+        w = peek_stack(vm, count - 1);
         sbV_append(&res, w);
-        param --;
+        count --;
       }
+      npop_stack(vm, param);
       push_stack(vm, &res);
       break;
     case BC_HASH_GATHER:
-      v = pop_stack(vm);
-      if (v->type != IT_INTEGER) {
+      vv = pop_stack(vm);
+      if (vv.type != IT_INTEGER) {
         /* internal error! this should be generated correctly */
         CHECK("internal violation: HASH_GATHER should receive an integer on top of stack");
       }
-      param = v->integer;
-      res = sbV_empty_hash(param * 3 / 2);
-      while (param > 0) {
+      count = vv.integer;
+      res = sbV_empty_hash(count * 3 / 2);
+      while (count > 0) {
         /* values come first, then keys, because of execution order */
-        x = pop_stack(vm);
-        w = pop_stack(vm);
-        sbV_scope_set(&res, w, x);
-        param --;
+        xx = pop_stack(vm);
+        ww = pop_stack(vm);
+        sbV_scope_set(&res, &ww, &xx);
+        count --;
       }
       push_stack(vm, &res);
       break;
+    case BC_LIST_SPILL:
+      vv = pop_stack(vm); /* list to spill */
+      if (vv.type != IT_LIST) {
+        /* user error */
+        PANIC("cannot use '...' operator on something that isn't a list");
+      }
+      ww = pop_stack(vm); /* current count */
+      if (ww.type != IT_INTEGER) {
+        /* internal error! this should be generated correctly */
+        CHECK("internal violation: LIST_SPILL should receive an integer on top of stack");
+      }
+      x = sbList_get_value(vv.list, &count);
+      for (usize i = 0; i < count; i++) {
+        push_stack(vm, &x[i]);
+      }
+      ww.integer += count;
+      push_stack(vm, &ww);
       break;
     case BC_LONG_NUM:
     case BC_VLONG_NUM: