DEV Community

Jason Barr

Posted on • Updated on

Create Your Own Programming Language 9: Iteration

In this installment of the Create Your Own Programming Language series we're going to add iteration to Wanda. We're also going to make some major improvements to the CLI at the end of the article.

As always, if you haven't read the previous article where we added conditionals, do that first and then continue below.

Ok, let's go!

Iteration in Wanda

The `for` form will handle iteration in Wanda, and it looks like this:

``````(for map ((i (range 5)))
(+ i i))
``````

The form takes an operator, a list of variables with their initializers, and a body with an arbitrary number of expressions.

Current operators in Wanda include `each`, `map`, `filter`, `fold`, and `fold-r`, but you can also define your own `for` operators by creating functions. They should be higher-order functions that take a function callback as the first argument, and the callback argument should take a parameter for each variable. Then the rest of the main functions' arguments should be the initializers for the callback's parameters.

For instance, here's an example implementation of a `map` operator:

``````(def map (fn lst)
(if (nil? lst)
lst
(cons (fn (head lst)) (map fn (tail lst)))))
``````

As you can see, the callback takes a single parameter and the higher-order function takes the callback and a list, which is the initializer for the callback parameter.

Here's how you'd use a `for` expression to sum up a list of numbers:

``````(for fold ((sum 0) (x (range 11)))
(+ sum x))
``````

A `for` expression desugars to a call expression that uses the operator as its function, constructs a lambda as the first argument to the operator, then passes in the initializers as the remaining arguments to the operator.

Inspiration for our `for` expressions comes chiefly from the Pyret language, which handles them similarly.

Pyret was inspired by Racket's list comprehensions, Ruby's blocks and iterators, and Smalltalk blocks.

New CLI Features

Here are the new features we're adding to the Wanda CLI:

• History (up to 2000 lines)
• Help info for the CLI, the `wandac` compiler, and the REPL
• The ability to load a Wanda file from within the REPL
• The ability to save a REPL session as a file

We'll get to those at the end of this article, but first let's implement `for` expressions!

An Easy Iterator

To make it easy to create an object to iterate over, let's add a `range` function to the core library.

In Python, a Range object doesn't actually contain all the numbers you iterate over. It computes them from the start, stop, and step values. We're not getting that fancy; our `range` function will just produce a list of numbers. It will work like Python's `range` function though, in that you can pass it 1, 2, or 3 arguments and it will calculate the range from them.

In `lib/js/core.js`, just below `typeof`, add an entry for `range`:

``````range: rt.makeFunction(
(start, stop = undefined, step = 1) => {
if (typeof stop === "undefined") {
stop = start;
start = 0;
}

let list = null;
if (start < stop) {
list = cons(start, list);
for (let i = start + step; i < stop; i += step) {
list.append(i);
}
} else if (stop < start) {
list = cons(start, list);
for (let i = start - step; i > stop; i -= step) {
list.append(i);
}
}

return list;
},
{
// contract is variadic because language has no concept of default parameters
contract: "(&(vector number) -> (list number))",
name: "range",
}
),
``````

Changes to The Lexer and Reader

None. In fact, for the most part the lexer and reader are done. We may add some small things to them later, but there shouldn't be any major changes to either for the rest of this series.

Changes to The Parser

We need a `ForExpression` AST node and we need to parse them.

First, add a member to the `ASTTypes` enum in `src/parser/ast.js`:

``````export const ASTTypes = {
// other members...
ForExpression: "ForExpression",
};
``````

Then add its constructor to the `AST` object:

``````export const AST = {
// other constructors...
ForExpression(op, vars, body, srcloc) {
return {
kind: ASTTypes.ForExpression,
op,
vars,
body,
srcloc,
};
},
}
``````

In `src/parser/parse.js` we'll need a new case for the `switch` statement in `parseList`:

``````    // other cases...
case "for":
return parseForExpression(form);
// default case
``````

In `parseForExpression` we'll get the operator, the variable/initializer list, and the list of body expressions. The operator should be a symbol (though there's technically no reason why it couldn't be a lambda). Then we iterate over the list of variable/initializer pairs and parse them. Finally, we iterate over the body and parse each expression in it.

Here's `parseForExpression`:

