I mentioned before that getting an interpreter working is a nerd-snipe. Once it works a little, it’s so much fun to keep adding functionality you end up working on it despite yourself. In this spirit, here is a writeup about my latest changes to ts-wolfram.
I initially wrote the interpreter using OOP. There were three AST types (Integer
, Symbol
, and Form
), each one derived from a Node
interface. This worked but bothered me, primarily as an aesthetic matter. I prefer to think of programs in terms of pipelines of transformations between data types. You can do this with classes, but stylistically it doesn’t quite fit and tends to leave a feeling of dissatisfaction. So I got rid of classes in favor of an algebraic data type, which to me looks much nicer:
export type Expr = Integer | Symbol | Form | String;
export enum Types {
Form, Integer, Symbol, String,
}
export type Form = {
type: Types.Form,
head: Expr,
parts: Expr[],
}
export type Integer = {
type: Types.Integer,
val: number,
}
// ...
Many small changes downstream of dropping OOP also made the code much nicer. For example, I got rid of ugly instanceof
calls. But overall, code organization could still use one more pass of deliberate thinking through the file structure, and putting code that belongs together in dedicated files.
Second, I changed the interpreter to support mixing “kernel” and “userspace” code in expression evaluation. Previously a function could either be defined in Typescript, or in Mathematica code, but not both. For example, Times
is defined in Typescript, which meant I couldn’t put convenient transformation code into the prelude. Typing -a (-b)
in the REPL produced Times[Minus[a], Minus[b]]
. It would be convenient to add a rule to transform this into Times[a, b]
, but the evaluator didn’t support that.
Supporting this ended up being a small change. Once the evaluator supported mixed definitions I added the following line to the prelude:
Times[Minus[x_], Minus[y_]] := Times[x, y];
And voilà! Typing -a (-b)
now produces Times[a, b]
without any changes to Typescript code.
Finally, I wanted to improve printing. Typing something like Hold[a /. a->b]
printed the full form Hold[ReplaceAll[a, Rule[a, b]]]
. I wanted to print shorthand. My original plan was to add string support and then use the same trick as with Times[Minus[...], Minus[...]]
to do pretty printing in userspace. I added strings, ToString
and <>
/StringJoin
, but then realized adding userspace rules to ToString
doesn’t quite work. Calling ToString
from the interpreter to pretty print caused additional evaluation and printed incorrect results.
Instead of going against the grain and trying to get this to work, I just kept the string commands and implemented pretty printing in Typescript. I added support for basic syntax so expressions like Hold[a /. a->b]
print correctly in shorthand. I also added the FullForm
command to help with debugging when I do need to see the full form.
However, getting a good printer working is non-trivial. For example, Sin[Cos[x]] Sin[x]
currently prints (Sin[Cos[x]]) (Sin[x])
– the printer doesn’t know to drop parentheses. I didn’t look into this too closely, but it seems like the naive approach would require lots of special cases to print expressions nicely. To get the printer working well, I need either to add many of the special cases, or find a more general approach (assuming one exists).
Oct 26, 2024