hash literals!! + read indexing
authorcassowarii <cassowary@cassowary.me>
Wed, 1 Jul 2026 04:56:44 +0000 (21:56 -0700)
committercassowarii <cassowary@cassowary.me>
Wed, 1 Jul 2026 04:56:44 +0000 (21:56 -0700)
13 files changed:
Makefile
src/compile/emit.c
src/compile/ir.c
src/compile/ir.h
src/data/hashtable.c
src/data/symbol.c
src/data/value.c
src/data/value.h
src/parse/parser.c
src/vm/bytecode.h
src/vm/exec.c
src/vm/operations.c
src/vm/operations.h

index 85b4716..a52d086 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -1,5 +1,5 @@
 CC=gcc
-CFLAGS=-Wall -g -DDEBUG -fsanitize=undefined
+CFLAGS=-Wall -g -Og -DDEBUG -fsanitize=undefined
 #CFLAGS=-Wall -O3 -flto
 LIBS=
 
index 46847d9..f208e3a 100644 (file)
@@ -192,6 +192,21 @@ void compile_list(sbVmCompiler *cm, sbIrExpr *expr) {
   EARG(count); /* calling convention: store argument count on stack */
 }
 
+void compile_hash(sbVmCompiler *cm, sbIrExpr *expr) {
+  sbIrExpr *considering = expr;
+  usize count = 0;
+  while (considering) {
+    count ++;
+    /* alternate keys and values on stack. push keys first,
+     * then values */
+    compile_expr(cm, considering->list.this->list.this);
+    compile_expr(cm, considering->list.this->list.next);
+    considering = considering->list.next;
+  }
+  EMIT(BC_LD_IMM);
+  EARG(count); /* calling convention: store argument count on stack */
+}
+
 void compile_op(sbVmCompiler *cm, sbAstOp op);
 void compile_expr(sbVmCompiler *cm, sbIrExpr *expr) {
   switch(expr->type) {
@@ -220,6 +235,10 @@ void compile_expr(sbVmCompiler *cm, sbIrExpr *expr) {
       compile_list(cm, expr);
       EMIT(BC_LIST_GATHER);
       break;
+    case IR_E_HASH:
+      compile_hash(cm, expr);
+      EMIT(BC_HASH_GATHER);
+      break;
     case IR_E_VALUE:
       if (expr->value.type == IT_NIL) {
         EMIT(BC_LD_NIL);
@@ -254,6 +273,7 @@ void compile_op(sbVmCompiler *cm, sbAstOp op) {
     case AST_OP_LE: EMIT(BC_OP_LE); break;
     case AST_OP_GE: EMIT(BC_OP_LT, BC_OP_NOT); break;
     case AST_OP_INDEX: EMIT(BC_OP_INDEX); break;
+    case AST_OP_SCOPE: EMIT(BC_OP_SCOPE); break;
     default:
       debug("unknown operation!\n");
   }
index 413e4f2..c635d44 100644 (file)
@@ -249,6 +249,13 @@ static sbIrExpr *expr_list(hIrChunk ck, sbIrExpr *value) {
   });
 }
 
+static sbIrExpr *expr_hash(hIrChunk ck, sbIrExpr *value) {
+  return new_expr(ck, &(sbIrExpr) {
+    .type = IR_E_HASH,
+    .list.this = value,
+  });
+}
+
 static void put_ir_stmt(hIrChunk ck, sbIrStmt *stmt) {
   sbIrStmt *where_to_put = sbArena_alloc(&ck->program->arena, sizeof(sbIrStmt));
   memcpy(where_to_put, stmt, sizeof(sbIrStmt));
@@ -739,6 +746,26 @@ static sbIrExpr *compile_ast_list(hIrChunk ck, sbAst node) {
   return list;
 }
 
+static sbIrExpr *compile_ast_hash(hIrChunk ck, sbAst node) {
+  sbAst considering = node;
+  sbIrExpr *list = NULL;
+  sbIrExpr **place_here = &list;
+  while (considering != NO_NODE) {
+    /* we'll just make a list of "lists" that are actually just cons-cells of a key and value */
+    sbAst hash_entry = considering->seq.left;
+    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
+
+    *place_here = expr_hash(ck, compiled_entry);
+    place_here = &(*place_here)->list.next;
+    considering = considering->seq.right;
+  }
+  return list;
+}
+
 static sbIrExpr *compile_ast_expr(hIrChunk ck, sbAst node) {
   if (node == NO_NODE) return NULL;
 
@@ -752,6 +779,9 @@ static sbIrExpr *compile_ast_expr(hIrChunk ck, sbAst node) {
   } else if (node->type == AST_VAL_LIST) {
     sbIrExpr *list = compile_ast_list(ck, node->seq.left);
     return list;
+  } else if (node->type == AST_VAL_HASH) {
+    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) {
@@ -765,6 +795,8 @@ static sbIrExpr *compile_ast_expr(hIrChunk ck, sbAst node) {
     return expr_value(ck, &HVINT(node->i));
   } else if (node->type == AST_VAL_STRING) {
     return expr_value(ck, &HVSTR(node->str));
+  } else if (node->type == AST_VAL_SYMBOL) {
+    return expr_value(ck, &HVSYM(node->symb));
   } 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);
index d6f22f7..057948e 100644 (file)
@@ -47,6 +47,7 @@ typedef enum sbIrExprType {
   IR_E_FUNC,
   IR_E_CALL,
   IR_E_LIST,
+  IR_E_HASH,
 } sbIrExprType;
 
 typedef struct sbIrLabel {
index 355e119..0fe91fe 100644 (file)
@@ -8,11 +8,6 @@
 /* TODO eliminate globals */
 sbPool g_hashtable_pool = {0};
 
-typedef struct hashentry {
-  hV key;
-  hV value;
-} hashentry;
-
 typedef struct hashtbl {
   usize size;
   usize used;
@@ -20,20 +15,26 @@ typedef struct hashtbl {
   flag allocated;
   flag is_inline;
   union {
-    /* TODO should be SoA instead of AoS */
-    hashentry inline_entries[INLINE_TABLE_LENGTH];
-    hashentry *external_entries;
+    struct {
+      hV keys[INLINE_TABLE_LENGTH];
+      hV values[INLINE_TABLE_LENGTH];
+    } internal;
+    struct {
+      hV *keys;
+      hV *values;
+    } external;
   };
 } hashtbl;
 
 static hashtbl *new_tbl(usize initial_size);
-static hashentry *get_entry_ptr_for_tbl(hashtbl *t, usize *length_out);
+static hV *get_key_ptr_for_tbl(hashtbl *t, usize *length_out);
+static void get_ptrs_for_tbl(hashtbl *t, hV **keys, hV **values);
 static hashtbl *find_tbl_for_handle(hHash handle);
-static hashentry *set_key(hashtbl *t, hV *key, hV *value);
-static hashentry delete_key(hashtbl *t, hV *key);
+static usize set_key(hashtbl *t, hV *key, hV *value);
+static hV delete_key(hashtbl *t, hV *key);
 static void set_hashtbl_size(hashtbl *t, usize new_size, flag rehash_all);
-static hashentry *set_key_in_array(hashentry *entries, usize length, hV *key, hV *value);
-static hashentry *find_entry_by_key(hashtbl *t, hV *key);
+static usize set_key_in_array(hV *keys, hV *values, usize length, hV *key, hV *value);
+static usize find_index_by_key(hashtbl *t, hV *key);
 
 void sbHash_sys_init() {
   sbPool_initialize(&g_hashtable_pool, sizeof(hashtbl), HASHES_PER_BLOCK);
@@ -59,6 +60,11 @@ sbHashValue sbHash_hash_obj(hV *obj) {
   if (obj->type == IT_NIL) {
     char zero = 0;
     return sbHash_hash_bytes(&zero, 1);
+  } else if (obj->type == IT_SYMBOL) {
+    return sbHash_hash_bytes((char*)&obj->symbol, sizeof(obj->symbol));
+  } else if (obj->type == IT_INTEGER) {
+    /* TODO bigint should be different i think */
+    return sbHash_hash_bytes((char*)&obj->integer, sizeof(obj->integer));
   } else if (obj->type == IT_STRING) {
     char scratch[8];
     usize length;
@@ -81,21 +87,25 @@ void sbHash_insert(hHash h, hV *key, hV *value) {
 
 hV sbHash_find(hHash h, hV *key) {
   hashtbl *t = find_tbl_for_handle(h);
-  hashentry *e = find_entry_by_key(t, key);
-  if (e->key.type == IT_NOTHING) {
-    return (hV) { .type = IT_NOTHING };
+  usize index = find_index_by_key(t, key);
+  hV *keys, *values;
+  get_ptrs_for_tbl(t, &keys, &values);
+  if (keys[index].type == IT_NOTHING) {
+    return HVNOTHING;
   } else {
-    return e->value;
+    return values[index];
   }
 }
 
 hV sbHash_find_or_insert(hHash h, hV *key, hV *value) {
   hashtbl *t = find_tbl_for_handle(h);
-  hashentry *e = find_entry_by_key(t, key);
-  if (e->key.type == IT_NOTHING) {
-    e->value = *value;
+  usize index = find_index_by_key(t, key);
+  hV *keys, *values;
+  get_ptrs_for_tbl(t, &keys, &values);
+  if (keys[index].type == IT_NOTHING) {
+    values[index] = *value;
   }
-  return e->value;
+  return values[index];
 }
 
 void sbHash_delete(hHash h, hV *key, hV *value) {
@@ -112,38 +122,53 @@ static void set_hashtbl_size(hashtbl *t, usize new_size, flag rehash_all) {
   if (new_size == INLINE_TABLE_LENGTH) {
     t->is_inline = 1;
   } else {
-    hashentry *new_data = calloc(new_size, sizeof(hashentry));
+    hV *new_keys = calloc(new_size, sizeof(hV));
+    hV *new_values = calloc(new_size, sizeof(hV));
     if (rehash_all) {
-      hashentry *current_data = get_entry_ptr_for_tbl(t, NULL);
+      hV *current_keys, *current_values;
+      get_ptrs_for_tbl(t, &current_keys, &current_values);
       for (usize i = 0; i < t->size; i++) {
-        if (current_data[i].key.type != IT_NOTHING && current_data[i].key.type != ITX_TOMBSTONE) {
-          set_key_in_array(new_data, new_size, &current_data[i].key, &current_data[i].value);
-          /* adding a new k/v pair will retain the values, so we need to release them
-           * from the previous allocation */
-          sbV_release(&current_data[i].key);
-          sbV_release(&current_data[i].value);
-        }
+        if (current_keys[i].type == IT_NOTHING || current_keys[i].type == ITX_TOMBSTONE) continue;
+        set_key_in_array(new_keys, new_values, new_size, &current_keys[i], &current_values[i]);
+        /* adding a new k/v pair will retain the values, so we need to release them
+         * from the previous allocation */
+        sbV_release(&current_keys[i]);
+        sbV_release(&current_values[i]);
       }
       /* now that we've migrated all our things to the new version, we can free the old
        * version (unless it was not alloc'd to begin with) */
-      if (!t->is_inline) free(current_data);
+      if (!t->is_inline) {
+        free(current_keys);
+        free(current_values);
+      }
     }
-    t->external_entries = new_data;
+    t->external.keys = new_keys;
+    t->external.values = new_values;
     t->is_inline = 0;
   }
   t->size = new_size;
 }
 
-static hashentry *get_entry_ptr_for_tbl(hashtbl *t, usize *length_out) {
+static hV *get_key_ptr_for_tbl(hashtbl *t, usize *length_out) {
   if (length_out) *length_out = t->size;
   if (t->is_inline) {
-    return t->inline_entries;
+    return t->internal.keys;
+  } else {
+    return t->external.keys;
+  }
+}
+
+static void get_ptrs_for_tbl(hashtbl *t, hV **keys, hV **values) {
+  if (t->is_inline) {
+    if (keys) *keys = t->internal.keys;
+    if (values) *values = t->internal.values;
   } else {
-    return t->external_entries;
+    if (keys) *keys = t->external.keys;
+    if (values) *values = t->external.values;
   }
 }
 
-static hashentry *find_entry_in_array(hashentry *entries, usize length, hV *key) {
+static usize find_key_index_in_array(hV *keys, usize length, hV *key) {
   sbHashValue hash = sbHash_hash_obj(key);
   u32 start = (hash & 0xFFFFFFFF);
   u32 move = (hash >> 32);
@@ -151,49 +176,53 @@ static hashentry *find_entry_in_array(hashentry *entries, usize length, hV *key)
     move ++;
   }
   usize index = start % length;
-  while (!sbV_c_eq(&entries[index].key, key) && entries[index].key.type != IT_NOTHING) {
+  while (!sbV_c_eq(&keys[index], key) && keys[index].type != IT_NOTHING) {
     index += move;
     index %= length;
   }
-  return &entries[index];
+  return index;
 }
 
-static hashentry *find_entry_by_key(hashtbl *t, hV *key) {
-  return find_entry_in_array(get_entry_ptr_for_tbl(t, NULL), t->size, key);
+static usize find_index_by_key(hashtbl *t, hV *key) {
+  return find_key_index_in_array(get_key_ptr_for_tbl(t, NULL), t->size, key);
 }
 
-static hashentry *set_key_in_array(hashentry *entries, usize length, hV *key, hV *value) {
-  hashentry *location = find_entry_in_array(entries, length, key);
+static usize set_key_in_array(hV *keys, hV *values, usize length, hV *key, hV *value) {
+  usize index = find_key_index_in_array(keys, length, key);
 
   /* now, we either found the current entry for this key,
    * or we found an empty slot that fits this key. */
-  if (location->key.type == IT_NOTHING) {
+  if (keys[index].type == IT_NOTHING) {
     /* key was not here before, so we need to retain the key
      * as well so it doesn't change out from under us */
     sbV_retain(key);
-    location->key = *key;
+    keys[index] = *key;
   } else {
     /* replacing something that already exists. we can keep
      * the key the same, but need to release the previous value. */
-    sbV_release(&location->value);
+    sbV_release(&values[index]);
   }
 
   sbV_retain(value);
-  location->value = *value;
+  values[index] = *value;
 
-  return location;
+  return index;
 }
 
-static hashentry *set_key(hashtbl *t, hV *key, hV *value) {
-  hashentry *location = find_entry_by_key(t, key);
+static usize set_key(hashtbl *t, hV *key, hV *value) {
+  usize index = find_index_by_key(t, key);
+
+  hV *keys, *values;
+  get_ptrs_for_tbl(t, &keys, &values);
 
   /* now, we either found the current entry for this key,
    * or we found an empty slot that fits this key. */
-  if (location->key.type == IT_NOTHING) {
+  if (keys[index].type == IT_NOTHING) {
     /* key was not here before, so we need to retain the key
      * as well so it doesn't change out from under us */
     sbV_retain(key);
-    location->key = *key;
+    keys[index] = *key;
+    values[index] = *value;
     t->used ++;
     if (t->used >= t->size * 3 / 4) {
       set_hashtbl_size(t, t->size * 2, TRUE);
@@ -201,19 +230,21 @@ static hashentry *set_key(hashtbl *t, hV *key, hV *value) {
   } else {
     /* replacing something that already exists. we can keep
      * the key the same, but need to release the previous value. */
-    sbV_release(&location->value);
+    sbV_release(&values[index]);
   }
 
   sbV_retain(value);
-  location->value = *value;
+  values[index] = *value;
 
-  return location;
+  return index;
 }
 
-static hashentry delete_key(hashtbl *t, hV *key) {
-  hashentry *location = find_entry_by_key(t, key);
-  hashentry to_return = *location;
-  location->key.type = ITX_TOMBSTONE;
+static hV delete_key(hashtbl *t, hV *key) {
+  usize index = find_index_by_key(t, key);
+  hV *keys, *values;
+  get_ptrs_for_tbl(t, &keys, &values);
+  hV to_return = values[index];
+  keys[index].type = ITX_TOMBSTONE;
   return to_return;
 }
 
index f2b5bf2..d4838d7 100644 (file)
@@ -33,7 +33,7 @@ hSymbol sbSymbol_from_bytes(const char *text, usize length) {
     /* return already existing symbol */
     return result.symbol;
   } else {
-    PANIC("Only symbol values are allowed in the symbol table!");
+    PANIC("Only symbol values are allowed in the symbol table! (%lld)", result.type);
   }
 }
 
index 1c99267..3563e1d 100644 (file)
@@ -66,6 +66,13 @@ hV sbV_empty_list(usize capacity) {
   };
 }
 
+hV sbV_empty_hash(usize capacity) {
+  return (hV) {
+    .type = IT_HASH,
+    .hash = sbHash_create(capacity),
+  };
+}
+
 flag sbV_c_eq(const hV *a, const hV *b) {
   if (a->type != b->type) return FALSE;
   if (a->type == ITX_TOMBSTONE || b->type == ITX_TOMBSTONE) return FALSE;
index 1375e7f..9c615e0 100644 (file)
@@ -5,10 +5,12 @@
 
 #define HVINT(n) ((hV) { .type = IT_INTEGER, .integer = n })
 #define HVSTR(s) ((hV) { .type = IT_STRING, .string = s })
+#define HVSYM(s) ((hV) { .type = IT_SYMBOL, .symbol = s })
 #define HVBOOL(b) ((hV) { .type = IT_BOOLEAN, .boolean = b })
 #define HVFUNC(i) ((hV) { .type = IT_FUNCTION, .data = i })
 #define HVLIST(l) ((hV) { .type = IT_LIST, .list = l })
 #define HVNIL ((hV) { .type = IT_NIL })
+#define HVNOTHING ((hV) {0})
 
 typedef u64 hHash;
 typedef u64 hString;
@@ -55,6 +57,7 @@ hV sbV_int(hInteger i);
 hV sbV_boolean(flag b);
 hV sbV_function(u64 id);
 hV sbV_empty_list(usize capacity);
+hV sbV_empty_hash(usize capacity);
 
 flag sbV_c_eq(const hV *a, const hV *b);
 flag sbV_c_falsy(const hV *a);
index cc17a33..0690d1d 100644 (file)
@@ -462,11 +462,6 @@ static sbAst parse_name_as_sym(hParser pr) {
 }
 
 static sbAst parse_hash_key(hParser pr) {
-  sbLexToken t = peek_ahead(pr, 0);
-  if (t.type == T_IDENTIFIER) {
-    return parse_name(pr);
-  }
-
   sbAst literal = parse_literal(pr);
   if (literal != NO_NODE) {
     return literal;
@@ -475,7 +470,7 @@ static sbAst parse_hash_key(hParser pr) {
     if (!expect(pr, ']')) return syntax_error(pr);
     return key;
   } else {
-    return NO_NODE;
+    return parse_name_as_sym(pr);
   }
 }
 
index b830490..769a364 100644 (file)
@@ -43,6 +43,7 @@ typedef enum sbOpcode {
   BC_OP_LT,             // less than
   BC_OP_LE,             // less than or equal to
   BC_OP_INDEX,          // index []
+  BC_OP_SCOPE,          // index ::
   BC_ALLOC_VARS,        // create space in rstack for local variables
   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
index 8d8a450..04d2a63 100644 (file)
@@ -174,7 +174,7 @@ void execute_instruction(hVm vm) {
   sbOpcode op = get_opcode(vm);
   if (vm->debugmode) debug("op %02X ", op);
   u64 param;
-  hV *v, *w, res;
+  hV *v, *w, *x, res;
 
   switch (op) {
     case BC_NOP:
@@ -364,6 +364,13 @@ void execute_instruction(hVm vm) {
       npop_stack(vm, 2);
       push_stack(vm, &res);
       break;
+    case BC_OP_SCOPE:
+      v = peek_stack(vm, 1);
+      w = peek_stack(vm, 0);
+      res = sbV_scope_get(v, w);
+      npop_stack(vm, 2);
+      push_stack(vm, &res);
+      break;
     case BC_OP_DEREF:
       PANIC("todo");
     case BC_ALLOC_VARS:
@@ -392,7 +399,23 @@ void execute_instruction(hVm vm) {
       push_stack(vm, &res);
       break;
     case BC_HASH_GATHER:
-      PANIC("todo");
+      v = pop_stack(vm);
+      if (v->type != IT_INTEGER) {
+        /* internal error! this should be generated correctly */
+        PANIC("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) {
+        /* values come first, then keys, because of execution order */
+        x = pop_stack(vm);
+        w = pop_stack(vm);
+        sbV_scope_set(&res, w, x);
+        param --;
+      }
+      push_stack(vm, &res);
+      break;
+      break;
     case BC_LONG_NUM:
     case BC_VLONG_NUM:
       PANIC("illegal opcode $%02X at position $%016zX", op, (usize)vm->ip);
index b3a8f21..3504db4 100644 (file)
@@ -2,6 +2,7 @@
 
 #include "data/integer.h"
 #include "data/list.h"
+#include "data/hashtable.h"
 
 hV sbV_add(const hV *a, const hV *b) {
   if (a->type == IT_INTEGER && b->type == IT_INTEGER) {
@@ -99,3 +100,19 @@ hV sbV_index(hV *a, hV *b) {
     PANIC("todo %lld %lld", a->type, b->type);
   }
 }
+
+hV sbV_scope_get(hV *obj, hV *key) {
+  if (obj->type == IT_HASH) {
+    return sbHash_find(obj->hash, key);
+  } else {
+    PANIC("todo %lld", obj->type);
+  }
+}
+
+void sbV_scope_set(hV *obj, hV *key, hV *value) {
+  if (obj->type == IT_HASH) {
+    sbHash_insert(obj->hash, key, value);
+  } else {
+    PANIC("todo %lld", obj->type);
+  }
+}
index c4ba868..672fa8b 100644 (file)
@@ -21,3 +21,7 @@ hV sbV_le(const hV *a, const hV *b);
 hV sbV_append(hV *a, hV *b);
 
 hV sbV_index(hV *a, hV *b);
+
+hV sbV_scope_get(hV *hash, hV *key);
+
+void sbV_scope_set(hV *hash, hV *key, hV *value);