``````const parseForExpression = (form) => {
const [_, op, rawVars, ...body] = form;
const srcloc = form.srcloc;
const parsedOp = parseExpr(op);

/** @type {import("./ast.js").ForVar[]} */
let vars = [];

for (let rawVar of rawVars) {
const varName = parseExpr(rawVar.car);
// need the head of the tail of the rawVar list
const initializer = parseExpr(rawVar.cdr.car);

vars.push({ var: varName, initializer });
}

/** @type {AST[]} */
let parsedBody = [];

for (let expr of body) {
parsedBody.push(parseExpr(expr));
}

return AST.ForExpression(parsedOp, vars, parsedBody, srcloc);
};
``````

That's it for changes to the parser. Now let's see how to type check a `for` expression.

Changes to The Type Checker

We actually only need to change 2 files in the type checker. First, we need to infer a type for the `for` expression.

We need to add a dispatch case to `infer` in `src/typechecker/infer.js`:

``````    // other cases...
case ASTTypes.ForExpression:
return inferForExpression(ast, env, constant);
// default case
``````

Now for `inferForExpression`.

We're actually going to deconstruct the `for` expression here and infer a type as if it were a function call. This means we need the operator as a function, a lambda as the first argument to the operator, and the list of arguments to follow the lambda.

We get the lambda's params by mapping over `node.vars` and constructing argument types from the initializers. If an initializer is a list or vector, we use the contained type.

Once we've got the lambda params we construct a `LambdaExpression` AST node using the params and the expression body. Then we construct an array of arguments for the call expression with the lambda as the first argument and mapping over `node.vars` again to get the initializers as the remaining arguments.

Then we construct a call expression from the operator and arguments and call `infer` on it.

Here's `inferForExpression`:

``````const inferForExpression = (node, env, constant) => {
const lambdaArgs = node.vars.map((v) => {
let varType = infer(v.initializer, env, constant);

if (Type.isList(varType)) {
varType = varType.listType;
} else if (Type.isVector(varType)) {
varType = varType.vectorType;
}

return { name: v.var, type: varType };
});
const lambda = AST.LambdaExpression(
lambdaArgs,
node.body,
false,
null,
node.srcloc
);
const opArgs = [lambda, ...node.vars.map((v) => v.initializer)];

return infer(AST.CallExpression(node.op, opArgs, node.srcloc), env, constant);
};
``````

The process of constructing the `CallExpression` node in `inferForExpression` is similar to how we'll handle `for` expressions in the desugarer.

Next we need to handle the `ForExpression` node in `src/typechecker/TypeChecker.js`.

First, add a case to the `switch` statement in `checkNode`:

``````      // other cases...
case ASTTypes.ForExpression:
return this.checkForExpression(node, env);
// default case
``````

The `checkForExpression` method is pretty simple:

``````  checkForExpression(node, env) {
const op = this.checkNode(node.op, env);
return { ...node, op, type: infer(node, env) };
}
``````

That's it for changes to the type checker. It's a lot less than it's been in the past few articles where we've focused more on type checker features.

Now let's look at changes to the default visitor.

Changes to The Visitor

We need a dispatch case and default visitor for the `ForExpression` node in `src/visitor/Visit.js`.

First, add a case to the `visit` method's `switch` statement:

``````      // other cases...
case ASTTypes.ForExpression:
return this.visitForExpression(node);
// default case
``````

In `visitForExpression` we simply visit the operator, visit each variable and initializer in `node.vars`, and then visit each body expression.

Here's `visitForExpression`:

``````  visitForExpression(node) {
const op = this.visit(node.op);

let vars = [];

for (let nodevar of node.vars) {
const v = this.visit(nodevar.var);
const initializer = this.visit(nodevar.initializer);

vars.push({ var: v, initializer });
}

let body = [];

for (let expr of node.body) {
body.push(this.visit(expr));
}

return { ...node, op, vars, body };
}
``````

Now that there's a default visitor for the `ForExpression` node, we need to handle it in the desugarer.

Changes to The Desugarer

The process is similar to how we inferred a type for the expression: we construct a lambda, then construct a call expression with that lambda as its first argument.

Here's `visitForExpression` in `src/desugarer/Desugarer.js`:

``````  visitForExpression(node) {
const op = this.visit(node.op);
const lambdaArgs = node.vars.map((v) => ({ name: v.var }));
const lambda = AST.LambdaExpression(
lambdaArgs,
node.body,
false,
null,
node.srcloc
);
const callArgs = [lambda, ...node.vars.map((v) => v.initializer)];

return AST.CallExpression(op, callArgs, node.srcloc);
}
``````

