import { compileCall as compileBuiltinCall } from "./builtins"; import { PATH_DELIMITER } from "./constants"; import { DiagnosticCode, DiagnosticEmitter } from "./diagnostics"; import { Module, MemorySegment, ExpressionRef, UnaryOp, BinaryOp, NativeType, FunctionTypeRef, getExpressionId, ExpressionId } from "./module"; import { Program, ClassPrototype, Class, Element, ElementKind, Enum, FunctionPrototype, Function, Global, Local, Namespace, Parameter } from "./program"; import { I64, U64, sb } from "./util"; import { Token } from "./tokenizer"; import { Node, NodeKind, TypeNode, TypeParameter, Source, // statements BlockStatement, BreakStatement, ClassDeclaration, ContinueStatement, DeclarationStatement, DoStatement, EmptyStatement, EnumDeclaration, EnumValueDeclaration, ExportMember, ExportStatement, ExpressionStatement, FieldDeclaration, FunctionDeclaration, ForStatement, IfStatement, ImportStatement, MethodDeclaration, ModifierKind, NamespaceDeclaration, ReturnStatement, Statement, SwitchCase, SwitchStatement, ThrowStatement, TryStatement, VariableLikeDeclarationStatement, VariableDeclaration, VariableStatement, WhileStatement, // expressions ArrayLiteralExpression, AssertionExpression, BinaryExpression, CallExpression, ElementAccessExpression, Expression, FloatLiteralExpression, IdentifierExpression, IntegerLiteralExpression, LiteralExpression, LiteralKind, NewExpression, ParenthesizedExpression, PropertyAccessExpression, TernaryExpression, StringLiteralExpression, UnaryPostfixExpression, UnaryPrefixExpression, hasModifier } from "./ast"; import { Type, TypeKind, } from "./types"; export enum Target { /** WebAssembly with 32-bit pointers. */ WASM32, /** WebAssembly with 64-bit pointers. Experimental and not supported by any runtime yet. */ WASM64 } export class Options { /** WebAssembly target. Defaults to {@link Target.WASM32}. */ target: Target = Target.WASM32; /** If true, performs compilation as usual but doesn't produce any output (all calls to module become nops). */ noEmit: bool = false; /** If true, compiles everything instead of just reachable code. */ noTreeShaking: bool = false; /** If true, replaces assertions with nops. */ noDebug: bool = false; } const enum ConversionKind { /** No conversion. */ NONE, /** Implicit conversion. */ IMPLICIT, /** Explicit conversion. */ EXPLICIT } export class Compiler extends DiagnosticEmitter { /** Program reference. */ program: Program; /** Provided options. */ options: Options; /** Module instance being compiled. */ module: Module; /** Start function being compiled. */ startFunction: Function; /** Start function expressions. */ startFunctionBody: ExpressionRef[] = new Array(); /** Current type in compilation. */ currentType: Type = Type.void; /** Current function in compilation. */ currentFunction: Function; /** Marker indicating whether continue statements are allowed in the current break context. */ disallowContinue: bool = true; /** Marker indicating that a new variable, if present, is always a local. Used to distinguish locals from globals in the start function. */ variableIsLocal: bool = false; /** Counting memory offset. */ memoryOffset: U64 = new U64(8, 0); // leave space for (any size of) NULL /** Memory segments being compiled. */ memorySegments: MemorySegment[] = new Array(); /** Already processed file names. */ files: Set = new Set(); /** Compiles a {@link Program} to a {@link Module} using the specified options. */ static compile(program: Program, options: Options | null = null): Module { const compiler: Compiler = new Compiler(program, options); return compiler.compile(); } /** Constructs a new compiler for a {@link Program} using the specified options. */ constructor(program: Program, options: Options | null = null) { super(program.diagnostics); this.program = program; this.options = options ? options : new Options(); this.module = this.options.noEmit ? Module.createStub() : Module.create(); const startFunctionTemplate: FunctionPrototype = new FunctionPrototype(program, "start", null); const startFunctionInstance: Function = new Function(startFunctionTemplate, startFunctionTemplate.internalName, [], [], Type.void, null); this.currentFunction = this.startFunction = startFunctionInstance; this.memoryOffset = new U64(2 * (this.options.target == Target.WASM64 ? 8 : 4), 0); // leave space for `null` and heapStart (both of usize type) } /** Performs compilation of the underlying {@link Program} to a {@link Module}. */ compile(): Module { const program: Program = this.program; // initialize lookup maps, built-ins, imports, exports, etc. program.initialize(this.options.target); // compile entry file (exactly one, usually) const sources: Source[] = program.sources; let i: i32, k: i32 = sources.length; for (i = 0; i < k; ++i) { const source: Source = sources[i]; if (source.isEntry) this.compileSource(source); } // make start function if not empty if (this.startFunctionBody.length) { let typeRef: FunctionTypeRef = this.module.getFunctionTypeBySignature(NativeType.None, []); if (!typeRef) typeRef = this.module.addFunctionType("v", NativeType.None, []); this.module.setStart( this.module.addFunction(this.startFunction.template.internalName, typeRef, typesToNativeTypes(this.startFunction.additionalLocals), this.module.createBlock(null, this.startFunctionBody)) ); } // set up memory // store heapStart at `sizeof()` (that is right after `null`) as an usize const initial: U64 = this.memoryOffset.clone(); let heapStartBuffer: Uint8Array; let heapStartOffset: i32; if (this.options.target == Target.WASM64) { heapStartBuffer = new Uint8Array(8); heapStartOffset = 8; heapStartBuffer[0] = (initial.lo ) as u8; heapStartBuffer[1] = (initial.lo >>> 8) as u8; heapStartBuffer[2] = (initial.lo >>> 16) as u8; heapStartBuffer[3] = (initial.lo >>> 24) as u8; heapStartBuffer[4] = (initial.hi ) as u8; heapStartBuffer[5] = (initial.hi >>> 8) as u8; heapStartBuffer[6] = (initial.hi >>> 16) as u8; heapStartBuffer[7] = (initial.hi >>> 24) as u8; } else { if (!initial.fitsInU32) throw new Error("static memory size overflows 32 bits"); heapStartBuffer = new Uint8Array(4); heapStartOffset = 4; heapStartBuffer[0] = (initial.lo ) as u8; heapStartBuffer[1] = (initial.lo >>> 8) as u8; heapStartBuffer[2] = (initial.lo >>> 16) as u8; heapStartBuffer[3] = (initial.lo >>> 24) as u8; } this.memorySegments.push(MemorySegment.create(heapStartBuffer, new U64(heapStartOffset, 0))); // TODO: use a global instead? // determine initial page size const initialOverlaps: U64 = initial.clone(); initialOverlaps.and32(0xffff); if (!initialOverlaps.isZero) { initial.or32(0xffff); initial.add32(1); } initial.shru32(16); // initial size in 64k pages this.module.setMemory(initial.toI32(), Module.MAX_MEMORY_WASM32 /* TODO: not WASM64 compatible yet */, this.memorySegments, this.options.target, "memory"); return this.module; } // sources compileSourceByPath(normalizedPath: string, reportNode: Node): void { for (let i: i32 = 0, k: i32 = this.program.sources.length; i < k; ++i) { const importedSource: Source = this.program.sources[i]; if (importedSource.normalizedPath == normalizedPath) { this.compileSource(importedSource); return; } } this.error(DiagnosticCode.File_0_not_found, reportNode.range, normalizedPath); } compileSource(source: Source): void { if (this.files.has(source.normalizedPath)) return; this.files.add(source.normalizedPath); const isEntry: bool = source.isEntry; const noTreeShaking: bool = this.options.noTreeShaking; for (let i: i32 = 0, k: i32 = source.statements.length; i < k; ++i) { const statement: Statement = source.statements[i]; switch (statement.kind) { case NodeKind.CLASS: if ((noTreeShaking || isEntry && hasModifier(ModifierKind.EXPORT, (statement).modifiers)) && !(statement).typeParameters.length) this.compileClassDeclaration(statement, []); break; case NodeKind.ENUM: if (noTreeShaking || isEntry && hasModifier(ModifierKind.EXPORT, (statement).modifiers)) this.compileEnumDeclaration(statement); break; case NodeKind.FUNCTION: if ((noTreeShaking || isEntry && hasModifier(ModifierKind.EXPORT, (statement).modifiers)) && !(statement).typeParameters.length) this.compileFunctionDeclaration(statement, []); break; case NodeKind.IMPORT: this.compileSourceByPath((statement).normalizedPath, (statement).path); break; case NodeKind.NAMESPACE: if (noTreeShaking || isEntry && hasModifier(ModifierKind.EXPORT, (statement).modifiers)) this.compileNamespaceDeclaration(statement); break; case NodeKind.VARIABLE: if (noTreeShaking || isEntry && hasModifier(ModifierKind.EXPORT, (statement).modifiers)) this.compileVariableStatement(statement); break; case NodeKind.EXPORT: if ((statement).normalizedPath != null) this.compileSourceByPath((statement).normalizedPath, (statement).path); if (noTreeShaking || isEntry) this.compileExportStatement(statement); break; // otherwise a top-level statement that is part of the start function's body default: { const previousFunction: Function = this.currentFunction; this.currentFunction = this.startFunction; this.startFunctionBody.push(this.compileStatement(statement)); this.currentFunction = previousFunction; break; } } } } // globals compileGlobalDeclaration(declaration: VariableDeclaration, isConst: bool): Global | null { const element: Element | null = this.program.elements.get(declaration.internalName); if (!element || element.kind != ElementKind.GLOBAL) throw new Error("unexpected missing global"); if (!this.compileGlobal(element)) return null; if (declaration.range.source.isEntry && (declaration.parent).parent == declaration.range.source && hasModifier(ModifierKind.EXPORT, declaration.modifiers)) { if (!(element).isCompiledMutable) this.module.addGlobalExport(element.internalName, declaration.identifier.name); else this.warning(DiagnosticCode.Cannot_export_a_mutable_global, declaration.range); } return element; } compileGlobal(element: Global): bool { if (element.isCompiled) return true; const declaration: VariableLikeDeclarationStatement | null = element.declaration; let type: Type | null = element.type; if (!type) { if (!declaration) throw new Error("unexpected missing declaration"); if (!declaration.type) return false; // TODO: infer type? currently reported by parser type = this.program.resolveType(declaration.type); // reports if (!type) return false; element.type = type; } if (this.module.noEmit) return true; const nativeType: NativeType = typeToNativeType(type); let initializer: ExpressionRef; let initializeInStart: bool; if (element.hasConstantValue) { if (type.isLongInteger) initializer = element.constantIntegerValue ? this.module.createI64(element.constantIntegerValue.lo, element.constantIntegerValue.hi) : this.module.createI64(0, 0); else if (type.kind == TypeKind.F32) initializer = this.module.createF32(element.constantFloatValue); else if (type.kind == TypeKind.F64) initializer = this.module.createF64(element.constantFloatValue); else if (type.isSmallInteger) { if (type.isSignedInteger) { const shift: i32 = type.smallIntegerShift; initializer = this.module.createI32(element.constantIntegerValue ? element.constantIntegerValue.toI32() << shift >> shift : 0); } else initializer = this.module.createI32(element.constantIntegerValue ? element.constantIntegerValue.toI32() & type.smallIntegerMask: 0); } else initializer = this.module.createI32(element.constantIntegerValue ? element.constantIntegerValue.toI32() : 0); initializeInStart = false; } else if (declaration) { if (declaration.initializer) { initializer = this.compileExpression(declaration.initializer, type); initializeInStart = getExpressionId(initializer) != ExpressionId.Const; // MVP doesn't support complex initializers } else { initializer = typeToNativeZero(this.module, type); initializeInStart = false; } } else throw new Error("unexpected missing declaration or constant value"); const internalName: string = element.internalName; if (initializeInStart) { this.module.addGlobal(internalName, nativeType, true, typeToNativeZero(this.module, type)); this.startFunctionBody.push(this.module.createSetGlobal(internalName, initializer)); element.isCompiledMutable = true; } else { this.module.addGlobal(internalName, nativeType, element.isMutable, initializer); element.isCompiledMutable = element.isMutable; } return element.isCompiled = true; } // enums compileEnumDeclaration(declaration: EnumDeclaration): void { const element: Element | null = this.program.elements.get(declaration.internalName); if (!element || element.kind != ElementKind.ENUM) throw new Error("unexpected missing enum"); this.compileEnum(element); } compileEnum(element: Enum): void { if (element.isCompiled) return; let previousInternalName: string | null = null; for (let [key, val] of element.members) { if (val.hasConstantValue) { this.module.addGlobal(val.internalName, NativeType.I32, false, this.module.createI32(val.constantValue)); } else if (val.declaration) { const declaration: EnumValueDeclaration = val.declaration; let initializer: ExpressionRef; let initializeInStart: bool = false; if (declaration.value) { initializer = this.compileExpression(declaration.value, Type.i32); initializeInStart = getExpressionId(initializer) != ExpressionId.Const; // MVP doesn't support complex initializers } else if (previousInternalName == null) { initializer = this.module.createI32(0); initializeInStart = false; } else { initializer = this.module.createBinary(BinaryOp.AddI32, this.module.createGetGlobal(previousInternalName, NativeType.I32), this.module.createI32(1) ); initializeInStart = true; } if (initializeInStart) { this.module.addGlobal(val.internalName, NativeType.I32, true, this.module.createI32(0)); this.startFunctionBody.push(this.module.createSetGlobal(val.internalName, initializer)); } else { this.module.addGlobal(val.internalName, NativeType.I32, false, initializer); // TODO: check export, requires updated binaryen.js with Module#addGlobalExport } } else throw new Error("unexpected missing declaration or constant value"); previousInternalName = val.internalName; } element.isCompiled = true; } // functions compileFunctionDeclaration(declaration: FunctionDeclaration, typeArguments: TypeNode[], contextualTypeArguments: Map | null = null, alternativeReportNode: Node | null = null): void { const internalName: string = declaration.internalName; const element: Element | null = this.program.elements.get(internalName); if (!element || element.kind != ElementKind.FUNCTION_PROTOTYPE) throw new Error("unexpected missing function"); const instance: Function | null = this.compileFunctionUsingTypeArguments(element, typeArguments, contextualTypeArguments, alternativeReportNode); if (!instance) return; if (declaration.range.source.isEntry && declaration.parent == declaration.range.source && hasModifier(ModifierKind.EXPORT, declaration.modifiers)) this.module.addFunctionExport(instance.internalName, declaration.identifier.name); } compileFunctionUsingTypeArguments(prototype: FunctionPrototype, typeArguments: TypeNode[], contextualTypeArguments: Map | null = null, alternativeReportNode: Node | null = null): Function | null { const instance: Function | null = prototype.resolveInclTypeArguments(typeArguments, contextualTypeArguments, alternativeReportNode); // reports if (!instance) return null; return this.compileFunction(instance) ? instance : null; } compileFunction(instance: Function): bool { if (instance.isCompiled) return true; const declaration: FunctionDeclaration | null = instance.template.declaration; if (!declaration) throw new Error("unexpected missing declaration"); if (instance.isDeclare) { if (declaration.statements) { this.error(DiagnosticCode.An_implementation_cannot_be_declared_in_ambient_contexts, declaration.identifier.range); return false; } } else { if (!declaration.statements) { this.error(DiagnosticCode.Function_implementation_is_missing_or_not_immediately_following_the_declaration, declaration.identifier.range); return false; } } instance.isCompiled = true; // compile statements let stmts: ExpressionRef[] | null = null; if (!instance.isDeclare) { const previousFunction: Function = this.currentFunction; this.currentFunction = instance; stmts = this.compileStatements(declaration.statements); this.currentFunction = previousFunction; } // create the function type let k: i32 = instance.parameters.length; const binaryenResultType: NativeType = typeToNativeType(instance.returnType); const binaryenParamTypes: NativeType[] = new Array(k); const signatureNameParts: string[] = new Array(k + 1); for (let i: i32 = 0; i < k; ++i) { binaryenParamTypes[i] = typeToNativeType(instance.parameters[i].type); signatureNameParts[i] = typeToSignatureNamePart(instance.parameters[i].type); } signatureNameParts[k] = typeToSignatureNamePart(instance.returnType); let typeRef: FunctionTypeRef = this.module.getFunctionTypeBySignature(binaryenResultType, binaryenParamTypes); if (!typeRef) typeRef = this.module.addFunctionType(signatureNameParts.join(""), binaryenResultType, binaryenParamTypes); // create the function const internalName: string = instance.internalName; if (instance.isDeclare) { this.module.addFunctionImport(internalName, "env", declaration.identifier.name, typeRef); } else { this.module.addFunction(internalName, typeRef, typesToNativeTypes(instance.additionalLocals), this.module.createBlock(null, stmts, NativeType.None)); } return true; } // namespaces compileNamespaceDeclaration(declaration: NamespaceDeclaration): void { const members: Statement[] = declaration.members; const noTreeShaking: bool = this.options.noTreeShaking; for (let i: i32 = 0, k: i32 = members.length; i < k; ++i) { const member: Statement = members[i]; switch (member.kind) { case NodeKind.CLASS: if ((noTreeShaking || hasModifier(ModifierKind.EXPORT, (member).modifiers)) && !(member).typeParameters.length) this.compileClassDeclaration(member, []); break; case NodeKind.ENUM: if (noTreeShaking || hasModifier(ModifierKind.EXPORT, (member).modifiers)) this.compileEnumDeclaration(member); break; case NodeKind.FUNCTION: if ((noTreeShaking || hasModifier(ModifierKind.EXPORT, (member).modifiers)) && !(member).typeParameters.length) this.compileFunctionDeclaration(member, []); break; case NodeKind.NAMESPACE: if (noTreeShaking || hasModifier(ModifierKind.EXPORT, (member).modifiers)) this.compileNamespaceDeclaration(member); break; case NodeKind.VARIABLE: if (noTreeShaking || hasModifier(ModifierKind.EXPORT, (member).modifiers)) this.compileVariableStatement(member); break; default: throw new Error("unexpected namespace member"); } } throw new Error("not implemented"); } compileNamespace(ns: Namespace): void { const noTreeShaking: bool = this.options.noTreeShaking; for (let [name, element] of ns.members) { switch (element.kind) { case ElementKind.CLASS_PROTOTYPE: if ((noTreeShaking || (element).isExport) && !(element).isGeneric) this.compileClassUsingTypeArguments(element, []); break; case ElementKind.ENUM: this.compileEnum(element); break; case ElementKind.FUNCTION_PROTOTYPE: if ((noTreeShaking || (element).isExport) && !(element).isGeneric) this.compileFunctionUsingTypeArguments(element, []); break; case ElementKind.GLOBAL: this.compileGlobal(element); break; case ElementKind.NAMESPACE: this.compileNamespace(element); break; } } } // exports compileExportStatement(statement: ExportStatement): void { const members: ExportMember[] = statement.members; for (let i: i32 = 0, k: i32 = members.length; i < k; ++i) { const member: ExportMember = members[i]; const internalExportName: string = statement.range.source.internalPath + PATH_DELIMITER + member.externalIdentifier.name; const element: Element | null = this.program.exports.get(internalExportName); if (!element) // reported in Program#initialize continue; switch (element.kind) { case ElementKind.CLASS_PROTOTYPE: if (!(element).isGeneric) this.compileClassUsingTypeArguments(element, []); break; case ElementKind.ENUM: this.compileEnum(element); break; case ElementKind.FUNCTION_PROTOTYPE: if (!(element).isGeneric) { const functionInstance: Function | null = this.compileFunctionUsingTypeArguments(element, []); if (functionInstance && statement.range.source.isEntry) this.module.addFunctionExport(functionInstance.internalName, member.externalIdentifier.name); } break; case ElementKind.GLOBAL: if (this.compileGlobal(element) && statement.range.source.isEntry) { if (!(element).isCompiledMutable) this.module.addGlobalExport(element.internalName, member.externalIdentifier.name); else this.warning(DiagnosticCode.Cannot_export_a_mutable_global, member.range); } break; case ElementKind.NAMESPACE: this.compileNamespace(element); break; } } } // classes compileClassDeclaration(declaration: ClassDeclaration, typeArguments: TypeNode[], contextualTypeArguments: Map | null = null, alternativeReportNode: Node | null = null): void { const internalName: string = declaration.internalName; const element: Element | null = this.program.elements.get(internalName); if (!element || element.kind != ElementKind.CLASS_PROTOTYPE) throw new Error("unexpected missing class"); this.compileClassUsingTypeArguments(element, typeArguments, contextualTypeArguments, alternativeReportNode); } compileClassUsingTypeArguments(prototype: ClassPrototype, typeArguments: TypeNode[], contextualTypeArguments: Map | null = null, alternativeReportNode: Node | null = null): void { const instance: Class | null = prototype.resolveInclTypeArguments(typeArguments, contextualTypeArguments, alternativeReportNode); if (!instance) return; this.compileClass(instance); } compileClass(cls: Class) { throw new Error("not implemented"); } // memory addMemorySegment(buffer: Uint8Array): MemorySegment { if (this.memoryOffset.lo & 7) { // align to 8 bytes so any possible data type is aligned here this.memoryOffset.or32(7); this.memoryOffset.add32(1); } const segment: MemorySegment = MemorySegment.create(buffer, this.memoryOffset.clone()); this.memorySegments.push(segment); this.memoryOffset.add32(buffer.length); return segment; } // types determineExpressionType(expression: Expression, contextualType: Type): Type { const previousType: Type = this.currentType; const previousNoEmit: bool = this.module.noEmit; this.module.noEmit = true; this.compileExpression(expression, contextualType, ConversionKind.NONE); // now performs a dry run const type: Type = this.currentType; this.currentType = previousType; this.module.noEmit = previousNoEmit; return type; } // statements compileStatement(statement: Statement): ExpressionRef { switch (statement.kind) { case NodeKind.BLOCK: return this.compileBlockStatement(statement); case NodeKind.BREAK: return this.compileBreakStatement(statement); case NodeKind.CONTINUE: return this.compileContinueStatement(statement); case NodeKind.DO: return this.compileDoStatement(statement); case NodeKind.EMPTY: return this.compileEmptyStatement(statement); case NodeKind.EXPRESSION: return this.compileExpressionStatement(statement); case NodeKind.FOR: return this.compileForStatement(statement); case NodeKind.IF: return this.compileIfStatement(statement); case NodeKind.RETURN: return this.compileReturnStatement(statement); case NodeKind.SWITCH: return this.compileSwitchStatement(statement); case NodeKind.THROW: return this.compileThrowStatement(statement); case NodeKind.TRY: return this.compileTryStatement(statement); case NodeKind.VARIABLE: return this.compileVariableStatement(statement); case NodeKind.WHILE: return this.compileWhileStatement(statement); } throw new Error("unexpected statement kind"); } compileStatements(statements: Statement[]): ExpressionRef[] { const k: i32 = statements.length; const stmts: ExpressionRef[] = new Array(k); for (let i: i32 = 0; i < k; ++i) stmts[i] = this.compileStatement(statements[i]); return stmts; } compileBlockStatement(statement: BlockStatement): ExpressionRef { const statements: Statement[] = statement.statements; if (statements.length == 0) return this.module.createNop(); if (statements.length == 1) return this.compileStatement(statements[0]); return this.module.createBlock(null, this.compileStatements(statements), NativeType.None); } compileBreakStatement(statement: BreakStatement): ExpressionRef { if (statement.label) throw new Error("not implemented"); const context: string | null = this.currentFunction.breakContext; if (context != null) return this.module.createBreak("break|" + (context)); this.error(DiagnosticCode.A_break_statement_can_only_be_used_within_an_enclosing_iteration_or_switch_statement, statement.range); return this.module.createUnreachable(); } compileContinueStatement(statement: ContinueStatement): ExpressionRef { if (statement.label) throw new Error("not implemented"); const context: string | null = this.currentFunction.breakContext; if (context != null && !this.disallowContinue) return this.module.createBreak("continue|" + (context)); this.error(DiagnosticCode.A_continue_statement_can_only_be_used_within_an_enclosing_iteration_statement, statement.range); return this.module.createUnreachable(); } compileDoStatement(statement: DoStatement): ExpressionRef { const label: string = this.currentFunction.enterBreakContext(); const condition: ExpressionRef = this.compileExpression(statement.condition, Type.i32); const body: ExpressionRef = this.compileStatement(statement.statement); this.currentFunction.leaveBreakContext(); const breakLabel: string = "break|" + label; const continueLabel: string = "continue|" + label; return this.module.createBlock(breakLabel, [ this.module.createLoop(continueLabel, this.module.createBlock(null, [ body, this.module.createBreak(continueLabel, condition) ], NativeType.None)) ], NativeType.None); } compileEmptyStatement(statement: EmptyStatement): ExpressionRef { return this.module.createNop(); } compileExpressionStatement(statement: ExpressionStatement): ExpressionRef { let expr: ExpressionRef = this.compileExpression(statement.expression, Type.void, ConversionKind.NONE); if (this.currentType != Type.void) { expr = this.module.createDrop(expr); this.currentType = Type.void; } return expr; } compileForStatement(statement: ForStatement): ExpressionRef { const context: string = this.currentFunction.enterBreakContext(); const variableWasLocal: bool = this.variableIsLocal; if (this.currentFunction == this.startFunction) this.variableIsLocal = true; const initializer: ExpressionRef = statement.initializer ? this.compileStatement(statement.initializer) : this.module.createNop(); this.variableIsLocal = variableWasLocal; const condition: ExpressionRef = statement.condition ? this.compileExpression(statement.condition, Type.i32) : this.module.createI32(1); const incrementor: ExpressionRef = statement.incrementor ? this.compileExpression(statement.incrementor, Type.void) : this.module.createNop(); const body: ExpressionRef = this.compileStatement(statement.statement); this.currentFunction.leaveBreakContext(); const continueLabel: string = "continue|" + context; const breakLabel: string = "break|" + context; return this.module.createBlock(breakLabel, [ initializer, this.module.createLoop(continueLabel, this.module.createBlock(null, [ this.module.createIf(condition, this.module.createBlock(null, [ body, incrementor, this.module.createBreak(continueLabel) ], NativeType.None)) ], NativeType.None)) ], NativeType.None); } compileIfStatement(statement: IfStatement): ExpressionRef { const condition: ExpressionRef = this.compileExpression(statement.condition, Type.i32); const ifTrue: ExpressionRef = this.compileStatement(statement.statement); const ifFalse: ExpressionRef = statement.elseStatement ? this.compileStatement(statement.elseStatement) : 0; return this.module.createIf(condition, ifTrue, ifFalse); } compileReturnStatement(statement: ReturnStatement): ExpressionRef { if (this.currentFunction) { const expression: ExpressionRef = statement.expression ? this.compileExpression(statement.expression, this.currentFunction.returnType) : 0; return this.module.createReturn(expression); } return this.module.createUnreachable(); } compileSwitchStatement(statement: SwitchStatement): ExpressionRef { const context: string = this.currentFunction.enterBreakContext(); const previousDisallowContinue: bool = this.disallowContinue; this.disallowContinue = true; // introduce a local for evaluating the condition (exactly once) const local: Local = this.currentFunction.addLocal(Type.i32); let i: i32, k: i32 = statement.cases.length; // prepend initializer to inner block const breaks: ExpressionRef[] = new Array(1 + k); breaks[0] = this.module.createSetLocal(local.index, this.compileExpression(statement.expression, Type.i32)); // initializer // make one br_if per (possibly dynamic) labeled case (binaryen optimizes to br_table where possible) let breakIndex: i32 = 1; let defaultIndex: i32 = -1; for (i = 0; i < k; ++i) { const case_: SwitchCase = statement.cases[i]; if (case_.label) { breaks[breakIndex++] = this.module.createBreak("case" + i.toString(10) + "|" + context, this.module.createBinary(BinaryOp.EqI32, this.module.createGetLocal(local.index, NativeType.I32), this.compileExpression(case_.label, Type.i32) ) ); } else defaultIndex = i; } // otherwise br to default respectively out of the switch if there is no default case breaks[breakIndex] = this.module.createBreak((defaultIndex >= 0 ? "case" + defaultIndex.toString(10) : "break" ) + "|" + context); // nest blocks in order let currentBlock: ExpressionRef = this.module.createBlock("case0|" + context, breaks, NativeType.None); for (i = 0; i < k; ++i) { const case_: SwitchCase = statement.cases[i]; const nextLabel: string = i == k - 1 ? "break|" + context : "case" + (i + 1).toString(10) + "|" + context; const l: i32 = case_.statements.length; const body: ExpressionRef[] = new Array(1 + l); body[0] = currentBlock; for (let j: i32 = 0; j < l; ++j) body[j + 1] = this.compileStatement(case_.statements[j]); currentBlock = this.module.createBlock(nextLabel, body, NativeType.None); } this.currentFunction.leaveBreakContext(); this.disallowContinue = previousDisallowContinue; return currentBlock; } compileThrowStatement(statement: ThrowStatement): ExpressionRef { return this.module.createUnreachable(); // TODO: waiting for exception-handling spec } compileTryStatement(statement: TryStatement): ExpressionRef { throw new Error("not implemented"); // can't yet support something like: try { return ... } finally { ... } // worthwhile to investigate lowering returns to block results (here)? } compileVariableStatement(statement: VariableStatement): ExpressionRef { const declarations: VariableDeclaration[] = statement.declarations; // top-level variables become globals if (this.currentFunction == this.startFunction && !this.variableIsLocal) { const isConst: bool = hasModifier(ModifierKind.CONST, statement.modifiers); for (let i: i32 = 0, k: i32 = declarations.length; i < k; ++i) this.compileGlobalDeclaration(declarations[i], isConst); return this.module.createNop(); } // other variables become locals const initializers: ExpressionRef[] = new Array(); for (let i: i32 = 0, k = declarations.length; i < k; ++i) { const declaration: VariableDeclaration = declarations[i]; if (declaration.type) { const name: string = declaration.identifier.name; const type: Type | null = this.program.resolveType(declaration.type, this.currentFunction.contextualTypeArguments, true); // reports if (type) { if (this.currentFunction.locals.has(name)) this.error(DiagnosticCode.Duplicate_identifier_0, declaration.identifier.range, name); // recoverable else this.currentFunction.addLocal(type, name); if (declaration.initializer) initializers.push(this.compileAssignment(declaration.identifier, declaration.initializer, Type.void)); } } } return initializers.length ? this.module.createBlock(null, initializers, NativeType.None) : this.module.createNop(); } compileWhileStatement(statement: WhileStatement): ExpressionRef { const label: string = this.currentFunction.enterBreakContext(); const condition: ExpressionRef = this.compileExpression(statement.condition, Type.i32); const breakLabel: string = "break|" + label; const continueLabel: string = "continue|" + label; const body: ExpressionRef = this.compileStatement(statement.statement); this.currentFunction.leaveBreakContext(); return this.module.createBlock(breakLabel, [ this.module.createLoop(continueLabel, this.module.createIf(condition, this.module.createBlock(null, [ body, this.module.createBreak(continueLabel) ], NativeType.None)) ) ], NativeType.None); } // expressions compileExpression(expression: Expression, contextualType: Type, conversionKind: ConversionKind = ConversionKind.IMPLICIT): ExpressionRef { this.currentType = contextualType; let expr: ExpressionRef; switch (expression.kind) { case NodeKind.ASSERTION: expr = this.compileAssertionExpression(expression, contextualType); break; case NodeKind.BINARY: expr = this.compileBinaryExpression(expression, contextualType); break; case NodeKind.CALL: expr = this.compileCallExpression(expression, contextualType); break; case NodeKind.ELEMENTACCESS: expr = this.compileElementAccessExpression(expression, contextualType); break; case NodeKind.IDENTIFIER: case NodeKind.FALSE: case NodeKind.NULL: case NodeKind.THIS: case NodeKind.TRUE: expr = this.compileIdentifierExpression(expression, contextualType); break; case NodeKind.LITERAL: expr = this.compileLiteralExpression(expression, contextualType); break; case NodeKind.NEW: expr = this.compileNewExpression(expression, contextualType); break; case NodeKind.PARENTHESIZED: expr = this.compileParenthesizedExpression(expression, contextualType); break; case NodeKind.PROPERTYACCESS: expr = this.compilePropertyAccessExpression(expression, contextualType); break; case NodeKind.TERNARY: expr = this.compileTernaryExpression(expression, contextualType); break; case NodeKind.UNARYPOSTFIX: expr = this.compileUnaryPostfixExpression(expression, contextualType); break; case NodeKind.UNARYPREFIX: expr = this.compileUnaryPrefixExpression(expression, contextualType); break; default: throw new Error("unexpected expression kind"); } if (conversionKind != ConversionKind.NONE && this.currentType != contextualType) { expr = this.convertExpression(expr, this.currentType, contextualType, conversionKind, expression); this.currentType = contextualType; } return expr; } convertExpression(expr: ExpressionRef, fromType: Type, toType: Type, conversionKind: ConversionKind, reportNode: Node): ExpressionRef { if (conversionKind == ConversionKind.NONE) return expr; if (!fromType) { _BinaryenExpressionPrint(expr); throw new Error("WHAT"); } // void to any if (fromType.kind == TypeKind.VOID) { this.error(DiagnosticCode.Operation_not_supported, reportNode.range); throw new Error("unexpected conversion from void"); } // any to void if (toType.kind == TypeKind.VOID) return this.module.createDrop(expr); const fromFloat: bool = fromType.isAnyFloat; const toFloat: bool = toType.isAnyFloat; const mod: Module = this.module; let losesInformation: bool = false; if (fromFloat) { // float to float if (toFloat) { if (fromType.kind == TypeKind.F32) { // f32 to f64 if (toType.kind == TypeKind.F64) expr = mod.createUnary(UnaryOp.PromoteF32, expr); // f64 to f32 } else if (toType.kind == TypeKind.F32) { losesInformation = true; expr = mod.createUnary(UnaryOp.DemoteF64, expr); } // float to int } else { losesInformation = true; // f32 to int if (fromType.kind == TypeKind.F32) { if (toType.isSignedInteger) { if (toType.isLongInteger) expr = mod.createUnary(UnaryOp.TruncF32_I64, expr); else { expr = mod.createUnary(UnaryOp.TruncF32_I32, expr); if (toType.isSmallInteger) { expr = mod.createBinary(BinaryOp.ShlI32, expr, mod.createI32(toType.smallIntegerShift)); expr = mod.createBinary(BinaryOp.ShrI32, expr, mod.createI32(toType.smallIntegerShift)); } } } else { if (toType.isLongInteger) expr = mod.createUnary(UnaryOp.TruncF32_U64, expr); else { expr = mod.createUnary(UnaryOp.TruncF32_U32, expr); if (toType.isSmallInteger) expr = mod.createBinary(BinaryOp.AndI32, expr, mod.createI32(toType.smallIntegerMask)); } } // f64 to int } else { if (toType.isSignedInteger) { if (toType.isLongInteger) expr = mod.createUnary(UnaryOp.TruncF64_I64, expr); else { expr = mod.createUnary(UnaryOp.TruncF64_I32, expr); if (toType.isSmallInteger) { expr = mod.createBinary(BinaryOp.ShlI32, expr, mod.createI32(toType.smallIntegerShift)); expr = mod.createBinary(BinaryOp.ShrI32, expr, mod.createI32(toType.smallIntegerShift)); } } } else { if (toType.isLongInteger) expr = mod.createUnary(UnaryOp.TruncF64_U64, expr); else { expr = mod.createUnary(UnaryOp.TruncF64_U32, expr); if (toType.isSmallInteger) expr = mod.createBinary(BinaryOp.AndI32, expr, mod.createI32(toType.smallIntegerMask)); } } } } // int to float } else if (toFloat) { // int to f32 if (toType.kind == TypeKind.F32) { if (fromType.isLongInteger) { losesInformation = true; if (fromType.isSignedInteger) expr = mod.createUnary(UnaryOp.ConvertI64_F32, expr); else expr = mod.createUnary(UnaryOp.ConvertU64_F32, expr); } else if (fromType.isSignedInteger) expr = mod.createUnary(UnaryOp.ConvertI32_F32, expr); else expr = mod.createUnary(UnaryOp.ConvertU32_F32, expr); // int to f64 } else { if (fromType.isLongInteger) { losesInformation = true; if (fromType.isSignedInteger) expr = mod.createUnary(UnaryOp.ConvertI64_F64, expr); else expr = mod.createUnary(UnaryOp.ConvertU64_F64, expr); } else if (fromType.isSignedInteger) expr = mod.createUnary(UnaryOp.ConvertI32_F64, expr); else expr = mod.createUnary(UnaryOp.ConvertU32_F64, expr); } // int to int } else { if (fromType.isLongInteger) { // i64 to i32 if (!toType.isLongInteger) { losesInformation = true; expr = mod.createUnary(UnaryOp.WrapI64, expr); if (toType.isSmallInteger) { if (toType.isSignedInteger) { expr = mod.createBinary(BinaryOp.ShlI32, expr, mod.createI32(toType.smallIntegerShift)); expr = mod.createBinary(BinaryOp.ShrI32, expr, mod.createI32(toType.smallIntegerShift)); } else expr = mod.createBinary(BinaryOp.AndI32, expr, mod.createI32(toType.smallIntegerMask)); } } // i32 to i64 } else if (toType.isLongInteger) { if (toType.isSignedInteger) expr = mod.createUnary(UnaryOp.ExtendI32, expr); else expr = mod.createUnary(UnaryOp.ExtendU32, expr); // i32 to smaller/change of signage i32 } else if (toType.isSmallInteger && (fromType.size > toType.size || (fromType.size == toType.size && fromType.kind != toType.kind))) { losesInformation = true; if (toType.isSignedInteger) { expr = mod.createBinary(BinaryOp.ShlI32, expr, mod.createI32(toType.smallIntegerShift)); expr = mod.createBinary(BinaryOp.ShrI32, expr, mod.createI32(toType.smallIntegerShift)); } else expr = mod.createBinary(BinaryOp.AndI32, expr, mod.createI32(toType.smallIntegerMask)); } } if (losesInformation && conversionKind == ConversionKind.IMPLICIT) this.error(DiagnosticCode.Conversion_from_type_0_to_1_requires_an_explicit_cast, reportNode.range, fromType.toString(), toType.toString()); return expr; } compileAssertionExpression(expression: AssertionExpression, contextualType: Type): ExpressionRef { const toType: Type | null = this.program.resolveType(expression.toType, this.currentFunction.contextualTypeArguments); // reports return toType && toType != contextualType ? this.compileExpression(expression.expression, toType, ConversionKind.EXPLICIT) : this.compileExpression(expression.expression, contextualType); } compileBinaryExpression(expression: BinaryExpression, contextualType: Type): ExpressionRef { let op: BinaryOp; let left: ExpressionRef; let right: ExpressionRef; let compound: Token = 0; let condition: ExpressionRef; let tempLocal: Local; switch (expression.operator) { case Token.LESSTHAN: left = this.compileExpression(expression.left, contextualType, ConversionKind.NONE); right = this.compileExpression(expression.right, this.currentType); op = this.currentType == Type.f32 ? BinaryOp.LtF32 : this.currentType == Type.f64 ? BinaryOp.LtF64 : this.currentType.isLongInteger ? BinaryOp.LtI64 : BinaryOp.LtI32; this.currentType = Type.bool; break; case Token.GREATERTHAN: left = this.compileExpression(expression.left, contextualType, ConversionKind.NONE); right = this.compileExpression(expression.right, this.currentType); op = this.currentType == Type.f32 ? BinaryOp.GtF32 : this.currentType == Type.f64 ? BinaryOp.GtF64 : this.currentType.isLongInteger ? BinaryOp.GtI64 : BinaryOp.GtI32; this.currentType = Type.bool; break; case Token.LESSTHAN_EQUALS: left = this.compileExpression(expression.left, contextualType, ConversionKind.NONE); right = this.compileExpression(expression.right, this.currentType); op = this.currentType == Type.f32 ? BinaryOp.LeF32 : this.currentType == Type.f64 ? BinaryOp.LeF64 : this.currentType.isLongInteger ? BinaryOp.LeI64 : BinaryOp.LeI32; this.currentType = Type.bool; break; case Token.GREATERTHAN_EQUALS: left = this.compileExpression(expression.left, contextualType, ConversionKind.NONE); right = this.compileExpression(expression.right, this.currentType); op = this.currentType == Type.f32 ? BinaryOp.GeF32 : this.currentType == Type.f64 ? BinaryOp.GeF64 : this.currentType.isLongInteger ? BinaryOp.GeI64 : BinaryOp.GeI32; this.currentType = Type.bool; break; case Token.EQUALS_EQUALS: case Token.EQUALS_EQUALS_EQUALS: left = this.compileExpression(expression.left, contextualType, ConversionKind.NONE); right = this.compileExpression(expression.right, this.currentType); op = this.currentType == Type.f32 ? BinaryOp.EqF32 : this.currentType == Type.f64 ? BinaryOp.EqF64 : this.currentType.isLongInteger ? BinaryOp.EqI64 : BinaryOp.EqI32; this.currentType = Type.bool; break; case Token.EXCLAMATION_EQUALS: case Token.EXCLAMATION_EQUALS_EQUALS: left = this.compileExpression(expression.left, contextualType, ConversionKind.NONE); right = this.compileExpression(expression.right, this.currentType); op = this.currentType == Type.f32 ? BinaryOp.NeF32 : this.currentType == Type.f64 ? BinaryOp.NeF64 : this.currentType.isLongInteger ? BinaryOp.NeI64 : BinaryOp.NeI32; this.currentType = Type.bool; break; case Token.EQUALS: return this.compileAssignment(expression.left, expression.right, contextualType); case Token.PLUS_EQUALS: compound = Token.EQUALS; case Token.PLUS: left = this.compileExpression(expression.left, contextualType, ConversionKind.NONE); right = this.compileExpression(expression.right, this.currentType); op = this.currentType == Type.f32 ? BinaryOp.AddF32 : this.currentType == Type.f64 ? BinaryOp.AddF64 : this.currentType.isLongInteger ? BinaryOp.AddI64 : BinaryOp.AddI32; break; case Token.MINUS_EQUALS: compound = Token.EQUALS; case Token.MINUS: left = this.compileExpression(expression.left, contextualType, ConversionKind.NONE); right = this.compileExpression(expression.right, this.currentType); op = this.currentType == Type.f32 ? BinaryOp.SubF32 : this.currentType == Type.f64 ? BinaryOp.SubF64 : this.currentType.isLongInteger ? BinaryOp.SubI64 : BinaryOp.SubI32; break; case Token.ASTERISK_EQUALS: compound = Token.EQUALS; case Token.ASTERISK: left = this.compileExpression(expression.left, contextualType, ConversionKind.NONE); right = this.compileExpression(expression.right, this.currentType); op = this.currentType == Type.f32 ? BinaryOp.MulF32 : this.currentType == Type.f64 ? BinaryOp.MulF64 : this.currentType.isLongInteger ? BinaryOp.MulI64 : BinaryOp.MulI32; break; case Token.SLASH_EQUALS: compound = Token.EQUALS; case Token.SLASH: left = this.compileExpression(expression.left, contextualType, ConversionKind.NONE); right = this.compileExpression(expression.right, this.currentType); op = this.currentType == Type.f32 ? BinaryOp.DivF32 : this.currentType == Type.f64 ? BinaryOp.DivF64 : this.currentType.isLongInteger ? BinaryOp.DivI64 : BinaryOp.DivI32; break; case Token.PERCENT_EQUALS: compound = Token.EQUALS; case Token.PERCENT: left = this.compileExpression(expression.left, contextualType, ConversionKind.NONE); right = this.compileExpression(expression.right, this.currentType); if (this.currentType.isAnyFloat) throw new Error("not implemented"); // TODO: internal fmod, possibly simply imported from JS op = this.currentType.isLongInteger ? BinaryOp.RemI64 : BinaryOp.RemI32; break; case Token.LESSTHAN_LESSTHAN_EQUALS: compound = Token.EQUALS; case Token.LESSTHAN_LESSTHAN: left = this.compileExpression(expression.left, contextualType.isAnyFloat ? Type.i64 : contextualType, contextualType == Type.void ? ConversionKind.NONE : ConversionKind.IMPLICIT); right = this.compileExpression(expression.right, this.currentType); op = this.currentType.isLongInteger ? BinaryOp.ShlI64 : BinaryOp.ShlI32; break; case Token.GREATERTHAN_GREATERTHAN_EQUALS: compound = Token.EQUALS; case Token.GREATERTHAN_GREATERTHAN: left = this.compileExpression(expression.left, contextualType.isAnyFloat ? Type.i64 : contextualType, contextualType == Type.void ? ConversionKind.NONE : ConversionKind.IMPLICIT); right = this.compileExpression(expression.right, this.currentType); op = this.currentType.isSignedInteger ? this.currentType.isLongInteger ? BinaryOp.ShrI64 : BinaryOp.ShrI32 : this.currentType.isLongInteger ? BinaryOp.ShrU64 : BinaryOp.ShrU32; break; case Token.GREATERTHAN_GREATERTHAN_GREATERTHAN_EQUALS: compound = Token.EQUALS; case Token.GREATERTHAN_GREATERTHAN_GREATERTHAN: left = this.compileExpression(expression.left, contextualType.isAnyFloat ? Type.u64 : contextualType, contextualType == Type.void ? ConversionKind.NONE : ConversionKind.IMPLICIT); right = this.compileExpression(expression.right, this.currentType); op = this.currentType.isLongInteger ? BinaryOp.ShrU64 : BinaryOp.ShrU32; break; case Token.AMPERSAND_EQUALS: compound = Token.EQUALS; case Token.AMPERSAND: left = this.compileExpression(expression.left, contextualType.isAnyFloat ? Type.i64 : contextualType, contextualType == Type.void ? ConversionKind.NONE : ConversionKind.IMPLICIT); right = this.compileExpression(expression.right, this.currentType); op = this.currentType.isLongInteger ? BinaryOp.AndI64 : BinaryOp.AndI32; break; case Token.BAR_EQUALS: compound = Token.EQUALS; case Token.BAR: left = this.compileExpression(expression.left, contextualType.isAnyFloat ? Type.i64 : contextualType, contextualType == Type.void ? ConversionKind.NONE : ConversionKind.IMPLICIT); right = this.compileExpression(expression.right, this.currentType); op = this.currentType.isLongInteger ? BinaryOp.OrI64 : BinaryOp.OrI32; break; case Token.CARET_EQUALS: compound = Token.EQUALS; case Token.CARET: left = this.compileExpression(expression.left, contextualType.isAnyFloat ? Type.i64 : contextualType, contextualType == Type.void ? ConversionKind.NONE : ConversionKind.IMPLICIT); right = this.compileExpression(expression.right, this.currentType); op = this.currentType.isLongInteger ? BinaryOp.XorI64 : BinaryOp.XorI32; break; case Token.AMPERSAND_AMPERSAND: // left && right left = this.compileExpression(expression.left, contextualType, contextualType == Type.void ? ConversionKind.NONE : ConversionKind.IMPLICIT); right = this.compileExpression(expression.right, this.currentType); tempLocal = this.currentFunction.addLocal(this.currentType); return this.module.createIf( this.currentType.isLongInteger ? this.module.createBinary(BinaryOp.NeI64, this.module.createTeeLocal(tempLocal.index, left), this.module.createI64(0, 0)) : this.currentType == Type.f64 ? this.module.createBinary(BinaryOp.NeF64, this.module.createTeeLocal(tempLocal.index, left), this.module.createF64(0)) : this.currentType == Type.f32 ? this.module.createBinary(BinaryOp.NeF32, this.module.createTeeLocal(tempLocal.index, left), this.module.createF32(0)) : this.module.createTeeLocal(tempLocal.index, left), right, this.module.createGetLocal(tempLocal.index, typeToNativeType(tempLocal.type)) ); case Token.BAR_BAR: // left || right left = this.compileExpression(expression.left, contextualType, contextualType == Type.void ? ConversionKind.NONE : ConversionKind.IMPLICIT); right = this.compileExpression(expression.right, this.currentType); tempLocal = this.currentFunction.addLocal(this.currentType); return this.module.createIf( this.currentType.isLongInteger ? this.module.createBinary(BinaryOp.NeI64, this.module.createTeeLocal(tempLocal.index, left), this.module.createI64(0, 0)) : this.currentType == Type.f64 ? this.module.createBinary(BinaryOp.NeF64, this.module.createTeeLocal(tempLocal.index, left), this.module.createF64(0)) : this.currentType == Type.f32 ? this.module.createBinary(BinaryOp.NeF32, this.module.createTeeLocal(tempLocal.index, left), this.module.createF32(0)) : this.module.createTeeLocal(tempLocal.index, left), this.module.createGetLocal(tempLocal.index, typeToNativeType(tempLocal.type)), right ); default: throw new Error("not implemented"); } if (compound) { right = this.module.createBinary(op, left, right); return this.compileAssignmentWithValue(expression.left, right, contextualType != Type.void); } return this.module.createBinary(op, left, right); } compileAssignment(expression: Expression, valueExpression: Expression, contextualType: Type): ExpressionRef { this.currentType = this.determineExpressionType(expression, contextualType); return this.compileAssignmentWithValue(expression, this.compileExpression(valueExpression, this.currentType, ConversionKind.IMPLICIT), contextualType != Type.void); } compileAssignmentWithValue(expression: Expression, valueWithCorrectType: ExpressionRef, tee: bool = false): ExpressionRef { const element: Element | null = this.program.resolveElement(expression, this.currentFunction); if (!element) return this.module.createUnreachable(); if (element.kind == ElementKind.LOCAL) { if (tee) { this.currentType = (element).type; return this.module.createTeeLocal((element).index, valueWithCorrectType); } this.currentType = Type.void; return this.module.createSetLocal((element).index, valueWithCorrectType); } if (element.kind == ElementKind.GLOBAL) { this.compileGlobal(element); if (!(element).isMutable) this.error(DiagnosticCode.Cannot_assign_to_0_because_it_is_a_constant_or_a_read_only_property, expression.range, element.internalName); if (tee) { if (!(element).type) return this.module.createUnreachable(); const globalNativeType: NativeType = typeToNativeType((element).type); this.currentType = (element).type; return this.module.createBlock(null, [ // teeGlobal this.module.createSetGlobal((element).internalName, valueWithCorrectType), this.module.createGetGlobal((element).internalName, globalNativeType) ], globalNativeType); } this.currentType = Type.void; return this.module.createSetGlobal((element).internalName, valueWithCorrectType); } // TODO: fields, setters throw new Error("not implemented"); } compileCallExpression(expression: CallExpression, contextualType: Type): ExpressionRef { const element: Element | null = this.program.resolveElement(expression.expression, this.currentFunction); // reports if (!element) return this.module.createUnreachable(); if (element.kind == ElementKind.FUNCTION_PROTOTYPE) { const functionPrototype: FunctionPrototype = element; let functionInstance: Function | null = null; if (functionPrototype.isBuiltIn) { const k: i32 = expression.typeArguments.length; const resolvedTypeArguments: Type[] = new Array(k); sb.length = 0; for (let i: i32 = 0; i < k; ++i) { let resolvedType: Type | null = this.program.resolveType(expression.typeArguments[i], this.currentFunction.contextualTypeArguments, true); // reports if (!resolvedType) return this.module.createUnreachable(); resolvedTypeArguments[i] = resolvedType; sb.push(resolvedType.toString()); } functionInstance = functionPrototype.instances.get(sb.join(",")); if (!functionInstance) { this.currentType = contextualType; let expr: ExpressionRef = compileBuiltinCall(this, functionPrototype.internalName, resolvedTypeArguments, expression.arguments, expression); if (!expr) { this.error(DiagnosticCode.Operation_not_supported, expression.range); return this.module.createUnreachable(); } return expr; } } else { // TODO: infer type arguments from parameter types if omitted functionInstance = (element).resolveInclTypeArguments(expression.typeArguments, this.currentFunction.contextualTypeArguments, expression); // reports } if (!functionInstance) return this.module.createUnreachable(); return this.compileCall(functionInstance, expression.arguments, expression); } this.error(DiagnosticCode.Cannot_invoke_an_expression_whose_type_lacks_a_call_signature_Type_0_has_no_compatible_call_signatures, expression.range, element.internalName); return this.module.createUnreachable(); } /** Compiles a call to a function. If an instance method, `this` is the first element in `argumentExpressions`. */ compileCall(functionInstance: Function, argumentExpressions: Expression[], reportNode: Node): ExpressionRef { const previousType: Type = this.currentType; // validate and compile arguments const parameters: Parameter[] = functionInstance.parameters; const parameterCount: i32 = parameters.length; const argumentCount: i32 = argumentExpressions.length; if (argumentExpressions.length > parameterCount) { // too many arguments this.error(DiagnosticCode.Expected_0_arguments_but_got_1, reportNode.range, (functionInstance.isInstance ? parameterCount - 1 : parameterCount).toString(10), (functionInstance.isInstance ? argumentCount - 1 : argumentCount).toString(10) ); return this.module.createUnreachable(); } const operands: ExpressionRef[] = new Array(parameterCount); for (let i: i32 = 0; i < parameterCount; ++i) { if (argumentExpressions.length > i) { operands[i] = this.compileExpression(argumentExpressions[i], parameters[i].type); } else { const initializer: Expression | null = parameters[i].initializer; if (initializer) { // omitted, uses initializer // FIXME: here, the initializer is compiled in the caller's scope. // a solution could be to use a stub for each possible overload, calling the // full function with optional arguments being part of the stub's body. operands[i] = this.compileExpression(initializer, parameters[i].type); } else { // too few arguments this.error(DiagnosticCode.Expected_at_least_0_arguments_but_got_1, reportNode.range, (functionInstance.isInstance ? i : i + 1).toString(10), (functionInstance.isInstance ? argumentCount - 1 : argumentCount).toString(10) ); return this.module.createUnreachable(); } } } this.currentType = functionInstance.returnType; if (!functionInstance.isCompiled) this.compileFunction(functionInstance); // imported function if (functionInstance.isDeclare) return this.module.createCallImport(functionInstance.internalName, operands, typeToNativeType(functionInstance.returnType)); // internal function return this.module.createCall(functionInstance.internalName, operands, typeToNativeType(functionInstance.returnType)); } compileElementAccessExpression(expression: ElementAccessExpression, contextualType: Type): ExpressionRef { const element: Element | null = this.program.resolveElement(expression.expression, this.currentFunction); // reports if (!element) return this.module.createUnreachable(); throw new Error("not implemented"); } compileIdentifierExpression(expression: IdentifierExpression, contextualType: Type): ExpressionRef { // null if (expression.kind == NodeKind.NULL) { if (contextualType.classType) // keep contextualType return this.options.target == Target.WASM64 ? this.module.createI64(0, 0) : this.module.createI32(0); if (this.options.target == Target.WASM64) { this.currentType = Type.u64; return this.module.createI64(0, 0); } else { this.currentType = Type.u32; return this.module.createI32(0); } // true } else if (expression.kind == NodeKind.TRUE) { this.currentType = Type.bool; return this.module.createI32(1); // false } else if (expression.kind == NodeKind.FALSE) { this.currentType = Type.bool; return this.module.createI32(0); // this } else if (expression.kind == NodeKind.THIS) { if (this.currentFunction.instanceMethodOf) { this.currentType = this.currentFunction.instanceMethodOf.type; return this.module.createGetLocal(0, this.options.target == Target.WASM64 ? NativeType.I64 : NativeType.I32); } this.error(DiagnosticCode._this_cannot_be_referenced_in_current_location, expression.range); this.currentType = this.options.target == Target.WASM64 ? Type.u64 : Type.u32; return this.module.createUnreachable(); } if (expression.kind == NodeKind.IDENTIFIER) { // NaN if ((expression).name == "NaN") if (this.currentType.kind == TypeKind.F32) return this.module.createF32(NaN); else { this.currentType = Type.f64; return this.module.createF64(NaN); } // Infinity if ((expression).name == "Infinity") if (this.currentType.kind == TypeKind.F32) return this.module.createF32(Infinity); else { this.currentType = Type.f64; return this.module.createF64(Infinity); } } const element: Element | null = this.program.resolveElement(expression, this.currentFunction); // reports if (!element) return this.module.createUnreachable(); // local if (element.kind == ElementKind.LOCAL) { this.currentType = (element).type; return this.module.createGetLocal((element).index, typeToNativeType(this.currentType = (element).type)); } // global if (element.kind == ElementKind.GLOBAL) { if ((element).type) this.currentType = (element).type; return this.compileGlobal(element) // reports ? this.module.createGetGlobal((element).internalName, typeToNativeType(this.currentType = (element).type)) : this.module.createUnreachable(); } // field // if (element.kind == ElementKind.FIELD) // throw new Error("not implemented"); // getter if (element.kind == ElementKind.FUNCTION_PROTOTYPE && (element).isGetter) throw new Error("not implemented"); this.error(DiagnosticCode.Operation_not_supported, expression.range); return this.module.createUnreachable(); } compileLiteralExpression(expression: LiteralExpression, contextualType: Type): ExpressionRef { switch (expression.literalKind) { // case LiteralKind.ARRAY: case LiteralKind.FLOAT: { const floatValue: f64 = (expression).value; if (contextualType == Type.f32) return this.module.createF32(Math.fround(floatValue)); this.currentType = Type.f64; return this.module.createF64(floatValue); } case LiteralKind.INTEGER: { const intValue: I64 = (expression).value; if (contextualType == Type.bool && (intValue.isZero || intValue.isOne)) return this.module.createI32(intValue.isZero ? 0 : 1); if (contextualType == Type.f64) return this.module.createF64((intValue.lo) + (intValue.hi) * 0xffffffff); if (contextualType == Type.f32) return this.module.createF32((intValue.lo) + (intValue.hi) * 0xffffffff); if (contextualType.isLongInteger) return this.module.createI64(intValue.lo, intValue.hi); if (!intValue.fitsInI32) { this.currentType = Type.i64; return this.module.createI64(intValue.lo, intValue.hi); } this.currentType = Type.i32; return this.module.createI32(intValue.toI32()); } // case LiteralKind.OBJECT: // case LiteralKind.REGEXP: // case LiteralKind.STRING: } throw new Error("not implemented"); } compileNewExpression(expression: NewExpression, contextualType: Type): ExpressionRef { throw new Error("not implemented"); } compileParenthesizedExpression(expression: ParenthesizedExpression, contextualType: Type): ExpressionRef { return this.compileExpression(expression.expression, contextualType); } compilePropertyAccessExpression(expression: PropertyAccessExpression, contextualType: Type): ExpressionRef { throw new Error("not implemented"); } compileTernaryExpression(expression: TernaryExpression, contextualType: Type): ExpressionRef { const condition: ExpressionRef = this.compileExpression(expression.condition, Type.i32); const ifThen: ExpressionRef = this.compileExpression(expression.ifThen, contextualType); const ifElse: ExpressionRef = this.compileExpression(expression.ifElse, contextualType); return this.module.createIf(condition, ifThen, ifElse); } compileUnaryPostfixExpression(expression: UnaryPostfixExpression, contextualType: Type): ExpressionRef { const operator: Token = expression.operator; // make a getter for the expression (also obtains the type) const getValue: ExpressionRef = this.compileExpression(expression.expression, contextualType, contextualType == Type.void ? ConversionKind.NONE : ConversionKind.IMPLICIT); // use a temp local for the intermediate value const tempLocal: Local = this.currentFunction.addLocal(this.currentType); let op: BinaryOp; let nativeType: NativeType; let nativeOne: ExpressionRef; if (tempLocal.type == Type.f32) { op = operator == Token.PLUS_PLUS ? BinaryOp.AddF32 : BinaryOp.SubF32; nativeType = NativeType.F32; nativeOne = this.module.createF32(1); } else if (tempLocal.type == Type.f64) { op = operator == Token.PLUS_PLUS ? BinaryOp.AddF64 : BinaryOp.SubF64; nativeType = NativeType.F64; nativeOne = this.module.createF64(1); } else if (tempLocal.type.isLongInteger) { op = operator == Token.PLUS_PLUS ? BinaryOp.AddI64 : BinaryOp.SubI64; nativeType = NativeType.I64; nativeOne = this.module.createI64(1, 0); } else { op = operator == Token.PLUS_PLUS ? BinaryOp.AddI32 : BinaryOp.SubI32; nativeType = NativeType.I32; nativeOne = this.module.createI32(1); } // make a setter that sets the new value (temp value +/- 1) const setValue: ExpressionRef = this.compileAssignmentWithValue(expression.expression, this.module.createBinary(op, this.module.createGetLocal(tempLocal.index, nativeType), nativeOne ), false ); // NOTE: can't preemptively tee_local the return value on the stack because binaryen expects // this to be well-formed. becomes a tee_local when optimizing, though. this.currentType = tempLocal.type; return this.module.createBlock(null, [ this.module.createSetLocal(tempLocal.index, getValue), // +++ this.module.createTeeLocal(tempLocal.index, getValue), setValue, this.module.createGetLocal(tempLocal.index, nativeType) // --- ], nativeType); } compileUnaryPrefixExpression(expression: UnaryPrefixExpression, contextualType: Type): ExpressionRef { const operandExpression: Expression = expression.expression; let operand: ExpressionRef; let op: UnaryOp; switch (expression.operator) { case Token.PLUS: return this.compileExpression(operandExpression, contextualType, contextualType == Type.void ? ConversionKind.NONE : ConversionKind.IMPLICIT); case Token.MINUS: operand = this.compileExpression(operandExpression, contextualType, contextualType == Type.void ? ConversionKind.NONE : ConversionKind.IMPLICIT); if (this.currentType == Type.f32) op = UnaryOp.NegF32; else if (this.currentType == Type.f64) op = UnaryOp.NegF64; else return this.currentType.isLongInteger ? this.module.createBinary(BinaryOp.SubI64, this.module.createI64(0, 0), operand) : this.module.createBinary(BinaryOp.SubI32, this.module.createI32(0), operand); break; case Token.PLUS_PLUS: operand = this.compileExpression(operandExpression, contextualType, contextualType == Type.void ? ConversionKind.NONE : ConversionKind.IMPLICIT); return this.currentType == Type.f32 ? this.compileAssignmentWithValue(operandExpression, this.module.createBinary(BinaryOp.AddF32, operand, this.module.createF32(1)), contextualType != Type.void) : this.currentType == Type.f64 ? this.compileAssignmentWithValue(operandExpression, this.module.createBinary(BinaryOp.AddF64, operand, this.module.createF64(1)), contextualType != Type.void) : this.currentType.isLongInteger ? this.compileAssignmentWithValue(operandExpression, this.module.createBinary(BinaryOp.AddI64, operand, this.module.createI64(1, 0)), contextualType != Type.void) : this.compileAssignmentWithValue(operandExpression, this.module.createBinary(BinaryOp.AddI32, operand, this.module.createI32(1)), contextualType != Type.void); case Token.MINUS_MINUS: operand = this.compileExpression(operandExpression, contextualType, contextualType == Type.void ? ConversionKind.NONE : ConversionKind.IMPLICIT); return this.currentType == Type.f32 ? this.compileAssignmentWithValue(operandExpression, this.module.createBinary(BinaryOp.SubF32, operand, this.module.createF32(1)), contextualType != Type.void) : this.currentType == Type.f64 ? this.compileAssignmentWithValue(operandExpression, this.module.createBinary(BinaryOp.SubF64, operand, this.module.createF64(1)), contextualType != Type.void) : this.currentType.isLongInteger ? this.compileAssignmentWithValue(operandExpression, this.module.createBinary(BinaryOp.SubI64, operand, this.module.createI64(1, 0)), contextualType != Type.void) : this.compileAssignmentWithValue(operandExpression, this.module.createBinary(BinaryOp.SubI32, operand, this.module.createI32(1)), contextualType != Type.void); case Token.EXCLAMATION: operand = this.compileExpression(operandExpression, Type.bool, ConversionKind.NONE); if (this.currentType == Type.f32) { this.currentType = Type.bool; return this.module.createBinary(BinaryOp.EqF32, operand, this.module.createF32(0)); } if (this.currentType == Type.f64) { this.currentType = Type.bool; return this.module.createBinary(BinaryOp.EqF64, operand, this.module.createF64(0)); } op = this.currentType.isLongInteger ? UnaryOp.EqzI64 // TODO: does this yield i64 0/1? : UnaryOp.EqzI32; this.currentType = Type.bool; break; case Token.TILDE: operand = this.compileExpression(operandExpression, contextualType.isAnyFloat ? Type.i64 : contextualType, contextualType == Type.void ? ConversionKind.NONE : ConversionKind.IMPLICIT); return this.currentType.isLongInteger ? this.module.createBinary(BinaryOp.XorI64, operand, this.module.createI64(-1, -1)) : this.module.createBinary(BinaryOp.XorI32, operand, this.module.createI32(-1)); default: throw new Error("not implemented"); } return this.module.createUnary(op, operand); } } // helpers export function typeToNativeType(type: Type): NativeType { return type.kind == TypeKind.F32 ? NativeType.F32 : type.kind == TypeKind.F64 ? NativeType.F64 : type.isLongInteger ? NativeType.I64 : type.isAnyInteger || type.kind == TypeKind.BOOL ? NativeType.I32 : NativeType.None; } export function typesToNativeTypes(types: Type[]): NativeType[] { const k: i32 = types.length; const ret: NativeType[] = new Array(k); for (let i: i32 = 0; i < k; ++i) ret[i] = typeToNativeType(types[i]); return ret; } export function typeToNativeZero(module: Module, type: Type): ExpressionRef { return type.kind == TypeKind.F32 ? module.createF32(0) : type.kind == TypeKind.F64 ? module.createF64(0) : type.isLongInteger ? module.createI64(0, 0) : module.createI32(0); } export function typeToNativeOne(module: Module, type: Type): ExpressionRef { return type.kind == TypeKind.F32 ? module.createF32(1) : type.kind == TypeKind.F64 ? module.createF64(1) : type.isLongInteger ? module.createI64(1, 0) : module.createI32(1); } function typeToSignatureNamePart(type: Type): string { return type.kind == TypeKind.VOID ? "v" : type.kind == TypeKind.F32 ? "f" : type.kind == TypeKind.F64 ? "F" : type.isLongInteger ? "I" : "i"; } function typesToSignatureName(paramTypes: Type[], returnType: Type): string { sb.length = 0; for (let i: i32 = 0, k: i32 = paramTypes.length; i < k; ++i) sb.push(typeToSignatureNamePart(paramTypes[i])); sb.push(typeToSignatureNamePart(returnType)); return sb.join(""); }