runtime.h (LLO Archive)

Created 2025-11-30, last modified 2025-11-30

Part of my archive of Layover Linux Official posts on Tumblr.


2025-09-24

My brain is too fried to post well about this, but I finally got an optimized interpreter-only header working for Prone. There's a bit that needs polishing, but it's finally a good overview of the code that you can read straight through top to bottom. If Tumblr had good code snippet support, I could paste it here (a mere 345 lines).

https://git.sr.ht/~maddiem4/prone/tree/8ecad20079f4002997377a214d0ebff715c7f9b9/item/src/runtime.h


Editor's note: now that I'm copying these posts off of Tumblr, I do have good snippet support! How convenient for you, and honestly, for me too. I'm migrating posts in the middle of some messy runtime optimization work, so it's charming to see this smaller and tidier version of the main header. Over the course of this project, I've pulled the code in one direction and then the opposite, yet somehow always forward. I'm looking forward to the header being well-organized and human-friendly again.

#pragma once
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <string.h>

// Everything you need to use the Prone runtime, nothing you don't.

// ============================================================================
//                                   TYPES
// ============================================================================

typedef uint16_t TC;          // Typecode
typedef uint16_t RC;          // Reference Count
typedef uint64_t DV;          // Dynamic Value
typedef uint32_t InlineValue; // Data stored inside an inline DV
typedef uint32_t AtomID;      // Numeric representation of an #ATOM

#define PRN_STRUCT(name)                                                       \
  typedef struct s_##name name;                                                \
  struct s_##name
#define VEC_INNARDS(t)                                                         \
  size_t len, cap;                                                             \
  t *buf
#define VEC_OF(t)                                                              \
  struct {                                                                     \
    VEC_INNARDS(t);                                                            \
  }

// Vec<any>, see Vector section for how to use less abstractly.
PRN_STRUCT(Vec) { VEC_INNARDS(void); };

PRN_STRUCT(String) {
  RC rc;
  VEC_OF(char) vec;
};

PRN_STRUCT(VVec) {
  RC rc;
  VEC_OF(DV) vec;
};

typedef DV (*NativeFn)(VVec *arguments);
PRN_STRUCT(Native) {
  RC rc;
  NativeFn fn;
  char *name;
};

PRN_STRUCT(Eval) {
  VVec *scope;
  DV value;
};

// Stuff below this line will eventually be moved to Private.
// ----------------------------------------------------------

// Metadata used and provided by the tracking allocator mode.
// Need alloc_count() to de-expose for most use cases.
PRN_STRUCT(Allocation) {
  void *start;
  size_t len;
};
PRN_STRUCT(AllocMetadata) { VEC_INNARDS(Allocation); };

// Internal storage for atoms.
PRN_STRUCT(AtomsMeta) {
  VEC_OF(char) chars;
  VEC_OF(size_t) offsets;
};

// Need parse_x() functions to de-expose.
PRN_STRUCT(Mark) {
  size_t pos;
  bool is_start;
  uint8_t token;
};
PRN_STRUCT(ParseData) {
  String *source;
  VEC_OF(Mark) marks;
};
PRN_STRUCT(LexCursor) {
  size_t src_pos;
  size_t n_marks;
  bool is_fail;
};
typedef LexCursor (*LexFn)(ParseData *pd, LexCursor c);

#undef PRN_STRUCT
#undef VEC_INNARDS
#undef VEC_OF

// ============================================================================
//                                 TYPECODES
// ============================================================================
// These are 16-bit numbers that identify a natively supported type in Prone.
// More detail about this in the Dynamic Value section, but the short version:
//  * For tagged pointer DVs, the typecode is the last 4 bits.
//  * For inline value DVs, the typecode is the last 16 bits (last 4 are zero).

// Inline values
#define DV_TC_TERM (0 << 4)
#define DV_TC_ATOM (1 << 4)
#define DV_TC_U8   (2 << 4)
#define DV_TC_U16  (3 << 4)
#define DV_TC_U32  (4 << 4)
#define DV_TC_I8   (5 << 4)
#define DV_TC_I16  (6 << 4)
#define DV_TC_I32  (7 << 4)
#define DV_TC_F32  (8 << 4)