Now we don't need to worry about handling `for` expressions in the emitter! We do need to fix a bug I found, though.

Changes to The Emitter

The bug is in `emitGlobalEnv` in `src/emitter/emitGlobalEnv.js`.

When emitting the actual variable assignments, we currently don't emit the `var` keyword with them because we needed them to be globals in the REPL.

Now that we're emitting compiled files and have the option to emit a file that imports the global environment using ES2015 imports we need to change that slightly.

The reason is that ES2015 modules use strict mode by default, and in strict mode it throws an error if you assign to a variable without declaring it first with `var`, `let`, or `const`.

Since this is only an issue for compiled files, and not in the REPL or bundled files, we'll add `var` based on an optional boolean flag passed into the `emitGlobalEnv` function.

Here's the new version of `emitGlobalEnv`:

``````import path from "path";
import { ROOT_PATH } from "../../root.js";
import { makeGlobal } from "../runtime/makeGlobals.js";

export const emitGlobalEnv = (useVar = false) => {
const globalEnv = makeGlobal();
let code = `import { makeGlobal } from "\${path.join(
ROOT_PATH,
"./src/runtime/makeGlobals.js"
)}";
import { makeRuntime } from "\${path.join(
ROOT_PATH,
"./src/runtime/makeRuntime.js"
)}";

const globalEnv = makeGlobal();
\${useVar ? "var " : ""}rt = makeRuntime();
`;

for (let [k] of globalEnv) {
code += `\${useVar ? "var " : ""}\${k} = globalEnv.get("\${k}");\n`;
}

return code;
};
``````

Now when we compile a file and run it as an ES2015 module we won't get an error because of undeclared variables.

Next we need to make a change to the AST printer to handle the new node type.

Changes to The Printer

We need to add the `ForExpression` node to the AST printer in `src/printer/printAST.js`.

First, let's add the case to the `switch` statement in the `print` method:

``````      // other cases...
case ASTTypes.ForExpression:
return this.printForExpression(node, indent);
// default case
``````

The `printForExpression` method is a little verbose because we have to iterate over both `node.vars` and `node.body` to print the subexpressions correctly.

Here's `printForExpression`:

``````  printForExpression(node, indent) {
let prStr = `\${prIndent(indent)}ForExpression:\n`;
prStr += `\${prIndent(indent + 2)}Operator:\n`;
prStr += ` \${this.print(node.op, indent + 4)}\n`;
prStr += `\${prIndent(indent + 2)}Vars:\n`;

for (let nodevar of node.vars) {
prStr += `\${prIndent(indent + 4)}Var: \${this.print(nodevar.var, 0)}\n`;
prStr += `\${prIndent(indent + 4)}Init: \${this.print(
nodevar.initializer,
0
)}\n`;
}

prStr += `\${prIndent(indent + 2)}Body:\n`;

for (let expr of node.body) {
prStr += this.print(expr, indent + 4) + "\n";
}

return prStr;
}
``````

That's it for changes to the printer! Now `for` expressions fully work within the language, so it's time to focus on our changes to the CLI.

Changes to The CLI

Remember, we're adding these new features to the CLI:

• History (up to 2000 lines)
• Help info for the CLI, the `wandac` compiler, and the REPL
• The ability to load a Wanda file from within the REPL
• The ability to save a REPL session as a file

In order to enable history, we're going to have to change how we get input in the REPL. Instead of using the `readline-sync` package, we're going to use Node's C++ API to directly access readline.

Don't worry, you won't have to write any C++ code to get this to work. There's a Node.js package that gives you access to the C++ API via JavaScript. Install the `ffi-napi` package with `npm install ffi-napi`.

We're going to put this in a new file in the CLI directory, `src/cli/readline.js`. This will handle both getting input and managing the history state when you fire up the REPL.

You'll need to import some dependencies:

``````import os from "os";
import fs from "fs";
import { join } from "path";
import ffi from "ffi-napi";
``````

NOTE: The following code accesses `libreadline` directly, so I have no idea if it's cross platform. It probably isn't. I don't believe Windows includes `libreadline` by default, though I could be mistaken. I do my development, including the code for this series, on Linux so I haven't tried to make this run on Windows yet. If I do try it on Windows I'll post an update in a later article.

