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:

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:

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 the if 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.

Enjoy this? Subscribe to my RSS feed or follow me on Twitter.