summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNathan Ringo <nathan@remexre.com>2024-11-16 12:38:37 -0600
committerNathan Ringo <nathan@remexre.com>2024-11-16 12:38:37 -0600
commit57331ba9756df043b5c665aa4952a0a7b38799e5 (patch)
tree0feb2ca5cbe38744088845b8bb105673016c1fac
Initial commit
-rw-r--r--.envrc1
-rw-r--r--.gitignore5
-rwxr-xr-xclean.sh13
l---------compile_commands.json1
-rw-r--r--flake.lock109
-rw-r--r--flake.nix43
-rwxr-xr-xmk.sh21
-rw-r--r--src/Makefile61
-rw-r--r--src/gc.h18
-rw-r--r--src/gc/gc-debug.c0
-rw-r--r--src/gc/gc.c213
-rw-r--r--src/main.c65
-rw-r--r--src/util.c51
-rw-r--r--src/util.h31
-rw-r--r--src/value.c1
-rw-r--r--src/value.h38
16 files changed, 671 insertions, 0 deletions
diff --git a/.envrc b/.envrc
new file mode 100644
index 0000000..3550a30
--- /dev/null
+++ b/.envrc
@@ -0,0 +1 @@
+use flake
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..15a3c56
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
+.cache/
+.direnv/
+build/
+result
+result-*
diff --git a/clean.sh b/clean.sh
new file mode 100755
index 0000000..caadbc0
--- /dev/null
+++ b/clean.sh
@@ -0,0 +1,13 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+cd "$(git rev-parse --show-toplevel)"
+
+if [ "$(git status --porcelain=v1 --ignored -z | grep -z '^!!' | wc -c)" = 0 ]; then
+ exit
+fi
+
+git status --porcelain=v1 --ignored -z \
+| grep -z '^!!' \
+| xargs -0 awk 'BEGIN { for(i = 1; i < ARGC; i++) printf "%s%c", substr(ARGV[i], 4), 0; }' \
+| xargs -0 rm -r
diff --git a/compile_commands.json b/compile_commands.json
new file mode 120000
index 0000000..25eb4b2
--- /dev/null
+++ b/compile_commands.json
@@ -0,0 +1 @@
+build/compile_commands.json \ No newline at end of file
diff --git a/flake.lock b/flake.lock
new file mode 100644
index 0000000..ad7a19d
--- /dev/null
+++ b/flake.lock
@@ -0,0 +1,109 @@
+{
+ "nodes": {
+ "devkitNix": {
+ "inputs": {
+ "flake-utils": "flake-utils",
+ "nixpkgs": "nixpkgs"
+ },
+ "locked": {
+ "lastModified": 1673397304,
+ "narHash": "sha256-air27biYDH08C++rRNxB0ijaSgcPMIbjcfMyZob+H2g=",
+ "owner": "bandithedoge",
+ "repo": "devkitNix",
+ "rev": "88fc7b2bc1fac5c0d9f833a8b099a94486c542c7",
+ "type": "github"
+ },
+ "original": {
+ "owner": "bandithedoge",
+ "repo": "devkitNix",
+ "type": "github"
+ }
+ },
+ "flake-utils": {
+ "locked": {
+ "lastModified": 1667395993,
+ "narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=",
+ "owner": "numtide",
+ "repo": "flake-utils",
+ "rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f",
+ "type": "github"
+ },
+ "original": {
+ "owner": "numtide",
+ "repo": "flake-utils",
+ "type": "github"
+ }
+ },
+ "flake-utils_2": {
+ "inputs": {
+ "systems": "systems"
+ },
+ "locked": {
+ "lastModified": 1731533236,
+ "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
+ "owner": "numtide",
+ "repo": "flake-utils",
+ "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
+ "type": "github"
+ },
+ "original": {
+ "id": "flake-utils",
+ "type": "indirect"
+ }
+ },
+ "nixpkgs": {
+ "locked": {
+ "lastModified": 1671877799,
+ "narHash": "sha256-jjC0NtPOT4huSwyichdrKHVCjuGr1al7Wu6PcHo5XZs=",
+ "owner": "NixOS",
+ "repo": "nixpkgs",
+ "rev": "8351f271f85dae1ee28269028acde661e60394dd",
+ "type": "github"
+ },
+ "original": {
+ "owner": "NixOS",
+ "ref": "nixpkgs-unstable",
+ "repo": "nixpkgs",
+ "type": "github"
+ }
+ },
+ "nixpkgs_2": {
+ "locked": {
+ "lastModified": 1728500571,
+ "narHash": "sha256-dOymOQ3AfNI4Z337yEwHGohrVQb4yPODCW9MDUyAc4w=",
+ "owner": "NixOS",
+ "repo": "nixpkgs",
+ "rev": "d51c28603def282a24fa034bcb007e2bcb5b5dd0",
+ "type": "github"
+ },
+ "original": {
+ "id": "nixpkgs",
+ "type": "indirect"
+ }
+ },
+ "root": {
+ "inputs": {
+ "devkitNix": "devkitNix",
+ "flake-utils": "flake-utils_2",
+ "nixpkgs": "nixpkgs_2"
+ }
+ },
+ "systems": {
+ "locked": {
+ "lastModified": 1681028828,
+ "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
+ "owner": "nix-systems",
+ "repo": "default",
+ "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
+ "type": "github"
+ },
+ "original": {
+ "owner": "nix-systems",
+ "repo": "default",
+ "type": "github"
+ }
+ }
+ },
+ "root": "root",
+ "version": 7
+}
diff --git a/flake.nix b/flake.nix
new file mode 100644
index 0000000..58b7597
--- /dev/null
+++ b/flake.nix
@@ -0,0 +1,43 @@
+{
+ description = "An interactive programming language sitting somewhere between Common Lisp and Forth.";
+ inputs.devkitNix.url = "github:bandithedoge/devkitNix";
+ outputs =
+ {
+ self,
+ devkitNix,
+ flake-utils,
+ nixpkgs,
+ }:
+ flake-utils.lib.eachDefaultSystem (
+ system:
+ let
+ pkgs = import nixpkgs {
+ inherit system;
+ overlays = [ devkitNix.overlays.default ];
+ };
+ in
+ rec {
+ devShells.default = pkgs.mkShell {
+ inputsFrom = builtins.attrValues packages;
+ nativeBuildInputs = [
+ pkgs.bear
+ pkgs.devkitNix.devkitARM
+ ];
+ inherit (pkgs.devkitNix.devkitARM) shellHook;
+ };
+
+ packages = {
+ default = packages.imb3;
+ imb3 = pkgs.stdenv.mkDerivation {
+ name = "imb3";
+ src = ./src;
+ preBuild = pkgs.devkitNix.devkitARM.shellHook;
+ installPhase = ''
+ cp imb3.3dsx $out
+ '';
+ meta.description = "An interactive programming language sitting somewhere between Common Lisp and Forth.";
+ };
+ };
+ }
+ );
+}
diff --git a/mk.sh b/mk.sh
new file mode 100755
index 0000000..1f749eb
--- /dev/null
+++ b/mk.sh
@@ -0,0 +1,21 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+repo=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)
+srcdir="$repo/src"
+
+if [[ "$(pwd -P)" = "$repo" ]]; then
+ test ! -d build || rm -r build
+ mkdir build
+ cd build
+fi
+
+cat >config.mak <<EOF
+CONFIGURED = 1
+srcdir = $srcdir
+EOF
+ln -s "$srcdir/Makefile"
+
+n="$(nproc)"
+watchexec -w "$srcdir" -- \
+make "-j$n" "-l$n" "$@"
diff --git a/src/Makefile b/src/Makefile
new file mode 100644
index 0000000..432b331
--- /dev/null
+++ b/src/Makefile
@@ -0,0 +1,61 @@
+ifeq ($(strip $(DEVKITPRO)),)
+$(error "Please set DEVKITPRO in your environment. export DEVKITPRO=<path to>devkitPRO")
+endif
+
+SRCS = $(sort $(wildcard $(srcdir)/*.c))
+OBJS = $(patsubst $(srcdir)/%.c,obj/%.o,$(SRCS))
+DEPS = $(patsubst $(srcdir)/%.c,obj/%.d,$(SRCS))
+
+CFLAGS =
+CPPFLAGS =
+LDFLAGS =
+
+CC = $(DEVKITPRO)/devkitARM/bin/arm-none-eabi-gcc
+
+CFLAGS_AUTO = -D__3DS__ -ffunction-sections -flto -g -march=armv6k -mtune=mpcore -mfloat-abi=hard -mtp=soft -mword-relocations -Og -std=c11 -Wall -Werror=implicit-function-declaration
+LDFLAGS_AUTO = -L$(DEVKITPRO)/libctru/lib -specs=3dsx.specs
+LDLIBS_AUTO = -lctru -lm
+
+CFLAGS_ALL = $(CPPFLAGS) $(CFLAGS_AUTO) $(CFLAGS)
+LDFLAGS_ALL = $(LDFLAGS_AUTO) $(LDFLAGS)
+LDLIBS_ALL = $(LDLIBS_AUTO) $(LDLIBS)
+
+-include config.mak
+
+ifeq ($(CONFIGURED),)
+
+all:
+ @printf >&2 'Please run mk.sh, not make.\n'
+ @exit 1
+
+else
+
+all: imb3.3dsx
+3dslink: imb3.3dsx
+ $(DEVKITPRO)/tools/bin/3dslink $<
+.PHONY: 3dslink
+
+imb3.3dsx: imb3.elf
+ 3dsxtool $< $@
+
+imb3.elf: $(OBJS) obj/gc/gc.o
+ $(CC) $(CFLAGS_ALL) $(LDFLAGS_ALL) -o $@ $^ $(LDLIBS_ALL)
+imb3-debug.elf: $(OBJS) obj/gc/gc-debug.o
+ $(CC) $(CFLAGS_ALL) $(LDFLAGS_ALL) -o $@ $^ $(LDLIBS_ALL)
+
+obj/%.o: $(srcdir)/%.c
+ @mkdir -p $(dir $@)
+ $(CC) $(CFLAGS_ALL) -c -o $@ $<
+
+# https://www.gnu.org/software/make/manual/html_node/Automatic-Prerequisites.html
+obj/%.d: $(srcdir)/%.c
+ @mkdir -p obj
+ @set -e; rm -f $@; \
+ $(CC) -M $(CPPFLAGS) $< > $@.$$$$; \
+ sed 's,\($*\)\.o[ :]*,obj/\1.o $@ : ,g' < $@.$$$$ > $@; \
+ rm -f $@.$$$$
+include $(DEPS)
+
+endif
+
+.PHONY: all
diff --git a/src/gc.h b/src/gc.h
new file mode 100644
index 0000000..dff3b79
--- /dev/null
+++ b/src/gc.h
@@ -0,0 +1,18 @@
+#ifndef IMB3_GC_H
+#define IMB3_GC_H
+
+#include "value.h"
+#include <stddef.h>
+
+void gc_init(void);
+void gc_collect(void);
+
+struct object;
+struct value gc_read_value_slot(struct object *, size_t);
+uintptr_t gc_read_untraced_slot(struct object *, size_t);
+uint8_t gc_read_untraced_byte(struct object *, size_t);
+void gc_write_value_slot(struct object *, size_t, struct value);
+void gc_write_untraced_slot(struct object *, size_t, uintptr_t);
+void gc_write_untraced_byte(struct object *, size_t, uint8_t);
+
+#endif // IMB3_GC_H
diff --git a/src/gc/gc-debug.c b/src/gc/gc-debug.c
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/gc/gc-debug.c
diff --git a/src/gc/gc.c b/src/gc/gc.c
new file mode 100644
index 0000000..f53fd79
--- /dev/null
+++ b/src/gc/gc.c
@@ -0,0 +1,213 @@
+#include "../util.h"
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+/**
+ * These cache-size details are from
+ * https://www.copetti.org/writings/consoles/nintendo-3ds/. We call the second
+ * level of cache (only available on N3DS) L3, since this GC design only cares
+ * about L1D and L3.
+ */
+static const size_t L1D_SIZE = 16 * 1024, L3_SIZE = 2 * 1024 * 1024;
+
+/**
+ * Constants that should probably be learned.
+ */
+static const size_t NURSERY_L1D_SIZE_FACTOR = 1;
+static const size_t YOUNG_HEAP_L3_SIZE_FACTOR = 2;
+
+/**
+ * Objects each have a header, with a slightly different layout depending on
+ * which heap the object is in. Each header tracks the layout of the object
+ * itself, as well as some other properties, in a uniform way. via the
+ * `enum object_kind` and `struct object_size` types.
+ */
+
+/**
+ * Gives special properties the object might have, as well as the interpretation
+ * of its size.
+ */
+enum object_kind {
+ /**
+ * A normal object. Has up to 2¹⁶ slots for values, and up to 2¹³ slots for
+ * untraced `uintptr_t`s (which may simply be used as bytes).
+ */
+ OBJECT_NORMAL,
+
+ /**
+ * An untraced object. Has up to 2²⁹ slots for untraced `uintptr_t`s (which
+ * may simply be used as bytes).
+ */
+ OBJECT_UNTRACED,
+
+ /**
+ * A hashtable keyed by the addresses. This behaves like a normal object, but
+ * the first untraced slot gets set to the `uintptr_t` 1 whenever a collection
+ * traces the hashtable.
+ *
+ * The hashtable uses this information to rehash after each collection.
+ */
+ OBJECT_EQ_HASHTABLE,
+
+ /**
+ * A wrapper for a pointer to a code address. When this object gets collected,
+ * calls `free_code` on its first untraced slot.
+ */
+ OBJECT_COMPILED_FUNCTION,
+};
+
+/**
+ * A `size_t`-sized encoding of the object size. Also stores an extra bit used
+ * to indicate that the object has been moved in nursery collection, and that
+ * the object has been marked in old-heap collection.
+ */
+struct object_size {
+ enum object_kind kind : 2;
+ bool mark : 1;
+ size_t bits : (8 * sizeof(size_t)) - 3;
+};
+
+/**
+ * Returns the number of slots for values the object has.
+ */
+static size_t object_value_slots(struct object_size size) {
+ switch (size.kind) {
+ case OBJECT_NORMAL:
+ case OBJECT_EQ_HASHTABLE:
+ case OBJECT_COMPILED_FUNCTION:
+ return size.bits >> ((4 * sizeof(size_t)) - 3);
+ case OBJECT_UNTRACED:
+ return 0;
+ }
+}
+
+/**
+ * Returns the number of untraced slots the object has.
+ */
+static size_t object_untraced_slots(struct object_size size) {
+ switch (size.kind) {
+ case OBJECT_NORMAL:
+ case OBJECT_EQ_HASHTABLE:
+ case OBJECT_COMPILED_FUNCTION:
+ return size.bits & ((1 << ((4 * sizeof(size_t)) - 3)) - 1);
+ case OBJECT_UNTRACED:
+ return size.bits;
+ }
+}
+
+/**
+ * This struct is used as an opaque type for the fields of an object.
+ */
+struct object {
+ uintptr_t hd;
+ uintptr_t tl[];
+};
+
+/**
+ * This is the layout of an object in the nursery.
+ */
+struct nursery_object {
+ struct object_size size;
+ uintptr_t slots[];
+};
+
+/**
+ * This is the layout of an object in the young-heap. Because the young-heap is
+ * collected with mark-compact, it needs a dedicated forwarding pointer (or in
+ * our case, a forwarding offset) and can't simply overwrite the object slots
+ * with one.
+ */
+struct young_heap_object {
+ struct object_size size;
+ size_t fwd;
+ uintptr_t slots[];
+};
+
+/**
+ * This is the layout of an object in the old-heap. It matches the nursery
+ * layout at the moment.
+ */
+struct old_heap_object {
+ struct object_size size;
+ uintptr_t slots[];
+};
+
+/**
+ * The nursery and young-heap each have a layout like:
+ *
+ * ┌──────────────┐ ←── Top
+ * │remembered set│
+ * ├──────────────┤ ←── Limit
+ * │ free │
+ * │ space │
+ * ├──────────────┤ ←── Next
+ * │ │
+ * │ │
+ * │ objects │
+ * │ │
+ * │ │
+ * └──────────────┘ ←── Bottom
+ *
+ * The layout of both regions are defined by the four labelled pointers, so we
+ * define them.
+ */
+uintptr_t nursery_bottom, nursery_next, nursery_limit, nursery_top;
+uintptr_t young_heap_bottom, young_heap_next, young_heap_limit, young_heap_top;
+
+/**
+ * The old-heap is composed of blocks, each of which are stored in an
+ * intrusive doubly linked list per size class. Size classes start at 8 bytes
+ * and go up to 64KiB, with a "large object" size class past that. For size
+ * classes up to and including 4KiB, blocks are 64KiB in size. Past that, blocks
+ * are sized to fit one object of the size class.
+ *
+ * Each block has a local free-list of objects of the size class.
+ */
+enum old_heap_size_class {
+ SIZE_8 = 0,
+ SIZE_16,
+ SIZE_32,
+ SIZE_64,
+ SIZE_128,
+ SIZE_256,
+ SIZE_512,
+ SIZE_1024,
+ SIZE_2048,
+ SIZE_4096,
+ SIZE_8192,
+ SIZE_16384,
+ SIZE_32768,
+ SIZE_65536,
+ SIZE_HUGE,
+
+ SIZE_CLASS_COUNT,
+};
+
+struct old_heap_block {
+ struct old_heap_block *prev, *next;
+ struct old_heap_free_object *free_list;
+};
+
+struct old_heap_free_object {
+ struct old_heap_free_object *next;
+};
+
+static struct old_heap_block old_heap_sentinels[SIZE_CLASS_COUNT];
+
+void gc_init(void) {
+ // Allocate the nursery and young-heap.
+ nursery_bottom = nursery_next =
+ (uintptr_t)malloc(L1D_SIZE * NURSERY_L1D_SIZE_FACTOR);
+ assume(nursery_bottom);
+ young_heap_bottom = young_heap_next =
+ (uintptr_t)malloc(L3_SIZE * YOUNG_HEAP_L3_SIZE_FACTOR);
+ assume(young_heap_bottom);
+
+ // Self-link the old-heap.
+ for (size_t i = 0; i < SIZE_CLASS_COUNT; i++) {
+ old_heap_sentinels[i].prev = old_heap_sentinels[i].next =
+ &old_heap_sentinels[i];
+ }
+}
diff --git a/src/main.c b/src/main.c
new file mode 100644
index 0000000..c8a1115
--- /dev/null
+++ b/src/main.c
@@ -0,0 +1,65 @@
+#include "gc.h"
+#include "util.h"
+#include <3ds.h>
+#include <inttypes.h>
+#include <stdio.h>
+
+int main(int argc, char **argv) {
+ gfxInit(GSP_BGR8_OES, GSP_BGR8_OES, false);
+
+ // Initialize console on top screen. Using NULL as the second argument tells
+ // the console library to use the internal console structure as current one
+ consoleInit(GFX_TOP, NULL);
+
+ // Move the cursor to row 15 and column 19 and then prints "Hello World!"
+ // To move the cursor you have to print "\x1b[r;cH", where r and c are
+ // respectively the row and column where you want your cursor to move The top
+ // screen has 30 rows and 50 columns The bottom screen has 30 rows and 40
+ // columns
+ printf("\x1b[16;20HHello World!");
+
+ printf("\x1b[29;16HPress Start to exit.\n");
+
+ gc_init();
+
+ // Main loop
+ while (aptMainLoop()) {
+ // Scan all the inputs. This should be done once for each frame
+ hidScanInput();
+
+ // hidKeysDown returns information about which buttons have been just
+ // pressed (and they weren't in the previous frame)
+ u32 kDown = hidKeysDown();
+
+ if (kDown & KEY_START)
+ break; // break in order to return to hbmenu
+ if (kDown & KEY_A)
+ todo("GM");
+
+ circlePosition pos;
+ hidCircleRead(&pos);
+ printf("\x1b[0;0H\x1b[K(%" PRId16 ", %" PRId16 ")\n", pos.dx, pos.dy);
+
+ // Get the bottom screen's frame buffer
+ u16 w, h;
+ u8 *fb = gfxGetFramebuffer(GFX_BOTTOM, GFX_LEFT, &w, &h);
+
+ for (u16 j = 0; j < h; j++) {
+ for (u16 i = 0; i < w; i++) {
+ *fb++ = (u8)(((float)i / (float)w) * 255.0);
+ *fb++ = (u8)(((float)j / (float)h) * 255.0);
+ *fb++ = 0x00;
+ }
+ }
+
+ // Flush and swap framebuffers
+ gfxFlushBuffers();
+ gfxSwapBuffers();
+
+ // Wait for VBlank
+ gspWaitForVBlank();
+ }
+
+ gfxExit();
+ return 0;
+}
diff --git a/src/util.c b/src/util.c
new file mode 100644
index 0000000..c219831
--- /dev/null
+++ b/src/util.c
@@ -0,0 +1,51 @@
+#include "util.h"
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+#ifdef __3DS__
+
+#include <3ds.h>
+
+static void panic_begin(void) { consoleInit(GFX_TOP, NULL); }
+
+static noreturn void panic(void) {
+ printf("\nPress Start to exit.\n");
+
+ while (aptMainLoop()) {
+ hidScanInput();
+ u32 keys = hidKeysDown();
+ if (keys & KEY_START)
+ break;
+ gfxFlushBuffers();
+ gfxSwapBuffers();
+ gspWaitForVBlank();
+ }
+
+ gfxExit();
+ exit(1);
+}
+
+#else
+
+static void panic_begin(void) {}
+
+static noreturn void panic(void) { abort(); }
+
+#endif
+
+noreturn void assume__failed(const char *file, int line, const char *expr) {
+ panic_begin();
+ fprintf(stderr, "%s:%d: assertion failed: %s\n", file, line, expr);
+ panic();
+}
+
+noreturn void todo__impl(const char *file, int line, const char *fmt, ...) {
+ panic_begin();
+ printf("%s:%d: TODO: ", file, line);
+ va_list ap;
+ va_start(ap, fmt);
+ vprintf(fmt, ap);
+ va_end(ap);
+ panic();
+}
diff --git a/src/util.h b/src/util.h
new file mode 100644
index 0000000..569054b
--- /dev/null
+++ b/src/util.h
@@ -0,0 +1,31 @@
+#ifndef IMB3_UTIL_H
+#define IMB3_UTIL_H
+
+#include <stdbool.h>
+#include <stdnoreturn.h>
+
+static inline bool likely(bool cond) { return __builtin_expect(cond, true); }
+static inline bool unlikely(bool cond) { return __builtin_expect(cond, false); }
+
+#ifndef NDEBUG
+noreturn void assume__failed(const char *file, int line, const char *expr);
+#endif // NDEBUG
+
+static inline void assume__impl(bool cond, const char *file, int line,
+ const char *expr) {
+#ifndef NDEBUG
+ if (unlikely(!cond))
+ assume__failed(file, line, expr);
+#endif // NDEBUG
+ if (unlikely(!cond))
+ __builtin_unreachable();
+}
+
+#define assume(COND) assume__impl(COND, __FILE__, __LINE__, #COND)
+
+__attribute__((format(printf, 3, 4))) noreturn void
+todo__impl(const char *file, int line, const char *fmt, ...);
+
+#define todo(...) todo__impl(__FILE__, __LINE__, __VA_ARGS__)
+
+#endif // IMB3_UTIL_H
diff --git a/src/value.c b/src/value.c
new file mode 100644
index 0000000..66d388c
--- /dev/null
+++ b/src/value.c
@@ -0,0 +1 @@
+#include "value.h"
diff --git a/src/value.h b/src/value.h
new file mode 100644
index 0000000..a932514
--- /dev/null
+++ b/src/value.h
@@ -0,0 +1,38 @@
+#ifndef IMB3_VALUE_H
+#define IMB3_VALUE_H
+
+#include <stdint.h>
+
+struct value {
+ uintptr_t bits;
+};
+
+enum value_tag {
+ /**
+ * A big integer or the nil constant.
+ */
+ TAG_BIGINT_OR_NIL = 0b000,
+
+ /**
+ * Fixnums, i.e. integers that are stored directly in the value rather than
+ * being pointed to. This is a two-bit tag rather than a three-bit tag...
+ */
+ VALUE_FIXNUM = 0b01,
+
+ /**
+ * A builtin-object.
+ */
+ TAG_BUILTIN_OBJECT = 0b010,
+
+ /**
+ * A simple-array, i.e. an array of values without a fill-pointer.
+ */
+ TAG_SIMPLE_ARRAY = 0b100,
+
+ /**
+ * A standard-object, effectively a pair of a class and a slot array.
+ */
+ TAG_STANDARD_OBJECT = 0b110,
+};
+
+#endif // IMB3_VALUE_H