Ok, with that out of the way, first we use `ffi` to gain access to the `readline` and `add_history` primitives and store them in an object. We also set the path to the history file and a flag for if history has been loaded:

``````const rllib = ffi.Library("libreadline", {
});

const HISTORY_FILE = join(os.homedir(), ".wanda-history");
``````

The `readline` function takes a prompt and returns the line given in response to the prompt. Most of what comes between those 2 things is managing history.

If the `historyLoaded` flag is false, we load the history. If the history file exists we read it, split on the end-of-line character, and filter out any blank lines.

If the history file doesn't yet exist or is blank, the array is empty.

Then we slice the remaining array so that a maximum of 2000 lines remain.

Next, loop over the array and add each line to the history.

After loading the history, we continue by prompting the user and getting text in reply to the prompt.

We add that line to the history, and append it to the end of the history file. Finally, return the text.

Here's the `readline` function:

``````export const readline = (prompt = ">") => {
let lines = [];

if (fs.existsSync(HISTORY_FILE)) {
lines = fs
.split(os.EOL)
// remove blank lines
.filter((line) => line !== "");
}

lines = lines.slice(Math.max(lines.length - 2000, 0));

for (let line of lines) {
}
}

if (line) {

try {
fs.appendFileSync(HISTORY_FILE, line + os.EOL, { encoding: "utf-8" });
} catch (e) {
// do nothing
}
}

return line;
};
``````

Getting The Version and Help Info

Next, in `src/cli/utils.js`, let's write 2 new functions: one to handle getting version info, and one to handle displaying help.

First, we need to import some values into the file:

``````import fs from "fs";
import { join } from "path";
import { ROOT_PATH } from "../../root.js";
``````

`getVersion` retrieves the contents of `package.json` and parses them, then returns the `version` property.

``````export const getVersion = () => {
const packageJson = JSON.parse(
encoding: "utf-8",
})
);
return packageJson.version;
};
``````

`printHelp` shows a short introductory message, then loops over an object of commands and displays information based on the contents of each command's object.

A command's object looks like this:

``````{ alias?: string; description: string; usage?: string; }
``````

Then after showing all the commands' info it shows a postscript message if one is included.

Here's `printHelp`:

``````export const printHelp = (commands, application, postscript = "") => {
console.log(`**** \${application} v\${getVersion()} help info ****`);
console.log();
console.log("Command:  |  Info:");
console.log();

for (let [name, command] of Object.entries(commands)) {
console.log(`\${name}`);
command.alias && console.log(`             Alias: wanda \${command.alias}`);
console.log(`             \${command.description}`);
command.usage && console.log(`             Usage: \${command.usage}`);
}

console.log();
postscript && console.log(postscript);
};
``````

Commands

Ok, now we need help command objects for each of the REPL, the `wanda` CLI, and the `wandac` compiler.

Here are the commands for the REPL which should be at the top of `src/cli/repl.js`, including the new commands to load and save files in the REPL:

``````const COMMANDS = {
":quit": {
description: "Quits the REPL with exit 0",
},
":print-ast": {
description:
"Makes a printed representation of the AST show when you enter an expression",
},
":print-ast -d": {
description:
"Like :print-ast, but shows the tree after the desugaring step, right before emitting code",
usage: ":print-ast -d",
},
":no-print-ast": {
description: "Turns off AST printing if it's on",
},
":save-file": {
description: "Saves the current REPL session as a file",
usage: "Prompts you for a path to save the file",
},
description:
"Loads the definitions from a file into the interactive session",
usage: "Prompts you for a path to load the file from",
},
":version": {
description: "Prints the currently installed version of Wanda",
},
":help": {
description: "Shows this help message",
},
};
``````

Here are the commands for the `wanda` CLI, which you should put at the top of `src/cli/run.js`:

``````const COMMANDS = {
alias: "-l",
description:
"Loads a Wanda file into an interactive session so you can use its definitions",
usage: "wanda load <filepath> or wanda -l <filepath>",
},
run: {
alias: "-r",
description: "Runs a Wanda file from the command line",
usage: "wanda run <filepath> or wanda -r <filepath>",
},
repl: {
alias: "-i",
description: "Starts an interactive session with the Wanda REPL",
usage: "wanda repl or wanda -i",
},
version: {
alias: "-v",
description: "Prints the current version number of your Wanda installation",
usage: "wanda version or wanda -v",
},
help: {
alias: "-h",
description: "Prints this help message on the screen",
usage: "wanda help or wanda -h",
},
};
``````

