Evaluating JavaScript code in the browser

Sayooj Surendran

Sayooj Surendran

October 8, 2024

Evaluating JavaScript code in the browser

NeetoCourse allows anyone to build interactive courses where they can add codeblocks and assessments. This allows the user to run their code, see the output and check if their solution is correct or not. Check out Bigbinary Academy's JavaScript course to see this in action.

Let's see how we evaluate JavaScript code and check if the output matches the corresponding solution.

Synchronous code

For a simple synchronous code, first thing we need to check is if everything logged by the user is same as that of the solution code. What we do here is aggregate all the logs to an array and then compare that array with the array generated by the solution code. This is done by transforming the code using an AST library

Take this exercise as an example.

code

Now let's see the transformed code.

transformed

Here pushTologs replaces the console.log function and logsAggregator is an array which stores all the logs. We also replace throw statements with pushToLogs to evaluate exceptions.

We also perform serialization to make comparison easier. The transformed code is then ran as an IIFE and the result is used for comparison.

We run the user submitted code in an iframe so that any bug in the submitted code doesn't mess up the page.

How code is transformed

Let's see how this "code transformation" works. We mentioned the use of an AST library. AST(abstract syntax tree) is a tree representation of the code which helps the compiler to understand the structure of the code. Let's use a tool called AST Explorer to see how the AST looks like for the below code.

const priceOfPencil = 5;

console.log("Price of 1 pencil:");
console.log(priceOfPencil);

astexplorer

Here, MemberExpression is a node and in that node we can use object.name. See the underline highlights using pink color.

Using object.name we can get to value console. See the green arrow.

Similarly using property.name we can get to log.

Now our goal is to walk the tree and replace all console.log statements with pushToLogs statement. For walking and replacing the value we will use the replace function provided by the library.

replace(tree, node => {
  const { callee } = node;

  // create the node that needs to be put instead of console.log
  const pushToLogsExpression = parse("pushToLogs()").body[0].expression;

  // check for the console.log node
  if (
    callee?.type === "MemberExpression" &&
    callee?.object?.name === "console" &&
    callee?.property?.name === "log"
  ) {
    pushToLogsExpression.arguments = node.arguments;
    node = pushToLogsExpression;
  }

  return node;
});

Here we are creating a new node by parsing the string "pushToLogs()". We are then adding the arguments of the console.log to the pushToLogs node. When we return this new node, the code transformation is complete.

Asynchronous code

Evaluating async code is a bit tricky since we won't get the output of the code right away. What we do in this case is transform the code to make it synchronous. For evaluating the output, these are the information we need:

  1. Console logs
  2. Exceptions
  3. The order in which the code needs to be executed

We will transform the code in such a way that these information are available to us. Let's see how the code is transformed in the following cases:

SetTimeout / SetInterval

code

transformed

In this case the function that needs to be executed after the specified timeout is executed inline. And the delay value is just added to the logsAggregator for record keeping. There will be no delay in the evaluation.

We do the same for setInterval.

In both the cases we evaluate the function "inline" and then compare the console.log outputs.

What if we want an exercise involving clearTimeout ? We simply add Timeout cleared to the logsAggregator.

Promises

code

transformed

Without going too much in details, to evaluate Promises all we did was move the callbacks from then method of the promise to arguments of a function.

We take care of async/await code in similar style.

What happens in case of promise chaining?

code

transformed

Here, if we detect that there are more than one then calls, then the second body of then is passed as resolveFn to the function that the first then returns. This can go multiple levels based on the chaining.

In order words we go back to adding "callbacks".

This is how we evaluate javascript in NeetoCourse. We evaluate HTML, CSS and SQL similarly on the browser. We have also recently added evaluation of HTML Canvas. Evaluation of HTML Canvas animations is on the roadmap. But these can be a story for another day.

Want to see code evaluation in action. Checkout this question from our JavaScript course.

Interested to know more about NeetoCourse? Follow @NeetoCourse to see what we're up to.

If this blog was helpful, check out our full blog archive.

Stay up to date with our blogs.

Subscribe to receive email notifications for new blog posts.