handleNode = {
let name;
const handleNode = (node, context = "stmt") => {
switch (node.kind) {
case ts.SyntaxKind.SourceFile:
return `module Output exposing (${node.statements
.filter((s) =>
s.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)
)
.flatMap((s) =>
s.declarationList.declarations.map((d) => d.name.escapedText)
)})\n\n${node.statements.map(handleNode).join("\n\n")}`;
case ts.SyntaxKind.TypeAliasDeclaration:
return `type alias ${node.name.getText()} =\n ${handleNode(
node.type,
"type"
)}`;
case ts.SyntaxKind.TypeLiteral:
return `{ ${node.members
.map((n) => handleNode(n, "type"))
.join("\n , ")} }`;
case ts.SyntaxKind.PropertySignature:
return `${node.name.getText()} : ${handleNode(node.type, "type")}`;
case ts.SyntaxKind.NumberKeyword:
return `Float`;
case ts.SyntaxKind.FunctionType:
return `${node.parameters
.map((n) => handleNode(n, "type"))
.join(" -> ")} -> ${handleNode(node.type, "type")}`;
case ts.SyntaxKind.Parameter:
if (context == "type") {
return handleNode(node.type);
} else {
return node.name.escapedText;
}
case ts.SyntaxKind.TypeReference:
return node.typeName.escapedText;
case ts.SyntaxKind.ArrayType:
console.log(node);
return `List ${handleNode(node.elementType, "type")}`;
case ts.SyntaxKind.VariableStatement:
return handleNode(node.declarationList);
case ts.SyntaxKind.VariableDeclarationList:
return node.declarations.map(handleNode).join("\n\n");
case ts.SyntaxKind.VariableDeclaration:
name = node.name.escapedText;
return `${name} : ${makeTypeSignature(node)}\n${name} =\n${indent(
handleNode(node.initializer)
)}`;
case ts.SyntaxKind.FunctionDeclaration:
name = node.name.escapedText;
return `${name} : ${makeTypeSignature(node)}\n${name} ${node.parameters
.map(handleNode)
.join(" ")} =\n${indent(handleNode(node.body))}`;
case ts.SyntaxKind.ArrowFunction:
return `(\\${node.parameters.map(handleNode).join(" ")} -> ${handleNode(
node.body
)})`;
case ts.SyntaxKind.Block:
if (
node.statements.length == 1 &&
node.statements[0].kind === ts.SyntaxKind.ReturnStatement
) {
return handleNode(node.statements[0].expression);
} else if (
node.statements[node.statements.length - 1].kind ===
ts.SyntaxKind.ReturnStatement
) {
const statemets = [...node.statements];
const returnS = statemets.pop().expression;
return `\n let\n${indent(
indent(statemets.map(handleNode).join("\n\n "))
)}\n in\n ${handleNode(returnS)}`;
} else {
throw `Can't translate an Arrow Function that is only run for side effects`;
}
case ts.SyntaxKind.BinaryExpression:
switch (node.operatorToken.getText()) {
case "%":
return `${handleNode(node.left)} |> modBy ${handleNode(
node.right
)}`;
default:
return `${handleNode(
node.left
)} ${node.operatorToken.getText()} ${handleNode(node.right)}`;
}
case ts.SyntaxKind.PropertyAccessExpression:
return `${handleNode(node.expression)}.${node.name.escapedText}`;
case ts.SyntaxKind.Identifier:
return node.escapedText;
case ts.SyntaxKind.NumericLiteral:
return node.text;
case ts.SyntaxKind.CallExpression:
return translateApiCalls(node);
case ts.SyntaxKind.ObjectLiteralExpression:
return `{ ${node.properties.map(handleNode).join(", ")} }`;
case ts.SyntaxKind.PropertyAssignment:
return `${node.name.escapedText} = ${handleNode(node.initializer)}`;
case ts.SyntaxKind.ArrayLiteralExpression:
return `[ ${node.elements.map(handleNode).join(", ")} ]`;
default:
console.log(node);
throw `node kind ${node.kind} is not supported`;
}
};
const makeTypeSignature = (node) => {
const type = checker.getTypeAtLocation(node);
console.log(type, checker.typeToString(type));
if (type.isLiteral()) {
if (type.isNumberLiteral()) {
return "Float";
} else {
throw "unsupported literal";
}
} else {
return handleNode(
checker.typeToTypeNode(checker.getTypeAtLocation(node))
);
}
};
const translateApiCalls = (node) => {
// there is probably a better way to do this using signatures and/or symbols
if (node.expression.kind == ts.SyntaxKind.PropertyAccessExpression) {
const t = checker.getTypeAtLocation(node.expression.expression);
// List
if (checker.isArrayType(t)) {
switch (node.expression.name.escapedText) {
case "map":
if (node.arguments.length !== 1) throw "Can't handle this map call";
return `List.map ${handleNode(node.arguments[0])} ${handleNode(
node.expression.expression
)}`;
case "filter":
if (node.arguments.length !== 1)
throw "Can't handle this filter call";
return `List.filter ${forceCastToBool(
node.arguments[0]
)} (${handleNode(node.expression.expression)})`;
}
}
}
return `${handleNode(node.expression)} ${node.arguments
.map(handleNode)
.join(" ")}`;
};
const forceCastToBool = (node) => {
let t;
if (
node.kind === ts.SyntaxKind.ArrowFunction &&
!checker.isTypeAssignableTo(
(t = checker.getTypeAtLocation(node.body)),
checker.getBooleanType()
)
) {
if (checker.isTypeAssignableTo(t, checker.getNumberType()))
return `(\\${node.parameters.map(handleNode).join(" ")} -> ${handleNode(
node.body
)} /= 0)`;
}
return handleNode(node);
};
return handleNode;
}