And here are the commands for the `wandac` compiler, which you should put at the top of `src/cli/wandac.js`:

``````const COMMANDS = {
compile: {
alias: "-c",
description:
"Compiles a single Wanda file to JavaScript that imports its dependencies",
usage: "wandac compile <filepath> or wandac -c <filepath>",
},
build: {
alias: "-b",
description:
"Compiles a Wanda file and builds it with bundled JavaScript dependencies",
usage: "wandac build <filepath> or wandac -b <filepath>",
},
version: {
alias: "-v",
description:
"Prints the current version number of your WandaC installation",
usage: "wandac version or wandac -v",
},
help: {
alias: "-h",
description: "Prints this help message on the screen",
usage: "wandac help or wandac -h",
},
};
``````

Changes to the REPL

Now let's go back to the REPL file.

First, make sure your imports look like this - there have been some changes:

``````import os from "os";
import vm from "vm";
import fs from "fs";
import { join } from "path";
import { pprintAST, pprintDesugaredAST } from "./pprint.js";
import { println } from "../printer/println.js";
import { makeGlobalNameMap } from "../runtime/makeGlobals.js";
import { emitGlobalEnv } from "../emitter/emitGlobalEnv.js";
import { build } from "./build.js";
import { compile } from "./compile.js";
import { makeGlobalTypeEnv } from "../typechecker/makeGlobalTypeEnv.js";
import { countIndent, inputFinished } from "./utils.js";
import { getVersion, printHelp } from "./utils.js";
``````

We're going to change the parameters the `repl` function takes. Instead of separate parameters for `mode` and `file` we're going to make them properties on a single options object and make them both optional.

Next, move these constants out of the `repl` function and put them at the top of the file:

``````// Create global compile environment
const compileEnv = makeGlobalNameMap();
const typeEnv = makeGlobalTypeEnv();
``````

We need them to be at the module level now because we're going to use them in multiple functions.

You should now have this at the top of the `repl` function:

``````  // Build global module and instantiate in REPL context
// This should make all compiled global symbols available
const globalCode = build(emitGlobalEnv());
vm.runInThisContext(globalCode);
``````

Now we'll take all the code that was used to compile and run Wanda files when loading a file into an interactive session on invoking the `wanda load <filename>` command and move it to its own function, `compileAndRunFromPath`:

``````const compileAndRunFromPath = (path) => {
const fileContents = fs.readFileSync(path, { encoding: "utf-8" });
const compiledFile = compile(fileContents, path, compileEnv, typeEnv);
vm.runInThisContext(compiledFile);
};
``````

Then replace that code in the `repl` function with a call to the new function and pass it the `path` option:

``````  if (path) {
// load file in REPL interactively
compileAndRunFromPath(path);
}
``````

We need 2 additional new functions in `src/cli/repl.js`: one to get a file path from input in the REPL, and one to save a REPL session as its own file:

``````const saveAsFile = (session) => {
const path = readlineSync.question("Enter the path to save the file at: ");
const filePath = join(process.cwd(), path);

try {
fs.writeFileSync(filePath, session, { encoding: "utf-8" });
console.log("File saved!");
} catch (e) {
console.log(
`Error while saving file, please try again later: \${e.message}`
);
}
};

const getPathFromInput = () => {
const path = readlineSync.question("Enter the path to load the file from: ");
return join(process.cwd(), path);
};
``````

Now, with the machinery in place, we turn back to the `repl` function.

Let's add a friendly welcome message with instructions on getting help that shows when you start a REPL session. Below the `if (path)` statement that calls `compileAndRunFromPath`, add this:

``````  console.log(
`**** Welcome to the Wanda Programming Language v\${getVersion()} interactive session ****`
);
``````

Now we'll need to add a variable for the session contents to our series of variables just before the actual loop:

``````  let prompt = "> ";
let input = "";
let indent = 0;
let session = "";
``````

Next is the main loop with a nested try/catch. Here it is without the main contents:

``````  while (true) {
try {
} catch (e) {
console.error(e.stack ? e.stack : e.message);
input = "";
indent = 0;
}
}
``````

Now for the main contents. The first thing we need to do in the `try` block is read the input, indenting if it's multiline input:

``````      input += read(prompt + "  ".repeat(indent));
``````

