Skip to content

Instantly share code, notes, and snippets.

@sogaiu
Last active September 20, 2024 10:59
Show Gist options
  • Save sogaiu/2fd9e47089716b38d9e04e964d2fb61f to your computer and use it in GitHub Desktop.
Save sogaiu/2fd9e47089716b38d9e04e964d2fb61f to your computer and use it in GitHub Desktop.
janetslot and registers

JanetSlot and Registers

From compile (janet code) to janetc_emit (bytecode)

Janet's compile calls janet_compile_lint:

/* C Function for compiling */
JANET_CORE_FN(cfun_compile,
              "(compile ast &opt env source lints)",
              "Compiles an Abstract Syntax Tree (ast) into a function. "
              "Pair the compile function with parsing functionality to implement "
              "eval. Returns a new function and does not modify ast. Returns an error "
              "struct with keys :line, :column, and :error if compilation fails. "
              "If a `lints` array is given, linting messages will be appended to the array. "
              "Each message will be a tuple of the form `(level line col message)`.") {
    // ...
    JanetCompileResult res = janet_compile_lint(argv[0], env, source, lints);
    // ...

janet_compile_lint calls janetc_value:

/* Compile a form. */
JanetCompileResult janet_compile_lint(Janet source,
                                      JanetTable *env, const uint8_t *where, JanetArray *lints) {
    JanetCompiler c;
    JanetScope rootscope;
    JanetFopts fopts;

    janetc_init(&c, env, where, lints);

    /* Push a function scope */
    janetc_scope(&rootscope, &c, JANET_SCOPE_FUNCTION | JANET_SCOPE_TOP, "root");

    /* Set initial form options */
    fopts.compiler = &c;
    fopts.flags = JANET_FOPTS_TAIL | JANET_SLOTTYPE_ANY;
    fopts.hint = janetc_cslot(janet_wrap_nil());

    /* Compile the value */
    janetc_value(fopts, source);
    
    if (c.result.status == JANET_COMPILE_OK) {
        JanetFuncDef *def = janetc_pop_funcdef(&c);
    // ...

janetc_value sometimes calls janetc_call and janetc_toslots:

/* Compile a single value */
JanetSlot janetc_value(JanetFopts opts, Janet x) {
    JanetSlot ret;
    JanetCompiler *c = opts.compiler;
    // ...
    c->recursion_guard--;
    // ...
            case JANET_TUPLE: {
                JanetFopts subopts = janetc_fopts_default(c);
                const Janet *tup = janet_unwrap_tuple(x);
                // ,,,
                } else {
                    JanetSlot head = janetc_value(subopts, tup[0]);
                    subopts.flags = JANET_FUNCTION | JANET_CFUNCTION;
                    ret = janetc_call(opts, janetc_toslots(c, tup + 1, janet_tuple_length(tup) - 1), head);
                    janetc_freeslot(c, head);
                }
    // ...

janetc_toslots takes Janet values contained in tup (except the first value) and produces JanetSlot* (multiple JanetSlots):

/* Get a bunch of slots for function arguments */
JanetSlot *janetc_toslots(JanetCompiler *c, const Janet *vals, int32_t len) {
    int32_t i;
    JanetSlot *ret = NULL;
    JanetFopts subopts = janetc_fopts_default(c);
    subopts.flags |= JANET_FOPTS_ACCEPT_SPLICE;
    for (i = 0; i < len; i++) {
        janet_v_push(ret, janetc_value(subopts, vals[i]));
    }
    return ret;
}

janetc_call can call janetc_pushslots with the JanetSlot* value returned from janetc_toslots:

/* Compile a call or tailcall instruction */
static JanetSlot janetc_call(JanetFopts opts, JanetSlot *slots, JanetSlot fun) {
    JanetSlot retslot;
    JanetCompiler *c = opts.compiler;
    int specialized = 0;
    // ...
    if (!specialized) {
        int32_t min_arity = janetc_pushslots(c, slots);
    // ...

Using slots (the JanetSlot* value returned by janetc_toslots above), janetc_pushslots emits bytecode (ultimately captured in c->buffer) to push the slot values on to the top of the stack (see source comment below for meaning of return value):

/* Push slots loaded via janetc_toslots. Return the minimum number of slots pushed,
 * or -1 - min_arity if there is a splice. (if there is no splice, min_arity is also
 * the maximum possible arity). */
int32_t janetc_pushslots(JanetCompiler *c, JanetSlot *slots) {
    int32_t i;
    int32_t count = janet_v_count(slots);
    int32_t min_arity = 0;
    int has_splice = 0;
    for (i = 0; i < count;) {
        // ...
        } else if (i + 1 == count) {
            janetc_emit_s(c, JOP_PUSH, slots[i], 0);
            i++;
            min_arity++;
        // ...
}

Note that the bytecode being generated doesn't imply it ever gets executed. This is a compile-time thing. The bytecode might eventually be executed or it might not. So it might be clearer to articulate the above text as arranging for slot values to be pushed on to the stack at runtime (if that ever comes).

Imagine that one of the slots was created via:

/* Create a slot with a constant */
JanetSlot janetc_cslot(Janet x) {
    JanetSlot ret;
    ret.flags = (1 << janet_type(x)) | JANET_SLOT_CONSTANT;
    ret.index = -1;
    ret.constant = x;
    ret.envindex = -1;
    return ret;
}

For the opcode JOP_PUSH and the JanetSlot created via janet_cslot, emit the corresponding bytecode (_s from janetc_emit_s means single 8-bit argument):

int32_t janetc_emit_s(JanetCompiler *c, uint8_t op, JanetSlot s, int wr) {
    int32_t reg = janetc_regfar(c, s, JANETC_REGTEMP_0);
    int32_t label = janet_v_count(c->buffer);
    janetc_emit(c, op | (reg << 8));
    if (wr)
        janetc_moveback(c, s, reg);
    janetc_free_regnear(c, s, reg, JANETC_REGTEMP_0);
    return label;
}

...but before doing so, call janetc_regfar:

/* Convert a slot to a two byte register */
static int32_t janetc_regfar(JanetCompiler *c, JanetSlot s, JanetcRegisterTemp tag) {
    /* check if already near register */
    if (s.envindex < 0 && s.index >= 0) {
        return s.index;
    }
    int32_t reg;
    int32_t nearreg = janetc_regalloc_temp(&c->scope->ra, tag);
    janetc_movenear(c, nearreg, s);
    if (nearreg >= 0xF0) {
        reg = janetc_allocfar(c);
        janetc_emit(c, JOP_MOVE_FAR | (nearreg << 8) | (reg << 16));
        janetc_regalloc_freetemp(&c->scope->ra, nearreg, tag);
    } else {
        reg = nearreg;
        janetc_regalloc_freetemp(&c->scope->ra, nearreg, tag);
        janetc_regalloc_touch(&c->scope->ra, reg);
    }
    return reg;
}

Since the slot (s) was made via janetc_cslot, s.index is -1, so the first if doesn't apply. Thus, janetc_movenear will be called:

/* Move a slot to a near register */
static void janetc_movenear(JanetCompiler *c,
                            int32_t dest,
                            JanetSlot src) {
    if (src.flags & (JANET_SLOT_CONSTANT | JANET_SLOT_REF)) {
        janetc_loadconst(c, src.constant, dest);
        /* If we also are a reference, deref the one element array */
        if (src.flags & JANET_SLOT_REF) {
            janetc_emit(c,
                        (dest << 16) |
                        (dest << 8) |
                        JOP_GET_INDEX);
        }
    } else if (src.envindex >= 0) {
        janetc_emit(c,
                    ((uint32_t)(src.index) << 24) |
                    ((uint32_t)(src.envindex) << 16) |
                    ((uint32_t)(dest) << 8) |
                    JOP_LOAD_UPVALUE);
    } else if (src.index != dest) {
        janet_assert(src.index >= 0, "bad slot");
        janetc_emit(c,
                    ((uint32_t)(src.index) << 16) |
                    ((uint32_t)(dest) << 8) |
                    JOP_MOVE_NEAR);
    }
}

Since the slot (src) was called via janetc_cslot, src.flags will have JANET_SLOT_CONSTANT set and thus the first if will apply and at minimum janetc_loadconst will be called:

/* Load a constant into a local register */
static void janetc_loadconst(JanetCompiler *c, Janet k, int32_t reg) {
    switch (janet_type(k)) {
        case JANET_NIL:
            janetc_emit(c, (reg << 8) | JOP_LOAD_NIL);
            break;
        case JANET_BOOLEAN:
            janetc_emit(c, (reg << 8) |
                        (janet_unwrap_boolean(k) ? JOP_LOAD_TRUE : JOP_LOAD_FALSE));
            break;
        case JANET_NUMBER: {
            double dval = janet_unwrap_number(k);
            if (dval < INT16_MIN || dval > INT16_MAX)
                goto do_constant;
            int32_t i = (int32_t) dval;
            if (dval != i)
                goto do_constant;
            uint32_t iu = (uint32_t)i;
            janetc_emit(c,
                        (iu << 16) |
                        (reg << 8) |
                        JOP_LOAD_INTEGER);
            break;
        }
        default:
        do_constant: {
                int32_t cindex = janetc_const(c, k);
                janetc_emit(c,
                            (cindex << 16) |
                            (reg << 8) |
                            JOP_LOAD_CONSTANT);
                break;
            }
    }
}

janetc_emit takes a single bytecode and appends it to c->buffer (where the JanetCompiler value accumulates emitted / generated bytecode):

/* Emit a raw instruction with source mapping. */
void janetc_emit(JanetCompiler *c, uint32_t instr) {
    janet_v_push(c->buffer, instr);
    janet_v_push(c->mapbuffer, c->current_mapping);
}

...and that my liege, is how we know the world to be banana-shaped.

Afterword: c->buffer to def->bytecode

janetc_pop_funcdef is called by janet_compile_lint after it calls janetc_value. janetc_pop_funcdef copies c->buffer to def->bytecode:

/* Compile a funcdef */
/* Once the various other settings of the FuncDef have been tweaked,
 * call janet_def_addflags to set the proper flags for the funcdef */
JanetFuncDef *janetc_pop_funcdef(JanetCompiler *c) {
    JanetScope *scope = c->scope;
    JanetFuncDef *def = janet_funcdef_alloc();
    def->slotcount = scope->ra.max + 1;

    // ...

    /* Copy bytecode (only last chunk) */
    def->bytecode_length = janet_v_count(c->buffer) - scope->bytecode_start;
    if (def->bytecode_length) {
        size_t s = sizeof(int32_t) * (size_t) def->bytecode_length;
        def->bytecode = janet_malloc(s);
        if (NULL == def->bytecode) {
            JANET_OUT_OF_MEMORY;
        }
        safe_memcpy(def->bytecode, c->buffer + scope->bytecode_start, s);
        janet_v__cnt(c->buffer) = scope->bytecode_start;
        // ...

Misc Bits Regarding Bytecode, Stacks, and Fibers

The following comment from vm.c may be helpful for imagining Janet bytecode. Each bytecode is 32 bits (4 bytes). The "rightmost" 8 bits are where the opcode is located:

/* Virtual registers
 *
 * One instruction word
 * CC | BB | AA | OP
 * DD | DD | DD | OP
 * EE | EE | AA | OP
 */

Some operations take 3 arguments (each 1 byte or 8 bits, e.g. CC, BB, AA), some take 2 arguments (1 2-byte, 1 1-byte, e.g. EE EE, AA), and some take a single argument (1 3-byte, e.g. DD DD DD).

Note that though AA, BB, ... EE have two letters, this does not appear to convey more than perhaps that each pair of letters is one byte or two nibbles. Might be less confusing to think of them as A, B, ..., E.

The vm loop makes use of some macros to refer to the arguments:

#define A ((*pc >> 8)  & 0xFF)
#define B ((*pc >> 16) & 0xFF)
#define C (*pc >> 24)
#define D (*pc >> 8)
#define E (*pc >> 16)

Note the use of single letters in the macros.

A couple of other macros that are probably worth being familiar with include:

/* Commit and restore VM state before possible longjmp */
#define vm_commit() do { janet_stack_frame(stack)->pc = pc; } while (0)
#define vm_restore() do { \
    stack = fiber->data + fiber->frame; \
    pc = janet_stack_frame(stack)->pc; \
    func = janet_stack_frame(stack)->func; \
} while (0)

Minimally, note:

  • janet_stack_frame (defined in fiber.h)
  • stack = fiber->data + fiber-frame;

fiber is of type JanetFiber.

fiber->data is of type * Janet and represents the bottom of the fiber's (upward growing) stack.

fiber->frame is an index to the base of the current stack frame.

Probably, stack = fiber->data + fiber->frame means something like "where the "inner" stack of the current stack frame" starts. That is, all registers for the current stack frame are computed based on this location (i.e. stack).

janet_stack_frame:

#define janet_stack_frame(s) ((JanetStackFrame *)((s) - JANET_FRAME_SIZE))
#define janet_fiber_frame(f) janet_stack_frame((f)->data + (f)->frame)

yields the location that a JanetStackFrame lives at along the fiber's stack. The - JANET_FRAME_SIZE suggests that (f)->data + (f)->frame yields a location right after where a JanetStackFrame sits. Further evidence for that is:

janet_stack_frame(stack)->pc = pc;

->pc is referring to a field in:

/* A stack frame on the fiber. Is stored along with the stack values. */
struct JanetStackFrame {
    JanetFunction *func;
    uint32_t *pc;
    JanetFuncEnv *env;
    int32_t prevframe;
    int32_t flags;
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment