From 74d5376aa28203bf1a61bc40cdf11b9cf5d686de Mon Sep 17 00:00:00 2001 From: cassowarii Date: Thu, 2 Jul 2026 22:26:36 -0700 Subject: [PATCH] "un-splatting" function params. + start reworking assignment in general --- src/compile/emit.c | 77 +++++++++++++++-- src/compile/ir.c | 224 +++++++++++++++++++++++++++++-------------------- src/compile/ir.h | 23 +++-- src/compile/print_ir.c | 37 ++++++-- src/mem/pool.c | 2 +- 5 files changed, 246 insertions(+), 117 deletions(-) diff --git a/src/compile/emit.c b/src/compile/emit.c index f6fe64d..2c91afc 100644 --- a/src/compile/emit.c +++ b/src/compile/emit.c @@ -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"); } } diff --git a/src/compile/ir.c b/src/compile/ir.c index c761109..70ba765 100644 --- a/src/compile/ir.c +++ b/src/compile/ir.c @@ -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)); diff --git a/src/compile/ir.h b/src/compile/ir.h index f60f251..3ddb128 100644 --- a/src/compile/ir.h +++ b/src/compile/ir.h @@ -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; diff --git a/src/compile/print_ir.c b/src/compile/print_ir.c index bb5cbb8..374eda4 100644 --- a/src/compile/print_ir.c +++ b/src/compile/print_ir.c @@ -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); diff --git a/src/mem/pool.c b/src/mem/pool.c index eab380b..1c7a215 100644 --- a/src/mem/pool.c +++ b/src/mem/pool.c @@ -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]; } -- 1.8.3.1