Now if the user enters a keyword that corresponds to a command we need to handle that. We'll extend our `switch (input)` statement to handle the new commands. Note that I've changed `:print-desugared` to just use a `-d` flag with the `:print-ast` command.

Here are the cases for the commands, switching on `input`:

``````        // If it's a command, execute it
case ":quit":
process.exit(0);
case ":print-ast":
mode = "printAST";
input = "";
break;
case ":print-ast -d":
mode = "printDesugared";
input = "";
break;
case ":no-print-ast":
mode = "repl";
input = "";
break;
case ":save-file":
saveAsFile(session);
input = "";
break;
compileAndRunFromPath(getPathFromInput());
input = "";
break;
case ":version":
console.log(getVersion());
input = "";
break;
case ":help":
printHelp(
COMMANDS,
"Wanda Interactive Session",
"Enter an expression at the prompt for immediate evaluation"
);
input = "";
break;
``````

And the default case, which runs the code, remains the same except that we add completed input to the session variable and make sure incomplete input includes the indentation so the output files will have the same indentation as the REPL shows:

``````        // If it's code, compile and run it
default:
if (inputFinished(input)) {
let compiled = compile(input, "stdin", compileEnv, typeEnv);
let result = vm.runInThisContext(compiled);

if (mode === "printAST") {
console.log(pprintAST(input));
} else if (mode === "printDesugared") {
console.log(pprintDesugaredAST(input));
}

println(result);
session += input + os.EOL + os.EOL;
input = "";
indent = 0;
} else {
indent = countIndent(input);
input += os.EOL + "  ".repeat(indent);
}
``````

Or, if you need to see it all together, here's the complete `repl` function:

``````export const repl = ({ mode = "repl", path = "" } = {}) => {
// Build global module and instantiate in REPL context
// This should make all compiled global symbols available
const globalCode = build(emitGlobalEnv());
vm.runInThisContext(globalCode);

if (path) {
// load file in REPL interactively
compileAndRunFromPath(path);
}

console.log(
`**** Welcome to the Wanda Programming Language v\${getVersion()} interactive session ****`
);

let prompt = "> ";
let input = "";
let indent = 0;
let session = "";

while (true) {
try {
input += read(prompt + "  ".repeat(indent));

switch (input) {
// If it's a command, execute it
case ":quit":
process.exit(0);
case ":print-ast":
mode = "printAST";
input = "";
break;
case ":print-ast -d":
mode = "printDesugared";
input = "";
break;
case ":no-print-ast":
mode = "repl";
input = "";
break;
case ":save-file":
saveAsFile(session);
input = "";
break;
compileAndRunFromPath(getPathFromInput());
input = "";
break;
case ":version":
console.log(getVersion());
input = "";
break;
case ":help":
printHelp(
COMMANDS,
"Wanda Interactive Session",
"Enter an expression at the prompt for immediate evaluation"
);
input = "";
break;
// If it's code, compile and run it
default:
if (inputFinished(input)) {
let compiled = compile(input, "stdin", compileEnv, typeEnv);
let result = vm.runInThisContext(compiled);

if (mode === "printAST") {
console.log(pprintAST(input));
} else if (mode === "printDesugared") {
console.log(pprintDesugaredAST(input));
}

println(result);
session += input + os.EOL + os.EOL;
input = "";
indent = 0;
} else {
indent = countIndent(input);
input += os.EOL + "  ".repeat(indent);
}
}
} catch (e) {
console.error(e.stack ? e.stack : e.message);
input = "";
indent = 0;
}
}
};
``````

Now you can load and save files from inside the REPL.

Changes in Running The Wanda CLI

Since we're going to be running files from the command line now, we need to make sure we import everything needed for that. Here's the new list of imports in `src/cli/run.js`:

``````import vm from "vm";
import fs from "fs";
import { join } from "path";
import { repl } from "./repl.js";
import { makeGlobalNameMap } from "../runtime/makeGlobals.js";
import { emitGlobalEnv } from "../emitter/emitGlobalEnv.js";
import { build } from "./build.js";
import { compile } from "./compile.js";
import { makeGlobalTypeEnv } from "../typechecker/makeGlobalTypeEnv.js";
import { getVersion, printHelp } from "./utils.js";
``````

We also need a new function `runFile` to compile and run the contents of a file passed to the CLI:

