New macro based emit

This commit is contained in:
2026-10-24 15:58:51 +00:00
parent cf455b88a8
commit 95e5c1225e
12 changed files with 1206 additions and 1587 deletions
+458 -1
View File
@@ -1,4 +1,5 @@
// Values: the types that every other part uses, the allocator and errors.
// Values: the types that every other part uses, the allocator, errors, and
// JS's operators on every kind of value.
// An object is a struct of its class, which the emitted C defines, and every
// pointer to one is a real object: undefined and null are values of their own
#pragma once
@@ -10,6 +11,9 @@
typedef struct { char _; } undefined_t;
typedef struct { char _; } null_t;
// A native fn as a value: its index in the compiler's list (see native.rs)
typedef struct { int32_t idx; } native_t;
// A string: UTF-16 code units, as JS counts them
typedef struct jsrt_string *string_t;
@@ -65,3 +69,456 @@ _Noreturn void __rt_type_error(const char *what) {
}
#endif // JSRT_IMPLEMENTATION
// ─────────────────────────────────── Values and operators ────────────────────────────────
//
// Infer gives each value a type. A value whose type has one member is a plain
// C value of that member's C type: undefined_t, null_t, bool, int32_t, double,
// string_t, native_t, or a pointer to an object's struct. A value whose type
// has several members is a jsrt_value: a tag, which says which kind it is, and
// the value. A number in a jsrt_value is always a double
//
// Each macro gives a result of one exact C type, because SET picks the
// conversion by it: a comparison is cast to bool, and a bitwise result to int32_t
//
// The macros here take a value of any of these C types. VALUE(x) makes a
// jsrt_value of any value, and each operator is one fn on jsrt_values. With a
// plain C value, the tag is a constant, so the C compiler removes the tag and
// the switch, and only the code for that kind is left
//
// The emitted C defines two things that the macros use:
// JSRT_CLASS(x) the class of `x`, a pointer to an object
// jsrt_class_is_function[k] whether the objects of class `k` are closures
//
// The array macros use their args more than once, so an arg must be a name
//
// The operators call str.h's and number.h's fns, so this part comes after them
#include <math.h>
#include <string.h>
#include "str.h"
#include "number.h"
enum {
JSRT_UNDEFINED,
JSRT_NULL,
JSRT_BOOL,
JSRT_NUMBER,
JSRT_STRING,
JSRT_NATIVE,
JSRT_OBJECT, // an object of class k has the tag JSRT_OBJECT + k
};
typedef struct {
uint32_t tag;
union {
bool b;
double n;
string_t s;
int32_t native;
void *o;
} as;
} jsrt_value;
extern const bool jsrt_class_is_function[];
#define JSRT_INLINE static inline __attribute__((always_inline))
#define JSRT_NO_CONVERSION "converting an object to a string or a number is not supported"
// ─────────────────────────────── Making and reading values ───────────────────────────────
// Any value, as a jsrt_value
#define VALUE(x) \
_Generic((x), \
undefined_t: jsrt_of_undefined, \
null_t: jsrt_of_null, \
bool: jsrt_of_bool, \
int32_t: jsrt_of_int, \
double: jsrt_of_number, \
string_t: jsrt_of_string, \
native_t: jsrt_of_native, \
jsrt_value: jsrt_of_value, \
default: jsrt_of_object)((x), JSRT_CLASS(x))
// A jsrt_value, as a value of the C type of `like`. Its kind must be one that
// the C type holds: infer makes sure of it
#define FROM_VALUE(like, v) \
_Generic((like), \
undefined_t: jsrt_to_undefined, \
null_t: jsrt_to_null, \
bool: jsrt_to_bool, \
int32_t: jsrt_to_int, \
double: jsrt_to_number, \
string_t: jsrt_to_string, \
native_t: jsrt_to_native, \
jsrt_value: jsrt_to_value, \
default: jsrt_to_object)(v)
// `dst = x`, with `x` converted to the C type of `dst`
#define SET(dst, x) ((dst) = FROM_VALUE(dst, VALUE(x)))
// `x`, converted to the C type `T`
#define CAST(T, x) FROM_VALUE((T){0}, VALUE(x))
JSRT_INLINE jsrt_value jsrt_of_undefined(undefined_t x, uint32_t class) {
(void)x, (void)class;
return (jsrt_value){.tag = JSRT_UNDEFINED};
}
JSRT_INLINE jsrt_value jsrt_of_null(null_t x, uint32_t class) {
(void)x, (void)class;
return (jsrt_value){.tag = JSRT_NULL};
}
JSRT_INLINE jsrt_value jsrt_of_bool(bool x, uint32_t class) {
(void)class;
return (jsrt_value){.tag = JSRT_BOOL, .as.b = x};
}
JSRT_INLINE jsrt_value jsrt_of_int(int32_t x, uint32_t class) {
(void)class;
return (jsrt_value){.tag = JSRT_NUMBER, .as.n = x};
}
JSRT_INLINE jsrt_value jsrt_of_number(double x, uint32_t class) {
(void)class;
return (jsrt_value){.tag = JSRT_NUMBER, .as.n = x};
}
JSRT_INLINE jsrt_value jsrt_of_string(string_t x, uint32_t class) {
(void)class;
return (jsrt_value){.tag = JSRT_STRING, .as.s = x};
}
JSRT_INLINE jsrt_value jsrt_of_native(native_t x, uint32_t class) {
(void)class;
return (jsrt_value){.tag = JSRT_NATIVE, .as.native = x.idx};
}
JSRT_INLINE jsrt_value jsrt_of_value(jsrt_value x, uint32_t class) {
(void)class;
return x;
}
JSRT_INLINE jsrt_value jsrt_of_object(const void *x, uint32_t class) {
return (jsrt_value){.tag = JSRT_OBJECT + class, .as.o = (void *)x};
}
// A read of a value as a kind that it is not is a bug in the compiler. The tag
// is a constant wherever the kind is known, so the check costs nothing there
JSRT_INLINE void jsrt_check(bool ok) {
if (!ok) __rt_type_error("jsrt: a value was read as a kind that it is not");
}
JSRT_INLINE undefined_t jsrt_to_undefined(jsrt_value v) {
jsrt_check(v.tag == JSRT_UNDEFINED);
return (undefined_t){0};
}
JSRT_INLINE null_t jsrt_to_null(jsrt_value v) {
jsrt_check(v.tag == JSRT_NULL);
return (null_t){0};
}
JSRT_INLINE bool jsrt_to_bool(jsrt_value v) {
jsrt_check(v.tag == JSRT_BOOL);
return v.as.b;
}
JSRT_INLINE int32_t jsrt_to_int(jsrt_value v) {
jsrt_check(v.tag == JSRT_NUMBER);
return (int32_t)v.as.n;
}
JSRT_INLINE double jsrt_to_number(jsrt_value v) {
jsrt_check(v.tag == JSRT_NUMBER);
return v.as.n;
}
JSRT_INLINE string_t jsrt_to_string(jsrt_value v) {
jsrt_check(v.tag == JSRT_STRING);
return v.as.s;
}
JSRT_INLINE native_t jsrt_to_native(jsrt_value v) {
jsrt_check(v.tag == JSRT_NATIVE);
return (native_t){v.as.native};
}
JSRT_INLINE jsrt_value jsrt_to_value(jsrt_value v) { return v; }
JSRT_INLINE void *jsrt_to_object(jsrt_value v) {
jsrt_check(v.tag >= JSRT_OBJECT);
return v.as.o;
}
// ────────────────────────────────────── Conversions ──────────────────────────────────────
// The strings that conversions and `typeof` give, made once
enum { JSRT_S_UNDEFINED, JSRT_S_NULL, JSRT_S_TRUE, JSRT_S_FALSE, JSRT_S_BOOLEAN,
JSRT_S_NUMBER, JSRT_S_STRING, JSRT_S_FUNCTION, JSRT_S_OBJECT };
static inline string_t jsrt_name(int name) {
static const char *const texts[] = {"undefined", "null", "true", "false", "boolean",
"number", "string", "function", "object"};
static string_t made[sizeof texts / sizeof *texts];
return made[name] ? made[name] : (made[name] = __rt_string_lit(texts[name]));
}
// JS's ToNumber. An object's conversion would call its methods, which is not supported
JSRT_INLINE double jsrt_number(jsrt_value v) {
switch (v.tag) {
case JSRT_UNDEFINED: return NAN;
case JSRT_NULL: return 0;
case JSRT_BOOL: return v.as.b;
case JSRT_NUMBER: return v.as.n;
case JSRT_STRING: return __rt_string_to_number(v.as.s);
default: __rt_type_error(JSRT_NO_CONVERSION);
}
}
// JS's ToString
JSRT_INLINE string_t jsrt_string(jsrt_value v) {
switch (v.tag) {
case JSRT_UNDEFINED: return jsrt_name(JSRT_S_UNDEFINED);
case JSRT_NULL: return jsrt_name(JSRT_S_NULL);
case JSRT_BOOL: return jsrt_name(v.as.b ? JSRT_S_TRUE : JSRT_S_FALSE);
case JSRT_NUMBER: return __rt_number_to_string(v.as.n);
case JSRT_STRING: return v.as.s;
default: __rt_type_error(JSRT_NO_CONVERSION);
}
}
// JS's ToBoolean
JSRT_INLINE bool jsrt_truthy(jsrt_value v) {
switch (v.tag) {
case JSRT_UNDEFINED: case JSRT_NULL: return false;
case JSRT_BOOL: return v.as.b;
case JSRT_NUMBER: return v.as.n == v.as.n && v.as.n != 0;
case JSRT_STRING: return __rt_string_length(v.as.s) != 0;
default: return true;
}
}
JSRT_INLINE bool jsrt_is_function(jsrt_value v) {
if (v.tag == JSRT_NATIVE) return true;
return v.tag >= JSRT_OBJECT && jsrt_class_is_function[v.tag - JSRT_OBJECT];
}
JSRT_INLINE string_t jsrt_typeof(jsrt_value v) {
switch (v.tag) {
case JSRT_UNDEFINED: return jsrt_name(JSRT_S_UNDEFINED);
case JSRT_BOOL: return jsrt_name(JSRT_S_BOOLEAN);
case JSRT_NUMBER: return jsrt_name(JSRT_S_NUMBER);
case JSRT_STRING: return jsrt_name(JSRT_S_STRING);
default: return jsrt_name(jsrt_is_function(v) ? JSRT_S_FUNCTION : JSRT_S_OBJECT);
}
}
// ─────────────────────────────────────── Operators ───────────────────────────────────────
// `a + b`: a concatenation if either side is a string or an object, else a sum
JSRT_INLINE jsrt_value jsrt_add(jsrt_value a, jsrt_value b) {
if (a.tag >= JSRT_STRING || b.tag >= JSRT_STRING) {
string_t s = __rt_string_concat(jsrt_string(a), jsrt_string(b));
return (jsrt_value){.tag = JSRT_STRING, .as.s = s};
}
return (jsrt_value){.tag = JSRT_NUMBER, .as.n = jsrt_number(a) + jsrt_number(b)};
}
// `a < b`, or `a <= b` if `or_equal`. Two strings compare as strings, anything else
// as numbers
JSRT_INLINE bool jsrt_less(jsrt_value a, jsrt_value b, bool or_equal) {
if (a.tag == JSRT_STRING && b.tag == JSRT_STRING) {
int32_t cmp = __rt_string_cmp(a.as.s, b.as.s);
return or_equal ? cmp <= 0 : cmp < 0;
}
double x = jsrt_number(a), y = jsrt_number(b);
return or_equal ? x <= y : x < y;
}
// `a === b`. Values of different kinds are never equal, and objects of
// different classes are different objects
JSRT_INLINE bool jsrt_strict_eq(jsrt_value a, jsrt_value b) {
if (a.tag != b.tag) return false;
switch (a.tag) {
case JSRT_UNDEFINED: case JSRT_NULL: return true;
case JSRT_BOOL: return a.as.b == b.as.b;
case JSRT_NUMBER: return a.as.n == b.as.n;
case JSRT_STRING: return __rt_string_eq(a.as.s, b.as.s);
case JSRT_NATIVE: return a.as.native == b.as.native;
default: return a.as.o == b.as.o;
}
}
// `a == b`. Undefined and null are equal to each other and to nothing else. Two
// objects compare as `===` does, and so do two values of one kind. Anything
// else compares as numbers
JSRT_INLINE bool jsrt_loose_eq(jsrt_value a, jsrt_value b) {
bool a_nullish = a.tag <= JSRT_NULL, b_nullish = b.tag <= JSRT_NULL;
bool objects = a.tag >= JSRT_NATIVE && b.tag >= JSRT_NATIVE;
if (a_nullish || b_nullish) return a_nullish && b_nullish;
if (objects || a.tag == b.tag) return jsrt_strict_eq(a, b);
return jsrt_number(a) == jsrt_number(b);
}
#define NUMBER(x) _Generic((x), double: (x), default: jsrt_number(VALUE(x)))
#define INT32(x) _Generic((x), int32_t: (x), default: __rt_to_int32(NUMBER(x)))
#define STRING(x) jsrt_string(VALUE(x))
#define TRUTHY(x) jsrt_truthy(VALUE(x))
// Arithmetic. Every number is a double, so every result is a double, except
// `+x` of an int
#define ADD(a, b) jsrt_add(VALUE(a), VALUE(b))
#define SUB(a, b) (NUMBER(a) - NUMBER(b))
#define MUL(a, b) (NUMBER(a) * NUMBER(b))
#define DIV(a, b) (NUMBER(a) / NUMBER(b))
#define MOD(a, b) fmod(NUMBER(a), NUMBER(b))
#define POW(a, b) __rt_pow(NUMBER(a), NUMBER(b))
#define NEG(x) (-NUMBER(x))
#define PLUS(x) _Generic((x), int32_t: (x), default: NUMBER(x))
#define INC(x) (NUMBER(x) + 1)
#define DEC(x) (NUMBER(x) - 1)
// `x++` and `x--`: `old` gets x as a number, and `new` gets the next number
#define POST_INC(old, new, x) \
do { \
double n = NUMBER(x); \
SET(old, n); \
SET(new, n + 1); \
} while (0)
#define POST_DEC(old, new, x) \
do { \
double n = NUMBER(x); \
SET(old, n); \
SET(new, n - 1); \
} while (0)
// Bitwise operators: on int32s, and a shift counts the low 5 bits only. `>>>`
// gives a uint32, which is a double
#define NOT(x) ((int32_t)~INT32(x))
#define AND(a, b) ((int32_t)(INT32(a) & INT32(b)))
#define OR(a, b) ((int32_t)(INT32(a) | INT32(b)))
#define XOR(a, b) ((int32_t)(INT32(a) ^ INT32(b)))
#define SHL(a, b) ((int32_t)((uint32_t)INT32(a) << (INT32(b) & 31)))
#define SAR(a, b) ((int32_t)(INT32(a) >> (INT32(b) & 31)))
#define SHR(a, b) ((double)((uint32_t)INT32(a) >> (INT32(b) & 31)))
// Comparisons. `a > b` is `b < a`, and `a >= b` is `b <= a`
#define LT(a, b) jsrt_less(VALUE(a), VALUE(b), false)
#define LTE(a, b) jsrt_less(VALUE(a), VALUE(b), true)
#define GT(a, b) jsrt_less(VALUE(b), VALUE(a), false)
#define GTE(a, b) jsrt_less(VALUE(b), VALUE(a), true)
#define STRICT_EQ(a, b) jsrt_strict_eq(VALUE(a), VALUE(b))
#define STRICT_NEQ(a, b) ((bool)!STRICT_EQ(a, b))
#define EQ(a, b) jsrt_loose_eq(VALUE(a), VALUE(b))
#define NEQ(a, b) ((bool)!EQ(a, b))
// Tests of one value
#define LNOT(x) ((bool)!TRUTHY(x))
#define TYPEOF(x) jsrt_typeof(VALUE(x))
#define IS_UNDEFINED(x) ((bool)(VALUE(x).tag == JSRT_UNDEFINED))
#define IS_NULL(x) ((bool)(VALUE(x).tag == JSRT_NULL))
#define IS_NULLISH(x) ((bool)(VALUE(x).tag <= JSRT_NULL))
#define IS_FUNCTION(x) jsrt_is_function(VALUE(x))
// ──────────────────────────────────── Natives' args ──────────────────────────────────────
// An arg of a native, as its param's C type. An int and a float both pass for a
// number, and an arg of any other wrong kind throws
#define ARG_INT(x) \
_Generic((x), int32_t: (x), default: __rt_to_int32(jsrt_arg_number(VALUE(x))))
#define ARG_NUMBER(x) jsrt_arg_number(VALUE(x))
#define ARG_STRING(x) jsrt_arg_string(VALUE(x))
#define ARG_UNDEFINED(x) jsrt_arg_undefined(VALUE(x))
#define JSRT_WRONG_ARG "a native got an arg of the wrong kind"
JSRT_INLINE double jsrt_arg_number(jsrt_value v) {
if (v.tag != JSRT_NUMBER) __rt_type_error(JSRT_WRONG_ARG);
return v.as.n;
}
JSRT_INLINE string_t jsrt_arg_string(jsrt_value v) {
if (v.tag != JSRT_STRING) __rt_type_error(JSRT_WRONG_ARG);
return v.as.s;
}
JSRT_INLINE undefined_t jsrt_arg_undefined(jsrt_value v) {
if (v.tag != JSRT_UNDEFINED) __rt_type_error(JSRT_WRONG_ARG);
return (undefined_t){0};
}
// ──────────────────────────────────── Arrays and cells ───────────────────────────────────
//
// Every array's struct has `length`, `cap` and `items`, so these work on any of them
// Whether `i`, a number, is an index below `length`
#define JSRT_IN_RANGE(i, length) ((i) >= 0 && (i) < (length) && (i) == (double)(int32_t)(i))
#define JSRT_BAD_INDEX "an array index must be a whole number from 0"
#define JSRT_BAD_LENGTH "an array's length must be a whole number from 0"
// `a = [...]` with room for `n` elements, which the caller sets
#define ARRAY_NEW(a, n) \
do { \
(a) = __rt_alloc(sizeof *(a)); \
(a)->items = __rt_grow(NULL, &(a)->cap, (n), sizeof *(a)->items); \
(a)->length = (n); \
} while (0)
// `dst = a[i]`: undefined if `i` is not an index in the array
#define ARRAY_GET(dst, a, i) \
do { \
double index = NUMBER(i); \
if (JSRT_IN_RANGE(index, (a)->length)) SET(dst, (a)->items[(int32_t)index]); \
else SET(dst, (undefined_t){0}); \
} while (0)
// `dst = s[i]`: undefined if `i` is not an index in the string
#define STRING_GET(dst, s, i) \
do { \
double index = NUMBER(i); \
if (!JSRT_IN_RANGE(index, __rt_string_length(s))) SET(dst, (undefined_t){0}); \
else SET(dst, __rt_string_at((s), (int32_t)index)); \
} while (0)
// `a[i] = v`. The array grows to hold `i`, and the elements between are holes.
// An `i` that is not an index would be a named field, which is not supported
#define ARRAY_PUT(a, i, v) \
do { \
double index = NUMBER(i); \
if (!JSRT_IN_RANGE(index, INT32_MAX)) __rt_type_error(JSRT_BAD_INDEX); \
int32_t at = (int32_t)index; \
if (at >= (a)->length) { \
(a)->items = __rt_grow((a)->items, &(a)->cap, at + 1, sizeof *(a)->items); \
(a)->length = at + 1; \
} \
SET((a)->items[at], v); \
} while (0)
// `a.length = n`: the array gets `n` elements, and the new ones are holes. `n`
// must be a whole number from 0, as JS's RangeError says
#define ARRAY_SET_LENGTH(a, n) \
do { \
double length = NUMBER(n); \
if (!JSRT_IN_RANGE(length, INT32_MAX)) __rt_type_error(JSRT_BAD_LENGTH); \
(a)->items = __rt_grow((a)->items, &(a)->cap, (int32_t)length, sizeof *(a)->items); \
(a)->length = (int32_t)length; \
} while (0)
// A new cell for `p`, a pointer to a cell, with the same value. Closures that
// point to the old cell keep it (see transform/closure.rs)
#define NEW_CELL(p) \
do { \
void *cell = __rt_alloc(sizeof *(p)); \
memcpy(cell, (p), sizeof *(p)); \
(p) = cell; \
} while (0)
+2 -1
View File
@@ -10,7 +10,8 @@
// the allocator and errors
// __native_* the platform's IO, which another platform's runtime replaces
//
// core.h values, the allocator, errors
// core.h values, the allocator, errors, and JS's operators on every
// kind of value
// str.h strings
// number.h numbers, and their conversions to and from strings
// io.h IO natives, async on io_uring (uring.h), and the host's loop