Compiling JS control-flow to C
Let's look at how I implemented JS's if
statement in the js-to-c
compiler. First, let's write a JS if
:
if (Math.random() > 0.5) {
console.log('heads');
} else {
console.log('tails');
}
This is very simple JS, but here are the steps any JS implementation will have to perform to implement the above:
- 1.evaluate the conditional
- 1.1 evaluate the Math.random() call
- 1.1.1 evaluate
Math
- 1.1.2 evaluate dereferencing
'random'
from Math - 1.1.3 attempt to call the dereferenced value
- 1.2 run
>
with the result and 0.5 - 1.3 check truthiness of the result
- run first block if it's truthy
- run second block if it's not, and there is a second block to run
Let's have a look at the resulting C. There's a lot of code, but you should be able to track back from the C to the steps above:
/* if */
JsValue* object_5 = (envGet(env, interned_7 /* Math */));
JsValue* property_6 = (interned_8 /* random */);
JsValue* callee_4 = (objectGet(object_5, property_6));
JsValue** args_4 = NULL;
JsValue* left_2 = (functionRunWithArguments(
callee_4,
args_4,
0
,NULL
));
JsValue* right_3 = (jsValueCreateNumber(0.5));
JsValue* conditionalPredicate_1 = (GTOperator(left_2, right_3));
if(isTruthy(conditionalPredicate_1)) {
JsValue* call9Arg_0;
JsValue* object_10 = (envGet(env, interned_12 /* console */));
JsValue* property_11 = (interned_13 /* log */);
JsValue* callee_9 = (objectGet(object_10, property_11));
call9Arg_0 = (interned_14 /* heads */);
JsValue* args_9[] = {call9Arg_0};
functionRunWithArguments(
callee_9,
args_9,
1
,NULL
);
} else {
JsValue* call15Arg_0;
JsValue* object_16 = (envGet(env, interned_12 /* console */));
JsValue* property_17 = (interned_13 /* log */);
JsValue* callee_15 = (objectGet(object_16, property_17));
call15Arg_0 = (interned_18 /* tails */);
JsValue* args_15[] = {call15Arg_0};
functionRunWithArguments(
callee_15,
args_15,
1
,NULL
);
}
This code was output from the compiler processing an IfStatement
node. You can see it defines an intermediate variable to store the result of the conditional. Once we've evaluated the conditional and stored it, we can use C's own if
statement with the ifTruthy()
js2c runtime function, which turns a JsValue into a c bool
. Since only 0
is false in C, all JsValue
structs would evaluate to true in C. We have compiled the consequent and alternate blocks into C code that lives inside the branches of the C if
.
function compileIfStatement(node: IfStatement, state: CompileTimeState) {
const testResultTarget =
new IntermediateVariableTarget(
state.getNextSymbol('conditionalPredicate')
);
const testSrc = compile(node.test, state.childState({
target: testResultTarget,
}));
const consequentSrc = compile(node.consequent, state);
const alternateSrc = node.alternate
? ` else {
${compile(node.alternate, state)}
}`
: '';
return `/* if */
${testSrc}
if(isTruthy(${testResultTarget.id})) {
${consequentSrc}
} ${alternateSrc}`
}
Loops
Compiling JS's for
loop was interesting. A for loop is comprised of three statements aside from its body block:
initialiser, e.g
var i = 0
test
i < xs.length
update
i++
Way too much is going on in each of those updates from the point of view of the compiled C code for us to directly piggy-back on top of C's
for
. Remember how many steps were involved in finding out the result of theif
conditional above. So instead we compile to an infinite C loop, and break out of it if required.
function compileForStatement(node: ForStatement, state: CompileTimeState) {
const testVariable = new IntermediateVariableTarget(state.getNextSymbol('testResult'));
const initSrc = node.init ? compile(node.init, state) : '/* no init */';
const testSrc = node.test
? compile(node.test, state.childState({
target: testVariable
}))
: `// no test`;
const testBranchSrc = node.test
? `if (!isTruthy(${testVariable.id})) {
break;
}`
: '';
const updateSrc = node.update ? compile(node.update, state) : `/* no update */`;
const bodySrc = compile(node.body, state);
return `/* for loop */
${initSrc}
while(1) {
${testSrc}
${testBranchSrc}
${bodySrc}
${updateSrc}
}`
}
A cool thing to note is how we can implement JS's break
and continue
:
function compileBreakStatement() { return `break;` }
function compileContinueStatement() { return `continue;` }
In the for
example, you can see all of the JS test
body
update
steps are in the C while
's body, so continue
and break
will work as expected by being compiled one-to-one with their C equivalent. Take a look at the forIn
and while
loops in the js2c source and you'll see the same is true of them. Compiling an imperative language into another imperative language has let us piggy-back off the target language's constructs. There's something deep and satisfying about the logic of that translation.
Leave a comment via Github 💬