``````const runFile = (path) => {
const fileContents = fs.readFileSync(path, { encoding: "utf-8" });
const globalNs = makeGlobalNameMap();
const typeEnv = makeGlobalTypeEnv();
const globalCode = build(emitGlobalEnv());
const compiledCode = compile(fileContents, path, globalNs, typeEnv);

vm.runInThisContext(globalCode);
return vm.runInThisContext(compiledCode);
};
``````

In the `run` function, we'll have the command and alias cases fall through to a single handler for each command. I've also removed the option to start a REPL session with AST printing on because it makes more sense to me to have that just be an option you set when you're inside the interactive session.

Here's the new version of `run`. Note that I've also changed it from throwing exceptions on bad commands to just ending the process with an error code:

``````export const run = () => {
switch (process.argv[2]) {
case "-l":
if (!process.argv[3]) {
console.log(`load command requires file path as argument`);
process.exit(1);
}
const path = join(process.cwd(), process.argv[3]);
repl({ path });
break;
case "run":
case "-r":
if (!process.argv[3]) {
console.log(`run command requires file path as argument`);
process.exit(1);
}
return runFile(join(process.cwd(), process.argv[3]));
case "-v":
case "version":
return console.log(getVersion());
case "help":
case "-h":
return printHelp(
COMMANDS,
"Wanda Programming Language",
"Just running wanda with no command also starts an interactive session"
);
case undefined:
case "repl":
case "-i":
return repl();
default:
console.log("Invalid command specified");
process.exit(1);
}
};
``````

Now you can run files directly from the command line.

Changes in Running The Compiler

First, we need to add an import for the new `getVersion` and `printHelp` functions:

``````import { getVersion, printHelp } from "./utils.js";
``````

Most of the changes simply involve handling the new commands and aliases for the old commands, but we're also passing `true` into `emitGlobalEnv` in the `default` case because of the issue we fixed above. Here's the `wandac` function in `src/cli/wandac.js`:

``````export const wandac = () => {
if (!process.argv[2]) {
console.log(`wandac requires either a file path or command argument`);
process.exit(1);
}

switch (process.argv[2]) {
case "build":
case "-b": {
const pathname = join(process.cwd(), process.argv[3]);
const compiledFile = compileFile(pathname);
const globals = emitGlobalEnv();
const code = globals + os.EOL + os.EOL + compiledFile;
const bName = basename(pathname).split(".")[0];
const outfile = bName + ".build" + ".js";
const built = build(code, outfile, bName);

fs.writeFileSync(outfile, built, { encoding: "utf-8" });
break;
}
case "version":
case "-v":
getVersion();
break;
case "help":
case "-h":
printHelp(
COMMANDS,
"WandaC Compiler",
"Just using wandac <filename> also compiles a single file"
);
break;
default: {
// should be a file path
const pathname = join(process.cwd(), process.argv[2]);
const compiledFile = compileFile(pathname);
const globals = emitGlobalEnv(true);
const code = globals + os.EOL + os.EOL + compiledFile;
const outfile = basename(pathname).split(".")[0] + ".js";

fs.writeFileSync(outfile, code, { encoding: "utf-8" });
break;
}
}
};
``````

Trying It Out

First, try out the new `version` and `help` commands with `wanda` and `wandac`. I think you'll agree they make the apps much more accessible than they used to be.

Now fire up a REPL session with `wanda`.

Enter this code into the REPL, one line at a time:

``````(for map ((i (range 5)))
(+ i i))
``````

Now enter `:save-file` and give it a filename at the prompt (I used `examples/for.wanda`). Your file should appear where you saved it!

You can also use the up arrow to cycle back through your history, and history is saved across sessions.

Try using the `:load-file` command and loading `examples/inc.wanda` from last time. Now you should be able to use the `inc` function just like if you'd defined it in your current session. Cool, right?

Conclusion

I hope this has been as fun for you as it's been for me.

As always, you can view the current state of the Wanda code as of the end of this article at the relevant tag in the GitHub repo.

Next time we're going to do something different. Instead of adding a new syntactic or type feature to Wanda, we're going to add an AST transformation and optimization.

We're going to implement an intermediate representation for the Wanda code between desugaring and code emitting called A Normal Form. I promise it's not as scary as it sounds.

We'll also add tail call optimization in the form of trampolining.

That will allow us to use essentially infinite recursion as long as the recursive call is in tail position. If you're not familiar with what that means, I'll explain all in the next article.

Stay tuned!