/** * The AssemblyScript compiler. * @module compiler *//***/ import { compileCall as compileBuiltinCall, compileAllocate as compileBuiltinAllocate, compileAbort as compileBuiltinAbort } from "./builtins"; import { DiagnosticCode, DiagnosticEmitter } from "./diagnostics"; import { Module, MemorySegment, ExpressionRef, UnaryOp, BinaryOp, NativeType, FunctionRef, ExpressionId, FunctionTypeRef, GlobalRef } from "./module"; import { Program, ClassPrototype, Class, Element, ElementKind, Enum, Field, FunctionPrototype, Function, FunctionTarget, Global, Local, Namespace, EnumValue, Property, VariableLikeElement, FlowFlags, CommonFlags, ConstantValueKind, Flow, OperatorKind, DecoratorFlags, PATH_DELIMITER, INNER_DELIMITER, INSTANCE_DELIMITER, STATIC_DELIMITER, GETTER_PREFIX, SETTER_PREFIX } from "./program"; import { Token, operatorTokenToString } from "./tokenizer"; import { Node, NodeKind, TypeNode, Source, Range, Statement, BlockStatement, BreakStatement, ClassDeclaration, ContinueStatement, DoStatement, EmptyStatement, EnumDeclaration, ExportStatement, ExpressionStatement, FunctionDeclaration, ForStatement, IfStatement, ImportStatement, InterfaceDeclaration, NamespaceDeclaration, ReturnStatement, SwitchStatement, ThrowStatement, TryStatement, VariableDeclaration, VariableStatement, VoidStatement, WhileStatement, Expression, AssertionExpression, BinaryExpression, CallExpression, CommaExpression, ElementAccessExpression, FloatLiteralExpression, FunctionExpression, IdentifierExpression, IntegerLiteralExpression, LiteralExpression, LiteralKind, NewExpression, ParenthesizedExpression, PropertyAccessExpression, TernaryExpression, ArrayLiteralExpression, StringLiteralExpression, UnaryPostfixExpression, UnaryPrefixExpression, FieldDeclaration } from "./ast"; import { Type, TypeKind, TypeFlags, Signature, typesToNativeTypes } from "./types"; import { writeI32, writeI64, writeF32, writeF64 } from "./util"; /** Compilation target. */ export enum Target { /** WebAssembly with 32-bit pointers. */ WASM32, /** WebAssembly with 64-bit pointers. Experimental and not supported by any runtime yet. */ WASM64 } /** Compiler options. */ export class Options { /** WebAssembly target. Defaults to {@link Target.WASM32}. */ target: Target = Target.WASM32; /** If true, compiles everything instead of just reachable code. */ noTreeShaking: bool = false; /** If true, replaces assertions with nops. */ noAssert: bool = false; /** If true, does not set up a memory. */ noMemory: bool = false; /** If true, imports the memory provided by the embedder. */ importMemory: bool = false; /** If true, imports the function table provided by the embedder. */ importTable: bool = false; /** Static memory start offset. */ memoryBase: u32 = 0; /** If true, generates information necessary for source maps. */ sourceMap: bool = false; /** Global aliases. */ globalAliases: Map | null = null; /** Tests if the target is WASM64 or, otherwise, WASM32. */ get isWasm64(): bool { return this.target == Target.WASM64; } /** Gets the unsigned size type matching the target. */ get usizeType(): Type { return this.target == Target.WASM64 ? Type.usize64 : Type.usize32; } /** Gets the signed size type matching the target. */ get isizeType(): Type { return this.target == Target.WASM64 ? Type.isize64 : Type.isize32; } /** Gets the native size type matching the target. */ get nativeSizeType(): NativeType { return this.target == Target.WASM64 ? NativeType.I64 : NativeType.I32; } } /** Indicates the desired kind of a conversion. */ export const enum ConversionKind { /** No conversion. */ NONE, /** Implicit conversion. */ IMPLICIT, /** Explicit conversion. */ EXPLICIT } /** Compiler interface. */ export class Compiler extends DiagnosticEmitter { /** Program reference. */ program: Program; /** Provided options. */ options: Options; /** Module instance being compiled. */ module: Module; /** Current function in compilation. */ currentFunction: Function; /** Outer function in compilation, if compiling a function expression. */ outerFunction: Function | null = null; /** Current enum in compilation. */ currentEnum: Enum | null = null; /** Current type in compilation. */ currentType: Type = Type.void; /** Start function being compiled. */ startFunction: Function; /** Start function statements. */ startFunctionBody: ExpressionRef[] = []; /** Counting memory offset. */ memoryOffset: I64; /** Memory segments being compiled. */ memorySegments: MemorySegment[] = new Array(); /** Map of already compiled static string segments. */ stringSegments: Map = new Map(); /** Function table being compiled. */ functionTable: Function[] = new Array(); /** Argument count helper global. */ argcVar: GlobalRef = 0; /** Argument count helper setter. */ argcSet: FunctionRef = 0; /** Compiles a {@link Program} to a {@link Module} using the specified options. */ static compile(program: Program, options: Options | null = null): Module { return new Compiler(program, options).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; if (!options) options = new Options(); this.options = options; this.memoryOffset = i64_new( max(options.memoryBase, options.usizeType.byteSize) // leave space for `null` ); this.module = Module.create(); } /** Performs compilation of the underlying {@link Program} to a {@link Module}. */ compile(): Module { var options = this.options; var module = this.module; var program = this.program; // initialize lookup maps, built-ins, imports, exports, etc. program.initialize(options); // set up the start function wrapping top-level statements, of all files. var startFunctionPrototype = assert(program.elementsLookup.get("start")); assert(startFunctionPrototype.kind == ElementKind.FUNCTION_PROTOTYPE); var startFunctionInstance = new Function( startFunctionPrototype, startFunctionPrototype.internalName, new Signature([], Type.void) ); this.startFunction = startFunctionInstance; this.currentFunction = startFunctionInstance; // compile entry file(s) while traversing reachable elements var sources = program.sources; for (let i = 0, k = sources.length; i < k; ++i) { if (sources[i].isEntry) this.compileSource(sources[i]); } // compile the start function if not empty var startFunctionBody = this.startFunctionBody; if (startFunctionBody.length) { let signature = startFunctionInstance.signature; let funcRef = module.addFunction( startFunctionInstance.internalName, this.ensureFunctionType( signature.parameterTypes, signature.returnType, signature.thisType ), typesToNativeTypes(startFunctionInstance.additionalLocals), module.createBlock(null, startFunctionBody) ); startFunctionInstance.finalize(module, funcRef); module.setStart(funcRef); } // set up static memory segments and the heap base pointer if (!options.noMemory) { let memoryOffset = this.memoryOffset; memoryOffset = i64_align(memoryOffset, options.usizeType.byteSize); this.memoryOffset = memoryOffset; if (options.isWasm64) { module.addGlobal( "HEAP_BASE", NativeType.I64, false, module.createI64(i64_low(memoryOffset), i64_high(memoryOffset)) ); } else { module.addGlobal( "HEAP_BASE", NativeType.I32, false, module.createI32(i64_low(memoryOffset)) ); } // determine initial page size let pages = i64_shr_u(i64_align(memoryOffset, 0x10000), i64_new(16, 0)); module.setMemory( i64_low(pages), this.options.isWasm64 ? Module.MAX_MEMORY_WASM64 : Module.MAX_MEMORY_WASM32, this.memorySegments, options.target, "memory" ); } // import memory if requested (default memory is named '0' by Binaryen) if (options.importMemory) module.addMemoryImport("0", "env", "memory"); // set up function table var functionTable = this.functionTable; var functionTableSize = functionTable.length; var functionTableExported = false; if (functionTableSize) { let entries = new Array(functionTableSize); for (let i = 0; i < functionTableSize; ++i) { entries[i] = functionTable[i].ref; } module.setFunctionTable(entries); module.addTableExport("0", "table"); functionTableExported = true; } // import table if requested (default table is named '0' by Binaryen) if (options.importTable) { module.addTableImport("0", "env", "table"); if (!functionTableExported) module.addTableExport("0", "table"); } return module; } // sources /** Compiles a source by looking it up by path first. */ compileSourceByPath(normalizedPathWithoutExtension: string, reportNode: Node): void { var source = this.program.lookupSourceByPath(normalizedPathWithoutExtension); if (!source) { this.error( DiagnosticCode.File_0_not_found, reportNode.range, normalizedPathWithoutExtension ); return; } this.compileSource(source); } /** Compiles a source. */ compileSource(source: Source): void { if (source.is(CommonFlags.COMPILED)) return; source.set(CommonFlags.COMPILED); // compile top-level statements var noTreeShaking = this.options.noTreeShaking; var isEntry = source.isEntry; var startFunction = this.startFunction; var startFunctionBody = this.startFunctionBody; var statements = source.statements; for (let i = 0, k = statements.length; i < k; ++i) { let statement = statements[i]; switch (statement.kind) { case NodeKind.CLASSDECLARATION: { if ( (noTreeShaking || (isEntry && statement.is(CommonFlags.EXPORT))) && !(statement).isGeneric ) { this.compileClassDeclaration(statement, []); } break; } case NodeKind.INTERFACEDECLARATION: break; case NodeKind.ENUMDECLARATION: { if (noTreeShaking || (isEntry && statement.is(CommonFlags.EXPORT))) { this.compileEnumDeclaration(statement); } break; } case NodeKind.FUNCTIONDECLARATION: { if ( (noTreeShaking || (isEntry && statement.is(CommonFlags.EXPORT))) && !(statement).isGeneric ) { this.compileFunctionDeclaration(statement, []); } break; } case NodeKind.IMPORT: { this.compileSourceByPath( (statement).normalizedPath, (statement).path ); break; } case NodeKind.NAMESPACEDECLARATION: { if (noTreeShaking || (isEntry && statement.is(CommonFlags.EXPORT))) { this.compileNamespaceDeclaration(statement); } break; } case NodeKind.VARIABLE: { // global, always compiled as initializers might have side effects let variableInit = this.compileVariableStatement(statement); if (variableInit) startFunctionBody.push(variableInit); break; } case NodeKind.EXPORT: { if ((statement).normalizedPath != null) { this.compileSourceByPath( (statement).normalizedPath, (statement).path ); } if (noTreeShaking || isEntry) { this.compileExportStatement(statement); } break; } default: { // otherwise a top-level statement that is part of the start function's body let previousFunction = this.currentFunction; this.currentFunction = startFunction; startFunctionBody.push(this.compileStatement(statement)); this.currentFunction = previousFunction; break; } } } } // globals compileGlobalDeclaration(declaration: VariableDeclaration): Global | null { // look up the initialized program element var element = assert(this.program.elementsLookup.get(declaration.fileLevelInternalName)); assert(element.kind == ElementKind.GLOBAL); if (!this.compileGlobal(element)) return null; // reports return element; } compileGlobal(global: Global): bool { if (global.is(CommonFlags.COMPILED)) return true; global.set(CommonFlags.COMPILED); var module = this.module; var declaration = global.declaration; var initExpr: ExpressionRef = 0; if (global.type == Type.void) { // type is void if not yet resolved or not annotated if (declaration) { // resolve now if annotated if (declaration.type) { let resolvedType = this.program.resolveType(declaration.type); // reports if (!resolvedType) return false; if (resolvedType == Type.void) { this.error( DiagnosticCode.Type_expected, declaration.type.range ); return false; } global.type = resolvedType; // infer from initializer if not annotated } else if (declaration.initializer) { // infer type using void/NONE for literal inference initExpr = this.compileExpression( // reports declaration.initializer, Type.void, ConversionKind.NONE ); if (this.currentType == Type.void) { this.error( DiagnosticCode.Type_0_is_not_assignable_to_type_1, declaration.initializer.range, this.currentType.toString(), "" ); return false; } global.type = this.currentType; // must either be annotated or have an initializer } else { this.error( DiagnosticCode.Type_expected, declaration.name.range.atEnd ); return false; } } else { assert(false); // must have a declaration if 'void' (and thus resolved later on) } } // ambient builtins like 'HEAP_BASE' need to be resolved but are added explicitly if (global.is(CommonFlags.AMBIENT | CommonFlags.BUILTIN)) return true; var nativeType = global.type.toNativeType(); var isConstant = global.isAny(CommonFlags.CONST) || global.is(CommonFlags.STATIC | CommonFlags.READONLY); // handle imports if (global.is(CommonFlags.AMBIENT)) { // constant global if (isConstant) { global.set(CommonFlags.MODULE_IMPORT); module.addGlobalImport( global.internalName, global.parent ? global.parent.simpleName : "env", global.simpleName, nativeType ); global.set(CommonFlags.COMPILED); return true; // importing mutable globals is not supported in the MVP } else { this.error( DiagnosticCode.Operation_not_supported, assert(declaration).range ); } return false; } // the MVP does not yet support initializer expressions other than constant values (and // get_globals), hence such initializations must be performed in the start function for now. var initializeInStart = false; // inlined constant can be compiled as-is if (global.is(CommonFlags.INLINED)) { initExpr = this.compileInlineConstant(global, global.type, true); } else { // evaluate initializer if present if (declaration && declaration.initializer) { if (!initExpr) { initExpr = this.compileExpression(declaration.initializer, global.type); } // check if the initializer is constant if (_BinaryenExpressionGetId(initExpr) != ExpressionId.Const) { // if a constant global, check if the initializer becomes constant after precompute if (isConstant) { initExpr = this.precomputeExpressionRef(initExpr); if (_BinaryenExpressionGetId(initExpr) != ExpressionId.Const) { this.warning( DiagnosticCode.Compiling_constant_with_non_constant_initializer_as_mutable, declaration.range ); initializeInStart = true; } } else { initializeInStart = true; } } // initialize to zero if there's no initializer } else { initExpr = global.type.toNativeZero(module); } } var internalName = global.internalName; if (initializeInStart) { // initialize to mutable zero and set the actual value in start module.addGlobal(internalName, nativeType, true, global.type.toNativeZero(module)); this.startFunctionBody.push(module.createSetGlobal(internalName, initExpr)); } else { // compile as-is if (isConstant) { let exprType = _BinaryenExpressionGetType(initExpr); switch (exprType) { case NativeType.I32: { global.constantValueKind = ConstantValueKind.INTEGER; global.constantIntegerValue = i64_new(_BinaryenConstGetValueI32(initExpr), 0); break; } case NativeType.I64: { global.constantValueKind = ConstantValueKind.INTEGER; global.constantIntegerValue = i64_new( _BinaryenConstGetValueI64Low(initExpr), _BinaryenConstGetValueI64High(initExpr) ); break; } case NativeType.F32: { global.constantValueKind = ConstantValueKind.FLOAT; global.constantFloatValue = _BinaryenConstGetValueF32(initExpr); break; } case NativeType.F64: { global.constantValueKind = ConstantValueKind.FLOAT; global.constantFloatValue = _BinaryenConstGetValueF64(initExpr); break; } default: { assert(false); this.error( DiagnosticCode.Operation_not_supported, assert(global.declaration).range ); return false; } } global.set(CommonFlags.INLINED); // inline the value from now on if (global.is(CommonFlags.MODULE_EXPORT)) { module.addGlobal(internalName, nativeType, false, initExpr); module.addGlobalExport(internalName, mangleExportName(global)); } else if (declaration && declaration.isTopLevel) { // might become re-exported module.addGlobal(internalName, nativeType, false, initExpr); } } else /* mutable */ { module.addGlobal(internalName, nativeType, !isConstant, initExpr); } } return true; } // enums compileEnumDeclaration(declaration: EnumDeclaration): Enum | null { var element = assert(this.program.elementsLookup.get(declaration.fileLevelInternalName)); assert(element.kind == ElementKind.ENUM); if (!this.compileEnum(element)) return null; return element; } compileEnum(element: Enum): bool { if (element.is(CommonFlags.COMPILED)) return true; element.set(CommonFlags.COMPILED); var module = this.module; this.currentEnum = element; var previousValue: EnumValue | null = null; if (element.members) { for (let member of element.members.values()) { if (member.kind != ElementKind.ENUMVALUE) continue; // happens if an enum is also a namespace let initInStart = false; let val = member; let valueDeclaration = val.declaration; val.set(CommonFlags.COMPILED); if (val.is(CommonFlags.INLINED)) { if (element.declaration.isTopLevelExport) { module.addGlobal( val.internalName, NativeType.I32, false, // constant module.createI32(val.constantValue) ); } } else { let initExpr: ExpressionRef; if (valueDeclaration.value) { initExpr = this.compileExpression(valueDeclaration.value, Type.i32); if (_BinaryenExpressionGetId(initExpr) != ExpressionId.Const) { initExpr = this.precomputeExpressionRef(initExpr); if (_BinaryenExpressionGetId(initExpr) != ExpressionId.Const) { if (element.is(CommonFlags.CONST)) { this.warning( DiagnosticCode.Compiling_constant_with_non_constant_initializer_as_mutable, valueDeclaration.range ); } initInStart = true; } } } else if (previousValue == null) { initExpr = module.createI32(0); } else if (previousValue.is(CommonFlags.INLINED)) { initExpr = module.createI32(previousValue.constantValue + 1); } else { // in TypeScript this errors with TS1061, but actually we can do: initExpr = module.createBinary(BinaryOp.AddI32, module.createGetGlobal(previousValue.internalName, NativeType.I32), module.createI32(1) ); if (element.is(CommonFlags.CONST)) { this.warning( DiagnosticCode.Compiling_constant_with_non_constant_initializer_as_mutable, valueDeclaration.range ); } initInStart = true; } if (initInStart) { module.addGlobal( val.internalName, NativeType.I32, true, // mutable module.createI32(0) ); this.startFunctionBody.push(module.createSetGlobal(val.internalName, initExpr)); } else { module.addGlobal(val.internalName, NativeType.I32, false, initExpr); if (_BinaryenExpressionGetType(initExpr) == NativeType.I32) { val.constantValue = _BinaryenConstGetValueI32(initExpr); val.set(CommonFlags.INLINED); } else { assert(false); this.error( DiagnosticCode.Operation_not_supported, valueDeclaration.range ); val.constantValue = 0; } } } previousValue = val; // export values if the enum is exported if (element.is(CommonFlags.MODULE_EXPORT)) { if (member.is(CommonFlags.INLINED)) { module.addGlobalExport(member.internalName, mangleExportName(member)); } else if (valueDeclaration) { this.warning( DiagnosticCode.Cannot_export_a_mutable_global, valueDeclaration.range ); } } } } this.currentEnum = null; return true; } // functions /** Compiles a top-level function given its declaration. */ compileFunctionDeclaration( declaration: FunctionDeclaration, typeArguments: TypeNode[], contextualTypeArguments: Map | null = null ): Function | null { var element = assert(this.program.elementsLookup.get(declaration.fileLevelInternalName)); assert(element.kind == ElementKind.FUNCTION_PROTOTYPE); return this.compileFunctionUsingTypeArguments( // reports element, typeArguments, contextualTypeArguments, null, // no outer scope (is top level) (element).declaration.name ); } /** Resolves the specified type arguments prior to compiling the resulting function instance. */ compileFunctionUsingTypeArguments( prototype: FunctionPrototype, typeArguments: TypeNode[], contextualTypeArguments: Map | null, outerScope: Flow | null, reportNode: Node ): Function | null { var instance = prototype.resolveUsingTypeArguments( // reports typeArguments, contextualTypeArguments, reportNode ); if (!instance) return null; instance.outerScope = outerScope; if (!this.compileFunction(instance)) return null; return instance; } /** Either reuses or creates the function type matching the specified signature. */ private ensureFunctionType( parameterTypes: Type[] | null, returnType: Type, thisType: Type | null = null ): FunctionTypeRef { var numParameters = parameterTypes ? parameterTypes.length : 0; var paramTypes: NativeType[]; var index = 0; if (thisType) { paramTypes = new Array(1 + numParameters); paramTypes[0] = thisType.toNativeType(); index = 1; } else { paramTypes = new Array(numParameters); } if (parameterTypes) { for (let i = 0; i < numParameters; ++i, ++index) { paramTypes[index] = parameterTypes[i].toNativeType(); } } var resultType = returnType.toNativeType(); var module = this.module; var typeRef = module.getFunctionTypeBySignature(resultType, paramTypes); if (!typeRef) { let name = Signature.makeSignatureString(parameterTypes, returnType, thisType); typeRef = module.addFunctionType(name, resultType, paramTypes); } return typeRef; } /** Compiles a readily resolved function instance. */ compileFunction(instance: Function): bool { if (instance.is(CommonFlags.COMPILED)) return true; assert(!instance.is(CommonFlags.AMBIENT | CommonFlags.BUILTIN) || instance.internalName == "abort"); instance.set(CommonFlags.COMPILED); // check that modifiers are matching but still compile as-is var declaration = instance.prototype.declaration; var body = declaration.body; if (body) { if (instance.is(CommonFlags.AMBIENT)) { this.error( DiagnosticCode.An_implementation_cannot_be_declared_in_ambient_contexts, declaration.name.range ); } } else { if (!instance.is(CommonFlags.AMBIENT)) { this.error( DiagnosticCode.Function_implementation_is_missing_or_not_immediately_following_the_declaration, declaration.name.range ); } } var ref: FunctionRef; var signature = instance.signature; var typeRef = this.ensureFunctionType(signature.parameterTypes, signature.returnType, signature.thisType); var module = this.module; if (body) { let isConstructor = instance.is(CommonFlags.CONSTRUCTOR); let returnType: Type = instance.signature.returnType; // compile body let previousFunction = this.currentFunction; this.currentFunction = instance; let flow = instance.flow; let stmt: ExpressionRef; if (body.kind == NodeKind.EXPRESSION) { // () => expression assert(!instance.isAny(CommonFlags.CONSTRUCTOR | CommonFlags.GET | CommonFlags.SET)); assert(instance.is(CommonFlags.ARROW)); stmt = this.compileExpression((body).expression, returnType); flow.set(FlowFlags.RETURNS); } else { assert(body.kind == NodeKind.BLOCK); stmt = this.compileStatement(body); flow.finalize(); if (isConstructor) { let nativeSizeType = this.options.nativeSizeType; assert(instance.is(CommonFlags.INSTANCE)); // implicitly return `this` if the constructor doesn't always return on its own if (!flow.is(FlowFlags.RETURNS)) { // if all branches are guaranteed to allocate, skip the final conditional allocation if (flow.is(FlowFlags.ALLOCATES)) { stmt = module.createBlock(null, [ stmt, module.createGetLocal(0, nativeSizeType) ], nativeSizeType); // if not all branches are guaranteed to allocate, also append a conditional allocation } else { let parent = assert(instance.parent); assert(parent.kind == ElementKind.CLASS); stmt = module.createBlock(null, [ stmt, module.createTeeLocal(0, makeConditionalAllocate(this, parent, declaration.name) ) ], nativeSizeType); } } // make sure all branches return } else if (returnType != Type.void && !flow.is(FlowFlags.RETURNS)) { this.error( DiagnosticCode.A_function_whose_declared_type_is_not_void_must_return_a_value, declaration.signature.returnType.range ); } } this.currentFunction = previousFunction; // create the function ref = module.addFunction( instance.internalName, typeRef, typesToNativeTypes(instance.additionalLocals), stmt ); } else { instance.set(CommonFlags.MODULE_IMPORT); // create the function import let parent = instance.prototype.parent; ref = module.addFunctionImport( instance.internalName, parent ? parent.simpleName : "env", instance.simpleName, typeRef ); } // check module-level export if (instance.is(CommonFlags.MODULE_EXPORT)) { if (signature.requiredParameters < signature.parameterTypes.length) { // export the trampoline if the function takes optional parameters instance = this.ensureTrampoline(instance); this.ensureArgcSet(); } module.addFunctionExport(instance.internalName, mangleExportName(instance)); } instance.finalize(module, ref); return true; } // namespaces compileNamespaceDeclaration(declaration: NamespaceDeclaration): void { var members = declaration.members; var noTreeShaking = this.options.noTreeShaking; for (let i = 0, k = members.length; i < k; ++i) { let member = members[i]; switch (member.kind) { case NodeKind.CLASSDECLARATION: { if ( (noTreeShaking || member.is(CommonFlags.EXPORT)) && !(member).isGeneric ) { this.compileClassDeclaration(member, []); } break; } case NodeKind.INTERFACEDECLARATION: { if ( (noTreeShaking || member.is(CommonFlags.EXPORT)) && !(member).isGeneric ) { this.compileInterfaceDeclaration(member, []); } break; } case NodeKind.ENUMDECLARATION: { if (noTreeShaking || member.is(CommonFlags.EXPORT)) { this.compileEnumDeclaration(member); } break; } case NodeKind.FUNCTIONDECLARATION: { if ( (noTreeShaking || member.is(CommonFlags.EXPORT)) && !(member).isGeneric ) { this.compileFunctionDeclaration(member, []); } break; } case NodeKind.NAMESPACEDECLARATION: { if (noTreeShaking || member.is(CommonFlags.EXPORT)) { this.compileNamespaceDeclaration(member); } break; } case NodeKind.VARIABLE: { if (noTreeShaking || member.is(CommonFlags.EXPORT)) { let variableInit = this.compileVariableStatement(member, true); if (variableInit) this.startFunctionBody.push(variableInit); } break; } default: { assert(false); this.error( DiagnosticCode.Operation_not_supported, member.range ); break; } } } } compileNamespace(ns: Namespace): void { if (!ns.members) return; var noTreeShaking = this.options.noTreeShaking; for (let element of ns.members.values()) { switch (element.kind) { case ElementKind.CLASS_PROTOTYPE: { if ( ( noTreeShaking || (element).is(CommonFlags.EXPORT) ) && !(element).is(CommonFlags.GENERIC) ) { this.compileClassUsingTypeArguments(element, []); } break; } case ElementKind.ENUM: { this.compileEnum(element); break; } case ElementKind.FUNCTION_PROTOTYPE: { if ( ( noTreeShaking || (element).is(CommonFlags.EXPORT) ) && !(element).is(CommonFlags.GENERIC) ) { this.compileFunctionUsingTypeArguments( element, [], null, // no contextual type arguments null, // no outer scope (element).declaration.name ); } break; } case ElementKind.GLOBAL: { this.compileGlobal(element); break; } case ElementKind.NAMESPACE: { this.compileNamespace(element); break; } } } } // exports compileExportStatement(statement: ExportStatement): void { var module = this.module; var exports = this.program.fileLevelExports; var members = statement.members; for (let i = 0, k = members.length; i < k; ++i) { let member = members[i]; let internalExportName = ( statement.range.source.internalPath + PATH_DELIMITER + member.externalName.text ); let element = exports.get(internalExportName); if (!element) continue; // reported in Program#initialize switch (element.kind) { case ElementKind.CLASS_PROTOTYPE: { if (!(element).is(CommonFlags.GENERIC)) { this.compileClassUsingTypeArguments(element, []); } break; } case ElementKind.ENUM: { this.compileEnum(element); break; } case ElementKind.FUNCTION_PROTOTYPE: { if ( !(element).is(CommonFlags.GENERIC) && statement.range.source.isEntry ) { let functionInstance = this.compileFunctionUsingTypeArguments( element, [], null, // no contextual type arguments null, // no outer scope (element).declaration.name ); if (functionInstance) { let functionDeclaration = functionInstance.prototype.declaration; if (functionDeclaration && functionDeclaration.needsExplicitExport(member)) { module.addFunctionExport(functionInstance.internalName, member.externalName.text); } } } break; } case ElementKind.GLOBAL: { if (this.compileGlobal(element) && statement.range.source.isEntry) { let globalDeclaration = (element).declaration; if (globalDeclaration && globalDeclaration.needsExplicitExport(member)) { if ((element).is(CommonFlags.INLINED)) { module.addGlobalExport(element.internalName, member.externalName.text); } 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 { var element = assert(this.program.elementsLookup.get(declaration.fileLevelInternalName)); assert(element.kind == ElementKind.CLASS_PROTOTYPE); this.compileClassUsingTypeArguments( element, typeArguments, contextualTypeArguments, alternativeReportNode ); } compileClassUsingTypeArguments( prototype: ClassPrototype, typeArguments: TypeNode[], contextualTypeArguments: Map | null = null, alternativeReportNode: Node | null = null ): void { var instance = prototype.resolveUsingTypeArguments( // reports typeArguments, contextualTypeArguments, alternativeReportNode ); if (!instance) return; this.compileClass(instance); } compileClass(instance: Class): bool { if (instance.is(CommonFlags.COMPILED)) return true; instance.set(CommonFlags.COMPILED); var staticMembers = instance.prototype.members; if (staticMembers) { for (let element of staticMembers.values()) { switch (element.kind) { case ElementKind.GLOBAL: { this.compileGlobal(element); break; } case ElementKind.FUNCTION_PROTOTYPE: { if ( !(element).is(CommonFlags.GENERIC) ) { this.compileFunctionUsingTypeArguments( element, [], null, null, (element).declaration.name ); } break; } case ElementKind.PROPERTY: { let getter = (element).getterPrototype; if (getter) { this.compileFunctionUsingTypeArguments( getter, [], null, null, getter.declaration.name ); } let setter = (element).setterPrototype; if (setter) { this.compileFunctionUsingTypeArguments( setter, [], null, null, setter.declaration.name ); } break; } } } } var ctorInstance = instance.constructorInstance; if (ctorInstance) this.compileFunction(ctorInstance); var instanceMembers = instance.members; if (instanceMembers) { for (let element of instanceMembers.values()) { switch (element.kind) { case ElementKind.FUNCTION_PROTOTYPE: { if ( !(element).is(CommonFlags.GENERIC) ) { this.compileFunctionUsingTypeArguments( element, [], instance.contextualTypeArguments, null, // no outer scope (element).declaration.name ); } break; } case ElementKind.FIELD: { element.set(CommonFlags.COMPILED); if (!instance.is(CommonFlags.MODULE_EXPORT) || element.is(CommonFlags.PRIVATE)) break; let module = this.module; let name = (element).simpleName; let type = (element).type; let nativeType = type.toNativeType(); let offset = (element).memoryOffset; let usizeType = this.options.usizeType; let nativeSizeType = this.options.nativeSizeType; // export an implicit getter: get:fieldName(this: usize) -> fieldType let getterName = mangleExportName(element, GETTER_PREFIX + name); module.addFunction( getterName, this.ensureFunctionType(null, type, usizeType), null, module.createLoad( type.byteSize, type.is(TypeFlags.SIGNED), module.createGetLocal(0, nativeSizeType), nativeType, offset ) ); module.addFunctionExport(getterName, getterName); // export an implicit setter: set:fieldName(this: usize, value: fieldType) -> void if (element.is(CommonFlags.READONLY)) break; let setterName = mangleExportName(element, SETTER_PREFIX + name); module.addFunction( setterName, this.ensureFunctionType([ type ], Type.void, usizeType), null, module.createStore( type.byteSize, module.createGetLocal(0, nativeSizeType), module.createGetLocal(1, nativeType), nativeType, offset ) ); module.addFunctionExport(setterName, setterName); break; } case ElementKind.PROPERTY: { let getter = (element).getterPrototype; if (getter) { this.compileFunctionUsingTypeArguments( getter, [], instance.contextualTypeArguments, null, getter.declaration.name ); } let setter = (element).setterPrototype; if (setter) { this.compileFunctionUsingTypeArguments( setter, [], instance.contextualTypeArguments, null, setter.declaration.name ); } break; } } } } return true; } compileInterfaceDeclaration( declaration: InterfaceDeclaration, typeArguments: TypeNode[], contextualTypeArguments: Map | null = null, alternativeReportNode: Node | null = null ): void { // TODO this.error( DiagnosticCode.Operation_not_supported, declaration.range ); } // memory /** Adds a static memory segment with the specified data. */ addMemorySegment(buffer: Uint8Array, alignment: i32 = 8): MemorySegment { var memoryOffset = i64_align(this.memoryOffset, alignment); var segment = MemorySegment.create(buffer, memoryOffset); this.memorySegments.push(segment); this.memoryOffset = i64_add(memoryOffset, i64_new(buffer.length, 0)); return segment; } // function table /** Ensures that a table entry exists for the specified function and returns its index. */ ensureFunctionTableEntry(func: Function): i32 { assert(func.is(CommonFlags.COMPILED)); if (func.functionTableIndex >= 0) { return func.functionTableIndex; } var functionTable = this.functionTable; var index = functionTable.length; if (!func.is(CommonFlags.TRAMPOLINE) && func.signature.requiredParameters < func.signature.parameterTypes.length) { // insert the trampoline if the function has optional parameters func = this.ensureTrampoline(func); } functionTable.push(func); func.functionTableIndex = index; return index; } // statements compileStatement(statement: Statement): ExpressionRef { var module = this.module; var expr: ExpressionRef; switch (statement.kind) { case NodeKind.BLOCK: { expr = this.compileBlockStatement(statement); break; } case NodeKind.BREAK: { expr = this.compileBreakStatement(statement); break; } case NodeKind.CONTINUE: { expr = this.compileContinueStatement(statement); break; } case NodeKind.DO: { expr = this.compileDoStatement(statement); break; } case NodeKind.EMPTY: { expr = this.compileEmptyStatement(statement); break; } case NodeKind.EXPRESSION: { expr = this.compileExpressionStatement(statement); break; } case NodeKind.FOR: { expr = this.compileForStatement(statement); break; } case NodeKind.IF: { expr = this.compileIfStatement(statement); break; } case NodeKind.RETURN: { expr = this.compileReturnStatement(statement); break; } case NodeKind.SWITCH: { expr = this.compileSwitchStatement(statement); break; } case NodeKind.THROW: { expr = this.compileThrowStatement(statement); break; } case NodeKind.TRY: { expr = this.compileTryStatement(statement); break; } case NodeKind.VARIABLE: { expr = this.compileVariableStatement(statement); if (!expr) expr = module.createNop(); break; } case NodeKind.VOID: { expr = this.compileVoidStatement(statement); break; } case NodeKind.WHILE: { expr = this.compileWhileStatement(statement); break; } case NodeKind.TYPEDECLARATION: { // type declarations must be top-level because function bodies are evaluated when // reachaable only. if (this.currentFunction == this.startFunction) { return module.createNop(); } // otherwise fall-through } default: { this.error( DiagnosticCode.Operation_not_supported, statement.range ); assert(false); expr = module.createUnreachable(); break; } } if (this.options.sourceMap) { addDebugLocation(expr, statement.range, module, this.currentFunction); } return expr; } compileStatements(statements: Statement[]): ExpressionRef[] { var numStatements = statements.length; var stmts = new Array(numStatements); for (let i = 0; i < numStatements; ++i) { stmts[i] = this.compileStatement(statements[i]); } return stmts; // array of 0-es in noEmit-mode } compileBlockStatement(statement: BlockStatement): ExpressionRef { var statements = statement.statements; // NOTE that we could optimize this to a NOP if empty or unwrap a single // statement, but that's not what the source told us to do and left to the // optimizer. // Not actually a branch, but can contain its own scoped variables. var flow = this.currentFunction.flow.enterBranchOrScope(); this.currentFunction.flow = flow; var stmt = this.module.createBlock(null, this.compileStatements(statements), NativeType.None); var stmtReturns = flow.is(FlowFlags.RETURNS); var stmtThrows = flow.is(FlowFlags.THROWS); var stmtAllocates = flow.is(FlowFlags.ALLOCATES); // Switch back to the parent flow flow = flow.leaveBranchOrScope(); this.currentFunction.flow = flow; if (stmtReturns) flow.set(FlowFlags.RETURNS); if (stmtThrows) flow.set(FlowFlags.THROWS); if (stmtAllocates) flow.set(FlowFlags.ALLOCATES); return stmt; } compileBreakStatement(statement: BreakStatement): ExpressionRef { var module = this.module; if (statement.label) { this.error( DiagnosticCode.Operation_not_supported, statement.label.range ); return module.createUnreachable(); } var flow = this.currentFunction.flow; var breakLabel = flow.breakLabel; if (breakLabel == null) { this.error( DiagnosticCode.A_break_statement_can_only_be_used_within_an_enclosing_iteration_or_switch_statement, statement.range ); return module.createUnreachable(); } flow.set(FlowFlags.BREAKS); return module.createBreak(breakLabel); } compileContinueStatement(statement: ContinueStatement): ExpressionRef { var module = this.module; var label = statement.label; if (label) { this.error( DiagnosticCode.Operation_not_supported, label.range ); return module.createUnreachable(); } // Check if 'continue' is allowed here var flow = this.currentFunction.flow; var continueLabel = flow.continueLabel; if (continueLabel == null) { this.error( DiagnosticCode.A_continue_statement_can_only_be_used_within_an_enclosing_iteration_statement, statement.range ); return module.createUnreachable(); } flow.set(FlowFlags.CONTINUES); return module.createBreak(continueLabel); } compileDoStatement(statement: DoStatement): ExpressionRef { // A do statement does not initiate a new branch because it is executed at // least once, but has its own break and continue labels. var currentFunction = this.currentFunction; var label = currentFunction.enterBreakContext(); var flow = currentFunction.flow; var previousBreakLabel = flow.breakLabel; var previousContinueLabel = flow.continueLabel; var breakLabel = "break|" + label; flow.breakLabel = breakLabel; var continueLabel = "continue|" + label; flow.continueLabel = continueLabel; var body = this.compileStatement(statement.statement); // Reset to the previous break and continue labels, if any. flow.breakLabel = previousBreakLabel; flow.continueLabel = previousContinueLabel; var module = this.module; var condExpr = makeIsTrueish( this.compileExpression(statement.condition, Type.i32, ConversionKind.NONE), this.currentType, module ); // No need to eliminate the condition in generic contexts as the statement is executed anyway. this.currentFunction.leaveBreakContext(); return module.createBlock(breakLabel, [ module.createLoop(continueLabel, module.createBlock(null, [ body, module.createBreak(continueLabel, condExpr) ], NativeType.None)) ], NativeType.None); } compileEmptyStatement(statement: EmptyStatement): ExpressionRef { return this.module.createNop(); } compileExpressionStatement(statement: ExpressionStatement): ExpressionRef { var expr = 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 { // A for statement initiates a new branch with its own scoped variables // possibly declared in its initializer, and break context. var currentFunction = this.currentFunction; var context = currentFunction.enterBreakContext(); var flow = currentFunction.flow.enterBranchOrScope(); currentFunction.flow = flow; var breakLabel = flow.breakLabel = "break|" + context; flow.breakLabel = breakLabel; var continueLabel = "continue|" + context; flow.continueLabel = continueLabel; // Compile in correct order var module = this.module; var initializer = statement.initializer ? this.compileStatement(statement.initializer) : module.createNop(); var condition = statement.condition ? this.compileExpression(statement.condition, Type.i32) : module.createI32(1); var incrementor = statement.incrementor ? this.compileExpression(statement.incrementor, Type.void) : module.createNop(); var body = this.compileStatement(statement.statement); var alwaysReturns = !statement.condition && flow.is(FlowFlags.RETURNS); var alwaysThrows = !statement.condition && flow.is(FlowFlags.THROWS); var alwaysAllocates = !statement.condition && flow.is(FlowFlags.ALLOCATES); // TODO: check other always-true conditions as well, not just omitted if (alwaysReturns) flow.set(FlowFlags.RETURNS); if (alwaysThrows) flow.set(FlowFlags.THROWS); if (alwaysAllocates) flow.set(FlowFlags.ALLOCATES); // Switch back to the parent flow currentFunction.flow = flow.leaveBranchOrScope(); currentFunction.leaveBreakContext(); var expr = module.createBlock(breakLabel, [ initializer, module.createLoop(continueLabel, module.createBlock(null, [ module.createIf(condition, module.createBlock(null, [ body, incrementor, module.createBreak(continueLabel) ], NativeType.None)) ], NativeType.None)) ], NativeType.None); // If the loop is guaranteed to run and return, append a hint if (alwaysReturns || alwaysThrows) { expr = module.createBlock(null, [ expr, module.createUnreachable() ]); } return expr; } compileIfStatement(statement: IfStatement): ExpressionRef { var module = this.module; var currentFunction = this.currentFunction; var ifTrue = statement.ifTrue; var ifFalse = statement.ifFalse; // The condition doesn't initiate a branch yet var condExpr = makeIsTrueish( this.compileExpression(statement.condition, Type.i32, ConversionKind.NONE), this.currentType, module ); if ( !this.options.noTreeShaking || this.currentFunction.isAny(CommonFlags.GENERIC | CommonFlags.GENERIC_CONTEXT) ) { // Try to eliminate unnecesssary branches if the condition is constant let condExprPrecomp = this.precomputeExpressionRef(condExpr); if ( _BinaryenExpressionGetId(condExprPrecomp) == ExpressionId.Const && _BinaryenExpressionGetType(condExprPrecomp) == NativeType.I32 ) { return _BinaryenConstGetValueI32(condExprPrecomp) ? this.compileStatement(ifTrue) : ifFalse ? this.compileStatement(ifFalse) : module.createNop(); // Otherwise recompile to the original and let the optimizer decide } else /* if (condExpr != condExprPrecomp) <- not guaranteed */ { condExpr = makeIsTrueish( this.compileExpression(statement.condition, Type.i32, ConversionKind.NONE), this.currentType, module ); } } // Each arm initiates a branch var flow = currentFunction.flow.enterBranchOrScope(); currentFunction.flow = flow; var ifTrueExpr = this.compileStatement(ifTrue); var ifTrueReturns = flow.is(FlowFlags.RETURNS); var ifTrueThrows = flow.is(FlowFlags.THROWS); var ifTrueAllocates = flow.is(FlowFlags.ALLOCATES); flow = flow.leaveBranchOrScope(); currentFunction.flow = flow; var ifFalseExpr: ExpressionRef = 0; var ifFalseReturns = false; var ifFalseThrows = false; var ifFalseAllocates = false; if (ifFalse) { flow = flow.enterBranchOrScope(); currentFunction.flow = flow; ifFalseExpr = this.compileStatement(ifFalse); ifFalseReturns = flow.is(FlowFlags.RETURNS); ifFalseThrows = flow.is(FlowFlags.THROWS); ifFalseAllocates = flow.is(FlowFlags.ALLOCATES); flow = flow.leaveBranchOrScope(); currentFunction.flow = flow; } if (ifTrueReturns && ifFalseReturns) flow.set(FlowFlags.RETURNS); if (ifTrueThrows && ifFalseThrows) flow.set(FlowFlags.THROWS); if (ifTrueAllocates && ifFalseAllocates) flow.set(FlowFlags.ALLOCATES); return module.createIf(condExpr, ifTrueExpr, ifFalseExpr); } compileReturnStatement(statement: ReturnStatement): ExpressionRef { var module = this.module; var currentFunction = this.currentFunction; var expression: ExpressionRef = 0; var flow = currentFunction.flow; // Remember that this flow returns flow.set(FlowFlags.RETURNS); // When inlining, break to the end of the inlined function's block if (flow.is(FlowFlags.INLINE_CONTEXT)) { if (statement.value) { expression = this.compileExpression( statement.value, assert(flow.returnType) ); } return module.createBreak(assert(flow.returnLabel), 0, expression); } // Otherwise return as usual if (statement.value) { expression = this.compileExpression( statement.value, flow.returnType ); } return module.createReturn(expression); } compileSwitchStatement(statement: SwitchStatement): ExpressionRef { var module = this.module; var currentFunction = this.currentFunction; // Everything within a switch uses the same break context var context = currentFunction.enterBreakContext(); // introduce a local for evaluating the condition (exactly once) var tempLocal = currentFunction.getTempLocal(Type.u32); var tempLocalIndex = tempLocal.index; var cases = statement.cases; var numCases = cases.length; // Prepend initializer to inner block. Does not initiate a new branch, yet. var breaks = new Array(1 + numCases); breaks[0] = module.createSetLocal( // initializer tempLocalIndex, this.compileExpression(statement.condition, Type.u32) ); // make one br_if per (possibly dynamic) labeled case (binaryen optimizes to br_table where possible) var breakIndex = 1; var defaultIndex = -1; for (let i = 0; i < numCases; ++i) { let case_ = cases[i]; let label = case_.label; if (label) { breaks[breakIndex++] = module.createBreak("case" + i.toString(10) + "|" + context, module.createBinary(BinaryOp.EqI32, module.createGetLocal(tempLocalIndex, NativeType.I32), this.compileExpression(label, Type.i32) ) ); } else { defaultIndex = i; } } currentFunction.freeTempLocal(tempLocal); // otherwise br to default respectively out of the switch if there is no default case breaks[breakIndex] = module.createBreak((defaultIndex >= 0 ? "case" + defaultIndex.toString(10) : "break" ) + "|" + context); // nest blocks in order var currentBlock = module.createBlock("case0|" + context, breaks, NativeType.None); var alwaysReturns = true; var alwaysThrows = true; var alwaysAllocates = true; for (let i = 0; i < numCases; ++i) { let case_ = cases[i]; let statements = case_.statements; let numStatements = statements.length; let body = new Array(1 + numStatements); body[0] = currentBlock; // Each switch case initiates a new branch let flow = currentFunction.flow.enterBranchOrScope(); currentFunction.flow = flow; let breakLabel = "break|" + context; flow.breakLabel = breakLabel; let fallsThrough = i != numCases - 1; let nextLabel = !fallsThrough ? breakLabel : "case" + (i + 1).toString(10) + "|" + context; for (let j = 0; j < numStatements; ++j) { body[j + 1] = this.compileStatement(statements[j]); } if (!(fallsThrough || flow.is(FlowFlags.RETURNS))) { alwaysReturns = false; // ignore fall-throughs } if (!(fallsThrough || flow.is(FlowFlags.THROWS))) { alwaysThrows = false; } if (!(fallsThrough || flow.is(FlowFlags.ALLOCATES))) { alwaysAllocates = false; } // Switch back to the parent flow currentFunction.flow = flow.leaveBranchOrScope(); currentBlock = module.createBlock(nextLabel, body, NativeType.None); } currentFunction.leaveBreakContext(); // If the switch has a default and always returns, propagate that if (defaultIndex >= 0) { let flow = currentFunction.flow; if (alwaysReturns) flow.set(FlowFlags.RETURNS); if (alwaysThrows) flow.set(FlowFlags.THROWS); if (alwaysAllocates) flow.set(FlowFlags.ALLOCATES); } return currentBlock; } compileThrowStatement(statement: ThrowStatement): ExpressionRef { var flow = this.currentFunction.flow; // Remember that this branch throws flow.set(FlowFlags.THROWS); // FIXME: without try-catch it is safe to assume RETURNS as well for now flow.set(FlowFlags.RETURNS); // TODO: requires exception-handling spec. return compileBuiltinAbort(this, null, statement); } compileTryStatement(statement: TryStatement): ExpressionRef { // TODO // can't yet support something like: try { return ... } finally { ... } // worthwhile to investigate lowering returns to block results (here)? this.error( DiagnosticCode.Operation_not_supported, statement.range ); return this.module.createUnreachable(); } /** * Compiles a variable statement. Returns `0` if an initializer is not * necessary. */ compileVariableStatement(statement: VariableStatement, isKnownGlobal: bool = false): ExpressionRef { var program = this.program; var currentFunction = this.currentFunction; var declarations = statement.declarations; var numDeclarations = declarations.length; // top-level variables and constants become globals if (isKnownGlobal || ( currentFunction == this.startFunction && statement.parent && statement.parent.kind == NodeKind.SOURCE )) { // NOTE that the above condition also covers top-level variables declared with 'let', even // though such variables could also become start function locals if, and only if, not used // within any function declared in the same source, which is unknown at this point. the only // efficient way to deal with this would be to keep track of all occasions it is used and // replace these instructions afterwards, dynamically. (TOOD: what about a Binaryen pass?) for (let i = 0; i < numDeclarations; ++i) { this.compileGlobalDeclaration(declarations[i]); } return 0; } // other variables become locals var initializers = new Array(); var flow = this.currentFunction.flow; for (let i = 0; i < numDeclarations; ++i) { let declaration = declarations[i]; let name = declaration.name.text; let type: Type | null = null; let init: ExpressionRef = 0; if (declaration.type) { type = program.resolveType( // reports declaration.type, flow.contextualTypeArguments ); if (!type) continue; if (declaration.initializer) { init = this.compileExpression(declaration.initializer, type); // reports } } else if (declaration.initializer) { // infer type using void/NONE for proper literal inference init = this.compileExpression( // reports declaration.initializer, Type.void, ConversionKind.NONE ); if (this.currentType == Type.void) { this.error( DiagnosticCode.Type_0_is_not_assignable_to_type_1, declaration.range, this.currentType.toString(), "" ); continue; } type = this.currentType; } else { this.error( DiagnosticCode.Type_expected, declaration.name.range.atEnd ); continue; } let isInlined = false; if (declaration.is(CommonFlags.CONST)) { if (init) { init = this.precomputeExpressionRef(init); if (_BinaryenExpressionGetId(init) == ExpressionId.Const) { let local = new Local(program, name, -1, type); switch (_BinaryenExpressionGetType(init)) { case NativeType.I32: { local = local.withConstantIntegerValue(_BinaryenConstGetValueI32(init), 0); break; } case NativeType.I64: { local = local.withConstantIntegerValue( _BinaryenConstGetValueI64Low(init), _BinaryenConstGetValueI64High(init) ); break; } case NativeType.F32: { local = local.withConstantFloatValue(_BinaryenConstGetValueF32(init)); break; } case NativeType.F64: { local = local.withConstantFloatValue(_BinaryenConstGetValueF64(init)); break; } default: { assert(false); this.error( DiagnosticCode.Operation_not_supported, declaration.range ); return this.module.createUnreachable(); } } // Create a virtual local that doesn't actually exist in WebAssembly let scopedLocals = currentFunction.flow.scopedLocals; if (!scopedLocals) currentFunction.flow.scopedLocals = scopedLocals = new Map(); else if (scopedLocals.has(name)) { this.error( DiagnosticCode.Duplicate_identifier_0, declaration.name.range, name ); return this.module.createUnreachable(); } scopedLocals.set(name, local); isInlined = true; } else { this.warning( DiagnosticCode.Compiling_constant_with_non_constant_initializer_as_mutable, declaration.range ); } } else { this.error( DiagnosticCode._const_declarations_must_be_initialized, declaration.range ); } } if (!isInlined) { if ( declaration.isAny(CommonFlags.LET | CommonFlags.CONST) || flow.is(FlowFlags.INLINE_CONTEXT) ) { // here: not top-level flow.addScopedLocal(type, name, declaration); // reports } else { currentFunction.addLocal(type, name, declaration); // reports } if (init) { initializers.push(this.compileAssignmentWithValue(declaration.name, init)); } } } return initializers.length // we can unwrap these here because the ? initializers.length == 1 // source didn't tell us exactly what to do ? initializers[0] : this.module.createBlock(null, initializers, NativeType.None) : 0; } compileVoidStatement(statement: VoidStatement): ExpressionRef { return this.compileExpression(statement.expression, Type.void, ConversionKind.EXPLICIT, false); } compileWhileStatement(statement: WhileStatement): ExpressionRef { var module = this.module; // The condition does not yet initialize a branch var condExpr = makeIsTrueish( this.compileExpression(statement.condition, Type.i32, ConversionKind.NONE), this.currentType, module ); if ( !this.options.noTreeShaking || this.currentFunction.isAny(CommonFlags.GENERIC | CommonFlags.GENERIC_CONTEXT) ) { // Try to eliminate unnecesssary loops if the condition is constant let condExprPrecomp = this.precomputeExpressionRef(condExpr); if ( _BinaryenExpressionGetId(condExprPrecomp) == ExpressionId.Const && _BinaryenExpressionGetType(condExprPrecomp) == NativeType.I32 ) { if (!_BinaryenConstGetValueI32(condExprPrecomp)) return module.createNop(); // Otherwise recompile to the original and let the optimizer decide } else /* if (condExpr != condExprPrecomp) <- not guaranteed */ { condExpr = makeIsTrueish( this.compileExpression(statement.condition, Type.i32, ConversionKind.NONE), this.currentType, module ); } } // Statements initiate a new branch with its own break context var currentFunction = this.currentFunction; var label = currentFunction.enterBreakContext(); var flow = currentFunction.flow.enterBranchOrScope(); currentFunction.flow = flow; var breakLabel = "break|" + label; flow.breakLabel = breakLabel; var continueLabel = "continue|" + label; flow.continueLabel = continueLabel; var body = this.compileStatement(statement.statement); var alwaysReturns = false; // CONDITION_IS_ALWAYS_TRUE && flow.is(FlowFlags.RETURNS); // TODO: evaluate if condition is always true // Switch back to the parent flow currentFunction.flow = flow.leaveBranchOrScope(); currentFunction.leaveBreakContext(); var expr = module.createBlock(breakLabel, [ module.createLoop(continueLabel, module.createIf(condExpr, module.createBlock(null, [ body, module.createBreak(continueLabel) ], NativeType.None)) ) ], NativeType.None); // If the loop is guaranteed to run and return, propagate that and append a hint if (alwaysReturns) { expr = module.createBlock(null, [ expr, module.createUnreachable() ]); } return expr; } // expressions /** * Compiles the value of an inlined constant element. * @param retainType If true, the annotated type of the constant is retained. Otherwise, the value * is precomputed according to context. */ compileInlineConstant( element: VariableLikeElement, contextualType: Type, retainType: bool ): ExpressionRef { assert(element.is(CommonFlags.INLINED)); var type = element.type; switch ( !retainType && type.is(TypeFlags.INTEGER) && contextualType.is(TypeFlags.INTEGER) && type.size < contextualType.size ? (this.currentType = contextualType).kind // essentially precomputes a (sign-)extension : (this.currentType = type).kind ) { case TypeKind.I8: case TypeKind.I16: { let shift = type.computeSmallIntegerShift(Type.i32); return this.module.createI32( element.constantValueKind == ConstantValueKind.INTEGER ? i64_low(element.constantIntegerValue) << shift >> shift : 0 ); } case TypeKind.U8: case TypeKind.U16: case TypeKind.BOOL: { let mask = element.type.computeSmallIntegerMask(Type.i32); return this.module.createI32( element.constantValueKind == ConstantValueKind.INTEGER ? i64_low(element.constantIntegerValue) & mask : 0 ); } case TypeKind.I32: case TypeKind.U32: { return this.module.createI32( element.constantValueKind == ConstantValueKind.INTEGER ? i64_low(element.constantIntegerValue) : 0 ); } case TypeKind.ISIZE: case TypeKind.USIZE: { if (!element.program.options.isWasm64) { return this.module.createI32( element.constantValueKind == ConstantValueKind.INTEGER ? i64_low(element.constantIntegerValue) : 0 ); } // fall-through } case TypeKind.I64: case TypeKind.U64: { return element.constantValueKind == ConstantValueKind.INTEGER ? this.module.createI64( i64_low(element.constantIntegerValue), i64_high(element.constantIntegerValue) ) : this.module.createI64(0); } case TypeKind.F64: { if (!(element.is(CommonFlags.BUILTIN) && contextualType == Type.f32)) { return this.module.createF64((element).constantFloatValue); } // otherwise fall-through: basically precomputes f32.demote/f64 of NaN / Infinity this.currentType = Type.f32; } case TypeKind.F32: { return this.module.createF32((element).constantFloatValue); } default: { assert(false); return this.module.createUnreachable(); } } } compileExpression( expression: Expression, contextualType: Type, conversionKind: ConversionKind = ConversionKind.IMPLICIT, wrapSmallIntegers: bool = true ): ExpressionRef { this.currentType = contextualType; var expr: ExpressionRef; switch (expression.kind) { case NodeKind.ASSERTION: { expr = this.compileAssertionExpression(expression, contextualType); break; } case NodeKind.BINARY: { expr = this.compileBinaryExpression(expression, contextualType, wrapSmallIntegers); break; } case NodeKind.CALL: { expr = this.compileCallExpression(expression, contextualType); break; } case NodeKind.COMMA: { expr = this.compileCommaExpression(expression, contextualType); break; } case NodeKind.ELEMENTACCESS: { expr = this.compileElementAccessExpression(expression, contextualType); break; } case NodeKind.FUNCTION: { expr = this.compileFunctionExpression(expression, contextualType); break; } case NodeKind.IDENTIFIER: case NodeKind.FALSE: case NodeKind.NULL: case NodeKind.THIS: case NodeKind.SUPER: case NodeKind.TRUE: { expr = this.compileIdentifierExpression( expression, contextualType, conversionKind == ConversionKind.NONE // retain type of inlined constants ); 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, wrapSmallIntegers ); break; } case NodeKind.PROPERTYACCESS: { expr = this.compilePropertyAccessExpression( expression, contextualType, conversionKind == ConversionKind.NONE // retain type of inlined constants ); 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, wrapSmallIntegers); break; } default: { assert(false); this.error( DiagnosticCode.Operation_not_supported, expression.range ); expr = this.module.createUnreachable(); break; } } var currentType = this.currentType; if (conversionKind != ConversionKind.NONE && currentType != contextualType) { expr = this.convertExpression(expr, currentType, contextualType, conversionKind, expression); this.currentType = contextualType; } if (this.options.sourceMap) { addDebugLocation(expr, expression.range, this.module, this.currentFunction); } return expr; } compileExpressionRetainType( expression: Expression, contextualType: Type, wrapSmallIntegers: bool = true ): ExpressionRef { return this.compileExpression( expression, contextualType == Type.void ? Type.i32 : contextualType, ConversionKind.NONE, wrapSmallIntegers ); } precomputeExpression( expression: Expression, contextualType: Type, conversionKind: ConversionKind = ConversionKind.IMPLICIT ): ExpressionRef { return this.precomputeExpressionRef(this.compileExpression(expression, contextualType, conversionKind)); } precomputeExpressionRef(expr: ExpressionRef): ExpressionRef { var module = this.module; var type = this.currentType; var nativeType = type.toNativeType(); var typeRef = module.getFunctionTypeBySignature(nativeType, null); var typeRefAdded = false; if (!typeRef) { typeRef = module.addFunctionType(type.toSignatureString(), nativeType, null); typeRefAdded = true; } var funcRef = module.addFunction("__precompute", typeRef, null, expr); module.runPasses([ "precompute" ], funcRef); var ret = _BinaryenFunctionGetBody(funcRef); module.removeFunction("__precompute"); if (typeRefAdded) { // TODO: also remove the function type somehow if no longer used or make the C-API accept // a `null` typeRef, using an implicit type. } return ret; } convertExpression( expr: ExpressionRef, fromType: Type, toType: Type, conversionKind: ConversionKind, reportNode: Node ): ExpressionRef { assert(conversionKind != ConversionKind.NONE); var module = this.module; // void to any if (fromType.kind == TypeKind.VOID) { assert(toType.kind != TypeKind.VOID); // convertExpression should not be called with void -> void this.error( DiagnosticCode.Type_0_is_not_assignable_to_type_1, reportNode.range, fromType.toString(), toType.toString() ); return module.createUnreachable(); } // any to void if (toType.kind == TypeKind.VOID) { return module.createDrop(expr); } if (conversionKind == ConversionKind.IMPLICIT && !fromType.isAssignableTo(toType)) { this.error( DiagnosticCode.Conversion_from_type_0_to_1_requires_an_explicit_cast, reportNode.range, fromType.toString(), toType.toString() ); // recoverable } // TODO: make this a proper switch? if (fromType.is(TypeFlags.FLOAT)) { // float to float if (toType.is(TypeFlags.FLOAT)) { if (fromType.kind == TypeKind.F32) { // f32 to f64 if (toType.kind == TypeKind.F64) { expr = module.createUnary(UnaryOp.PromoteF32, expr); } // otherwise f32 to f32 // f64 to f32 } else if (toType.kind == TypeKind.F32) { expr = module.createUnary(UnaryOp.DemoteF64, expr); } // otherwise f64 to f64 // float to int } else if (toType.is(TypeFlags.INTEGER)) { // f32 to int if (fromType.kind == TypeKind.F32) { if (toType.is(TypeFlags.SIGNED)) { if (toType.is(TypeFlags.LONG)) { expr = module.createUnary(UnaryOp.TruncF32ToI64, expr); } else { expr = module.createUnary(UnaryOp.TruncF32ToI32, expr); if (toType.is(TypeFlags.SHORT)) expr = makeSmallIntegerWrap(expr, toType, module); } } else { if (toType.is(TypeFlags.LONG)) { expr = module.createUnary(UnaryOp.TruncF32ToU64, expr); } else { expr = module.createUnary(UnaryOp.TruncF32ToU32, expr); if (toType.is(TypeFlags.SHORT)) expr = makeSmallIntegerWrap(expr, toType, module); } } // f64 to int } else { if (toType.is(TypeFlags.SIGNED)) { if (toType.is(TypeFlags.LONG)) { expr = module.createUnary(UnaryOp.TruncF64ToI64, expr); } else { expr = module.createUnary(UnaryOp.TruncF64ToI32, expr); if (toType.is(TypeFlags.SHORT)) expr = makeSmallIntegerWrap(expr, toType, module); } } else { if (toType.is(TypeFlags.LONG)) { expr = module.createUnary(UnaryOp.TruncF64ToU64, expr); } else { expr = module.createUnary(UnaryOp.TruncF64ToU32, expr); if (toType.is(TypeFlags.SHORT)) expr = makeSmallIntegerWrap(expr, toType, module); } } } // float to void } else { assert(toType.flags == TypeFlags.NONE, "void type expected"); expr = module.createDrop(expr); } // int to float } else if (fromType.is(TypeFlags.INTEGER) && toType.is(TypeFlags.FLOAT)) { // int to f32 if (toType.kind == TypeKind.F32) { if (fromType.is(TypeFlags.LONG)) { expr = module.createUnary( fromType.is(TypeFlags.SIGNED) ? UnaryOp.ConvertI64ToF32 : UnaryOp.ConvertU64ToF32, expr ); } else { expr = module.createUnary( fromType.is(TypeFlags.SIGNED) ? UnaryOp.ConvertI32ToF32 : UnaryOp.ConvertU32ToF32, expr ); } // int to f64 } else { if (fromType.is(TypeFlags.LONG)) { expr = module.createUnary( fromType.is(TypeFlags.SIGNED) ? UnaryOp.ConvertI64ToF64 : UnaryOp.ConvertU64ToF64, expr ); } else { expr = module.createUnary( fromType.is(TypeFlags.SIGNED) ? UnaryOp.ConvertI32ToF64 : UnaryOp.ConvertU32ToF64, expr ); } } // int to int } else { if (fromType.is(TypeFlags.LONG)) { // i64 to i32 if (!toType.is(TypeFlags.LONG)) { expr = module.createUnary(UnaryOp.WrapI64, expr); // discards upper bits if (toType.is(TypeFlags.SHORT)) expr = makeSmallIntegerWrap(expr, toType, module); } // i32 to i64 } else if (toType.is(TypeFlags.LONG)) { expr = module.createUnary(toType.is(TypeFlags.SIGNED) ? UnaryOp.ExtendI32 : UnaryOp.ExtendU32, expr); // i32 or smaller to even smaller or same size int with change of sign } else if ( toType.is(TypeFlags.SHORT) && ( fromType.size > toType.size || ( fromType.size == toType.size && fromType.is(TypeFlags.SIGNED) != toType.is(TypeFlags.SIGNED) ) ) ) { expr = makeSmallIntegerWrap(expr, toType, module); } // otherwise (smaller) i32/u32 to (same size) i32/u32 } this.currentType = toType; return expr; } compileAssertionExpression(expression: AssertionExpression, contextualType: Type): ExpressionRef { var toType = this.program.resolveType( // reports expression.toType, this.currentFunction.flow.contextualTypeArguments ); if (!toType) return this.module.createUnreachable(); return this.compileExpression(expression.expression, toType, ConversionKind.EXPLICIT); } private f32ModInstance: Function | null = null; private f64ModInstance: Function | null = null; private f32PowInstance: Function | null = null; private f64PowInstance: Function | null = null; compileBinaryExpression( expression: BinaryExpression, contextualType: Type, wrapSmallIntegers: bool = true ): ExpressionRef { var module = this.module; var left = expression.left; var right = expression.right; var leftExpr: ExpressionRef; var leftType: Type; var rightExpr: ExpressionRef; var rightType: Type; var commonType: Type | null; var condExpr: ExpressionRef; var expr: ExpressionRef; var compound = false; var possiblyOverflows = false; var tempLocal: Local | null = null; var operator = expression.operator; switch (operator) { case Token.LESSTHAN: { leftExpr = this.compileExpressionRetainType(left, contextualType); leftType = this.currentType; // check operator overload let classReference = leftType.classReference; if (classReference) { let overload = classReference.lookupOverload(OperatorKind.LT); if (overload) { expr = this.compileBinaryOverload(overload, left, right, expression); break; } } rightExpr = this.compileExpressionRetainType(right, leftType); rightType = this.currentType; if (commonType = Type.commonCompatible(leftType, rightType, true)) { leftExpr = this.convertExpression(leftExpr, leftType, commonType, ConversionKind.IMPLICIT, left); rightExpr = this.convertExpression(rightExpr, rightType, commonType, ConversionKind.IMPLICIT, right); } else { this.error( DiagnosticCode.Operator_0_cannot_be_applied_to_types_1_and_2, expression.range, "<", leftType.toString(), rightType.toString() ); this.currentType = contextualType; return module.createUnreachable(); } switch (commonType.kind) { case TypeKind.I8: case TypeKind.I16: case TypeKind.I32: { expr = module.createBinary(BinaryOp.LtI32, leftExpr, rightExpr); break; } case TypeKind.I64: { expr = module.createBinary(BinaryOp.LtI64, leftExpr, rightExpr); break; } case TypeKind.ISIZE: { expr = module.createBinary( this.options.isWasm64 ? BinaryOp.LtI64 : BinaryOp.LtI32, leftExpr, rightExpr ); break; } case TypeKind.U8: case TypeKind.U16: case TypeKind.U32: case TypeKind.BOOL: { expr = module.createBinary(BinaryOp.LtU32, leftExpr, rightExpr); break; } case TypeKind.USIZE: { expr = module.createBinary( this.options.isWasm64 ? BinaryOp.LtU64 : BinaryOp.LtU32, leftExpr, rightExpr ); break; } case TypeKind.U64: { expr = module.createBinary(BinaryOp.LtU64, leftExpr, rightExpr); break; } case TypeKind.F32: { expr = module.createBinary(BinaryOp.LtF32, leftExpr, rightExpr); break; } case TypeKind.F64: { expr = module.createBinary(BinaryOp.LtF64, leftExpr, rightExpr); break; } default: { assert(false); this.error( DiagnosticCode.Operation_not_supported, expression.range ); expr = module.createUnreachable(); break; } } this.currentType = Type.bool; break; } case Token.GREATERTHAN: { leftExpr = this.compileExpressionRetainType(left, contextualType); leftType = this.currentType; // check operator overload let classReference = leftType.classReference; if (classReference) { let overload = classReference.lookupOverload(OperatorKind.GT); if (overload) { expr = this.compileBinaryOverload(overload, left, right, expression); break; } } rightExpr = this.compileExpressionRetainType(right, leftType); rightType = this.currentType; if (commonType = Type.commonCompatible(leftType, rightType, true)) { leftExpr = this.convertExpression(leftExpr, leftType, commonType, ConversionKind.IMPLICIT, left); rightExpr = this.convertExpression(rightExpr, rightType, commonType, ConversionKind.IMPLICIT, right); } else { this.error( DiagnosticCode.Operator_0_cannot_be_applied_to_types_1_and_2, expression.range, ">", leftType.toString(), rightType.toString() ); this.currentType = contextualType; return module.createUnreachable(); } switch (commonType.kind) { case TypeKind.I8: case TypeKind.I16: case TypeKind.I32: { expr = module.createBinary(BinaryOp.GtI32, leftExpr, rightExpr); break; } case TypeKind.ISIZE: { expr = module.createBinary( this.options.isWasm64 ? BinaryOp.GtI64 : BinaryOp.GtI32, leftExpr, rightExpr ); break; } case TypeKind.I64: { expr = module.createBinary(BinaryOp.GtI64, leftExpr, rightExpr); break; } case TypeKind.U8: case TypeKind.U16: case TypeKind.U32: case TypeKind.BOOL: { expr = module.createBinary(BinaryOp.GtU32, leftExpr, rightExpr); break; } case TypeKind.USIZE: { expr = module.createBinary( this.options.isWasm64 ? BinaryOp.GtU64 : BinaryOp.GtU32, leftExpr, rightExpr ); break; } case TypeKind.U64: { expr = module.createBinary(BinaryOp.GtU64, leftExpr, rightExpr); break; } case TypeKind.F32: { expr = module.createBinary(BinaryOp.GtF32, leftExpr, rightExpr); break; } case TypeKind.F64: { expr = module.createBinary(BinaryOp.GtF64, leftExpr, rightExpr); break; } default: { assert(false); this.error( DiagnosticCode.Operation_not_supported, expression.range ); expr = module.createUnreachable(); break; } } this.currentType = Type.bool; break; } case Token.LESSTHAN_EQUALS: { leftExpr = this.compileExpressionRetainType(left, contextualType); leftType = this.currentType; // check operator overload let classReference = leftType.classReference; if (classReference) { let overload = classReference.lookupOverload(OperatorKind.LE); if (overload) { expr = this.compileBinaryOverload(overload, left, right, expression); break; } } rightExpr = this.compileExpressionRetainType(right, leftType); rightType = this.currentType; if (commonType = Type.commonCompatible(leftType, rightType, true)) { leftExpr = this.convertExpression(leftExpr, leftType, commonType, ConversionKind.IMPLICIT, left); rightExpr = this.convertExpression(rightExpr, rightType, commonType, ConversionKind.IMPLICIT, right); } else { this.error( DiagnosticCode.Operator_0_cannot_be_applied_to_types_1_and_2, expression.range, "<=", leftType.toString(), rightType.toString() ); this.currentType = contextualType; return module.createUnreachable(); } switch (commonType.kind) { case TypeKind.I8: case TypeKind.I16: case TypeKind.I32: { expr = module.createBinary(BinaryOp.LeI32, leftExpr, rightExpr); break; } case TypeKind.ISIZE: { expr = module.createBinary( this.options.isWasm64 ? BinaryOp.LeI64 : BinaryOp.LeI32, leftExpr, rightExpr ); break; } case TypeKind.I64: { expr = module.createBinary(BinaryOp.LeI64, leftExpr, rightExpr); break; } case TypeKind.U8: case TypeKind.U16: case TypeKind.U32: case TypeKind.BOOL: { expr = module.createBinary(BinaryOp.LeU32, leftExpr, rightExpr); break; } case TypeKind.USIZE: { expr = module.createBinary( this.options.isWasm64 ? BinaryOp.LeU64 : BinaryOp.LeU32, leftExpr, rightExpr ); break; } case TypeKind.U64: { expr = module.createBinary(BinaryOp.LeU64, leftExpr, rightExpr); break; } case TypeKind.F32: { expr = module.createBinary(BinaryOp.LeF32, leftExpr, rightExpr); break; } case TypeKind.F64: { expr = module.createBinary(BinaryOp.LeF64, leftExpr, rightExpr); break; } default: { assert(false); this.error( DiagnosticCode.Operation_not_supported, expression.range ); expr = module.createUnreachable(); break; } } this.currentType = Type.bool; break; } case Token.GREATERTHAN_EQUALS: { leftExpr = this.compileExpressionRetainType(left, contextualType); leftType = this.currentType; // check operator overload let classReference = leftType.classReference; if (classReference) { let overload = classReference.lookupOverload(OperatorKind.GE); if (overload) { expr = this.compileBinaryOverload(overload, left, right, expression); break; } } rightExpr = this.compileExpressionRetainType(right, leftType); rightType = this.currentType; if (commonType = Type.commonCompatible(leftType, rightType, true)) { leftExpr = this.convertExpression(leftExpr, leftType, commonType, ConversionKind.IMPLICIT, left); rightExpr = this.convertExpression(rightExpr, rightType, commonType, ConversionKind.IMPLICIT, right); } else { this.error( DiagnosticCode.Operator_0_cannot_be_applied_to_types_1_and_2, expression.range, ">=", leftType.toString(), rightType.toString() ); this.currentType = contextualType; return module.createUnreachable(); } switch (commonType.kind) { case TypeKind.I8: case TypeKind.I16: case TypeKind.I32: { expr = module.createBinary(BinaryOp.GeI32, leftExpr, rightExpr); break; } case TypeKind.ISIZE: { expr = module.createBinary( this.options.isWasm64 ? BinaryOp.GeI64 : BinaryOp.GeI32, leftExpr, rightExpr ); break; } case TypeKind.I64: { expr = module.createBinary(BinaryOp.GeI64, leftExpr, rightExpr); break; } case TypeKind.U8: case TypeKind.U16: case TypeKind.U32: case TypeKind.BOOL: { expr = module.createBinary(BinaryOp.GeU32, leftExpr, rightExpr); break; } case TypeKind.USIZE: { expr = module.createBinary( this.options.isWasm64 ? BinaryOp.GeU64 : BinaryOp.GeU32, leftExpr, rightExpr ); break; } case TypeKind.U64: { expr = module.createBinary(BinaryOp.GeU64, leftExpr, rightExpr); break; } case TypeKind.F32: { expr = module.createBinary(BinaryOp.GeF32, leftExpr, rightExpr); break; } case TypeKind.F64: { expr = module.createBinary(BinaryOp.GeF64, leftExpr, rightExpr); break; } default: { assert(false); this.error( DiagnosticCode.Operation_not_supported, expression.range ); expr = module.createUnreachable(); break; } } this.currentType = Type.bool; break; } case Token.EQUALS_EQUALS_EQUALS: case Token.EQUALS_EQUALS: { // NOTE that this favors correctness, in terms of emitting a binary expression, over // checking for a possible use of unary EQZ. while the most classic of all optimizations, // that's not what the source told us to do. for reference, `!left` emits unary EQZ. leftExpr = this.compileExpressionRetainType(left, contextualType); leftType = this.currentType; if (operator == Token.EQUALS_EQUALS) { // check operator overload let classReference = leftType.classReference; if (classReference) { let overload = classReference.lookupOverload(OperatorKind.EQ); if (overload) { expr = this.compileBinaryOverload(overload, left, right, expression); break; } } } rightExpr = this.compileExpressionRetainType(right, leftType); rightType = this.currentType; if (commonType = Type.commonCompatible(leftType, rightType, false)) { leftExpr = this.convertExpression(leftExpr, leftType, commonType, ConversionKind.IMPLICIT, left); rightExpr = this.convertExpression(rightExpr, rightType, commonType, ConversionKind.IMPLICIT, right); } else { this.error( DiagnosticCode.Operator_0_cannot_be_applied_to_types_1_and_2, expression.range, operatorTokenToString(expression.operator), leftType.toString(), rightType.toString() ); this.currentType = contextualType; return module.createUnreachable(); } switch (commonType.kind) { case TypeKind.I8: case TypeKind.I16: case TypeKind.I32: case TypeKind.U8: case TypeKind.U16: case TypeKind.U32: case TypeKind.BOOL: { expr = module.createBinary(BinaryOp.EqI32, leftExpr, rightExpr); break; } case TypeKind.USIZE: case TypeKind.ISIZE: { expr = module.createBinary( this.options.isWasm64 ? BinaryOp.EqI64 : BinaryOp.EqI32, leftExpr, rightExpr ); break; } case TypeKind.I64: case TypeKind.U64: { expr = module.createBinary(BinaryOp.EqI64, leftExpr, rightExpr); break; } case TypeKind.F32: { expr = module.createBinary(BinaryOp.EqF32, leftExpr, rightExpr); break; } case TypeKind.F64: { expr = module.createBinary(BinaryOp.EqF64, leftExpr, rightExpr); break; } default: { assert(false); this.error( DiagnosticCode.Operation_not_supported, expression.range ); expr = module.createUnreachable(); break; } } this.currentType = Type.bool; break; } case Token.EXCLAMATION_EQUALS_EQUALS: case Token.EXCLAMATION_EQUALS: { leftExpr = this.compileExpressionRetainType(left, contextualType); leftType = this.currentType; if (operator == Token.EXCLAMATION_EQUALS) { // check operator overload let classReference = leftType.classReference; if (classReference) { let overload = classReference.lookupOverload(OperatorKind.NE); if (overload) { expr = this.compileBinaryOverload(overload, left, right, expression); break; } } } rightExpr = this.compileExpressionRetainType(right, leftType); rightType = this.currentType; if (commonType = Type.commonCompatible(leftType, rightType, false)) { leftExpr = this.convertExpression(leftExpr, leftType, commonType, ConversionKind.IMPLICIT, left); rightExpr = this.convertExpression(rightExpr, rightType, commonType, ConversionKind.IMPLICIT, right); } else { this.error( DiagnosticCode.Operator_0_cannot_be_applied_to_types_1_and_2, expression.range, operatorTokenToString(expression.operator), leftType.toString(), rightType.toString() ); this.currentType = contextualType; return module.createUnreachable(); } switch (commonType.kind) { case TypeKind.I8: case TypeKind.I16: case TypeKind.I32: case TypeKind.U8: case TypeKind.U16: case TypeKind.U32: case TypeKind.BOOL: { expr = module.createBinary(BinaryOp.NeI32, leftExpr, rightExpr); break; } case TypeKind.USIZE: case TypeKind.ISIZE: { expr = module.createBinary( this.options.isWasm64 ? BinaryOp.NeI64 : BinaryOp.NeI32, leftExpr, rightExpr ); break; } case TypeKind.I64: case TypeKind.U64: { expr = module.createBinary(BinaryOp.NeI64, leftExpr, rightExpr); break; } case TypeKind.F32: { expr = module.createBinary(BinaryOp.NeF32, leftExpr, rightExpr); break; } case TypeKind.F64: { expr = module.createBinary(BinaryOp.NeF64, leftExpr, rightExpr); break; } default: { assert(false); this.error( DiagnosticCode.Operation_not_supported, expression.range ); expr = module.createUnreachable(); } } this.currentType = Type.bool; break; } case Token.EQUALS: { return this.compileAssignment(left, right, contextualType); } case Token.PLUS_EQUALS: compound = true; case Token.PLUS: { leftExpr = this.compileExpressionRetainType( left, contextualType, false // retains low bits of small integers ); leftType = this.currentType; // check operator overload let classReference = leftType.classReference; if (classReference) { let overload = classReference.lookupOverload(OperatorKind.ADD); if (overload) { expr = this.compileBinaryOverload(overload, left, right, expression); break; } } if (compound) { rightExpr = this.compileExpression( right, leftType, ConversionKind.IMPLICIT, false // ^ ); } else { rightExpr = this.compileExpressionRetainType( right, leftType, false // ^ ); rightType = this.currentType; if (commonType = Type.commonCompatible(leftType, rightType, false)) { leftExpr = this.convertExpression(leftExpr, leftType, commonType, ConversionKind.IMPLICIT, left); rightExpr = this.convertExpression(rightExpr, rightType, commonType, ConversionKind.IMPLICIT, right); } else { this.error( DiagnosticCode.Operator_0_cannot_be_applied_to_types_1_and_2, expression.range, "+", leftType.toString(), rightType.toString() ); this.currentType = contextualType; return module.createUnreachable(); } } switch (this.currentType.kind) { case TypeKind.I8: case TypeKind.I16: case TypeKind.U8: case TypeKind.U16: case TypeKind.BOOL: possiblyOverflows = true; case TypeKind.I32: case TypeKind.U32: { expr = module.createBinary(BinaryOp.AddI32, leftExpr, rightExpr); break; } case TypeKind.USIZE: case TypeKind.ISIZE: { expr = module.createBinary( this.options.isWasm64 ? BinaryOp.AddI64 : BinaryOp.AddI32, leftExpr, rightExpr ); break; } case TypeKind.I64: case TypeKind.U64: { expr = module.createBinary(BinaryOp.AddI64, leftExpr, rightExpr); break; } case TypeKind.F32: { expr = module.createBinary(BinaryOp.AddF32, leftExpr, rightExpr); break; } case TypeKind.F64: { expr = module.createBinary(BinaryOp.AddF64, leftExpr, rightExpr); break; } default: { assert(false); this.error( DiagnosticCode.Operation_not_supported, expression.range ); expr = module.createUnreachable(); break; } } break; } case Token.MINUS_EQUALS: compound = true; case Token.MINUS: { leftExpr = this.compileExpressionRetainType( left, contextualType, false // retains low bits of small integers ); leftType = this.currentType; // check operator overload let classReference = leftType.classReference; if (classReference) { let overload = classReference.lookupOverload(OperatorKind.SUB); if (overload) { expr = this.compileBinaryOverload(overload, left, right, expression); break; } } if (compound) { rightExpr = this.compileExpression( right, leftType, ConversionKind.IMPLICIT, false // ^ ); } else { rightExpr = this.compileExpressionRetainType( right, leftType, false // ^ ); rightType = this.currentType; if (commonType = Type.commonCompatible(leftType, rightType, false)) { leftExpr = this.convertExpression(leftExpr, leftType, commonType, ConversionKind.IMPLICIT, left); rightExpr = this.convertExpression(rightExpr, rightType, commonType, ConversionKind.IMPLICIT, right); } else { this.error( DiagnosticCode.Operator_0_cannot_be_applied_to_types_1_and_2, expression.range, "-", leftType.toString(), rightType.toString() ); this.currentType = contextualType; return module.createUnreachable(); } } switch (this.currentType.kind) { case TypeKind.I8: case TypeKind.I16: case TypeKind.U8: case TypeKind.U16: case TypeKind.BOOL: possiblyOverflows = true; case TypeKind.I32: case TypeKind.U32: { expr = module.createBinary(BinaryOp.SubI32, leftExpr, rightExpr); break; } case TypeKind.USIZE: case TypeKind.ISIZE: { expr = module.createBinary( this.options.isWasm64 ? BinaryOp.SubI64 : BinaryOp.SubI32, leftExpr, rightExpr ); break; } case TypeKind.I64: case TypeKind.U64: { expr = module.createBinary(BinaryOp.SubI64, leftExpr, rightExpr); break; } case TypeKind.F32: { expr = module.createBinary(BinaryOp.SubF32, leftExpr, rightExpr); break; } case TypeKind.F64: { expr = module.createBinary(BinaryOp.SubF64, leftExpr, rightExpr); break; } default: { assert(false); this.error( DiagnosticCode.Operation_not_supported, expression.range ); expr = module.createUnreachable(); break; } } break; } case Token.ASTERISK_EQUALS: compound = true; case Token.ASTERISK: { leftExpr = this.compileExpressionRetainType( left, contextualType, false // retains low bits of small integers ); leftType = this.currentType; // check operator overload let classReference = leftType.classReference; if (classReference) { let overload = classReference.lookupOverload(OperatorKind.MUL); if (overload) { expr = this.compileBinaryOverload(overload, left, right, expression); break; } } if (compound) { rightExpr = this.compileExpression( right, leftType, ConversionKind.IMPLICIT, false // ^ ); } else { rightExpr = this.compileExpressionRetainType( right, leftType, false // ^ ); rightType = this.currentType; if (commonType = Type.commonCompatible(leftType, rightType, false)) { leftExpr = this.convertExpression(leftExpr, leftType, commonType, ConversionKind.IMPLICIT, left); rightExpr = this.convertExpression(rightExpr, rightType, commonType, ConversionKind.IMPLICIT, right); } else { this.error( DiagnosticCode.Operator_0_cannot_be_applied_to_types_1_and_2, expression.range, "*", leftType.toString(), rightType.toString() ); this.currentType = contextualType; return module.createUnreachable(); } } switch (this.currentType.kind) { case TypeKind.I8: case TypeKind.I16: case TypeKind.U8: case TypeKind.U16: case TypeKind.BOOL: possiblyOverflows = true; case TypeKind.I32: case TypeKind.U32: { expr = module.createBinary(BinaryOp.MulI32, leftExpr, rightExpr); break; } case TypeKind.USIZE: case TypeKind.ISIZE: { expr = module.createBinary( this.options.isWasm64 ? BinaryOp.MulI64 : BinaryOp.MulI32, leftExpr, rightExpr ); break; } case TypeKind.I64: case TypeKind.U64: { expr = module.createBinary(BinaryOp.MulI64, leftExpr, rightExpr); break; } case TypeKind.F32: { expr = module.createBinary(BinaryOp.MulF32, leftExpr, rightExpr); break; } case TypeKind.F64: { expr = module.createBinary(BinaryOp.MulF64, leftExpr, rightExpr); break; } default: { assert(false); this.error( DiagnosticCode.Operation_not_supported, expression.range ); expr = module.createUnreachable(); break; } } break; } case Token.ASTERISK_ASTERISK_EQUALS: compound = true; case Token.ASTERISK_ASTERISK: { leftExpr = this.compileExpressionRetainType( left, contextualType, true // must be wrapped ); leftType = this.currentType; // check operator overload let classReference = leftType.classReference; if (classReference) { let overload = classReference.lookupOverload(OperatorKind.POW); if (overload) { expr = this.compileBinaryOverload(overload, left, right, expression); break; } } let instance: Function | null; // Mathf.pow if lhs is f32 (result is f32) if (this.currentType == Type.f32) { rightExpr = this.compileExpression( right, this.currentType ); if (!(instance = this.f32PowInstance)) { let namespace = this.program.elementsLookup.get("Mathf"); if (!namespace) { this.error( DiagnosticCode.Cannot_find_name_0, expression.range, "Mathf" ); expr = module.createUnreachable(); break; } let prototype = namespace.members ? namespace.members.get("pow") : null; if (!prototype) { this.error( DiagnosticCode.Cannot_find_name_0, expression.range, "Mathf.pow" ); expr = module.createUnreachable(); break; } assert(prototype.kind == ElementKind.FUNCTION_PROTOTYPE); this.f32PowInstance = instance = (prototype).resolve(); } // Math.pow otherwise (result is f64) // TODO: should the result be converted back? } else { leftExpr = this.convertExpression( leftExpr, this.currentType, Type.f64, ConversionKind.IMPLICIT, left ); rightExpr = this.compileExpression( right, Type.f64 ); if (!(instance = this.f64PowInstance)) { let namespace = this.program.elementsLookup.get("Math"); if (!namespace) { this.error( DiagnosticCode.Cannot_find_name_0, expression.range, "Math" ); expr = module.createUnreachable(); break; } let prototype = namespace.members ? namespace.members.get("pow") : null; if (!prototype) { this.error( DiagnosticCode.Cannot_find_name_0, expression.range, "Math.pow" ); expr = module.createUnreachable(); break; } assert(prototype.kind == ElementKind.FUNCTION_PROTOTYPE); this.f64PowInstance = instance = (prototype).resolve(); } } if (!(instance && this.compileFunction(instance))) { expr = module.createUnreachable(); } else { expr = this.makeCallDirect(instance, [ leftExpr, rightExpr ]); } break; } case Token.SLASH_EQUALS: compound = true; case Token.SLASH: { leftExpr = this.compileExpressionRetainType( left, contextualType, true // TODO: when can division remain unwrapped? does it overflow? ); leftType = this.currentType; // check operator overload let classReference = leftType.classReference; if (classReference) { let overload = classReference.lookupOverload(OperatorKind.DIV); if (overload) { expr = this.compileBinaryOverload(overload, left, right, expression); break; } } if (compound) { rightExpr = this.compileExpression( right, leftType, ConversionKind.IMPLICIT, false // ^ ); } else { rightExpr = this.compileExpressionRetainType( right, leftType, false // ^ ); rightType = this.currentType; if (commonType = Type.commonCompatible(leftType, rightType, false)) { leftExpr = this.convertExpression(leftExpr, leftType, commonType, ConversionKind.IMPLICIT, left); rightExpr = this.convertExpression(rightExpr, rightType, commonType, ConversionKind.IMPLICIT, right); } else { this.error( DiagnosticCode.Operator_0_cannot_be_applied_to_types_1_and_2, expression.range, "/", leftType.toString(), rightType.toString() ); this.currentType = contextualType; return module.createUnreachable(); } } switch (this.currentType.kind) { case TypeKind.I8: case TypeKind.I16: possiblyOverflows = true; case TypeKind.I32: { expr = module.createBinary(BinaryOp.DivI32, leftExpr, rightExpr); break; } case TypeKind.ISIZE: { expr = module.createBinary( this.options.isWasm64 ? BinaryOp.DivI64 : BinaryOp.DivI32, leftExpr, rightExpr ); break; } case TypeKind.I64: { expr = module.createBinary(BinaryOp.DivI64, leftExpr, rightExpr); break; } case TypeKind.U8: case TypeKind.U16: case TypeKind.BOOL: possiblyOverflows = true; case TypeKind.U32: { expr = module.createBinary(BinaryOp.DivU32, leftExpr, rightExpr); break; } case TypeKind.USIZE: { expr = module.createBinary( this.options.isWasm64 ? BinaryOp.DivU64 : BinaryOp.DivU32, leftExpr, rightExpr ); break; } case TypeKind.U64: { expr = module.createBinary(BinaryOp.DivU64, leftExpr, rightExpr); break; } case TypeKind.F32: { expr = module.createBinary(BinaryOp.DivF32, leftExpr, rightExpr); break; } case TypeKind.F64: { expr = module.createBinary(BinaryOp.DivF64, leftExpr, rightExpr); break; } default: { assert(false); this.error( DiagnosticCode.Operation_not_supported, expression.range ); expr = module.createUnreachable(); break; } } break; } case Token.PERCENT_EQUALS: compound = true; case Token.PERCENT: { leftExpr = this.compileExpressionRetainType( left, contextualType, true // TODO: when can remainder remain unwrapped? does it overflow? ); leftType = this.currentType; // check operator overload let classReference = leftType.classReference; if (classReference) { let overload = classReference.lookupOverload(OperatorKind.REM); if (overload) { expr = this.compileBinaryOverload(overload, left, right, expression); break; } } if (compound) { rightExpr = this.compileExpression( right, leftType, ConversionKind.IMPLICIT, false // ^ ); } else { rightExpr = this.compileExpressionRetainType( right, leftType, false // ^ ); rightType = this.currentType; if (commonType = Type.commonCompatible(leftType, rightType, false)) { leftExpr = this.convertExpression(leftExpr, leftType, commonType, ConversionKind.IMPLICIT, left); rightExpr = this.convertExpression(rightExpr, rightType, commonType, ConversionKind.IMPLICIT, right); } else { this.error( DiagnosticCode.Operator_0_cannot_be_applied_to_types_1_and_2, expression.range, "%", leftType.toString(), rightType.toString() ); this.currentType = contextualType; return module.createUnreachable(); } } switch (this.currentType.kind) { case TypeKind.I8: case TypeKind.I16: case TypeKind.I32: { expr = module.createBinary(BinaryOp.RemI32, leftExpr, rightExpr); break; } case TypeKind.ISIZE: { expr = module.createBinary( this.options.isWasm64 ? BinaryOp.RemI64 : BinaryOp.RemI32, leftExpr, rightExpr ); break; } case TypeKind.I64: { expr = module.createBinary(BinaryOp.RemI64, leftExpr, rightExpr); break; } case TypeKind.U8: case TypeKind.U16: case TypeKind.U32: case TypeKind.BOOL: { expr = module.createBinary(BinaryOp.RemU32, leftExpr, rightExpr); break; } case TypeKind.USIZE: { expr = module.createBinary( this.options.isWasm64 ? BinaryOp.RemU64 : BinaryOp.RemU32, leftExpr, rightExpr ); break; } case TypeKind.U64: { expr = module.createBinary(BinaryOp.RemU64, leftExpr, rightExpr); break; } case TypeKind.F32: { let instance = this.f32ModInstance; if (!instance) { let namespace = this.program.elementsLookup.get("Mathf"); if (!namespace) { this.error( DiagnosticCode.Cannot_find_name_0, expression.range, "Mathf" ); expr = module.createUnreachable(); break; } let prototype = namespace.members ? namespace.members.get("mod") : null; if (!prototype) { this.error( DiagnosticCode.Cannot_find_name_0, expression.range, "Mathf.mod" ); expr = module.createUnreachable(); break; } assert(prototype.kind == ElementKind.FUNCTION_PROTOTYPE); this.f32ModInstance = instance = (prototype).resolve(); } if (!(instance && this.compileFunction(instance))) { expr = module.createUnreachable(); } else { expr = this.makeCallDirect(instance, [ leftExpr, rightExpr ]); } break; } case TypeKind.F64: { let instance = this.f64ModInstance; if (!instance) { let namespace = this.program.elementsLookup.get("Math"); if (!namespace) { this.error( DiagnosticCode.Cannot_find_name_0, expression.range, "Math" ); expr = module.createUnreachable(); break; } let prototype = namespace.members ? namespace.members.get("mod") : null; if (!prototype) { this.error( DiagnosticCode.Cannot_find_name_0, expression.range, "Math.mod" ); expr = module.createUnreachable(); break; } assert(prototype.kind == ElementKind.FUNCTION_PROTOTYPE); this.f64ModInstance = instance = (prototype).resolve(); } if (!(instance && this.compileFunction(instance))) { expr = module.createUnreachable(); } else { expr = this.makeCallDirect(instance, [ leftExpr, rightExpr ]); } break; } default: { assert(false); this.error( DiagnosticCode.Operation_not_supported, expression.range ); expr = module.createUnreachable(); break; } } break; } case Token.LESSTHAN_LESSTHAN_EQUALS: compound = true; case Token.LESSTHAN_LESSTHAN: { leftExpr = this.compileExpressionRetainType( left, contextualType, false // retains low bits of small integers ); rightExpr = this.compileExpression( right, this.currentType, ConversionKind.IMPLICIT, false // ^ ); switch (this.currentType.kind) { case TypeKind.I8: case TypeKind.I16: case TypeKind.U8: case TypeKind.U16: case TypeKind.BOOL: possiblyOverflows = true; default: { expr = module.createBinary(BinaryOp.ShlI32, leftExpr, rightExpr); break; } case TypeKind.I64: case TypeKind.U64: { expr = module.createBinary(BinaryOp.ShlI64, leftExpr, rightExpr); break; } case TypeKind.USIZE: // TODO: check operator overload case TypeKind.ISIZE: { expr = module.createBinary( this.options.isWasm64 ? BinaryOp.ShlI64 : BinaryOp.ShlI32, leftExpr, rightExpr ); break; } case TypeKind.F32: case TypeKind.F64: { this.error( DiagnosticCode.The_0_operator_cannot_be_applied_to_type_1, expression.range, operatorTokenToString(expression.operator), this.currentType.toString() ); return module.createUnreachable(); } case TypeKind.VOID: { assert(false); this.error( DiagnosticCode.Operation_not_supported, expression.range ); expr = module.createUnreachable(); break; } } break; } case Token.GREATERTHAN_GREATERTHAN_EQUALS: compound = true; case Token.GREATERTHAN_GREATERTHAN: { leftExpr = this.compileExpressionRetainType( left, contextualType, true // must wrap small integers ); rightExpr = this.compileExpression( right, this.currentType, ConversionKind.IMPLICIT, true // ^ ); switch (this.currentType.kind) { default: { // assumes signed shr on signed small integers does not overflow expr = module.createBinary(BinaryOp.ShrI32, leftExpr, rightExpr); break; } case TypeKind.I64: { expr = module.createBinary(BinaryOp.ShrI64, leftExpr, rightExpr); break; } case TypeKind.ISIZE: { expr = module.createBinary( this.options.isWasm64 ? BinaryOp.ShrI64 : BinaryOp.ShrI32, leftExpr, rightExpr ); break; } case TypeKind.U8: case TypeKind.U16: case TypeKind.BOOL: // assumes unsigned shr on unsigned small integers does not overflow case TypeKind.U32: { expr = module.createBinary(BinaryOp.ShrU32, leftExpr, rightExpr); break; } case TypeKind.U64: { expr = module.createBinary(BinaryOp.ShrU64, leftExpr, rightExpr); break; } case TypeKind.USIZE: { // TODO: check operator overload expr = module.createBinary( this.options.isWasm64 ? BinaryOp.ShrU64 : BinaryOp.ShrU32, leftExpr, rightExpr ); break; } case TypeKind.F32: case TypeKind.F64: { this.error( DiagnosticCode.The_0_operator_cannot_be_applied_to_type_1, expression.range, operatorTokenToString(expression.operator), this.currentType.toString() ); return module.createUnreachable(); } case TypeKind.VOID: { assert(false); this.error( DiagnosticCode.Operation_not_supported, expression.range ); expr = module.createUnreachable(); break; } } break; } case Token.GREATERTHAN_GREATERTHAN_GREATERTHAN_EQUALS: compound = true; case Token.GREATERTHAN_GREATERTHAN_GREATERTHAN: { leftExpr = this.compileExpressionRetainType( left, contextualType, true // modifies low bits of small integers if unsigned ); rightExpr = this.compileExpression( right, this.currentType, ConversionKind.IMPLICIT, true // ^ ); switch (this.currentType.kind) { case TypeKind.I8: case TypeKind.I16: possiblyOverflows = true; default: { // assumes that unsigned shr on unsigned small integers does not overflow expr = module.createBinary(BinaryOp.ShrU32, leftExpr, rightExpr); break; } case TypeKind.I64: case TypeKind.U64: { expr = module.createBinary(BinaryOp.ShrU64, leftExpr, rightExpr); break; } case TypeKind.USIZE: // TODO: check operator overload case TypeKind.ISIZE: { expr = module.createBinary( this.options.isWasm64 ? BinaryOp.ShrU64 : BinaryOp.ShrU32, leftExpr, rightExpr ); break; } case TypeKind.VOID: { assert(false); this.error( DiagnosticCode.Operation_not_supported, expression.range ); expr = module.createUnreachable(); break; } } break; } case Token.AMPERSAND_EQUALS: compound = true; case Token.AMPERSAND: { leftExpr = this.compileExpressionRetainType( left, contextualType, false // retains low bits of small integers ); leftType = this.currentType; // check operator overloadd let classReference = leftType.classReference; if (classReference) { let overload = classReference.lookupOverload(OperatorKind.AND); if (overload) { expr = this.compileBinaryOverload(overload, left, right, expression); break; } } if (compound) { rightExpr = this.compileExpression( right, leftType, ConversionKind.IMPLICIT, false // ^ ); } else { rightExpr = this.compileExpressionRetainType( right, leftType, false // ^ ); rightType = this.currentType; if (commonType = Type.commonCompatible(leftType, rightType, false)) { leftExpr = this.convertExpression(leftExpr, leftType, commonType, ConversionKind.IMPLICIT, left); rightExpr = this.convertExpression(rightExpr, rightType, commonType, ConversionKind.IMPLICIT, right); } else { this.error( DiagnosticCode.Operator_0_cannot_be_applied_to_types_1_and_2, expression.range, "&", leftType.toString(), rightType.toString() ); this.currentType = contextualType; return module.createUnreachable(); } } switch (this.currentType.kind) { case TypeKind.I8: case TypeKind.I16: case TypeKind.U8: case TypeKind.U16: case TypeKind.BOOL: possiblyOverflows = true; // if left or right already did default: { expr = module.createBinary(BinaryOp.AndI32, leftExpr, rightExpr); break; } case TypeKind.I64: case TypeKind.U64: { expr = module.createBinary(BinaryOp.AndI64, leftExpr, rightExpr); break; } case TypeKind.USIZE: case TypeKind.ISIZE: { expr = module.createBinary( this.options.isWasm64 ? BinaryOp.AndI64 : BinaryOp.AndI32, leftExpr, rightExpr ); break; } case TypeKind.VOID: { assert(false); this.error( DiagnosticCode.Operation_not_supported, expression.range ); expr = module.createUnreachable(); break; } } break; } case Token.BAR_EQUALS: compound = true; case Token.BAR: { leftExpr = this.compileExpressionRetainType( left, contextualType, false // retains low bits of small integers ); leftType = this.currentType; // check operator overload let classReference = leftType.classReference; if (classReference) { let overload = classReference.lookupOverload(OperatorKind.OR); if (overload) { expr = this.compileBinaryOverload(overload, left, right, expression); break; } } if (compound) { rightExpr = this.compileExpression( right, leftType, ConversionKind.IMPLICIT, false // ^ ); } else { rightExpr = this.compileExpressionRetainType( right, leftType, false // ^ ); rightType = this.currentType; if (commonType = Type.commonCompatible(leftType, rightType, false)) { leftExpr = this.convertExpression(leftExpr, leftType, commonType, ConversionKind.IMPLICIT, left); rightExpr = this.convertExpression(rightExpr, rightType, commonType, ConversionKind.IMPLICIT, right); } else { this.error( DiagnosticCode.Operator_0_cannot_be_applied_to_types_1_and_2, expression.range, "|", leftType.toString(), rightType.toString() ); this.currentType = contextualType; return module.createUnreachable(); } } switch (this.currentType.kind) { case TypeKind.I8: case TypeKind.I16: case TypeKind.U8: case TypeKind.U16: case TypeKind.BOOL: possiblyOverflows = true; // if left or right already did default: { expr = module.createBinary(BinaryOp.OrI32, leftExpr, rightExpr); break; } case TypeKind.I64: case TypeKind.U64: { expr = module.createBinary(BinaryOp.OrI64, leftExpr, rightExpr); break; } case TypeKind.USIZE: case TypeKind.ISIZE: { expr = module.createBinary( this.options.isWasm64 ? BinaryOp.OrI64 : BinaryOp.OrI32, leftExpr, rightExpr ); break; } case TypeKind.VOID: { assert(false); this.error( DiagnosticCode.Operation_not_supported, expression.range ); expr = module.createUnreachable(); break; } } break; } case Token.CARET_EQUALS: compound = true; case Token.CARET: { leftExpr = this.compileExpressionRetainType( left, contextualType, false // retains low bits of small integers ); leftType = this.currentType; // check operator overload let classReference = leftType.classReference; if (classReference) { let overload = classReference.lookupOverload(OperatorKind.XOR); if (overload) { expr = this.compileBinaryOverload(overload, left, right, expression); break; } } if (compound) { rightExpr = this.compileExpression( right, leftType, ConversionKind.IMPLICIT, false // ^ ); } else { rightExpr = this.compileExpressionRetainType( right, leftType, false // ^ ); rightType = this.currentType; if (commonType = Type.commonCompatible(leftType, rightType, false)) { leftExpr = this.convertExpression(leftExpr, leftType, commonType, ConversionKind.IMPLICIT, left); rightExpr = this.convertExpression(rightExpr, rightType, commonType, ConversionKind.IMPLICIT, right); } else { this.error( DiagnosticCode.Operator_0_cannot_be_applied_to_types_1_and_2, expression.range, "^", leftType.toString(), rightType.toString() ); this.currentType = contextualType; return module.createUnreachable(); } } switch (this.currentType.kind) { case TypeKind.I8: case TypeKind.I16: case TypeKind.U8: case TypeKind.U16: case TypeKind.BOOL: possiblyOverflows = true; // if left or right already did default: { expr = module.createBinary(BinaryOp.XorI32, leftExpr, rightExpr); break; } case TypeKind.I64: case TypeKind.U64: { expr = module.createBinary(BinaryOp.XorI64, leftExpr, rightExpr); break; } case TypeKind.USIZE: case TypeKind.ISIZE: { expr = module.createBinary( this.options.isWasm64 ? BinaryOp.XorI64 : BinaryOp.XorI32, leftExpr, rightExpr ); break; } case TypeKind.VOID: { assert(false); this.error( DiagnosticCode.Operation_not_supported, expression.range ); expr = module.createUnreachable(); break; } } break; } // logical (no overloading) case Token.AMPERSAND_AMPERSAND: { // left && right leftExpr = this.compileExpressionRetainType( left, contextualType ); rightExpr = this.compileExpression( right, this.currentType, ConversionKind.IMPLICIT, false ); // clone left if free of side effects expr = module.cloneExpression(leftExpr, true, 0); // if not possible, tee left to a temp. local if (!expr) { tempLocal = this.currentFunction.getAndFreeTempLocal(this.currentType); leftExpr = module.createTeeLocal(tempLocal.index, leftExpr); } possiblyOverflows = this.currentType.is(TypeFlags.SHORT | TypeFlags.INTEGER); condExpr = makeIsTrueish(leftExpr, this.currentType, module); // simplify when cloning left without side effects was successful if (expr) { expr = module.createIf( condExpr, // left rightExpr, // ? right expr // : cloned left ); } // otherwise make use of the temp. local else { expr = module.createIf( condExpr, rightExpr, module.createGetLocal( assert(tempLocal).index, // to be sure this.currentType.toNativeType() ) ); } break; } case Token.BAR_BAR: { // left || right leftExpr = this.compileExpressionRetainType( left, contextualType ); rightExpr = this.compileExpression( right, this.currentType, ConversionKind.IMPLICIT, false ); // clone left if free of side effects expr = this.module.cloneExpression(leftExpr, true, 0); // if not possible, tee left to a temp. local if (!expr) { tempLocal = this.currentFunction.getAndFreeTempLocal(this.currentType); leftExpr = module.createTeeLocal(tempLocal.index, leftExpr); } possiblyOverflows = this.currentType.is(TypeFlags.SHORT | TypeFlags.INTEGER); // if right did condExpr = makeIsTrueish(leftExpr, this.currentType, module); // simplify when cloning left without side effects was successful if (expr) { expr = this.module.createIf( condExpr, // left expr, // ? cloned left rightExpr // : right ); } // otherwise make use of the temp. local else { expr = module.createIf( condExpr, module.createGetLocal( assert(tempLocal).index, // to be sure this.currentType.toNativeType() ), rightExpr ); } break; } default: { assert(false); this.error( DiagnosticCode.Operation_not_supported, expression.range ); expr = this.module.createUnreachable(); break; } } if (possiblyOverflows && wrapSmallIntegers) { assert(this.currentType.is(TypeFlags.SHORT | TypeFlags.INTEGER)); // must be a small int expr = makeSmallIntegerWrap(expr, this.currentType, module); } return compound ? this.compileAssignmentWithValue(left, expr, contextualType != Type.void) : expr; } compileUnaryOverload( operatorInstance: Function, value: Expression, reportNode: Node ): ExpressionRef { // checks and recompiles the argument according to its actual annotated type var argumentExpressions: Expression[]; var thisArg: ExpressionRef = 0; if (operatorInstance.is(CommonFlags.INSTANCE)) { let parent = assert(operatorInstance.parent); assert(parent.kind == ElementKind.CLASS); thisArg = this.compileExpression(value, (parent).type); argumentExpressions = []; } else { argumentExpressions = [ value ]; } return this.compileCallDirect( operatorInstance, argumentExpressions, reportNode, thisArg, operatorInstance.hasDecorator(DecoratorFlags.INLINE) ); } compileBinaryOverload( operatorInstance: Function, left: Expression, right: Expression, reportNode: Node ): ExpressionRef { // checks and recompiles the arguments according to their actual annotated types var argumentExpressions: Expression[]; var thisArg: ExpressionRef = 0; if (operatorInstance.is(CommonFlags.INSTANCE)) { let parent = assert(operatorInstance.parent); assert(parent.kind == ElementKind.CLASS); thisArg = this.compileExpression(left, (parent).type); argumentExpressions = [ right ]; } else { argumentExpressions = [ left, right ]; } return this.compileCallDirect( operatorInstance, argumentExpressions, reportNode, thisArg, operatorInstance.hasDecorator(DecoratorFlags.INLINE) ); } compileAssignment(expression: Expression, valueExpression: Expression, contextualType: Type): ExpressionRef { var program = this.program; var currentFunction = this.currentFunction; var target = program.resolveExpression(expression, currentFunction); // reports if (!target) return this.module.createUnreachable(); // to compile just the value, we need to know the target's type var elementType: Type; switch (target.kind) { case ElementKind.GLOBAL: { if (!this.compileGlobal(target)) { // reports; not yet compiled if a static field compiled as a global return this.module.createUnreachable(); } assert((target).type != Type.void); // compileGlobal must guarantee this // fall-through } case ElementKind.LOCAL: case ElementKind.FIELD: { elementType = (target).type; break; } case ElementKind.PROPERTY: { let prototype = (target).setterPrototype; if (prototype) { let instance = prototype.resolve(); // reports if (!instance) return this.module.createUnreachable(); assert(instance.signature.parameterTypes.length == 1); // parser must guarantee this elementType = instance.signature.parameterTypes[0]; break; } this.error( DiagnosticCode.Cannot_assign_to_0_because_it_is_a_constant_or_a_read_only_property, expression.range, (target).internalName ); return this.module.createUnreachable(); } case ElementKind.CLASS: { if (program.resolvedElementExpression) { // indexed access let indexedSet = (target).lookupOverload(OperatorKind.INDEXED_SET); if (!indexedSet) { let indexedGet = (target).lookupOverload(OperatorKind.INDEXED_GET); if (!indexedGet) { this.error( DiagnosticCode.Index_signature_is_missing_in_type_0, expression.range, (target).internalName ); } else { this.error( DiagnosticCode.Index_signature_in_type_0_only_permits_reading, expression.range, (target).internalName ); } return this.module.createUnreachable(); } assert(indexedSet.signature.parameterTypes.length == 2); // parser must guarantee this elementType = indexedSet.signature.parameterTypes[1]; // 2nd parameter is the element break; } // fall-through } default: { this.error( DiagnosticCode.Operation_not_supported, expression.range ); return this.module.createUnreachable(); } } // compile the value and do the assignment var valueExpr = this.compileExpression(valueExpression, elementType); return this.compileAssignmentWithValue( expression, valueExpr, contextualType != Type.void ); } compileAssignmentWithValue( expression: Expression, valueWithCorrectType: ExpressionRef, tee: bool = false ): ExpressionRef { var module = this.module; var target = this.program.resolveExpression(expression, this.currentFunction); // reports if (!target) return module.createUnreachable(); switch (target.kind) { case ElementKind.LOCAL: { this.currentType = tee ? (target).type : Type.void; if ((target).is(CommonFlags.CONST)) { this.error( DiagnosticCode.Cannot_assign_to_0_because_it_is_a_constant_or_a_read_only_property, expression.range, target.internalName ); return module.createUnreachable(); } return tee ? module.createTeeLocal((target).index, valueWithCorrectType) : module.createSetLocal((target).index, valueWithCorrectType); } case ElementKind.GLOBAL: { if (!this.compileGlobal(target)) return module.createUnreachable(); let type = (target).type; assert(type != Type.void); this.currentType = tee ? type : Type.void; if ((target).is(CommonFlags.CONST)) { this.error( DiagnosticCode.Cannot_assign_to_0_because_it_is_a_constant_or_a_read_only_property, expression.range, target.internalName ); return module.createUnreachable(); } if (tee) { let nativeType = type.toNativeType(); let internalName = target.internalName; return module.createBlock(null, [ // emulated teeGlobal module.createSetGlobal(internalName, valueWithCorrectType), module.createGetGlobal(internalName, nativeType) ], nativeType); } else { return module.createSetGlobal(target.internalName, valueWithCorrectType); } } case ElementKind.FIELD: { const declaration = (target).declaration; if ( (target).is(CommonFlags.READONLY) && !( this.currentFunction.is(CommonFlags.CONSTRUCTOR) || declaration == null || declaration.initializer != null ) ) { this.error( DiagnosticCode.Cannot_assign_to_0_because_it_is_a_constant_or_a_read_only_property, expression.range, (target).internalName ); return module.createUnreachable(); } let thisExpression = assert(this.program.resolvedThisExpression); let thisExpr = this.compileExpressionRetainType( thisExpression, this.options.usizeType ); let type = (target).type; this.currentType = tee ? type : Type.void; let nativeType = type.toNativeType(); if (tee) { let tempLocal = this.currentFunction.getAndFreeTempLocal(type); let tempLocalIndex = tempLocal.index; // TODO: simplify if valueWithCorrectType has no side effects return module.createBlock(null, [ module.createSetLocal(tempLocalIndex, valueWithCorrectType), module.createStore( type.size >> 3, thisExpr, module.createGetLocal(tempLocalIndex, nativeType), nativeType, (target).memoryOffset ), module.createGetLocal(tempLocalIndex, nativeType) ], nativeType); } else { return module.createStore( type.size >> 3, thisExpr, valueWithCorrectType, nativeType, (target).memoryOffset ); } } case ElementKind.PROPERTY: { let setterPrototype = (target).setterPrototype; if (setterPrototype) { let setterInstance = setterPrototype.resolve(); // reports if (!setterInstance) return module.createUnreachable(); // call just the setter if the return value isn't of interest if (!tee) { if (setterInstance.is(CommonFlags.INSTANCE)) { let thisExpression = assert(this.program.resolvedThisExpression); let thisExpr = this.compileExpressionRetainType( thisExpression, this.options.usizeType ); return this.makeCallDirect(setterInstance, [ thisExpr, valueWithCorrectType ]); } else { return this.makeCallDirect(setterInstance, [ valueWithCorrectType ]); } } // otherwise call the setter first, then the getter let getterPrototype = (target).getterPrototype; assert(getterPrototype != null); // must have one if there is a setter let getterInstance = (getterPrototype).resolve(); // reports if (!getterInstance) return module.createUnreachable(); let returnType = getterInstance.signature.returnType; let nativeReturnType = returnType.toNativeType(); if (setterInstance.is(CommonFlags.INSTANCE)) { let thisExpression = assert(this.program.resolvedThisExpression); let thisExpr = this.compileExpressionRetainType( thisExpression, this.options.usizeType ); let tempLocal = this.currentFunction.getAndFreeTempLocal(returnType); let tempLocalIndex = tempLocal.index; return module.createBlock(null, [ this.makeCallDirect(setterInstance, [ // set and remember the target module.createTeeLocal(tempLocalIndex, thisExpr), valueWithCorrectType ]), this.makeCallDirect(getterInstance, [ // get from remembered target module.createGetLocal(tempLocalIndex, nativeReturnType) ]) ], nativeReturnType); } else { // note that this must be performed here because `resolved` is shared return module.createBlock(null, [ this.makeCallDirect(setterInstance, [ valueWithCorrectType ]), this.makeCallDirect(getterInstance) ], nativeReturnType); } } else { this.error( DiagnosticCode.Cannot_assign_to_0_because_it_is_a_constant_or_a_read_only_property, expression.range, target.internalName ); } return module.createUnreachable(); } case ElementKind.CLASS: { let elementExpression = this.program.resolvedElementExpression; if (elementExpression) { let indexedGet = (target).lookupOverload(OperatorKind.INDEXED_GET); if (!indexedGet) { this.error( DiagnosticCode.Index_signature_is_missing_in_type_0, expression.range, target.internalName ); return module.createUnreachable(); } let indexedSet = (target).lookupOverload(OperatorKind.INDEXED_SET); if (!indexedSet) { this.error( DiagnosticCode.Index_signature_in_type_0_only_permits_reading, expression.range, target.internalName ); this.currentType = tee ? indexedGet.signature.returnType : Type.void; return module.createUnreachable(); } let targetType = (target).type; let thisExpression = assert(this.program.resolvedThisExpression); let thisExpr = this.compileExpressionRetainType( thisExpression, this.options.usizeType ); let elementExpr = this.compileExpression( elementExpression, Type.i32 ); if (tee) { let tempLocalTarget = this.currentFunction.getTempLocal(targetType); let tempLocalElement = this.currentFunction.getAndFreeTempLocal(this.currentType); let returnType = indexedGet.signature.returnType; this.currentFunction.freeTempLocal(tempLocalTarget); return module.createBlock(null, [ this.makeCallDirect(indexedSet, [ module.createTeeLocal(tempLocalTarget.index, thisExpr), module.createTeeLocal(tempLocalElement.index, elementExpr), valueWithCorrectType ]), this.makeCallDirect(indexedGet, [ module.createGetLocal(tempLocalTarget.index, tempLocalTarget.type.toNativeType()), module.createGetLocal(tempLocalElement.index, tempLocalElement.type.toNativeType()) ]) ], returnType.toNativeType()); } else { return this.makeCallDirect(indexedSet, [ thisExpr, elementExpr, valueWithCorrectType ]); } } // fall-through } } this.error( DiagnosticCode.Operation_not_supported, expression.range ); return module.createUnreachable(); } compileCallExpression(expression: CallExpression, contextualType: Type): ExpressionRef { var module = this.module; var currentFunction = this.currentFunction; var target = this.program.resolveExpression(expression.expression, currentFunction); // reports if (!target) return module.createUnreachable(); var signature: Signature | null; var indexArg: ExpressionRef; switch (target.kind) { // direct call: concrete function case ElementKind.FUNCTION_PROTOTYPE: { let prototype = target; let typeArguments = expression.typeArguments; // builtins handle present respectively omitted type arguments on their own if (prototype.is(CommonFlags.AMBIENT | CommonFlags.BUILTIN)) { return this.compileCallExpressionBuiltin(prototype, expression, contextualType); } let instance: Function | null = null; // resolve generic call if type arguments have been provided if (typeArguments) { if (!prototype.is(CommonFlags.GENERIC)) { this.error( DiagnosticCode.Type_0_is_not_generic, expression.expression.range, prototype.internalName ); return module.createUnreachable(); } instance = prototype.resolveUsingTypeArguments( // reports typeArguments, this.currentFunction.flow.contextualTypeArguments, expression ); // infer generic call if type arguments have been omitted } else if (prototype.is(CommonFlags.GENERIC)) { let inferredTypes = new Map(); let typeParameters = assert(prototype.declaration.typeParameters); let numTypeParameters = typeParameters.length; for (let i = 0; i < numTypeParameters; ++i) { inferredTypes.set(typeParameters[i].name.text, null); } // let numInferred = 0; let parameterTypes = prototype.declaration.signature.parameterTypes; let numParameterTypes = parameterTypes.length; let argumentExpressions = expression.arguments; let numArguments = argumentExpressions.length; let argumentExprs = new Array(numArguments); for (let i = 0; i < numParameterTypes; ++i) { let typeNode = parameterTypes[i].type; let name = typeNode.kind == NodeKind.TYPE ? (typeNode).name.text : null; let argumentExpression = i < numArguments ? argumentExpressions[i] : prototype.declaration.signature.parameterTypes[i].initializer; if (!argumentExpression) { // missing initializer -> too few arguments this.error( DiagnosticCode.Expected_0_arguments_but_got_1, expression.range, numParameterTypes.toString(10), numArguments.toString(10) ); return module.createUnreachable(); } if (name !== null && inferredTypes.has(name)) { let inferredType = inferredTypes.get(name); if (inferredType) { argumentExprs[i] = this.compileExpressionRetainType(argumentExpression, inferredType); let commonType: Type | null; if (!(commonType = Type.commonCompatible(inferredType, this.currentType, true))) { if (!(commonType = Type.commonCompatible(inferredType, this.currentType, false))) { this.error( DiagnosticCode.Type_0_is_not_assignable_to_type_1, parameterTypes[i].type.range, this.currentType.toString(), inferredType.toString() ); return module.createUnreachable(); } } inferredType = commonType; } else { argumentExprs[i] = this.compileExpressionRetainType(argumentExpression, Type.i32); inferredType = this.currentType; // ++numInferred; } inferredTypes.set(name, inferredType); } else { let concreteType = this.program.resolveType( parameterTypes[i].type, this.currentFunction.flow.contextualTypeArguments, true ); if (!concreteType) return module.createUnreachable(); argumentExprs[i] = this.compileExpression(argumentExpression, concreteType); } } let resolvedTypeArguments = new Array(numTypeParameters); for (let i = 0; i < numTypeParameters; ++i) { let inferredType = assert(inferredTypes.get(typeParameters[i].name.text)); // TODO resolvedTypeArguments[i] = inferredType; } instance = prototype.resolve( resolvedTypeArguments, this.currentFunction.flow.contextualTypeArguments ); if (!instance) return this.module.createUnreachable(); return this.makeCallDirect(instance, argumentExprs); // TODO: this skips inlining because inlining requires compiling its temporary locals in // the scope of the inlined flow. might need another mechanism to lock temp. locals early, // so inlining can be performed in `makeCallDirect` instead? // otherwise resolve the non-generic call as usual } else { instance = prototype.resolve( null, this.currentFunction.flow.contextualTypeArguments ); } if (!instance) return this.module.createUnreachable(); // compile 'this' expression if an instance method let thisExpr: ExpressionRef = 0; if (instance.is(CommonFlags.INSTANCE)) { thisExpr = this.compileExpressionRetainType( assert(this.program.resolvedThisExpression), this.options.usizeType ); } return this.compileCallDirect( instance, expression.arguments, expression, thisExpr, instance.hasDecorator(DecoratorFlags.INLINE) ); } // indirect call: index argument with signature (non-generic, can't be inlined) case ElementKind.LOCAL: { if (signature = (target).type.signatureReference) { indexArg = module.createGetLocal((target).index, NativeType.I32); break; } else { this.error( DiagnosticCode.Cannot_invoke_an_expression_whose_type_lacks_a_call_signature_Type_0_has_no_compatible_call_signatures, expression.range, (target).type.toString() ); return module.createUnreachable(); } } case ElementKind.GLOBAL: { if (signature = (target).type.signatureReference) { indexArg = module.createGetGlobal((target).internalName, (target).type.toNativeType()); break; } else { this.error( DiagnosticCode.Cannot_invoke_an_expression_whose_type_lacks_a_call_signature_Type_0_has_no_compatible_call_signatures, expression.range, (target).type.toString() ); return module.createUnreachable(); } } case ElementKind.FIELD: { let type = (target).type; if (signature = type.signatureReference) { let thisExpression = assert(this.program.resolvedThisExpression); let thisExpr = this.compileExpressionRetainType( thisExpression, this.options.usizeType ); indexArg = module.createLoad( 4, false, thisExpr, NativeType.I32, (target).memoryOffset ); break; } else { this.error( DiagnosticCode.Cannot_invoke_an_expression_whose_type_lacks_a_call_signature_Type_0_has_no_compatible_call_signatures, expression.range, (target).type.toString() ); return module.createUnreachable(); } } case ElementKind.FUNCTION_TARGET: { signature = (target).signature; indexArg = this.compileExpression(expression.expression, (target).type); break; } case ElementKind.PROPERTY: // TODO // not supported default: { this.error( DiagnosticCode.Operation_not_supported, expression.range ); return module.createUnreachable(); } } return this.compileCallIndirect( signature, indexArg, expression.arguments, expression ); } private compileCallExpressionBuiltin( prototype: FunctionPrototype, expression: CallExpression, contextualType: Type ): ExpressionRef { var expr = compileBuiltinCall( // reports this, prototype, prototype.resolveBuiltinTypeArguments( expression.typeArguments, this.currentFunction.flow.contextualTypeArguments ), expression.arguments, contextualType, expression ); if (!expr) { this.error( DiagnosticCode.Operation_not_supported, expression.range ); return this.module.createUnreachable(); } return expr; } /** * Checks that a call with the given number as arguments can be performed according to the * specified signature. */ checkCallSignature( signature: Signature, numArguments: i32, hasThis: bool, reportNode: Node ): bool { // cannot call an instance method without a `this` argument (TODO: `.call`?) var thisType = signature.thisType; if (hasThis != (thisType != null)) { this.error( DiagnosticCode.Operation_not_supported, // TODO: better message? reportNode.range ); return false; } // not yet implemented (TODO: maybe some sort of an unmanaged/lightweight array?) var hasRest = signature.hasRest; if (hasRest) { this.error( DiagnosticCode.Operation_not_supported, reportNode.range ); return false; } var minimum = signature.requiredParameters; var maximum = signature.parameterTypes.length; // must at least be called with required arguments if (numArguments < minimum) { this.error( minimum < maximum ? DiagnosticCode.Expected_at_least_0_arguments_but_got_1 : DiagnosticCode.Expected_0_arguments_but_got_1, reportNode.range, minimum.toString(), numArguments.toString() ); return false; } // must not be called with more than the maximum arguments if (numArguments > maximum && !hasRest) { this.error( DiagnosticCode.Expected_0_arguments_but_got_1, reportNode.range, maximum.toString(), numArguments.toString() ); return false; } return true; } /** Compiles a direct call to a concrete function. */ compileCallDirect( instance: Function, argumentExpressions: Expression[], reportNode: Node, thisArg: ExpressionRef = 0, inline: bool = false ): ExpressionRef { var numArguments = argumentExpressions.length; var signature = instance.signature; if (!this.checkCallSignature( // reports signature, numArguments, thisArg != 0, reportNode )) { return this.module.createUnreachable(); } // Inline if explicitly requested if (inline) { assert(!instance.is(CommonFlags.TRAMPOLINE)); // doesn't make sense return this.compileCallInlineUnchecked(instance, argumentExpressions, reportNode, thisArg); } // Otherwise compile to just a call var numArgumentsInclThis = thisArg ? numArguments + 1 : numArguments; var operands = new Array(numArgumentsInclThis); var index = 0; if (thisArg) { operands[0] = thisArg; index = 1; } var parameterTypes = signature.parameterTypes; for (let i = 0; i < numArguments; ++i, ++index) { operands[index] = this.compileExpression( argumentExpressions[i], parameterTypes[i] ); } assert(index == numArgumentsInclThis); return this.makeCallDirect(instance, operands); } // Depends on being pre-checked in compileCallDirect private compileCallInlineUnchecked( instance: Function, argumentExpressions: Expression[], reportNode: Node, thisArg: ExpressionRef = 0 ): ExpressionRef { var numArguments = argumentExpressions.length; var signature = instance.signature; var currentFunction = this.currentFunction; var module = this.module; var declaration = instance.prototype.declaration; // Create an empty child flow with its own scope and mark it for inlining var previousFlow = currentFunction.flow; var returnLabel = instance.internalName + "|inlined." + (instance.nextInlineId++).toString(10); var returnType = instance.signature.returnType; var flow = Flow.create(currentFunction); flow.set(FlowFlags.INLINE_CONTEXT); flow.returnLabel = returnLabel; flow.returnType = returnType; flow.contextualTypeArguments = instance.contextualTypeArguments; // Convert provided call arguments to temporary locals. It is important that these are compiled // here, with their respective locals being blocked. There is no 'makeCallInline'. var body = []; if (thisArg) { let parent = assert(instance.parent); assert(parent.kind == ElementKind.CLASS); let thisLocal = flow.addScopedLocal((parent).type, "this"); body.push( module.createSetLocal(thisLocal.index, thisArg) ); } var parameterTypes = signature.parameterTypes; for (let i = 0; i < numArguments; ++i) { let argumentLocal = flow.addScopedLocal(parameterTypes[i], signature.getParameterName(i)); body.push( module.createSetLocal(argumentLocal.index, this.compileExpression( argumentExpressions[i], parameterTypes[i] ) ) ); } // Compile optional parameter initializers in the scope of the inlined flow currentFunction.flow = flow; var numParameters = signature.parameterTypes.length; for (let i = numArguments; i < numParameters; ++i) { let argumentLocal = flow.addScopedLocal(parameterTypes[i], signature.getParameterName(i)); body.push( module.createSetLocal(argumentLocal.index, this.compileExpression( assert(declaration.signature.parameterTypes[i].initializer), parameterTypes[i] ) ) ); } // Compile the called function's body in the scope of the inlined flow var bodyStatement = assert(declaration.body); if (bodyStatement.kind == NodeKind.BLOCK) { // it's ok to unwrap the block here let statements = (bodyStatement).statements; for (let i = 0, k = statements.length; i < k; ++i) { body.push(this.compileStatement(statements[i])); } } else { body.push(this.compileStatement(bodyStatement)); } // Free any new scoped locals and reset to the original flow var scopedLocals = flow.scopedLocals; if (scopedLocals) { for (let scopedLocal of scopedLocals.values()) { currentFunction.freeTempLocal(scopedLocal); } flow.scopedLocals = null; } flow.finalize(); this.currentFunction.flow = previousFlow; this.currentType = returnType; // Check that all branches return if (returnType != Type.void && !flow.is(FlowFlags.RETURNS)) { this.error( DiagnosticCode.A_function_whose_declared_type_is_not_void_must_return_a_value, declaration.signature.returnType.range ); return module.createUnreachable(); } return module.createBlock(returnLabel, body, returnType.toNativeType()); } /** Gets the trampoline for the specified function. */ ensureTrampoline(original: Function): Function { // A trampoline is a function that takes a fixed amount of operands with some of them possibly // being zeroed. It takes one additional argument denoting the number of actual operands // provided to the call, and takes appropriate steps to initialize zeroed operands to their // default values using the optional parameter initializers of the original function. Doing so // allows calls to functions with optional parameters to circumvent the trampoline when all // parameters are provided as a fast route, respectively setting up omitted operands in a proper // context otherwise. var trampoline = original.trampoline; if (trampoline) return trampoline; var originalSignature = original.signature; var originalName = original.internalName; var originalParameterTypes = originalSignature.parameterTypes; var originalParameterDeclarations = original.prototype.declaration.signature.parameterTypes; var commonReturnType = originalSignature.returnType; var commonThisType = originalSignature.thisType; var isInstance = original.is(CommonFlags.INSTANCE); // arguments excl. `this`, operands incl. `this` var minArguments = originalSignature.requiredParameters; var minOperands = minArguments; var maxArguments = originalParameterTypes.length; var maxOperands = maxArguments; if (isInstance) { ++minOperands; ++maxOperands; } var numOptional = assert(maxOperands - minOperands); var forwardedOperands = new Array(minOperands); var operandIndex = 0; // forward `this` if applicable var module = this.module; if (isInstance) { forwardedOperands[0] = module.createGetLocal(0, this.options.nativeSizeType); operandIndex = 1; } // forward required arguments for (let i = 0; i < minArguments; ++i, ++operandIndex) { forwardedOperands[operandIndex] = module.createGetLocal(operandIndex, originalParameterTypes[i].toNativeType()); } assert(operandIndex == minOperands); // create the trampoline element var trampolineSignature = new Signature(originalParameterTypes, commonReturnType, commonThisType); var trampolineName = originalName + "|trampoline"; trampolineSignature.requiredParameters = maxArguments; trampoline = new Function( original.prototype, trampolineName, trampolineSignature, original.parent, original.contextualTypeArguments ); trampoline.set(original.flags | CommonFlags.TRAMPOLINE | CommonFlags.COMPILED); original.trampoline = trampoline; // compile initializers of omitted arguments in scope of the trampoline function // this is necessary because initializers might need additional locals and a proper this context var previousFunction = this.currentFunction; this.currentFunction = trampoline; // create a br_table switching over the number of optional parameters provided var numNames = numOptional + 1; // incl. outer block var names = new Array(numNames); var ofN = "of" + numOptional.toString(10); for (let i = 0; i < numNames; ++i) { let label = i.toString(10) + ofN; names[i] = label; } var body = module.createBlock(names[0], [ module.createBlock("oob", [ module.createSwitch(names, "oob", // condition is number of provided optional arguments, so subtract required arguments minArguments ? module.createBinary( BinaryOp.SubI32, module.createGetGlobal("~argc", NativeType.I32), module.createI32(minArguments) ) : module.createGetGlobal("~argc", NativeType.I32) ) ]), module.createUnreachable() ]); for (let i = 0; i < numOptional; ++i, ++operandIndex) { let type = originalParameterTypes[minArguments + i]; body = module.createBlock(names[i + 1], [ body, module.createSetLocal(operandIndex, this.compileExpression( assert(originalParameterDeclarations[minArguments + i].initializer), type ) ) ]); forwardedOperands[operandIndex] = module.createGetLocal(operandIndex, type.toNativeType()); } this.currentFunction = previousFunction; assert(operandIndex == maxOperands); var funcRef = module.addFunction( trampolineName, this.ensureFunctionType( trampolineSignature.parameterTypes, trampolineSignature.returnType, trampolineSignature.thisType ), typesToNativeTypes(trampoline.additionalLocals), module.createBlock(null, [ body, module.createCall( originalName, forwardedOperands, commonReturnType.toNativeType() ) ], commonReturnType.toNativeType()) ); trampoline.finalize(module, funcRef); return trampoline; } /** Makes sure that the argument count helper global is present and returns its name. */ private ensureArgcVar(): string { var internalName = "~argc"; if (!this.argcVar) { let module = this.module; this.argcVar = module.addGlobal( internalName, NativeType.I32, true, module.createI32(0) ); } return internalName; } /** Makes sure that the argument count helper setter is present and returns its name. */ private ensureArgcSet(): string { var internalName = "~setargc"; if (!this.argcSet) { let module = this.module; this.argcSet = module.addFunction(internalName, this.ensureFunctionType([ Type.u32 ], Type.void), null, module.createSetGlobal(this.ensureArgcVar(), module.createGetLocal(0, NativeType.I32) ) ); module.addFunctionExport(internalName, "_setargc"); } return internalName; } /** Creates a direct call to the specified function. */ makeCallDirect( instance: Function, operands: ExpressionRef[] | null = null ): ExpressionRef { var numOperands = operands ? operands.length : 0; var numArguments = numOperands; var minArguments = instance.signature.requiredParameters; var minOperands = minArguments; var maxArguments = instance.signature.parameterTypes.length; var maxOperands = maxArguments; if (instance.is(CommonFlags.INSTANCE)) { ++minOperands; ++maxOperands; --numArguments; } assert(numOperands >= minOperands); var module = this.module; if (!this.compileFunction(instance)) return module.createUnreachable(); var returnType = instance.signature.returnType; var isCallImport = instance.is(CommonFlags.MODULE_IMPORT); // fill up omitted arguments with zeroes if (numOperands < maxOperands) { if (!operands) { operands = new Array(maxOperands); operands.length = 0; } let parameterTypes = instance.signature.parameterTypes; for (let i = numArguments; i < maxArguments; ++i) { operands.push(parameterTypes[i].toNativeZero(module)); } if (!isCallImport) { // call the trampoline instance = this.ensureTrampoline(instance); if (!this.compileFunction(instance)) return module.createUnreachable(); let nativeReturnType = returnType.toNativeType(); this.currentType = returnType; return module.createBlock(null, [ module.createSetGlobal(this.ensureArgcVar(), module.createI32(numArguments)), module.createCall(instance.internalName, operands, nativeReturnType) ], nativeReturnType); } } // otherwise just call through this.currentType = returnType; return isCallImport ? module.createCallImport(instance.internalName, operands, returnType.toNativeType()) : module.createCall(instance.internalName, operands, returnType.toNativeType()); } /** Compiles an indirect call using an index argument and a signature. */ compileCallIndirect( signature: Signature, indexArg: ExpressionRef, argumentExpressions: Expression[], reportNode: Node, thisArg: ExpressionRef = 0 ): ExpressionRef { var numArguments = argumentExpressions.length; if (!this.checkCallSignature( // reports signature, numArguments, thisArg != 0, reportNode )) { return this.module.createUnreachable(); } var numArgumentsInclThis = thisArg ? numArguments + 1 : numArguments; var operands = new Array(numArgumentsInclThis); var index = 0; if (thisArg) { operands[0] = thisArg; index = 1; } var parameterTypes = signature.parameterTypes; for (let i = 0; i < numArguments; ++i, ++index) { operands[index] = this.compileExpression( argumentExpressions[i], parameterTypes[i] ); } assert(index == numArgumentsInclThis); return this.makeCallIndirect(signature, indexArg, operands); } /** Creates an indirect call to the function at `indexArg` in the function table. */ makeCallIndirect( signature: Signature, indexArg: ExpressionRef, operands: ExpressionRef[] | null = null ): ExpressionRef { var numOperands = operands ? operands.length : 0; var numArguments = numOperands; var minArguments = signature.requiredParameters; var minOperands = minArguments; var maxArguments = signature.parameterTypes.length; var maxOperands = maxArguments; if (signature.thisType) { ++minOperands; ++maxOperands; --numArguments; } assert(numOperands >= minOperands); this.ensureFunctionType(signature.parameterTypes, signature.returnType, signature.thisType); var module = this.module; // fill up omitted arguments with zeroes if (numOperands < maxOperands) { if (!operands) { operands = new Array(maxOperands); operands.length = 0; } let parameterTypes = signature.parameterTypes; for (let i = numArguments; i < maxArguments; ++i) { operands.push(parameterTypes[i].toNativeZero(module)); } } var returnType = signature.returnType; this.currentType = returnType; return module.createBlock(null, [ module.createSetGlobal(this.ensureArgcVar(), // might still be calling a trampoline module.createI32(numArguments) ), module.createCallIndirect(indexArg, operands, signature.toSignatureString()) ], returnType.toNativeType()); } compileCommaExpression(expression: CommaExpression, contextualType: Type): ExpressionRef { var expressions = expression.expressions; var numExpressions = expressions.length; var exprs = new Array(numExpressions--); for (let i = 0; i < numExpressions; ++i) { exprs[i] = this.compileExpression(expressions[i], Type.void); // drop all } exprs[numExpressions] = this.compileExpression(expressions[numExpressions], contextualType); // except last return this.module.createBlock(null, exprs, this.currentType.toNativeType()); } compileElementAccessExpression(expression: ElementAccessExpression, contextualType: Type): ExpressionRef { var target = this.program.resolveElementAccess(expression, this.currentFunction); // reports if (!target) return this.module.createUnreachable(); switch (target.kind) { case ElementKind.CLASS: { let indexedGet = (target).lookupOverload(OperatorKind.INDEXED_GET); if (!indexedGet) { this.error( DiagnosticCode.Index_signature_is_missing_in_type_0, expression.expression.range, (target).internalName ); return this.module.createUnreachable(); } let thisArg = this.compileExpression(expression.expression, (target).type); return this.compileCallDirect(indexedGet, [ expression.elementExpression ], expression, thisArg); } } this.error( DiagnosticCode.Operation_not_supported, expression.range ); return this.module.createUnreachable(); } compileFunctionExpression(expression: FunctionExpression, contextualType: Type): ExpressionRef { var declaration = expression.declaration; var name = declaration.name; var simpleName = (name.text.length ? name.text : "anonymous") + "|" + this.functionTable.length.toString(10); var currentFunction = this.currentFunction; var prototype = new FunctionPrototype( this.program, simpleName, currentFunction.internalName + INNER_DELIMITER + simpleName, declaration, null, DecoratorFlags.NONE ); var flow = currentFunction.flow; var instance = this.compileFunctionUsingTypeArguments( prototype, [], flow.contextualTypeArguments, flow, declaration ); if (!instance) return this.module.createUnreachable(); this.currentType = instance.signature.type; // TODO: get cached type? // NOTE that, in order to make this work in every case, the function must be represented by a // value, so we add it and rely on the optimizer to figure out where it can be called directly. var index = this.ensureFunctionTableEntry(instance); // reports return index < 0 ? this.module.createUnreachable() : this.module.createI32(index); } /** * Compiles an identifier in the specified context. * @param retainConstantType Retains the type of inlined constants if `true`, otherwise * precomputes them according to context. */ compileIdentifierExpression( expression: IdentifierExpression, contextualType: Type, retainConstantType: bool ): ExpressionRef { var module = this.module; // check special keywords first switch (expression.kind) { case NodeKind.NULL: { let options = this.options; if (!contextualType.classReference) { this.currentType = options.usizeType; } return options.isWasm64 ? module.createI64(0) : module.createI32(0); } case NodeKind.TRUE: { this.currentType = Type.bool; return module.createI32(1); } case NodeKind.FALSE: { this.currentType = Type.bool; return module.createI32(0); } case NodeKind.THIS: { let currentFunction = this.currentFunction; let flow = currentFunction.flow; if (flow.is(FlowFlags.INLINE_CONTEXT)) { let scopedThis = flow.getScopedLocal("this"); if (scopedThis) { this.currentType = scopedThis.type; return module.createGetLocal(scopedThis.index, scopedThis.type.toNativeType()); } } if (currentFunction.is(CommonFlags.INSTANCE)) { let parent = assert(currentFunction.parent); assert(parent.kind == ElementKind.CLASS); let thisType = (parent).type; if (currentFunction.is(CommonFlags.CONSTRUCTOR)) { if (!flow.is(FlowFlags.ALLOCATES)) { flow.set(FlowFlags.ALLOCATES); // must be conditional because `this` could have been provided by a derived class this.currentType = thisType; return module.createTeeLocal(0, makeConditionalAllocate(this, parent, expression) ); } } this.currentType = thisType; return module.createGetLocal(0, thisType.toNativeType()); } this.error( DiagnosticCode._this_cannot_be_referenced_in_current_location, expression.range ); this.currentType = this.options.usizeType; return module.createUnreachable(); } case NodeKind.SUPER: { let currentFunction = this.currentFunction; let flow = currentFunction.flow; if (flow.is(FlowFlags.INLINE_CONTEXT)) { let scopedThis = flow.getScopedLocal("this"); if (scopedThis) { let scopedThisClass = assert(scopedThis.type.classReference); let base = scopedThisClass.base; if (base) { this.currentType = base.type; return module.createGetLocal(scopedThis.index, base.type.toNativeType()); } } } if (currentFunction.is(CommonFlags.INSTANCE)) { let parent = assert(currentFunction.parent); assert(parent.kind == ElementKind.CLASS); let base = (parent).base; if (base) { let superType = base.type; this.currentType = superType; return module.createGetLocal(0, superType.toNativeType()); } } this.error( DiagnosticCode._super_can_only_be_referenced_in_a_derived_class, expression.range ); this.currentType = this.options.usizeType; return module.createUnreachable(); } } // otherwise resolve var target = this.program.resolveIdentifier( // reports expression, this.currentFunction, this.currentEnum ); if (!target) return module.createUnreachable(); switch (target.kind) { case ElementKind.LOCAL: { if ((target).is(CommonFlags.INLINED)) { return this.compileInlineConstant(target, contextualType, retainConstantType); } let localType = (target).type; let localIndex = (target).index; assert(localIndex >= 0); this.currentType = localType; return this.module.createGetLocal(localIndex, localType.toNativeType()); } case ElementKind.GLOBAL: { if (!this.compileGlobal(target)) { // reports; not yet compiled if a static field return this.module.createUnreachable(); } let globalType = (target).type; assert(globalType != Type.void); if ((target).is(CommonFlags.INLINED)) { return this.compileInlineConstant(target, contextualType, retainConstantType); } this.currentType = globalType; return this.module.createGetGlobal((target).internalName, globalType.toNativeType()); } case ElementKind.ENUMVALUE: { // here: if referenced from within the same enum if (!target.is(CommonFlags.COMPILED)) { this.error( DiagnosticCode.A_member_initializer_in_a_enum_declaration_cannot_reference_members_declared_after_it_including_members_defined_in_other_enums, expression.range ); this.currentType = Type.i32; return this.module.createUnreachable(); } this.currentType = Type.i32; if ((target).is(CommonFlags.INLINED)) { return this.module.createI32((target).constantValue); } return this.module.createGetGlobal((target).internalName, NativeType.I32); } case ElementKind.FUNCTION_PROTOTYPE: { let instance = (target).resolve( null, this.currentFunction.flow.contextualTypeArguments ); if (!(instance && this.compileFunction(instance))) return module.createUnreachable(); let index = this.ensureFunctionTableEntry(instance); this.currentType = instance.signature.type; return this.module.createI32(index); } } this.error( DiagnosticCode.Operation_not_supported, expression.range ); return this.module.createUnreachable(); } compileLiteralExpression( expression: LiteralExpression, contextualType: Type, implicitNegate: bool = false ): ExpressionRef { var module = this.module; switch (expression.literalKind) { case LiteralKind.ARRAY: { assert(!implicitNegate); let classType = contextualType.classReference; if ( classType && classType.prototype == this.program.arrayPrototype ) { return this.compileArrayLiteral( assert(classType.typeArguments)[0], (expression).elementExpressions, expression ); } this.error( DiagnosticCode.Operation_not_supported, expression.range ); return module.createUnreachable(); } case LiteralKind.FLOAT: { let floatValue = (expression).value; if (implicitNegate) { floatValue = -floatValue; } if (contextualType == Type.f32) { return module.createF32(floatValue); } this.currentType = Type.f64; return module.createF64(floatValue); } case LiteralKind.INTEGER: { let intValue = (expression).value; if (implicitNegate) { intValue = i64_sub( i64_new(0), intValue ); } switch (contextualType.kind) { // compile to contextualType if matching case TypeKind.I8: { if (i64_is_i8(intValue)) return module.createI32(i64_low(intValue)); break; } case TypeKind.U8: { if (i64_is_u8(intValue)) return module.createI32(i64_low(intValue)); break; } case TypeKind.I16: { if (i64_is_i16(intValue)) return module.createI32(i64_low(intValue)); break; } case TypeKind.U16: { if (i64_is_u16(intValue)) return module.createI32(i64_low(intValue)); break; } case TypeKind.I32: case TypeKind.U32: { if (i64_is_i32(intValue) || i64_is_u32(intValue)) return module.createI32(i64_low(intValue)); break; } case TypeKind.BOOL: { if (i64_is_bool(intValue)) return module.createI32(i64_low(intValue)); break; } case TypeKind.ISIZE: { if (!this.options.isWasm64) { if (i64_is_i32(intValue) || i64_is_u32(intValue)) return module.createI32(i64_low(intValue)); break; } return module.createI64(i64_low(intValue), i64_high(intValue)); } case TypeKind.USIZE: { if (!this.options.isWasm64) { if (i64_is_i32(intValue) || i64_is_u32(intValue)) return module.createI32(i64_low(intValue)); break; } return module.createI64(i64_low(intValue), i64_high(intValue)); } case TypeKind.I64: case TypeKind.U64: { return module.createI64(i64_low(intValue), i64_high(intValue)); } case TypeKind.F32: { if (i64_is_f32(intValue)) return module.createF32(i64_to_f32(intValue)); break; } case TypeKind.F64: { if (i64_is_f64(intValue)) return module.createF64(i64_to_f64(intValue)); break; } case TypeKind.VOID: { break; // compiles to best fitting type below, being dropped } default: { assert(false); this.error( DiagnosticCode.Operation_not_supported, expression.range ); return module.createUnreachable(); } } // otherwise compile to best fitting native type if (i64_is_i32(intValue)) { this.currentType = Type.i32; return module.createI32(i64_low(intValue)); } else { this.currentType = Type.i64; return module.createI64(i64_low(intValue), i64_high(intValue)); } } case LiteralKind.STRING: { assert(!implicitNegate); return this.compileStaticString((expression).value); } // case LiteralKind.OBJECT: // case LiteralKind.REGEXP: } this.error( DiagnosticCode.Operation_not_supported, expression.range ); this.currentType = contextualType; return module.createUnreachable(); } compileStaticString(stringValue: string): ExpressionRef { var module = this.module; var options = this.options; var stringSegments = this.stringSegments; var stringSegment: MemorySegment | null = stringSegments.get(stringValue); if (!stringSegment) { let stringLength = stringValue.length; let stringBuffer = new Uint8Array(4 + stringLength * 2); stringBuffer[0] = stringLength & 0xff; stringBuffer[1] = (stringLength >>> 8) & 0xff; stringBuffer[2] = (stringLength >>> 16) & 0xff; stringBuffer[3] = (stringLength >>> 24) & 0xff; for (let i = 0; i < stringLength; ++i) { stringBuffer[4 + i * 2] = stringValue.charCodeAt(i) & 0xff; stringBuffer[5 + i * 2] = (stringValue.charCodeAt(i) >>> 8) & 0xff; } stringSegment = this.addMemorySegment(stringBuffer, options.usizeType.byteSize); stringSegments.set(stringValue, stringSegment); } var stringOffset = stringSegment.offset; var stringType = this.program.typesLookup.get("string"); this.currentType = stringType ? stringType : options.usizeType; if (options.isWasm64) { return module.createI64(i64_low(stringOffset), i64_high(stringOffset)); } assert(i64_is_i32(stringOffset)); return module.createI32(i64_low(stringOffset)); } compileArrayLiteral(elementType: Type, expressions: (Expression | null)[], reportNode: Node): ExpressionRef { var isStatic = true; var module = this.module; // obtain the array type var arrayPrototype = assert(this.program.arrayPrototype); if (!arrayPrototype || arrayPrototype.kind != ElementKind.CLASS_PROTOTYPE) return module.createUnreachable(); var arrayInstance = (arrayPrototype).resolve([ elementType ]); if (!arrayInstance) return module.createUnreachable(); var arrayType = arrayInstance.type; var elementCount = expressions.length; if (elementCount) { // non-empty static or dynamic let nativeElementType = elementType.toNativeType(); let values: usize; let byteLength: usize; switch (nativeElementType) { case NativeType.I32: { values = changetype(new Int32Array(elementCount)); byteLength = elementCount * 4; break; } case NativeType.I64: { values = changetype(new Array(elementCount)); byteLength = elementCount * 8; break; } case NativeType.F32: { values = changetype(new Float32Array(elementCount)); byteLength = elementCount * 4; break; } case NativeType.F64: { values = changetype(new Float64Array(elementCount)); byteLength = elementCount * 8; break; } default: { assert(false); this.error( DiagnosticCode.Operation_not_supported, reportNode.range ); return module.createUnreachable(); } } // precompute value expressions let exprs = new Array(elementCount); let expr: BinaryenExpressionRef; for (let i = 0; i < elementCount; ++i) { exprs[i] = expressions[i] ? this.compileExpression(expressions[i], elementType) : elementType.toNativeZero(module); if (isStatic) { expr = this.precomputeExpressionRef(exprs[i]); if (_BinaryenExpressionGetId(expr) == ExpressionId.Const) { assert(_BinaryenExpressionGetType(expr) == nativeElementType); switch (nativeElementType) { case NativeType.I32: { changetype(values)[i] = _BinaryenConstGetValueI32(expr); break; } case NativeType.I64: { changetype(values)[i] = i64_new( _BinaryenConstGetValueI64Low(expr), _BinaryenConstGetValueI64High(expr) ); break; } case NativeType.F32: { changetype(values)[i] = _BinaryenConstGetValueF32(expr); break; } case NativeType.F64: { changetype(values)[i] = _BinaryenConstGetValueF64(expr); break; } default: { assert(false); // checked above } } } else { // TODO: emit a warning if declared 'const' // if (isConst) { // this.warn( // DiagnosticCode.Compiling_constant_with_non_constant_initializer_as_mutable, // reportNode.range // ); // } isStatic = false; } } } let usizeTypeSize = this.options.usizeType.byteSize; if (isStatic) { // non-empty, all elements can be precomputed // Create a combined static memory segment composed of: // Array struct + ArrayBuffer struct + aligned ArrayBuffer data let arraySize = usizeTypeSize + 4; // buffer_ & length_ let bufferHeaderSize = (4 + 7) & ~7; // aligned byteLength (8) let bufferTotalSize = 1 << (32 - clz(byteLength + bufferHeaderSize - 1)); // see internals let data = new Uint8Array(arraySize + bufferTotalSize); let segment = this.addMemorySegment(data); let offset = 0; // write Array struct if (usizeTypeSize == 8) { writeI64(i64_add(segment.offset, i64_new(arraySize)), data, offset); // buffer_ @ segment[arSize] offset += 8; } else { assert(i64_high(segment.offset) == 0); writeI32(i64_low(segment.offset) + arraySize, data, offset); // buffer_ @ segment[arSize] offset += 4; } writeI32(elementCount, data, offset); // length_ offset += 4; assert(offset == arraySize); // write ArrayBuffer struct writeI32(byteLength, data, offset); offset += bufferHeaderSize; // incl. alignment // write ArrayBuffer data switch (nativeElementType) { case NativeType.I32: { for (let i = 0; i < elementCount; ++i) { writeI32(changetype(values)[i], data, offset); offset += 4; } break; } case NativeType.I64: { for (let i = 0; i < elementCount; ++i) { writeI64(changetype(values)[i], data, offset); offset += 8; } break; } case NativeType.F32: { for (let i = 0; i < elementCount; ++i) { writeF32(changetype(values)[i], data, offset); offset += 4; } break; } case NativeType.F64: { for (let i = 0; i < elementCount; ++i) { writeF64(changetype(values)[i], data, offset); offset += 8; } break; } default: { assert(false); this.error( DiagnosticCode.Operation_not_supported, reportNode.range ); return module.createUnreachable(); } } assert(offset <= arraySize + bufferTotalSize); this.currentType = arrayType; return usizeTypeSize == 8 ? module.createI64( i64_low(segment.offset), i64_high(segment.offset) ) : module.createI32( i64_low(segment.offset) ); } else { // non-empty, some elements can't be precomputed this.currentType = arrayType; let setter = arrayInstance.lookupOverload(OperatorKind.INDEXED_SET); if (!setter) { this.error( DiagnosticCode.Index_signature_in_type_0_only_permits_reading, reportNode.range, arrayInstance.internalName ); return module.createUnreachable(); } let nativeArrayType = arrayType.toNativeType(); let currentFunction = this.currentFunction; let tempLocal = currentFunction.getTempLocal(arrayType); let stmts = new Array(2 + elementCount); let index = 0; stmts[index++] = module.createSetLocal(tempLocal.index, this.makeCallDirect(assert(arrayInstance.constructorInstance), [ module.createI32(0), // this module.createI32(elementCount) ]) ); for (let i = 0; i < elementCount; ++i) { stmts[index++] = this.makeCallDirect(setter, [ module.createGetLocal(tempLocal.index, nativeArrayType), // this module.createI32(i), exprs[i] ]); } assert(index + 1 == stmts.length); stmts[index] = module.createGetLocal(tempLocal.index, nativeArrayType); currentFunction.freeTempLocal(tempLocal); this.currentType = arrayType; return module.createBlock(null, stmts, nativeArrayType); } } else { // empty, TBD: cache this somehow? this.currentType = arrayType; return this.makeCallDirect(assert(arrayInstance.constructorInstance), [ module.createI32(0), // this module.createI32(0) ]); } } compileNewExpression(expression: NewExpression, contextualType: Type): ExpressionRef { var module = this.module; var options = this.options; var currentFunction = this.currentFunction; // obtain the class being instantiated var target = this.program.resolveExpression( // reports expression.expression, currentFunction ); if (!target) return module.createUnreachable(); if (target.kind != ElementKind.CLASS_PROTOTYPE) { this.error( DiagnosticCode.Cannot_use_new_with_an_expression_whose_type_lacks_a_construct_signature, expression.expression.range ); return this.module.createUnreachable(); } var classPrototype = target; var classInstance = classPrototype.resolveUsingTypeArguments( // reports expression.typeArguments, currentFunction.flow.contextualTypeArguments, expression ); if (!classInstance) return module.createUnreachable(); var expr: ExpressionRef; // traverse to the first matching constructor var currentClassInstance: Class | null = classInstance; var constructorInstance = classInstance.constructorInstance; while (!constructorInstance && (currentClassInstance = classInstance.base)) { constructorInstance = currentClassInstance.constructorInstance; } // if a constructor is present, call it with a zero `this` if (constructorInstance) { expr = this.compileCallDirect(constructorInstance, expression.arguments, expression, options.usizeType.toNativeZero(module) ); // otherwise simply allocate a new instance and initialize its fields } else { expr = makeAllocate(this, classInstance, expression); } this.currentType = classInstance.type; return expr; } compileParenthesizedExpression( expression: ParenthesizedExpression, contextualType: Type, wrapSmallIntegers: bool = true ): ExpressionRef { // does not change types, just order return this.compileExpression( expression.expression, contextualType, ConversionKind.NONE, wrapSmallIntegers ); } /** * Compiles a property access in the specified context. * @param retainConstantType Retains the type of inlined constants if `true`, otherwise * precomputes them according to context. */ compilePropertyAccessExpression( propertyAccess: PropertyAccessExpression, contextualType: Type, retainConstantType: bool ): ExpressionRef { var program = this.program; var module = this.module; var target = program.resolvePropertyAccess(propertyAccess, this.currentFunction); // reports if (!target) return module.createUnreachable(); switch (target.kind) { case ElementKind.GLOBAL: { // static property if (!this.compileGlobal(target)) { // reports; not yet compiled if a static field return module.createUnreachable(); } let globalType = (target).type; assert(globalType != Type.void); if ((target).is(CommonFlags.INLINED)) { return this.compileInlineConstant(target, contextualType, retainConstantType); } this.currentType = globalType; return module.createGetGlobal((target).internalName, globalType.toNativeType()); } case ElementKind.ENUMVALUE: { // enum value let parent = (target).parent; assert(parent !== null && parent.kind == ElementKind.ENUM); if (!this.compileEnum(parent)) { return this.module.createUnreachable(); } this.currentType = Type.i32; if ((target).is(CommonFlags.INLINED)) { return module.createI32((target).constantValue); } return module.createGetGlobal((target).internalName, NativeType.I32); } case ElementKind.FIELD: { // instance field let thisExpression = assert(program.resolvedThisExpression); assert((target).memoryOffset >= 0); let thisExpr = this.compileExpressionRetainType( thisExpression, this.options.usizeType ); this.currentType = (target).type; return module.createLoad( (target).type.byteSize, (target).type.is(TypeFlags.SIGNED | TypeFlags.INTEGER), thisExpr, (target).type.toNativeType(), (target).memoryOffset ); } case ElementKind.PROPERTY: { // instance property (here: getter) let prototype = (target).getterPrototype; if (prototype) { let instance = prototype.resolve(null); // reports if (!instance) return module.createUnreachable(); let signature = instance.signature; if (!this.checkCallSignature( // reports signature, 0, instance.is(CommonFlags.INSTANCE), propertyAccess )) { return module.createUnreachable(); } if (instance.is(CommonFlags.INSTANCE)) { let parent = assert(instance.parent); assert(parent.kind == ElementKind.CLASS); let thisExpression = assert(program.resolvedThisExpression); let thisExpr = this.compileExpressionRetainType( thisExpression, this.options.usizeType ); this.currentType = signature.returnType; return this.compileCallDirect(instance, [], propertyAccess, thisExpr); } else { this.currentType = signature.returnType; return this.compileCallDirect(instance, [], propertyAccess); } } else { this.error( DiagnosticCode.Property_0_does_not_exist_on_type_1, propertyAccess.range, (target).simpleName, (target).parent.toString() ); return module.createUnreachable(); } } } this.error( DiagnosticCode.Operation_not_supported, propertyAccess.range ); return module.createUnreachable(); } compileTernaryExpression(expression: TernaryExpression, contextualType: Type): ExpressionRef { var ifThen = expression.ifThen; var ifElse = expression.ifElse; var currentFunction = this.currentFunction; var condExpr = makeIsTrueish( this.compileExpression(expression.condition, Type.u32, ConversionKind.NONE), this.currentType, this.module ); if ( !this.options.noTreeShaking || this.currentFunction.isAny(CommonFlags.GENERIC | CommonFlags.GENERIC_CONTEXT) ) { // Try to eliminate unnecesssary branches if the condition is constant let condExprPrecomp = this.precomputeExpressionRef(condExpr); if ( _BinaryenExpressionGetId(condExprPrecomp) == ExpressionId.Const && _BinaryenExpressionGetType(condExprPrecomp) == NativeType.I32 ) { return _BinaryenConstGetValueI32(condExprPrecomp) ? this.compileExpression(ifThen, contextualType) : this.compileExpression(ifElse, contextualType); // Otherwise recompile to the original and let the optimizer decide } else /* if (condExpr != condExprPrecomp) <- not guaranteed */ { condExpr = makeIsTrueish( this.compileExpression(expression.condition, Type.u32, ConversionKind.NONE), this.currentType, this.module ); } } var ifThenExpr: ExpressionRef; var ifElseExpr: ExpressionRef; var ifThenType: Type; var ifElseType: Type; // if part of a constructor, keep track of memory allocations if (currentFunction.is(CommonFlags.CONSTRUCTOR)) { let flow = currentFunction.flow; flow = flow.enterBranchOrScope(); currentFunction.flow = flow; ifThenExpr = this.compileExpressionRetainType(ifThen, contextualType); ifThenType = this.currentType; let ifThenAllocates = flow.is(FlowFlags.ALLOCATES); flow = flow.leaveBranchOrScope(); currentFunction.flow = flow; flow = flow.enterBranchOrScope(); currentFunction.flow = flow; ifElseExpr = this.compileExpressionRetainType(ifElse, contextualType); ifElseType = this.currentType; let ifElseAllocates = flow.is(FlowFlags.ALLOCATES); flow = flow.leaveBranchOrScope(); currentFunction.flow = flow; if (ifThenAllocates && ifElseAllocates) flow.set(FlowFlags.ALLOCATES); // otherwise simplify } else { ifThenExpr = this.compileExpressionRetainType(ifThen, contextualType); ifThenType = this.currentType; ifElseExpr = this.compileExpressionRetainType(ifElse, contextualType); ifElseType = this.currentType; } var commonType = Type.commonCompatible(ifThenType, ifElseType, false); if (!commonType) { this.error( DiagnosticCode.Type_0_is_not_assignable_to_type_1, expression.range, ifThenType.toString(), ifElseType.toString() ); this.currentType = contextualType; return this.module.createUnreachable(); } ifThenExpr = this.convertExpression(ifThenExpr, ifThenType, commonType, ConversionKind.IMPLICIT, ifThen); ifElseExpr = this.convertExpression(ifElseExpr, ifElseType, commonType, ConversionKind.IMPLICIT, ifElse); this.currentType = commonType; return this.module.createIf(condExpr, ifThenExpr, ifElseExpr); } compileUnaryPostfixExpression(expression: UnaryPostfixExpression, contextualType: Type): ExpressionRef { var module = this.module; var currentFunction = this.currentFunction; // make a getter for the expression (also obtains the type) var getValue = this.compileExpression( // reports expression.operand, contextualType == Type.void ? Type.i32 : contextualType, ConversionKind.NONE, false // wrapped below ); if (_BinaryenExpressionGetId(getValue) == ExpressionId.Unreachable) { // shortcut if compiling the getter already failed return getValue; } var currentType = this.currentType; var op: BinaryOp; var nativeType: NativeType; var nativeOne: ExpressionRef; var possiblyOverflows = false; switch (expression.operator) { case Token.PLUS_PLUS: { if (currentType.is(TypeFlags.REFERENCE)) { this.error( DiagnosticCode.Operation_not_supported, expression.range ); return module.createUnreachable(); } switch (currentType.kind) { case TypeKind.I8: case TypeKind.I16: case TypeKind.U8: case TypeKind.U16: case TypeKind.BOOL: possiblyOverflows = true; default: { op = BinaryOp.AddI32; nativeType = NativeType.I32; nativeOne = module.createI32(1); break; } case TypeKind.USIZE: // TODO: check operator overload case TypeKind.ISIZE: { let options = this.options; op = options.isWasm64 ? BinaryOp.AddI64 : BinaryOp.AddI32; nativeType = options.nativeSizeType; nativeOne = currentType.toNativeOne(module); break; } case TypeKind.I64: case TypeKind.U64: { op = BinaryOp.AddI64; nativeType = NativeType.I64; nativeOne = module.createI64(1); break; } case TypeKind.F32: { op = BinaryOp.AddF32; nativeType = NativeType.F32; nativeOne = module.createF32(1); break; } case TypeKind.F64: { op = BinaryOp.AddF64; nativeType = NativeType.F64; nativeOne = module.createF64(1); break; } case TypeKind.VOID: { assert(false); this.error( DiagnosticCode.Operation_not_supported, expression.range ); return module.createUnreachable(); } } break; } case Token.MINUS_MINUS: { if (currentType.is(TypeFlags.REFERENCE)) { this.error( DiagnosticCode.Operation_not_supported, expression.range ); return module.createUnreachable(); } switch (currentType.kind) { case TypeKind.I8: case TypeKind.I16: case TypeKind.U8: case TypeKind.U16: case TypeKind.BOOL: possiblyOverflows = true; default: { op = BinaryOp.SubI32; nativeType = NativeType.I32; nativeOne = module.createI32(1); break; } case TypeKind.USIZE: // TODO: check operator overload case TypeKind.ISIZE: { let options = this.options; op = options.isWasm64 ? BinaryOp.SubI64 : BinaryOp.SubI32; nativeType = options.nativeSizeType; nativeOne = currentType.toNativeOne(module); break; } case TypeKind.I64: case TypeKind.U64: { op = BinaryOp.SubI64; nativeType = NativeType.I64; nativeOne = module.createI64(1); break; } case TypeKind.F32: { op = BinaryOp.SubF32; nativeType = NativeType.F32; nativeOne = module.createF32(1); break; } case TypeKind.F64: { op = BinaryOp.SubF64; nativeType = NativeType.F64; nativeOne = module.createF64(1); break; } case TypeKind.VOID: { assert(false); this.error( DiagnosticCode.Operation_not_supported, expression.range ); return module.createUnreachable(); } } break; } default: { assert(false); this.error( DiagnosticCode.Operation_not_supported, expression.range ); return module.createUnreachable(); } } var setValue: ExpressionRef; var tempLocal: Local | null = null; // simplify if dropped anyway if (contextualType == Type.void) { setValue = module.createBinary(op, getValue, nativeOne ); // otherwise use a temp local for the intermediate value } else { tempLocal = currentFunction.getTempLocal(currentType); setValue = module.createBinary(op, this.module.createGetLocal(tempLocal.index, nativeType), nativeOne ); } if (possiblyOverflows) { assert(currentType.is(TypeFlags.SHORT | TypeFlags.INTEGER)); setValue = makeSmallIntegerWrap(setValue, currentType, module); } setValue = this.compileAssignmentWithValue(expression.operand, setValue, false); // ^ sets currentType = void if (contextualType == Type.void) { assert(!tempLocal); return setValue; } this.currentType = assert(tempLocal).type; currentFunction.freeTempLocal(tempLocal); var localIndex = (tempLocal).index; return module.createBlock(null, [ module.createSetLocal(localIndex, getValue), setValue, module.createGetLocal(localIndex, nativeType) ], nativeType); } compileUnaryPrefixExpression( expression: UnaryPrefixExpression, contextualType: Type, wrapSmallIntegers: bool = true ): ExpressionRef { var module = this.module; var currentType = this.currentType; var possiblyOverflows = false; var compound = false; var expr: ExpressionRef; switch (expression.operator) { case Token.PLUS: { if (currentType.is(TypeFlags.REFERENCE)) { this.error( DiagnosticCode.Operation_not_supported, expression.range ); return module.createUnreachable(); } expr = this.compileExpression( expression.operand, contextualType == Type.void ? Type.i32 : contextualType, ConversionKind.NONE, false // wrapped below ); currentType = this.currentType; possiblyOverflows = currentType.is(TypeFlags.SHORT | TypeFlags.INTEGER); // if operand already did break; } case Token.MINUS: { if (currentType.is(TypeFlags.REFERENCE)) { this.error( DiagnosticCode.Operation_not_supported, expression.range ); return module.createUnreachable(); } if (expression.operand.kind == NodeKind.LITERAL && ( (expression.operand).literalKind == LiteralKind.INTEGER || (expression.operand).literalKind == LiteralKind.FLOAT )) { // implicitly negate integer and float literals. also enables proper checking of literal ranges. expr = this.compileLiteralExpression(expression.operand, contextualType, true); if (this.options.sourceMap) { // compileExpression normally does this addDebugLocation(expr, expression.range, module, this.currentFunction); } currentType = this.currentType; } else { expr = this.compileExpression( expression.operand, contextualType == Type.void ? Type.i32 : contextualType, ConversionKind.NONE, false // wrapped below ); currentType = this.currentType; switch (currentType.kind) { case TypeKind.I8: case TypeKind.I16: case TypeKind.U8: case TypeKind.U16: case TypeKind.BOOL: possiblyOverflows = true; // or if operand already did default: { expr = module.createBinary(BinaryOp.SubI32, module.createI32(0), expr); break; } case TypeKind.USIZE: { if (currentType.is(TypeFlags.REFERENCE)) { this.error( DiagnosticCode.Operation_not_supported, expression.range ); return module.createUnreachable(); } // fall-through } case TypeKind.ISIZE: { expr = module.createBinary( this.options.isWasm64 ? BinaryOp.SubI64 : BinaryOp.SubI32, currentType.toNativeZero(module), expr ); break; } case TypeKind.I64: case TypeKind.U64: { expr = module.createBinary(BinaryOp.SubI64, module.createI64(0), expr); break; } case TypeKind.F32: { expr = module.createUnary(UnaryOp.NegF32, expr); break; } case TypeKind.F64: { expr = module.createUnary(UnaryOp.NegF64, expr); break; } } } break; } case Token.PLUS_PLUS: { if (currentType.is(TypeFlags.REFERENCE)) { this.error( DiagnosticCode.Operation_not_supported, expression.range ); return module.createUnreachable(); } compound = true; expr = this.compileExpression( expression.operand, contextualType == Type.void ? Type.i32 : contextualType, ConversionKind.NONE, false // wrapped below ); currentType = this.currentType; switch (currentType.kind) { case TypeKind.I8: case TypeKind.I16: case TypeKind.U8: case TypeKind.U16: case TypeKind.BOOL: possiblyOverflows = true; // or if operand already did default: { expr = module.createBinary(BinaryOp.AddI32, expr, this.module.createI32(1)); break; } case TypeKind.USIZE: { if (currentType.is(TypeFlags.REFERENCE)) { this.error( DiagnosticCode.Operation_not_supported, expression.range ); return module.createUnreachable(); } // fall-through } case TypeKind.ISIZE: { expr = module.createBinary( this.options.isWasm64 ? BinaryOp.AddI64 : BinaryOp.AddI32, expr, currentType.toNativeOne(module) ); break; } case TypeKind.I64: case TypeKind.U64: { expr = module.createBinary(BinaryOp.AddI64, expr, module.createI64(1)); break; } case TypeKind.F32: { expr = module.createBinary(BinaryOp.AddF32, expr, module.createF32(1)); break; } case TypeKind.F64: { expr = module.createBinary(BinaryOp.AddF64, expr, module.createF64(1)); break; } } break; } case Token.MINUS_MINUS: { if (currentType.is(TypeFlags.REFERENCE)) { this.error( DiagnosticCode.Operation_not_supported, expression.range ); return module.createUnreachable(); } compound = true; expr = this.compileExpression( expression.operand, contextualType == Type.void ? Type.i32 : contextualType, ConversionKind.NONE, false // wrapped below ); currentType = this.currentType; switch (currentType.kind) { case TypeKind.I8: case TypeKind.I16: case TypeKind.U8: case TypeKind.U16: case TypeKind.BOOL: possiblyOverflows = true; // or if operand already did default: { expr = module.createBinary(BinaryOp.SubI32, expr, module.createI32(1)); break; } case TypeKind.USIZE: { if (currentType.is(TypeFlags.REFERENCE)) { this.error( DiagnosticCode.Operation_not_supported, expression.range ); return module.createUnreachable(); } // fall-through } case TypeKind.ISIZE: { expr = module.createBinary( this.options.isWasm64 ? BinaryOp.SubI64 : BinaryOp.SubI32, expr, currentType.toNativeOne(module) ); break; } case TypeKind.I64: case TypeKind.U64: { expr = module.createBinary(BinaryOp.SubI64, expr, module.createI64(1)); break; } case TypeKind.F32: { expr = module.createBinary(BinaryOp.SubF32, expr, module.createF32(1)); break; } case TypeKind.F64: { expr = module.createBinary(BinaryOp.SubF64, expr, module.createF64(1)); break; } } break; } case Token.EXCLAMATION: { expr = this.compileExpression( expression.operand, contextualType == Type.void ? Type.i32 : contextualType, ConversionKind.NONE, true // must wrap small integers ); expr = makeIsFalseish(expr, this.currentType, module); this.currentType = Type.bool; break; } case Token.TILDE: { if (currentType.is(TypeFlags.REFERENCE)) { this.error( DiagnosticCode.Operation_not_supported, expression.range ); return module.createUnreachable(); } expr = this.compileExpression( expression.operand, contextualType == Type.void ? Type.i32 : contextualType.is(TypeFlags.FLOAT) ? Type.i64 : contextualType, contextualType == Type.void ? ConversionKind.NONE : ConversionKind.IMPLICIT, false // retains low bits of small integers ); currentType = this.currentType; switch (currentType.kind) { case TypeKind.I8: case TypeKind.I16: case TypeKind.U8: case TypeKind.U16: case TypeKind.BOOL: possiblyOverflows = true; // or if operand already did default: { expr = module.createBinary(BinaryOp.XorI32, expr, module.createI32(-1)); break; } case TypeKind.USIZE: { if (currentType.is(TypeFlags.REFERENCE)) { this.error( DiagnosticCode.Operation_not_supported, expression.range ); return module.createUnreachable(); } // fall-through } case TypeKind.ISIZE: { expr = module.createBinary( this.options.isWasm64 ? BinaryOp.XorI64 : BinaryOp.XorI32, expr, currentType.toNativeNegOne(module) ); break; } case TypeKind.I64: case TypeKind.U64: { expr = module.createBinary(BinaryOp.XorI64, expr, module.createI64(-1, -1)); break; } } break; } case Token.TYPEOF: { // it might make sense to implement typeof in a way that a generic function can detect // whether its type argument is a class type or string. that could then be used, for // example, to generate hash codes for sets and maps, depending on the kind of type // parameter we have. ideally the comparison would not involve actual string comparison and // limit available operations to hard-coded string literals. this.error( DiagnosticCode.Operation_not_supported, expression.range ); return module.createUnreachable(); } default: { assert(false); this.error( DiagnosticCode.Operation_not_supported, expression.range ); return module.createUnreachable(); } } if (possiblyOverflows && wrapSmallIntegers) { assert(currentType.is(TypeFlags.SHORT | TypeFlags.INTEGER)); expr = makeSmallIntegerWrap(expr, currentType, module); } return compound ? this.compileAssignmentWithValue(expression.operand, expr, contextualType != Type.void) : expr; } } // helpers function mangleExportName(element: Element, explicitSimpleName: string | null = null): string { var simpleName = explicitSimpleName != null ? explicitSimpleName : element.simpleName; switch (element.kind) { case ElementKind.FUNCTION: { let parent = (element).parent || (element).prototype.parent; return parent ? mangleExportName(parent) + (element.is(CommonFlags.INSTANCE) ? INSTANCE_DELIMITER : STATIC_DELIMITER) + simpleName : simpleName; } case ElementKind.FIELD: { let parent = assert((element).parent); return mangleExportName(parent) + (element.is(CommonFlags.INSTANCE) ? INSTANCE_DELIMITER : STATIC_DELIMITER) + simpleName; } case ElementKind.ENUMVALUE: { let parent = assert((element).parent); return mangleExportName(parent) + (element.is(CommonFlags.INSTANCE) ? INSTANCE_DELIMITER : STATIC_DELIMITER) + simpleName; } case ElementKind.CLASS: { let parent = (element).prototype.parent; return parent ? mangleExportName(parent) + STATIC_DELIMITER + simpleName : simpleName; } default: { let parent = element.parent; return parent ? mangleExportName(parent) + STATIC_DELIMITER + simpleName : simpleName; } } } /** Adds the debug location of the specified expression at the specified range to the source map. */ function addDebugLocation(expr: ExpressionRef, range: Range, module: Module, currentFunction: Function): void { var source = range.source; if (source.debugInfoIndex < 0) { source.debugInfoIndex = module.addDebugInfoFile(source.normalizedPath); } range.debugInfoRef = expr; if (!currentFunction.debugLocations) currentFunction.debugLocations = []; currentFunction.debugLocations.push(range); } /** Wraps a 32-bit integer expression so it evaluates to a valid value of the specified type. */ export function makeSmallIntegerWrap(expr: ExpressionRef, type: Type, module: Module): ExpressionRef { switch (type.kind) { case TypeKind.I8: { return module.createBinary(BinaryOp.ShrI32, module.createBinary(BinaryOp.ShlI32, expr, module.createI32(24) ), module.createI32(24) ); } case TypeKind.I16: { return module.createBinary(BinaryOp.ShrI32, module.createBinary(BinaryOp.ShlI32, expr, module.createI32(16) ), module.createI32(16) ); } case TypeKind.U8: { return module.createBinary(BinaryOp.AndI32, expr, module.createI32(0xff) ); } case TypeKind.U16: { return module.createBinary(BinaryOp.AndI32, expr, module.createI32(0xffff) ); } case TypeKind.BOOL: { return module.createBinary(BinaryOp.AndI32, expr, module.createI32(0x1) ); } default: { assert(false); return expr; } } } /** Creates a comparison whether an expression is not 'true' in a broader sense. */ export function makeIsFalseish(expr: ExpressionRef, type: Type, module: Module): ExpressionRef { switch (type.kind) { default: { // any native i32 return module.createUnary(UnaryOp.EqzI32, expr); } case TypeKind.I64: case TypeKind.U64: { return module.createUnary(UnaryOp.EqzI64, expr); } case TypeKind.USIZE: // TODO: strings? case TypeKind.ISIZE: { return module.createUnary(type.size == 64 ? UnaryOp.EqzI64 : UnaryOp.EqzI32, expr); } case TypeKind.F32: { return module.createBinary(BinaryOp.EqF32, expr, module.createF32(0)); } case TypeKind.F64: { return module.createBinary(BinaryOp.EqF64, expr, module.createF64(0)); } case TypeKind.VOID: { assert(false); return module.createI32(1); } } } /** Creates a comparison whether an expression is 'true' in a broader sense. */ export function makeIsTrueish(expr: ExpressionRef, type: Type, module: Module): ExpressionRef { switch (type.kind) { default: { // any native i32 return expr; } case TypeKind.I64: case TypeKind.U64: { return module.createBinary(BinaryOp.NeI64, expr, module.createI64(0)); } case TypeKind.USIZE: // TODO: strings? case TypeKind.ISIZE: { return type.size == 64 ? module.createBinary(BinaryOp.NeI64, expr, module.createI64(0)) : expr; } case TypeKind.F32: { return module.createBinary(BinaryOp.NeF32, expr, module.createF32(0)); } case TypeKind.F64: { return module.createBinary(BinaryOp.NeF64, expr, module.createF64(0)); } case TypeKind.VOID: { assert(false); return module.createI32(0); } } } /** Makes an allocation expression for an instance of the specified class. */ export function makeAllocate(compiler: Compiler, classInstance: Class, reportNode: Node): ExpressionRef { var module = compiler.module; var currentFunction = compiler.currentFunction; var nativeSizeType = compiler.options.nativeSizeType; var tempLocal = currentFunction.getTempLocal(classInstance.type); // allocate the necessary memory var initializers = new Array(); initializers.push( module.createSetLocal(tempLocal.index, compileBuiltinAllocate(compiler, classInstance, reportNode) ) ); // apply field initializers if (classInstance.members) { for (let member of classInstance.members.values()) { if (member.kind == ElementKind.FIELD) { let field = member; let fieldType = field.type; let nativeFieldType = fieldType.toNativeType(); let fieldDeclaration = field.prototype.declaration; assert(!field.isAny(CommonFlags.CONST)); if (fieldDeclaration.initializer) { // use initializer initializers.push(module.createStore(fieldType.byteSize, module.createGetLocal(tempLocal.index, nativeSizeType), compiler.compileExpression(fieldDeclaration.initializer, fieldType), // reports nativeFieldType, field.memoryOffset )); } else { // initialize with zero // TODO: might be unnecessary if the ctor initializes the field let parameterIndex = (field.prototype.declaration).parameterIndex; initializers.push(module.createStore(fieldType.byteSize, module.createGetLocal(tempLocal.index, nativeSizeType), parameterIndex >= 0 // initialized via parameter ? module.createGetLocal(1 + parameterIndex, nativeFieldType) : fieldType.toNativeZero(module), nativeFieldType, field.memoryOffset )); } } } } // return `this` initializers.push( module.createGetLocal(tempLocal.index, nativeSizeType) ); currentFunction.freeTempLocal(tempLocal); compiler.currentType = classInstance.type; return module.createBlock(null, initializers, nativeSizeType); } /** Makes a conditional allocation expression inside of the constructor of the specified class. */ export function makeConditionalAllocate(compiler: Compiler, classInstance: Class, reportNode: Node): ExpressionRef { // requires that `this` is the first local var module = compiler.module; var nativeSizeType = compiler.options.nativeSizeType; compiler.currentType = classInstance.type; return module.createIf( nativeSizeType == NativeType.I64 ? module.createBinary( BinaryOp.NeI64, module.createGetLocal(0, NativeType.I64), module.createI64(0) ) : module.createGetLocal(0, NativeType.I32), module.createGetLocal(0, nativeSizeType), module.createTeeLocal(0, makeAllocate(compiler, classInstance, reportNode) ) ); } export function isI32Const(expr: ExpressionRef): bool { return _BinaryenExpressionGetId(expr) == ExpressionId.Const && _BinaryenExpressionGetType(expr) == NativeType.I32; }