/** * The AssemblyScript compiler. * @module compiler *//***/ import { BuiltinSymbols, compileCall as compileBuiltinCall, compileAbort, compileVisitGlobals, compileVisitMembers, compileRTTI, } from "./builtins"; import { DiagnosticCode, DiagnosticEmitter } from "./diagnostics"; import { Module, MemorySegment, ExpressionRef, UnaryOp, BinaryOp, NativeType, FunctionRef, ExpressionId, FunctionTypeRef, GlobalRef, getExpressionId, getExpressionType, getConstValueI32, getConstValueI64Low, getConstValueI64High, getConstValueF32, getConstValueF64, getBlockChildCount, getBlockChild, getBlockName, needsExplicitUnreachable, getGetLocalIndex, FeatureFlags, isTeeLocal, getSetLocalIndex } from "./module"; import { CommonFlags, INSTANCE_DELIMITER, STATIC_DELIMITER, GETTER_PREFIX, SETTER_PREFIX, CommonSymbols, INDEX_SUFFIX, Feature, Target } from "./common"; import { Program, ClassPrototype, Class, Element, ElementKind, Enum, Field, FunctionPrototype, Function, FunctionTarget, Global, Local, EnumValue, Property, VariableLikeElement, ConstantValueKind, OperatorKind, DecoratorFlags, PropertyPrototype, File, mangleInternalName } from "./program"; import { FlowFlags, Flow, LocalFlags } from "./flow"; import { Resolver, ReportMode } from "./resolver"; import { Token, operatorTokenToString } from "./tokenizer"; import { Node, NodeKind, TypeNode, Range, DecoratorKind, AssertionKind, Statement, BlockStatement, BreakStatement, ClassDeclaration, ContinueStatement, DeclarationStatement, DoStatement, EmptyStatement, EnumDeclaration, ExportStatement, ExpressionStatement, FieldDeclaration, ForStatement, FunctionDeclaration, IfStatement, ImportStatement, InstanceOfExpression, InterfaceDeclaration, NamespaceDeclaration, ReturnStatement, SwitchStatement, ThrowStatement, TryStatement, VariableStatement, VoidStatement, WhileStatement, Expression, AssertionExpression, BinaryExpression, CallExpression, CommaExpression, ElementAccessExpression, FloatLiteralExpression, FunctionExpression, IdentifierExpression, IntegerLiteralExpression, LiteralExpression, LiteralKind, NewExpression, ObjectLiteralExpression, ParenthesizedExpression, PropertyAccessExpression, TernaryExpression, ArrayLiteralExpression, StringLiteralExpression, UnaryPostfixExpression, UnaryPrefixExpression, nodeIsConstantValue, findDecorator, isTypeOmitted } from "./ast"; import { Type, TypeKind, TypeFlags, Signature, typesToNativeTypes } from "./types"; import { writeI8, writeI16, writeI32, writeI64, writeF32, writeF64, makeMap } from "./util"; /** Compiler options. */ export class Options { /** WebAssembly target. Defaults to {@link Target.WASM32}. */ target: Target = Target.WASM32; /** If true, replaces assertions with nops. */ noAssert: bool = false; /** If true, imports the memory provided by the embedder. */ importMemory: bool = false; /** If greater than zero, declare memory as shared by setting max memory to sharedMemory. */ sharedMemory: i32 = 0; /** If true, imports the function table provided by the embedder. */ importTable: bool = false; /** If true, generates information necessary for source maps. */ sourceMap: bool = false; /** Static memory start offset. */ memoryBase: i32 = 0; /** Global aliases. */ globalAliases: Map | null = null; /** Additional features to activate. */ features: Feature = Feature.NONE; /** Hinted optimize level. Not applied by the compiler itself. */ optimizeLevelHint: i32 = 0; /** Hinted shrink level. Not applied by the compiler itself. */ shrinkLevelHint: i32 = 0; /** 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; } /** Tests if a specific feature is activated. */ hasFeature(feature: Feature): bool { return (this.features & feature) != 0; } } /** Requests or indicates compilation conditions of statements and expressions. */ export const enum ContextualFlags { NONE = 0, /** Implicit conversion required. */ IMPLICIT = 1 << 0, /** Explicit conversion required. */ EXPLICIT = 1 << 1, /** Small integer wrap required. */ WRAP = 1 << 2, /** Value is known to be immediately dropped. */ WILL_DROP = 1 << 3, /** Value is known to be immediately assigned to a retaining target. */ SKIP_AUTORELEASE = 1 << 4, /** Is the last statement in a function body. */ LAST_IN_BODY = 1 << 5, /** Data can be compiled statically. */ STATIC_CAPABLE = 1 << 6 } /** Runtime features to be activated by the compiler. */ export const enum RuntimeFeatures { NONE = 0, /** Requires HEAP_BASE and heap setup. */ HEAP = 1 << 0, /** Requires RTTI_BASE and RTTI setup. */ RTTI = 1 << 1, /** Requires the built-in globals visitor. */ visitGlobals = 1 << 2, /** Requires the built-in members visitor. */ visitMembers = 1 << 3 } /** Compiler interface. */ export class Compiler extends DiagnosticEmitter { /** Program reference. */ program: Program; /** Resolver reference. */ resolver: Resolver; /** Provided options. */ options: Options; /** Module instance being compiled. */ module: Module; /** Current control flow. */ currentFlow: Flow; /** Current inline functions stack. */ currentInlineFunctions: Function[] = []; /** Current enum in compilation. */ currentEnum: Enum | null = null; /** Current type in compilation. */ currentType: Type = Type.void; /** Start function statements. */ currentBody: ExpressionRef[]; /** Counting memory offset. */ memoryOffset: I64; /** Memory segments being compiled. */ memorySegments: MemorySegment[] = []; /** Map of already compiled static string segments. */ stringSegments: Map = new Map(); /** Function table being compiled. */ functionTable: string[] = [ "null" ]; /** Argument count helper global. */ argcVar: GlobalRef = 0; /** Argument count helper setter. */ argcSet: FunctionRef = 0; /** Requires runtime features. */ runtimeFeatures: RuntimeFeatures = RuntimeFeatures.NONE; /** Expressions known to have skipped an autorelease. Usually function returns. */ skippedAutoreleases: Set = new Set(); /** 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; this.resolver = program.resolver; if (!options) options = new Options(); this.options = options; this.memoryOffset = i64_new( // leave space for `null`. also functions as a sentinel for erroneous stores at offset 0. // note that Binaryen's asm.js output utilizes the first 8 bytes for reinterpretations (#1547) max(options.memoryBase, 8) ); this.module = Module.create(); var featureFlags: BinaryenFeatureFlags = 0; if (this.options.hasFeature(Feature.THREADS)) featureFlags |= FeatureFlags.Atomics; if (this.options.hasFeature(Feature.MUTABLE_GLOBAL)) featureFlags |= FeatureFlags.MutableGloabls; // if (this.options.hasFeature(Feature.TRUNC_SAT)) featureFlags |= FeatureFlags.NontrappingFPToInt; if (this.options.hasFeature(Feature.SIMD)) featureFlags |= FeatureFlags.SIMD128; if (this.options.hasFeature(Feature.BULK_MEMORY)) featureFlags |= FeatureFlags.BulkMemory; if (this.options.hasFeature(Feature.SIGN_EXTENSION)) featureFlags |= FeatureFlags.SignExt; // if (this.options.hasFeature(Feature.EXCEPTION_HANDLING)) featureFlags |= FeatureFlags.ExceptionHandling; this.module.setFeatures(featureFlags); } /** 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 main start function var startFunctionInstance = program.makeNativeFunction("start", new Signature([], Type.void)); startFunctionInstance.internalName = "start"; var startFunctionBody = new Array(); this.currentFlow = startFunctionInstance.flow; this.currentBody = startFunctionBody; // add a mutable heap and rtti base dummies if (options.isWasm64) { module.addGlobal(BuiltinSymbols.HEAP_BASE, NativeType.I64, true, module.createI64(0)); module.addGlobal(BuiltinSymbols.RTTI_BASE, NativeType.I64, true, module.createI64(0)); } else { module.addGlobal(BuiltinSymbols.HEAP_BASE, NativeType.I32, true, module.createI32(0)); module.addGlobal(BuiltinSymbols.RTTI_BASE, NativeType.I32, true, module.createI32(0)); } // compile entry file(s) while traversing reachable elements var files = program.filesByName; for (let file of files.values()) { if (file.source.isEntry) { this.compileFile(file); this.compileExports(file); } } // compile the start function if not empty or called by main var hasExplicitStart = program.explicitStartFunction !== null; if (startFunctionBody.length || hasExplicitStart) { 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); if (!hasExplicitStart) module.setStart(funcRef); } // compile runtime features if (this.runtimeFeatures & RuntimeFeatures.visitGlobals) compileVisitGlobals(this); if (this.runtimeFeatures & RuntimeFeatures.visitMembers) compileVisitMembers(this); module.removeGlobal(BuiltinSymbols.RTTI_BASE); if (this.runtimeFeatures & RuntimeFeatures.RTTI) compileRTTI(this); // update the heap base pointer var memoryOffset = this.memoryOffset; memoryOffset = i64_align(memoryOffset, options.usizeType.byteSize); this.memoryOffset = memoryOffset; module.removeGlobal(BuiltinSymbols.HEAP_BASE); if (this.runtimeFeatures & RuntimeFeatures.HEAP) { if (options.isWasm64) { module.addGlobal( BuiltinSymbols.HEAP_BASE, NativeType.I64, false, module.createI64(i64_low(memoryOffset), i64_high(memoryOffset)) ); } else { module.addGlobal( BuiltinSymbols.HEAP_BASE, NativeType.I32, false, module.createI32(i64_low(memoryOffset)) ); } } // set up memory var isSharedMemory = options.hasFeature(Feature.THREADS) && options.sharedMemory > 0; module.setMemory( this.options.memoryBase /* is specified */ || this.memorySegments.length ? i64_low(i64_shr_u(i64_align(memoryOffset, 0x10000), i64_new(16, 0))) : 0, isSharedMemory ? options.sharedMemory : Module.UNLIMITED_MEMORY, this.memorySegments, options.target, "memory", isSharedMemory ); // import memory if requested (default memory is named '0' by Binaryen) if (options.importMemory) module.addMemoryImport("0", "env", "memory", isSharedMemory); // set up function table var functionTable = this.functionTable; module.setFunctionTable(functionTable.length, 0xffffffff, functionTable); module.addFunction("null", this.ensureFunctionType(null, Type.void), null, module.createBlock(null, [])); // import table if requested (default table is named '0' by Binaryen) if (options.importTable) module.addTableImport("0", "env", "table"); // set up module exports for (let file of this.program.filesByName.values()) { if (file.source.isEntry) this.ensureModuleExports(file); } return module; } /** Applies the respective module exports for the specified file. */ private ensureModuleExports(file: File): void { var members = file.exports; if (members) for (let [name, member] of members) this.ensureModuleExport(name, member); var exportsStar = file.exportsStar; if (exportsStar) { for (let i = 0, k = exportsStar.length; i < k; ++i) this.ensureModuleExports(exportsStar[i]); } } /** Applies the respective module export(s) for the specified element. */ private ensureModuleExport(name: string, element: Element, prefix: string = ""): void { switch (element.kind) { // traverse instances case ElementKind.FUNCTION_PROTOTYPE: { let instances = (element).instances; if (instances) { for (let instance of instances.values()) { let instanceName = name; if (instance.is(CommonFlags.GENERIC)) { let fullName = instance.internalName; instanceName += fullName.substring(fullName.lastIndexOf("<")); } this.ensureModuleExport(instanceName, instance, prefix); } } break; } case ElementKind.CLASS_PROTOTYPE: { let instances = (element).instances; if (instances) { for (let instance of instances.values()) { let instanceName = name; if (instance.is(CommonFlags.GENERIC)) { let fullName = instance.internalName; instanceName += fullName.substring(fullName.lastIndexOf("<")); } this.ensureModuleExport(instanceName, instance, prefix); } } break; } case ElementKind.PROPERTY_PROTOTYPE: { let getter = (element).getterPrototype; let setter = (element).setterPrototype; if (getter) this.ensureModuleExport(GETTER_PREFIX + name, getter, prefix); if (setter) this.ensureModuleExport(SETTER_PREFIX + name, setter, prefix); break; } // export concrete elements case ElementKind.GLOBAL: { let isConst = element.is(CommonFlags.CONST) || element.is(CommonFlags.STATIC | CommonFlags.READONLY); if (!isConst && !this.options.hasFeature(Feature.MUTABLE_GLOBAL)) { this.error( DiagnosticCode.Cannot_export_a_mutable_global, (element).identifierNode.range ); } else { this.module.addGlobalExport(element.internalName, prefix + name); } break; } case ElementKind.ENUMVALUE: { if (!(element).isImmutable && !this.options.hasFeature(Feature.MUTABLE_GLOBAL)) { this.error( DiagnosticCode.Cannot_export_a_mutable_global, (element).identifierNode.range ); } else { this.module.addGlobalExport(element.internalName, prefix + name); } break; } case ElementKind.FUNCTION: { let instance = element; let signature = instance.signature; if (signature.requiredParameters < signature.parameterTypes.length) { // utilize trampoline to fill in omitted arguments instance = this.ensureTrampoline(instance); this.ensureArgcSet(); } if (instance.is(CommonFlags.COMPILED)) this.module.addFunctionExport(instance.internalName, prefix + name); break; } case ElementKind.PROPERTY: { let getter = (element).getterInstance; if (getter) this.ensureModuleExport(GETTER_PREFIX + name, getter, prefix); let setter = (element).setterInstance; if (setter) this.ensureModuleExport(SETTER_PREFIX + name, setter, prefix); break; } case ElementKind.FIELD: { this.ensureModuleFieldGetter(prefix + GETTER_PREFIX + name, element); if (!element.is(CommonFlags.READONLY)) { this.ensureModuleFieldSetter(prefix + SETTER_PREFIX + name, element); } break; } case ElementKind.CLASS: { // make the class name itself represent its runtime id if (!(element).type.isUnmanaged) { let module = this.module; let internalName = (element).internalName; module.addGlobal(internalName, NativeType.I32, false, module.createI32((element).id)); module.addGlobalExport(internalName, prefix + name); } break; } // just traverse members below case ElementKind.ENUM: case ElementKind.NAMESPACE: case ElementKind.FILE: case ElementKind.TYPEDEFINITION: break; default: assert(false); // unexpected module export } // traverse members var members = element.members; if (members) { let subPrefix = prefix + name + (element.kind == ElementKind.CLASS ? INSTANCE_DELIMITER : STATIC_DELIMITER ); if ( element.kind == ElementKind.NAMESPACE || element.kind == ElementKind.FILE ) { for (let member of members.values()) { if (!member.is(CommonFlags.EXPORT)) continue; this.ensureModuleExport(member.name, member, subPrefix); } } else { for (let member of members.values()) { if (member.is(CommonFlags.PRIVATE)) continue; this.ensureModuleExport(member.name, member, subPrefix); } } } } /** Makes a function to get the value of a field of an exported class. */ private ensureModuleFieldGetter(name: string, field: Field): void { var module = this.module; var type = field.type; var usizeType = this.options.usizeType; var loadExpr = module.createLoad(type.byteSize, type.is(TypeFlags.SIGNED), module.createGetLocal(0, usizeType.toNativeType()), type.toNativeType(), field.memoryOffset ); // a user-defined getter would return a retained value, so: if (type.isManaged) loadExpr = this.makeRetain(loadExpr); module.addFunction( name, this.ensureFunctionType(null, type, usizeType), null, loadExpr ); module.addFunctionExport(name, name); } /** Makes a function to set the value of a field of an exported class. */ private ensureModuleFieldSetter(name: string, field: Field): void { var module = this.module; var type = field.type; var nativeType = type.toNativeType(); var usizeType = this.options.usizeType; var nativeSizeType = usizeType.toNativeType(); var valueExpr = module.createGetLocal(1, nativeType); if (type.isManaged) { valueExpr = this.makeRetainRelease( module.createLoad(type.byteSize, false, // oldRef module.createGetLocal(0, nativeSizeType), nativeType, field.memoryOffset ), valueExpr // newRef ); } module.addFunction( name, this.ensureFunctionType([ type ], Type.void, usizeType), null, module.createStore( type.byteSize, module.createGetLocal(0, nativeSizeType), valueExpr, nativeType, field.memoryOffset ) ); module.addFunctionExport(name, name); } // === Elements ================================================================================= /** Compiles any element. */ compileElement(element: Element, compileMembers: bool = true): void { switch (element.kind) { case ElementKind.GLOBAL: { this.compileGlobal(element); break; } case ElementKind.ENUM: { this.compileEnum(element); break; } case ElementKind.FUNCTION_PROTOTYPE: { if (!element.is(CommonFlags.GENERIC)) { this.compileFunctionUsingTypeArguments(element, []); } break; } case ElementKind.CLASS_PROTOTYPE: { if (!element.is(CommonFlags.GENERIC)) { this.compileClassUsingTypeArguments(element, []); } break; } case ElementKind.PROPERTY_PROTOTYPE: { let getterPrototype = (element).getterPrototype; if (getterPrototype) { this.compileFunctionUsingTypeArguments(getterPrototype, []); } let setterPrototype = (element).setterPrototype; if (setterPrototype) { this.compileFunctionUsingTypeArguments(setterPrototype, []); } break; } case ElementKind.NAMESPACE: case ElementKind.TYPEDEFINITION: case ElementKind.ENUMVALUE: break; default: assert(false, ElementKind[element.kind]); } if (compileMembers) this.compileMembers(element); } /** Compiles an element's members. */ compileMembers(element: Element): void { var members = element.members; if (members) for (let element of members.values()) this.compileElement(element); } /** Compiles a file's exports. */ compileExports(file: File): void { var exports = file.exports; if (exports) for (let element of exports.values()) this.compileElement(element); var exportsStar = file.exportsStar; if (exportsStar) { for (let exportStar of exportsStar) { this.compileFile(exportStar); this.compileExports(exportStar); } } } // files /** Compiles the file matching the specified path. */ compileFileByPath(normalizedPathWithoutExtension: string, reportNode: Node): void { var file: File; var filesByName = this.program.filesByName; var pathWithIndex: string; if (filesByName.has(normalizedPathWithoutExtension)) { file = filesByName.get(normalizedPathWithoutExtension)!; } else if (filesByName.has(pathWithIndex = normalizedPathWithoutExtension + INDEX_SUFFIX)) { file = filesByName.get(pathWithIndex)!; } else { this.error( DiagnosticCode.File_0_not_found, reportNode.range, normalizedPathWithoutExtension ); return; } this.compileFile(file); } /** Compiles the specified file. */ compileFile(file: File): void { if (file.is(CommonFlags.COMPILED)) return; file.set(CommonFlags.COMPILED); // compile top-level statements within the file's start function var startFunction = file.startFunction; var previousBody = this.currentBody; var startFunctionBody = new Array(); this.currentBody = startFunctionBody; // compile top-level statements var previousFlow = this.currentFlow; var flow = startFunction.flow; this.currentFlow = flow; for (let statements = file.source.statements, i = 0, k = statements.length; i < k; ++i) { this.compileTopLevelStatement(statements[i], startFunctionBody); } this.finishAutoreleases(flow, startFunctionBody); this.currentFlow = previousFlow; this.currentBody = previousBody; // if top-level statements are present, make the per-file start function and call it in start if (startFunctionBody.length) { let module = this.module; let locals = startFunction.localsByIndex; let numLocals = locals.length; let varTypes = new Array(numLocals); for (let i = 0; i < numLocals; ++i) varTypes[i] = locals[i].type.toNativeType(); module.addFunction( startFunction.internalName, this.ensureFunctionType(startFunction.signature.parameterTypes, startFunction.signature.returnType), varTypes, startFunctionBody.length > 1 ? module.createBlock(null, startFunctionBody) : startFunctionBody[0] ); previousBody.push( module.createCall(startFunction.internalName, null, NativeType.None) ); } } // === Globals ================================================================================== compileGlobal(global: Global): bool { if (global.is(CommonFlags.COMPILED)) return true; global.set(CommonFlags.COMPILED); var module = this.module; var initExpr: ExpressionRef = 0; var initAutoreleaseSkipped: bool = false; var typeNode = global.typeNode; var initializerNode = global.initializerNode; if (!global.is(CommonFlags.RESOLVED)) { // resolve now if annotated if (typeNode) { let resolvedType = this.resolver.resolveType(typeNode, global.parent); // reports if (!resolvedType) return false; if (resolvedType == Type.void) { this.error( DiagnosticCode.Type_expected, typeNode.range ); return false; } global.setType(resolvedType); // infer from initializer if not annotated } else if (initializerNode) { // infer type using void/NONE for literal inference let previousFlow = this.currentFlow; if (global.hasDecorator(DecoratorFlags.LAZY)) { this.currentFlow = global.file.startFunction.flow; } initExpr = this.compileExpression(initializerNode, Type.i32, // reports ContextualFlags.WRAP | ContextualFlags.SKIP_AUTORELEASE ); if (this.skippedAutoreleases.has(initExpr)) initAutoreleaseSkipped = true; this.currentFlow = previousFlow; if (this.currentType == Type.void) { this.error( DiagnosticCode.Type_0_is_not_assignable_to_type_1, initializerNode.range, this.currentType.toString(), "" ); return false; } global.setType(this.currentType); // must either be annotated or have an initializer } else { this.error( DiagnosticCode.Type_expected, global.identifierNode.range.atEnd ); return false; } } // ambient builtins like 'HEAP_BASE' need to be resolved but are added explicitly if (global.is(CommonFlags.AMBIENT) && global.hasDecorator(DecoratorFlags.BUILTIN)) { if (global.internalName == BuiltinSymbols.HEAP_BASE) this.runtimeFeatures |= RuntimeFeatures.HEAP; else if (global.internalName == BuiltinSymbols.RTTI_BASE) this.runtimeFeatures |= RuntimeFeatures.RTTI; return true; } var type = global.type; var nativeType = type.toNativeType(); var isDeclaredConstant = global.is(CommonFlags.CONST) || global.is(CommonFlags.STATIC | CommonFlags.READONLY); // handle imports if (global.is(CommonFlags.AMBIENT)) { // constant global if (isDeclaredConstant || this.options.hasFeature(Feature.MUTABLE_GLOBAL)) { global.set(CommonFlags.MODULE_IMPORT); mangleImportName(global, global.declaration); module.addGlobalImport( global.internalName, mangleImportName_moduleName, mangleImportName_elementName, nativeType ); global.set(CommonFlags.COMPILED); return true; // importing mutable globals is not supported in the MVP } else { this.error( DiagnosticCode.Operation_not_supported, global.declaration.range ); } return false; } // the MVP does not yet support initializer expressions other than constant values (and constant // get_globals), hence such initializations must be performed in the start function for now. var initializeInStart = false; // evaluate initializer if present if (initializerNode) { if (!initExpr) { let previousFlow = this.currentFlow; if (global.hasDecorator(DecoratorFlags.LAZY)) { this.currentFlow = global.file.startFunction.flow; } initExpr = this.compileExpression(initializerNode, type, ContextualFlags.IMPLICIT | ContextualFlags.WRAP | ContextualFlags.SKIP_AUTORELEASE | ContextualFlags.STATIC_CAPABLE ); if (this.skippedAutoreleases.has(initExpr)) initAutoreleaseSkipped = true; this.currentFlow = previousFlow; } if (getExpressionId(initExpr) != ExpressionId.Const) { if (isDeclaredConstant) { initExpr = module.precomputeExpression(initExpr); if (getExpressionId(initExpr) != ExpressionId.Const) { this.warning( DiagnosticCode.Compiling_constant_with_non_constant_initializer_as_mutable, initializerNode.range ); initializeInStart = true; } } else { initializeInStart = true; } } // explicitly inline if annotated if (global.hasDecorator(DecoratorFlags.INLINE)) { if (!initializeInStart) { // reported above assert(getExpressionId(initExpr) == ExpressionId.Const); let exprType = getExpressionType(initExpr); switch (exprType) { case NativeType.I32: { global.constantValueKind = ConstantValueKind.INTEGER; global.constantIntegerValue = i64_new(getConstValueI32(initExpr), 0); break; } case NativeType.I64: { global.constantValueKind = ConstantValueKind.INTEGER; global.constantIntegerValue = i64_new( getConstValueI64Low(initExpr), getConstValueI64High(initExpr) ); break; } case NativeType.F32: { global.constantValueKind = ConstantValueKind.FLOAT; global.constantFloatValue = getConstValueF32(initExpr); break; } case NativeType.F64: { global.constantValueKind = ConstantValueKind.FLOAT; global.constantFloatValue = getConstValueF64(initExpr); break; } default: { assert(false); return false; } } global.set(CommonFlags.INLINED); // inline the value from now on } } // initialize to zero if there's no initializer } else { initExpr = type.toNativeZero(module); } var internalName = global.internalName; if (initializeInStart) { // initialize to mutable zero and set the actual value in start if (global.hasDecorator(DecoratorFlags.INLINE)) { this.error( DiagnosticCode.Decorator_0_is_not_valid_here, assert(findDecorator(DecoratorKind.INLINE, global.decoratorNodes)).range, "inline" ); } module.addGlobal(internalName, nativeType, true, type.toNativeZero(module)); if (type.isManaged && !initAutoreleaseSkipped) initExpr = this.makeRetain(initExpr); this.currentBody.push( module.createSetGlobal(internalName, initExpr) ); } else if (!global.hasDecorator(DecoratorFlags.INLINE)) { // compile normally module.addGlobal(internalName, nativeType, !isDeclaredConstant, initExpr); } return true; } // === Enums ==================================================================================== 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; var previousValueIsMut = false; var isInline = element.is(CommonFlags.CONST) || element.hasDecorator(DecoratorFlags.INLINE); 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 valueNode = val.valueNode; val.set(CommonFlags.COMPILED); let previousFlow = this.currentFlow; if (element.hasDecorator(DecoratorFlags.LAZY)) { this.currentFlow = element.file.startFunction.flow; } let initExpr: ExpressionRef; if (valueNode) { initExpr = this.compileExpression(valueNode, Type.i32, ContextualFlags.IMPLICIT // autorelease is not applicable in i32 context ); if (getExpressionId(initExpr) != ExpressionId.Const) { initExpr = module.precomputeExpression(initExpr); if (getExpressionId(initExpr) != ExpressionId.Const) { if (element.is(CommonFlags.CONST)) { this.error( DiagnosticCode.In_const_enum_declarations_member_initializer_must_be_constant_expression, valueNode.range ); } initInStart = true; } } } else if (previousValue == null) { initExpr = module.createI32(0); } else { if (previousValueIsMut) { this.error( DiagnosticCode.Enum_member_must_have_initializer, (member).identifierNode.range.atEnd ); } initExpr = module.createBinary(BinaryOp.AddI32, module.createGetGlobal(previousValue.internalName, NativeType.I32), module.createI32(1) ); initExpr = module.precomputeExpression(initExpr); if (getExpressionId(initExpr) != ExpressionId.Const) { if (element.is(CommonFlags.CONST)) { this.error( DiagnosticCode.In_const_enum_declarations_member_initializer_must_be_constant_expression, member.declaration.range ); } initInStart = true; } } this.currentFlow = previousFlow; if (initInStart) { module.addGlobal(val.internalName, NativeType.I32, true, module.createI32(0)); this.currentBody.push( this.makeGlobalAssignment(val, initExpr, false) ); previousValueIsMut = true; } else { if (isInline) { val.setConstantIntegerValue(i64_new(getConstValueI32(initExpr)), Type.i32); if (val.is(CommonFlags.MODULE_EXPORT)) { module.addGlobal(val.internalName, NativeType.I32, false, initExpr); } } else { module.addGlobal(val.internalName, NativeType.I32, false, initExpr); } val.isImmutable = true; previousValueIsMut = false; } previousValue = val; } } this.currentEnum = null; return true; } // === Functions ================================================================================ /** Resolves the specified type arguments prior to compiling the resulting function instance. */ compileFunctionUsingTypeArguments( prototype: FunctionPrototype, typeArguments: TypeNode[], contextualTypeArguments: Map = makeMap(), alternativeReportNode: Node | null = null ): Function | null { if (prototype.hasDecorator(DecoratorFlags.BUILTIN)) return null; var instance = this.resolver.resolveFunctionInclTypeArguments( prototype, typeArguments, prototype.parent, // relative to itself contextualTypeArguments, alternativeReportNode || prototype.declaration ); if (!instance) return null; if (!this.compileFunction(instance)) return null; // reports return instance; } /** Either reuses or creates the function type matching the specified signature. */ 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 the body of a function within the specified flow. */ compileFunctionBody( /** Function to compile. */ instance: Function, /** Target array of statements. */ stmts: ExpressionRef[] | null = null ): ExpressionRef[] { var module = this.module; var bodyNode = assert(instance.prototype.bodyNode); var returnType = instance.signature.returnType; var flow = this.currentFlow; // compile statements if (bodyNode.kind == NodeKind.BLOCK) { stmts = this.compileStatements((bodyNode).statements, true, stmts); } else { // must be an expression statement if not a block assert(bodyNode.kind == NodeKind.EXPRESSION); // must be an arrow function assert(instance.prototype.arrowKind); // none of the following can be an arrow function assert(!instance.isAny(CommonFlags.CONSTRUCTOR | CommonFlags.GET | CommonFlags.SET | CommonFlags.MAIN)); let expr = this.compileExpression((bodyNode).expression, returnType, ContextualFlags.IMPLICIT ); if (!stmts) stmts = [ expr ]; else stmts.push(expr); if (!flow.isAny(FlowFlags.ANY_TERMINATING)) { // TODO: detect if returning an AUTORELEASE local let indexBefore = stmts.length; this.performAutoreleases(flow, stmts); this.finishAutoreleases(flow, stmts); let canOverflow = flow.canOverflow(expr, returnType); let nonNull = flow.isNonnull(returnType, expr); if (stmts.length > indexBefore) { let temp = flow.getAndFreeTempLocal(returnType, !canOverflow, nonNull); stmts[indexBefore - 1] = module.createSetLocal(temp.index, expr); stmts.push(module.createGetLocal(temp.index, returnType.toNativeType())); } if (!canOverflow) flow.set(FlowFlags.RETURNS_WRAPPED); if (nonNull) flow.set(FlowFlags.RETURNS_NONNULL); flow.set(FlowFlags.RETURNS); // now is terminating } } // make the main function call `start` implicitly, but only once if (instance.prototype == this.program.explicitStartFunction) { module.addGlobal(BuiltinSymbols.started, NativeType.I32, true, module.createI32(0)); stmts.unshift( module.createIf( module.createUnary( UnaryOp.EqzI32, module.createGetGlobal(BuiltinSymbols.started, NativeType.I32) ), module.createBlock(null, [ module.createCall("start", null, NativeType.None), module.createSetGlobal(BuiltinSymbols.started, module.createI32(1)) ]) ) ); } // make constructors return their instance pointer if (instance.is(CommonFlags.CONSTRUCTOR)) { let nativeSizeType = this.options.nativeSizeType; assert(instance.is(CommonFlags.INSTANCE)); let classInstance = assert(instance.parent); assert(classInstance.kind == ElementKind.CLASS); if (!flow.isAny(FlowFlags.ANY_TERMINATING)) { let thisLocalIndex = flow.is(FlowFlags.INLINE_CONTEXT) ? assert(flow.lookupLocal(CommonSymbols.this_)).index : 0; // if `this` wasn't accessed before, allocate if necessary and initialize `this` if (!flow.is(FlowFlags.ALLOCATES)) { // { // if (!this) this = // this.a = X // this.b = Y // } stmts.push( module.createIf( module.createUnary(nativeSizeType == NativeType.I64 ? UnaryOp.EqzI64 : UnaryOp.EqzI32, module.createGetLocal(thisLocalIndex, nativeSizeType) ), module.createSetLocal(thisLocalIndex, this.makeRetain( this.makeAllocation(classInstance) ), ) ) ); this.makeFieldInitialization(classInstance, stmts); } // implicitly return `this`. unlike for normal functions, retaining the value isn't // necessary because the allocation (constructor call) already did (RC=1) stmts.push( module.createGetLocal(thisLocalIndex, nativeSizeType) ); flow.set(FlowFlags.RETURNS); } // check that super has been called if this is a derived class if ((classInstance).base && !flow.is(FlowFlags.CALLS_SUPER)) { this.error( DiagnosticCode.Constructors_for_derived_classes_must_contain_a_super_call, instance.prototype.declaration.range ); } // if this is a normal function, make sure that 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, instance.prototype.signatureNode.returnType.range ); } return stmts; } /** Compiles a readily resolved function instance. */ compileFunction(instance: Function): bool { if (instance.is(CommonFlags.COMPILED)) return true; assert(!(instance.is(CommonFlags.AMBIENT) && instance.hasDecorator(DecoratorFlags.BUILTIN))); var previousType = this.currentType; // remember to retain it if compiling a function lazily instance.set(CommonFlags.COMPILED); var module = this.module; var signature = instance.signature; var bodyNode = instance.prototype.bodyNode; var typeRef = this.ensureFunctionType(signature.parameterTypes, signature.returnType, signature.thisType); var funcRef: FunctionRef; // concrete function if (bodyNode) { // must not be ambient if (instance.is(CommonFlags.AMBIENT)) { this.error( DiagnosticCode.An_implementation_cannot_be_declared_in_ambient_contexts, instance.identifierNode.range ); } // cannot have an annotated external name if (instance.hasDecorator(DecoratorFlags.EXTERNAL)) { let decoratorNodes = instance.decoratorNodes; let decorator = assert(findDecorator(DecoratorKind.EXTERNAL, decoratorNodes)); this.error( DiagnosticCode.Operation_not_supported, decorator.range ); } // compile body in this function's context let previousFlow = this.currentFlow; let flow = instance.flow; this.currentFlow = flow; let stmts = new Array(); // retain each argument for as long as the function lives let index = 0; let thisType = signature.thisType; if (thisType) { // No need to retain `this` as it can't be reassigned and thus can't become prematurely released ++index; } let parameterTypes = signature.parameterTypes; for (let i = 0, k = parameterTypes.length; i < k; ++i, ++index) { let type = parameterTypes[i]; if (type.isManaged) { stmts.push( module.createDrop( this.makeRetain( module.createGetLocal(index, type.toNativeType()) ) ) ); flow.setLocalFlag(index, LocalFlags.RETAINED); } } this.compileFunctionBody(instance, stmts); if (!flow.isAny(FlowFlags.ANY_TERMINATING)) { this.performAutoreleases(flow, stmts); this.finishAutoreleases(flow, stmts); } this.currentFlow = previousFlow; // create the function funcRef = module.addFunction( instance.internalName, typeRef, typesToNativeTypes(instance.additionalLocals), flatten(module, stmts, instance.signature.returnType.toNativeType()) ); // imported function } else { if (!instance.is(CommonFlags.AMBIENT)) { this.error( DiagnosticCode.Function_implementation_is_missing_or_not_immediately_following_the_declaration, instance.identifierNode.range ); } instance.set(CommonFlags.MODULE_IMPORT); mangleImportName(instance, instance.declaration); // TODO: check for duplicates // create the import funcRef = module.addFunctionImport( instance.internalName, mangleImportName_moduleName, mangleImportName_elementName, typeRef ); } instance.finalize(module, funcRef); this.currentType = previousType; return true; } // === Classes ================================================================================== compileClassUsingTypeArguments( prototype: ClassPrototype, typeArguments: TypeNode[], contextualTypeArguments: Map = makeMap(), alternativeReportNode: Node | null = null ): void { var instance = this.resolver.resolveClassInclTypeArguments( prototype, typeArguments, prototype.parent, // relative to itself contextualTypeArguments, alternativeReportNode || prototype.declaration ); if (!instance) return; this.compileClass(instance); } compileClass(instance: Class): bool { if (instance.is(CommonFlags.COMPILED)) return true; instance.set(CommonFlags.COMPILED); var prototype = instance.prototype; var staticMembers = (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, [], makeMap(), (element).identifierNode ); } break; } case ElementKind.PROPERTY_PROTOTYPE: { let getter = (element).getterPrototype; if (getter) { this.compileFunctionUsingTypeArguments( getter, [], makeMap(), getter.identifierNode ); } let setter = (element).setterPrototype; if (setter) { this.compileFunctionUsingTypeArguments( setter, [], makeMap(), setter.identifierNode ); } break; } case ElementKind.PROPERTY: { } } } } 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, [], makeMap(instance.contextualTypeArguments), (element).declaration.name ); } break; } case ElementKind.FIELD_PROTOTYPE: { element.set(CommonFlags.COMPILED); break; } case ElementKind.PROPERTY: { let getter = (element).getterInstance; if (getter) this.compileFunction(getter); let setter = (element).setterInstance; if (setter) this.compileFunction(setter); 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; } /** Ensures that the specified string exists in static memory and returns a pointer to it. */ ensureStaticString(stringValue: string): ExpressionRef { var program = this.program; var rtHeaderSize = program.runtimeHeaderSize; var stringInstance = assert(program.stringInstance); var stringSegment: MemorySegment; var segments = this.stringSegments; if (segments.has(stringValue)) { stringSegment = segments.get(stringValue)!; // reuse } else { let length = stringValue.length; let buffer = new Uint8Array(rtHeaderSize + (length << 1)); program.writeRuntimeHeader(buffer, 0, stringInstance, length << 1); for (let i = 0; i < length; ++i) { writeI16(stringValue.charCodeAt(i), buffer, rtHeaderSize + (i << 1)); } stringSegment = this.addMemorySegment(buffer); segments.set(stringValue, stringSegment); } var ref = i64_add(stringSegment.offset, i64_new(rtHeaderSize)); this.currentType = stringInstance.type; if (this.options.isWasm64) { return this.module.createI64(i64_low(ref), i64_high(ref)); } else { assert(i64_is_u32(ref)); return this.module.createI32(i64_low(ref)); } } ensureStaticArrayBuffer(elementType: Type, values: ExpressionRef[]): MemorySegment { var program = this.program; var length = values.length; var byteSize = elementType.byteSize; var byteLength = length * byteSize; var bufferInstance = assert(program.arrayBufferInstance); var runtimeHeaderSize = program.runtimeHeaderSize; var buf = new Uint8Array(runtimeHeaderSize + byteLength); program.writeRuntimeHeader(buf, 0, bufferInstance, byteLength); var pos = runtimeHeaderSize; var nativeType = elementType.toNativeType(); switch (nativeType) { case NativeType.I32: { switch (byteSize) { case 1: { for (let i = 0; i < length; ++i) { let value = values[i]; assert(getExpressionType(value) == nativeType); assert(getExpressionId(value) == ExpressionId.Const); writeI8(getConstValueI32(value), buf, pos); pos += 1; } break; } case 2: { for (let i = 0; i < length; ++i) { let value = values[i]; assert(getExpressionType(value) == nativeType); assert(getExpressionId(value) == ExpressionId.Const); writeI16(getConstValueI32(value), buf, pos); pos += 2; } break; } case 4: { for (let i = 0; i < length; ++i) { let value = values[i]; assert(getExpressionType(value) == nativeType); assert(getExpressionId(value) == ExpressionId.Const); writeI32(getConstValueI32(value), buf, pos); pos += 4; } break; } default: assert(false); } break; } case NativeType.I64: { for (let i = 0; i < length; ++i) { let value = values[i]; assert(getExpressionType(value) == nativeType); assert(getExpressionId(value) == ExpressionId.Const); writeI64(i64_new(getConstValueI64Low(value), getConstValueI64High(value)), buf, pos); pos += 8; } break; } case NativeType.F32: { for (let i = 0; i < length; ++i) { let value = values[i]; assert(getExpressionType(value) == nativeType); assert(getExpressionId(value) == ExpressionId.Const); writeF32(getConstValueF32(value), buf, pos); pos += 4; } break; } case NativeType.F64: { for (let i = 0; i < length; ++i) { let value = values[i]; assert(getExpressionType(value) == nativeType); assert(getExpressionId(value) == ExpressionId.Const); writeF64(getConstValueF64(value), buf, pos); pos += 8; } break; } default: assert(false); } assert(pos == buf.length); return this.addMemorySegment(buf); } ensureStaticArrayHeader(elementType: Type, bufferSegment: MemorySegment): MemorySegment { var program = this.program; var runtimeHeaderSize = program.runtimeHeaderSize; var arrayPrototype = assert(program.arrayPrototype); var arrayInstance = assert(this.resolver.resolveClass(arrayPrototype, [ elementType ])); var arrayInstanceSize = arrayInstance.currentMemoryOffset; var bufferLength = bufferSegment.buffer.length - runtimeHeaderSize; var arrayLength = i32(bufferLength / elementType.byteSize); var buf = new Uint8Array(runtimeHeaderSize + arrayInstanceSize); program.writeRuntimeHeader(buf, 0, arrayInstance, arrayInstanceSize); var bufferAddress32 = i64_low(bufferSegment.offset) + runtimeHeaderSize; assert(!program.options.isWasm64); // TODO assert(arrayInstance.writeField("data", bufferAddress32, buf, runtimeHeaderSize)); assert(arrayInstance.writeField("dataStart", bufferAddress32, buf, runtimeHeaderSize)); assert(arrayInstance.writeField("dataLength", bufferLength, buf, runtimeHeaderSize)); assert(arrayInstance.writeField("length_", arrayLength, buf, runtimeHeaderSize)); return this.addMemorySegment(buf); } // === 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.internalName); func.functionTableIndex = index; return index; } // === Statements =============================================================================== compileTopLevelStatement(statement: Statement, body: ExpressionRef[]): void { switch (statement.kind) { case NodeKind.CLASSDECLARATION: { let memberStatements = (statement).members; for (let i = 0, k = memberStatements.length; i < k; ++i) { this.compileTopLevelStatement(memberStatements[i], body); } break; } case NodeKind.ENUMDECLARATION: { let element = this.program.getElementByDeclaration(statement); assert(element.kind == ElementKind.ENUM); if (!element.hasDecorator(DecoratorFlags.LAZY)) this.compileEnum(element); break; } case NodeKind.NAMESPACEDECLARATION: { let memberStatements = (statement).members; for (let i = 0, k = memberStatements.length; i < k; ++i) { this.compileTopLevelStatement(memberStatements[i], body); } break; } case NodeKind.VARIABLE: { let declarations = (statement).declarations; for (let i = 0, k = declarations.length; i < k; ++i) { let element = this.program.getElementByDeclaration(declarations[i]); assert(element.kind == ElementKind.GLOBAL); if ( !element.is(CommonFlags.AMBIENT) && // delay imports !element.hasDecorator(DecoratorFlags.LAZY) ) this.compileGlobal(element); } break; } case NodeKind.FIELDDECLARATION: { let element = this.program.getElementByDeclaration(statement); if (element.kind == ElementKind.GLOBAL) { // static if (!element.hasDecorator(DecoratorFlags.LAZY)) this.compileGlobal(element); } break; } case NodeKind.EXPORT: { if ((statement).normalizedPath != null) { this.compileFileByPath( (statement).normalizedPath, (statement).path ); } break; } case NodeKind.IMPORT: { this.compileFileByPath( (statement).normalizedPath, (statement).path ); break; } case NodeKind.FUNCTIONDECLARATION: case NodeKind.METHODDECLARATION: case NodeKind.INTERFACEDECLARATION: case NodeKind.INDEXSIGNATUREDECLARATION: case NodeKind.TYPEDECLARATION: break; default: { // otherwise a top-level statement that is part of the start function's body let stmt = this.compileStatement(statement); if (getExpressionId(stmt) != ExpressionId.Nop) body.push(stmt); break; } } } compileStatement( statement: Statement, contextualFlags: ContextualFlags = ContextualFlags.NONE ): ExpressionRef { var module = this.module; var stmt: ExpressionRef; switch (statement.kind) { case NodeKind.BLOCK: { stmt = this.compileBlockStatement(statement, contextualFlags); break; } case NodeKind.BREAK: { stmt = this.compileBreakStatement(statement, contextualFlags); break; } case NodeKind.CONTINUE: { stmt = this.compileContinueStatement(statement, contextualFlags); break; } case NodeKind.DO: { stmt = this.compileDoStatement(statement, contextualFlags); break; } case NodeKind.EMPTY: { stmt = this.compileEmptyStatement(statement, contextualFlags); break; } case NodeKind.EXPRESSION: { stmt = this.compileExpressionStatement(statement, contextualFlags); break; } case NodeKind.FOR: { stmt = this.compileForStatement(statement, contextualFlags); break; } case NodeKind.IF: { stmt = this.compileIfStatement(statement, contextualFlags); break; } case NodeKind.RETURN: { stmt = this.compileReturnStatement(statement, contextualFlags); break; } case NodeKind.SWITCH: { stmt = this.compileSwitchStatement(statement, contextualFlags); break; } case NodeKind.THROW: { stmt = this.compileThrowStatement(statement, contextualFlags); break; } case NodeKind.TRY: { stmt = this.compileTryStatement(statement, contextualFlags); break; } case NodeKind.VARIABLE: { stmt = this.compileVariableStatement(statement, contextualFlags); if (!stmt) stmt = module.createNop(); break; } case NodeKind.VOID: { stmt = this.compileVoidStatement(statement, contextualFlags); break; } case NodeKind.WHILE: { stmt = this.compileWhileStatement(statement, contextualFlags); break; } case NodeKind.TYPEDECLARATION: { // TODO: integrate inner type declaration into flow this.error( DiagnosticCode.Operation_not_supported, statement.range ); stmt = module.createUnreachable(); break; } default: { assert(false); stmt = module.createUnreachable(); } } if (this.options.sourceMap) this.addDebugLocation(stmt, statement.range); return stmt; } compileStatements( statements: Statement[], isBody: bool = false, stmts: ExpressionRef[] | null = null ): ExpressionRef[] { var numStatements = statements.length; if (!stmts) { stmts = new Array(numStatements); stmts.length = 0; } var flow = this.currentFlow; for (let i = 0; i < numStatements; ++i) { let stmt = this.compileStatement(statements[i], isBody && i == numStatements - 1 ? ContextualFlags.LAST_IN_BODY : ContextualFlags.NONE ); switch (getExpressionId(stmt)) { case ExpressionId.Block: { if (!getBlockName(stmt)) { for (let j = 0, k = getBlockChildCount(stmt); j < k; ++j) stmts.push(getBlockChild(stmt, j)); break; } // fall-through } default: stmts.push(stmt); case ExpressionId.Nop: } if (flow.isAny(FlowFlags.ANY_TERMINATING)) { if (needsExplicitUnreachable(stmt)) stmts.push(this.module.createUnreachable()); break; } } return stmts; } compileBlockStatement( statement: BlockStatement, contextualFlags: ContextualFlags ): ExpressionRef { var statements = statement.statements; var outerFlow = this.currentFlow; var innerFlow = outerFlow.fork(); this.currentFlow = innerFlow; var stmts = this.compileStatements(statements); if (!innerFlow.isAny(FlowFlags.ANY_TERMINATING)) this.performAutoreleases(innerFlow, stmts); innerFlow.freeScopedLocals(); outerFlow.inherit(innerFlow); // TODO: only if not terminated? this.currentFlow = outerFlow; return flatten(this.module, stmts, NativeType.None); } compileBreakStatement( statement: BreakStatement, contextualFlags: ContextualFlags ): ExpressionRef { var module = this.module; if (statement.label) { this.error( DiagnosticCode.Operation_not_supported, statement.label.range ); return module.createUnreachable(); } var flow = this.currentFlow; 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(); } var stmts = new Array(); this.performAutoreleases(flow, stmts); var current: Flow | null = flow.parent; while (current && current.breakLabel === breakLabel) { this.performAutoreleases(current, stmts, /* clearFlags */ false); current = current.parent; } flow.freeScopedLocals(); stmts.push(module.createBreak(breakLabel)); flow.set(FlowFlags.BREAKS); return flatten(module, stmts, NativeType.None); } compileContinueStatement( statement: ContinueStatement, contextualFlags: ContextualFlags ): 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.currentFlow; 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); var stmts = new Array(); this.performAutoreleases(flow, stmts); var current: Flow | null = flow.parent; while (current && current.continueLabel === continueLabel) { this.performAutoreleases(current, stmts, /* clearFlags */ false); current = current.parent; } flow.freeScopedLocals(); stmts.push(module.createBreak(continueLabel)); return flatten(module, stmts, NativeType.None); } compileDoStatement( statement: DoStatement, contextualFlags: ContextualFlags ): ExpressionRef { var module = this.module; var outerFlow = this.currentFlow; var label = outerFlow.pushBreakLabel(); var innerFlow = outerFlow.fork(); this.currentFlow = innerFlow; var breakLabel = "break|" + label; innerFlow.breakLabel = breakLabel; var continueLabel = "continue|" + label; innerFlow.continueLabel = continueLabel; var stmts = new Array(); if (statement.statement.kind == NodeKind.BLOCK) { this.compileStatements((statement.statement).statements, false, stmts); } else { stmts.push( this.compileStatement(statement.statement) ); } var condExpr = this.makeIsTrueish( this.compileExpression(statement.condition, Type.i32), this.currentType ); // TODO: check if condition is always false and if so, omit it (just a block) // Switch back to the parent flow var terminated = innerFlow.isAny(FlowFlags.ANY_TERMINATING); if (!terminated) this.performAutoreleases(innerFlow, stmts); innerFlow.freeScopedLocals(); outerFlow.popBreakLabel(); this.currentFlow = outerFlow; innerFlow.unset( FlowFlags.BREAKS | FlowFlags.CONDITIONALLY_BREAKS | FlowFlags.CONTINUES | FlowFlags.CONDITIONALLY_CONTINUES ); outerFlow.inherit(innerFlow); var block: ExpressionRef[] = [ module.createLoop(continueLabel, terminated ? flatten(module, stmts, NativeType.None) // skip trailing continue if unnecessary : module.createBlock(null, [ flatten(module, stmts, NativeType.None), module.createBreak(continueLabel, condExpr) ], NativeType.None) ) ]; if (terminated) block.push(module.createUnreachable()); return module.createBlock(breakLabel, block); } compileEmptyStatement( statement: EmptyStatement, contextualFlags: ContextualFlags ): ExpressionRef { return this.module.createNop(); } compileExpressionStatement( statement: ExpressionStatement, contextualFlags: ContextualFlags, ): ExpressionRef { return this.compileExpression(statement.expression, Type.void, contextualFlags | ContextualFlags.EXPLICIT | ContextualFlags.WILL_DROP ); } compileForStatement( statement: ForStatement, contextualFlags: ContextualFlags ): ExpressionRef { // A for statement initiates a new branch with its own scoped variables // possibly declared in its initializer, and break context. var outerFlow = this.currentFlow; var label = outerFlow.pushBreakLabel(); var innerFlow = outerFlow.fork(); this.currentFlow = innerFlow; var breakLabel = innerFlow.breakLabel = "break|" + label; innerFlow.breakLabel = breakLabel; var continueLabel = "continue|" + label; innerFlow.continueLabel = continueLabel; var repeatLabel = "repeat|" + label; // Compile in correct order var module = this.module; var initExpr = statement.initializer ? this.compileStatement(statement.initializer) : 0; var condExpr: ExpressionRef = 0; var alwaysTrue = false; if (statement.condition) { condExpr = module.precomputeExpression( this.makeIsTrueish( this.compileExpressionRetainType(statement.condition, Type.bool), this.currentType ) ); // check if the condition is always true if (getExpressionId(condExpr) == ExpressionId.Const) { assert(getExpressionType(condExpr) == NativeType.I32); if (getConstValueI32(condExpr) != 0) alwaysTrue = true; // TODO: could skip compilation if the condition is always false here, but beware that the // initializer could still declare new 'var's that are used later on. } } else { // omitted condition is always true condExpr = module.createI32(1); alwaysTrue = true; } innerFlow.inheritNonnullIfTrue(condExpr); var incrExpr = statement.incrementor ? this.compileExpression(statement.incrementor, Type.void, ContextualFlags.IMPLICIT | ContextualFlags.WILL_DROP ) : 0; var bodyStatement = statement.statement; var stmts = new Array(); if (bodyStatement.kind == NodeKind.BLOCK) { this.compileStatements((bodyStatement).statements, false, stmts); } else { stmts.push( this.compileStatement(bodyStatement) ); } if (!innerFlow.isAny(FlowFlags.ANY_TERMINATING)) this.performAutoreleases(innerFlow, stmts); // Switch back to the parent flow innerFlow.freeScopedLocals(); outerFlow.popBreakLabel(); this.currentFlow = outerFlow; var usesContinue = innerFlow.isAny(FlowFlags.CONTINUES | FlowFlags.CONDITIONALLY_CONTINUES); innerFlow.unset( FlowFlags.BREAKS | FlowFlags.CONDITIONALLY_BREAKS | FlowFlags.CONTINUES | FlowFlags.CONDITIONALLY_CONTINUES ); if (alwaysTrue) outerFlow.inherit(innerFlow); else outerFlow.inheritConditional(innerFlow); var breakBlock = new Array(); // outer 'break' block if (initExpr) breakBlock.push(initExpr); var repeatBlock = new Array(); // block repeating the loop if (usesContinue) { stmts.unshift( module.createBreak(breakLabel, module.createUnary(UnaryOp.EqzI32, condExpr)) ); repeatBlock.push( module.createBlock(continueLabel, stmts, NativeType.None) ); } else { // can omit the 'continue' block repeatBlock.push( module.createBreak(breakLabel, module.createUnary(UnaryOp.EqzI32, condExpr)) ); for (let i = 0, k = stmts.length; i < k; ++i) { repeatBlock.push(stmts[i]); } } if (incrExpr) repeatBlock.push(incrExpr); repeatBlock.push( module.createBreak(repeatLabel) ); breakBlock.push( module.createLoop(repeatLabel, module.createBlock(null, repeatBlock, NativeType.None)) ); return module.createBlock(breakLabel, breakBlock); } compileIfStatement( statement: IfStatement, contextualFlags: ContextualFlags ): ExpressionRef { var module = this.module; var ifTrue = statement.ifTrue; var ifFalse = statement.ifFalse; var outerFlow = this.currentFlow; // The condition doesn't initiate a branch yet var condExpr = module.precomputeExpression( this.makeIsTrueish( this.compileExpressionRetainType(statement.condition, Type.bool), this.currentType ) ); // Try to eliminate unnecesssary branches if the condition is constant if ( getExpressionId(condExpr) == ExpressionId.Const && getExpressionType(condExpr) == NativeType.I32 ) { return getConstValueI32(condExpr) ? this.compileStatement(ifTrue) : ifFalse ? this.compileStatement(ifFalse) : module.createNop(); } // Each arm initiates a branch var ifTrueStmts = new Array(); var ifTrueFlow = outerFlow.fork(); this.currentFlow = ifTrueFlow; ifTrueFlow.inheritNonnullIfTrue(condExpr); if (ifTrue.kind == NodeKind.BLOCK) { this.compileStatements((ifTrue).statements, false, ifTrueStmts); } else { ifTrueStmts.push(this.compileStatement(ifTrue)); } if (!ifTrueFlow.isAny(FlowFlags.ANY_TERMINATING)) this.performAutoreleases(ifTrueFlow, ifTrueStmts); ifTrueFlow.freeScopedLocals(); this.currentFlow = outerFlow; if (ifFalse) { let ifFalseFlow = outerFlow.fork(); this.currentFlow = ifFalseFlow; ifFalseFlow.inheritNonnullIfFalse(condExpr); let ifFalseStmts = new Array(); if (ifFalse.kind == NodeKind.BLOCK) { this.compileStatements((ifFalse).statements, false, ifFalseStmts); } else { ifFalseStmts.push(this.compileStatement(ifFalse)); } if (!ifFalseFlow.isAny(FlowFlags.ANY_TERMINATING)) this.performAutoreleases(ifFalseFlow, ifFalseStmts); ifFalseFlow.freeScopedLocals(); this.currentFlow = outerFlow; outerFlow.inheritMutual(ifTrueFlow, ifFalseFlow); return module.createIf(condExpr, flatten(module, ifTrueStmts, NativeType.None), flatten(module, ifFalseStmts, NativeType.None) ); } else { outerFlow.inheritConditional(ifTrueFlow); if (ifTrueFlow.isAny(FlowFlags.ANY_TERMINATING)) { outerFlow.inheritNonnullIfFalse(condExpr); } return module.createIf(condExpr, flatten(module, ifTrueStmts, NativeType.None) ); } } compileReturnStatement( statement: ReturnStatement, contextualFlags: ContextualFlags ): ExpressionRef { var module = this.module; var expr: ExpressionRef = 0; var flow = this.currentFlow; var returnType = flow.returnType; // Remember that this flow returns flow.set(FlowFlags.RETURNS); var valueExpression = statement.value; if (valueExpression) { if (returnType == Type.void) { this.error( DiagnosticCode.Type_0_is_not_assignable_to_type_1, valueExpression.range, this.currentType.toString(), returnType.toString() ); this.currentType = Type.void; return module.createUnreachable(); } let contextualFlags = ContextualFlags.IMPLICIT; if (flow.actualFunction.is(CommonFlags.MODULE_EXPORT)) contextualFlags |= ContextualFlags.WRAP; expr = this.compileExpression(valueExpression, returnType, contextualFlags | ContextualFlags.SKIP_AUTORELEASE); // when returning a local, and it is already retained, skip the final set // of retaining it as the return value and releasing it as a variable if (!this.skippedAutoreleases.has(expr)) { if (returnType.isManaged) { if (getExpressionId(expr) == ExpressionId.GetLocal) { if (flow.isAnyLocalFlag(getGetLocalIndex(expr), LocalFlags.ANY_RETAINED)) { flow.unsetLocalFlag(getGetLocalIndex(expr), LocalFlags.ANY_RETAINED); this.skippedAutoreleases.add(expr); } } } } // Remember whether returning a properly wrapped value if (!flow.canOverflow(expr, returnType)) flow.set(FlowFlags.RETURNS_WRAPPED); } else if (returnType != Type.void) { this.error( DiagnosticCode.Type_0_is_not_assignable_to_type_1, statement.range, "void", returnType.toString() ); expr = module.createUnreachable(); } var stmts = new Array(); this.performAutoreleases(flow, stmts); this.finishAutoreleases(flow, stmts); // Make sure that the return value is retained for the caller if (returnType.isManaged && !this.skippedAutoreleases.has(expr)) expr = this.makeRetain(expr); if (returnType != Type.void && stmts.length) { let temp = flow.getAndFreeTempLocal(returnType, false, flow.isNonnull(returnType, expr)); stmts.unshift( module.createSetLocal(temp.index, expr) ); expr = module.createGetLocal(temp.index, returnType.toNativeType()); } flow.freeScopedLocals(); // If the last statement anyway, make it the block's return value if ((contextualFlags & ContextualFlags.LAST_IN_BODY) != 0 && expr && returnType != Type.void) { if (!stmts.length) return expr; stmts.push(expr); return module.createBlock(null, stmts, returnType.toNativeType()); } // When inlining, break to the end of the inlined function's block (no need to wrap) if (flow.is(FlowFlags.INLINE_CONTEXT)) { if (!stmts.length) return module.createBreak(assert(flow.inlineReturnLabel), 0, expr); stmts.push(module.createBreak(assert(flow.inlineReturnLabel), 0, expr)); // stmts.push(module.createUnreachable()); return module.createBlock(null, stmts); } // Otherwise emit a normal return if (!stmts.length) return module.createReturn(expr); stmts.push(module.createReturn(expr)); return module.createBlock(null, stmts); } compileSwitchStatement(statement: SwitchStatement, contextualFlags: ContextualFlags): ExpressionRef { var module = this.module; var cases = statement.cases; var numCases = cases.length; if (!numCases) { return this.compileExpression(statement.condition, Type.void, ContextualFlags.IMPLICIT ); } // Everything within a switch uses the same break context var outerFlow = this.currentFlow; var context = outerFlow.pushBreakLabel(); // introduce a local for evaluating the condition (exactly once) var tempLocal = outerFlow.getTempLocal(Type.u32); var tempLocalIndex = tempLocal.index; // 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, ContextualFlags.IMPLICIT ) ); // 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.u32, ContextualFlags.IMPLICIT ) ) ); } else { defaultIndex = i; } } outerFlow.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 alwaysReturnsWrapped = 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; // Each switch case initiates a new branch let innerFlow = outerFlow.fork(); this.currentFlow = innerFlow; let breakLabel = "break|" + context; innerFlow.breakLabel = breakLabel; let isLast = i == numCases - 1; let nextLabel = isLast ? breakLabel : "case" + (i + 1).toString(10) + "|" + context; let stmts = new Array(1 + numStatements); stmts[0] = currentBlock; let count = 1; let terminated = false; for (let j = 0; j < numStatements; ++j) { let stmt = this.compileStatement(statements[j]); if (getExpressionId(stmt) != ExpressionId.Nop) { stmts[count++] = stmt; if (innerFlow.isAny(FlowFlags.ANY_TERMINATING)) { terminated = true; break; } } } stmts.length = count; if (terminated || isLast) { if (!innerFlow.is(FlowFlags.RETURNS)) alwaysReturns = false; if (!innerFlow.is(FlowFlags.RETURNS_WRAPPED)) alwaysReturnsWrapped = false; if (!innerFlow.is(FlowFlags.THROWS)) alwaysThrows = false; if (!innerFlow.is(FlowFlags.ALLOCATES)) alwaysAllocates = false; } // Switch back to the parent flow if (!innerFlow.isAny(FlowFlags.ANY_TERMINATING)) this.performAutoreleases(innerFlow, stmts); innerFlow.unset( FlowFlags.BREAKS | FlowFlags.CONDITIONALLY_BREAKS ); innerFlow.freeScopedLocals(); this.currentFlow = outerFlow; currentBlock = module.createBlock(nextLabel, stmts, NativeType.None); // must be a labeled block } outerFlow.popBreakLabel(); // If the switch has a default (guaranteed to handle any value), propagate common flags if (defaultIndex >= 0) { if (alwaysReturns) outerFlow.set(FlowFlags.RETURNS); if (alwaysReturnsWrapped) outerFlow.set(FlowFlags.RETURNS_WRAPPED); if (alwaysThrows) outerFlow.set(FlowFlags.THROWS); if (alwaysAllocates) outerFlow.set(FlowFlags.ALLOCATES); } return currentBlock; } compileThrowStatement( statement: ThrowStatement, contextualFlags: ContextualFlags ): ExpressionRef { var flow = this.currentFlow; // 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); var stmts = new Array(); this.finishAutoreleases(flow, stmts); // TODO: requires exception-handling spec. var value = statement.value; var message: Expression | null = null; if (value.kind == NodeKind.NEW) { let newArgs = (value).arguments; if (newArgs.length) message = newArgs[0]; // FIXME: naively assumes type string } stmts.push(compileAbort(this, message, statement)); return flatten(this.module, stmts, NativeType.None); } compileTryStatement( statement: TryStatement, contextualFlags: ContextualFlags ): 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, contextualFlags: ContextualFlags): ExpressionRef { var module = this.module; var declarations = statement.declarations; var numDeclarations = declarations.length; var flow = this.currentFlow; var initializers = new Array(); var resolver = this.resolver; for (let i = 0; i < numDeclarations; ++i) { let declaration = declarations[i]; let name = declaration.name.text; let type: Type | null = null; let initExpr: ExpressionRef = 0; let initAutoreleaseSkipped = false; if (declaration.type) { type = resolver.resolveType( // reports declaration.type, flow.actualFunction, flow.contextualTypeArguments ); if (!type) continue; if (declaration.initializer) { initExpr = this.compileExpression(declaration.initializer, type, // reports ContextualFlags.IMPLICIT | ContextualFlags.SKIP_AUTORELEASE ); initAutoreleaseSkipped = this.skippedAutoreleases.has(initExpr); } } else if (declaration.initializer) { // infer type using void/NONE for proper literal inference initExpr = this.compileExpressionRetainType(declaration.initializer, Type.void, ContextualFlags.SKIP_AUTORELEASE ); // reports initAutoreleaseSkipped = this.skippedAutoreleases.has(initExpr); 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 (initExpr) { initExpr = module.precomputeExpression(initExpr); if (getExpressionId(initExpr) == ExpressionId.Const) { let local = new Local(name, -1, type, flow.parentFunction); switch (getExpressionType(initExpr)) { case NativeType.I32: { local.setConstantIntegerValue( i64_new( getConstValueI32(initExpr), 0 ), type ); break; } case NativeType.I64: { local.setConstantIntegerValue( i64_new( getConstValueI64Low(initExpr), getConstValueI64High(initExpr) ), type ); break; } case NativeType.F32: { local.setConstantFloatValue(getConstValueF32(initExpr), type); break; } case NativeType.F64: { local.setConstantFloatValue(getConstValueF64(initExpr), type); break; } default: { assert(false); return module.createUnreachable(); } } // Create a virtual local that doesn't actually exist in WebAssembly let scopedLocals = flow.scopedLocals; if (!scopedLocals) 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) { let local: Local; if ( declaration.isAny(CommonFlags.LET | CommonFlags.CONST) || flow.is(FlowFlags.INLINE_CONTEXT) ) { // here: not top-level local = flow.addScopedLocal(name, type, false, declaration.name); // reports if duplicate } else { if (flow.lookupLocal(name)) { this.error( DiagnosticCode.Duplicate_identifier_0, declaration.name.range, name ); continue; } local = flow.parentFunction.addLocal(type, name, declaration); } let isManaged = type.isManaged; if (initExpr) { if (isManaged) { flow.setLocalFlag(local.index, LocalFlags.RETAINED); initializers.push( module.createSetLocal(local.index, initAutoreleaseSkipped ? initExpr : this.makeRetain(initExpr) ) ); } else { initializers.push( module.createSetLocal(local.index, initExpr) ); if (local.type.is(TypeFlags.SHORT | TypeFlags.INTEGER)) { if (!flow.canOverflow(initExpr, type)) flow.setLocalFlag(local.index, LocalFlags.WRAPPED); else flow.unsetLocalFlag(local.index, LocalFlags.WRAPPED); } } } else { if (isManaged) { // This is necessary because the first use (and assign) of the local could be taking place // in a loop, subsequently marking it retained, but the second iteration of the loop // still wouldn't release whatever is assigned in the first. Likewise, if the variable wasn't // initialized but becomes released later on, whatever was stored before would be released. // TODO: Detect this condition inside of a loop instead? initializers.push( module.createSetLocal(local.index, type.toNativeZero(module) ) ); flow.setLocalFlag(local.index, LocalFlags.CONDITIONALLY_RETAINED); } else if (local.type.is(TypeFlags.SHORT | TypeFlags.INTEGER)) { flow.setLocalFlag(local.index, LocalFlags.WRAPPED); } } } } return initializers.length == 0 ? 0 : flatten(module, initializers, NativeType.None); } compileVoidStatement(statement: VoidStatement, contextualFlags: ContextualFlags): ExpressionRef { return this.compileExpression(statement.expression, Type.void, ContextualFlags.EXPLICIT | ContextualFlags.WILL_DROP ); } compileWhileStatement(statement: WhileStatement, contextualFlags: ContextualFlags): ExpressionRef { var module = this.module; var outerFlow = this.currentFlow; // The condition does not yet initialize a branch var condExpr = module.precomputeExpression( this.makeIsTrueish( this.compileExpressionRetainType(statement.condition, Type.bool), this.currentType ) ); // Try to eliminate unnecesssary loops if the condition is constant if ( getExpressionId(condExpr) == ExpressionId.Const && getExpressionType(condExpr) == NativeType.I32 ) { if (!getConstValueI32(condExpr)) return module.createNop(); } // Statements initiate a new branch with its own break context var label = outerFlow.pushBreakLabel(); var innerFlow = outerFlow.fork(); this.currentFlow = innerFlow; var breakLabel = "break|" + label; innerFlow.breakLabel = breakLabel; var continueLabel = "continue|" + label; innerFlow.continueLabel = continueLabel; innerFlow.inheritNonnullIfTrue(condExpr); var stmts = new Array(); if (statement.statement.kind == NodeKind.BLOCK) { this.compileStatements((statement.statement).statements, false, stmts); } else { stmts.push(this.compileStatement(statement.statement)); } var alwaysTrue = false; // TODO var terminated = innerFlow.isAny(FlowFlags.ANY_TERMINATING); if (!terminated) { this.performAutoreleases(innerFlow, stmts); stmts.push(module.createBreak(continueLabel)); } innerFlow.freeScopedLocals(); // Switch back to the parent flow outerFlow.popBreakLabel(); this.currentFlow = outerFlow; innerFlow.unset( FlowFlags.BREAKS | FlowFlags.CONDITIONALLY_BREAKS | FlowFlags.CONTINUES | FlowFlags.CONDITIONALLY_CONTINUES ); if (alwaysTrue) outerFlow.inherit(innerFlow); else outerFlow.inheritConditional(innerFlow); return module.createBlock(breakLabel, [ module.createLoop(continueLabel, module.createIf(condExpr, flatten(module, stmts, NativeType.None) ) ) ]); } // === 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, contextualFlags: ContextualFlags ): ExpressionRef { assert(element.is(CommonFlags.INLINED)); var type = element.type; switch ( !(contextualFlags & (ContextualFlags.IMPLICIT | ContextualFlags.EXPLICIT)) && 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 ); // recognized by canOverflow } 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 ); // recognized by canOverflow } 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: { // monkey-patch for converting built-in floats to f32 implicitly if (!(element.hasDecorator(DecoratorFlags.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, contextualFlags: ContextualFlags = ContextualFlags.NONE ): ExpressionRef { this.currentType = contextualType; var expr: ExpressionRef; if (contextualType == Type.void) contextualFlags |= ContextualFlags.WILL_DROP; switch (expression.kind) { case NodeKind.ASSERTION: { expr = this.compileAssertionExpression(expression, contextualType, contextualFlags); break; } case NodeKind.BINARY: { expr = this.compileBinaryExpression(expression, contextualType, contextualFlags); break; } case NodeKind.CALL: { expr = this.compileCallExpression(expression, contextualType, contextualFlags); break; } case NodeKind.COMMA: { expr = this.compileCommaExpression(expression, contextualType, contextualFlags); break; } case NodeKind.ELEMENTACCESS: { expr = this.compileElementAccessExpression(expression, contextualType, contextualFlags); break; } case NodeKind.FUNCTION: { expr = this.compileFunctionExpression(expression, contextualType.signatureReference, contextualFlags); break; } case NodeKind.IDENTIFIER: case NodeKind.FALSE: case NodeKind.NULL: case NodeKind.THIS: case NodeKind.SUPER: case NodeKind.TRUE: { expr = this.compileIdentifierExpression(expression, contextualType, contextualFlags); break; } case NodeKind.INSTANCEOF: { expr = this.compileInstanceOfExpression(expression, contextualType, contextualFlags); break; } case NodeKind.LITERAL: { expr = this.compileLiteralExpression(expression, contextualType, contextualFlags); break; } case NodeKind.NEW: { expr = this.compileNewExpression(expression, contextualType, contextualFlags); break; } case NodeKind.PARENTHESIZED: { expr = this.compileExpression((expression).expression, contextualType, contextualFlags); break; } case NodeKind.PROPERTYACCESS: { expr = this.compilePropertyAccessExpression(expression, contextualType, contextualFlags); break; } case NodeKind.TERNARY: { expr = this.compileTernaryExpression(expression, contextualType, contextualFlags); break; } case NodeKind.UNARYPOSTFIX: { expr = this.compileUnaryPostfixExpression(expression, contextualType, contextualFlags); break; } case NodeKind.UNARYPREFIX: { expr = this.compileUnaryPrefixExpression(expression, contextualType, contextualFlags); break; } default: { this.error( DiagnosticCode.Operation_not_supported, expression.range ); expr = this.module.createUnreachable(); } } // ensure conversion and wrapping in case the respective function doesn't on its own var currentType = this.currentType; var wrap = (contextualFlags & ContextualFlags.WRAP) != 0; if (currentType != contextualType) { if (contextualFlags & ContextualFlags.EXPLICIT) { expr = this.convertExpression(expr, currentType, contextualType, true, wrap, expression); wrap = false; this.currentType = contextualType; } else if (contextualFlags & ContextualFlags.IMPLICIT) { expr = this.convertExpression(expr, currentType, contextualType, false, wrap, expression); wrap = false; this.currentType = contextualType; } } if (wrap) expr = this.ensureSmallIntegerWrap(expr, currentType); // debug location is added here so the caller doesn't have to. means: compilation of an expression // must go through this function, with the respective per-kind functions not being used directly. if (this.options.sourceMap) this.addDebugLocation(expr, expression.range); return expr; } /** Compiles an expression while retaining the type, that is not void, it ultimately compiles to. */ compileExpressionRetainType( expression: Expression, contextualType: Type, contextualFlags: ContextualFlags = ContextualFlags.NONE ): ExpressionRef { return this.compileExpression(expression, contextualType == Type.void ? Type.i32 // default to i32 : contextualType, (contextualFlags & ~(ContextualFlags.IMPLICIT | ContextualFlags.EXPLICIT)) ); } /** Compiles and precomputes an expression, possibly yielding a costant value. */ precomputeExpression( expression: Expression, contextualType: Type, contextualFlags: ContextualFlags = ContextualFlags.NONE ): ExpressionRef { return this.module.precomputeExpression( this.compileExpression(expression, contextualType, contextualFlags) ); } convertExpression( expr: ExpressionRef, /** Original type. */ fromType: Type, /** New type. */ toType: Type, /** Whether the conversion is explicit.*/ explicit: bool, /** Whether the result should be wrapped, if a small integer. */ wrap: bool, reportNode: Node ): ExpressionRef { 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 (this.currentFlow.isNonnull(fromType, expr)) fromType = fromType.nonNullableType; if (!fromType.isAssignableTo(toType)) { if (!explicit) { if (fromType.nonNullableType == toType) { this.error( DiagnosticCode.Object_is_possibly_null, reportNode.range ); // recoverable } else { this.error( DiagnosticCode.Conversion_from_type_0_to_1_requires_an_explicit_cast, reportNode.range, fromType.toString(), toType.toString() ); // recoverable } } } 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 == Type.bool) { expr = module.createBinary(BinaryOp.NeF32, expr, module.createF32(0)); wrap = false; } else if (toType.is(TypeFlags.SIGNED)) { if (toType.is(TypeFlags.LONG)) { expr = module.createUnary(UnaryOp.TruncF32ToI64, expr); } else { expr = module.createUnary(UnaryOp.TruncF32ToI32, expr); } } else { if (toType.is(TypeFlags.LONG)) { expr = module.createUnary(UnaryOp.TruncF32ToU64, expr); } else { expr = module.createUnary(UnaryOp.TruncF32ToU32, expr); } } // f64 to int } else { if (toType == Type.bool) { expr = module.createBinary(BinaryOp.NeF64, expr, module.createF64(0)); wrap = false; } else if (toType.is(TypeFlags.SIGNED)) { if (toType.is(TypeFlags.LONG)) { expr = module.createUnary(UnaryOp.TruncF64ToI64, expr); } else { expr = module.createUnary(UnaryOp.TruncF64ToI32, expr); } } else { if (toType.is(TypeFlags.LONG)) { expr = module.createUnary(UnaryOp.TruncF64ToU64, expr); } else { expr = module.createUnary(UnaryOp.TruncF64ToU32, expr); } } } // 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 { // i64 to ... if (fromType.is(TypeFlags.LONG)) { // i64 to i32 or smaller if (toType == Type.bool) { expr = module.createBinary(BinaryOp.NeI64, expr, module.createI64(0)); wrap = false; } else if (!toType.is(TypeFlags.LONG)) { expr = module.createUnary(UnaryOp.WrapI64, expr); // discards upper bits } // i32 or smaller to i64 } else if (toType.is(TypeFlags.LONG)) { expr = module.createUnary( fromType.is(TypeFlags.SIGNED) ? UnaryOp.ExtendI32 : UnaryOp.ExtendU32, this.ensureSmallIntegerWrap(expr, fromType) // must clear garbage bits ); wrap = false; // i32 to i32 } else { // small i32 to ... if (fromType.is(TypeFlags.SHORT)) { // small i32 to larger i32 if (fromType.size < toType.size) { expr = this.ensureSmallIntegerWrap(expr, fromType); // must clear garbage bits wrap = false; } } } } this.currentType = toType; return wrap ? this.ensureSmallIntegerWrap(expr, toType) : expr; } compileAssertionExpression( expression: AssertionExpression, contextualType: Type, contextualFlags: ContextualFlags ): ExpressionRef { switch (expression.assertionKind) { case AssertionKind.PREFIX: case AssertionKind.AS: { let flow = this.currentFlow; let toType = this.resolver.resolveType( // reports assert(expression.toType), flow.actualFunction, flow.contextualTypeArguments ); if (!toType) return this.module.createUnreachable(); return this.compileExpression(expression.expression, toType, contextualFlags | ContextualFlags.EXPLICIT ); } case AssertionKind.NONNULL: { assert(!expression.toType); let expr = this.compileExpressionRetainType(expression.expression, contextualType); let type = this.currentType; if (!type.is(TypeFlags.NULLABLE | TypeFlags.REFERENCE) || this.currentFlow.isNonnull(type, expr)) { this.info( DiagnosticCode.Expression_is_never_null, expression.expression.range ); } else if (!this.options.noAssert) { let module = this.module; let flow = this.currentFlow; let tempIndex = flow.getAndFreeTempLocal(type, !flow.canOverflow(expr, type), false).index; expr = module.createIf( module.createTeeLocal(tempIndex, expr), module.createGetLocal(tempIndex, type.toNativeType()), module.createUnreachable() ); } this.currentType = this.currentType.nonNullableType; return expr; } default: assert(false); } return this.module.createUnreachable(); } 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, contextualFlags: ContextualFlags ): 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 expr: ExpressionRef; var compound = false; var operator = expression.operator; switch (operator) { case Token.LESSTHAN: { leftExpr = this.compileExpressionRetainType(left, contextualType); leftType = this.currentType; // check operator overload if (this.currentType.is(TypeFlags.REFERENCE)) { let classReference = leftType.classReference; if (classReference) { let overload = classReference.lookupOverload(OperatorKind.LT); if (overload) { expr = this.compileBinaryOverload(overload, left, leftExpr, right, expression); break; } } this.error( DiagnosticCode.Operation_not_supported, expression.range ); return this.module.createUnreachable(); } rightExpr = this.compileExpressionRetainType(right, leftType); rightType = this.currentType; if (commonType = Type.commonDenominator(leftType, rightType, true)) { leftExpr = this.convertExpression(leftExpr, leftType, leftType = commonType, false, true, // ! left ); rightExpr = this.convertExpression(rightExpr, rightType, rightType = commonType, false, true, // ! 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); expr = module.createUnreachable(); } } this.currentType = Type.bool; break; } case Token.GREATERTHAN: { leftExpr = this.compileExpressionRetainType(left, contextualType); leftType = this.currentType; // check operator overload if (this.currentType.is(TypeFlags.REFERENCE)) { let classReference = leftType.classReference; if (classReference) { let overload = classReference.lookupOverload(OperatorKind.GT); if (overload) { expr = this.compileBinaryOverload(overload, left, leftExpr, right, expression); break; } } this.error( DiagnosticCode.Operation_not_supported, expression.range ); return this.module.createUnreachable(); } rightExpr = this.compileExpressionRetainType(right, leftType); rightType = this.currentType; if (commonType = Type.commonDenominator(leftType, rightType, true)) { leftExpr = this.convertExpression(leftExpr, leftType, leftType = commonType, false, true, // ! left ); rightExpr = this.convertExpression(rightExpr, rightType, rightType = commonType, false, true, // ! 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); expr = module.createUnreachable(); } } this.currentType = Type.bool; break; } case Token.LESSTHAN_EQUALS: { leftExpr = this.compileExpressionRetainType(left, contextualType); leftType = this.currentType; // check operator overload if (this.currentType.is(TypeFlags.REFERENCE)) { let classReference = leftType.classReference; if (classReference) { let overload = classReference.lookupOverload(OperatorKind.LE); if (overload) { expr = this.compileBinaryOverload(overload, left, leftExpr, right, expression); break; } } this.error( DiagnosticCode.Operation_not_supported, expression.range ); return this.module.createUnreachable(); } rightExpr = this.compileExpressionRetainType(right, leftType); rightType = this.currentType; if (commonType = Type.commonDenominator(leftType, rightType, true)) { leftExpr = this.convertExpression(leftExpr, leftType, leftType = commonType, false, true, // ! left ); rightExpr = this.convertExpression(rightExpr, rightType, rightType = commonType, false, true, // ! 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); expr = module.createUnreachable(); } } this.currentType = Type.bool; break; } case Token.GREATERTHAN_EQUALS: { leftExpr = this.compileExpressionRetainType(left, contextualType); leftType = this.currentType; // check operator overload if (this.currentType.is(TypeFlags.REFERENCE)) { let classReference = leftType.classReference; if (classReference) { let overload = classReference.lookupOverload(OperatorKind.GE); if (overload) { expr = this.compileBinaryOverload(overload, left, leftExpr, right, expression); break; } } this.error( DiagnosticCode.Operation_not_supported, expression.range ); return this.module.createUnreachable(); } rightExpr = this.compileExpressionRetainType(right, leftType); rightType = this.currentType; if (commonType = Type.commonDenominator(leftType, rightType, true)) { leftExpr = this.convertExpression(leftExpr, leftType, leftType = commonType, false, true, // ! left ); rightExpr = this.convertExpression(rightExpr, rightType, rightType = commonType, false, true, // ! 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); expr = module.createUnreachable(); } } 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; // check operator overload if (operator == Token.EQUALS_EQUALS && this.currentType.is(TypeFlags.REFERENCE)) { let classReference = leftType.classReference; if (classReference) { let overload = classReference.lookupOverload(OperatorKind.EQ); if (overload) { expr = this.compileBinaryOverload(overload, left, leftExpr, right, expression); break; } } // still allow '==' with references } rightExpr = this.compileExpressionRetainType(right, leftType); rightType = this.currentType; if (commonType = Type.commonDenominator(leftType, rightType, false)) { leftExpr = this.convertExpression(leftExpr, leftType, leftType = commonType, false, true, // ! left ); rightExpr = this.convertExpression(rightExpr, rightType, rightType = commonType, false, true, // ! 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; } case TypeKind.V128: { expr = module.createUnary(UnaryOp.AllTrueVecI8x16, module.createBinary(BinaryOp.EqVecI8x16, leftExpr, rightExpr) ); break; } default: { assert(false); expr = module.createUnreachable(); } } this.currentType = Type.bool; break; } case Token.EXCLAMATION_EQUALS_EQUALS: case Token.EXCLAMATION_EQUALS: { leftExpr = this.compileExpressionRetainType(left, contextualType); leftType = this.currentType; // check operator overload if (operator == Token.EXCLAMATION_EQUALS && this.currentType.is(TypeFlags.REFERENCE)) { let classReference = leftType.classReference; if (classReference) { let overload = classReference.lookupOverload(OperatorKind.NE); if (overload) { expr = this.compileBinaryOverload(overload, left, leftExpr, right, expression); break; } } // still allow '!=' with references } rightExpr = this.compileExpressionRetainType(right, leftType); rightType = this.currentType; if (commonType = Type.commonDenominator(leftType, rightType, false)) { leftExpr = this.convertExpression(leftExpr, leftType, leftType = commonType, false, true, // ! left ); rightExpr = this.convertExpression(rightExpr, rightType, rightType = commonType, false, true, // ! 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; } case TypeKind.V128: { expr = module.createUnary(UnaryOp.AnyTrueVecI8x16, module.createBinary(BinaryOp.NeVecI8x16, leftExpr, rightExpr) ); break; } default: { assert(false); 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); leftType = this.currentType; // check operator overload if (this.currentType.is(TypeFlags.REFERENCE)) { let classReference = leftType.classReference; if (classReference) { let overload = classReference.lookupOverload(OperatorKind.ADD); if (overload) { expr = this.compileBinaryOverload(overload, left, leftExpr, right, expression); break; } } this.error( DiagnosticCode.Operation_not_supported, expression.range ); return this.module.createUnreachable(); } if (compound) { rightExpr = this.compileExpression(right, leftType, ContextualFlags.IMPLICIT); } else { rightExpr = this.compileExpressionRetainType(right, leftType); rightType = this.currentType; if (commonType = Type.commonDenominator(leftType, rightType, false)) { leftExpr = this.convertExpression(leftExpr, leftType, leftType = commonType, false, false, left ); rightExpr = this.convertExpression(rightExpr, rightType, rightType = commonType, false, false, 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: // addition might overflow case TypeKind.I16: // ^ case TypeKind.U8: // ^ case TypeKind.U16: // ^ case TypeKind.BOOL: // ^ 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); expr = module.createUnreachable(); } } break; } case Token.MINUS_EQUALS: compound = true; case Token.MINUS: { leftExpr = this.compileExpressionRetainType(left, contextualType); leftType = this.currentType; // check operator overload if (this.currentType.is(TypeFlags.REFERENCE)) { let classReference = leftType.classReference; if (classReference) { let overload = classReference.lookupOverload(OperatorKind.SUB); if (overload) { expr = this.compileBinaryOverload(overload, left, leftExpr, right, expression); break; } } this.error( DiagnosticCode.Operation_not_supported, expression.range ); return this.module.createUnreachable(); } if (compound) { rightExpr = this.compileExpression(right, leftType, ContextualFlags.IMPLICIT); rightType = this.currentType; } else { rightExpr = this.compileExpressionRetainType(right, leftType); rightType = this.currentType; if (commonType = Type.commonDenominator(leftType, rightType, false)) { leftExpr = this.convertExpression(leftExpr, leftType, leftType = commonType, false, false, left ); rightExpr = this.convertExpression(rightExpr, rightType, rightType = commonType, false, false, 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: // subtraction might overflow case TypeKind.I16: // ^ case TypeKind.U8: // ^ case TypeKind.U16: // ^ case TypeKind.BOOL: // ^ 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); expr = module.createUnreachable(); } } break; } case Token.ASTERISK_EQUALS: compound = true; case Token.ASTERISK: { leftExpr = this.compileExpressionRetainType(left, contextualType); leftType = this.currentType; // check operator overload if (this.currentType.is(TypeFlags.REFERENCE)) { let classReference = leftType.classReference; if (classReference) { let overload = classReference.lookupOverload(OperatorKind.MUL); if (overload) { expr = this.compileBinaryOverload(overload, left, leftExpr, right, expression); break; } } this.error( DiagnosticCode.Operation_not_supported, expression.range ); return this.module.createUnreachable(); } if (compound) { leftExpr = this.ensureSmallIntegerWrap(leftExpr, leftType); rightExpr = this.compileExpression(right, leftType, ContextualFlags.IMPLICIT); } else { rightExpr = this.compileExpressionRetainType(right, leftType); rightType = this.currentType; if (commonType = Type.commonDenominator(leftType, rightType, false)) { leftExpr = this.convertExpression(leftExpr, leftType, leftType = commonType, false, false, left ); rightExpr = this.convertExpression(rightExpr, rightType, rightType = commonType, false, false, 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: 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); expr = module.createUnreachable(); } } break; } case Token.ASTERISK_ASTERISK_EQUALS: compound = true; case Token.ASTERISK_ASTERISK: { leftExpr = this.compileExpressionRetainType(left, contextualType); leftType = this.currentType; // check operator overload if (this.currentType.is(TypeFlags.REFERENCE)) { let classReference = leftType.classReference; if (classReference) { let overload = classReference.lookupOverload(OperatorKind.POW); if (overload) { expr = this.compileBinaryOverload(overload, left, leftExpr, right, expression); break; } } this.error( DiagnosticCode.Operation_not_supported, expression.range ); return this.module.createUnreachable(); } let instance: Function | null; // Mathf.pow if lhs is f32 (result is f32) if (this.currentType.kind == TypeKind.F32) { rightExpr = this.compileExpression(right, Type.f32, ContextualFlags.IMPLICIT); rightType = this.currentType; if (!(instance = this.f32PowInstance)) { let namespace = this.program.lookupGlobal(CommonSymbols.Mathf); if (!namespace) { this.error( DiagnosticCode.Cannot_find_name_0, expression.range, "Mathf" ); expr = module.createUnreachable(); break; } let prototype = namespace.members ? namespace.members.get(CommonSymbols.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 = this.resolver.resolveFunction(prototype, null); } // Math.pow otherwise (result is f64) // TODO: should the result be converted back? } else { leftExpr = this.convertExpression(leftExpr, this.currentType, Type.f64, false, false, left ); leftType = this.currentType; rightExpr = this.compileExpression(right, Type.f64, ContextualFlags.IMPLICIT); rightType = this.currentType; if (!(instance = this.f64PowInstance)) { let namespace = this.program.lookupGlobal(CommonSymbols.Math); if (!namespace) { this.error( DiagnosticCode.Cannot_find_name_0, expression.range, "Math" ); expr = module.createUnreachable(); break; } let prototype = namespace.members ? namespace.members.get(CommonSymbols.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 = this.resolver.resolveFunction(prototype, null); } } if (!(instance && this.compileFunction(instance))) { expr = module.createUnreachable(); } else { expr = this.makeCallDirect(instance, [ leftExpr, rightExpr ], expression); } break; } case Token.SLASH_EQUALS: compound = true; case Token.SLASH: { leftExpr = this.compileExpressionRetainType(left, contextualType); leftType = this.currentType; // check operator overload if (this.currentType.is(TypeFlags.REFERENCE)) { let classReference = leftType.classReference; if (classReference) { let overload = classReference.lookupOverload(OperatorKind.DIV); if (overload) { expr = this.compileBinaryOverload(overload, left, leftExpr, right, expression); break; } } this.error( DiagnosticCode.Operation_not_supported, expression.range ); return this.module.createUnreachable(); } if (compound) { leftExpr = this.ensureSmallIntegerWrap(leftExpr, leftType); rightExpr = this.compileExpression(right, leftType, ContextualFlags.IMPLICIT); rightType = this.currentType; } else { rightExpr = this.compileExpressionRetainType(right, leftType); rightType = this.currentType; if (commonType = Type.commonDenominator(leftType, rightType, false)) { leftExpr = this.convertExpression(leftExpr, leftType, leftType = commonType, false, true, // ! left ); rightExpr = this.convertExpression(rightExpr, rightType, rightType = commonType, false, true, // ! 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: // signed div on signed small integers might overflow, e.g. -128/-1 case TypeKind.I16: // ^ 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: { expr = module.createBinary(BinaryOp.DivU32, leftExpr, rightExpr); break; } 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); expr = module.createUnreachable(); } } break; } case Token.PERCENT_EQUALS: compound = true; case Token.PERCENT: { leftExpr = this.compileExpressionRetainType(left, contextualType); leftType = this.currentType; // check operator overload if (this.currentType.is(TypeFlags.REFERENCE)) { let classReference = leftType.classReference; if (classReference) { let overload = classReference.lookupOverload(OperatorKind.REM); if (overload) { expr = this.compileBinaryOverload(overload, left, leftExpr, right, expression); break; } } this.error( DiagnosticCode.Operation_not_supported, expression.range ); return this.module.createUnreachable(); } if (compound) { leftExpr = this.ensureSmallIntegerWrap(leftExpr, leftType); rightExpr = this.compileExpression(right, leftType, ContextualFlags.IMPLICIT); rightType = this.currentType; } else { rightExpr = this.compileExpressionRetainType(right, leftType); rightType = this.currentType; if (commonType = Type.commonDenominator(leftType, rightType, false)) { leftExpr = this.convertExpression(leftExpr, leftType, leftType = commonType, false, true, // ! left ); rightExpr = this.convertExpression(rightExpr, rightType, rightType = commonType, false, true, // ! 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: { expr = module.createBinary(BinaryOp.RemI32, leftExpr, rightExpr); break; } 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.BOOL: { expr = module.createBinary(BinaryOp.RemU32, leftExpr, rightExpr); break; } case TypeKind.U32: { 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.lookupGlobal(CommonSymbols.Mathf); if (!namespace) { this.error( DiagnosticCode.Cannot_find_name_0, expression.range, "Mathf" ); expr = module.createUnreachable(); break; } let prototype = namespace.members ? namespace.members.get(CommonSymbols.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 = this.resolver.resolveFunction(prototype, null); } if (!(instance && this.compileFunction(instance))) { expr = module.createUnreachable(); } else { expr = this.makeCallDirect(instance, [ leftExpr, rightExpr ], expression); } break; } case TypeKind.F64: { let instance = this.f64ModInstance; if (!instance) { let namespace = this.program.lookupGlobal(CommonSymbols.Math); if (!namespace) { this.error( DiagnosticCode.Cannot_find_name_0, expression.range, "Math" ); expr = module.createUnreachable(); break; } let prototype = namespace.members ? namespace.members.get(CommonSymbols.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 = this.resolver.resolveFunction(prototype, null); } if (!(instance && this.compileFunction(instance))) { expr = module.createUnreachable(); } else { expr = this.makeCallDirect(instance, [ leftExpr, rightExpr ], expression); } break; } default: { assert(false); expr = module.createUnreachable(); } } break; } case Token.LESSTHAN_LESSTHAN_EQUALS: compound = true; case Token.LESSTHAN_LESSTHAN: { leftExpr = this.compileExpressionRetainType(left, contextualType.intType); leftType = this.currentType; // check operator overload if (this.currentType.is(TypeFlags.REFERENCE)) { let classReference = leftType.classReference; if (classReference) { let overload = classReference.lookupOverload(OperatorKind.BITWISE_SHL); if (overload) { expr = this.compileBinaryOverload(overload, left, leftExpr, right, expression); break; } } this.error( DiagnosticCode.Operation_not_supported, expression.range ); return this.module.createUnreachable(); } rightExpr = this.compileExpression(right, leftType, ContextualFlags.IMPLICIT); rightType = this.currentType; switch (this.currentType.kind) { case TypeKind.I8: case TypeKind.I16: case TypeKind.U8: case TypeKind.U16: case TypeKind.BOOL: case TypeKind.I32: case TypeKind.U32: { 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: 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(); } default: { assert(false); expr = module.createUnreachable(); } } break; } case Token.GREATERTHAN_GREATERTHAN_EQUALS: compound = true; case Token.GREATERTHAN_GREATERTHAN: { leftExpr = this.compileExpressionRetainType(left, contextualType.intType); leftType = this.currentType; // check operator overload if (this.currentType.is(TypeFlags.REFERENCE)) { let classReference = leftType.classReference; if (classReference) { let overload = classReference.lookupOverload(OperatorKind.BITWISE_SHR); if (overload) { expr = this.compileBinaryOverload(overload, left, leftExpr, right, expression); break; } } this.error( DiagnosticCode.Operation_not_supported, expression.range ); return this.module.createUnreachable(); } leftExpr = this.ensureSmallIntegerWrap(leftExpr, leftType); // must clear garbage bits rightExpr = this.compileExpression(right, leftType, ContextualFlags.IMPLICIT); rightType = this.currentType; switch (this.currentType.kind) { case TypeKind.I8: case TypeKind.I16: { expr = module.createBinary(BinaryOp.ShrI32, leftExpr, rightExpr); break; } case TypeKind.I32: { 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: { expr = module.createBinary(BinaryOp.ShrU32, leftExpr, rightExpr); break; } 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: { 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(); } default: { assert(false); expr = module.createUnreachable(); } } break; } case Token.GREATERTHAN_GREATERTHAN_GREATERTHAN_EQUALS: compound = true; case Token.GREATERTHAN_GREATERTHAN_GREATERTHAN: { leftExpr = this.compileExpressionRetainType(left, contextualType.intType); leftType = this.currentType; // check operator overload if (this.currentType.is(TypeFlags.REFERENCE)) { let classReference = leftType.classReference; if (classReference) { let overload = classReference.lookupOverload(OperatorKind.BITWISE_SHR_U); if (overload) { expr = this.compileBinaryOverload(overload, left, leftExpr, right, expression); break; } } this.error( DiagnosticCode.Operation_not_supported, expression.range ); return this.module.createUnreachable(); } leftExpr = this.ensureSmallIntegerWrap(leftExpr, leftType); // must clear garbage bits rightExpr = this.compileExpression(right, leftType, ContextualFlags.IMPLICIT); rightType = this.currentType; switch (this.currentType.kind) { case TypeKind.U8: case TypeKind.U16: case TypeKind.BOOL: { // assumes that unsigned shr on unsigned small integers does not overflow expr = module.createBinary(BinaryOp.ShrU32, leftExpr, rightExpr); } case TypeKind.I8: case TypeKind.I16: case TypeKind.I32: case TypeKind.U32: { 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: case TypeKind.ISIZE: { 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(); } default: { assert(false); expr = module.createUnreachable(); } } break; } case Token.AMPERSAND_EQUALS: compound = true; case Token.AMPERSAND: { leftExpr = this.compileExpressionRetainType(left, contextualType.intType); leftType = this.currentType; // check operator overloadd if (this.currentType.is(TypeFlags.REFERENCE)) { let classReference = leftType.classReference; if (classReference) { let overload = classReference.lookupOverload(OperatorKind.BITWISE_AND); if (overload) { expr = this.compileBinaryOverload(overload, left, leftExpr, right, expression); break; } } this.error( DiagnosticCode.Operation_not_supported, expression.range ); return this.module.createUnreachable(); } if (compound) { rightExpr = this.compileExpression(right, leftType, ContextualFlags.IMPLICIT); rightType = this.currentType; } else { rightExpr = this.compileExpressionRetainType(right, leftType); rightType = this.currentType; if (commonType = Type.commonDenominator(leftType, rightType, false)) { leftExpr = this.convertExpression(leftExpr, leftType, leftType = commonType, false, false, left ); rightExpr = this.convertExpression(rightExpr, rightType, rightType = commonType, false, false, 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: case TypeKind.U8: case TypeKind.U16: case TypeKind.BOOL: case TypeKind.U32: { 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.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(); } default: { assert(false); expr = module.createUnreachable(); } } break; } case Token.BAR_EQUALS: compound = true; case Token.BAR: { leftExpr = this.compileExpressionRetainType(left, contextualType.intType); leftType = this.currentType; // check operator overload if (this.currentType.is(TypeFlags.REFERENCE)) { let classReference = leftType.classReference; if (classReference) { let overload = classReference.lookupOverload(OperatorKind.BITWISE_OR); if (overload) { expr = this.compileBinaryOverload(overload, left, leftExpr, right, expression); break; } } this.error( DiagnosticCode.Operation_not_supported, expression.range ); return this.module.createUnreachable(); } if (compound) { rightExpr = this.compileExpression(right, leftType, ContextualFlags.IMPLICIT); rightType = this.currentType; } else { rightExpr = this.compileExpressionRetainType(right, leftType); rightType = this.currentType; if (commonType = Type.commonDenominator(leftType, rightType, false)) { leftExpr = this.convertExpression(leftExpr, leftType, leftType = commonType, false, false, left ); rightExpr = this.convertExpression(rightExpr, rightType, rightType = commonType, false, false, 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: { expr = module.createBinary(BinaryOp.OrI32, leftExpr, rightExpr); break; } case TypeKind.I32: case TypeKind.U32: { 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.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(); } default: { assert(false); expr = module.createUnreachable(); } } break; } case Token.CARET_EQUALS: compound = true; case Token.CARET: { leftExpr = this.compileExpressionRetainType(left, contextualType.intType); leftType = this.currentType; // check operator overload if (this.currentType.is(TypeFlags.REFERENCE)) { let classReference = leftType.classReference; if (classReference) { let overload = classReference.lookupOverload(OperatorKind.BITWISE_XOR); if (overload) { expr = this.compileBinaryOverload(overload, left, leftExpr, right, expression); break; } } this.error( DiagnosticCode.Operation_not_supported, expression.range ); return this.module.createUnreachable(); } if (compound) { rightExpr = this.compileExpression(right, leftType, ContextualFlags.IMPLICIT); rightType = this.currentType; } else { rightExpr = this.compileExpressionRetainType(right, leftType); rightType = this.currentType; if (commonType = Type.commonDenominator(leftType, rightType, false)) { leftExpr = this.convertExpression(leftExpr, leftType, leftType = commonType, false, false, left ); rightExpr = this.convertExpression(rightExpr, rightType, rightType = commonType, false, false, 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: { expr = module.createBinary(BinaryOp.XorI32, leftExpr, rightExpr); break; } case TypeKind.I32: case TypeKind.U32: { 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.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(); } default: { assert(false); expr = module.createUnreachable(); } } break; } // logical (no overloading) case Token.AMPERSAND_AMPERSAND: { // left && right -> (t = left) ? right : t let flow = this.currentFlow; leftExpr = this.compileExpressionRetainType(left, contextualType, contextualFlags); leftType = this.currentType; let rightFlow = flow.fork(); this.currentFlow = rightFlow; rightFlow.inheritNonnullIfTrue(leftExpr); rightExpr = this.compileExpression(right, leftType, ContextualFlags.IMPLICIT | (contextualFlags & ~ContextualFlags.WILL_DROP)); rightType = leftType; // simplify if only interested in true or false if (contextualType == Type.bool || contextualType == Type.void) { rightExpr = this.performAutoreleasesWithValue(rightFlow, rightExpr, rightType); rightFlow.freeScopedLocals(); this.currentFlow = flow; this.currentType = Type.bool; expr = module.createIf( this.makeIsTrueish(leftExpr, leftType), this.makeIsTrueish(rightExpr, rightType), module.createI32(0) ); // references must properly retain and release, with the same outcome independent of the branch taken } else if (leftType.isManaged) { let leftAutoreleaseSkipped = this.skippedAutoreleases.has(leftExpr); let rightAutoreleaseSkipped = this.skippedAutoreleases.has(rightExpr); let temp = flow.getTempLocal(leftType); leftExpr = module.createTeeLocal(temp.index, leftExpr); // instead of retaining left and releasing it again in right when right // is taken, we can also just retain left if right is not taken let retainLeftInElse = false; if (leftAutoreleaseSkipped != rightAutoreleaseSkipped) { // xor if (!leftAutoreleaseSkipped) { retainLeftInElse = true; } else { rightExpr = this.makeRetain(rightExpr); rightAutoreleaseSkipped = true; } } else if (!(contextualFlags & ContextualFlags.SKIP_AUTORELEASE)) { // otherwise keep right alive a little longer rightExpr = this.moveAutorelease(rightExpr, rightFlow, flow); } let rightStmts = new Array(); if (leftAutoreleaseSkipped) { // left turned out to be true'ish and is dropped rightStmts.unshift( this.makeRelease( module.createGetLocal(temp.index, leftType.toNativeType()) ) ); } rightExpr = this.performAutoreleasesWithValue(rightFlow, rightExpr, rightType, rightStmts); rightFlow.freeScopedLocals(); this.currentFlow = flow; expr = module.createIf( this.makeIsTrueish(leftExpr, leftType), rightExpr, retainLeftInElse ? this.makeRetain( module.createGetLocal(temp.index, leftType.toNativeType()) ) : module.createGetLocal(temp.index, leftType.toNativeType()) ); if (leftAutoreleaseSkipped || rightAutoreleaseSkipped) this.skippedAutoreleases.add(expr); if (temp) flow.freeTempLocal(temp); // basic values can use more aggressive optimizations } else { rightExpr = this.performAutoreleasesWithValue(rightFlow, rightExpr, rightType); rightFlow.freeScopedLocals(); this.currentFlow = flow; // simplify if cloning left without side effects is possible if (expr = module.cloneExpression(leftExpr, true, 0)) { expr = module.createIf( this.makeIsTrueish(leftExpr, this.currentType), rightExpr, expr ); // if not possible, tee left to a temp } else { let tempLocal = flow.getTempLocal(leftType, !flow.canOverflow(leftExpr, leftType)); expr = module.createIf( this.makeIsTrueish(module.createTeeLocal(tempLocal.index, leftExpr), leftType), rightExpr, module.createGetLocal(tempLocal.index, leftType.toNativeType()) ); } } this.currentType = leftType; break; } case Token.BAR_BAR: { // left || right -> ((t = left) ? t : right) let flow = this.currentFlow; leftExpr = this.compileExpressionRetainType(left, contextualType, contextualFlags); leftType = this.currentType; let rightFlow = flow.fork(); this.currentFlow = rightFlow; rightFlow.inheritNonnullIfFalse(leftExpr); rightExpr = this.compileExpression(right, leftType, ContextualFlags.IMPLICIT | contextualFlags); rightType = leftType; // simplify if only interested in true or false if (contextualType == Type.bool || contextualType == Type.void) { rightExpr = this.performAutoreleasesWithValue(rightFlow, rightExpr, leftType); rightFlow.freeScopedLocals(); this.currentFlow = flow; this.currentType = Type.bool; expr = module.createIf( this.makeIsTrueish(leftExpr, leftType), module.createI32(1), this.makeIsTrueish(rightExpr, rightType) ); // references must properly retain and release, with the same outcome independent of the branch taken } else if (leftType.isManaged) { let leftAutoreleaseSkipped = this.skippedAutoreleases.has(leftExpr); let rightAutoreleaseSkipped = this.skippedAutoreleases.has(rightExpr); let temp = flow.getTempLocal(leftType); leftExpr = module.createTeeLocal(temp.index, leftExpr); // instead of retaining left and releasing it again in right when right // is taken, we can also just retain left if right is not taken let retainLeftInThen = false; if (leftAutoreleaseSkipped != rightAutoreleaseSkipped) { // xor if (!leftAutoreleaseSkipped) { retainLeftInThen = true; } else { rightExpr = this.makeRetain(rightExpr); rightAutoreleaseSkipped = true; } } else if (!(contextualFlags & ContextualFlags.SKIP_AUTORELEASE)) { // otherwise keep right alive a little longer rightExpr = this.moveAutorelease(rightExpr, rightFlow, flow); } let rightStmts = new Array(); if (leftAutoreleaseSkipped) { // left turned out to be false'ish and is dropped // TODO: usually, false'ish means left is null, but this might not hold // once implicit conversion with strings is performed and left is "", so: rightStmts.unshift( this.makeRelease( module.createGetLocal(temp.index, leftType.toNativeType()) ) ); } rightExpr = this.performAutoreleasesWithValue(rightFlow, rightExpr, rightType, rightStmts); rightFlow.freeScopedLocals(); this.currentFlow = flow; expr = module.createIf( this.makeIsTrueish(leftExpr, leftType), retainLeftInThen ? this.makeRetain( module.createGetLocal(temp.index, leftType.toNativeType()) ) : module.createGetLocal(temp.index, leftType.toNativeType()), rightExpr ); if (leftAutoreleaseSkipped || rightAutoreleaseSkipped) this.skippedAutoreleases.add(expr); if (temp) flow.freeTempLocal(temp); // basic values can use more aggressive optimizations } else { rightExpr = this.performAutoreleasesWithValue(rightFlow, rightExpr, rightType); rightFlow.freeScopedLocals(); this.currentFlow = flow; // simplify if cloning left without side effects is possible if (expr = module.cloneExpression(leftExpr, true, 0)) { expr = module.createIf( this.makeIsTrueish(leftExpr, this.currentType), expr, rightExpr ); // if not possible, tee left to a temp. local } else { let tempLocal = flow.getAndFreeTempLocal(this.currentType, !flow.canOverflow(leftExpr, this.currentType), flow.isNonnull(this.currentType, leftExpr) ); expr = module.createIf( this.makeIsTrueish(module.createTeeLocal(tempLocal.index, leftExpr), this.currentType), module.createGetLocal(tempLocal.index, this.currentType.toNativeType()), rightExpr ); } } this.currentType = leftType; break; } default: { assert(false); expr = this.module.createUnreachable(); } } if (!compound) return expr; var resolver = this.resolver; var target = this.resolver.resolveExpression(left, this.currentFlow); if (!target) return module.createUnreachable(); return this.makeAssignment( target, expr, // TODO: delay release above if possible? left, resolver.currentThisExpression, resolver.currentElementExpression, contextualType != Type.void ); } compileUnaryOverload( operatorInstance: Function, value: Expression, valueExpr: ExpressionRef, reportNode: Node ): ExpressionRef { // FIXME: see comment in compileBinaryOverload below why recompiling on type mismatch // is a bad idea currently. so this assumes that the type matches. return this.makeCallDirect(operatorInstance, [ valueExpr ], reportNode, false); } compileBinaryOverload( operatorInstance: Function, left: Expression, leftExpr: ExpressionRef, right: Expression, reportNode: Node ): ExpressionRef { var rightType: Type; if (operatorInstance.is(CommonFlags.INSTANCE)) { let classInstance = assert(operatorInstance.parent); assert(classInstance.kind == ElementKind.CLASS); rightType = operatorInstance.signature.parameterTypes[0]; } else { // FIXME: if LHS type differs we can't recompile left because that'd completely confuse // local states, like having retained locals that actually do not even exist, possibly // releasing something random in that local before and evil things like that. Hence this // assumes that LHS type matches, which in turn means that static overloads must be // guaranteed to never mismatch LHS type, which in turn means that we can't have shiny // things like multiple static overloads for different combinations of LHS/RHS types. // We might want that at some point of course, but requires to complete the resolver so // it can actually resolve every kind of expression without ever having to recompile. rightType = operatorInstance.signature.parameterTypes[1]; } var rightExpr = this.compileExpression(right, rightType, ContextualFlags.IMPLICIT); return this.makeCallDirect(operatorInstance, [ leftExpr, rightExpr ], reportNode); } compileAssignment(expression: Expression, valueExpression: Expression, contextualType: Type): ExpressionRef { var program = this.program; var resolver = program.resolver; var flow = this.currentFlow; var target = resolver.resolveExpression(expression, flow); // reports if (!target) return this.module.createUnreachable(); var thisExpression = resolver.currentThisExpression; var elementExpression = resolver.currentElementExpression; // to compile just the value, we need to know the target's type var targetType: Type; switch (target.kind) { case ElementKind.GLOBAL: { // not yet compiled if a static field compiled as a global if (!this.compileGlobal(target)) return this.module.createUnreachable(); // reports // fall-through } case ElementKind.FIELD: { targetType = (target).type; break; } case ElementKind.LOCAL: { targetType = (target).type; break; } case ElementKind.PROPERTY_PROTOTYPE: { // static property let setterPrototype = (target).setterPrototype; if (!setterPrototype) { 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(); } let setterInstance = this.resolver.resolveFunction(setterPrototype, null, makeMap(), ReportMode.REPORT); if (!setterInstance) return this.module.createUnreachable(); assert(setterInstance.signature.parameterTypes.length == 1); // parser must guarantee this targetType = setterInstance.signature.parameterTypes[0]; break; } case ElementKind.PROPERTY: { // instance property let setterInstance = (target).setterInstance; if (!setterInstance) { 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(); } assert(setterInstance.signature.parameterTypes.length == 1); // parser must guarantee this targetType = setterInstance.signature.parameterTypes[0]; break; } case ElementKind.CLASS: { if (elementExpression) { // indexed access let isUnchecked = flow.is(FlowFlags.UNCHECKED_CONTEXT); // if (isUnchecked) { // let arrayType = this.program.determineBuiltinArrayType(target); // if (arrayType) { // return compileBuiltinArraySet( // this, // target, // assert(this.resolver.currentThisExpression), // elementExpression, // valueExpression, // contextualType // ); // } // } let indexedSet = (target).lookupOverload(OperatorKind.INDEXED_SET, isUnchecked); if (!indexedSet) { let indexedGet = (target).lookupOverload(OperatorKind.INDEXED_GET, isUnchecked); 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 targetType = 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 assert(targetType != Type.void); return this.makeAssignment( target, this.compileExpression(valueExpression, targetType, ContextualFlags.IMPLICIT | ContextualFlags.SKIP_AUTORELEASE), expression, thisExpression, elementExpression, contextualType != Type.void ); } /** Makes an assignment expression or block, assigning a value to a target. */ makeAssignment( /** Target element, e.g. a Local. */ target: Element, /** Value expression that has been compiled in a previous step already. */ valueExpr: ExpressionRef, /** Expression reference. Has already been compiled to `valueExpr`. */ valueExpression: Expression, /** `this` expression reference if a field or property set. */ thisExpression: Expression | null, /** Index expression reference if an indexed set. */ indexExpression: Expression | null, /** Whether to tee the value. */ tee: bool ): ExpressionRef { var module = this.module; var flow = this.currentFlow; // NOTE: Only locals, globals and fields can make use of skipped autoreleases while // everything else must insert the skipped autorelease now. See (*) switch (target.kind) { case ElementKind.LOCAL: { if (target.is(CommonFlags.CONST)) { this.error( DiagnosticCode.Cannot_assign_to_0_because_it_is_a_constant_or_a_read_only_property, valueExpression.range, target.internalName ); this.currentType = tee ? (target).type : Type.void; return module.createUnreachable(); } return this.makeLocalAssignment(target, valueExpr, tee); } case ElementKind.GLOBAL: { if (!this.compileGlobal(target)) return module.createUnreachable(); if (target.is(CommonFlags.CONST)) { this.error( DiagnosticCode.Cannot_assign_to_0_because_it_is_a_constant_or_a_read_only_property, valueExpression.range, target.internalName ); this.currentType = tee ? (target).type : Type.void; return module.createUnreachable(); } return this.makeGlobalAssignment(target, valueExpr, tee); } case ElementKind.FIELD: { let initializerNode = (target).initializerNode; if ( (target).is(CommonFlags.READONLY) && !( flow.actualFunction.is(CommonFlags.CONSTRUCTOR) || initializerNode ) ) { this.error( DiagnosticCode.Cannot_assign_to_0_because_it_is_a_constant_or_a_read_only_property, valueExpression.range, (target).internalName ); return module.createUnreachable(); } return this.makeFieldAssignment(target, valueExpr, this.compileExpressionRetainType( assert(thisExpression), // FIXME: explicit type (currently fails due to missing null checking) this.options.usizeType ), tee ); } case ElementKind.PROPERTY_PROTOTYPE: { // static property if (this.skippedAutoreleases.has(valueExpr)) valueExpr = this.makeAutorelease(valueExpr, flow); // (*) let setterPrototype = (target).setterPrototype; if (!setterPrototype) { this.error( DiagnosticCode.Cannot_assign_to_0_because_it_is_a_constant_or_a_read_only_property, valueExpression.range, target.internalName ); return module.createUnreachable(); } let setterInstance = this.resolver.resolveFunction(setterPrototype, null, makeMap(), ReportMode.REPORT); if (!setterInstance) return module.createUnreachable(); // call just the setter if the return value isn't of interest if (!tee) return this.makeCallDirect(setterInstance, [ valueExpr ], valueExpression); // otherwise call the setter first, then the getter let getterPrototype = assert((target).getterPrototype); // must be present let getterInstance = this.resolver.resolveFunction(getterPrototype, null, makeMap(), ReportMode.REPORT); if (!getterInstance) return module.createUnreachable(); let returnType = getterInstance.signature.returnType; let nativeReturnType = returnType.toNativeType(); return module.createBlock(null, [ this.makeCallDirect(setterInstance, [ valueExpr ], valueExpression), this.makeCallDirect(getterInstance, null, valueExpression) // sets currentType ], nativeReturnType); } case ElementKind.PROPERTY: { // instance property if (this.skippedAutoreleases.has(valueExpr)) valueExpr = this.makeAutorelease(valueExpr, flow); // (*) let setterInstance = (target).setterInstance; if (!setterInstance) { this.error( DiagnosticCode.Cannot_assign_to_0_because_it_is_a_constant_or_a_read_only_property, valueExpression.range, target.internalName ); return module.createUnreachable(); } // call just the setter if the return value isn't of interest if (!tee) { let thisExpr = this.compileExpressionRetainType( assert(thisExpression), this.options.usizeType ); return this.makeCallDirect(setterInstance, [ thisExpr, valueExpr ], valueExpression); } // otherwise call the setter first, then the getter let getterInstance = assert((target).getterInstance); // must be present let returnType = getterInstance.signature.returnType; let nativeReturnType = returnType.toNativeType(); let thisExpr = this.compileExpressionRetainType( assert(thisExpression), this.options.usizeType ); let tempLocal = flow.getAndFreeTempLocal(returnType, false); let tempLocalIndex = tempLocal.index; return module.createBlock(null, [ this.makeCallDirect(setterInstance, [ // set and remember the target module.createTeeLocal(tempLocalIndex, thisExpr), valueExpr ], valueExpression), this.makeCallDirect(getterInstance, [ // get from remembered target module.createGetLocal(tempLocalIndex, nativeReturnType) ], valueExpression) ], nativeReturnType); } case ElementKind.CLASS: { if (this.skippedAutoreleases.has(valueExpr)) valueExpr = this.makeAutorelease(valueExpr, flow); // (*) if (indexExpression) { let isUnchecked = flow.is(FlowFlags.UNCHECKED_CONTEXT); let indexedGet = (target).lookupOverload(OperatorKind.INDEXED_GET, isUnchecked); if (!indexedGet) { this.error( DiagnosticCode.Index_signature_is_missing_in_type_0, valueExpression.range, target.internalName ); return module.createUnreachable(); } let indexedSet = (target).lookupOverload(OperatorKind.INDEXED_SET, isUnchecked); if (!indexedSet) { this.error( DiagnosticCode.Index_signature_in_type_0_only_permits_reading, valueExpression.range, target.internalName ); this.currentType = tee ? indexedGet.signature.returnType : Type.void; return module.createUnreachable(); } let targetType = (target).type; let thisExpr = this.compileExpressionRetainType( assert(thisExpression), this.options.usizeType ); let elementExpr = this.compileExpression(indexExpression, Type.i32, ContextualFlags.IMPLICIT); if (tee) { let tempLocalTarget = flow.getTempLocal(targetType); let tempLocalElement = flow.getAndFreeTempLocal(this.currentType, false); let returnType = indexedGet.signature.returnType; flow.freeTempLocal(tempLocalTarget); return module.createBlock(null, [ this.makeCallDirect(indexedSet, [ module.createTeeLocal(tempLocalTarget.index, thisExpr), module.createTeeLocal(tempLocalElement.index, elementExpr), valueExpr ], valueExpression), this.makeCallDirect(indexedGet, [ module.createGetLocal(tempLocalTarget.index, tempLocalTarget.type.toNativeType()), module.createGetLocal(tempLocalElement.index, tempLocalElement.type.toNativeType()) ], valueExpression) ], returnType.toNativeType()); } else { return this.makeCallDirect(indexedSet, [ thisExpr, elementExpr, valueExpr ], valueExpression); } } // fall-through } } this.error( DiagnosticCode.Operation_not_supported, valueExpression.range ); return module.createUnreachable(); } /** Makes an assignment to a local, possibly retaining and releasing affected references and keeping track of wrap and null states. */ private makeLocalAssignment( /** The local to assign to. */ local: Local, /** The value to assign. */ valueExpr: ExpressionRef, /** Whether to tee the value. */ tee: bool ): ExpressionRef { var type = local.type; assert(type != Type.void); var flow = this.currentFlow; var localIndex = local.index; if (type.is(TypeFlags.NULLABLE)) { if (flow.isNonnull(type, valueExpr)) flow.setLocalFlag(localIndex, LocalFlags.NONNULL); else flow.unsetLocalFlag(localIndex, LocalFlags.NONNULL); } if (type.isManaged) { let module = this.module; let nativeType = type.toNativeType(); if (flow.isAnyLocalFlag(localIndex, LocalFlags.ANY_RETAINED)) { if (this.skippedAutoreleases.has(valueExpr)) { valueExpr = this.makeSkippedRelease( module.createGetLocal(localIndex, nativeType), // oldRef valueExpr // newRef ); if (tee) { // TEE(local = __skippedRelease(local, value)) this.currentType = type; return module.createTeeLocal(localIndex, valueExpr); } else { // local = __skippedRelease(local, value) this.currentType = Type.void; return module.createSetLocal(localIndex, valueExpr); } } else { valueExpr = this.makeRetainRelease( module.createGetLocal(localIndex, nativeType), // oldRef valueExpr // newRef ); if (tee) { // TEE(local = __retainRelease(local, value)) this.currentType = type; return module.createTeeLocal(localIndex, valueExpr); } else { // local = __retainRelease(local, value) this.currentType = Type.void; return module.createSetLocal(localIndex, valueExpr); } } } else { flow.unsetLocalFlag(localIndex, LocalFlags.CONDITIONALLY_RETAINED); flow.setLocalFlag(localIndex, LocalFlags.RETAINED); if (!this.skippedAutoreleases.has(valueExpr)) valueExpr = this.makeRetain(valueExpr); if (tee) { // TEE(local = __retain(value, local)) this.currentType = type; return module.createTeeLocal(localIndex, valueExpr); } else { // local = __retain(value, local) this.currentType = Type.void; return module.createSetLocal(localIndex, valueExpr); } } } else { if (type.is(TypeFlags.SHORT | TypeFlags.INTEGER)) { if (!flow.canOverflow(valueExpr, type)) flow.setLocalFlag(localIndex, LocalFlags.WRAPPED); else flow.unsetLocalFlag(localIndex, LocalFlags.WRAPPED); } if (tee) { // TEE(local = value) this.currentType = type; return this.module.createTeeLocal(localIndex, valueExpr); } else { // local = value this.currentType = Type.void; return this.module.createSetLocal(localIndex, valueExpr); } } } /** Makes an assignment to a global, possibly retaining and releasing affected references. */ private makeGlobalAssignment( /** The global to assign to. */ global: Global, /** The value to assign. */ valueExpr: ExpressionRef, /** Whether to tee the value. */ tee: bool ): ExpressionRef { var module = this.module; var type = global.type; assert(type != Type.void); var nativeType = type.toNativeType(); if (type.isManaged) { if (this.skippedAutoreleases.has(valueExpr)) { if (tee) { // (global = __skippedRelease(global, value)), global this.currentType = type; return module.createBlock(null, [ module.createSetGlobal(global.internalName, this.makeSkippedRelease( module.createGetGlobal(global.internalName, nativeType), // oldRef valueExpr // newRef ) ), module.createGetGlobal(global.internalName, nativeType) ], nativeType); } else { // global = __skippedRelease(global, value) this.currentType = Type.void; return module.createSetGlobal(global.internalName, this.makeSkippedRelease( module.createGetGlobal(global.internalName, nativeType), // oldRef valueExpr // newRef ) ); } } else { if (tee) { // (global = __retainRelease(global, value)), global this.currentType = type; return module.createBlock(null, [ module.createSetGlobal(global.internalName, this.makeRetainRelease( module.createGetGlobal(global.internalName, nativeType), // oldRef valueExpr // newRef ) ), module.createGetGlobal(global.internalName, nativeType) ], nativeType); } else { // global = __retainRelease(global, value) this.currentType = Type.void; return module.createSetGlobal(global.internalName, this.makeRetainRelease( module.createGetGlobal(global.internalName, nativeType), // oldRef valueExpr // newRef ) ); } } } else { valueExpr = this.ensureSmallIntegerWrap(valueExpr, type); // globals must be wrapped if (tee) { // (global = value), global this.currentType = type; return module.createBlock(null, [ module.createSetGlobal(global.internalName, valueExpr), module.createGetGlobal(global.internalName, nativeType) ], nativeType); } else { // global = value this.currentType = Type.void; return module.createSetGlobal(global.internalName, valueExpr ); } } } /** Makes an assignment to a field, possibly retaining and releasing affected references. */ makeFieldAssignment( /** The field to assign to. */ field: Field, /** The value to assign. */ valueExpr: ExpressionRef, /** The value of `this`. */ thisExpr: ExpressionRef, /** Whether to tee the value. */ tee: bool ): ExpressionRef { var module = this.module; var flow = this.currentFlow; var fieldType = field.type; var nativeFieldType = fieldType.toNativeType(); assert(field.parent.kind == ElementKind.CLASS); var thisType = (field.parent).type; var nativeThisType = thisType.toNativeType(); if (fieldType.isManaged && thisType.isManaged) { let tempThis = flow.getTempLocal(thisType); if (this.skippedAutoreleases.has(valueExpr)) { if (tee) { // ((t1 = this).field = __skippedRelease(t1.field, t2 = value)), t2 let tempValue = flow.getAndFreeTempLocal(fieldType, !flow.canOverflow(valueExpr, fieldType)); flow.freeTempLocal(tempThis); this.currentType = fieldType; return module.createBlock(null, [ module.createStore(fieldType.byteSize, module.createTeeLocal(tempThis.index, thisExpr), this.makeSkippedRelease( module.createLoad(fieldType.byteSize, fieldType.is(TypeFlags.SIGNED), // oldRef module.createGetLocal(tempThis.index, nativeThisType), nativeFieldType, field.memoryOffset ), module.createTeeLocal(tempValue.index, valueExpr), // newRef ), nativeFieldType, field.memoryOffset ), module.createGetLocal(tempValue.index, nativeFieldType) ], nativeFieldType); } else { // (t1 = this).field = __skippedRelease(t1.field, value) flow.freeTempLocal(tempThis); this.currentType = Type.void; return module.createStore(fieldType.byteSize, module.createTeeLocal(tempThis.index, thisExpr), this.makeSkippedRelease( module.createLoad(fieldType.byteSize, fieldType.is(TypeFlags.SIGNED), // oldRef module.createGetLocal(tempThis.index, nativeThisType), nativeFieldType, field.memoryOffset ), valueExpr, // newRef ), nativeFieldType, field.memoryOffset ); } } else { if (tee) { // ((t1 = this).field = __retainRelease(t1.field, t2 = value)), t2 let tempValue = flow.getAndFreeTempLocal(fieldType, !flow.canOverflow(valueExpr, fieldType)); flow.freeTempLocal(tempThis); this.currentType = fieldType; return module.createBlock(null, [ module.createStore(fieldType.byteSize, module.createTeeLocal(tempThis.index, thisExpr), this.makeRetainRelease( module.createLoad(fieldType.byteSize, fieldType.is(TypeFlags.SIGNED), // oldRef module.createGetLocal(tempThis.index, nativeThisType), nativeFieldType, field.memoryOffset ), module.createTeeLocal(tempValue.index, valueExpr) // newRef ), nativeFieldType, field.memoryOffset ), module.createGetLocal(tempValue.index, nativeFieldType) ], nativeFieldType); } else { // (t1 = this).field = __retainRelease(t1.field, value) flow.freeTempLocal(tempThis); this.currentType = Type.void; return module.createStore(fieldType.byteSize, module.createTeeLocal(tempThis.index, thisExpr), this.makeRetainRelease( module.createLoad(fieldType.byteSize, fieldType.is(TypeFlags.SIGNED), // oldRef module.createGetLocal(tempThis.index, nativeThisType), nativeFieldType, field.memoryOffset ), valueExpr // newRef ), nativeFieldType, field.memoryOffset ); } } } else { if (tee) { // (this.field = (t1 = value)), t1 let tempValue = flow.getAndFreeTempLocal(fieldType, !flow.canOverflow(valueExpr, fieldType)); this.currentType = fieldType; return module.createBlock(null, [ module.createStore(fieldType.byteSize, thisExpr, module.createTeeLocal(tempValue.index, valueExpr), nativeFieldType, field.memoryOffset ), module.createGetLocal(tempValue.index, nativeFieldType) ], nativeFieldType); } else { // this.field = value this.currentType = Type.void; return module.createStore(fieldType.byteSize, thisExpr, valueExpr, nativeFieldType, field.memoryOffset ); } } } /** Compiles a call expression according to the specified context. */ compileCallExpression( /** Call expression to compile. */ expression: CallExpression, /** Contextual type indicating the return type the caller expects, if any. */ contextualType: Type, /** Contextual flags indicating contextual conditions. */ contextualFlags: ContextualFlags ): ExpressionRef { var module = this.module; var flow = this.currentFlow; // handle call to super if (expression.expression.kind == NodeKind.SUPER) { let flow = this.currentFlow; let actualFunction = flow.actualFunction; if (!actualFunction.is(CommonFlags.CONSTRUCTOR)) { this.error( DiagnosticCode.Super_calls_are_not_permitted_outside_constructors_or_in_nested_functions_inside_constructors, expression.range ); return module.createUnreachable(); } let classInstance = assert(actualFunction.parent); assert(classInstance.kind == ElementKind.CLASS); let baseClassInstance = assert((classInstance).base); let thisLocal = assert(flow.lookupLocal(CommonSymbols.this_)); let nativeSizeType = this.options.nativeSizeType; // { // this = super(this || , ...args) // this.a = X // this.b = Y // } let stmts: ExpressionRef[] = [ module.createSetLocal(thisLocal.index, this.compileCallDirect( this.ensureConstructor(baseClassInstance, expression), expression.arguments, expression, module.createIf( module.createGetLocal(thisLocal.index, nativeSizeType), module.createGetLocal(thisLocal.index, nativeSizeType), this.makeRetain( this.makeAllocation(classInstance) ) ) ) ) ]; this.makeFieldInitialization(classInstance, stmts); // check that super had been called before accessing allocating `this` if (flow.isAny( FlowFlags.ALLOCATES | FlowFlags.CONDITIONALLY_ALLOCATES )) { this.error( DiagnosticCode._super_must_be_called_before_accessing_this_in_the_constructor_of_a_derived_class, expression.range ); return module.createUnreachable(); } flow.set(FlowFlags.ALLOCATES | FlowFlags.CALLS_SUPER); this.currentType = Type.void; return module.createBlock(null, stmts); } // otherwise resolve normally var target = this.resolver.resolveExpression(expression.expression, flow); // 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.hasDecorator(DecoratorFlags.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 = this.resolver.resolveFunctionInclTypeArguments( prototype, typeArguments, flow.actualFunction.parent, // relative to caller makeMap(flow.contextualTypeArguments), expression ); // infer generic call if type arguments have been omitted } else if (prototype.is(CommonFlags.GENERIC)) { let inferredTypes = new Map(); let typeParameterNodes = assert(prototype.typeParameterNodes); let numTypeParameters = typeParameterNodes.length; for (let i = 0; i < numTypeParameters; ++i) { inferredTypes.set(typeParameterNodes[i].name.text, null); } // let numInferred = 0; let parameterNodes = prototype.signatureNode.parameters; let numParameters = parameterNodes.length; let argumentNodes = expression.arguments; let numArguments = argumentNodes.length; let argumentExprs = new Array(numArguments); for (let i = 0; i < numParameters; ++i) { let typeNode = parameterNodes[i].type; let templateName = typeNode.kind == NodeKind.TYPE && !(typeNode).name.next ? (typeNode).name.identifier.text : null; let argumentExpression = i < numArguments ? argumentNodes[i] : parameterNodes[i].initializer; if (!argumentExpression) { // missing initializer -> too few arguments this.error( DiagnosticCode.Expected_0_arguments_but_got_1, expression.range, numParameters.toString(10), numArguments.toString(10) ); return module.createUnreachable(); } if (templateName !== null && inferredTypes.has(templateName)) { let inferredType = inferredTypes.get(templateName); if (inferredType) { argumentExprs[i] = this.compileExpressionRetainType(argumentExpression, inferredType); let commonType: Type | null; if (!(commonType = Type.commonDenominator(inferredType, this.currentType, true))) { if (!(commonType = Type.commonDenominator(inferredType, this.currentType, false))) { this.error( DiagnosticCode.Type_0_is_not_assignable_to_type_1, parameterNodes[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(templateName, inferredType); } else { let concreteType = this.resolver.resolveType( parameterNodes[i].type, flow.actualFunction, flow.contextualTypeArguments ); if (!concreteType) return module.createUnreachable(); argumentExprs[i] = this.compileExpression(argumentExpression, concreteType, ContextualFlags.IMPLICIT); } } let resolvedTypeArguments = new Array(numTypeParameters); for (let i = 0; i < numTypeParameters; ++i) { let inferredType = assert(inferredTypes.get(typeParameterNodes[i].name.text)); // TODO resolvedTypeArguments[i] = inferredType; } instance = this.resolver.resolveFunction( prototype, resolvedTypeArguments, makeMap(flow.contextualTypeArguments) ); if (!instance) return this.module.createUnreachable(); return this.makeCallDirect(instance, argumentExprs, expression, contextualType == Type.void); // 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 = this.resolver.resolveFunction(prototype, null); } 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.resolver.currentThisExpression), this.options.usizeType ); } return this.compileCallDirect( instance, expression.arguments, expression, thisExpr, false, contextualFlags ); } // indirect call: index argument with signature (non-generic, can't be inlined) case ElementKind.LOCAL: { if (signature = (target).type.signatureReference) { if ((target).is(CommonFlags.INLINED)) { indexArg = module.createI32(i64_low((target).constantIntegerValue)); } else { 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.resolver.currentThisExpression); 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, type.toString() ); return module.createUnreachable(); } } case ElementKind.FUNCTION_TARGET: { signature = (target).signature; indexArg = this.compileExpression(expression.expression, (target).type, ContextualFlags.IMPLICIT); break; } case ElementKind.PROPERTY_PROTOTYPE: { // static property let getterPrototype = assert((target).getterPrototype); let getterInstance = this.resolver.resolveFunction(getterPrototype, null); if (!getterInstance) return module.createUnreachable(); indexArg = this.compileCallDirect(getterInstance, [], expression.expression); signature = this.currentType.signatureReference; if (!signature) { this.error( DiagnosticCode.Cannot_invoke_an_expression_whose_type_lacks_a_call_signature_Type_0_has_no_compatible_call_signatures, expression.range, this.currentType.toString() ); return module.createUnreachable(); } break; } case ElementKind.PROPERTY: { // instance property let getterInstance = assert((target).getterInstance); indexArg = this.compileCallDirect(getterInstance, [], expression.expression, this.compileExpressionRetainType( assert(this.resolver.currentThisExpression), this.options.usizeType ) ); signature = this.currentType.signatureReference; if (!signature) { this.error( DiagnosticCode.Cannot_invoke_an_expression_whose_type_lacks_a_call_signature_Type_0_has_no_compatible_call_signatures, expression.range, this.currentType.toString() ); return module.createUnreachable(); } break; } // not supported default: { this.error( DiagnosticCode.Operation_not_supported, expression.range ); return module.createUnreachable(); } } return this.compileCallIndirect( signature, indexArg, expression.arguments, expression, 0, contextualType == Type.void ); } private compileCallExpressionBuiltin( prototype: FunctionPrototype, expression: CallExpression, contextualType: Type ): ExpressionRef { var typeArguments: Type[] | null = null; // builtins handle omitted type arguments on their own. if present, however, resolve them here // and pass them to the builtin, even if it's still up to the builtin how to handle them. var typeParameterNodes = prototype.typeParameterNodes; var typeArgumentNodes = expression.typeArguments; if (expression.typeArguments) { if (!prototype.is(CommonFlags.GENERIC)) { this.error( DiagnosticCode.Type_0_is_not_generic, expression.range, prototype.internalName ); } typeArguments = this.resolver.resolveTypeArguments( assert(typeParameterNodes), typeArgumentNodes, this.currentFlow.actualFunction.parent, makeMap(this.currentFlow.contextualTypeArguments), expression ); } // now compile the builtin, which usually returns a block of code that replaces the call. var expr = compileBuiltinCall( this, prototype, typeArguments, 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, inlineCanAlias: bool = false, contextualFlags: ContextualFlags = ContextualFlags.NONE ): ExpressionRef { var numArguments = argumentExpressions.length; var signature = instance.signature; if (!this.checkCallSignature( // reports signature, numArguments, thisArg != 0, reportNode )) { this.currentType = signature.returnType; return this.module.createUnreachable(); } // Inline if explicitly requested if (instance.hasDecorator(DecoratorFlags.INLINE)) { assert(!instance.is(CommonFlags.TRAMPOLINE)); // doesn't make sense if (this.currentInlineFunctions.includes(instance)) { this.warning( DiagnosticCode.Function_0_cannot_be_inlined_into_itself, reportNode.range, instance.internalName ); } else { this.currentInlineFunctions.push(instance); let expr = this.compileCallInlinePrechecked(instance, argumentExpressions, thisArg, inlineCanAlias, (contextualFlags & ContextualFlags.WILL_DROP) != 0); this.currentInlineFunctions.pop(); return expr; } } // 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], ContextualFlags.IMPLICIT ); } assert(index == numArgumentsInclThis); if (signature.returnType.isManaged) { if (contextualFlags & ContextualFlags.WILL_DROP) { // Skip autorelease and simply release return this.makeCallDirect(instance, operands, reportNode, true); } else if (contextualFlags & ContextualFlags.SKIP_AUTORELEASE) { // Skip autorelease and remember let expr = this.makeCallDirect(instance, operands, reportNode, false, true); this.skippedAutoreleases.add(expr); return expr; } else { return this.makeCallDirect(instance, operands, reportNode, false, false); } } return this.makeCallDirect(instance, operands, reportNode, (contextualFlags & ContextualFlags.WILL_DROP) != 0 ); } compileCallInline( instance: Function, argumentExpressions: Expression[], thisArg: ExpressionRef, reportNode: Node, canAlias: bool = false ): ExpressionRef { var numArguments = argumentExpressions.length; var signature = instance.signature; if (!this.checkCallSignature( // reports signature, numArguments, thisArg != 0, reportNode )) { this.currentType = instance.signature.returnType; return this.module.createUnreachable(); } return this.compileCallInlinePrechecked(instance, argumentExpressions, thisArg, canAlias); } private compileCallInlinePrechecked( instance: Function, argumentExpressions: Expression[], thisArg: ExpressionRef = 0, canAlias: bool = false, immediatelyDropped: bool = false ): ExpressionRef { var numArguments = argumentExpressions.length; var signature = instance.signature; var parameterTypes = signature.parameterTypes; assert(numArguments <= parameterTypes.length); var args = new Array(numArguments); var flow = this.currentFlow; // compile arguments possibly using their own temp locals var temps = flow.blockLocalsBeforeInlining(instance); for (let i = 0; i < numArguments; ++i) { args[i] = this.compileExpression(argumentExpressions[i], parameterTypes[i], ContextualFlags.IMPLICIT ); } flow.unblockLocals(temps); return this.makeCallInlinePrechecked(instance, args, thisArg, canAlias, immediatelyDropped); } makeCallInlinePrechecked( instance: Function, args: ExpressionRef[], thisArg: ExpressionRef = 0, canAlias: bool = false, immediatelyDropped: bool = false ): ExpressionRef { // CAUTION: Imagine a call like `theCall(a, b)`. Unless canAlias, inlining needs a temporary local for // each argument, looking something like `BLOCK { t1 = a, t2 = b, inlinedTheCall }`. Now, if argument b, // which is compiled beforehand, itself required a temporary local, it is likely that it did pick `t1` // for this, making it something like `BLOCK { t1 = a, t2 = (t1 = c, t1), inlinedTheCall }`, which is // overwriting t1. Hence, whenever makeCallInline is used, this condition must be taken into account. // Flows provide the helpers Flow#blockLocalsBeforeInlining and Flow#unblockLocals for this. var module = this.module; // Create a new inline flow and use it to compile the function as a block var previousFlow = this.currentFlow; var flow = Flow.createInline(previousFlow.parentFunction, instance); var body = []; // Convert provided call arguments to temporary locals if (thisArg) { let classInstance = assert(instance.parent); assert(classInstance.kind == ElementKind.CLASS); let thisType = assert(instance.signature.thisType); if (canAlias && getExpressionId(thisArg) == ExpressionId.GetLocal) { flow.addScopedAlias(CommonSymbols.this_, thisType, getGetLocalIndex(thisArg)); let baseInstance = (classInstance).base; if (baseInstance) flow.addScopedAlias(CommonSymbols.super_, baseInstance.type, getGetLocalIndex(thisArg)); } else { let thisLocal = flow.addScopedLocal(CommonSymbols.this_, thisType, false); // No need to retain `this` as it can't be reassigned and thus can't become prematurely released body.push( module.createSetLocal(thisLocal.index, thisArg) ); let baseInstance = (classInstance).base; if (baseInstance) flow.addScopedAlias(CommonSymbols.super_, baseInstance.type, thisLocal.index); } } else { assert(!instance.signature.thisType); } var numArguments = args.length; var signature = instance.signature; var parameterTypes = signature.parameterTypes; for (let i = 0; i < numArguments; ++i) { let paramExpr = args[i]; let paramType = parameterTypes[i]; if (canAlias && getExpressionId(paramExpr) == ExpressionId.GetLocal) { flow.addScopedAlias(signature.getParameterName(i), paramType, getGetLocalIndex(paramExpr)); } else { let argumentLocal = flow.addScopedLocal( signature.getParameterName(i), paramType, !previousFlow.canOverflow(paramExpr, paramType) ); if (paramType.isManaged) { flow.setLocalFlag(argumentLocal.index, LocalFlags.RETAINED); body.push( module.createSetLocal(argumentLocal.index, this.makeRetain(paramExpr) ) ); } else { body.push( module.createSetLocal(argumentLocal.index, paramExpr) ); } } } // Compile optional parameter initializers in the scope of the inlined flow this.currentFlow = flow; var numParameters = signature.parameterTypes.length; for (let i = numArguments; i < numParameters; ++i) { let initType = parameterTypes[i]; let initExpr = this.compileExpression( assert(instance.prototype.signatureNode.parameters[i].initializer), initType, ContextualFlags.IMPLICIT ); if (canAlias && getExpressionId(initExpr) == ExpressionId.GetLocal) { flow.addScopedAlias(signature.getParameterName(i), initType, getGetLocalIndex(initExpr)); } else { let argumentLocal = flow.addScopedLocal( signature.getParameterName(i), initType, !flow.canOverflow(initExpr, initType) ); if (initType.isManaged) { flow.setLocalFlag(argumentLocal.index, LocalFlags.RETAINED); body.push( module.createSetLocal(argumentLocal.index, this.makeRetain(initExpr) ) ); } else { body.push( module.createSetLocal(argumentLocal.index, initExpr) ); } } } // Compile the called function's body in the scope of the inlined flow this.compileFunctionBody(instance, body); // Free any new scoped locals and reset to the original flow if (!flow.isAny(FlowFlags.ANY_TERMINATING)) { this.performAutoreleases(flow, body); this.finishAutoreleases(flow, body); } flow.freeScopedLocals(); var returnType = flow.returnType; this.currentFlow = previousFlow; // Create an outer block that we can break to when returning a value out of order var expr = module.createBlock(flow.inlineReturnLabel, body, returnType.toNativeType()); this.currentType = returnType; if (returnType.isManaged) { if (immediatelyDropped) { expr = this.makeRelease(expr); this.currentType = Type.void; } else { expr = this.makeAutorelease(expr); } } return expr; } /** 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.signatureNode.parameters; 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); trampolineSignature.requiredParameters = maxArguments; trampolineSignature.parameterNames = originalSignature.parameterNames; trampoline = new Function( original.name + "|trampoline", original.prototype, trampolineSignature, 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 previousFlow = this.currentFlow; this.currentFlow = trampoline.flow; // 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("outOfRange", [ module.createSwitch(names, "outOfRange", // condition is number of provided optional arguments, so subtract required arguments minArguments ? module.createBinary( BinaryOp.SubI32, module.createGetGlobal(BuiltinSymbols.argc, NativeType.I32), module.createI32(minArguments) ) : module.createGetGlobal(BuiltinSymbols.argc, NativeType.I32) ) ]), module.createUnreachable() ]); for (let i = 0; i < numOptional; ++i, ++operandIndex) { let type = originalParameterTypes[minArguments + i]; let declaration = originalParameterDeclarations[minArguments + i]; let initializer = declaration.initializer; let initExpr: ExpressionRef; if (initializer) { initExpr = module.createSetLocal(operandIndex, this.compileExpression( initializer, type, ContextualFlags.IMPLICIT ) ); } else { this.error( DiagnosticCode.Optional_parameter_must_have_an_initializer, declaration.range ); initExpr = module.createUnreachable(); } body = module.createBlock(names[i + 1], [ body, initExpr, ]); forwardedOperands[operandIndex] = module.createGetLocal(operandIndex, type.toNativeType()); } this.currentFlow = previousFlow; assert(operandIndex == maxOperands); var funcRef = module.addFunction( trampoline.internalName, 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 { if (!this.argcVar) { let module = this.module; this.argcVar = module.addGlobal( BuiltinSymbols.argc, NativeType.I32, true, module.createI32(0) ); } return BuiltinSymbols.argc; } /** Makes sure that the argument count helper setter is present and returns its name. */ private ensureArgcSet(): string { if (!this.argcSet) { let module = this.module; this.argcSet = module.addFunction(BuiltinSymbols.setargc, this.ensureFunctionType([ Type.u32 ], Type.void), null, module.createSetGlobal(this.ensureArgcVar(), module.createGetLocal(0, NativeType.I32) ) ); module.addFunctionExport(BuiltinSymbols.setargc, "__setargc"); } return BuiltinSymbols.setargc; } // /** Makes retain call, retaining the expression's value. */ makeRetain(expr: ExpressionRef): ExpressionRef { var retainInstance = this.program.retainInstance; this.compileFunction(retainInstance); return this.module.createCall(retainInstance.internalName, [ expr ], this.options.nativeSizeType); } /** Makes a retainRelease call, retaining the new expression's value and releasing the old expression's value, in this order. */ makeRetainRelease(oldExpr: ExpressionRef, newExpr: ExpressionRef): ExpressionRef { // FIXME: this is a workaround, see https://github.com/WebAssembly/binaryen/issues/2135 var retainReleaseInstance = this.program.retainReleaseInstance; this.compileFunction(retainReleaseInstance); return this.module.createCall(retainReleaseInstance.internalName, [ oldExpr, newExpr ], this.options.nativeSizeType); } /** Makes a skippedRelease call, ignoring the new expression's value and releasing the old expression's value, in this order. */ makeSkippedRelease(oldExpr: ExpressionRef, newExpr: ExpressionRef): ExpressionRef { // FIXME: this is a workaround, see https://github.com/WebAssembly/binaryen/issues/2135 var skippedReleaseInstance = this.program.skippedReleaseInstance; this.compileFunction(skippedReleaseInstance); return this.module.createCall(skippedReleaseInstance.internalName, [ oldExpr, newExpr ], this.options.nativeSizeType); } /** Makes a release call, releasing the expression's value. Changes the current type to void.*/ makeRelease(expr: ExpressionRef): ExpressionRef { var releaseInstance = this.program.releaseInstance; this.compileFunction(releaseInstance); return this.module.createCall(releaseInstance.internalName, [ expr ], NativeType.None); } /** Makes an automatic release call at the end of the current flow. */ makeAutorelease(expr: ExpressionRef, flow: Flow = this.currentFlow): ExpressionRef { return this.module.createTeeLocal(flow.getAutoreleaseLocal(this.options.usizeType).index, expr); } /** Attempts to undo a final autorelease, returning the index of the previously retaining variable or -1 if not possible. */ undoAutorelease(expr: ExpressionRef, flow: Flow): i32 { // NOTE: Can't remove the local.tee completely because it's already compiled // and a child of something else. Preventing the final release however should // make it optimize away. switch (getExpressionId(expr)) { case ExpressionId.SetLocal: { // local.tee(__retain(expr)) if (isTeeLocal(expr)) { let index = getSetLocalIndex(expr); if (flow.isAnyLocalFlag(index, LocalFlags.ANY_RETAINED)) { // Assumes that the expression actually belongs to the flow and that // top-level autoreleases are never undone. While that's true, it's // not necessary to check presence in scopedLocals. flow.unsetLocalFlag(index, LocalFlags.ANY_RETAINED); return index; } } break; } case ExpressionId.Block: { // { ..., local.tee(__retain(expr)) } if (getBlockName(expr) === null) { // must not be a break target let count = getBlockChildCount(expr); if (count) { return this.undoAutorelease(getBlockChild(expr, count - 1), flow); } } break; } } return -1; } /** * Attemps to move a final autorelease from one flow to a parent. * It is crucial that from flow hasn't processed autoreleases yet because otherwise the final * retain would have been written already. */ moveAutorelease(expr: ExpressionRef, fromInnerFlow: Flow, toOuterFlow: Flow): ExpressionRef { // NOTE: While this avoids an unnecessary set of retain and release, it cannot // eliminate the now unnecessary temporary local.tee because it is already part of // another expression in the tree. But optimizing should collapse locals again. var index = this.undoAutorelease(expr, fromInnerFlow); return ~index ? this.makeAutorelease(expr, toOuterFlow) // undone, can skip the retain : this.makeAutorelease(this.makeRetain(expr), toOuterFlow); } /** Performs any queued autoreleases in the specified flow. */ performAutoreleases(flow: Flow, stmts: ExpressionRef[], clearFlags: bool = true): void { var scopedLocals = flow.scopedLocals; if (scopedLocals) { let module = this.module; for (let local of scopedLocals.values()) { if (local.is(CommonFlags.SCOPED)) { // otherwise an alias let localIndex = local.index; if (flow.isAnyLocalFlag(localIndex, LocalFlags.ANY_RETAINED)) { if (clearFlags) flow.unsetLocalFlag(localIndex, LocalFlags.ANY_RETAINED); stmts.push( this.makeRelease( module.createGetLocal(localIndex, local.type.toNativeType()) ) ); } } } } } /** Performs any queued autoreleases in the specified flow and returns the value. */ performAutoreleasesWithValue( flow: Flow, valueExpr: ExpressionRef, valueType: Type, stmts: ExpressionRef[] | null = null, clearFlags: bool = true ): ExpressionRef { if (!stmts) stmts = new Array(); stmts.push( this.module.createNop() ); var lengthBefore = stmts.length; this.performAutoreleases(flow, stmts, clearFlags); if (stmts.length > lengthBefore) { let nativeType = valueType.toNativeType(); let temp = flow.getAndFreeTempLocal(valueType, !flow.canOverflow(valueExpr, valueType), flow.isNonnull(valueType, valueExpr)); let module = this.module; stmts[lengthBefore - 1] = module.createSetLocal(temp.index, valueExpr); // nop -> set stmts.push( module.createGetLocal(temp.index, nativeType) // append get ); return module.createBlock(null, stmts, nativeType); } else if (stmts.length > 1) { stmts[lengthBefore - 1] = valueExpr; // nop -> value return this.module.createBlock(null, stmts, valueType.toNativeType()); } return valueExpr; } /** Finishes any queued top-level autoreleases in the actual function of the specified flow. */ finishAutoreleases(flow: Flow, stmts: ExpressionRef[]): void { var module = this.module; if (flow.is(FlowFlags.INLINE_CONTEXT)) { // traverse to the top-most flow containing the inlined function's // locals as scoped locals and release these instead. let parent: Flow | null; while (parent = flow.parent) flow = parent; this.performAutoreleases(flow, stmts, false); } else { for (let local of flow.parentFunction.localsByIndex) { let localIndex = local.index; if (flow.isAnyLocalFlag(localIndex, LocalFlags.ANY_RETAINED)) { flow.unsetLocalFlag(localIndex, LocalFlags.ANY_RETAINED); stmts.push( this.makeRelease( module.createGetLocal(localIndex, local.type.toNativeType()) ) ); } } } } // /** Creates a direct call to the specified function. */ makeCallDirect( instance: Function, operands: ExpressionRef[] | null, reportNode: Node, immediatelyDropped: bool = false, /** Skip the usual autorelease and manage this at the callsite instead. */ skipAutorelease: bool = false ): ExpressionRef { if (instance.hasDecorator(DecoratorFlags.INLINE)) { this.warning( DiagnosticCode.TODO_Cannot_inline_inferred_calls_and_specific_internals_yet, reportNode.range, instance.internalName ); } 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 their initializers, if constant, otherwise with zeroes. if (numOperands < maxOperands) { if (!operands) { operands = new Array(maxOperands); operands.length = 0; } let parameterTypes = instance.signature.parameterTypes; let parameterNodes = instance.prototype.signatureNode.parameters; assert(parameterNodes.length == parameterTypes.length); let allOptionalsAreConstant = true; for (let i = numArguments; i < maxArguments; ++i) { let initializer = parameterNodes[i].initializer; if (initializer) { let resolved: Element | null; if ( nodeIsConstantValue(initializer.kind) || ( (resolved = this.resolver.resolveExpression(initializer, instance.flow, parameterTypes[i])) && ( resolved.kind == ElementKind.GLOBAL // resolved.kind == ElementKind.FUNCTION_TARGET ) ) ) { // inline into the call let previousFlow = this.currentFlow; this.currentFlow = instance.flow; operands.push(this.compileExpression( parameterNodes[i].initializer, parameterTypes[i], ContextualFlags.IMPLICIT )); this.currentFlow = previousFlow; continue; } } operands.push(parameterTypes[i].toNativeZero(module)); allOptionalsAreConstant = false; } if (!allOptionalsAreConstant) { if (!isCallImport) { let original = instance; instance = this.ensureTrampoline(instance); if (!this.compileFunction(instance)) return module.createUnreachable(); instance.flow.flags = original.flow.flags; let nativeReturnType = returnType.toNativeType(); let expr = module.createCall(instance.internalName, operands, nativeReturnType); this.currentType = returnType; if (returnType.isManaged) { if (immediatelyDropped) { expr = this.makeRelease(expr); this.currentType = Type.void; } else if (!skipAutorelease) { expr = this.makeAutorelease(expr); } else { this.skippedAutoreleases.add(expr); } } return module.createBlock(null, [ module.createSetGlobal(this.ensureArgcVar(), module.createI32(numArguments)), expr ], this.currentType.toNativeType()); } } } // If the return value is of a reference type it has not yet been released but is in flight // which is equivalent to a skipped autorelease. Hence, insert either a release if it is // dropped anyway, preserve the skipped autorelease if explicitly requested or autorelease now. var expr = module.createCall(instance.internalName, operands, returnType.toNativeType()); this.currentType = returnType; if (returnType.isManaged) { if (immediatelyDropped) { expr = this.makeRelease(expr); this.currentType = Type.void; } else if (!skipAutorelease) { expr = this.makeAutorelease(expr); } } return expr; } /** Compiles an indirect call using an index argument and a signature. */ compileCallIndirect( signature: Signature, indexArg: ExpressionRef, argumentExpressions: Expression[], reportNode: Node, thisArg: ExpressionRef = 0, immediatelyDropped: bool = false ): 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], ContextualFlags.IMPLICIT ); } assert(index == numArgumentsInclThis); return this.makeCallIndirect(signature, indexArg, operands, immediatelyDropped); } /** Creates an indirect call to the function at `indexArg` in the function table. */ makeCallIndirect( signature: Signature, indexArg: ExpressionRef, operands: ExpressionRef[] | null = null, immediatelyDropped: bool = false ): 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; var expr = module.createCallIndirect(indexArg, operands, signature.toSignatureString()); this.currentType = returnType; if (returnType.isManaged) { if (immediatelyDropped) { expr = this.makeRelease(expr); this.currentType = Type.void; } else { expr = this.makeAutorelease(expr); } } return module.createBlock(null, [ module.createSetGlobal(this.ensureArgcVar(), // might be calling a trampoline module.createI32(numArguments) ), expr ], this.currentType.toNativeType()); // not necessarily wrapped } compileCommaExpression( expression: CommaExpression, contextualType: Type, contextualFlags: ContextualFlags ): 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 except last ContextualFlags.IMPLICIT | ContextualFlags.WILL_DROP ); } exprs[numExpressions] = this.compileExpression(expressions[numExpressions], contextualType, contextualFlags); return this.module.createBlock(null, exprs, this.currentType.toNativeType()); } compileElementAccessExpression( expression: ElementAccessExpression, contextualType: Type, contextualFlags: ContextualFlags ): ExpressionRef { var target = this.resolver.resolveElementAccessExpression( expression, this.currentFlow, contextualType ); // reports if (!target) return this.module.createUnreachable(); switch (target.kind) { case ElementKind.CLASS: { let indexedGet = (target).lookupOverload(OperatorKind.INDEXED_GET, this.currentFlow.is(FlowFlags.UNCHECKED_CONTEXT)); 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, ContextualFlags.IMPLICIT ); return this.compileCallDirect(indexedGet, [ expression.elementExpression ], expression, thisArg, false, contextualFlags & (ContextualFlags.WILL_DROP | ContextualFlags.SKIP_AUTORELEASE)); } } this.error( DiagnosticCode.Operation_not_supported, expression.range ); return this.module.createUnreachable(); } compileFunctionExpression( expression: FunctionExpression, contextualSignature: Signature | null, contextualFlags: ContextualFlags ): ExpressionRef { var declaration = expression.declaration.clone(); // generic contexts can have multiple assert(!declaration.typeParameters); // function expression cannot be generic var flow = this.currentFlow; var actualFunction = flow.actualFunction; var prototype = new FunctionPrototype( declaration.name.text.length ? declaration.name.text : "anonymous|" + (actualFunction.nextAnonymousId++).toString(10), actualFunction, declaration, DecoratorFlags.NONE ); var instance: Function | null; var contextualTypeArguments = makeMap(flow.contextualTypeArguments); // compile according to context. this differs from a normal function in that omitted parameter // and return types can be inferred and omitted arguments can be replaced with dummies. if (contextualSignature) { let signatureNode = prototype.signatureNode; let parameterNodes = signatureNode.parameters; let numPresentParameters = parameterNodes.length; // must not require more than the maximum number of parameters let parameterTypes = contextualSignature.parameterTypes; let numParameters = parameterTypes.length; if (numPresentParameters > numParameters) { this.error( DiagnosticCode.Expected_0_arguments_but_got_1, expression.range, numParameters.toString(), numPresentParameters.toString() ); return this.module.createUnreachable(); } // check non-omitted parameter types let parameterNames = new Array(numPresentParameters); for (let i = 0; i < numPresentParameters; ++i) { let parameterNode = parameterNodes[i]; parameterNames[i] = parameterNode.name.text; // use actual name if (!isTypeOmitted(parameterNode.type)) { let resolvedType = this.resolver.resolveType( parameterNode.type, actualFunction.parent, contextualTypeArguments ); if (!resolvedType) return this.module.createUnreachable(); if (!parameterTypes[i].isStrictlyAssignableTo(resolvedType)) { this.error( DiagnosticCode.Type_0_is_not_assignable_to_type_1, parameterNode.range, parameterTypes[i].toString(), resolvedType.toString() ); return this.module.createUnreachable(); } } // any unused parameters are inherited but ignored } // check non-omitted return type let returnType = contextualSignature.returnType; if (!isTypeOmitted(signatureNode.returnType)) { let resolvedType = this.resolver.resolveType( signatureNode.returnType, actualFunction.parent, contextualTypeArguments ); if (!resolvedType) return this.module.createUnreachable(); if ( returnType == Type.void ? resolvedType != Type.void : !resolvedType.isStrictlyAssignableTo(returnType) ) { this.error( DiagnosticCode.Type_0_is_not_assignable_to_type_1, signatureNode.returnType.range, resolvedType.toString(), returnType.toString() ); return this.module.createUnreachable(); } } // check explicit this type let thisType = contextualSignature.thisType; let thisTypeNode = signatureNode.explicitThisType; if (thisTypeNode) { if (!thisType) { this.error( DiagnosticCode._this_cannot_be_referenced_in_current_location, thisTypeNode.range ); return this.module.createUnreachable(); } let resolvedType = this.resolver.resolveType( thisTypeNode, actualFunction.parent, contextualTypeArguments ); if (!resolvedType) return this.module.createUnreachable(); if (!thisType.isStrictlyAssignableTo(resolvedType)) { this.error( DiagnosticCode.Type_0_is_not_assignable_to_type_1, thisTypeNode.range, thisType.toString(), resolvedType.toString() ); return this.module.createUnreachable(); } } let signature = new Signature(parameterTypes, returnType, thisType); signature.requiredParameters = numParameters; // ! signature.parameterNames = parameterNames; instance = new Function( prototype.name, prototype, signature, contextualTypeArguments ); if (!this.compileFunction(instance)) return this.module.createUnreachable(); this.currentType = contextualSignature.type; // otherwise compile like a normal function } else { instance = this.compileFunctionUsingTypeArguments( prototype, [], contextualTypeArguments ); if (!instance) return this.module.createUnreachable(); this.currentType = instance.signature.type; } var index = this.ensureFunctionTableEntry(instance); // reports return index < 0 ? this.module.createUnreachable() : this.module.createI32(index); } /** Makes sure the enclosing source file of the specified expression has been compiled. */ private maybeCompileEnclosingSource(expression: Expression): void { var internalPath = expression.range.source.internalPath; var filesByName = this.program.filesByName; assert(filesByName.has(internalPath)); var enclosingFile = filesByName.get(internalPath)!; if (!enclosingFile.is(CommonFlags.COMPILED)) { this.compileFileByPath(internalPath, expression); } } /** * 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, contextualFlags: ContextualFlags ): ExpressionRef { var module = this.module; var flow = this.currentFlow; var actualFunction = flow.actualFunction; // 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: { if (actualFunction.is(CommonFlags.INSTANCE)) { let thisLocal = assert(flow.lookupLocal(CommonSymbols.this_)); let classInstance = assert(actualFunction.parent); assert(classInstance.kind == ElementKind.CLASS); let nativeSizeType = this.options.nativeSizeType; if (actualFunction.is(CommonFlags.CONSTRUCTOR)) { if (!flow.is(FlowFlags.ALLOCATES)) { flow.set(FlowFlags.ALLOCATES); // { // if (!this) this = // this.a = X // this.b = Y // return this // } let stmts: ExpressionRef[] = [ module.createIf( module.createUnary(nativeSizeType == NativeType.I64 ? UnaryOp.EqzI64 : UnaryOp.EqzI32, module.createGetLocal(thisLocal.index, nativeSizeType) ), module.createSetLocal(thisLocal.index, this.makeRetain( this.makeAllocation(classInstance) ) ) ) ]; this.makeFieldInitialization(classInstance, stmts); stmts.push( module.createGetLocal(thisLocal.index, nativeSizeType) ); this.currentType = thisLocal.type; return module.createBlock(null, stmts, nativeSizeType); } } // if not a constructor, `this` type can differ let thisType = assert(actualFunction.signature.thisType); this.currentType = thisType; return module.createGetLocal(thisLocal.index, 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 flow = this.currentFlow; let actualFunction = flow.actualFunction; if (actualFunction.is(CommonFlags.CONSTRUCTOR)) { if (!flow.is(FlowFlags.CALLS_SUPER)) { // TS1034 in the parser effectively limits this to property accesses this.error( DiagnosticCode._super_must_be_called_before_accessing_a_property_of_super_in_the_constructor_of_a_derived_class, expression.range ); } } if (flow.is(FlowFlags.INLINE_CONTEXT)) { let scopedThis = flow.lookupLocal(CommonSymbols.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 (actualFunction.is(CommonFlags.INSTANCE)) { let classInstance = assert(actualFunction.parent); assert(classInstance.kind == ElementKind.CLASS); let baseClassInstance = (classInstance).base; if (baseClassInstance) { let superType = baseClassInstance.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(); } } this.maybeCompileEnclosingSource(expression); // otherwise resolve var target = this.resolver.resolveIdentifier( // reports expression, flow, this.currentEnum || actualFunction ); if (!target) return module.createUnreachable(); switch (target.kind) { case ElementKind.LOCAL: { let type = (target).type; assert(type != Type.void); if ((target).is(CommonFlags.INLINED)) { return this.compileInlineConstant(target, contextualType, contextualFlags); } let localIndex = (target).index; assert(localIndex >= 0); if (type.is(TypeFlags.NULLABLE) && flow.isLocalFlag(localIndex, LocalFlags.NONNULL, false)) { type = type.nonNullableType; } this.currentType = type; return this.module.createGetLocal(localIndex, type.toNativeType()); } case ElementKind.GLOBAL: { if (!this.compileGlobal(target)) { // reports; not yet compiled if a static field return this.module.createUnreachable(); } let type = (target).type; assert(type != Type.void); if ((target).is(CommonFlags.INLINED)) { return this.compileInlineConstant(target, contextualType, contextualFlags); } this.currentType = type; return this.module.createGetGlobal((target).internalName, type.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)) { assert((target).constantValueKind == ConstantValueKind.INTEGER); return this.module.createI32(i64_low((target).constantIntegerValue)); } return this.module.createGetGlobal((target).internalName, NativeType.I32); } case ElementKind.FUNCTION_PROTOTYPE: { let instance = this.resolver.resolveFunction( target, null, makeMap(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(); } compileInstanceOfExpression( expression: InstanceOfExpression, contextualType: Type, contextualFlags: ContextualFlags ): ExpressionRef { var module = this.module; // NOTE that this differs from TypeScript in that the rhs is a type, not an expression. at the // time of implementation, this seemed more useful because dynamic rhs expressions are not // possible in AS anyway. also note that the code generated below must preserve side-effects of // the LHS expression even when the result is a constant, i.e. return a block dropping `expr`. var expr = this.compileExpressionRetainType(expression.expression, this.options.usizeType); var actualType = this.currentType; var expectedType = this.resolver.resolveType(expression.isType, this.currentFlow.actualFunction); this.currentType = Type.bool; if (!expectedType) return module.createUnreachable(); // instanceof - must be exact if (!expectedType.is(TypeFlags.REFERENCE)) { return module.createBlock(null, [ this.convertExpression(expr, actualType, Type.void, (contextualFlags & ContextualFlags.EXPLICIT) != 0, false, expression.expression), module.createI32(actualType == expectedType ? 1 : 0) ], NativeType.I32); } // instanceof - always false if (!actualType.is(TypeFlags.REFERENCE)) { return module.createBlock(null, [ this.convertExpression(expr, actualType, Type.void, (contextualFlags & ContextualFlags.EXPLICIT) != 0,false, expression.expression), module.createI32(0) ], NativeType.I32); } // both LHS and RHS are references now var nativeSizeType = actualType.toNativeType(); // instanceof - LHS must be != 0 if (actualType.is(TypeFlags.NULLABLE) && !expectedType.is(TypeFlags.NULLABLE)) { // downcast - check statically if (actualType.nonNullableType.isAssignableTo(expectedType)) { return module.createBinary( nativeSizeType == NativeType.I64 ? BinaryOp.NeI64 : BinaryOp.NeI32, expr, actualType.toNativeZero(module) ); } // upcast - check dynamically if (expectedType.isAssignableTo(actualType)) { let program = this.program; if (!(actualType.isUnmanaged || expectedType.isUnmanaged)) { let flow = this.currentFlow; let tempLocal = flow.getAndFreeTempLocal(actualType, false); let instanceofInstance = assert(program.instanceofInstance); this.compileFunction(instanceofInstance); return module.createIf( module.createUnary( nativeSizeType == NativeType.I64 ? UnaryOp.EqzI64 : UnaryOp.EqzI32, module.createTeeLocal(tempLocal.index, expr), ), module.createI32(0), this.makeCallDirect(instanceofInstance, [ module.createGetLocal(tempLocal.index, nativeSizeType), module.createI32(expectedType.classReference!.id) ], expression) ); } else { this.error( DiagnosticCode.Operation_not_supported, expression.range ); } } // either none or both nullable } else { // downcast - check statically if (actualType.isAssignableTo(expectedType)) { return module.createBlock(null, [ this.convertExpression(expr, actualType, Type.void, (contextualFlags & ContextualFlags.EXPLICIT) != 0, false, expression.expression), module.createI32(1) ], NativeType.I32); // upcast - check dynamically } else if (expectedType.isAssignableTo(actualType)) { let program = this.program; if (!(actualType.isUnmanaged || expectedType.isUnmanaged)) { // FIXME: the temp local and the if can be removed here once flows // perform null checking, which would error earlier when checking // uninitialized (thus zero) `var a: A` to be an instance of something. let flow = this.currentFlow; let tempLocal = flow.getAndFreeTempLocal(actualType, false); let instanceofInstance = assert(program.instanceofInstance); this.compileFunction(instanceofInstance); return module.createIf( module.createUnary( nativeSizeType == NativeType.I64 ? UnaryOp.EqzI64 : UnaryOp.EqzI32, module.createTeeLocal(tempLocal.index, expr), ), module.createI32(0), this.makeCallDirect(instanceofInstance, [ module.createGetLocal(tempLocal.index, nativeSizeType), module.createI32(expectedType.classReference!.id) ], expression) ); } else { this.error( DiagnosticCode.Operation_not_supported, expression.range ); } } } // false return module.createBlock(null, [ this.convertExpression(expr, actualType, Type.void, (contextualFlags & ContextualFlags.EXPLICIT) != 0, false, expression.expression), module.createI32(0) ], NativeType.I32); } compileLiteralExpression( expression: LiteralExpression, contextualType: Type, contextualFlags: ContextualFlags, implicitlyNegate: bool = false ): ExpressionRef { var module = this.module; switch (expression.literalKind) { case LiteralKind.ARRAY: { assert(!implicitlyNegate); let classType = contextualType.classReference; if (classType) { if (classType.prototype == this.program.arrayPrototype) { return this.compileArrayLiteral( assert(classType.typeArguments)[0], (expression).elementExpressions, false, // TODO: isConst? contextualFlags, expression ); } } this.error( DiagnosticCode.Operation_not_supported, expression.range ); return module.createUnreachable(); } case LiteralKind.FLOAT: { let floatValue = (expression).value; if (implicitlyNegate) { 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 (implicitlyNegate) { intValue = i64_sub( i64_new(0), intValue ); } let type = this.resolver.determineIntegerLiteralType(intValue, contextualType); this.currentType = type; switch (type.kind) { case TypeKind.ISIZE: if (!this.options.isWasm64) return module.createI32(i64_low(intValue)); case TypeKind.I64: return module.createI64(i64_low(intValue), i64_high(intValue)); case TypeKind.USIZE: if (!this.options.isWasm64) return module.createI32(i64_low(intValue)); case TypeKind.U64: return module.createI64(i64_low(intValue), i64_high(intValue)); case TypeKind.F32: return module.createF32(i64_to_f32(intValue)); case TypeKind.F64: return module.createF64(i64_to_f64(intValue)); default: return module.createI32(i64_low(intValue)); } } case LiteralKind.STRING: { assert(!implicitlyNegate); return this.compileStringLiteral(expression); } case LiteralKind.OBJECT: { assert(!implicitlyNegate); return this.compileObjectLiteral(expression, contextualType); } // case LiteralKind.REGEXP: } this.error( DiagnosticCode.Operation_not_supported, expression.range ); this.currentType = contextualType; return module.createUnreachable(); } compileStringLiteral(expression: StringLiteralExpression): ExpressionRef { return this.ensureStaticString(expression.value); } compileArrayLiteral( elementType: Type, expressions: (Expression | null)[], isConst: bool, contextualFlags: ContextualFlags, reportNode: Node ): ExpressionRef { var module = this.module; var program = this.program; var arrayPrototype = assert(program.arrayPrototype); var arrayInstance = assert(this.resolver.resolveClass(arrayPrototype, [ elementType ])); var arrayBufferInstance = assert(program.arrayBufferInstance); var arrayType = arrayInstance.type; var flow = this.currentFlow; // block those here so compiling expressions doesn't conflict var tempThis = flow.getTempLocal(arrayType); var tempDataStart = flow.getTempLocal(arrayBufferInstance.type); // compile value expressions and find out whether all are constant var length = expressions.length; var values = new Array(length); var allValuesAreConstant = true; var nativeElementType = elementType.toNativeType(); for (let i = 0; i < length; ++i) { let expression = expressions[i]; let expr = expression ? module.precomputeExpression( this.compileExpression(expression, elementType, ContextualFlags.IMPLICIT ) ) : elementType.toNativeZero(module); if (getExpressionId(expr) == ExpressionId.Const) { assert(getExpressionType(expr) == nativeElementType); } else { if (isConst) { this.warning( DiagnosticCode.Compiling_constant_with_non_constant_initializer_as_mutable, reportNode.range ); } allValuesAreConstant = false; } values[i] = expr; } // if the array is static, make a static arraybuffer segment if (allValuesAreConstant) { flow.freeTempLocal(tempThis); flow.freeTempLocal(tempDataStart); let runtimeHeaderSize = program.runtimeHeaderSize; let bufferSegment = this.ensureStaticArrayBuffer(elementType, values); let bufferAddress = i64_add(bufferSegment.offset, i64_new(runtimeHeaderSize)); // make both the buffer and array header static if assigned to a global. this can't be done // if inside of a function because each invocation must create a new array reference then. if (contextualFlags & ContextualFlags.STATIC_CAPABLE) { let arraySegment = this.ensureStaticArrayHeader(elementType, bufferSegment); let arrayAddress = i64_add(arraySegment.offset, i64_new(runtimeHeaderSize)); this.currentType = arrayType; return program.options.isWasm64 ? this.module.createI64(i64_low(arrayAddress), i64_high(arrayAddress)) : this.module.createI32(i64_low(arrayAddress)); // otherwise allocate a new array header and make it wrap a copy of the static buffer } else { // makeArray(length, alignLog2, classId, staticBuffer) let expr = this.makeCallDirect(program.allocArrayInstance, [ module.createI32(length), program.options.isWasm64 ? module.createI64(elementType.alignLog2) : module.createI32(elementType.alignLog2), module.createI32(arrayInstance.id), program.options.isWasm64 ? module.createI64(i64_low(bufferAddress), i64_high(bufferAddress)) : module.createI32(i64_low(bufferAddress)) ], reportNode); this.currentType = arrayType; return this.makeAutorelease(this.makeRetain(expr)); } } // otherwise compile an explicit instantiation with indexed sets var setter = arrayInstance.lookupOverload(OperatorKind.INDEXED_SET, true); if (!setter) { flow.freeTempLocal(tempThis); flow.freeTempLocal(tempDataStart); this.error( DiagnosticCode.Index_signature_in_type_0_only_permits_reading, reportNode.range, arrayInstance.internalName ); this.currentType = arrayType; return module.createUnreachable(); } var nativeArrayType = arrayType.toNativeType(); var stmts = new Array(); // tempThis = makeArray(length, alignLog2, classId, source = 0) stmts.push( module.createSetLocal(tempThis.index, this.makeCallDirect(program.allocArrayInstance, [ module.createI32(length), program.options.isWasm64 ? module.createI64(elementType.alignLog2) : module.createI32(elementType.alignLog2), module.createI32(arrayInstance.id), program.options.isWasm64 ? module.createI64(0) : module.createI32(0) ], reportNode), ) ); // tempData = tempThis.dataStart var dataStart = assert(arrayInstance.lookupInSelf("dataStart")); assert(dataStart.kind == ElementKind.FIELD); stmts.push( module.createSetLocal(tempDataStart.index, module.createLoad(arrayType.byteSize, false, module.createGetLocal(tempThis.index, nativeArrayType), nativeArrayType, (dataStart).memoryOffset ) ) ); var isManaged = elementType.isManaged; for (let i = 0, alignLog2 = elementType.alignLog2; i < length; ++i) { let valueExpr = values[i]; if (isManaged) { // value = __retain(value) valueExpr = this.makeRetain(valueExpr); } // store(tempData, value, immOffset) stmts.push( module.createStore(elementType.byteSize, module.createGetLocal(tempDataStart.index, nativeArrayType), valueExpr, nativeElementType, i << alignLog2 ) ); } // -> tempThis stmts.push( module.createGetLocal(tempThis.index, nativeArrayType) ); flow.freeTempLocal(tempThis); flow.freeTempLocal(tempDataStart); this.currentType = arrayType; return module.createBlock(null, stmts, nativeArrayType); } compileObjectLiteral(expression: ObjectLiteralExpression, contextualType: Type): ExpressionRef { var module = this.module; // contextual type must be a class var classReference = contextualType.classReference; if (!classReference || classReference.is(CommonFlags.ABSTRACT)) { this.error( DiagnosticCode.Type_0_is_not_assignable_to_type_1, expression.range, "", contextualType.toString() ); return module.createUnreachable(); } // if present, check that the constructor is compatible with object literals var ctor = classReference.constructorInstance; if (ctor) { // TODO: if the constructor requires parameters, check whether these are given as part of the // object literal and use them to call the ctor while not generating a store. if (ctor.signature.requiredParameters) { this.error( DiagnosticCode.Constructor_of_class_0_must_not_require_any_arguments, expression.range, classReference.toString() ); return module.createUnreachable(); } if (ctor.is(CommonFlags.PRIVATE)) { this.error( DiagnosticCode.Constructor_of_class_0_is_private_and_only_accessible_within_the_class_declaration, expression.range, classReference.toString() ); return module.createUnreachable(); } if (ctor.is(CommonFlags.PROTECTED)) { this.error( DiagnosticCode.Constructor_of_class_0_is_protected_and_only_accessible_within_the_class_declaration, expression.range, classReference.toString() ); return module.createUnreachable(); } } // check and compile field values var names = expression.names; var numNames = names.length; var values = expression.values; var members = classReference.members; var hasErrors = false; var exprs = new Array(numNames + 2); var flow = this.currentFlow; var tempLocal = flow.getAutoreleaseLocal(this.options.usizeType); assert(numNames == values.length); for (let i = 0, k = numNames; i < k; ++i) { let member = members ? members.get(names[i].text) : null; if (!member || member.kind != ElementKind.FIELD) { this.error( DiagnosticCode.Property_0_does_not_exist_on_type_1, names[i].range, names[i].text, classReference.toString() ); hasErrors = true; continue; } let type = (member).type; exprs[i + 1] = this.module.createStore( // TODO: handle setters as well type.byteSize, this.module.createGetLocal(tempLocal.index, this.options.nativeSizeType), this.compileExpression(values[i], (member).type, ContextualFlags.IMPLICIT), type.toNativeType(), (member).memoryOffset ); } this.currentType = classReference.type.nonNullableType; if (hasErrors) return module.createUnreachable(); // allocate a new instance first and assign 'this' to the temp. local exprs[0] = module.createSetLocal( tempLocal.index, this.makeRetain( this.makeAllocation(classReference) ) ); // once all field values have been set, return 'this' exprs[exprs.length - 1] = module.createGetLocal(tempLocal.index, this.options.nativeSizeType); this.currentType = classReference.type; return module.createBlock(null, exprs, this.options.nativeSizeType); } compileNewExpression( expression: NewExpression, contextualType: Type, contextualFlags: ContextualFlags ): ExpressionRef { var module = this.module; var flow = this.currentFlow; // obtain the class being instantiated var target = this.resolver.resolveExpression( // reports expression.expression, flow ); 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: Class | null = null; var typeArguments = expression.typeArguments; var classReference: Class | null; if ( !typeArguments && (classReference = contextualType.classReference) !== null && classReference.is(CommonFlags.GENERIC) ) { classInstance = this.resolver.resolveClass( classPrototype, classReference.typeArguments, makeMap(flow.contextualTypeArguments) ); } else { classInstance = this.resolver.resolveClassInclTypeArguments( classPrototype, typeArguments, flow.actualFunction.parent, // relative to caller makeMap(flow.contextualTypeArguments), expression ); } if (!classInstance) return module.createUnreachable(); if (contextualType == Type.void) contextualFlags |= ContextualFlags.WILL_DROP; return this.compileInstantiate(classInstance, expression.arguments, contextualFlags, expression); } /** Gets the compiled constructor of the specified class or generates one if none is present. */ ensureConstructor(classInstance: Class, reportNode: Node): Function { var instance = classInstance.constructorInstance; if (instance) { // do not attempt to compile it if inlined anyway if (!instance.hasDecorator(DecoratorFlags.INLINE)) this.compileFunction(instance); return instance; } // clone base constructor if a derived class var baseClass = classInstance.base; if (baseClass) { let baseCtor = this.ensureConstructor(baseClass, reportNode); instance = new Function( CommonSymbols.constructor, new FunctionPrototype( CommonSymbols.constructor, classInstance, // declaration is important, i.e. to access optional parameter initializers (baseCtor.declaration).clone() ), baseCtor.signature, null ); // otherwise make a default constructor } else { instance = new Function( CommonSymbols.constructor, new FunctionPrototype( CommonSymbols.constructor, classInstance, this.program.makeNativeFunctionDeclaration(CommonSymbols.constructor, CommonFlags.INSTANCE | CommonFlags.CONSTRUCTOR ) ), new Signature(null, classInstance.type, classInstance.type), null ); } instance.internalName = classInstance.internalName + INSTANCE_DELIMITER + "constructor"; instance.set(CommonFlags.COMPILED); instance.prototype.setResolvedInstance("", instance); classInstance.constructorInstance = instance; var previousFlow = this.currentFlow; this.currentFlow = instance.flow; // generate body var signature = instance.signature; var module = this.module; var nativeSizeType = this.options.nativeSizeType; var stmts = new Array(); // { // if (!this) this = // IF_DERIVED: this = super(this, ...args) // this.a = X // this.b = Y // return this // } stmts.push( module.createIf( module.createUnary(nativeSizeType == NativeType.I64 ? UnaryOp.EqzI64 : UnaryOp.EqzI32, module.createGetLocal(0, nativeSizeType) ), module.createSetLocal(0, this.makeRetain( this.makeAllocation(classInstance) ) ) ) ); if (baseClass) { let parameterTypes = signature.parameterTypes; let numParameters = parameterTypes.length; let operands = new Array(1 + numParameters); operands[0] = module.createGetLocal(0, nativeSizeType); for (let i = 0; i < numParameters; ++i) { operands[i + 1] = module.createGetLocal(i + 1, parameterTypes[i].toNativeType()); } // TODO: base constructor might be inlined, but makeCallDirect can't do this stmts.push( module.createSetLocal(0, this.makeCallDirect(assert(baseClass.constructorInstance), operands, reportNode) ) ); } this.makeFieldInitialization(classInstance, stmts); stmts.push( module.createGetLocal(0, nativeSizeType) ); // make the function var typeRef = this.ensureFunctionType(signature.parameterTypes, signature.returnType, signature.thisType); var locals = instance.localsByIndex; var varTypes = new Array(); // of temp. vars added while compiling initializers var numOperands = 1 + signature.parameterTypes.length; var numLocals = locals.length; if (numLocals > numOperands) { for (let i = numOperands; i < numLocals; ++i) varTypes.push(locals[i].type.toNativeType()); } var funcRef = module.addFunction(instance.internalName, typeRef, varTypes, flatten(module, stmts, nativeSizeType) ); instance.finalize(module, funcRef); this.currentFlow = previousFlow; return instance; } compileInstantiate( /** Class to instantiate. */ classInstance: Class, /** Constructor arguments. */ argumentExpressions: Expression[], /** Contextual flags. */ contextualFlags: ContextualFlags, /** Node to report on. */ reportNode: Node ): ExpressionRef { var ctor = this.ensureConstructor(classInstance, reportNode); var expr = this.compileCallDirect( // no need for another autoreleased local ctor, argumentExpressions, reportNode, this.options.usizeType.toNativeZero(this.module), false, contextualFlags ); if (getExpressionType(expr) != NativeType.None) { // possibly IMM_DROPPED this.currentType = classInstance.type; // important because a super ctor could be called } return expr; } /** * 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, contextualFlags: ContextualFlags ): ExpressionRef { var module = this.module; var flow = this.currentFlow; this.maybeCompileEnclosingSource(propertyAccess); var target = this.resolver.resolvePropertyAccessExpression(propertyAccess, flow, contextualType); // reports if (!target) return module.createUnreachable(); switch (target.kind) { case ElementKind.GLOBAL: { // static field 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, contextualFlags); } this.currentType = globalType; return module.createGetGlobal((target).internalName, globalType.toNativeType()); } case ElementKind.ENUMVALUE: { // enum value let theEnum = assert((target).parent); assert(theEnum.kind == ElementKind.ENUM); if (!this.compileEnum(theEnum)) { this.currentType = Type.i32; return this.module.createUnreachable(); } this.currentType = Type.i32; if ((target).is(CommonFlags.INLINED)) { assert((target).constantValueKind == ConstantValueKind.INTEGER); return module.createI32(i64_low((target).constantIntegerValue)); } return module.createGetGlobal((target).internalName, NativeType.I32); } case ElementKind.FIELD: { // instance field assert((target).memoryOffset >= 0); let thisExpr = this.compileExpressionRetainType( assert(this.resolver.currentThisExpression), 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_PROTOTYPE: {// static property let getterPrototype = (target).getterPrototype; if (getterPrototype) { let getter = this.resolver.resolveFunction(getterPrototype, null); if (getter) return this.compileCallDirect(getter, [], propertyAccess, 0); } return module.createUnreachable(); } case ElementKind.PROPERTY: { // instance property let getterInstance = assert((target).getterInstance); return this.compileCallDirect(getterInstance, [], propertyAccess, this.compileExpressionRetainType( assert(this.resolver.currentThisExpression), this.options.usizeType ) ); } case ElementKind.FUNCTION_PROTOTYPE: { this.error( DiagnosticCode.Cannot_access_method_0_without_calling_it_as_it_requires_this_to_be_set, propertyAccess.range, (target).name ); return module.createUnreachable(); } } this.error( DiagnosticCode.Operation_not_supported, propertyAccess.range ); return module.createUnreachable(); } compileTernaryExpression( expression: TernaryExpression, contextualType: Type, contextualFlags: ContextualFlags ): ExpressionRef { var ifThen = expression.ifThen; var ifElse = expression.ifElse; var outerFlow = this.currentFlow; var condExpr = this.module.precomputeExpression( this.makeIsTrueish( this.compileExpressionRetainType(expression.condition, Type.bool), this.currentType ) ); // Try to eliminate unnecesssary branches if the condition is constant if ( getExpressionId(condExpr) == ExpressionId.Const && getExpressionType(condExpr) == NativeType.I32 ) { return getConstValueI32(condExpr) ? this.compileExpressionRetainType(ifThen, contextualType) : this.compileExpressionRetainType(ifElse, contextualType); } var ifThenFlow = outerFlow.fork(); this.currentFlow = ifThenFlow; var ifThenExpr = this.compileExpressionRetainType(ifThen, contextualType, contextualFlags & ContextualFlags.SKIP_AUTORELEASE); var ifThenType = this.currentType; var IfThenAutoreleaseSkipped = this.skippedAutoreleases.has(ifThenExpr); var ifElseFlow = outerFlow.fork(); this.currentFlow = ifElseFlow; var ifElseExpr = this.compileExpressionRetainType(ifElse, contextualType, contextualFlags & ContextualFlags.SKIP_AUTORELEASE); var ifElseType = this.currentType; var ifElseAutoreleaseSkipped = this.skippedAutoreleases.has(ifElseExpr); var commonType = Type.commonDenominator(ifThenType, ifElseType, false); if (!commonType) { this.error( DiagnosticCode.Type_0_is_not_assignable_to_type_1, ifElse.range, ifElseType.toString(), ifThenType.toString() ); this.currentType = contextualType; return this.module.createUnreachable(); } ifThenExpr = this.convertExpression( ifThenExpr, ifThenType, commonType, false, false, ifThen ); ifElseExpr = this.convertExpression( ifElseExpr, ifElseType, commonType, false, false, ifElse ); this.currentType = commonType; if (IfThenAutoreleaseSkipped != ifElseAutoreleaseSkipped) { // unify to both skipped if (!IfThenAutoreleaseSkipped) { ifThenExpr = this.makeRetain(ifThenExpr); IfThenAutoreleaseSkipped = true; } else { ifElseExpr = this.makeRetain(ifElseExpr); ifElseAutoreleaseSkipped = true; } } else if (!IfThenAutoreleaseSkipped && commonType.isManaged) { // keep alive a little longer // if (!(contextualFlags & ContextualFlags.SKIP_AUTORELEASE)) { ifThenExpr = this.moveAutorelease(ifThenExpr, ifThenFlow, outerFlow); ifElseExpr = this.moveAutorelease(ifElseExpr, ifElseFlow, outerFlow); // } } ifThenExpr = this.performAutoreleasesWithValue(ifThenFlow, ifThenExpr, commonType); ifThenFlow.freeScopedLocals(); ifElseExpr = this.performAutoreleasesWithValue(ifElseFlow, ifElseExpr, commonType); ifElseFlow.freeScopedLocals(); this.currentFlow = outerFlow; outerFlow.inheritMutual(ifThenFlow, ifElseFlow); var expr = this.module.createIf(condExpr, ifThenExpr, ifElseExpr); assert(IfThenAutoreleaseSkipped == ifElseAutoreleaseSkipped); if (IfThenAutoreleaseSkipped) this.skippedAutoreleases.add(expr); return expr; } compileUnaryPostfixExpression( expression: UnaryPostfixExpression, contextualType: Type, contextualFlags: ContextualFlags ): ExpressionRef { var module = this.module; var flow = this.currentFlow; // make a getter for the expression (also obtains the type) var getValue = this.compileExpression( // reports expression.operand, contextualType == Type.void ? Type.i32 : contextualType, ContextualFlags.NONE ); // shortcut if compiling the getter already failed if (getExpressionId(getValue) == ExpressionId.Unreachable) return getValue; // if the value isn't dropped, a temp. local is required to remember the original value var tempLocal: Local | null = null; if (contextualType != Type.void) { tempLocal = flow.getTempLocal(this.currentType); getValue = module.createTeeLocal( tempLocal.index, getValue ); } var expr: ExpressionRef; switch (expression.operator) { case Token.PLUS_PLUS: { switch (this.currentType.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.AddI32, getValue, module.createI32(1) ); break; } case TypeKind.USIZE: { // check operator overload if (this.currentType.is(TypeFlags.REFERENCE)) { let classReference = this.currentType.classReference; if (classReference) { let overload = classReference.lookupOverload(OperatorKind.POSTFIX_INC); if (overload) { expr = this.compileUnaryOverload(overload, expression.operand, getValue, expression); break; } } this.error( DiagnosticCode.Operation_not_supported, expression.range ); return module.createUnreachable(); } } case TypeKind.ISIZE: { let options = this.options; expr = module.createBinary( options.isWasm64 ? BinaryOp.AddI64 : BinaryOp.AddI32, getValue, this.currentType.toNativeOne(module) ); break; } case TypeKind.I64: case TypeKind.U64: { expr = module.createBinary( BinaryOp.AddI64, getValue, module.createI64(1) ); break; } case TypeKind.F32: { expr = module.createBinary( BinaryOp.AddF32, getValue, module.createF32(1) ); break; } case TypeKind.F64: { expr = module.createBinary( BinaryOp.AddF64, getValue, module.createF64(1) ); break; } default: { assert(false); return module.createUnreachable(); } } break; } case Token.MINUS_MINUS: { switch (this.currentType.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.SubI32, getValue, module.createI32(1) ); break; } case TypeKind.USIZE: { // check operator overload if (this.currentType.is(TypeFlags.REFERENCE)) { let classReference = this.currentType.classReference; if (classReference) { let overload = classReference.lookupOverload(OperatorKind.POSTFIX_DEC); if (overload) { expr = this.compileUnaryOverload(overload, expression.operand, getValue, expression); break; } } this.error( DiagnosticCode.Operation_not_supported, expression.range ); return module.createUnreachable(); } } case TypeKind.ISIZE: { let options = this.options; expr = module.createBinary( options.isWasm64 ? BinaryOp.SubI64 : BinaryOp.SubI32, getValue, this.currentType.toNativeOne(module) ); break; } case TypeKind.I64: case TypeKind.U64: { expr = module.createBinary( BinaryOp.SubI64, getValue, module.createI64(1) ); break; } case TypeKind.F32: { expr = module.createBinary( BinaryOp.SubF32, getValue, module.createF32(1) ); break; } case TypeKind.F64: { expr = module.createBinary( BinaryOp.SubF64, getValue, module.createF64(1) ); break; } default: { assert(false); return module.createUnreachable(); } } break; } default: { assert(false); return module.createUnreachable(); } } var resolver = this.resolver; var target = resolver.resolveExpression(expression.operand, flow); // reports // simplify if dropped anyway if (!tempLocal) { this.currentType = Type.void; if (!target) return module.createUnreachable(); return this.makeAssignment( target, expr, expression.operand, resolver.currentThisExpression, resolver.currentElementExpression, false ); } else if (!target) { return module.createUnreachable(); } // otherwise use the temp. local for the intermediate value (always possibly overflows) var setValue = this.makeAssignment( target, expr, // includes a tee of getValue to tempLocal expression.operand, resolver.currentThisExpression, resolver.currentElementExpression, false ); this.currentType = tempLocal.type; flow.freeTempLocal(tempLocal); var nativeType = tempLocal.type.toNativeType(); return module.createBlock(null, [ setValue, module.createGetLocal(tempLocal.index, nativeType) ], nativeType); // result of 'x++' / 'x--' might overflow } compileUnaryPrefixExpression( expression: UnaryPrefixExpression, contextualType: Type, contextualFlags: ContextualFlags ): ExpressionRef { var module = this.module; var compound = false; var expr: ExpressionRef; switch (expression.operator) { case Token.PLUS: { expr = this.compileExpression( expression.operand, contextualType == Type.void ? Type.i32 : contextualType, ContextualFlags.NONE ); // check operator overload if (this.currentType.is(TypeFlags.REFERENCE)) { let classReference = this.currentType.classReference; if (classReference) { let overload = classReference.lookupOverload(OperatorKind.PLUS); if (overload) { expr = this.compileUnaryOverload(overload, expression.operand, expr, expression); break; } } this.error( DiagnosticCode.Operation_not_supported, expression.range ); return module.createUnreachable(); } // nop break; } case Token.MINUS: { 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, ContextualFlags.NONE, true); // compileExpression normally does this: if (this.options.sourceMap) this.addDebugLocation(expr, expression.range); break; } expr = this.compileExpression( expression.operand, contextualType == Type.void ? Type.i32 : contextualType, ContextualFlags.NONE ); // check operator overload if (this.currentType.is(TypeFlags.REFERENCE)) { let classReference = this.currentType.classReference; if (classReference) { let overload = classReference.lookupOverload(OperatorKind.MINUS); if (overload) { expr = this.compileUnaryOverload(overload, expression.operand, expr, expression); break; } } this.error( DiagnosticCode.Operation_not_supported, expression.range ); return module.createUnreachable(); } switch (this.currentType.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.SubI32, module.createI32(0), expr); break; } case TypeKind.USIZE: case TypeKind.ISIZE: { expr = module.createBinary( this.options.isWasm64 ? BinaryOp.SubI64 : BinaryOp.SubI32, this.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; } default: { assert(false); expr = module.createUnreachable(); } } break; } case Token.PLUS_PLUS: { compound = true; expr = this.compileExpression( expression.operand, contextualType == Type.void ? Type.i32 : contextualType, ContextualFlags.NONE ); // check operator overload if (this.currentType.is(TypeFlags.REFERENCE)) { let classReference = this.currentType.classReference; if (classReference) { let overload = classReference.lookupOverload(OperatorKind.PREFIX_INC); if (overload) { expr = this.compileUnaryOverload(overload, expression.operand, expr, expression); break; } } this.error( DiagnosticCode.Operation_not_supported, expression.range ); return module.createUnreachable(); } switch (this.currentType.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.AddI32, expr, this.module.createI32(1)); break; } case TypeKind.USIZE: case TypeKind.ISIZE: { expr = module.createBinary( this.options.isWasm64 ? BinaryOp.AddI64 : BinaryOp.AddI32, expr, this.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; } default: { assert(false); expr = module.createUnreachable(); } } break; } case Token.MINUS_MINUS: { compound = true; expr = this.compileExpression( expression.operand, contextualType == Type.void ? Type.i32 : contextualType, ContextualFlags.NONE ); // check operator overload if (this.currentType.is(TypeFlags.REFERENCE)) { let classReference = this.currentType.classReference; if (classReference) { let overload = classReference.lookupOverload(OperatorKind.PREFIX_DEC); if (overload) { expr = this.compileUnaryOverload(overload, expression.operand, expr, expression); break; } } this.error( DiagnosticCode.Operation_not_supported, expression.range ); return module.createUnreachable(); } switch (this.currentType.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.SubI32, expr, module.createI32(1)); break; } case TypeKind.USIZE: case TypeKind.ISIZE: { expr = module.createBinary( this.options.isWasm64 ? BinaryOp.SubI64 : BinaryOp.SubI32, expr, this.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; } default: { assert(false); expr = module.createUnreachable(); } } break; } case Token.EXCLAMATION: { expr = this.compileExpression( expression.operand, contextualType == Type.void ? Type.i32 : contextualType, ContextualFlags.NONE ); // check operator overload if (this.currentType.is(TypeFlags.REFERENCE)) { let classReference = this.currentType.classReference; if (classReference) { let overload = classReference.lookupOverload(OperatorKind.NOT); if (overload) { expr = this.compileUnaryOverload(overload, expression.operand, expr, expression); break; } } // allow '!' for references even without an overload } expr = this.makeIsFalseish(expr, this.currentType); this.currentType = Type.bool; break; } case Token.TILDE: { expr = this.compileExpression( expression.operand, contextualType == Type.void ? Type.i32 : contextualType.is(TypeFlags.FLOAT) ? Type.i64 : contextualType, ContextualFlags.NONE ); // check operator overload if (this.currentType.is(TypeFlags.REFERENCE)) { let classReference = this.currentType.classReference; if (classReference) { let overload = classReference.lookupOverload(OperatorKind.BITWISE_NOT); if (overload) { expr = this.compileUnaryOverload(overload, expression.operand, expr, expression); break; } } this.error( DiagnosticCode.Operation_not_supported, expression.range ); return module.createUnreachable(); } else { expr = this.convertExpression(expr, this.currentType, this.currentType.intType, false, false, expression.operand ); } switch (this.currentType.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.XorI32, expr, module.createI32(-1)); break; } case TypeKind.USIZE: case TypeKind.ISIZE: { expr = module.createBinary( this.options.isWasm64 ? BinaryOp.XorI64 : BinaryOp.XorI32, expr, this.currentType.toNativeNegOne(module) ); break; } case TypeKind.I64: case TypeKind.U64: { expr = module.createBinary(BinaryOp.XorI64, expr, module.createI64(-1, -1)); break; } default: { assert(false); expr = module.createUnreachable(); } } break; } case Token.TYPEOF: { this.error( DiagnosticCode.Operation_not_supported, expression.range ); return module.createUnreachable(); } default: { assert(false); return module.createUnreachable(); } } if (!compound) return expr; var resolver = this.resolver; var target = resolver.resolveExpression(expression.operand, this.currentFlow); if (!target) return module.createUnreachable(); return this.makeAssignment( target, expr, expression.operand, resolver.currentThisExpression, resolver.currentElementExpression, contextualType != Type.void ); } /** Makes sure that a 32-bit integer value is wrapped to a valid value of the specified type. */ ensureSmallIntegerWrap(expr: ExpressionRef, type: Type): ExpressionRef { var module = this.module; var flow = this.currentFlow; switch (type.kind) { case TypeKind.I8: { if (flow.canOverflow(expr, type)) { expr = this.options.hasFeature(Feature.SIGN_EXTENSION) ? module.createUnary(UnaryOp.ExtendI8ToI32, expr) : module.createBinary(BinaryOp.ShrI32, module.createBinary(BinaryOp.ShlI32, expr, module.createI32(24) ), module.createI32(24) ); } break; } case TypeKind.I16: { if (flow.canOverflow(expr, type)) { expr = this.options.hasFeature(Feature.SIGN_EXTENSION) ? module.createUnary(UnaryOp.ExtendI16ToI32, expr) : module.createBinary(BinaryOp.ShrI32, module.createBinary(BinaryOp.ShlI32, expr, module.createI32(16) ), module.createI32(16) ); } break; } case TypeKind.U8: { if (flow.canOverflow(expr, type)) { expr = module.createBinary(BinaryOp.AndI32, expr, module.createI32(0xff) ); } break; } case TypeKind.U16: { if (flow.canOverflow(expr, type)) { expr = module.createBinary(BinaryOp.AndI32, expr, module.createI32(0xffff) ); } break; } case TypeKind.BOOL: { if (flow.canOverflow(expr, type)) { // bool is special in that it compares to 0 instead of masking with 0x1 expr = module.createBinary(BinaryOp.NeI32, expr, module.createI32(0) ); } break; } } return expr; } /** Adds the debug location of the specified expression at the specified range to the source map. */ addDebugLocation(expr: ExpressionRef, range: Range): void { var parentFunction = this.currentFlow.parentFunction; var source = range.source; if (source.debugInfoIndex < 0) source.debugInfoIndex = this.module.addDebugInfoFile(source.normalizedPath); range.debugInfoRef = expr; parentFunction.debugLocations.push(range); } // === Specialized code generation ============================================================== /** Creates a comparison whether an expression is 'false' in a broader sense. */ makeIsFalseish(expr: ExpressionRef, type: Type): ExpressionRef { var module = this.module; switch (type.kind) { case TypeKind.I8: case TypeKind.I16: case TypeKind.U8: case TypeKind.U16: case TypeKind.BOOL: { expr = this.ensureSmallIntegerWrap(expr, type); // fall-through } case TypeKind.I32: case TypeKind.U32: { return module.createUnary(UnaryOp.EqzI32, expr); } case TypeKind.I64: case TypeKind.U64: { return module.createUnary(UnaryOp.EqzI64, expr); } case TypeKind.USIZE: if (this.skippedAutoreleases.has(expr)) expr = this.makeAutorelease(expr); 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)); } default: { assert(false); return module.createI32(1); } } } /** Creates a comparison whether an expression is 'true' in a broader sense. */ makeIsTrueish(expr: ExpressionRef, type: Type): ExpressionRef { var module = this.module; switch (type.kind) { case TypeKind.I8: case TypeKind.I16: case TypeKind.U8: case TypeKind.U16: { expr = this.ensureSmallIntegerWrap(expr, type); // fall-through } case TypeKind.BOOL: // not a mask, just != 0 case TypeKind.I32: case TypeKind.U32: { return expr; } case TypeKind.I64: case TypeKind.U64: { return module.createBinary(BinaryOp.NeI64, expr, module.createI64(0)); } case TypeKind.USIZE: if (this.skippedAutoreleases.has(expr)) expr = this.makeAutorelease(expr); 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)); } default: { assert(false); return module.createI32(0); } } } /** Makes an allocation suitable to hold the data of an instance of the given class. */ makeAllocation( classInstance: Class ): ExpressionRef { // TODO: investigate if it's possible to allocate with RC=1 immediately var program = this.program; assert(classInstance.program == program); var module = this.module; var options = this.options; this.currentType = classInstance.type; var allocInstance = program.allocInstance; this.compileFunction(allocInstance); return module.createCall(allocInstance.internalName, [ options.isWasm64 ? module.createI64(classInstance.currentMemoryOffset) : module.createI32(classInstance.currentMemoryOffset), module.createI32( classInstance.hasDecorator(DecoratorFlags.UNMANAGED) ? 0 : classInstance.id ) ], options.nativeSizeType); } /** Makes the initializers for a class's fields. */ makeFieldInitialization( classInstance: Class, stmts: ExpressionRef[] = [] ): ExpressionRef[] { var members = classInstance.members; if (!members) return []; var module = this.module; var flow = this.currentFlow; var isInline = flow.is(FlowFlags.INLINE_CONTEXT); var thisLocalIndex = isInline ? assert(flow.lookupLocal(CommonSymbols.this_)).index : 0; var nativeSizeType = this.options.nativeSizeType; for (let member of members.values()) { if ( member.kind != ElementKind.FIELD || // not a field member.parent != classInstance // inherited field ) continue; let field = member; assert(!field.isAny(CommonFlags.CONST)); let fieldType = field.type; let nativeFieldType = fieldType.toNativeType(); let initializerNode = field.prototype.initializerNode; if (initializerNode) { // use initializer stmts.push( module.createStore(fieldType.byteSize, module.createGetLocal(thisLocalIndex, nativeSizeType), this.compileExpression(initializerNode, fieldType, // reports ContextualFlags.IMPLICIT ), nativeFieldType, field.memoryOffset ) ); } else { let parameterIndex = field.prototype.parameterIndex; stmts.push( module.createStore(fieldType.byteSize, module.createGetLocal(thisLocalIndex, nativeSizeType), parameterIndex >= 0 // initialized via parameter (here: a local) ? module.createGetLocal( isInline ? assert(flow.lookupLocal(field.name)).index : 1 + parameterIndex, // this is local 0 nativeFieldType ) : fieldType.toNativeZero(module), nativeFieldType, field.memoryOffset ) ); } } return stmts; } makeInstanceOfClass( expr: ExpressionRef, classInstance: Class ): ExpressionRef { var module = this.module; var flow = this.currentFlow; var idTemp = flow.getTempLocal(Type.i32); var idExpr = module.createLoad(4, false, module.createBinary(BinaryOp.SubI32, expr, module.createI32(this.program.runtimeHeaderSize) ), NativeType.I32 ); var label = "instanceof_" + classInstance.name + "|" + flow.pushBreakLabel(); var conditions: ExpressionRef[] = []; conditions.push( module.createDrop( // br_if returns the value too module.createBreak(label, module.createBinary(BinaryOp.EqI32, // classId == class.id module.createTeeLocal(idTemp.index, idExpr), module.createI32(classInstance.id) ), module.createI32(1) // ? true ) ) ); // TODO: insert conditions for all possible subclasses (i.e. cat is also animal) // TODO: simplify if there are none conditions.push( module.createI32(0) // : false ); flow.freeTempLocal(idTemp); flow.popBreakLabel(); return module.createBlock(label, conditions, NativeType.I32); } } // helpers function mangleImportName( element: Element, declaration: DeclarationStatement ): void { // by default, use the file name as the module name mangleImportName_moduleName = declaration.range.source.simplePath; // and the internal name of the element within that file as the element name mangleImportName_elementName = mangleInternalName( element.name, element.parent, element.is(CommonFlags.INSTANCE), true ); if (!element.hasDecorator(DecoratorFlags.EXTERNAL)) return; var program = element.program; var decorator = assert(findDecorator(DecoratorKind.EXTERNAL, declaration.decorators)); var args = decorator.arguments; if (args && args.length) { let arg = args[0]; // if one argument is given, override just the element name // if two arguments are given, override both module and element name if (arg.kind == NodeKind.LITERAL && (arg).literalKind == LiteralKind.STRING) { mangleImportName_elementName = (arg).value; if (args.length >= 2) { arg = args[1]; if (arg.kind == NodeKind.LITERAL && (arg).literalKind == LiteralKind.STRING) { mangleImportName_moduleName = mangleImportName_elementName; mangleImportName_elementName = (arg).value; if (args.length > 2) { program.error( DiagnosticCode.Expected_0_arguments_but_got_1, decorator.range, "2", args.length.toString() ); } } else { program.error( DiagnosticCode.String_literal_expected, arg.range ); } } } else { program.error( DiagnosticCode.String_literal_expected, arg.range ); } } else { program.error( DiagnosticCode.Expected_at_least_0_arguments_but_got_1, decorator.range, "1", "0" ); } } var mangleImportName_moduleName: string; var mangleImportName_elementName: string; /** Flattens a series of expressions to a nop, a single statement or a block depending on statement count. */ export function flatten(module: Module, stmts: ExpressionRef[], type: NativeType): ExpressionRef { var length = stmts.length; if (length == 0) return module.createNop(); // usually filtered out again if (length == 1) return stmts[0]; return module.createBlock(null, stmts, type == NativeType.Auto ? getExpressionType(stmts[length - 1]) : type ); }