"un-splatting" function params. + start reworking assignment in general
authorcassowarii <cassowary@cassowary.me>
Fri, 3 Jul 2026 05:26:36 +0000 (22:26 -0700)
committercassowarii <cassowary@cassowary.me>
Fri, 3 Jul 2026 05:26:36 +0000 (22:26 -0700)
src/compile/emit.c
src/compile/ir.c
src/compile/ir.h
src/compile/print_ir.c
src/mem/pool.c

index f6fe64d..2c91afc 100644 (file)
@@ -81,8 +81,9 @@ void compile_chunk(sbVmCompiler *cm, sbIrChunk *chunk) {
     /* don't check args for initial block
      * (note: when we have modules or whatever, this might
      * be an incorrect way to do this) */
-    EMIT(BC_NUMARG);
-    EARG(chunk->num_args);
+    /* TODO re-add checking this */
+    //EMIT(BC_NUMARG);
+    //EARG(chunk->num_args);
   }
 
   if (chunk->variable_count > 0) {
@@ -124,6 +125,8 @@ void record_labelpos(sbVmCompiler *cm, sbIrLabel *label, u32 offset) {
   sbBuffer_append(&cm->label_positions, &lp, sizeof(struct labelpos));
 }
 
+void compile_list(sbVmCompiler *cm, sbIrExpr *expr);
+void compile_bind_list(sbVmCompiler *cm, sbIrBindList *list);
 void compile_stmt(sbVmCompiler *cm, sbIrStmt *stmt) {
   debug("%3zu ", sbVmCompiler_get_position(cm));
   switch (stmt->type) {
@@ -171,13 +174,11 @@ void compile_stmt(sbVmCompiler *cm, sbIrStmt *stmt) {
       }
       EARG(stmt->assign.var->slot_id);
       break;
-    case IR_S_ARG:
-      if (stmt->arg.var->closed_over) {
-        EMIT(BC_ST_ARG_IND);
-      } else {
-        EMIT(BC_ST_ARG);
+    case IR_S_BIND:
+      if (stmt->bind.values) {
+        compile_list(cm, stmt->bind.values);
       }
-      EARG(stmt->arg.var->slot_id);
+      compile_bind_list(cm, stmt->bind.vars);
       break;
     case IR_S_RETURN:
       if (stmt->expr) {
@@ -190,6 +191,14 @@ void compile_stmt(sbVmCompiler *cm, sbIrStmt *stmt) {
   }
 }
 
+/* compile... this isn't actually really for lists only, it's compile a
+ * "multival": the value type of list that is a bunch of things separated
+ * by commas, that, e.g. things can be splatted into. this also handles
+ * function arguments and such. leaves its elements bottom-to-top on the
+ * stack, with the top of the stack being the number of elements. this
+ * can then be passed to a function using BC_CALL, or converted to a list
+ * using BC_LIST_GATHER, or destructured as arguments using BC_ST_ARG and
+ * friends. */
 void compile_list(sbVmCompiler *cm, sbIrExpr *expr) {
   sbIrExpr *considering = expr;
   usize count = 0;
@@ -248,6 +257,56 @@ void compile_hash(sbVmCompiler *cm, sbIrExpr *expr) {
   EARG(count); /* calling convention: store argument count on stack */
 }
 
+/* destructuring assignment, basically. take the output of compile_list,
+ * and assign it to various variables (or (TODO) fail in some mysterious
+ * way (exception?) if there is the wrong amount of stuff) */
+void compile_bind_list(sbVmCompiler *cm, sbIrBindList *list) {
+  for (sbIrBindList *considering = list; considering; considering = considering->next) {
+    sbIrExpr *elem = considering->this;
+    if (elem->type == IR_E_OP && elem->op.type == AST_OP_SPLAT) {
+      /* okay. we want to leave some number of elements on the stack
+       * for whatever other arguments there are. currently we have the
+       * total count on top of the stack. so, we'll reduce the count
+       * by the number we want to save, gather into a list, and assign
+       * to that, then replace the count we wanted to keep on the stack */
+      if (list->pre_splat_count > 0) {
+        EMIT(BC_LD_IMM);
+        EARG(list->pre_splat_count);
+        EMIT(BC_OP_SUB);
+      }
+      /* create list of this length and store in thing */
+      EMIT(BC_LIST_GATHER);
+      /* TODO actually, vv THIS vv should be a recursive call. otherwise,
+       * we don't actually check that it's a variable that the "..." is
+       * attached to, and we may fail in weird cases like "...2". but we
+       * need to restructure the BindList data structure. */
+      if (elem->op.left->var->closed_over) {
+        EMIT(BC_ST_IND);
+      } else {
+        EMIT(BC_ST_VAR);
+      }
+      EARG(elem->op.left->var->slot_id);
+      if (list->pre_splat_count > 0) {
+        /* put pre splat count back if we need it, to bind the rest of
+         * the variables */
+        EMIT(BC_LD_IMM);
+        EARG(list->pre_splat_count);
+      }
+    } else if (elem->type == IR_E_VAR) {
+      /* normal sequence, no splat (so far): top thing goes in this
+       * variable using ST_ARG */
+      if (elem->var->closed_over) {
+        EMIT(BC_ST_ARG_IND);
+      } else {
+        EMIT(BC_ST_ARG);
+      }
+      EARG(elem->var->slot_id);
+    } else {
+      PANIC("bind todo!");
+    }
+  }
+}
+
 void compile_op(sbVmCompiler *cm, sbAstOp op);
 void compile_expr(sbVmCompiler *cm, sbIrExpr *expr) {
   switch(expr->type) {
@@ -356,6 +415,6 @@ void compile_op(sbVmCompiler *cm, sbAstOp op) {
     case AST_OP_INDEX: EMIT(BC_OP_INDEX); break;
     case AST_OP_SCOPE: EMIT(BC_OP_SCOPE); break;
     default:
-      debug("unknown operation!\n");
+      PANIC("unknown operation!\n");
   }
 }
index c761109..70ba765 100644 (file)
@@ -64,13 +64,6 @@ static void vprogram_error(hIrProgram ir, const char *error, va_list args) {
   vfprintf(stderr, error, args);
 }
 
-static void program_error(hIrProgram ir, const char *error, ...) {
-  va_list args;
-  va_start(args, error);
-  vprogram_error(ir, error, args);
-  va_end(args);
-}
-
 static void chunk_error(hIrChunk ck, const char *error, ...) {
   va_list args;
   va_start(args, error);
@@ -124,7 +117,7 @@ static sbIrVariable *create_upvalue(sbIrChunk *ck, sbIrVariable *v, usize slot_i
 
   /* We can close over a variable if it has been assigned a value somewhere in our
    * scope. */
-  new_var->initialized = v->initialized;
+  new_var->introduced = v->introduced;
 
   /* Upvalues actually can be closed over, but when initially created they won't be. */
   new_var->closed_over = FALSE;
@@ -238,6 +231,16 @@ static sbIrLabel *new_label(hIrChunk ck) {
   return l;
 }
 
+static sbIrBindList *new_bind_list(hIrChunk ck, sbIrExpr *value, usize pre_splat_count) {
+  sbIrBindList bl = {
+    .this = value,
+    .pre_splat_count = pre_splat_count,
+  };
+  sbIrBindList *where_to_put = sbArena_alloc(&ck->program->arena, sizeof(sbIrBindList));
+  memcpy(where_to_put, &bl, sizeof(sbIrBindList));
+  return where_to_put;
+}
+
 static sbIrExpr *new_expr(hIrChunk ck, sbIrExpr *expr) {
   sbIrExpr *where_to_put = sbArena_alloc(&ck->program->arena, sizeof(sbIrExpr));
   memcpy(where_to_put, expr, sizeof(sbIrExpr));
@@ -427,19 +430,19 @@ static void put_expr(hIrChunk ck, sbIrExpr *expr) {
   });
 }
 
-static void put_assign(hIrChunk ck, sbIrVariable *assign_to, sbIrExpr *expr) {
+static void put_assign(hIrChunk ck, sbIrVariable *var, sbIrExpr *expr) {
   put_ir_stmt(ck, &(sbIrStmt) {
     .type = IR_S_ASSIGN,
-    .assign.var = assign_to,
+    .assign.var = var,
     .assign.expr = expr,
   });
 }
 
-static void put_argument(hIrChunk ck, sbIrVariable *v, flag last) {
+static void put_bind(hIrChunk ck, sbIrBindList *vars, sbIrExpr *values) {
   put_ir_stmt(ck, &(sbIrStmt) {
-    .type = IR_S_ARG,
-    .arg.last = last,
-    .arg.var = v,
+    .type = IR_S_BIND,
+    .bind.vars = vars,
+    .bind.values = values,
   });
 }
 
@@ -468,6 +471,28 @@ 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, flag list_context);
 static sbIrVariable *compile_ast_var(hIrChunk ck, sbAst node);
+static sbIrBindList *compile_ast_bind_list(hIrChunk ck, sbAst node, flag create_vars, sbIrNameIntroduceType type);
+static sbIrExpr *compile_ast_list(hIrChunk ck, sbAst node);
+
+static sbIrVariable *lookup_node_var(hIrChunk ck, sbAst node) {
+  if (node->type != AST_NODE_NAME) {
+    PANIC("wrong node type passed to lookup_node_var (%d)", node->type);
+  }
+  const char *vname = sbSymbol_name(node->symb);
+  /* TODO I don't love strlen here. Is there something better we can do to
+   * handle symbols in general? */
+  return var_name(ck, vname, strlen(vname));
+}
+
+static sbIrVariable *create_node_var(hIrChunk ck, sbAst node) {
+  if (node->type != AST_NODE_NAME) {
+    PANIC("wrong node type passed to create_node_var (%d)", node->type);
+  }
+  const char *vname = sbSymbol_name(node->symb);
+  /* TODO I don't love strlen here. Is there something better we can do to
+   * handle symbols in general? */
+  return create_var(ck, vname, strlen(vname));
+}
 
 static sbIrChunk *compile_ast_function(hIrProgram ir, sbAst paramsAst, sbAst seqast) {
   sbIrChunk *ck = new_chunk(ir);
@@ -475,29 +500,10 @@ static sbIrChunk *compile_ast_function(hIrProgram ir, sbAst paramsAst, sbAst seq
   /* create new scope to hold function parameters */
   usize parameter_scope_level = ck->program->varmapping.size / sizeof(varmapentry);
 
-  sbBuffer arg_vars;
-  sbBuffer_initialize(&arg_vars, 1024);
-  sbAst param_comma = paramsAst;
-  while (param_comma != NO_NODE) {
-    if (param_comma->seq.left->type != AST_NODE_NAME) {
-      /* TODO: Actually... */
-      program_error(ir, "Only variable names are permitted as function parameters!");
-      break;
-    }
-    const char *vname = sbSymbol_name(param_comma->seq.left->symb);
-    sbIrVariable *v1 = create_var(ck, vname, strlen(vname));
-    sbBuffer_append(&arg_vars, &v1, sizeof(sbIrVariable*));
-    v1->initialized = BY_PARAM;
-    param_comma = param_comma->seq.right;
-    ck->num_args ++;
-  }
-
-  while (arg_vars.size > 0) {
-    /* We need to bind function arguments in reverse order that they are passed,
-     * because arguments are going to be passed on a stack, and we want to evaluate
-     * function arguments left-to-right. */
-    sbIrVariable *var = *(sbIrVariable**)sbBuffer_shrink(&arg_vars, sizeof(sbIrVariable*));
-    put_argument(ck, var, arg_vars.size == 0);
+  /* compile parameters */
+  sbIrBindList *arg_binding = compile_ast_bind_list(ck, paramsAst, TRUE, BY_PARAM);
+  if (arg_binding) {
+    put_bind(ck, arg_binding, NULL);
   }
 
   compile_ast_stmtseq(ck, seqast, TRUE);
@@ -519,12 +525,11 @@ static void compile_ast_stmtseq(hIrChunk ck, sbAst seqast, flag implicit_return)
      * call functions defined later in the program. however, LET won't receive a value / be
      * allowed to be used until the actual line of the declaration */
     if (node->type == AST_NODE_DEF) {
-      const char *vname = sbSymbol_name(node->seq.left->symb);
-      sbIrVariable *V1 = create_var(ck, vname, strlen(vname));
-      /* if something was declared with DEF, it must be initialized, because DEF requires
+      /* if something was declared with DEF, it must be introduced, because DEF requires
        * you to provide a value. so we can close over DEF's that are defined later in the
        * scope. */
-      V1->initialized = BY_DEF;
+      sbIrVariable *V1 = create_node_var(ck, node->seq.left);
+      V1->introduced = BY_DEF;
     }
     considering = considering->seq.right;
   }
@@ -545,8 +550,7 @@ static void compile_ast_stmtseq(hIrChunk ck, sbAst seqast, flag implicit_return)
       }
       if (body->type != AST_NODE_SEQ) PANIC("expected SEQ node as function body!");
 
-      const char *vname = sbSymbol_name(node->seq.left->symb);
-      sbIrVariable *V1 = var_name(ck, vname, strlen(vname));
+      sbIrVariable *V1 = lookup_node_var(ck, node->seq.left);
       sbIrChunk *C1 = compile_ast_function(ck->program, params, body);
       sbIrExpr *E1 = expr_func(ck, C1);
       put_assign(ck, V1, E1);
@@ -554,25 +558,16 @@ static void compile_ast_stmtseq(hIrChunk ck, sbAst seqast, flag implicit_return)
 
     if (node->type == AST_NODE_LET) {
       sbAst N1 = node->seq.left;  /* things to bind to */
-      while (N1 != NO_NODE) {
-        if (N1->seq.left->type != AST_NODE_NAME) {
-          chunk_error(ck, "Only variable names are permitted after 'let'!");
-          break;
-        }
-        const char *vname = sbSymbol_name(N1->seq.left->symb);
-        sbIrVariable *V1 = create_var(ck, vname, strlen(vname));
-        V1->initialized = BY_LET;
-        N1 = N1->seq.right;
-      }
+      compile_ast_bind_list(ck, N1, TRUE, BY_LET);
     }
     considering = considering->seq.right;
   }
 
-  /* now reset the 'initialized' flag of anything that was initialized by LET so
+  /* now reset the 'introduced' flag of anything that was introduced by LET so
    * that we can't refer to them earlier than they are declared */
-  BUFFER_ITER(ck->program->varmapping, varmapentry, entry) {
-    if (entry->var->initialized == BY_LET) {
-      entry->var->initialized = FALSE;
+  BUFFER_ITER_FROM(ck->program->varmapping, varmapentry, entry, ck->lowest_var_id) {
+    if (entry->var->introduced == BY_LET) {
+      entry->var->introduced = NOT_INTRODUCED;
     }
   }
 
@@ -657,45 +652,23 @@ static void compile_ast_stmt(hIrChunk ck, sbAst node, flag implicit_return) {
       break;
 
     case AST_NODE_LET:
-      /*
-       *    LET a = 3
-       * linearizes to:
-       *    LET a
-       *    ...
-       *    a = 3
-       *
-       *    LET a, b = 5, 6
-       * linearizes to:
-       *    LET a, b
-       *    ...
-       *    a = 5
-       *    b = 6
-       *
-       * The LET part actually gets moved to the top of the enclosing block along with
+      /* The LET part actually gets moved to the top of the enclosing block along with
        * any DEFs. So we only care about encountering a LET node in this context if it
        * is an assignment.
-       */
+       *
+       * Then the assignment part basically gets turned into the equivalent of something
+       * like
+       *        ...[a, b, c] = ...[1, 2, 3]
+       *
+       * We destructure some multi-value that we're assigning to onto the stack along
+       * with a count, and then we use the argument-binding instructions to assign
+       * those pieces to variables.  */
       if (node->seq.right != NO_NODE) {
-        /* let ... = ... */
+        if (node->seq.left->type != AST_NODE_MULTIVAL) PANIC("expected multival on left side of let!");
         if (node->seq.right->type != AST_NODE_MULTIVAL) PANIC("expected multival on right side of let!");
-        N1 = node->seq.left;  /* things to bind to */
-        N2 = node->seq.right; /* values to assign */
-        while (N1 != NO_NODE && N2 != NO_NODE) {
-          /* now assign to all the variables we just created */
-          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, TRUE);
-          put_assign(ck, V1, E1);
-          N1 = N1->seq.right;
-          N2 = N2->seq.right;
-        }
-
-        if (N1 != NO_NODE) {
-          chunk_error(ck, "too many bindings on left side of assignment!");
-        } else if (N2 != NO_NODE) {
-          chunk_error(ck, "too many values on right side of assignment!");
-        }
+        sbIrBindList *variables = compile_ast_bind_list(ck, node->seq.left, FALSE, BY_LET); /* things being assigned to */
+        sbIrExpr *assigned = compile_ast_list(ck, node->seq.right); /* values to assign */
+        put_bind(ck, variables, assigned);
       }
       break;
 
@@ -884,6 +857,71 @@ static sbIrExpr *compile_ast_hash(hIrChunk ck, sbAst node) {
   return list;
 }
 
+/* compile one individual element to bind to: might be a bare variable name,
+ * or might be "...name", or might be some other expression, in which case
+ * we bind by matching it exactly? (TODO) */
+static sbIrExpr *compile_ast_binding(hIrChunk ck, sbAst node, flag should_create_var, sbIrNameIntroduceType type) {
+  if (node->type == AST_NODE_NAME) {
+    sbIrVariable *V1;
+    if (should_create_var) {
+      V1 = create_node_var(ck, node);
+    } else {
+      V1 = lookup_node_var(ck, node);
+    }
+    V1->introduced = type;
+    return expr_var(ck, V1);
+  } else if (node->type == AST_NODE_OP) {
+    sbIrExpr *left = NULL, *right = NULL;
+    if (node->op.left != NO_NODE) {
+      left = compile_ast_binding(ck, node->op.left, should_create_var, type);
+    }
+    if (node->op.right != NO_NODE) {
+      right = compile_ast_binding(ck, node->op.right, should_create_var, type);
+    }
+    return expr_op(ck, node->op.type, left, right);
+  } else {
+    /* assume it's a literal value (TODO allow destructuring of list and
+     * hash via this) */
+    return compile_ast_expr(ck, node, FALSE);
+  }
+}
+
+/* compile a list of names to bind to: on the left side of a 'let' expression,
+ * for function parameters, and at the top of a 'match' expression. */
+static sbIrBindList *compile_ast_bind_list(hIrChunk ck, sbAst node, flag create_vars, sbIrNameIntroduceType type) {
+  sbAst considering = node;
+  sbIrBindList *list = NULL;
+  flag was_splat = FALSE;
+  usize pre_splat_count = 0;
+  while (considering != NO_NODE) {
+    /* bind list needs to be reversed, because we're going to bind to some stack
+     * that was built bottom-to-top. so we build it in the reverse order from a
+     * value list. */
+    sbAst elem = considering->seq.left;
+    if (elem->type == AST_NODE_OP && elem->op.type == AST_OP_SPLAT) {
+      if (!was_splat) {
+        was_splat = TRUE;
+      } else {
+        chunk_error(ck, "Multiple '...' assignments are not permitted!\n");
+        return NULL;
+      }
+    } else if (!was_splat) {
+      /* keep track of how many variables existed *before* we ran into a "...a"
+       * type expression, so we know how many to leave on the stack when binding
+       * a "..." */
+      pre_splat_count ++;
+    }
+    sbIrBindList *new_list = new_bind_list(
+        ck, compile_ast_binding(ck, considering->seq.left, create_vars, type), pre_splat_count
+    );
+    new_list->next = list;
+    list = new_list;
+    considering = considering->seq.right;
+  }
+
+  return list;
+}
+
 static sbIrExpr *compile_ast_expr(hIrChunk ck, sbAst node, flag list_context) {
   if (node == NO_NODE) return NULL;
 
@@ -929,7 +967,7 @@ static sbIrExpr *compile_ast_expr(hIrChunk ck, sbAst node, flag list_context) {
   } else if (node->type == AST_NODE_NAME) {
     /* TODO: I don't want to use strlen. Can we remember the lengths of symbols? */
     sbIrVariable *var = compile_ast_var(ck, node);
-    if (var->initialized) {
+    if (var->introduced) {
       return expr_var(ck, var);
     } else {
       chunk_error(ck, "Variable '%s' has not been declared here!\n", sbSymbol_name(node->symb));
index f60f251..3ddb128 100644 (file)
@@ -35,7 +35,7 @@ typedef enum sbIrStmtType {
   IR_S_EXPR,
   IR_S_ASSIGN,
   IR_S_JUMP,
-  IR_S_ARG,
+  IR_S_BIND,
   IR_S_RETURN,
 } sbIrStmtType;
 
@@ -50,6 +50,13 @@ typedef enum sbIrExprType {
   IR_E_HASH,
 } sbIrExprType;
 
+typedef enum sbIrNameIntroduceType {
+  NOT_INTRODUCED,
+  BY_DEF,
+  BY_LET,
+  BY_PARAM,
+} sbIrNameIntroduceType;
+
 typedef struct sbIrLabel {
   flag found_yet;
   struct sbIrLabel *aliased_to;
@@ -59,7 +66,7 @@ typedef struct sbIrLabel {
 
 typedef struct sbIrVariable {
   usize slot_id;
-  flag initialized;
+  sbIrNameIntroduceType introduced;
   flag closed_over;
   flag is_upvalue;
   usize mapping_index;
@@ -101,6 +108,12 @@ typedef struct sbIrAssign {
   sbIrExpr *expr;
 } sbIrAssign;
 
+typedef struct sbIrBindList {
+  usize pre_splat_count;
+  sbIrExpr *this;
+  struct sbIrBindList *next;
+} sbIrBindList;
+
 typedef struct sbIrStmt {
   i32 position;
   sbIrStmtType type;
@@ -110,9 +123,9 @@ typedef struct sbIrStmt {
     sbIrAssign assign;
     sbIrJump jump;
     struct {
-      flag last;
-      sbIrVariable *var;
-    } arg;
+      sbIrBindList *vars;
+      sbIrExpr *values;
+    } bind;
   };
 } sbIrStmt;
 
index bb5cbb8..374eda4 100644 (file)
@@ -38,7 +38,6 @@ static void print_var(sbIrVariable *v) {
 }
 
 static void print_expr(sbIrExpr *e) {
-  debug("(");
   switch(e->type) {
     case IR_E_VAR:
       print_var(e->var);
@@ -64,11 +63,13 @@ static void print_expr(sbIrExpr *e) {
       }
       break;
     case IR_E_OP:
+      debug("(");
       print_expr(e->op.left);
       debug(" %c ", e->op.type);
       if (e->op.right) {
         print_expr(e->op.right);
       }
+      debug(")");
       break;
     case IR_E_CALL:
       debug("CALL: ");
@@ -81,21 +82,39 @@ static void print_expr(sbIrExpr *e) {
         param = param->list.next;
       }
       break;
+    case IR_E_LIST:
+      print_expr(e->list.this);
+      if (e->list.next) {
+        debug(", ");
+        print_expr(e->list.next);
+      }
+      break;
     default:
-      debug("something");
+      debug("something %d", e->type);
   }
-  debug(")");
+}
+
+static void print_bind_list(sbIrBindList *e) {
+  if (e->next) {
+    /* print in reverse order */
+    print_bind_list(e->next);
+    debug(", ");
+  }
+  print_expr(e->this);
 }
 
 static void print_stmt(sbIrStmt *s) {
   switch (s->type) {
-    case IR_S_ARG:
-      debug("  bind function argument to ");
-      print_var(s->arg.var);
-      debug("\n");
-      if (s->arg.last) {
-        debug("  (no function arguments left on the stack now)\n");
+    case IR_S_BIND:
+      debug("  bind [");
+      print_bind_list(s->bind.vars);
+      debug("] to [");
+      if (s->bind.values) {
+        print_expr(s->bind.values);
+      } else {
+        debug(" ~~~ ");
       }
+      debug("]\n");
       break;
     case IR_S_LABEL:
       debug("label %zu:\n", s->label->id);
index eab380b..1c7a215 100644 (file)
@@ -79,7 +79,7 @@ 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");
+    CHECK("attempt to get unallocated index from pool");
   }
   return &BLOCK_ALLOCATED_DATA(pl, block_id)[elem_id * pl->elem_size];
 }