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 JanetSlot
s):
/* 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.
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;
// ...
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 infiber.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;
};