// Tagged pointers
#define DV_TC_U64       (0b0001)
#define DV_TC_I64       (0b0010)
#define DV_TC_F64       (0b0011)
#define DV_TC_STRING    (0b0100)
#define DV_TC_LIST      (0b0101)
#define DV_TC_CONSTRUCT (0b0110)
#define DV_TC_NATIVE    (0b0111)

// ============================================================================
//                              DYNAMIC VALUES
// ============================================================================
// DV is a tagged pointer type. Since malloc'd pointers are 16-bit aligned, we
// know that the lowest four bits of every "legit" pointer should be 0. This
// lets us store a very small amount of metadata in those four bits, and also,
// store lots of non-pointer values inline.
//
// The overall structure is as follows:
//
//                                                   [     typecode  ]
// vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv mmmmmmmmmmmmmmmm ssssssssssss tttt
//  32 bits of value data            16 bits meta        12b sub   tag
//
// "Inline" values are stored with TAG=0. We have plenty of other bits to
// clarify the actual type in this case. For pointer values, we only have 4
// bits to work with, so we need to treat them as precious.

TC dv_typecode(DV dv);
DV dv_inline(InlineValue v, TC tc);
DV dv_ptr(void *ptr, TC tc);
void *dv_to_ptr(DV dv);
char const *dv_typestring(DV dv);

// ============================================================================
//                            REFERENCE COUNTING
// ============================================================================
// Usage guide:
//  * When passing arguments, each must be lent or moved.
//    * Do you use it again later in the function? Lend it.
//    * Otherwise, move it.
//  * By the end of a function, release or move EVERY value that was in scope.
//  * Moving is implicit:
//    * sometype_move(value) is a no-op, but acts like a visual marker.
//    * You don't need to mark temporary values you're constructing INSIDE the
//      parameters of a function call. It's better to omit the noise here.
//  * If you need to mutate a value, "own" it first.
//    * You can leave this out if you created the value in the same function.
//    * This automatically clones the value if anyone else has a copy.
//
// x_clone(v)   -> Make a shallow copy of v.
// x_lend(v)    -> Increase refcount before passing as an argument.
// x_move(v)    -> Pass as argument without increasing refcount.
// x_own(v)     -> Clone if I don't have an exclusive copy of v.
// x_release(v) -> Decrease the refcount of v, return whether this freed v.
// x_free(v)    -> Free v no matter the refcount.

#define RC_DEFS(T, t)                                                          \
  T t##_clone(T x);                                                            \
  T t##_lend(T x);                                                             \
  T t##_move(T x);                                                             \
  T t##_own(T x);                                                              \
  bool t##_release(T x);                                                       \
  void t##_free(T x);

RC_DEFS(DV, dv)
RC_DEFS(String *, string)
RC_DEFS(VVec *, vvec)
RC_DEFS(Native *, native)

#undef RC_DEFS

// ============================================================================
//                                 ATOMS
// ============================================================================
// Interned text snippets that can be compared at high speed.
// Cannot contain null bytes in the text.

#define ATOM_NOT_FOUND    ((AtomID)(0) - 1)
#define ATOM_STORE_FAILED ((AtomID)(0) - 2)

const AtomsMeta *atoms_metadata();
AtomID atoms_lookup(const char *str);
AtomID atoms_store(const char *str);
const char *atoms_text(AtomID id);
void atoms_reset();

DV dv_atom_from_id(AtomID id);
DV dv_atom(const char *str);
AtomID dv_to_atom(DV dv);

// ============================================================================
//                                 NUMERICS
// ============================================================================

// Convert to DV
DV dv_u8(uint8_t v);
DV dv_u16(uint16_t v);
DV dv_u32(uint32_t v);

// Convert from DV
uint8_t dv_to_u8(DV dv);
uint16_t dv_to_u16(DV dv);
uint32_t dv_to_u32(DV dv);

// ============================================================================
//                                 STRINGS
// ============================================================================
// The String type is primarily targeted at storing UTF-8 encoded strings which
// may validly contain zero (NULL) bytes. However, in certain situations, it's
// sure nice to be able to convert quickly to a C string.
//
// For this reason, .buf is allocated as length .len+1, where the final byte is
// a null terminator for insurance. This is handy, but obviously if the string
// value contains nulls you're on your own to deal with that mess.

