UNPKG

@kipper/target-js

Version:

The JavaScript target for the Kipper compiler 🦊

686 lines (599 loc) • 25.7 kB
/** * The JavaScript target-specific code generator for translating Kipper code into JavaScript. * @since 0.10.0 */ import { ComparativeExpressionSemantics, LogicalExpressionSemantics, TranslatedCodeLine, TranslatedExpression, AdditiveExpression, AssignmentExpression, BoolPrimaryExpression, CastOrConvertExpression, ComparativeExpression, ConditionalExpression, EqualityExpression, Expression, ExpressionStatement, FStringPrimaryExpression, FunctionCallExpression, FunctionDeclaration, GenericTypeSpecifierExpression, IdentifierPrimaryExpression, IdentifierTypeSpecifierExpression, IncrementOrDecrementPostfixExpression, IncrementOrDecrementUnaryExpression, JumpStatement, KipperProgramContext, ArrayLiteralPrimaryExpression, LogicalAndExpression, LogicalExpression, LogicalOrExpression, MultiplicativeExpression, NumberPrimaryExpression, OperatorModifiedUnaryExpression, ParameterDeclaration, RelationalExpression, ReturnStatement, StringPrimaryExpression, SwitchStatement, TangledPrimaryExpression, TypeofTypeSpecifierExpression, VariableDeclaration, } from "@kipper/core"; import { CompoundStatement, DoWhileLoopStatement, ForLoopStatement, getConversionFunctionIdentifier, IfStatement, KipperTargetCodeGenerator, MemberAccessExpression, ScopeDeclaration, ScopeFunctionDeclaration, VoidOrNullOrUndefinedPrimaryExpression, WhileLoopStatement, } from "@kipper/core"; import { createJSFunctionSignature, getJSFunctionSignature, indentLines, removeBraces } from "./tools"; import { TargetJS, version } from "./index"; function removeBrackets(lines: Array<TranslatedCodeLine>) { return lines.slice(1, lines.length - 1); } /** * The JavaScript target-specific code generator for translating Kipper code into JavaScript. * @since 0.10.0 */ export class JavaScriptTargetCodeGenerator extends KipperTargetCodeGenerator { /** * Code generation function, which is called at the start of a translation and generates * the dependencies for a file in the target language. * * This should be only used to set up a Kipper file in the target language and not as a * replacement to {@link KipperTargetBuiltInGenerator}. * @since 0.10.0 */ setUp = async (programCtx: KipperProgramContext): Promise<Array<TranslatedCodeLine>> => { return [ [`/* Generated from '${programCtx.fileName}' by the Kipper Compiler v${version} */`], // Always enable strict mode when using Kipper ['"use strict"', ";"], // Determine the global scope in the JS execution environment ["// @ts-ignore"], [ 'var __kipperGlobalScope = typeof __kipperGlobalScope !== "undefined" ? __kipperGlobalScope : typeof' + ' globalThis !== "undefined" ?' + " globalThis : typeof" + ' window !== "undefined" ?' + ' window : typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : {}', ";", ], // Create global kipper object - Always prefer the global '__kipper' instance ["// @ts-ignore"], ["var __kipper = __kipperGlobalScope.__kipper = __kipperGlobalScope.__kipper || __kipper || {}", ";"], // The following error classes are simply used for errors thrown in internal Kipper functions and should be used // when the user code uses a Kipper-specific feature, syntax or function incorrectly. [ '__kipper.TypeError = __kipper.TypeError || (class KipperTypeError extends TypeError { constructor(msg) { super(msg); this.name="TypeError"; }})', ";", ], [ '__kipper.IndexError = __kipper.IndexError || (class KipperIndexError extends Error { constructor(msg) { super(msg); this.name="IndexError"; }})', ";", ], ]; }; /** * Code generation function, which is called at the end of a translation and should wrap * up a program in the target language. * * This should be only used to add additional items to finish a Kipper file in the target * language and not as a replacement to {@link KipperTargetBuiltInGenerator}. * @since 0.10.0 */ wrapUp = async (programCtx: KipperProgramContext): Promise<Array<TranslatedCodeLine>> => { return []; }; /** * Translates a {@link CompoundStatement} into the JavaScript language. */ compoundStatement = async (node: CompoundStatement): Promise<Array<TranslatedCodeLine>> => { let blockItem: Array<TranslatedCodeLine> = []; for (let child of node.children) { const childCode = await child.translateCtxAndChildren(); blockItem = blockItem.concat(childCode); } return [["{"], ...indentLines(blockItem), ["}"]]; }; /** * Translates a {@link IfStatement} into the JavaScript language. * * Implementation notes: * - This algorithm is indirectly recursive, as else-if statements are handling like else statements with an immediate * if statement in them. * - The formatting algorithm tries to start at the top and slowly go down each area of the abstract syntax tree. * First the starting 'if' will be formatted, and afterwards the alternative branches are processed if they exists. * If they do, it is also formatted like with a regular starting 'if', unless there is another nested if-statement * in which case it will pass that job down to the child if-statement. * @since 0.10.0 */ ifStatement = async (node: IfStatement): Promise<Array<TranslatedCodeLine>> => { const semanticData = node.getSemanticData(); // Core items, which will be always present let condition = await semanticData.condition.translateCtxAndChildren(); let statement = await semanticData.ifBranch.translateCtxAndChildren(); if (semanticData.ifBranch instanceof CompoundStatement) { statement = removeBrackets(statement); // remove brackets -> will be added later } else { statement = indentLines(statement); // Apply indent to the single statement } let baseCode = [ ["if", " ", "(", ...condition, ")", " ", "{"], ...statement, // Statement, which is executed if the first condition is true ["}", " "], ]; // If there is no alternative branch, return with this code if (!semanticData.elseBranch) { return baseCode; } let secondBranchIsCompoundStatement = semanticData.elseBranch instanceof CompoundStatement; let secondBranchIsElseIf = semanticData.elseBranch instanceof IfStatement; let secondBranchIsElse = !secondBranchIsElseIf; let secondCondition: Array<string> | null = null; let secondBranch: Array<TranslatedCodeLine> | null = null; // Evaluate the alternative branch if it exists if (semanticData.elseBranch) { secondBranch = await semanticData.elseBranch.translateCtxAndChildren(); if (secondBranchIsElseIf) { // Else if statement // Move 'if' condition into the else line -> 'else if (condition)' secondCondition = ["else", " ", ...secondBranch[0]]; secondBranch = secondBranch.slice(1, secondBranch.length); } else { // Else statement secondCondition = ["else"]; } if (secondBranchIsCompoundStatement) { // Format code and remove brackets from compound statements if they exist secondBranch = removeBrackets(secondBranch); } else if (secondBranchIsElse) { // If the second branch is else, then the end of this branch of the AST was reached secondBranch = indentLines(secondBranch); } } // Return with the second branch added. (Since the function calls itself indirectly recursively, there can be as // many else-ifs as the user wants.) return [ ...baseCode.slice(0, baseCode.length - 1), // Add all lines except the last one that ends the if-statement ["}", " ", ...(secondCondition ?? []), ...(secondBranchIsCompoundStatement ? [" ", "{"] : [])], ...(secondBranch ?? []), // Else-if/else statement, which is executed if the second condition is true ...(secondBranchIsCompoundStatement ? [["}", " "]] : []), ]; }; /** * Translates a {@link SwitchStatement} into the JavaScript language. * * @since 0.10.0 */ switchStatement = async (node: SwitchStatement): Promise<Array<TranslatedCodeLine>> => { return []; }; /** * Translates a {@link ExpressionStatement} into the JavaScript language. */ expressionStatement = async (node: ExpressionStatement): Promise<Array<TranslatedCodeLine>> => { let exprCode: Array<TranslatedCodeLine> = []; for (let child of node.children) { // Expression lists (expression statements) will be evaluated per each expression, meaning every expression // can be considered a single line of code. const childCode = await child.translateCtxAndChildren(); exprCode.push(childCode.concat(";")); } return exprCode; }; /** * Translates a {@link DoWhileLoopStatement} into the JavaScript language. * @since 0.10.0 */ doWhileLoopStatement = async (node: DoWhileLoopStatement): Promise<Array<TranslatedCodeLine>> => { return []; }; /** * Translates a {@link WhileLoopStatement} into the JavaScript language. * @since 0.10.0 */ whileLoopStatement = async (node: WhileLoopStatement): Promise<Array<TranslatedCodeLine>> => { const semanticData = node.getSemanticData(); const condition = await semanticData.loopCondition.translateCtxAndChildren(); const statement = await semanticData.loopBody.translateCtxAndChildren(); // Check whether the loop body is a compound statement const isCompound = semanticData.loopBody instanceof CompoundStatement; return [ ["while", " ", "(", ...condition, ")", " ", isCompound ? "{" : ""], ...(isCompound ? removeBraces(statement) : indentLines(statement)), [isCompound ? "}" : ""], ]; }; /** * Translates a {@link ForLoopStatement} into the JavaScript language. * @since 0.10.0 */ forLoopStatement = async (node: ForLoopStatement): Promise<Array<TranslatedCodeLine>> => { const semanticData = node.getSemanticData(); // Translate the parts of the for loop statement - Everything except the loop body is optional let forDeclaration: TranslatedExpression | Array<TranslatedCodeLine> = semanticData.forDeclaration ? await semanticData.forDeclaration.translateCtxAndChildren() : []; const condition: TranslatedExpression = semanticData.loopCondition ? await semanticData.loopCondition.translateCtxAndChildren() : []; const forIterationExp: TranslatedExpression = semanticData.forIterationExp ? await semanticData.forIterationExp.translateCtxAndChildren() : []; // Apply formatting for the loop body (compound statements are formatted differently) let isCompound = semanticData.loopBody instanceof CompoundStatement; let loopBody = await semanticData.loopBody.translateCtxAndChildren(); if (isCompound) { loopBody = removeBrackets(loopBody); // remove brackets -> will be added later } else { loopBody = indentLines(loopBody); // Indent the loop body } // Ensure the variable declaration in the for declaration is properly included in the output code forDeclaration = <TranslatedExpression>( // Variable declarations are already translated as a single line of code and have a semicolon at the end -> // We need to ensure that the semicolon is not added twice (semanticData.forDeclaration instanceof VariableDeclaration ? forDeclaration[0] : [...forDeclaration, ";"]) ); return [ [ "for", " ", "(", ...forDeclaration, " ", ...condition, ";", " ", ...forIterationExp, ")", " ", ...(isCompound ? ["{"] : []), ], ...loopBody, isCompound ? ["}"] : [], ]; }; /** * Translates a {@link JumpStatement} into the JavaScript language. */ jumpStatement = async (node: JumpStatement): Promise<Array<TranslatedCodeLine>> => { const semanticData = node.getSemanticData(); return [semanticData.jmpType === "break" ? ["break", ";"] : ["continue", ";"]]; }; /** * Translates a {@link ReturnStatement} into the JavaScript language. * @since 0.10.0 */ returnStatement = async (node: ReturnStatement): Promise<Array<TranslatedCodeLine>> => { const semanticData = node.getSemanticData(); const returnValue = await semanticData.returnValue?.translateCtxAndChildren(); return [["return", ...(returnValue ? [" ", ...returnValue] : []), ";"]]; }; /** * Translates a {@link ParameterDeclaration} into the JavaScript language. */ parameterDeclaration = async (node: ParameterDeclaration): Promise<Array<TranslatedCodeLine>> => { return []; }; /** * Translates a {@link FunctionDeclaration} into the JavaScript language. */ functionDeclaration = async (node: FunctionDeclaration): Promise<Array<TranslatedCodeLine>> => { const semanticData = node.getSemanticData(); // Function signature and body const signature = getJSFunctionSignature(node); const functionBody = await semanticData.functionBody.translateCtxAndChildren(); // Define the function signature and its body. We will simply use 'console.log(msg)' for printing out IO. return [[createJSFunctionSignature(signature)], ...functionBody]; }; /** * Translates a {@link VariableDeclaration} into the JavaScript language. */ variableDeclaration = async (node: VariableDeclaration): Promise<Array<TranslatedCodeLine>> => { const semanticData = node.getSemanticData(); const storage = semanticData.storageType === "const" ? "const" : "let"; const assign = semanticData.value ? await semanticData.value.translateCtxAndChildren() : []; // Only add ' = EXP' if assignValue is defined return [[storage, " ", semanticData.identifier, ...(assign.length > 0 ? [" ", "=", " ", ...assign] : []), ";"]]; }; /** * Translates a {@link NumberPrimaryExpression} into the JavaScript language. */ numberPrimaryExpression = async (node: NumberPrimaryExpression): Promise<TranslatedExpression> => { const semanticData = node.getSemanticData(); return [ semanticData.value, // Simply get the constant value ]; }; /** * Translates a {@link ArrayLiteralPrimaryExpression} into the JavaScript language. */ arrayLiteralExpression = async (node: ArrayLiteralPrimaryExpression): Promise<TranslatedExpression> => { return []; }; /** * Translates a {@link IdentifierPrimaryExpression} into the JavaScript language. */ identifierPrimaryExpression = async (node: IdentifierPrimaryExpression): Promise<TranslatedExpression> => { const semanticData = node.getSemanticData(); let identifier: string = semanticData.identifier; // If the identifier is not declared by the user, assume it's a built-in function and format the identifier // accordingly. if (!(semanticData.ref.refTarget instanceof ScopeDeclaration)) { identifier = TargetJS.getBuiltInIdentifier(semanticData.ref.refTarget); } return [identifier]; }; /** * Translates a {@link IdentifierPrimaryExpression} into the JavaScript language. * @since 0.10.0 */ memberAccessExpression = async (node: MemberAccessExpression): Promise<TranslatedExpression> => { const semanticData = node.getSemanticData(); const object = await semanticData.objectLike.translateCtxAndChildren(); switch (<"dot" | "bracket" | "slice">semanticData.accessType) { case "dot": return []; // TODO: Not implemented case "bracket": { // -> The member access is done via brackets, meaning the member name is an expression // In this case, only indexes are allowed, not keys, but in the future, this will change with the implementation // of objects. const keyOrIndex = await (<Expression>semanticData.propertyIndexOrKeyOrSlice).translateCtxAndChildren(); // Return the member access expression in form of a function call to the internal 'index' function const sliceIdentifier = TargetJS.getBuiltInIdentifier("index"); return [sliceIdentifier, "(", ...object, ", ", ...keyOrIndex, ")"]; } case "slice": { // -> The member access is done via a slice, meaning the member name is a slice expression const slice = <{ start?: Expression; end?: Expression }>semanticData.propertyIndexOrKeyOrSlice; // Translate the start and end expression, if they exist. // If they don't, simply undefined will be passed onto the underlying function const start = slice.start ? await slice.start.translateCtxAndChildren() : "undefined"; const end = slice.end ? await slice.end.translateCtxAndChildren() : "undefined"; // Return the slice expression in form of a function call to the internal 'slice' function const sliceIdentifier = TargetJS.getBuiltInIdentifier("slice"); return [sliceIdentifier, "(", ...object, ", ", ...start, ", ", ...end, ")"]; } } }; /** * Translates a {@link IdentifierTypeSpecifierExpression} into the JavaScript language. */ identifierTypeSpecifierExpression = async ( node: IdentifierTypeSpecifierExpression, ): Promise<TranslatedExpression> => { return []; }; /** * Translates a {@link GenericTypeSpecifierExpression} into the JavaScript language. */ genericTypeSpecifierExpression = async (node: GenericTypeSpecifierExpression): Promise<TranslatedExpression> => { return []; }; /** * Translates a {@link TypeofTypeSpecifierExpression} into the JavaScript language. */ typeofTypeSpecifierExpression = async (node: TypeofTypeSpecifierExpression): Promise<TranslatedExpression> => { return []; }; /** * Translates a {@link StringPrimaryExpression} into the JavaScript language. */ stringPrimaryExpression = async (node: StringPrimaryExpression): Promise<TranslatedExpression> => { const semanticData = node.getSemanticData(); return [`${semanticData.quotationMarks}${semanticData.value}${semanticData.quotationMarks}`]; }; /** * Translates a {@link FStringPrimaryExpression} into the JavaScript language. */ fStringPrimaryExpression = async (node: FStringPrimaryExpression): Promise<TranslatedExpression> => { return []; }; /** * Translates a {@link BoolPrimaryExpression} into the JavaScript language. */ boolPrimaryExpression = async (node: BoolPrimaryExpression): Promise<TranslatedExpression> => { return [node.getSemanticData().value]; }; /** * Translates a {@link TangledPrimaryExpression} into the JavaScript language. */ tangledPrimaryExpression = async (node: TangledPrimaryExpression): Promise<TranslatedExpression> => { // Tangled expressions always contain only a single child (Enforced by Parser) return ["(", ...(await node.children[0].translateCtxAndChildren()), ")"]; }; /** * Translates a {@link IncrementOrDecrementPostfixExpression} into the JavaScript language. */ voidOrNullOrUndefinedPrimaryExpression = async ( node: VoidOrNullOrUndefinedPrimaryExpression, ): Promise<TranslatedExpression> => { const constantIdentifier = node.getSemanticData().constantIdentifier; return [constantIdentifier === "void" ? "void(0)" : constantIdentifier]; }; /** * Translates a {@link IncrementOrDecrementPostfixExpression} into the JavaScript language. */ incrementOrDecrementPostfixExpression = async ( node: IncrementOrDecrementPostfixExpression, ): Promise<TranslatedExpression> => { const semanticData = node.getSemanticData(); const operandCode = await semanticData.operand.translateCtxAndChildren(); return [...operandCode, semanticData.operator]; }; /** * Translates a {@link FunctionCallExpression} into the JavaScript language. */ functionCallExpression = async (node: FunctionCallExpression): Promise<TranslatedExpression> => { // Get the function and semantic data const semanticData = node.getSemanticData(); const func = node.getTypeSemanticData().func; // Get the proper identifier for the function const identifier = func instanceof ScopeFunctionDeclaration ? func.identifier : TargetJS.getBuiltInIdentifier(func.identifier); // Generate the arguments let args: TranslatedExpression = []; for (const i of semanticData.args) { const arg = await i.translateCtxAndChildren(); args = args.concat(arg.concat(", ")); } args = args.slice(0, -1); // Removing last whitespace and comma before the closing parenthesis // Return the compiled function call return [identifier, "(", ...args, ")"]; }; /** * Translates a {@link IncrementOrDecrementUnaryExpression} into the JavaScript language. */ incrementOrDecrementUnaryExpression = async ( node: IncrementOrDecrementUnaryExpression, ): Promise<TranslatedExpression> => { const semanticData = node.getSemanticData(); const operandCode = await semanticData.operand.translateCtxAndChildren(); return [semanticData.operator, ...operandCode]; }; /** * Translates a {@link OperatorModifiedUnaryExpression} into the JavaScript language. */ operatorModifiedUnaryExpression = async (node: OperatorModifiedUnaryExpression): Promise<TranslatedExpression> => { // Get the semantic data const semanticData = node.getSemanticData(); // Get the operator and the operand const operator: string = semanticData.operator; const operand: Expression = semanticData.operand; // Return the generated unary expression return [operator].concat(await operand.translateCtxAndChildren()); }; /** * Translates a {@link CastOrConvertExpression} into the JavaScript language. */ castOrConvertExpression = async (node: CastOrConvertExpression): Promise<TranslatedExpression> => { const semanticData = node.getSemanticData(); const typeData = node.getTypeSemanticData(); const exp: TranslatedExpression = await semanticData.exp.translateCtxAndChildren(); const originalType = semanticData.exp.getTypeSemanticData().evaluatedType.getCompilableType(); const destType = typeData.castType.getCompilableType(); if (originalType === destType) { // If both types are the same we will only return the translated expression to avoid useless conversions. return exp; } else { const func: string = TargetJS.getBuiltInIdentifier(getConversionFunctionIdentifier(originalType, destType)); return [func, "(", ...exp, ")"]; } }; /** * Translates a {@link MultiplicativeExpression} into the JavaScript language. */ multiplicativeExpression = async (node: MultiplicativeExpression): Promise<TranslatedExpression> => { // Get the semantic data const semanticData = node.getSemanticData(); const exp1: TranslatedExpression = await semanticData.leftOp.translateCtxAndChildren(); const exp2: TranslatedExpression = await semanticData.rightOp.translateCtxAndChildren(); return [...exp1, " ", semanticData.operator, " ", ...exp2]; }; /** * Translates a {@link AdditiveExpression} into the JavaScript language. */ additiveExpression = async (node: AdditiveExpression): Promise<TranslatedExpression> => { // Get the semantic data const semanticData = node.getSemanticData(); const exp1: TranslatedExpression = await semanticData.leftOp.translateCtxAndChildren(); const exp2: TranslatedExpression = await semanticData.rightOp.translateCtxAndChildren(); return [...exp1, " ", semanticData.operator, " ", ...exp2]; }; /** * Translates any form of operator-based expression with two operands into the JavaScript language. * @param node The node to translate. * @since 0.10.0 * @private */ protected translateOperatorExpressionWithOperands = async ( node: ComparativeExpression | LogicalExpression, ): Promise<TranslatedExpression> => { // Get the semantic data const semanticData: ComparativeExpressionSemantics | LogicalExpressionSemantics = node.getSemanticData(); // Generate the code for the operands const exp1: TranslatedExpression = await semanticData.leftOp.translateCtxAndChildren(); const exp2: TranslatedExpression = await semanticData.rightOp.translateCtxAndChildren(); let operator: string = semanticData.operator; // Make sure equality checks are done with ===/!== and not ==/!= to ensure strict equality if (operator === "==" || operator === "!=") { operator = semanticData.operator + "="; } return [...exp1, " ", operator, " ", ...exp2]; }; /** * Translates a {@link RelationalExpression} into the JavaScript language. */ relationalExpression = async (node: RelationalExpression): Promise<TranslatedExpression> => { return await this.translateOperatorExpressionWithOperands(node); }; /** * Translates a {@link EqualityExpression} into the JavaScript language. */ equalityExpression = async (node: EqualityExpression): Promise<TranslatedExpression> => { return await this.translateOperatorExpressionWithOperands(node); }; /** * Translates a {@link LogicalAndExpression} into the JavaScript language. */ logicalAndExpression = async (node: LogicalAndExpression): Promise<TranslatedExpression> => { return await this.translateOperatorExpressionWithOperands(node); }; /** * Translates a {@link LogicalOrExpression} into the JavaScript language. */ logicalOrExpression = async (node: LogicalOrExpression): Promise<TranslatedExpression> => { return await this.translateOperatorExpressionWithOperands(node); }; /** * Translates a {@link ConditionalExpression} into the JavaScript language. */ conditionalExpression = async (node: ConditionalExpression): Promise<TranslatedExpression> => { return []; }; /** * Translates a {@link AssignmentExpression} into the JavaScript language. */ assignmentExpression = async (node: AssignmentExpression): Promise<TranslatedExpression> => { const semanticData = node.getSemanticData(); let identifier = semanticData.identifier; // If the identifier is not found in the global scope, assume it's a built-in function and format the identifier // accordingly. if (!(semanticData.assignTarget.refTarget instanceof ScopeDeclaration)) { identifier = TargetJS.getBuiltInIdentifier(identifier); } // The expression that is assigned to the reference const assignExp = await semanticData.value.translateCtxAndChildren(); return [identifier, " ", semanticData.operator, " ", ...assignExp]; }; }