String *string_alloc(size_t len);
String *string_create(size_t len, const char *src);

// Convenient helpers (not NULL friendly)
#define STR(s)    (string_create(strlen(s), s))
#define DV_STR(s) dv_string(STR(s))

DV dv_string(String *s);
String *dv_to_string(DV dv);

// ============================================================================
//                              VALUE VECTORS
// ============================================================================
// Sequences of DVs. Lots of things are VVecs under the hood.

VVec *vvec_alloc(size_t cap);
VVec *vvec_from_elements(DV *elements);
#define VVEC(...)         vvec_from_elements((DV[]){__VA_ARGS__ __VA_OPT__(, ) 0})
#define DV_LIST(...)      dv_list(VVEC(__VA_ARGS__))
#define DV_CONSTRUCT(...) dv_construct(VVEC(__VA_ARGS__))

// Worth noting, this does not check boundaries or modify length.
#define VVEC_AT(v, i) (v->vec.buf[i])

// TODO: Delete in favor of vvec_push
void vvec_append(VVec *v, DV value);

DV dv_list(VVec *v);
DV dv_construct(VVec *v);
VVec *dv_to_vvec(DV dv);

// Helpers for common constructs
DV dv_stmt_expr(DV expr);
DV dv_stmt_assign(DV left, DV right);
DV dv_literal_num(String *str);
DV dv_identifier(String *str);
DV dv_fn_call(String *ident, VVec *args);

// ============================================================================
//                            NATIVE C FUNCTIONS
// ============================================================================
Native *native_create(NativeFn fn, char const *name);
DV dv_native(Native *n);
Native *dv_to_native(DV dv);

// ============================================================================
//                             TOOLKIT (TYPED)
// ============================================================================
// A variety of functions on a variety of types, usually the foundation of the
// untyped function library below.
String *atom_repr(AtomID atom);

String *construct_repr(VVec *construct);

String *list_repr(VVec *list);

String *native_repr(Native *n);

#define STRING_CAT(...) string_cat((String *[]){__VA_ARGS__, NULL})
String *string_cat(String **strs);
int string_cmp(String *first, String *second);
String *string_repr(String *value);

String *u8_repr(uint8_t value);

DV vvec_pop(VVec *v);
VVec *vvec_push(VVec *v, DV value);
DV vvec_table_get(VVec *table, DV key);
VVec *vvec_table_set(VVec *table, DV key, DV value);
VVec *vvec_table_del(VVec *table, DV key);
bool vvec_deep_equals(VVec *a, VVec *b);

bool dv_is_equal(DV a, DV b);
String *dv_repr(DV value);

// ============================================================================
//                             STDLIB (UNTYPED)
// ============================================================================
VVec *generate_stdlib();

DV conv_construct(VVec *arguments);
DV conv_fn(VVec *arguments);
DV conv_list(VVec *arguments);
DV conv_repr(VVec *arguments);
DV conv_u8(VVec *arguments);
DV list_pop(VVec *arguments);
DV list_push(VVec *arguments);
// DV list_reserve(VVec *arguments);
DV literal_num(VVec *arguments);
DV op_is_equal(VVec *arguments);
DV op_plus(VVec *arguments);

// ============================================================================
//                             PARSE AND EVAL
// ============================================================================
DV parse_statement(String *src);
Eval evaluate(VVec *scope, DV input);

// ============================================================================
//                            ALLOCATOR CONFIG
// ============================================================================
// These bit flags are not mutually exclusive. In fact, DBG and PARANOID
// both depend on turning TRACKED on.
//
//  TRACKED: Managed allocs are recorded. Freeing unmanaged memory is a no-op.
//  DBG: Print memory analysis data on every alloc and free.
//  PARANOID: Zeros out memory on free to catch use-after-free.
//
// The default mode, being untracked, passes through to underlying malloc/free.

#define PRN_ALLOC_TRACKED  1
#define PRN_ALLOC_DBG      2
#define PRN_ALLOC_PARANOID 4
void prn_alloc_set_mode(uint8_t mode);
uint8_t prn_alloc_get_mode();

#define alloc_reset() prn_alloc_reset()
void prn_alloc_reset();   // Clear all existing allocations
size_t prn_alloc_count(); // How many objects are allocated right now?