assemblyscript/src/compiler.ts
dcodeIO 83e96892f2 Statically eliminate unnecessary branches in generic contexts
In order to use the new compile time type checks in generics, untaken branches must be skipped because these might be invalid.
2018-03-17 14:40:58 +01:00

5657 lines
188 KiB
TypeScript

import {
compileCall as compileBuiltinCall,
compileGetConstant as compileBuiltinGetConstant,
compileAllocate as compileBuiltinAllocate
} from "./builtins";
import {
DiagnosticCode,
DiagnosticEmitter
} from "./diagnostics";
import {
Module,
MemorySegment,
ExpressionRef,
UnaryOp,
BinaryOp,
NativeType,
FunctionRef,
ExpressionId,
FunctionTypeRef
} from "./module";
import {
Program,
ClassPrototype,
Class,
Element,
ElementKind,
Enum,
Field,
FunctionPrototype,
Function,
FunctionTarget,
Global,
Local,
Namespace,
EnumValue,
Property,
VariableLikeElement,
FlowFlags,
CommonFlags,
ConstantValueKind,
PATH_DELIMITER,
LIBRARY_PREFIX
} from "./program";
import {
Token,
operatorTokenToString
} from "./tokenizer";
import {
Node,
NodeKind,
TypeNode,
Source,
Range,
Statement,
BlockStatement,
BreakStatement,
ClassDeclaration,
ContinueStatement,
DoStatement,
EmptyStatement,
EnumDeclaration,
ExportStatement,
ExpressionStatement,
FunctionDeclaration,
ForStatement,
IfStatement,
ImportStatement,
InterfaceDeclaration,
NamespaceDeclaration,
ReturnStatement,
SwitchStatement,
ThrowStatement,
TryStatement,
VariableDeclaration,
VariableStatement,
VoidStatement,
WhileStatement,
Expression,
AssertionExpression,
BinaryExpression,
CallExpression,
CommaExpression,
ElementAccessExpression,
FloatLiteralExpression,
FunctionExpression,
IdentifierExpression,
IntegerLiteralExpression,
LiteralExpression,
LiteralKind,
NewExpression,
ParenthesizedExpression,
PropertyAccessExpression,
TernaryExpression,
ArrayLiteralExpression,
StringLiteralExpression,
UnaryPostfixExpression,
UnaryPrefixExpression
} from "./ast";
import {
Type,
TypeKind,
TypeFlags,
Signature,
typesToNativeTypes
} from "./types";
/** Compilation target. */
export enum Target {
/** WebAssembly with 32-bit pointers. */
WASM32,
/** WebAssembly with 64-bit pointers. Experimental and not supported by any runtime yet. */
WASM64
}
/** Compiler options. */
export class Options {
/** WebAssembly target. Defaults to {@link Target.WASM32}. */
target: Target = Target.WASM32;
/** If true, compiles everything instead of just reachable code. */
noTreeShaking: bool = false;
/** If true, replaces assertions with nops. */
noAssert: bool = false;
/** If true, does not set up a memory. */
noMemory: bool = false;
/** If true, imports the memory provided by the embedder. */
importMemory: bool = false;
/** Static memory start offset. */
memoryBase: u32 = 0;
/** Memory allocation implementation to use. */
allocateImpl: string = "allocate_memory";
/** Memory freeing implementation to use. */
freeImpl: string = "free_memory";
/** If true, generates information necessary for source maps. */
sourceMap: bool = false;
/** Tests if the target is WASM64 or, otherwise, WASM32. */
get isWasm64(): bool {
return this.target == Target.WASM64;
}
/** Gets the unsigned size type matching the target. */
get usizeType(): Type {
return this.target == Target.WASM64 ? Type.usize64 : Type.usize32;
}
/** Gets the signed size type matching the target. */
get isizeType(): Type {
return this.target == Target.WASM64 ? Type.isize64 : Type.isize32;
}
/** Gets the native size type matching the target. */
get nativeSizeType(): NativeType {
return this.target == Target.WASM64 ? NativeType.I64 : NativeType.I32;
}
}
/** Indicates the desired kind of a conversion. */
export const enum ConversionKind {
/** No conversion. */
NONE,
/** Implicit conversion. */
IMPLICIT,
/** Explicit conversion. */
EXPLICIT
}
/** Compiler interface. */
export class Compiler extends DiagnosticEmitter {
/** Program reference. */
program: Program;
/** Provided options. */
options: Options;
/** Module instance being compiled. */
module: Module;
/** Start function being compiled. */
startFunction: Function;
/** Start function statements. */
startFunctionBody: ExpressionRef[] = [];
/** Current function in compilation. */
currentFunction: Function;
/** Current enum in compilation. */
currentEnum: Enum | null = null;
/** Current type in compilation. */
currentType: Type = Type.void;
/** Counting memory offset. */
memoryOffset: I64;
/** Memory segments being compiled. */
memorySegments: MemorySegment[] = new Array();
/** Map of already compiled static string segments. */
stringSegments: Map<string,MemorySegment> = new Map();
/** Function table being compiled. */
functionTable: Function[] = new Array();
/** Already processed file names. */
files: Set<string> = 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;
if (!options) options = new Options();
this.options = options;
this.memoryOffset = i64_new(
max(options.memoryBase, options.usizeType.byteSize) // leave space for `null`
);
this.module = Module.create();
}
/** Performs compilation of the underlying {@link Program} to a {@link Module}. */
compile(): Module {
var options = this.options;
var module = this.module;
var program = this.program;
// initialize lookup maps, built-ins, imports, exports, etc.
program.initialize(options);
// set up the start function wrapping top-level statements, of all files.
var startFunctionPrototype = assert(program.elementsLookup.get("start"));
assert(startFunctionPrototype.kind == ElementKind.FUNCTION_PROTOTYPE);
var startFunctionInstance = new Function(
<FunctionPrototype>startFunctionPrototype,
startFunctionPrototype.internalName,
new Signature([], Type.void)
);
this.startFunction = startFunctionInstance;
this.currentFunction = startFunctionInstance;
// compile entry file(s) while traversing to reachable elements
var sources = program.sources;
for (let i = 0, k = sources.length; i < k; ++i) {
if (sources[i].isEntry) {
this.compileSource(sources[i]);
}
}
// compile the start function if not empty
var startFunctionBody = this.startFunctionBody;
if (startFunctionBody.length) {
let typeRef = this.ensureFunctionType(startFunctionInstance.signature);
let funcRef: FunctionRef;
module.setStart(
funcRef = module.addFunction(
startFunctionInstance.internalName,
typeRef,
typesToNativeTypes(startFunctionInstance.additionalLocals),
module.createBlock(null, startFunctionBody)
)
);
startFunctionInstance.finalize(module, funcRef);
}
// set up static memory segments and the heap base pointer
if (!options.noMemory) {
let memoryOffset = this.memoryOffset;
memoryOffset = i64_align(memoryOffset, options.usizeType.byteSize);
this.memoryOffset = memoryOffset;
if (options.isWasm64) {
module.addGlobal(
"HEAP_BASE",
NativeType.I64,
false,
module.createI64(i64_low(memoryOffset), i64_high(memoryOffset))
);
} else {
module.addGlobal(
"HEAP_BASE",
NativeType.I32,
false,
module.createI32(i64_low(memoryOffset))
);
}
// determine initial page size
let pages = i64_shr_u(i64_align(memoryOffset, 0x10000), i64_new(16, 0));
module.setMemory(
i64_low(pages),
Module.MAX_MEMORY_WASM32, // TODO: not WASM64 compatible yet
this.memorySegments,
options.target,
"memory"
);
}
// import memory if requested
if (options.importMemory) {
module.addMemoryImport("0", "env", "memory");
}
// set up function table
var functionTable = this.functionTable;
var functionTableSize = functionTable.length;
if (functionTableSize) {
let entries = new Array<FunctionRef>(functionTableSize);
for (let i = 0; i < functionTableSize; ++i) {
entries[i] = functionTable[i].ref;
}
module.setFunctionTable(entries);
}
return module;
}
// sources
compileSourceByPath(normalizedPathWithoutExtension: string, reportNode: Node): void {
var sources = this.program.sources;
// try file.ts
var expected = normalizedPathWithoutExtension + ".ts";
for (let i = 0, k = sources.length; i < k; ++i) {
let source = sources[i];
if (source.normalizedPath == expected) {
this.compileSource(source);
return;
}
}
// try file/index.ts
expected = normalizedPathWithoutExtension + "/index.ts";
for (let i = 0, k = sources.length; i < k; ++i) {
let source = sources[i];
if (source.normalizedPath == expected) {
this.compileSource(source);
return;
}
}
// try (lib)/file.ts
expected = LIBRARY_PREFIX + normalizedPathWithoutExtension + ".ts";
for (let i = 0, k = sources.length; i < k; ++i) {
let source = sources[i];
if (source.normalizedPath == expected) {
this.compileSource(source);
return;
}
}
this.error(
DiagnosticCode.File_0_not_found,
reportNode.range, normalizedPathWithoutExtension
);
}
compileSource(source: Source): void {
var files = this.files;
var normalizedPath = source.normalizedPath;
if (files.has(normalizedPath)) return;
files.add(normalizedPath);
// compile top-level statements
var noTreeShaking = this.options.noTreeShaking;
var isEntry = source.isEntry;
var startFunction = this.startFunction;
var startFunctionBody = this.startFunctionBody;
var statements = source.statements;
for (let i = 0, k = statements.length; i < k; ++i) {
let statement = statements[i];
switch (statement.kind) {
case NodeKind.CLASSDECLARATION: {
if (
(noTreeShaking || (isEntry && statement.is(CommonFlags.EXPORT))) &&
!(<ClassDeclaration>statement).isGeneric
) {
this.compileClassDeclaration(<ClassDeclaration>statement, []);
}
break;
}
case NodeKind.ENUMDECLARATION: {
if (noTreeShaking || (isEntry && statement.is(CommonFlags.EXPORT))) {
this.compileEnumDeclaration(<EnumDeclaration>statement);
}
break;
}
case NodeKind.FUNCTIONDECLARATION: {
if (
(noTreeShaking || (isEntry && statement.is(CommonFlags.EXPORT))) &&
!(<FunctionDeclaration>statement).isGeneric
) {
this.compileFunctionDeclaration(<FunctionDeclaration>statement, []);
}
break;
}
case NodeKind.IMPORT: {
this.compileSourceByPath(
(<ImportStatement>statement).normalizedPath,
(<ImportStatement>statement).path
);
break;
}
case NodeKind.NAMESPACEDECLARATION: {
if (noTreeShaking || (isEntry && statement.is(CommonFlags.EXPORT))) {
this.compileNamespaceDeclaration(<NamespaceDeclaration>statement);
}
break;
}
case NodeKind.VARIABLE: { // global, always compiled as initializers might have side effects
let variableInit = this.compileVariableStatement(<VariableStatement>statement);
if (variableInit) startFunctionBody.push(variableInit);
break;
}
case NodeKind.EXPORT: {
if ((<ExportStatement>statement).normalizedPath != null) {
this.compileSourceByPath(
<string>(<ExportStatement>statement).normalizedPath,
<StringLiteralExpression>(<ExportStatement>statement).path
);
}
if (noTreeShaking || isEntry) {
this.compileExportStatement(<ExportStatement>statement);
}
break;
}
default: { // otherwise a top-level statement that is part of the start function's body
let previousFunction = this.currentFunction;
this.currentFunction = startFunction;
startFunctionBody.push(this.compileStatement(statement));
this.currentFunction = previousFunction;
break;
}
}
}
}
// globals
compileGlobalDeclaration(declaration: VariableDeclaration): Global | null {
// look up the initialized program element
var element = assert(this.program.elementsLookup.get(declaration.fileLevelInternalName));
assert(element.kind == ElementKind.GLOBAL);
if (!this.compileGlobal(<Global>element)) return null; // reports
return <Global>element;
}
compileGlobal(global: Global): bool {
if (global.is(CommonFlags.COMPILED) || global.is(CommonFlags.BUILTIN)) return true;
global.set(CommonFlags.COMPILED); // ^ built-ins are compiled on use
var module = this.module;
var declaration = global.declaration;
var initExpr: ExpressionRef = 0;
if (global.type == Type.void) { // type is void if not yet resolved or not annotated
// resolve now if annotated
if (declaration.type) {
let resolvedType = this.program.resolveType(declaration.type); // reports
if (!resolvedType) return false;
if (resolvedType == Type.void) {
this.error(
DiagnosticCode.Type_expected,
declaration.type.range
);
return false;
}
global.type = resolvedType;
// infer from initializer if not annotated
} else if (declaration.initializer) { // infer type using void/NONE for literal inference
initExpr = this.compileExpression( // reports
declaration.initializer,
Type.void,
ConversionKind.NONE
);
if (this.currentType == Type.void) {
this.error(
DiagnosticCode.Type_0_is_not_assignable_to_type_1,
declaration.initializer.range, this.currentType.toString(), "<auto>"
);
return false;
}
global.type = this.currentType;
// must either be annotated or have an initializer
} else {
this.error(
DiagnosticCode.Type_expected,
declaration.name.range.atEnd
);
return false;
}
}
var nativeType = global.type.toNativeType();
// handle imports
if (global.is(CommonFlags.DECLARE)) {
// constant global
if (global.is(CommonFlags.CONST)) {
global.set(CommonFlags.MODULE_IMPORT);
module.addGlobalImport(
global.internalName,
global.namespace
? global.namespace.simpleName
: "env",
global.simpleName,
nativeType
);
global.set(CommonFlags.COMPILED);
return true;
// importing mutable globals is not supported in the MVP
} else {
this.error(
DiagnosticCode.Operation_not_supported,
declaration.range
);
}
return false;
}
// the MVP does not yet support initializer expressions other than constant values (and
// get_globals), hence such initializations must be performed in the start function for now.
var initializeInStart = false;
// inlined constant can be compiled as-is
if (global.is(CommonFlags.INLINED)) {
initExpr = this.compileInlineConstant(global, global.type, true);
} else {
// evaluate initializer if present
if (declaration.initializer) {
if (!initExpr) {
initExpr = this.compileExpression(declaration.initializer, global.type);
}
// check if the initializer is constant
if (_BinaryenExpressionGetId(initExpr) != ExpressionId.Const) {
// if a constant global, check if the initializer becomes constant after precompute
if (global.is(CommonFlags.CONST)) {
initExpr = this.precomputeExpressionRef(initExpr);
if (_BinaryenExpressionGetId(initExpr) != ExpressionId.Const) {
this.warning(
DiagnosticCode.Compiling_constant_with_non_constant_initializer_as_mutable,
declaration.range
);
initializeInStart = true;
}
} else {
initializeInStart = true;
}
}
// initialize to zero if there's no initializer
} else {
initExpr = global.type.toNativeZero(module);
}
}
var internalName = global.internalName;
if (initializeInStart) { // initialize to mutable zero and set the actual value in start
module.addGlobal(internalName, nativeType, true, global.type.toNativeZero(module));
this.startFunctionBody.push(module.createSetGlobal(internalName, initExpr));
} else { // compile as-is
if (global.is(CommonFlags.CONST)) {
let exprType = _BinaryenExpressionGetType(initExpr);
switch (exprType) {
case NativeType.I32: {
global.constantValueKind = ConstantValueKind.INTEGER;
global.constantIntegerValue = i64_new(_BinaryenConstGetValueI32(initExpr), 0);
break;
}
case NativeType.I64: {
global.constantValueKind = ConstantValueKind.INTEGER;
global.constantIntegerValue = i64_new(
_BinaryenConstGetValueI64Low(initExpr),
_BinaryenConstGetValueI64High(initExpr)
);
break;
}
case NativeType.F32: {
global.constantValueKind = ConstantValueKind.FLOAT;
global.constantFloatValue = _BinaryenConstGetValueF32(initExpr);
break;
}
case NativeType.F64: {
global.constantValueKind = ConstantValueKind.FLOAT;
global.constantFloatValue = _BinaryenConstGetValueF64(initExpr);
break;
}
default: {
assert(false);
this.error(
DiagnosticCode.Operation_not_supported,
global.declaration.range
);
global.constantValueKind = ConstantValueKind.INTEGER;
global.constantIntegerValue = i64_new(0);
break;
}
}
global.set(CommonFlags.INLINED); // inline the value from now on
if (declaration.isTopLevel) { // but keep the element if it might be re-exported
module.addGlobal(internalName, nativeType, false, initExpr);
}
if (declaration.range.source.isEntry && declaration.isTopLevelExport) {
module.addGlobalExport(global.internalName, declaration.programLevelInternalName);
}
} else /* mutable */ {
module.addGlobal(internalName, nativeType, !global.is(CommonFlags.CONST), initExpr);
}
}
return true;
}
// enums
compileEnumDeclaration(declaration: EnumDeclaration): Enum | null {
var element = assert(this.program.elementsLookup.get(declaration.fileLevelInternalName));
assert(element.kind == ElementKind.ENUM);
if (!this.compileEnum(<Enum>element)) return null;
return <Enum>element;
}
compileEnum(element: Enum): bool {
if (element.is(CommonFlags.COMPILED)) return true;
element.set(CommonFlags.COMPILED);
var module = this.module;
this.currentEnum = element;
var previousValue: EnumValue | null = null;
if (element.members) {
for (let member of element.members.values()) {
if (member.kind != ElementKind.ENUMVALUE) continue; // happens if an enum is also a namespace
let initInStart = false;
let val = <EnumValue>member;
let valueDeclaration = val.declaration;
val.set(CommonFlags.COMPILED);
if (val.is(CommonFlags.INLINED)) {
if (element.declaration.isTopLevelExport) {
module.addGlobal(
val.internalName,
NativeType.I32,
false, // constant
module.createI32(val.constantValue)
);
}
} else {
let initExpr: ExpressionRef;
if (valueDeclaration.value) {
initExpr = this.compileExpression(<Expression>valueDeclaration.value, Type.i32);
if (_BinaryenExpressionGetId(initExpr) != ExpressionId.Const) {
initExpr = this.precomputeExpressionRef(initExpr);
if (_BinaryenExpressionGetId(initExpr) != ExpressionId.Const) {
if (element.is(CommonFlags.CONST)) {
this.warning(
DiagnosticCode.Compiling_constant_with_non_constant_initializer_as_mutable,
valueDeclaration.range
);
}
initInStart = true;
}
}
} else if (previousValue == null) {
initExpr = module.createI32(0);
} else if (previousValue.is(CommonFlags.INLINED)) {
initExpr = module.createI32(previousValue.constantValue + 1);
} else {
// in TypeScript this errors with TS1061, but actually we can do:
initExpr = module.createBinary(BinaryOp.AddI32,
module.createGetGlobal(previousValue.internalName, NativeType.I32),
module.createI32(1)
);
if (element.is(CommonFlags.CONST)) {
this.warning(
DiagnosticCode.Compiling_constant_with_non_constant_initializer_as_mutable,
valueDeclaration.range
);
}
initInStart = true;
}
if (initInStart) {
module.addGlobal(
val.internalName,
NativeType.I32,
true, // mutable
module.createI32(0)
);
this.startFunctionBody.push(module.createSetGlobal(val.internalName, initExpr));
} else {
module.addGlobal(val.internalName, NativeType.I32, false, initExpr);
if (_BinaryenExpressionGetType(initExpr) == NativeType.I32) {
val.constantValue = _BinaryenConstGetValueI32(initExpr);
val.set(CommonFlags.INLINED);
} else {
assert(false);
this.error(
DiagnosticCode.Operation_not_supported,
valueDeclaration.range
);
val.constantValue = 0;
}
}
}
previousValue = <EnumValue>val;
// export values if the enum is exported
if (element.declaration.range.source.isEntry && element.declaration.isTopLevelExport) {
if (member.is(CommonFlags.INLINED)) {
module.addGlobalExport(member.internalName, member.internalName);
} else if (valueDeclaration) {
this.warning(
DiagnosticCode.Cannot_export_a_mutable_global,
valueDeclaration.range
);
}
}
}
}
this.currentEnum = null;
return true;
}
// functions
/** Compiles a function given its declaration. */
compileFunctionDeclaration(
declaration: FunctionDeclaration,
typeArguments: TypeNode[],
contextualTypeArguments: Map<string,Type> | null = null
): Function | null {
var element = assert(this.program.elementsLookup.get(declaration.fileLevelInternalName));
assert(element.kind == ElementKind.FUNCTION_PROTOTYPE);
return this.compileFunctionUsingTypeArguments( // reports
<FunctionPrototype>element,
typeArguments,
contextualTypeArguments,
(<FunctionPrototype>element).declaration.name
);
}
/** Resolves the specified type arguments prior to compiling the resulting function instance. */
compileFunctionUsingTypeArguments(
prototype: FunctionPrototype,
typeArguments: TypeNode[],
contextualTypeArguments: Map<string,Type> | null,
reportNode: Node
): Function | null {
var instance = prototype.resolveUsingTypeArguments( // reports
typeArguments,
contextualTypeArguments,
reportNode
);
if (!(instance && this.compileFunction(instance))) return null;
return instance;
}
/** Either reuses or creates the function type matching the specified signature. */
private ensureFunctionType(signature: Signature): FunctionTypeRef {
var parameters = signature.parameterTypes;
var numParameters = parameters.length;
var thisType = signature.thisType;
var paramTypes: NativeType[];
var index = 0;
if (thisType) {
paramTypes = new Array(1 + numParameters);
paramTypes[0] = thisType.toNativeType();
index = 1;
} else {
paramTypes = new Array(numParameters);
}
for (let i = 0; i < numParameters; ++i, ++index) {
paramTypes[index] = signature.parameterTypes[i].toNativeType();
}
var resultType = signature.returnType.toNativeType();
var module = this.module;
var typeRef = module.getFunctionTypeBySignature(resultType, paramTypes);
if (!typeRef) {
typeRef = module.addFunctionType(signature.toSignatureString(), resultType, paramTypes);
}
return typeRef;
}
/** Compiles a readily resolved function instance. */
compileFunction(instance: Function): bool {
if (instance.is(CommonFlags.COMPILED)) return true;
assert(!instance.is(CommonFlags.BUILTIN) || instance.simpleName == "abort");
instance.set(CommonFlags.COMPILED);
// check that modifiers are matching but still compile as-is
var declaration = instance.prototype.declaration;
var body = declaration.body;
if (body) {
if (instance.is(CommonFlags.DECLARE)) {
this.error(
DiagnosticCode.An_implementation_cannot_be_declared_in_ambient_contexts,
declaration.name.range
);
}
} else {
if (!instance.is(CommonFlags.DECLARE)) {
this.error(
DiagnosticCode.Function_implementation_is_missing_or_not_immediately_following_the_declaration,
declaration.name.range
);
}
}
var ref: FunctionRef;
var typeRef = this.ensureFunctionType(instance.signature);
var module = this.module;
if (body) {
// compile body
let previousFunction = this.currentFunction;
this.currentFunction = instance;
let stmt = this.compileStatement(body);
// make sure all branches return
let allBranchesReturn = instance.flow.finalize();
let returnType = instance.signature.returnType;
if (returnType != Type.void && !allBranchesReturn) {
this.error(
DiagnosticCode.A_function_whose_declared_type_is_not_void_must_return_a_value,
assert(declaration.signature.returnType, "return type expected").range
);
}
this.currentFunction = previousFunction;
// create the function
ref = module.addFunction(
instance.internalName,
typeRef,
typesToNativeTypes(instance.additionalLocals),
stmt
);
} else {
instance.set(CommonFlags.MODULE_IMPORT);
// create the function import
let namespace = instance.prototype.namespace;
ref = module.addFunctionImport(
instance.internalName,
namespace
? namespace.simpleName
: "env",
instance.simpleName,
typeRef
);
}
// check module-level export
if (declaration.range.source.isEntry && declaration.isTopLevelExport) {
module.addFunctionExport(instance.internalName, declaration.name.text);
}
instance.finalize(module, ref);
return true;
}
// namespaces
compileNamespaceDeclaration(declaration: NamespaceDeclaration): void {
var members = declaration.members;
var noTreeShaking = this.options.noTreeShaking;
for (let i = 0, k = members.length; i < k; ++i) {
let member = members[i];
switch (member.kind) {
case NodeKind.CLASSDECLARATION: {
if (
(noTreeShaking || member.is(CommonFlags.EXPORT)) &&
!(<ClassDeclaration>member).isGeneric
) {
this.compileClassDeclaration(<ClassDeclaration>member, []);
}
break;
}
case NodeKind.INTERFACEDECLARATION: {
if (
(noTreeShaking || member.is(CommonFlags.EXPORT)) &&
!(<InterfaceDeclaration>member).isGeneric
) {
this.compileInterfaceDeclaration(<InterfaceDeclaration>member, []);
}
break;
}
case NodeKind.ENUMDECLARATION: {
if (noTreeShaking || member.is(CommonFlags.EXPORT)) {
this.compileEnumDeclaration(<EnumDeclaration>member);
}
break;
}
case NodeKind.FUNCTIONDECLARATION: {
if (
(noTreeShaking || member.is(CommonFlags.EXPORT)) &&
!(<FunctionDeclaration>member).isGeneric
) {
this.compileFunctionDeclaration(<FunctionDeclaration>member, []);
}
break;
}
case NodeKind.NAMESPACEDECLARATION: {
if (noTreeShaking || member.is(CommonFlags.EXPORT)) {
this.compileNamespaceDeclaration(<NamespaceDeclaration>member);
}
break;
}
case NodeKind.VARIABLE: {
if (noTreeShaking || member.is(CommonFlags.EXPORT)) {
let variableInit = this.compileVariableStatement(<VariableStatement>member, true);
if (variableInit) this.startFunctionBody.push(variableInit);
}
break;
}
default: {
assert(false);
this.error(
DiagnosticCode.Operation_not_supported,
member.range
);
break;
}
}
}
}
compileNamespace(ns: Namespace): void {
if (!ns.members) return;
var noTreeShaking = this.options.noTreeShaking;
for (let element of ns.members.values()) {
switch (element.kind) {
case ElementKind.CLASS_PROTOTYPE: {
if (
(
noTreeShaking ||
(<ClassPrototype>element).is(CommonFlags.EXPORT)
) && !(<ClassPrototype>element).is(CommonFlags.GENERIC)
) {
this.compileClassUsingTypeArguments(<ClassPrototype>element, []);
}
break;
}
case ElementKind.ENUM: {
this.compileEnum(<Enum>element);
break;
}
case ElementKind.FUNCTION_PROTOTYPE: {
if (
(
noTreeShaking || (<FunctionPrototype>element).is(CommonFlags.EXPORT)
) && !(<FunctionPrototype>element).is(CommonFlags.GENERIC)
) {
this.compileFunctionUsingTypeArguments(
<FunctionPrototype>element,
[],
null,
(<FunctionPrototype>element).declaration.name
);
}
break;
}
case ElementKind.GLOBAL: {
this.compileGlobal(<Global>element);
break;
}
case ElementKind.NAMESPACE: {
this.compileNamespace(<Namespace>element);
break;
}
}
}
}
// exports
compileExportStatement(statement: ExportStatement): void {
var module = this.module;
var exports = this.program.fileLevelExports;
var members = statement.members;
for (let i = 0, k = members.length; i < k; ++i) {
let member = members[i];
let internalExportName = (
statement.range.source.internalPath +
PATH_DELIMITER +
member.externalName.text
);
let element = exports.get(internalExportName);
if (!element) continue; // reported in Program#initialize
switch (element.kind) {
case ElementKind.CLASS_PROTOTYPE: {
if (!(<ClassPrototype>element).is(CommonFlags.GENERIC)) {
this.compileClassUsingTypeArguments(<ClassPrototype>element, []);
}
break;
}
case ElementKind.ENUM: {
this.compileEnum(<Enum>element);
break;
}
case ElementKind.FUNCTION_PROTOTYPE: {
if (
!(<FunctionPrototype>element).is(CommonFlags.GENERIC) &&
statement.range.source.isEntry
) {
let functionInstance = this.compileFunctionUsingTypeArguments(
<FunctionPrototype>element,
[],
null,
(<FunctionPrototype>element).declaration.name
);
if (functionInstance) {
let functionDeclaration = functionInstance.prototype.declaration;
if (functionDeclaration && functionDeclaration.needsExplicitExport(member)) {
module.addFunctionExport(functionInstance.internalName, member.externalName.text);
}
}
}
break;
}
case ElementKind.GLOBAL: {
if (this.compileGlobal(<Global>element) && statement.range.source.isEntry) {
let globalDeclaration = (<Global>element).declaration;
if (globalDeclaration && globalDeclaration.needsExplicitExport(member)) {
if ((<Global>element).is(CommonFlags.INLINED)) {
module.addGlobalExport(element.internalName, member.externalName.text);
} else {
this.warning(
DiagnosticCode.Cannot_export_a_mutable_global,
member.range
);
}
}
}
break;
}
case ElementKind.NAMESPACE: {
this.compileNamespace(<Namespace>element);
break;
}
}
}
}
// classes
compileClassDeclaration(
declaration: ClassDeclaration,
typeArguments: TypeNode[],
contextualTypeArguments: Map<string,Type> | null = null,
alternativeReportNode: Node | null = null
): void {
var element = assert(this.program.elementsLookup.get(declaration.fileLevelInternalName));
assert(element.kind == ElementKind.CLASS_PROTOTYPE);
this.compileClassUsingTypeArguments(
<ClassPrototype>element,
typeArguments,
contextualTypeArguments,
alternativeReportNode
);
}
compileClassUsingTypeArguments(
prototype: ClassPrototype,
typeArguments: TypeNode[],
contextualTypeArguments: Map<string,Type> | null = null,
alternativeReportNode: Node | null = null
): void {
var instance = prototype.resolveUsingTypeArguments( // reports
typeArguments,
contextualTypeArguments,
alternativeReportNode
);
if (!instance) return;
this.compileClass(instance);
}
compileClass(instance: Class): bool {
if (instance.is(CommonFlags.COMPILED)) return true;
instance.set(CommonFlags.COMPILED);
return true;
}
compileInterfaceDeclaration(
declaration: InterfaceDeclaration,
typeArguments: TypeNode[],
contextualTypeArguments: Map<string,Type> | null = null,
alternativeReportNode: Node | null = null
): void {
// TODO
this.error(
DiagnosticCode.Operation_not_supported,
declaration.range
);
}
// memory
/** Adds a static memory segment with the specified data. */
addMemorySegment(buffer: Uint8Array, alignment: i32 = 8): MemorySegment {
var memoryOffset = i64_align(this.memoryOffset, alignment);
var segment = MemorySegment.create(buffer, memoryOffset);
this.memorySegments.push(segment);
this.memoryOffset = i64_add(memoryOffset, i64_new(buffer.length, 0));
return segment;
}
// function table
/** Ensures that a table entry exists for the specified function and returns its index. */
ensureFunctionTableEntry(func: Function): i32 {
assert(func.is(CommonFlags.COMPILED));
if (func.functionTableIndex >= 0) {
return func.functionTableIndex;
}
var functionTable = this.functionTable;
var index = functionTable.length;
functionTable.push(func);
func.functionTableIndex = index;
return index;
}
// statements
compileStatement(statement: Statement): ExpressionRef {
var module = this.module;
var expr: ExpressionRef;
switch (statement.kind) {
case NodeKind.BLOCK: {
expr = this.compileBlockStatement(<BlockStatement>statement);
break;
}
case NodeKind.BREAK: {
expr = this.compileBreakStatement(<BreakStatement>statement);
break;
}
case NodeKind.CONTINUE: {
expr = this.compileContinueStatement(<ContinueStatement>statement);
break;
}
case NodeKind.DO: {
expr = this.compileDoStatement(<DoStatement>statement);
break;
}
case NodeKind.EMPTY: {
expr = this.compileEmptyStatement(<EmptyStatement>statement);
break;
}
case NodeKind.EXPRESSION: {
expr = this.compileExpressionStatement(<ExpressionStatement>statement);
break;
}
case NodeKind.FOR: {
expr = this.compileForStatement(<ForStatement>statement);
break;
}
case NodeKind.IF: {
expr = this.compileIfStatement(<IfStatement>statement);
break;
}
case NodeKind.RETURN: {
expr = this.compileReturnStatement(<ReturnStatement>statement);
break;
}
case NodeKind.SWITCH: {
expr = this.compileSwitchStatement(<SwitchStatement>statement);
break;
}
case NodeKind.THROW: {
expr = this.compileThrowStatement(<ThrowStatement>statement);
break;
}
case NodeKind.TRY: {
expr = this.compileTryStatement(<TryStatement>statement);
break;
}
case NodeKind.VARIABLE: {
expr = this.compileVariableStatement(<VariableStatement>statement);
if (!expr) expr = module.createNop();
break;
}
case NodeKind.VOID: {
expr = this.compileVoidStatement(<VoidStatement>statement);
break;
}
case NodeKind.WHILE: {
expr = this.compileWhileStatement(<WhileStatement>statement);
break;
}
case NodeKind.TYPEDECLARATION: {
// type declarations must be top-level because function bodies are evaluated when
// reachaable only.
if (this.currentFunction == this.startFunction) {
return module.createNop();
}
// otherwise fall-through
}
default: {
assert(false);
this.error(
DiagnosticCode.Operation_not_supported,
statement.range
);
expr = module.createUnreachable();
break;
}
}
if (this.options.sourceMap) {
addDebugLocation(expr, statement.range, module, this.currentFunction);
}
return expr;
}
compileStatements(statements: Statement[]): ExpressionRef[] {
var numStatements = statements.length;
var stmts = new Array<ExpressionRef>(numStatements);
for (let i = 0; i < numStatements; ++i) {
stmts[i] = this.compileStatement(statements[i]);
}
return stmts; // array of 0-es in noEmit-mode
}
compileBlockStatement(statement: BlockStatement): ExpressionRef {
var statements = statement.statements;
// NOTE that we could optimize this to a NOP if empty or unwrap a single
// statement, but that's not what the source told us to do and left to the
// optimizer.
// Not actually a branch, but can contain its own scoped variables.
var flow = this.currentFunction.flow.enterBranchOrScope();
this.currentFunction.flow = flow;
var stmt = this.module.createBlock(null, this.compileStatements(statements), NativeType.None);
var stmtReturns = flow.is(FlowFlags.RETURNS);
// Switch back to the parent flow
flow = flow.leaveBranchOrScope();
this.currentFunction.flow = flow;
if (stmtReturns) {
flow.set(FlowFlags.RETURNS);
}
return stmt;
}
compileBreakStatement(statement: BreakStatement): ExpressionRef {
var module = this.module;
if (statement.label) {
this.error(
DiagnosticCode.Operation_not_supported,
statement.label.range
);
return module.createUnreachable();
}
var flow = this.currentFunction.flow;
var breakLabel = flow.breakLabel;
if (breakLabel == null) {
this.error(
DiagnosticCode.A_break_statement_can_only_be_used_within_an_enclosing_iteration_or_switch_statement,
statement.range
);
return module.createUnreachable();
}
flow.set(FlowFlags.POSSIBLY_BREAKS);
return module.createBreak(breakLabel);
}
compileContinueStatement(statement: ContinueStatement): ExpressionRef {
var module = this.module;
var label = statement.label;
if (label) {
this.error(
DiagnosticCode.Operation_not_supported,
label.range
);
return module.createUnreachable();
}
// Check if 'continue' is allowed here
var flow = this.currentFunction.flow;
var continueLabel = flow.continueLabel;
if (continueLabel == null) {
this.error(
DiagnosticCode.A_continue_statement_can_only_be_used_within_an_enclosing_iteration_statement,
statement.range
);
return module.createUnreachable();
}
flow.set(FlowFlags.POSSIBLY_CONTINUES);
return module.createBreak(continueLabel);
}
compileDoStatement(statement: DoStatement): ExpressionRef {
// A do statement does not initiate a new branch because it is executed at
// least once, but has its own break and continue labels.
var currentFunction = this.currentFunction;
var label = currentFunction.enterBreakContext();
var flow = currentFunction.flow;
var previousBreakLabel = flow.breakLabel;
var previousContinueLabel = flow.continueLabel;
var breakLabel = "break|" + label;
flow.breakLabel = breakLabel;
var continueLabel = "continue|" + label;
flow.continueLabel = continueLabel;
var body = this.compileStatement(statement.statement);
// Reset to the previous break and continue labels, if any.
flow.breakLabel = previousBreakLabel;
flow.continueLabel = previousContinueLabel;
var module = this.module;
var condExpr = makeIsTrueish(
this.compileExpression(statement.condition, Type.i32, ConversionKind.NONE),
this.currentType,
module
);
// No need to eliminate the condition in generic contexts as the statement is executed anyway.
this.currentFunction.leaveBreakContext();
return module.createBlock(breakLabel, [
module.createLoop(continueLabel,
module.createBlock(null, [
body,
module.createBreak(continueLabel, condExpr)
], NativeType.None))
], NativeType.None);
}
compileEmptyStatement(statement: EmptyStatement): ExpressionRef {
return this.module.createNop();
}
compileExpressionStatement(statement: ExpressionStatement): ExpressionRef {
var expr = this.compileExpression(statement.expression, Type.void, ConversionKind.NONE);
if (this.currentType != Type.void) {
expr = this.module.createDrop(expr);
this.currentType = Type.void;
}
return expr;
}
compileForStatement(statement: ForStatement): ExpressionRef {
// A for statement initiates a new branch with its own scoped variables
// possibly declared in its initializer, and break context.
var currentFunction = this.currentFunction;
var context = currentFunction.enterBreakContext();
var flow = currentFunction.flow.enterBranchOrScope();
currentFunction.flow = flow;
var breakLabel = flow.breakLabel = "break|" + context;
flow.breakLabel = breakLabel;
var continueLabel = "continue|" + context;
flow.continueLabel = continueLabel;
// Compile in correct order
var module = this.module;
var initializer = statement.initializer
? this.compileStatement(<Statement>statement.initializer)
: module.createNop();
var condition = statement.condition
? this.compileExpression(<Expression>statement.condition, Type.i32)
: module.createI32(1);
var incrementor = statement.incrementor
? this.compileExpression(<Expression>statement.incrementor, Type.void)
: module.createNop();
var body = this.compileStatement(statement.statement);
var alwaysReturns = !statement.condition && flow.is(FlowFlags.RETURNS);
// TODO: check other always-true conditions as well, not just omitted
// Switch back to the parent flow
flow = flow.leaveBranchOrScope();
currentFunction.flow = flow;
currentFunction.leaveBreakContext();
var expr = module.createBlock(breakLabel, [
initializer,
module.createLoop(continueLabel, module.createBlock(null, [
module.createIf(condition, module.createBlock(null, [
body,
incrementor,
module.createBreak(continueLabel)
], NativeType.None))
], NativeType.None))
], NativeType.None);
// If the loop is guaranteed to run and return, propagate that and append a hint
if (alwaysReturns) {
flow.set(FlowFlags.RETURNS);
expr = module.createBlock(null, [
expr,
module.createUnreachable()
]);
}
return expr;
}
compileIfStatement(statement: IfStatement): ExpressionRef {
var module = this.module;
var currentFunction = this.currentFunction;
var ifTrue = statement.ifTrue;
var ifFalse = statement.ifFalse;
// The condition doesn't initiate a branch yet
var condExpr = makeIsTrueish(
this.compileExpression(statement.condition, Type.i32, ConversionKind.NONE),
this.currentType,
module
);
// Eliminate unnecesssary branches in generic contexts if the condition is constant
if (
this.currentFunction.isAny(CommonFlags.GENERIC | CommonFlags.GENERIC_CONTEXT) &&
_BinaryenExpressionGetId(condExpr = this.precomputeExpressionRef(condExpr)) == ExpressionId.Const &&
_BinaryenExpressionGetType(condExpr) == NativeType.I32
) {
let ret: ExpressionRef;
if (_BinaryenConstGetValueI32(condExpr)) {
ret = this.compileStatement(ifTrue);
} else if (ifFalse) {
ret = this.compileStatement(ifFalse);
} else {
ret = module.createNop();
}
return ret;
}
// Each arm initiates a branch
var flow = currentFunction.flow.enterBranchOrScope();
currentFunction.flow = flow;
var ifTrueExpr = this.compileStatement(ifTrue);
var ifTrueReturns = flow.is(FlowFlags.RETURNS);
flow = flow.leaveBranchOrScope();
currentFunction.flow = flow;
var ifFalseExpr: ExpressionRef = 0;
var ifFalseReturns = false;
if (ifFalse) {
flow = flow.enterBranchOrScope();
currentFunction.flow = flow;
ifFalseExpr = this.compileStatement(ifFalse);
ifFalseReturns = flow.is(FlowFlags.RETURNS);
flow = flow.leaveBranchOrScope();
currentFunction.flow = flow;
}
if (ifTrueReturns && ifFalseReturns) { // not necessary to append a hint
flow.set(FlowFlags.RETURNS);
}
return module.createIf(condExpr, ifTrueExpr, ifFalseExpr);
}
compileReturnStatement(statement: ReturnStatement): ExpressionRef {
var currentFunction = this.currentFunction;
var expression: ExpressionRef = 0;
if (statement.value) {
expression = this.compileExpression(
statement.value,
currentFunction.signature.returnType
);
}
// Remember that this flow returns
currentFunction.flow.set(FlowFlags.RETURNS);
return this.module.createReturn(expression);
}
compileSwitchStatement(statement: SwitchStatement): ExpressionRef {
var module = this.module;
var currentFunction = this.currentFunction;
// Everything within a switch uses the same break context
var context = currentFunction.enterBreakContext();
// introduce a local for evaluating the condition (exactly once)
var tempLocal = currentFunction.getTempLocal(Type.u32);
var tempLocalIndex = tempLocal.index;
var cases = statement.cases;
var numCases = cases.length;
// Prepend initializer to inner block. Does not initiate a new branch, yet.
var breaks = new Array<ExpressionRef>(1 + numCases);
breaks[0] = module.createSetLocal( // initializer
tempLocalIndex,
this.compileExpression(statement.condition, Type.u32)
);
// make one br_if per (possibly dynamic) labeled case (binaryen optimizes to br_table where possible)
var breakIndex = 1;
var defaultIndex = -1;
for (let i = 0; i < numCases; ++i) {
let case_ = cases[i];
let label = case_.label;
if (label) {
breaks[breakIndex++] = module.createBreak("case" + i.toString(10) + "|" + context,
module.createBinary(BinaryOp.EqI32,
module.createGetLocal(tempLocalIndex, NativeType.I32),
this.compileExpression(label, Type.i32)
)
);
} else {
defaultIndex = i;
}
}
currentFunction.freeTempLocal(tempLocal);
// otherwise br to default respectively out of the switch if there is no default case
breaks[breakIndex] = module.createBreak((defaultIndex >= 0
? "case" + defaultIndex.toString(10)
: "break"
) + "|" + context);
// nest blocks in order
var currentBlock = module.createBlock("case0|" + context, breaks, NativeType.None);
var alwaysReturns = true;
for (let i = 0; i < numCases; ++i) {
let case_ = cases[i];
let statements = case_.statements;
let numStatements = statements.length;
let body = new Array<ExpressionRef>(1 + numStatements);
body[0] = currentBlock;
// Each switch case initiates a new branch
let flow = currentFunction.flow.enterBranchOrScope();
currentFunction.flow = flow;
let breakLabel = "break|" + context;
flow.breakLabel = breakLabel;
let fallsThrough = i != numCases - 1;
let nextLabel = !fallsThrough ? breakLabel : "case" + (i + 1).toString(10) + "|" + context;
for (let j = 0; j < numStatements; ++j) {
body[j + 1] = this.compileStatement(statements[j]);
}
if (!(fallsThrough || flow.is(FlowFlags.RETURNS))) {
alwaysReturns = false; // ignore fall-throughs
}
// Switch back to the parent flow
currentFunction.flow = flow.leaveBranchOrScope();
currentBlock = module.createBlock(nextLabel, body, NativeType.None);
}
currentFunction.leaveBreakContext();
// If the switch has a default and always returns, propagate that
if (defaultIndex >= 0 && alwaysReturns) {
currentFunction.flow.set(FlowFlags.RETURNS);
// Binaryen understands that so we don't need a hint
}
return currentBlock;
}
compileThrowStatement(statement: ThrowStatement): ExpressionRef {
var flow = this.currentFunction.flow;
// Remember that this branch possibly throws
flow.set(FlowFlags.POSSIBLY_THROWS);
// FIXME: without try-catch it is safe to assume RETURNS as well for now
flow.set(FlowFlags.RETURNS);
// TODO: requires exception-handling spec.
return this.module.createUnreachable();
}
compileTryStatement(statement: TryStatement): ExpressionRef {
// TODO
// can't yet support something like: try { return ... } finally { ... }
// worthwhile to investigate lowering returns to block results (here)?
this.error(
DiagnosticCode.Operation_not_supported,
statement.range
);
return this.module.createUnreachable();
}
/**
* Compiles a variable statement. Returns `0` if an initializer is not
* necessary.
*/
compileVariableStatement(statement: VariableStatement, isKnownGlobal: bool = false): ExpressionRef {
var program = this.program;
var currentFunction = this.currentFunction;
var declarations = statement.declarations;
var numDeclarations = declarations.length;
// top-level variables and constants become globals
if (isKnownGlobal || (
currentFunction == this.startFunction &&
statement.parent && statement.parent.kind == NodeKind.SOURCE
)) {
// NOTE that the above condition also covers top-level variables declared with 'let', even
// though such variables could also become start function locals if, and only if, not used
// within any function declared in the same source, which is unknown at this point. the only
// efficient way to deal with this would be to keep track of all occasions it is used and
// replace these instructions afterwards, dynamically. (TOOD: what about a Binaryen pass?)
for (let i = 0; i < numDeclarations; ++i) {
this.compileGlobalDeclaration(declarations[i]);
}
return 0;
}
// other variables become locals
var initializers = new Array<ExpressionRef>();
for (let i = 0; i < numDeclarations; ++i) {
let declaration = declarations[i];
let name = declaration.name.text;
let type: Type | null = null;
let init: ExpressionRef = 0;
if (declaration.type) {
type = program.resolveType( // reports
declaration.type,
currentFunction.contextualTypeArguments
);
if (!type) continue;
if (declaration.initializer) {
init = this.compileExpression(declaration.initializer, type); // reports
}
} else if (declaration.initializer) { // infer type using void/NONE for proper literal inference
init = this.compileExpression( // reports
declaration.initializer,
Type.void,
ConversionKind.NONE
);
if (this.currentType == Type.void) {
this.error(
DiagnosticCode.Type_0_is_not_assignable_to_type_1,
declaration.range, this.currentType.toString(), "<auto>"
);
continue;
}
type = this.currentType;
} else {
this.error(
DiagnosticCode.Type_expected,
declaration.name.range.atEnd
);
continue;
}
if (declaration.is(CommonFlags.CONST)) {
if (init) {
init = this.precomputeExpressionRef(init);
if (_BinaryenExpressionGetId(init) == ExpressionId.Const) {
let local = new Local(program, name, -1, type);
switch (_BinaryenExpressionGetType(init)) {
case NativeType.I32: {
local = local.withConstantIntegerValue(_BinaryenConstGetValueI32(init), 0);
break;
}
case NativeType.I64: {
local = local.withConstantIntegerValue(
_BinaryenConstGetValueI64Low(init),
_BinaryenConstGetValueI64High(init)
);
break;
}
case NativeType.F32: {
local = local.withConstantFloatValue(<f64>_BinaryenConstGetValueF32(init));
break;
}
case NativeType.F64: {
local = local.withConstantFloatValue(_BinaryenConstGetValueF64(init));
break;
}
default: {
assert(false);
this.error(
DiagnosticCode.Operation_not_supported,
declaration.range
);
return this.module.createUnreachable();
}
}
// Create a virtual local that doesn't actually exist in WebAssembly
let scopedLocals = currentFunction.flow.scopedLocals;
if (!scopedLocals) currentFunction.flow.scopedLocals = scopedLocals = new Map();
else if (scopedLocals.has(name)) {
this.error(
DiagnosticCode.Duplicate_identifier_0,
declaration.name.range, name
);
return 0;
}
scopedLocals.set(name, local);
return 0;
} 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 (declaration.is(CommonFlags.LET)) { // here: not top-level
currentFunction.flow.addScopedLocal(name, type, declaration.name); // reports
} else {
currentFunction.addLocal(type, name); // reports
}
if (init) {
initializers.push(this.compileAssignmentWithValue(declaration.name, init));
}
}
return initializers.length // we can unwrap these here because the
? initializers.length == 1 // source didn't tell us exactly what to do
? initializers[0]
: this.module.createBlock(null, initializers, NativeType.None)
: 0;
}
compileVoidStatement(statement: VoidStatement): ExpressionRef {
return this.compileExpression(statement.expression, Type.void, ConversionKind.EXPLICIT, false);
}
compileWhileStatement(statement: WhileStatement): ExpressionRef {
var module = this.module;
// The condition does not yet initialize a branch
var condExpr = makeIsTrueish(
this.compileExpression(statement.condition, Type.i32, ConversionKind.NONE),
this.currentType,
module
);
// Eliminate unnecesssary loops in generic contexts if the condition is constant
if (
this.currentFunction.isAny(CommonFlags.GENERIC | CommonFlags.GENERIC_CONTEXT) &&
_BinaryenExpressionGetId(condExpr = this.precomputeExpressionRef(condExpr)) == ExpressionId.Const &&
_BinaryenExpressionGetType(condExpr) == NativeType.I32
) {
if (!_BinaryenConstGetValueI32(condExpr)) {
return module.createNop();
}
}
// Statements initiate a new branch with its own break context
var currentFunction = this.currentFunction;
var label = currentFunction.enterBreakContext();
var flow = currentFunction.flow.enterBranchOrScope();
currentFunction.flow = flow;
var breakLabel = "break|" + label;
flow.breakLabel = breakLabel;
var continueLabel = "continue|" + label;
flow.continueLabel = continueLabel;
var body = this.compileStatement(statement.statement);
var alwaysReturns = false && flow.is(FlowFlags.RETURNS);
// TODO: evaluate possible always-true conditions
// Switch back to the parent flow
currentFunction.flow = flow.leaveBranchOrScope();
currentFunction.leaveBreakContext();
var expr = module.createBlock(breakLabel, [
module.createLoop(continueLabel,
module.createIf(condExpr, module.createBlock(null, [
body,
module.createBreak(continueLabel)
], NativeType.None))
)
], NativeType.None);
// If the loop is guaranteed to run and return, propagate that and append a hint
if (alwaysReturns) {
expr = module.createBlock(null, [
expr,
module.createUnreachable()
]);
}
return expr;
}
// expressions
/**
* Compiles the value of an inlined constant element.
* @param retainType If true, the annotated type of the constant is retained. Otherwise, the value
* is precomputed according to context.
*/
compileInlineConstant(
element: VariableLikeElement,
contextualType: Type,
retainType: bool
): ExpressionRef {
assert(element.is(CommonFlags.INLINED));
var type = element.type;
switch (
!retainType &&
type.is(TypeFlags.INTEGER) &&
contextualType.is(TypeFlags.INTEGER) &&
type.size < contextualType.size
? (this.currentType = contextualType).kind // essentially precomputes a (sign-)extension
: (this.currentType = type).kind
) {
case TypeKind.I8:
case TypeKind.I16: {
let shift = type.computeSmallIntegerShift(Type.i32);
return this.module.createI32(
element.constantValueKind == ConstantValueKind.INTEGER
? i64_low(element.constantIntegerValue) << shift >> shift
: 0
);
}
case TypeKind.U8:
case TypeKind.U16:
case TypeKind.BOOL: {
let mask = element.type.computeSmallIntegerMask(Type.i32);
return this.module.createI32(
element.constantValueKind == ConstantValueKind.INTEGER
? i64_low(element.constantIntegerValue) & mask
: 0
);
}
case TypeKind.I32:
case TypeKind.U32: {
return this.module.createI32(
element.constantValueKind == ConstantValueKind.INTEGER
? i64_low(element.constantIntegerValue)
: 0
);
}
case TypeKind.ISIZE:
case TypeKind.USIZE: {
if (!element.program.options.isWasm64) {
return this.module.createI32(
element.constantValueKind == ConstantValueKind.INTEGER
? i64_low(element.constantIntegerValue)
: 0
);
}
// fall-through
}
case TypeKind.I64:
case TypeKind.U64: {
return element.constantValueKind == ConstantValueKind.INTEGER
? this.module.createI64(
i64_low(element.constantIntegerValue),
i64_high(element.constantIntegerValue)
)
: this.module.createI64(0);
}
case TypeKind.F32: {
return this.module.createF32((<VariableLikeElement>element).constantFloatValue);
}
case TypeKind.F64: {
return this.module.createF64((<VariableLikeElement>element).constantFloatValue);
}
default: {
assert(false);
this.error(
DiagnosticCode.Operation_not_supported,
element.declaration.range
);
return this.module.createUnreachable();
}
}
}
compileExpression(
expression: Expression,
contextualType: Type,
conversionKind: ConversionKind = ConversionKind.IMPLICIT,
wrapSmallIntegers: bool = true
): ExpressionRef {
this.currentType = contextualType;
var expr: ExpressionRef;
switch (expression.kind) {
case NodeKind.ASSERTION: {
expr = this.compileAssertionExpression(<AssertionExpression>expression, contextualType);
break;
}
case NodeKind.BINARY: {
expr = this.compileBinaryExpression(<BinaryExpression>expression, contextualType, wrapSmallIntegers);
break;
}
case NodeKind.CALL: {
expr = this.compileCallExpression(<CallExpression>expression, contextualType);
break;
}
case NodeKind.COMMA: {
expr = this.compileCommaExpression(<CommaExpression>expression, contextualType);
break;
}
case NodeKind.ELEMENTACCESS: {
expr = this.compileElementAccessExpression(<ElementAccessExpression>expression, contextualType);
break;
}
case NodeKind.FUNCTION: {
expr = this.compileFunctionExpression(<FunctionExpression>expression, contextualType);
break;
}
case NodeKind.IDENTIFIER:
case NodeKind.FALSE:
case NodeKind.NULL:
case NodeKind.THIS:
case NodeKind.TRUE: {
expr = this.compileIdentifierExpression(
<IdentifierExpression>expression,
contextualType,
conversionKind == ConversionKind.NONE // retain type of inlined constants
);
break;
}
case NodeKind.LITERAL: {
expr = this.compileLiteralExpression(<LiteralExpression>expression, contextualType);
break;
}
case NodeKind.NEW: {
expr = this.compileNewExpression(<NewExpression>expression, contextualType);
break;
}
case NodeKind.PARENTHESIZED: {
expr = this.compileParenthesizedExpression(
<ParenthesizedExpression>expression,
contextualType,
wrapSmallIntegers
);
break;
}
case NodeKind.PROPERTYACCESS: {
expr = this.compilePropertyAccessExpression(
<PropertyAccessExpression>expression,
contextualType,
conversionKind == ConversionKind.NONE // retain type of inlined constants
);
break;
}
case NodeKind.TERNARY: {
expr = this.compileTernaryExpression(<TernaryExpression>expression, contextualType);
break;
}
case NodeKind.UNARYPOSTFIX: {
expr = this.compileUnaryPostfixExpression(<UnaryPostfixExpression>expression, contextualType);
break;
}
case NodeKind.UNARYPREFIX: {
expr = this.compileUnaryPrefixExpression(<UnaryPrefixExpression>expression, contextualType, wrapSmallIntegers);
break;
}
default: {
assert(false);
this.error(
DiagnosticCode.Operation_not_supported,
expression.range
);
expr = this.module.createUnreachable();
break;
}
}
var currentType = this.currentType;
if (conversionKind != ConversionKind.NONE && currentType != contextualType) {
expr = this.convertExpression(expr, currentType, contextualType, conversionKind, expression);
this.currentType = contextualType;
}
if (this.options.sourceMap) {
addDebugLocation(expr, expression.range, this.module, this.currentFunction);
}
return expr;
}
compileExpressionRetainType(
expression: Expression,
contextualType: Type,
wrapSmallIntegers: bool = true
): ExpressionRef {
return this.compileExpression(
expression,
contextualType == Type.void
? Type.i32
: contextualType,
ConversionKind.NONE,
wrapSmallIntegers
);
}
precomputeExpression(
expression: Expression,
contextualType: Type,
conversionKind: ConversionKind = ConversionKind.IMPLICIT
): ExpressionRef {
return this.precomputeExpressionRef(this.compileExpression(expression, contextualType, conversionKind));
}
precomputeExpressionRef(expr: ExpressionRef): ExpressionRef {
var module = this.module;
var type = this.currentType;
var nativeType = type.toNativeType();
var typeRef = module.getFunctionTypeBySignature(nativeType, null);
var typeRefAdded = false;
if (!typeRef) {
typeRef = module.addFunctionType(type.toSignatureString(), nativeType, null);
typeRefAdded = true;
}
var funcRef = module.addFunction("__precompute", typeRef, null, expr);
module.runPasses([ "precompute" ], funcRef);
var ret = _BinaryenFunctionGetBody(funcRef);
module.removeFunction("__precompute");
if (typeRefAdded) {
// TODO: also remove the function type somehow if no longer used or make the C-API accept
// a `null` typeRef, using an implicit type.
}
return ret;
}
convertExpression(
expr: ExpressionRef,
fromType: Type,
toType: Type,
conversionKind: ConversionKind,
reportNode: Node
): ExpressionRef {
assert(conversionKind != ConversionKind.NONE);
var module = this.module;
// void to any
if (fromType.kind == TypeKind.VOID) {
this.error(
DiagnosticCode.Type_0_is_not_assignable_to_type_1,
reportNode.range, fromType.toString(), toType.toString()
);
return module.createUnreachable();
}
// any to void
if (toType.kind == TypeKind.VOID) {
return module.createDrop(expr);
}
if (conversionKind == ConversionKind.IMPLICIT && !fromType.isAssignableTo(toType)) {
this.error(
DiagnosticCode.Conversion_from_type_0_to_1_requires_an_explicit_cast,
reportNode.range, fromType.toString(), toType.toString()
); // recoverable
}
// TODO: make this a proper switch?
if (fromType.is(TypeFlags.FLOAT)) {
// float to float
if (toType.is(TypeFlags.FLOAT)) {
if (fromType.kind == TypeKind.F32) {
// f32 to f64
if (toType.kind == TypeKind.F64) {
expr = module.createUnary(UnaryOp.PromoteF32, expr);
}
// otherwise f32 to f32
// f64 to f32
} else if (toType.kind == TypeKind.F32) {
expr = module.createUnary(UnaryOp.DemoteF64, expr);
}
// otherwise f64 to f64
// float to int
} else if (toType.is(TypeFlags.INTEGER)) {
// f32 to int
if (fromType.kind == TypeKind.F32) {
if (toType.is(TypeFlags.SIGNED)) {
if (toType.is(TypeFlags.LONG)) {
expr = module.createUnary(UnaryOp.TruncF32ToI64, expr);
} else {
expr = module.createUnary(UnaryOp.TruncF32ToI32, expr);
if (toType.is(TypeFlags.SMALL)) {
expr = makeSmallIntegerWrap(expr, toType, module);
}
}
} else {
if (toType.is(TypeFlags.LONG)) {
expr = module.createUnary(UnaryOp.TruncF32ToU64, expr);
} else {
expr = module.createUnary(UnaryOp.TruncF32ToU32, expr);
if (toType.is(TypeFlags.SMALL)) {
expr = makeSmallIntegerWrap(expr, toType, module);
}
}
}
// f64 to int
} else {
if (toType.is(TypeFlags.SIGNED)) {
if (toType.is(TypeFlags.LONG)) {
expr = module.createUnary(UnaryOp.TruncF64ToI64, expr);
} else {
expr = module.createUnary(UnaryOp.TruncF64ToI32, expr);
if (toType.is(TypeFlags.SMALL)) {
expr = makeSmallIntegerWrap(expr, toType, module);
}
}
} else {
if (toType.is(TypeFlags.LONG)) {
expr = module.createUnary(UnaryOp.TruncF64ToU64, expr);
} else {
expr = module.createUnary(UnaryOp.TruncF64ToU32, expr);
if (toType.is(TypeFlags.SMALL)) {
expr = makeSmallIntegerWrap(expr, toType, module);
}
}
}
}
// float to void
} else {
assert(toType.flags == TypeFlags.NONE, "void type expected");
expr = module.createDrop(expr);
}
// int to float
} else if (fromType.is(TypeFlags.INTEGER) && toType.is(TypeFlags.FLOAT)) {
// int to f32
if (toType.kind == TypeKind.F32) {
if (fromType.is(TypeFlags.LONG)) {
expr = module.createUnary(
fromType.is(TypeFlags.SIGNED)
? UnaryOp.ConvertI64ToF32
: UnaryOp.ConvertU64ToF32,
expr
);
} else {
expr = module.createUnary(
fromType.is(TypeFlags.SIGNED)
? UnaryOp.ConvertI32ToF32
: UnaryOp.ConvertU32ToF32,
expr
);
}
// int to f64
} else {
if (fromType.is(TypeFlags.LONG)) {
expr = module.createUnary(
fromType.is(TypeFlags.SIGNED)
? UnaryOp.ConvertI64ToF64
: UnaryOp.ConvertU64ToF64,
expr
);
} else {
expr = module.createUnary(
fromType.is(TypeFlags.SIGNED)
? UnaryOp.ConvertI32ToF64
: UnaryOp.ConvertU32ToF64,
expr
);
}
}
// int to int
} else {
if (fromType.is(TypeFlags.LONG)) {
// i64 to i32
if (!toType.is(TypeFlags.LONG)) {
expr = module.createUnary(UnaryOp.WrapI64, expr); // discards upper bits
if (toType.is(TypeFlags.SMALL)) {
expr = makeSmallIntegerWrap(expr, toType, module);
}
}
// i32 to i64
} else if (toType.is(TypeFlags.LONG)) {
expr = module.createUnary(toType.is(TypeFlags.SIGNED) ? UnaryOp.ExtendI32 : UnaryOp.ExtendU32, expr);
// i32 or smaller to even smaller or same size int with change of sign
} else if (
toType.is(TypeFlags.SMALL) &&
(
fromType.size > toType.size ||
(
fromType.size == toType.size &&
fromType.is(TypeFlags.SIGNED) != toType.is(TypeFlags.SIGNED)
)
)
) {
expr = makeSmallIntegerWrap(expr, toType, module);
}
// otherwise (smaller) i32/u32 to (same size) i32/u32
}
this.currentType = toType;
return expr;
}
compileAssertionExpression(expression: AssertionExpression, contextualType: Type): ExpressionRef {
var toType = this.program.resolveType( // reports
expression.toType,
this.currentFunction.contextualTypeArguments
);
if (!toType) return this.module.createUnreachable();
return this.compileExpression(expression.expression, toType, ConversionKind.EXPLICIT);
}
compileBinaryExpression(
expression: BinaryExpression,
contextualType: Type,
wrapSmallIntegers: bool = true
): ExpressionRef {
var module = this.module;
var left = expression.left;
var right = expression.right;
var leftExpr: ExpressionRef;
var leftType: Type;
var rightExpr: ExpressionRef;
var rightType: Type;
var commonType: Type | null;
var condExpr: ExpressionRef;
var expr: ExpressionRef;
var compound = false;
var possiblyOverflows = false;
var tempLocal: Local | null = null;
switch (expression.operator) {
case Token.LESSTHAN: {
leftExpr = this.compileExpressionRetainType(left, contextualType);
leftType = this.currentType;
rightExpr = this.compileExpressionRetainType(right, leftType);
rightType = this.currentType;
if (commonType = Type.commonCompatible(leftType, rightType, true)) {
leftExpr = this.convertExpression(leftExpr, leftType, commonType, ConversionKind.IMPLICIT, left);
rightExpr = this.convertExpression(rightExpr, rightType, commonType, ConversionKind.IMPLICIT, right);
} else {
this.error(
DiagnosticCode.Operator_0_cannot_be_applied_to_types_1_and_2,
expression.range, "<", leftType.toString(), rightType.toString()
);
this.currentType = contextualType;
return module.createUnreachable();
}
switch (commonType.kind) {
case TypeKind.I8:
case TypeKind.I16:
case TypeKind.I32: {
expr = module.createBinary(BinaryOp.LtI32, leftExpr, rightExpr);
break;
}
case TypeKind.I64: {
expr = module.createBinary(BinaryOp.LtI64, leftExpr, rightExpr);
break;
}
case TypeKind.ISIZE: {
expr = module.createBinary(
this.options.isWasm64
? BinaryOp.LtI64
: BinaryOp.LtI32,
leftExpr,
rightExpr
);
break;
}
case TypeKind.U8:
case TypeKind.U16:
case TypeKind.U32:
case TypeKind.BOOL: {
expr = module.createBinary(BinaryOp.LtU32, leftExpr, rightExpr);
break;
}
case TypeKind.USIZE: { // TODO: check operator overload
expr = module.createBinary(
this.options.isWasm64
? BinaryOp.LtU64
: BinaryOp.LtU32,
leftExpr,
rightExpr
);
break;
}
case TypeKind.U64: {
expr = module.createBinary(BinaryOp.LtU64, leftExpr, rightExpr);
break;
}
case TypeKind.F32: {
expr = module.createBinary(BinaryOp.LtF32, leftExpr, rightExpr);
break;
}
case TypeKind.F64: {
expr = module.createBinary(BinaryOp.LtF64, leftExpr, rightExpr);
break;
}
default: {
assert(false);
this.error(
DiagnosticCode.Operation_not_supported,
expression.range
);
expr = module.createUnreachable();
break;
}
}
this.currentType = Type.bool;
break;
}
case Token.GREATERTHAN: {
leftExpr = this.compileExpressionRetainType(left, contextualType);
leftType = this.currentType;
rightExpr = this.compileExpressionRetainType(right, leftType);
rightType = this.currentType;
if (commonType = Type.commonCompatible(leftType, rightType, true)) {
leftExpr = this.convertExpression(leftExpr, leftType, commonType, ConversionKind.IMPLICIT, left);
rightExpr = this.convertExpression(rightExpr, rightType, commonType, ConversionKind.IMPLICIT, right);
} else {
this.error(
DiagnosticCode.Operator_0_cannot_be_applied_to_types_1_and_2,
expression.range, ">", leftType.toString(), rightType.toString()
);
this.currentType = contextualType;
return module.createUnreachable();
}
switch (commonType.kind) {
case TypeKind.I8:
case TypeKind.I16:
case TypeKind.I32: {
expr = module.createBinary(BinaryOp.GtI32, leftExpr, rightExpr);
break;
}
case TypeKind.ISIZE: {
expr = module.createBinary(
this.options.isWasm64
? BinaryOp.GtI64
: BinaryOp.GtI32,
leftExpr,
rightExpr
);
break;
}
case TypeKind.I64: {
expr = module.createBinary(BinaryOp.GtI64, leftExpr, rightExpr);
break;
}
case TypeKind.U8:
case TypeKind.U16:
case TypeKind.U32:
case TypeKind.BOOL: {
expr = module.createBinary(BinaryOp.GtU32, leftExpr, rightExpr);
break;
}
case TypeKind.USIZE: { // TODO: check operator overload
expr = module.createBinary(
this.options.isWasm64
? BinaryOp.GtU64
: BinaryOp.GtU32,
leftExpr,
rightExpr
);
break;
}
case TypeKind.U64: {
expr = module.createBinary(BinaryOp.GtU64, leftExpr, rightExpr);
break;
}
case TypeKind.F32: {
expr = module.createBinary(BinaryOp.GtF32, leftExpr, rightExpr);
break;
}
case TypeKind.F64: {
expr = module.createBinary(BinaryOp.GtF64, leftExpr, rightExpr);
break;
}
default: {
assert(false);
this.error(
DiagnosticCode.Operation_not_supported,
expression.range
);
expr = module.createUnreachable();
break;
}
}
this.currentType = Type.bool;
break;
}
case Token.LESSTHAN_EQUALS: {
leftExpr = this.compileExpressionRetainType(left, contextualType);
leftType = this.currentType;
rightExpr = this.compileExpressionRetainType(right, leftType);
rightType = this.currentType;
if (commonType = Type.commonCompatible(leftType, rightType, true)) {
leftExpr = this.convertExpression(leftExpr, leftType, commonType, ConversionKind.IMPLICIT, left);
rightExpr = this.convertExpression(rightExpr, rightType, commonType, ConversionKind.IMPLICIT, right);
} else {
this.error(
DiagnosticCode.Operator_0_cannot_be_applied_to_types_1_and_2,
expression.range, "<=", leftType.toString(), rightType.toString()
);
this.currentType = contextualType;
return module.createUnreachable();
}
switch (commonType.kind) {
case TypeKind.I8:
case TypeKind.I16:
case TypeKind.I32: {
expr = module.createBinary(BinaryOp.LeI32, leftExpr, rightExpr);
break;
}
case TypeKind.ISIZE: {
expr = module.createBinary(
this.options.isWasm64
? BinaryOp.LeI64
: BinaryOp.LeI32,
leftExpr,
rightExpr
);
break;
}
case TypeKind.I64: {
expr = module.createBinary(BinaryOp.LeI64, leftExpr, rightExpr);
break;
}
case TypeKind.U8:
case TypeKind.U16:
case TypeKind.U32:
case TypeKind.BOOL: {
expr = module.createBinary(BinaryOp.LeU32, leftExpr, rightExpr);
break;
}
case TypeKind.USIZE: { // TODO: check operator overload
expr = module.createBinary(
this.options.isWasm64
? BinaryOp.LeU64
: BinaryOp.LeU32,
leftExpr,
rightExpr
);
break;
}
case TypeKind.U64: {
expr = module.createBinary(BinaryOp.LeU64, leftExpr, rightExpr);
break;
}
case TypeKind.F32: {
expr = module.createBinary(BinaryOp.LeF32, leftExpr, rightExpr);
break;
}
case TypeKind.F64: {
expr = module.createBinary(BinaryOp.LeF64, leftExpr, rightExpr);
break;
}
default: {
assert(false);
this.error(
DiagnosticCode.Operation_not_supported,
expression.range
);
expr = module.createUnreachable();
break;
}
}
this.currentType = Type.bool;
break;
}
case Token.GREATERTHAN_EQUALS: {
leftExpr = this.compileExpressionRetainType(left, contextualType);
leftType = this.currentType;
rightExpr = this.compileExpressionRetainType(right, leftType);
rightType = this.currentType;
if (commonType = Type.commonCompatible(leftType, rightType, true)) {
leftExpr = this.convertExpression(leftExpr, leftType, commonType, ConversionKind.IMPLICIT, left);
rightExpr = this.convertExpression(rightExpr, rightType, commonType, ConversionKind.IMPLICIT, right);
} else {
this.error(
DiagnosticCode.Operator_0_cannot_be_applied_to_types_1_and_2,
expression.range, ">=", leftType.toString(), rightType.toString()
);
this.currentType = contextualType;
return module.createUnreachable();
}
switch (commonType.kind) {
case TypeKind.I8:
case TypeKind.I16:
case TypeKind.I32: {
expr = module.createBinary(BinaryOp.GeI32, leftExpr, rightExpr);
break;
}
case TypeKind.ISIZE: {
expr = module.createBinary(
this.options.isWasm64
? BinaryOp.GeI64
: BinaryOp.GeI32,
leftExpr,
rightExpr
);
break;
}
case TypeKind.I64: {
expr = module.createBinary(BinaryOp.GeI64, leftExpr, rightExpr);
break;
}
case TypeKind.U8:
case TypeKind.U16:
case TypeKind.U32:
case TypeKind.BOOL: {
expr = module.createBinary(BinaryOp.GeU32, leftExpr, rightExpr);
break;
}
case TypeKind.USIZE: { // TODO: check operator overload
expr = module.createBinary(
this.options.isWasm64
? BinaryOp.GeU64
: BinaryOp.GeU32,
leftExpr,
rightExpr
);
break;
}
case TypeKind.U64: {
expr = module.createBinary(BinaryOp.GeU64, leftExpr, rightExpr);
break;
}
case TypeKind.F32: {
expr = module.createBinary(BinaryOp.GeF32, leftExpr, rightExpr);
break;
}
case TypeKind.F64: {
expr = module.createBinary(BinaryOp.GeF64, leftExpr, rightExpr);
break;
}
default: {
assert(false);
this.error(
DiagnosticCode.Operation_not_supported,
expression.range
);
expr = module.createUnreachable();
break;
}
}
this.currentType = Type.bool;
break;
}
case Token.EQUALS_EQUALS_EQUALS:
// TODO?
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;
rightExpr = this.compileExpressionRetainType(right, leftType);
rightType = this.currentType;
if (commonType = Type.commonCompatible(leftType, rightType, false)) {
leftExpr = this.convertExpression(leftExpr, leftType, commonType, ConversionKind.IMPLICIT, left);
rightExpr = this.convertExpression(rightExpr, rightType, commonType, ConversionKind.IMPLICIT, right);
} else {
this.error(
DiagnosticCode.Operator_0_cannot_be_applied_to_types_1_and_2,
expression.range, operatorTokenToString(expression.operator), leftType.toString(), rightType.toString()
);
this.currentType = contextualType;
return module.createUnreachable();
}
switch (commonType.kind) {
case TypeKind.I8:
case TypeKind.I16:
case TypeKind.I32:
case TypeKind.U8:
case TypeKind.U16:
case TypeKind.U32:
case TypeKind.BOOL: {
expr = module.createBinary(BinaryOp.EqI32, leftExpr, rightExpr);
break;
}
case TypeKind.USIZE: // TODO: check operator overload
case TypeKind.ISIZE: {
expr = module.createBinary(
this.options.isWasm64
? BinaryOp.EqI64
: BinaryOp.EqI32,
leftExpr,
rightExpr
);
break;
}
case TypeKind.I64:
case TypeKind.U64: {
expr = module.createBinary(BinaryOp.EqI64, leftExpr, rightExpr);
break;
}
case TypeKind.F32: {
expr = module.createBinary(BinaryOp.EqF32, leftExpr, rightExpr);
break;
}
case TypeKind.F64: {
expr = module.createBinary(BinaryOp.EqF64, leftExpr, rightExpr);
break;
}
default: {
assert(false);
this.error(
DiagnosticCode.Operation_not_supported,
expression.range
);
expr = module.createUnreachable();
break;
}
}
this.currentType = Type.bool;
break;
}
case Token.EXCLAMATION_EQUALS_EQUALS:
// TODO?
case Token.EXCLAMATION_EQUALS: {
leftExpr = this.compileExpressionRetainType(left, contextualType);
leftType = this.currentType;
rightExpr = this.compileExpressionRetainType(right, leftType);
rightType = this.currentType;
if (commonType = Type.commonCompatible(leftType, rightType, false)) {
leftExpr = this.convertExpression(leftExpr, leftType, commonType, ConversionKind.IMPLICIT, left);
rightExpr = this.convertExpression(rightExpr, rightType, commonType, ConversionKind.IMPLICIT, right);
} else {
this.error(
DiagnosticCode.Operator_0_cannot_be_applied_to_types_1_and_2,
expression.range, operatorTokenToString(expression.operator), leftType.toString(), rightType.toString()
);
this.currentType = contextualType;
return module.createUnreachable();
}
switch (commonType.kind) {
case TypeKind.I8:
case TypeKind.I16:
case TypeKind.I32:
case TypeKind.U8:
case TypeKind.U16:
case TypeKind.U32:
case TypeKind.BOOL: {
expr = module.createBinary(BinaryOp.NeI32, leftExpr, rightExpr);
break;
}
case TypeKind.USIZE: // TODO: check operator overload
case TypeKind.ISIZE: {
expr = module.createBinary(
this.options.isWasm64
? BinaryOp.NeI64
: BinaryOp.NeI32,
leftExpr,
rightExpr
);
break;
}
case TypeKind.I64:
case TypeKind.U64: {
expr = module.createBinary(BinaryOp.NeI64, leftExpr, rightExpr);
break;
}
case TypeKind.F32: {
expr = module.createBinary(BinaryOp.NeF32, leftExpr, rightExpr);
break;
}
case TypeKind.F64: {
expr = module.createBinary(BinaryOp.NeF64, leftExpr, rightExpr);
break;
}
default: {
assert(false);
this.error(
DiagnosticCode.Operation_not_supported,
expression.range
);
expr = module.createUnreachable();
}
}
this.currentType = Type.bool;
break;
}
case Token.EQUALS: {
return this.compileAssignment(left, right, contextualType);
}
case Token.PLUS_EQUALS: compound = true;
case Token.PLUS: {
leftExpr = this.compileExpressionRetainType(
left,
contextualType,
false // retains low bits of small integers
);
if (compound) {
rightExpr = this.compileExpression(
right,
this.currentType,
ConversionKind.IMPLICIT,
false // ^
);
} else {
leftType = this.currentType;
rightExpr = this.compileExpressionRetainType(
right,
leftType,
false // ^
);
rightType = this.currentType;
if (commonType = Type.commonCompatible(leftType, rightType, false)) {
leftExpr = this.convertExpression(leftExpr, leftType, commonType, ConversionKind.IMPLICIT, left);
rightExpr = this.convertExpression(rightExpr, rightType, commonType, ConversionKind.IMPLICIT, right);
} else {
this.error(
DiagnosticCode.Operator_0_cannot_be_applied_to_types_1_and_2,
expression.range, "+", leftType.toString(), rightType.toString()
);
this.currentType = contextualType;
return module.createUnreachable();
}
}
switch (this.currentType.kind) {
case TypeKind.I8:
case TypeKind.I16:
case TypeKind.U8:
case TypeKind.U16:
case TypeKind.BOOL: possiblyOverflows = true;
case TypeKind.I32:
case TypeKind.U32: {
expr = module.createBinary(BinaryOp.AddI32, leftExpr, rightExpr);
break;
}
case TypeKind.USIZE: // TODO: check operator overload
case TypeKind.ISIZE: {
expr = module.createBinary(
this.options.isWasm64
? BinaryOp.AddI64
: BinaryOp.AddI32,
leftExpr,
rightExpr
);
break;
}
case TypeKind.I64:
case TypeKind.U64: {
expr = module.createBinary(BinaryOp.AddI64, leftExpr, rightExpr);
break;
}
case TypeKind.F32: {
expr = module.createBinary(BinaryOp.AddF32, leftExpr, rightExpr);
break;
}
case TypeKind.F64: {
expr = module.createBinary(BinaryOp.AddF64, leftExpr, rightExpr);
break;
}
default: {
assert(false);
this.error(
DiagnosticCode.Operation_not_supported,
expression.range
);
expr = module.createUnreachable();
break;
}
}
break;
}
case Token.MINUS_EQUALS: compound = true;
case Token.MINUS: {
leftExpr = this.compileExpressionRetainType(
left,
contextualType,
false // retains low bits of small integers
);
if (compound) {
rightExpr = this.compileExpression(
right,
this.currentType,
ConversionKind.IMPLICIT,
false // ^
);
} else {
leftType = this.currentType;
rightExpr = this.compileExpressionRetainType(
right,
leftType,
false // ^
);
rightType = this.currentType;
if (commonType = Type.commonCompatible(leftType, rightType, false)) {
leftExpr = this.convertExpression(leftExpr, leftType, commonType, ConversionKind.IMPLICIT, left);
rightExpr = this.convertExpression(rightExpr, rightType, commonType, ConversionKind.IMPLICIT, right);
} else {
this.error(
DiagnosticCode.Operator_0_cannot_be_applied_to_types_1_and_2,
expression.range, "-", leftType.toString(), rightType.toString()
);
this.currentType = contextualType;
return module.createUnreachable();
}
}
switch (this.currentType.kind) {
case TypeKind.I8:
case TypeKind.I16:
case TypeKind.U8:
case TypeKind.U16:
case TypeKind.BOOL: possiblyOverflows = true;
case TypeKind.I32:
case TypeKind.U32: {
expr = module.createBinary(BinaryOp.SubI32, leftExpr, rightExpr);
break;
}
case TypeKind.USIZE: // TODO: check operator overload
case TypeKind.ISIZE: {
expr = module.createBinary(
this.options.isWasm64
? BinaryOp.SubI64
: BinaryOp.SubI32,
leftExpr,
rightExpr
);
break;
}
case TypeKind.I64:
case TypeKind.U64: {
expr = module.createBinary(BinaryOp.SubI64, leftExpr, rightExpr);
break;
}
case TypeKind.F32: {
expr = module.createBinary(BinaryOp.SubF32, leftExpr, rightExpr);
break;
}
case TypeKind.F64: {
expr = module.createBinary(BinaryOp.SubF64, leftExpr, rightExpr);
break;
}
default: {
assert(false);
this.error(
DiagnosticCode.Operation_not_supported,
expression.range
);
expr = module.createUnreachable();
break;
}
}
break;
}
case Token.ASTERISK_EQUALS: compound = true;
case Token.ASTERISK: {
leftExpr = this.compileExpressionRetainType(
left,
contextualType,
false // retains low bits of small integers
);
if (compound) {
rightExpr = this.compileExpression(
right,
this.currentType,
ConversionKind.IMPLICIT,
false // ^
);
} else {
leftType = this.currentType;
rightExpr = this.compileExpressionRetainType(
right,
leftType,
false // ^
);
rightType = this.currentType;
if (commonType = Type.commonCompatible(leftType, rightType, false)) {
leftExpr = this.convertExpression(leftExpr, leftType, commonType, ConversionKind.IMPLICIT, left);
rightExpr = this.convertExpression(rightExpr, rightType, commonType, ConversionKind.IMPLICIT, right);
} else {
this.error(
DiagnosticCode.Operator_0_cannot_be_applied_to_types_1_and_2,
expression.range, "*", leftType.toString(), rightType.toString()
);
this.currentType = contextualType;
return module.createUnreachable();
}
}
switch (this.currentType.kind) {
case TypeKind.I8:
case TypeKind.I16:
case TypeKind.U8:
case TypeKind.U16:
case TypeKind.BOOL: possiblyOverflows = true;
case TypeKind.I32:
case TypeKind.U32: {
expr = module.createBinary(BinaryOp.MulI32, leftExpr, rightExpr);
break;
}
case TypeKind.USIZE: // TODO: check operator overload
case TypeKind.ISIZE: {
expr = module.createBinary(
this.options.isWasm64
? BinaryOp.MulI64
: BinaryOp.MulI32,
leftExpr,
rightExpr
);
break;
}
case TypeKind.I64:
case TypeKind.U64: {
expr = module.createBinary(BinaryOp.MulI64, leftExpr, rightExpr);
break;
}
case TypeKind.F32: {
expr = module.createBinary(BinaryOp.MulF32, leftExpr, rightExpr);
break;
}
case TypeKind.F64: {
expr = module.createBinary(BinaryOp.MulF64, leftExpr, rightExpr);
break;
}
default: {
assert(false);
this.error(
DiagnosticCode.Operation_not_supported,
expression.range
);
expr = module.createUnreachable();
break;
}
}
break;
}
case Token.SLASH_EQUALS: compound = true;
case Token.SLASH: {
leftExpr = this.compileExpressionRetainType(
left,
contextualType,
true // TODO: when can division remain unwrapped? does it overflow?
);
if (compound) {
rightExpr = this.compileExpression(
right,
this.currentType,
ConversionKind.IMPLICIT,
false // ^
);
} else {
leftType = this.currentType;
rightExpr = this.compileExpressionRetainType(
right,
leftType,
false // ^
);
rightType = this.currentType;
if (commonType = Type.commonCompatible(leftType, rightType, false)) {
leftExpr = this.convertExpression(leftExpr, leftType, commonType, ConversionKind.IMPLICIT, left);
rightExpr = this.convertExpression(rightExpr, rightType, commonType, ConversionKind.IMPLICIT, right);
} else {
this.error(
DiagnosticCode.Operator_0_cannot_be_applied_to_types_1_and_2,
expression.range, "/", leftType.toString(), rightType.toString()
);
this.currentType = contextualType;
return module.createUnreachable();
}
}
switch (this.currentType.kind) {
case TypeKind.I8:
case TypeKind.I16: possiblyOverflows = true;
case TypeKind.I32: {
expr = module.createBinary(BinaryOp.DivI32, leftExpr, rightExpr);
break;
}
case TypeKind.ISIZE: {
expr = module.createBinary(
this.options.isWasm64
? BinaryOp.DivI64
: BinaryOp.DivI32,
leftExpr,
rightExpr
);
break;
}
case TypeKind.I64: {
expr = module.createBinary(BinaryOp.DivI64, leftExpr, rightExpr);
break;
}
case TypeKind.U8:
case TypeKind.U16:
case TypeKind.BOOL: possiblyOverflows = true;
case TypeKind.U32: {
expr = module.createBinary(BinaryOp.DivU32, leftExpr, rightExpr);
break;
}
case TypeKind.USIZE: { // TODO: check operator overload
expr = module.createBinary(
this.options.isWasm64
? BinaryOp.DivU64
: BinaryOp.DivU32,
leftExpr,
rightExpr
);
break;
}
case TypeKind.U64: {
expr = module.createBinary(BinaryOp.DivU64, leftExpr, rightExpr);
break;
}
case TypeKind.F32: {
expr = module.createBinary(BinaryOp.DivF32, leftExpr, rightExpr);
break;
}
case TypeKind.F64: {
expr = module.createBinary(BinaryOp.DivF64, leftExpr, rightExpr);
break;
}
default: {
assert(false);
this.error(
DiagnosticCode.Operation_not_supported,
expression.range
);
expr = module.createUnreachable();
break;
}
}
break;
}
case Token.PERCENT_EQUALS: compound = true;
case Token.PERCENT: {
leftExpr = this.compileExpressionRetainType(
left,
contextualType,
true // TODO: when can remainder remain unwrapped? does it overflow?
);
if (compound) {
rightExpr = this.compileExpression(
right,
this.currentType,
ConversionKind.IMPLICIT,
false // ^
);
} else {
leftType = this.currentType;
rightExpr = this.compileExpressionRetainType(
right,
leftType,
false // ^
);
rightType = this.currentType;
if (commonType = Type.commonCompatible(leftType, rightType, false)) {
leftExpr = this.convertExpression(leftExpr, leftType, commonType, ConversionKind.IMPLICIT, left);
rightExpr = this.convertExpression(rightExpr, rightType, commonType, ConversionKind.IMPLICIT, right);
} else {
this.error(
DiagnosticCode.Operator_0_cannot_be_applied_to_types_1_and_2,
expression.range, "%", leftType.toString(), rightType.toString()
);
this.currentType = contextualType;
return module.createUnreachable();
}
}
switch (this.currentType.kind) {
case TypeKind.I8:
case TypeKind.I16:
case TypeKind.I32: {
expr = module.createBinary(BinaryOp.RemI32, leftExpr, rightExpr);
break;
}
case TypeKind.ISIZE: {
expr = module.createBinary(
this.options.isWasm64
? BinaryOp.RemI64
: BinaryOp.RemI32,
leftExpr,
rightExpr
);
break;
}
case TypeKind.I64: {
expr = module.createBinary(BinaryOp.RemI64, leftExpr, rightExpr);
break;
}
case TypeKind.U8:
case TypeKind.U16:
case TypeKind.U32:
case TypeKind.BOOL: {
expr = module.createBinary(BinaryOp.RemU32, leftExpr, rightExpr);
break;
}
case TypeKind.USIZE: { // TODO: check operator overload
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:
case TypeKind.F64: {
// TODO: internal fmod, possibly simply imported from JS
this.error(
DiagnosticCode.Operation_not_supported,
expression.range
);
expr = module.createUnreachable();
break;
}
default: {
assert(false);
this.error(
DiagnosticCode.Operation_not_supported,
expression.range
);
expr = module.createUnreachable();
break;
}
}
break;
}
case Token.LESSTHAN_LESSTHAN_EQUALS: compound = true;
case Token.LESSTHAN_LESSTHAN: {
leftExpr = this.compileExpressionRetainType(
left,
contextualType,
false // retains low bits of small integers
);
rightExpr = this.compileExpression(
right,
this.currentType,
ConversionKind.IMPLICIT,
false // ^
);
switch (this.currentType.kind) {
case TypeKind.I8:
case TypeKind.I16:
case TypeKind.U8:
case TypeKind.U16:
case TypeKind.BOOL: possiblyOverflows = true;
default: {
expr = module.createBinary(BinaryOp.ShlI32, leftExpr, rightExpr);
break;
}
case TypeKind.I64:
case TypeKind.U64: {
expr = module.createBinary(BinaryOp.ShlI64, leftExpr, rightExpr);
break;
}
case TypeKind.USIZE: // TODO: check operator overload
case TypeKind.ISIZE: {
expr = module.createBinary(
this.options.isWasm64
? BinaryOp.ShlI64
: BinaryOp.ShlI32,
leftExpr,
rightExpr
);
break;
}
case TypeKind.F32:
case TypeKind.F64: {
this.error(
DiagnosticCode.The_0_operator_cannot_be_applied_to_type_1,
expression.range, operatorTokenToString(expression.operator), this.currentType.toString()
);
return module.createUnreachable();
}
case TypeKind.VOID: {
assert(false);
this.error(
DiagnosticCode.Operation_not_supported,
expression.range
);
expr = module.createUnreachable();
break;
}
}
break;
}
case Token.GREATERTHAN_GREATERTHAN_EQUALS: compound = true;
case Token.GREATERTHAN_GREATERTHAN: {
leftExpr = this.compileExpressionRetainType(
left,
contextualType,
true // must wrap small integers
);
rightExpr = this.compileExpression(
right,
this.currentType,
ConversionKind.IMPLICIT,
true // ^
);
switch (this.currentType.kind) {
default: {
// assumes signed shr on signed small integers does not overflow
expr = module.createBinary(BinaryOp.ShrI32, leftExpr, rightExpr);
break;
}
case TypeKind.I64: {
expr = module.createBinary(BinaryOp.ShrI64, leftExpr, rightExpr);
break;
}
case TypeKind.ISIZE: {
expr = module.createBinary(
this.options.isWasm64
? BinaryOp.ShrI64
: BinaryOp.ShrI32,
leftExpr,
rightExpr
);
break;
}
case TypeKind.U8:
case TypeKind.U16:
case TypeKind.BOOL: // assumes unsigned shr on unsigned small integers does not overflow
case TypeKind.U32: {
expr = module.createBinary(BinaryOp.ShrU32, leftExpr, rightExpr);
break;
}
case TypeKind.U64: {
expr = module.createBinary(BinaryOp.ShrU64, leftExpr, rightExpr);
break;
}
case TypeKind.USIZE: { // TODO: check operator overload
expr = module.createBinary(
this.options.isWasm64
? BinaryOp.ShrU64
: BinaryOp.ShrU32,
leftExpr,
rightExpr
);
break;
}
case TypeKind.F32:
case TypeKind.F64: {
this.error(
DiagnosticCode.The_0_operator_cannot_be_applied_to_type_1,
expression.range, operatorTokenToString(expression.operator), this.currentType.toString()
);
return module.createUnreachable();
}
case TypeKind.VOID: {
assert(false);
this.error(
DiagnosticCode.Operation_not_supported,
expression.range
);
expr = module.createUnreachable();
break;
}
}
break;
}
case Token.GREATERTHAN_GREATERTHAN_GREATERTHAN_EQUALS: compound = true;
case Token.GREATERTHAN_GREATERTHAN_GREATERTHAN: {
leftExpr = this.compileExpressionRetainType(
left,
contextualType,
true // modifies low bits of small integers if unsigned
);
rightExpr = this.compileExpression(
right,
this.currentType,
ConversionKind.IMPLICIT,
true // ^
);
switch (this.currentType.kind) {
case TypeKind.I8:
case TypeKind.I16: possiblyOverflows = true;
default: {
// assumes that unsigned shr on unsigned small integers does not overflow
expr = module.createBinary(BinaryOp.ShrU32, leftExpr, rightExpr);
break;
}
case TypeKind.I64:
case TypeKind.U64: {
expr = module.createBinary(BinaryOp.ShrU64, leftExpr, rightExpr);
break;
}
case TypeKind.USIZE: // TODO: check operator overload
case TypeKind.ISIZE: {
expr = module.createBinary(
this.options.isWasm64
? BinaryOp.ShrU64
: BinaryOp.ShrU32,
leftExpr,
rightExpr
);
break;
}
case TypeKind.VOID: {
assert(false);
this.error(
DiagnosticCode.Operation_not_supported,
expression.range
);
expr = module.createUnreachable();
break;
}
}
break;
}
case Token.AMPERSAND_EQUALS: compound = true;
case Token.AMPERSAND: {
leftExpr = this.compileExpressionRetainType(
left,
contextualType,
false // retains low bits of small integers
);
if (compound) {
rightExpr = this.compileExpression(
right,
this.currentType,
ConversionKind.IMPLICIT,
false // ^
);
} else {
leftType = this.currentType;
rightExpr = this.compileExpressionRetainType(
right,
leftType,
false // ^
);
rightType = this.currentType;
if (commonType = Type.commonCompatible(leftType, rightType, false)) {
leftExpr = this.convertExpression(leftExpr, leftType, commonType, ConversionKind.IMPLICIT, left);
rightExpr = this.convertExpression(rightExpr, rightType, commonType, ConversionKind.IMPLICIT, right);
} else {
this.error(
DiagnosticCode.Operator_0_cannot_be_applied_to_types_1_and_2,
expression.range, "&", leftType.toString(), rightType.toString()
);
this.currentType = contextualType;
return module.createUnreachable();
}
}
switch (this.currentType.kind) {
case TypeKind.I8:
case TypeKind.I16:
case TypeKind.U8:
case TypeKind.U16:
case TypeKind.BOOL: possiblyOverflows = true; // if left or right already did
default: {
expr = module.createBinary(BinaryOp.AndI32, leftExpr, rightExpr);
break;
}
case TypeKind.I64:
case TypeKind.U64: {
expr = module.createBinary(BinaryOp.AndI64, leftExpr, rightExpr);
break;
}
case TypeKind.USIZE: // TODO: check operator overload
case TypeKind.ISIZE: {
expr = module.createBinary(
this.options.isWasm64
? BinaryOp.AndI64
: BinaryOp.AndI32,
leftExpr,
rightExpr
);
break;
}
case TypeKind.VOID: {
assert(false);
this.error(
DiagnosticCode.Operation_not_supported,
expression.range
);
expr = module.createUnreachable();
break;
}
}
break;
}
case Token.BAR_EQUALS: compound = true;
case Token.BAR: {
leftExpr = this.compileExpressionRetainType(
left,
contextualType,
false // retains low bits of small integers
);
if (compound) {
rightExpr = this.compileExpression(
right,
this.currentType,
ConversionKind.IMPLICIT,
false // ^
);
} else {
leftType = this.currentType;
rightExpr = this.compileExpressionRetainType(
right,
leftType,
false // ^
);
rightType = this.currentType;
if (commonType = Type.commonCompatible(leftType, rightType, false)) {
leftExpr = this.convertExpression(leftExpr, leftType, commonType, ConversionKind.IMPLICIT, left);
rightExpr = this.convertExpression(rightExpr, rightType, commonType, ConversionKind.IMPLICIT, right);
} else {
this.error(
DiagnosticCode.Operator_0_cannot_be_applied_to_types_1_and_2,
expression.range, "|", leftType.toString(), rightType.toString()
);
this.currentType = contextualType;
return module.createUnreachable();
}
}
switch (this.currentType.kind) {
case TypeKind.I8:
case TypeKind.I16:
case TypeKind.U8:
case TypeKind.U16:
case TypeKind.BOOL: possiblyOverflows = true; // if left or right already did
default: {
expr = module.createBinary(BinaryOp.OrI32, leftExpr, rightExpr);
break;
}
case TypeKind.I64:
case TypeKind.U64: {
expr = module.createBinary(BinaryOp.OrI64, leftExpr, rightExpr);
break;
}
case TypeKind.USIZE: // TODO: check operator overload
case TypeKind.ISIZE: {
expr = module.createBinary(
this.options.isWasm64
? BinaryOp.OrI64
: BinaryOp.OrI32,
leftExpr,
rightExpr
);
break;
}
case TypeKind.VOID: {
assert(false);
this.error(
DiagnosticCode.Operation_not_supported,
expression.range
);
expr = module.createUnreachable();
break;
}
}
break;
}
case Token.CARET_EQUALS: compound = true;
case Token.CARET: {
leftExpr = this.compileExpressionRetainType(
left,
contextualType,
false // retains low bits of small integers
);
if (compound) {
rightExpr = this.compileExpression(
right,
this.currentType,
ConversionKind.IMPLICIT,
false // ^
);
} else {
leftType = this.currentType;
rightExpr = this.compileExpressionRetainType(
right,
leftType,
false // ^
);
rightType = this.currentType;
if (commonType = Type.commonCompatible(leftType, rightType, false)) {
leftExpr = this.convertExpression(leftExpr, leftType, commonType, ConversionKind.IMPLICIT, left);
rightExpr = this.convertExpression(rightExpr, rightType, commonType, ConversionKind.IMPLICIT, right);
} else {
this.error(
DiagnosticCode.Operator_0_cannot_be_applied_to_types_1_and_2,
expression.range, "^", leftType.toString(), rightType.toString()
);
this.currentType = contextualType;
return module.createUnreachable();
}
}
switch (this.currentType.kind) {
case TypeKind.I8:
case TypeKind.I16:
case TypeKind.U8:
case TypeKind.U16:
case TypeKind.BOOL: possiblyOverflows = true; // if left or right already did
default: {
expr = module.createBinary(BinaryOp.XorI32, leftExpr, rightExpr);
break;
}
case TypeKind.I64:
case TypeKind.U64: {
expr = module.createBinary(BinaryOp.XorI64, leftExpr, rightExpr);
break;
}
case TypeKind.USIZE: // TODO: check operator overload
case TypeKind.ISIZE: {
expr = module.createBinary(
this.options.isWasm64
? BinaryOp.XorI64
: BinaryOp.XorI32,
leftExpr,
rightExpr
);
break;
}
case TypeKind.VOID: {
assert(false);
this.error(
DiagnosticCode.Operation_not_supported,
expression.range
);
expr = module.createUnreachable();
break;
}
}
break;
}
// logical (no overloading)
case Token.AMPERSAND_AMPERSAND: { // left && right
leftExpr = this.compileExpressionRetainType(
left,
contextualType
);
rightExpr = this.compileExpression(
right,
this.currentType,
ConversionKind.IMPLICIT,
false
);
// clone left if free of side effects
expr = module.cloneExpression(leftExpr, true, 0);
// if not possible, tee left to a temp. local
if (!expr) {
tempLocal = this.currentFunction.getAndFreeTempLocal(this.currentType);
leftExpr = module.createTeeLocal(tempLocal.index, leftExpr);
}
possiblyOverflows = this.currentType.is(TypeFlags.SMALL | TypeFlags.INTEGER);
condExpr = makeIsTrueish(leftExpr, this.currentType, module);
// simplify when cloning left without side effects was successful
if (expr) {
expr = module.createIf(
condExpr, // left
rightExpr, // ? right
expr // : cloned left
);
}
// otherwise make use of the temp. local
else {
expr = module.createIf(
condExpr,
rightExpr,
module.createGetLocal(
assert(tempLocal).index, // to be sure
this.currentType.toNativeType()
)
);
}
break;
}
case Token.BAR_BAR: { // left || right
leftExpr = this.compileExpressionRetainType(
left,
contextualType
);
rightExpr = this.compileExpression(
right,
this.currentType,
ConversionKind.IMPLICIT,
false
);
// clone left if free of side effects
expr = this.module.cloneExpression(leftExpr, true, 0);
// if not possible, tee left to a temp. local
if (!expr) {
tempLocal = this.currentFunction.getAndFreeTempLocal(this.currentType);
leftExpr = module.createTeeLocal(tempLocal.index, leftExpr);
}
possiblyOverflows = this.currentType.is(TypeFlags.SMALL | TypeFlags.INTEGER); // if right did
condExpr = makeIsTrueish(leftExpr, this.currentType, module);
// simplify when cloning left without side effects was successful
if (expr) {
expr = this.module.createIf(
condExpr, // left
expr, // ? cloned left
rightExpr // : right
);
}
// otherwise make use of the temp. local
else {
expr = module.createIf(
condExpr,
module.createGetLocal(
assert(tempLocal).index, // to be sure
this.currentType.toNativeType()
),
rightExpr
);
}
break;
}
default: {
assert(false);
this.error(
DiagnosticCode.Operation_not_supported,
expression.range
);
expr = this.module.createUnreachable();
break;
}
}
if (possiblyOverflows && wrapSmallIntegers) {
assert(this.currentType.is(TypeFlags.SMALL | TypeFlags.INTEGER)); // must be a small int
expr = makeSmallIntegerWrap(expr, this.currentType, module);
}
return compound
? this.compileAssignmentWithValue(left, expr, contextualType != Type.void)
: expr;
}
compileAssignment(expression: Expression, valueExpression: Expression, contextualType: Type): ExpressionRef {
var currentFunction = this.currentFunction;
var resolved = this.program.resolveExpression(expression, currentFunction); // reports
if (!resolved) return this.module.createUnreachable();
// to compile just the value, we need to know the target's type
var element = resolved.element;
var elementType: Type;
switch (element.kind) {
case ElementKind.GLOBAL: {
if (!this.compileGlobal(<Global>element)) { // reports; not yet compiled if a static field compiled as a global
return this.module.createUnreachable();
}
assert((<Global>element).type != Type.void, "concrete type expected");
// fall-through
}
case ElementKind.LOCAL:
case ElementKind.FIELD: {
elementType = (<VariableLikeElement>element).type;
break;
}
case ElementKind.PROPERTY: {
let prototype = (<Property>element).setterPrototype;
if (prototype) {
let instance = prototype.resolve(); // reports
if (!instance) return this.module.createUnreachable();
assert(instance.signature.parameterTypes.length == 1);
elementType = instance.signature.parameterTypes[0];
break;
}
this.error(
DiagnosticCode.Cannot_assign_to_0_because_it_is_a_constant_or_a_read_only_property,
expression.range, (<Property>element).internalName
);
return this.module.createUnreachable();
}
case ElementKind.FUNCTION_PROTOTYPE: {
if (expression.kind == NodeKind.ELEMENTACCESS) { // @operator("[]")
if (resolved.target && resolved.target.kind == ElementKind.CLASS) {
if (element.simpleName == (<Class>resolved.target).prototype.fnIndexedGet) {
let resolvedIndexedSet = (<FunctionPrototype>element).resolve(null); // reports
if (resolvedIndexedSet) {
elementType = resolvedIndexedSet.signature.returnType;
break;
}
} else {
this.error(
DiagnosticCode.Index_signature_is_missing_in_type_0,
expression.range, (<Class>resolved.target).toString()
);
return this.module.createUnreachable();
}
}
}
// fall-through
}
default: {
this.error(
DiagnosticCode.Operation_not_supported,
expression.range
);
return this.module.createUnreachable();
}
}
// compile the value and do the assignment
var valueExpr = this.compileExpression(valueExpression, elementType);
return this.compileAssignmentWithValue(
expression,
valueExpr,
contextualType != Type.void
);
}
compileAssignmentWithValue(
expression: Expression,
valueWithCorrectType: ExpressionRef,
tee: bool = false
): ExpressionRef {
var module = this.module;
var resolved = this.program.resolveExpression(expression, this.currentFunction); // reports
if (!resolved) return module.createUnreachable();
var element = resolved.element;
switch (element.kind) {
case ElementKind.LOCAL: {
this.currentType = tee ? (<Local>element).type : Type.void;
if ((<Local>element).is(CommonFlags.CONST)) {
this.error(
DiagnosticCode.Cannot_assign_to_0_because_it_is_a_constant_or_a_read_only_property,
expression.range, (<Local>element).internalName
);
return module.createUnreachable();
}
return tee
? module.createTeeLocal((<Local>element).index, valueWithCorrectType)
: module.createSetLocal((<Local>element).index, valueWithCorrectType);
}
case ElementKind.GLOBAL: {
if (!this.compileGlobal(<Global>element)) return module.createUnreachable();
let type = (<Global>element).type;
assert(type != Type.void);
this.currentType = tee ? type : Type.void;
if ((<Local>element).is(CommonFlags.CONST)) {
this.error(
DiagnosticCode.Cannot_assign_to_0_because_it_is_a_constant_or_a_read_only_property,
expression.range,
(<Local>element).internalName
);
return module.createUnreachable();
}
if (tee) {
let nativeType = type.toNativeType();
let internalName = (<Global>element).internalName;
return module.createBlock(null, [ // emulated teeGlobal
module.createSetGlobal(internalName, valueWithCorrectType),
module.createGetGlobal(internalName, nativeType)
], nativeType);
} else {
return module.createSetGlobal((<Global>element).internalName, valueWithCorrectType);
}
}
case ElementKind.FIELD: {
if ((<Field>element).is(CommonFlags.READONLY)) {
this.error(
DiagnosticCode.Cannot_assign_to_0_because_it_is_a_constant_or_a_read_only_property,
expression.range, (<Field>element).internalName
);
return module.createUnreachable();
}
assert(resolved.isInstanceTarget);
let targetExpr = this.compileExpression(
<Expression>resolved.targetExpression,
(<Class>resolved.target).type
);
let type = (<Field>element).type;
this.currentType = tee ? type : Type.void;
let nativeType = type.toNativeType();
if (tee) {
let tempLocal = this.currentFunction.getAndFreeTempLocal(type);
let tempLocalIndex = tempLocal.index;
// TODO: simplify if valueWithCorrectType has no side effects
return module.createBlock(null, [
module.createSetLocal(tempLocalIndex, valueWithCorrectType),
module.createStore(
type.size >> 3,
targetExpr,
module.createGetLocal(tempLocalIndex, nativeType),
nativeType,
(<Field>element).memoryOffset
),
module.createGetLocal(tempLocalIndex, nativeType)
], nativeType);
} else {
return module.createStore(
type.size >> 3,
targetExpr,
valueWithCorrectType,
nativeType,
(<Field>element).memoryOffset
);
}
}
case ElementKind.PROPERTY: {
let setterPrototype = (<Property>element).setterPrototype;
if (setterPrototype) {
let setterInstance = setterPrototype.resolve(); // reports
if (!setterInstance) return module.createUnreachable();
// call just the setter if the return value isn't of interest
if (!tee) {
if (setterInstance.is(CommonFlags.INSTANCE)) {
assert(resolved.isInstanceTarget);
let thisArg = this.compileExpression(
<Expression>resolved.targetExpression,
(<Class>resolved.target).type
);
return this.makeCallDirect(setterInstance, [ thisArg, valueWithCorrectType ]);
} else {
return this.makeCallDirect(setterInstance, [ valueWithCorrectType ]);
}
}
// otherwise call the setter first, then the getter
let getterPrototype = (<Property>element).getterPrototype;
assert(getterPrototype != null); // must have one if there is a setter
let getterInstance = (<FunctionPrototype>getterPrototype).resolve(); // reports
if (!getterInstance) return module.createUnreachable();
let returnType = getterInstance.signature.returnType;
let nativeReturnType = returnType.toNativeType();
if (setterInstance.is(CommonFlags.INSTANCE)) {
assert(resolved.isInstanceTarget);
let thisArg = this.compileExpression(
<Expression>resolved.targetExpression,
(<Class>resolved.target).type
);
let tempLocal = this.currentFunction.getAndFreeTempLocal(returnType);
let tempLocalIndex = tempLocal.index;
return module.createBlock(null, [
this.makeCallDirect(setterInstance, [ // set and remember the target
module.createTeeLocal(tempLocalIndex, thisArg),
valueWithCorrectType
]),
this.makeCallDirect(getterInstance, [ // get from remembered target
module.createGetLocal(tempLocalIndex, nativeReturnType)
])
], nativeReturnType);
} else {
// note that this must be performed here because `resolved` is shared
return module.createBlock(null, [
this.makeCallDirect(setterInstance, [ valueWithCorrectType ]),
this.makeCallDirect(getterInstance)
], nativeReturnType);
}
} else {
this.error(
DiagnosticCode.Cannot_assign_to_0_because_it_is_a_constant_or_a_read_only_property,
expression.range, (<Property>element).internalName
);
}
return module.createUnreachable();
}
case ElementKind.FUNCTION_PROTOTYPE: { // @operator("[]") ?
if (expression.kind == NodeKind.ELEMENTACCESS) {
assert(resolved.isInstanceTarget);
let getterInstance = (<FunctionPrototype>element).resolve(); // reports
if (!getterInstance) return module.createUnreachable();
// obtain @operator("[]=")
let setElementName = (<Class>resolved.target).prototype.fnIndexedSet;
let setElement: Element | null;
if (
setElementName != null &&
(<Class>resolved.target).members &&
(setElement = (<Map<string,Element>>(<Class>resolved.target).members).get(setElementName)) &&
setElement.kind == ElementKind.FUNCTION_PROTOTYPE
) {
let setterInstance = (<FunctionPrototype>setElement).resolve(); // reports
if (!setterInstance) return module.createUnreachable();
let targetType = (<Class>resolved.target).type;
let targetExpr = this.compileExpression(
<Expression>resolved.targetExpression,
targetType
);
let elementExpr = this.compileExpression(
(<ElementAccessExpression>expression).elementExpression,
Type.i32
);
if (tee) {
let tempLocalTarget = this.currentFunction.getTempLocal(targetType);
let tempLocalElement = this.currentFunction.getAndFreeTempLocal(this.currentType);
let returnType = getterInstance.signature.returnType;
this.currentFunction.freeTempLocal(tempLocalTarget);
return module.createBlock(null, [
this.makeCallDirect(setterInstance, [
module.createTeeLocal(tempLocalTarget.index, targetExpr),
module.createTeeLocal(tempLocalElement.index, elementExpr),
valueWithCorrectType
]),
this.makeCallDirect(getterInstance, [
module.createGetLocal(tempLocalTarget.index, tempLocalTarget.type.toNativeType()),
module.createGetLocal(tempLocalElement.index, tempLocalElement.type.toNativeType())
])
], returnType.toNativeType());
} else {
return this.makeCallDirect(setterInstance, [
targetExpr,
elementExpr,
valueWithCorrectType
]);
}
} else {
this.error(
DiagnosticCode.Index_signature_in_type_0_only_permits_reading,
expression.range, (<Class>resolved.target).internalName
);
return module.createUnreachable();
}
}
// fall-through
}
}
this.error(
DiagnosticCode.Operation_not_supported,
expression.range
);
return module.createUnreachable();
}
compileCallExpression(expression: CallExpression, contextualType: Type): ExpressionRef {
var module = this.module;
var currentFunction = this.currentFunction;
var resolved = this.program.resolveExpression(expression.expression, currentFunction); // reports
if (!resolved) return module.createUnreachable();
var element = resolved.element;
var signature: Signature | null;
var indexArg: ExpressionRef;
switch (element.kind) {
// direct call: concrete function
case ElementKind.FUNCTION_PROTOTYPE: {
let prototype = <FunctionPrototype>element;
// builtins are compiled on the fly
if (prototype.is(CommonFlags.BUILTIN)) {
let expr = compileBuiltinCall( // reports
this,
prototype,
prototype.resolveBuiltinTypeArguments(
expression.typeArguments,
currentFunction.contextualTypeArguments
),
expression.arguments,
contextualType,
expression
);
if (!expr) {
this.error(
DiagnosticCode.Operation_not_supported,
expression.range
);
return module.createUnreachable();
}
return expr;
// otherwise compile to a call
} else {
let instance = prototype.resolveUsingTypeArguments( // reports
expression.typeArguments,
currentFunction.contextualTypeArguments,
expression
);
if (!instance) return module.createUnreachable();
let thisArg: ExpressionRef = 0;
if (instance.is(CommonFlags.INSTANCE)) {
assert(resolved.isInstanceTarget);
thisArg = this.compileExpression(
<Expression>resolved.targetExpression,
(<Class>resolved.target).type
);
if (!thisArg) return module.createUnreachable();
} else {
assert(!resolved.isInstanceTarget);
}
return this.compileCallDirect(instance, expression.arguments, expression, thisArg);
}
}
// indirect call: index argument with signature
case ElementKind.LOCAL: {
if (signature = (<Local>element).type.functionType) {
indexArg = module.createGetLocal((<Local>element).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, (<Local>element).type.toString()
);
return module.createUnreachable();
}
}
case ElementKind.GLOBAL: {
if (signature = (<Global>element).type.functionType) {
indexArg = module.createGetGlobal((<Global>element).internalName, (<Global>element).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, (<Global>element).type.toString()
);
return module.createUnreachable();
}
}
case ElementKind.FIELD: {
let type = (<Field>element).type;
if (signature = type.functionType) {
let targetExpr = this.compileExpression(assert(resolved.targetExpression), type);
indexArg = module.createLoad(
4,
false,
targetExpr,
NativeType.I32,
(<Field>element).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, (<Field>element).type.toString()
);
return module.createUnreachable();
}
}
case ElementKind.FUNCTION_TARGET: {
signature = (<FunctionTarget>element).signature;
indexArg = this.compileExpression(expression.expression, (<FunctionTarget>element).type);
break;
}
case ElementKind.PROPERTY: // TODO
// not supported
default: {
this.error(
DiagnosticCode.Operation_not_supported,
expression.range
);
return module.createUnreachable();
}
}
return this.compileCallIndirect(
signature,
indexArg,
expression.arguments,
expression
);
}
/**
* 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
): ExpressionRef {
var numArguments = argumentExpressions.length;
var signature = instance.signature;
if (!this.checkCallSignature( // reports
signature,
numArguments,
thisArg != 0,
reportNode
)) {
return this.module.createUnreachable();
}
var numArgumentsInclThis = thisArg ? numArguments + 1 : numArguments;
var operands = new Array<ExpressionRef>(numArgumentsInclThis);
var index = 0;
if (thisArg) {
operands[0] = thisArg;
index = 1;
}
var parameterTypes = signature.parameterTypes;
for (let i = 0; i < numArguments; ++i, ++index) {
operands[index] = this.compileExpression(
argumentExpressions[i],
parameterTypes[i]
);
}
assert(index == numArgumentsInclThis);
return this.makeCallDirect(instance, operands);
}
/** Gets the trampoline for the specified function. */
ensureTrampoline(original: Function): Function {
var trampoline = original.trampoline;
if (trampoline) return trampoline;
var originalSignature = original.signature;
var originalName = original.internalName;
var originalParameterTypes = originalSignature.parameterTypes;
var originalParameterDeclarations = original.prototype.declaration.signature.parameterTypes;
var commonReturnType = originalSignature.returnType;
var commonThisType = originalSignature.thisType;
var isInstance = original.is(CommonFlags.INSTANCE);
// arguments excl. `this`, operands incl. `this`
var minArguments = originalSignature.requiredParameters;
var minOperands = minArguments;
var maxArguments = originalParameterTypes.length;
var maxOperands = maxArguments;
if (isInstance) {
++minOperands;
++maxOperands;
}
var numOptional = maxOperands - minOperands;
assert(numOptional);
var forwardedOperands = new Array<ExpressionRef>(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);
// append an additional parameter taking the number of optional arguments provided
var trampolineParameterTypes = new Array<Type>(maxArguments + 1);
for (let i = 0; i < maxArguments; ++i) {
trampolineParameterTypes[i] = originalParameterTypes[i];
}
trampolineParameterTypes[maxArguments] = Type.i32;
// create the trampoline element
var trampolineSignature = new Signature(trampolineParameterTypes, commonReturnType, commonThisType);
var trampolineName = originalName + "|trampoline";
trampolineSignature.requiredParameters = maxArguments + 1;
trampoline = new Function(original.prototype, trampolineName, trampolineSignature, original.instanceMethodOf);
trampoline.flags = original.flags;
trampoline.set(CommonFlags.COMPILED);
original.trampoline = trampoline;
// compile initializers of omitted arguments in scope of the trampoline function
// this is necessary because initializers might need additional locals and a proper this context
var previousFunction = this.currentFunction;
this.currentFunction = trampoline;
// create a br_table switching over the number of optional parameters provided
var numNames = numOptional + 1; // incl. 'with0'
var names = new Array<string>(numNames);
for (let i = 0; i < numNames; ++i) {
let label = "N=" + i.toString();
names[i] = label;
}
var body = module.createBlock(names[0], [
module.createBlock("N=invalid", [
module.createSwitch(names, "N=invalid",
module.createGetLocal(maxOperands, NativeType.I32)
)
]),
module.createUnreachable()
]);
for (let i = 0; i < numOptional; ++i, ++operandIndex) {
let type = originalParameterTypes[minArguments + i];
body = module.createBlock(names[i + 1], [
body,
module.createSetLocal(operandIndex,
this.compileExpression(
assert(originalParameterDeclarations[minArguments + i].initializer),
type
)
)
]);
forwardedOperands[operandIndex] = module.createGetLocal(operandIndex, type.toNativeType());
}
this.currentFunction = previousFunction;
assert(operandIndex == maxOperands);
var typeRef = this.ensureFunctionType(trampolineSignature);
var funcRef = module.addFunction(trampolineName, typeRef, typesToNativeTypes(trampoline.additionalLocals),
module.createBlock(null, [
body,
module.createCall(
originalName,
forwardedOperands,
commonReturnType.toNativeType()
)
], commonReturnType.toNativeType())
);
trampoline.finalize(module, funcRef);
return trampoline;
}
/** Creates a direct call to the specified function. */
makeCallDirect(instance: Function, operands: ExpressionRef[] | null = null): ExpressionRef {
var numOperands = operands ? operands.length : 0;
var numArguments = numOperands;
var minArguments = instance.signature.requiredParameters;
var minOperands = minArguments;
var maxArguments = instance.signature.parameterTypes.length;
var maxOperands = maxArguments;
if (instance.is(CommonFlags.INSTANCE)) {
++minOperands;
++maxOperands;
--numArguments;
}
assert(numOperands >= minOperands);
var module = this.module;
if (!this.compileFunction(instance)) return module.createUnreachable();
if (numOperands < maxOperands) {
instance = this.ensureTrampoline(instance);
if (!this.compileFunction(instance)) return module.createUnreachable();
if (!operands) {
operands = new Array(maxOperands + 1);
operands.length = 0;
}
for (let i = numArguments; i < maxArguments; ++i) {
operands.push(instance.signature.parameterTypes[i].toNativeZero(module));
}
operands.push(module.createI32(numOperands - minOperands));
}
var returnType = instance.signature.returnType;
this.currentType = returnType;
if (instance.is(CommonFlags.MODULE_IMPORT)) {
return module.createCallImport(instance.internalName, operands, returnType.toNativeType());
} else {
return module.createCall(instance.internalName, operands, returnType.toNativeType());
}
}
/** Compiles an indirect call using an index argument and a signature. */
compileCallIndirect(
signature: Signature,
indexArg: ExpressionRef,
argumentExpressions: Expression[],
reportNode: Node,
thisArg: ExpressionRef = 0
): ExpressionRef {
var numArguments = argumentExpressions.length;
if (!this.checkCallSignature( // reports
signature,
numArguments,
thisArg != 0,
reportNode
)) {
return this.module.createUnreachable();
}
var numArgumentsInclThis = thisArg ? numArguments + 1 : numArguments;
var operands = new Array<ExpressionRef>(numArgumentsInclThis);
var index = 0;
if (thisArg) {
operands[0] = thisArg;
index = 1;
}
var parameterTypes = signature.parameterTypes;
for (let i = 0; i < numArguments; ++i, ++index) {
operands[index] = this.compileExpression(
argumentExpressions[i],
parameterTypes[i]
);
}
assert(index == numArgumentsInclThis);
return this.makeCallIndirect(signature, indexArg, operands);
}
/** Creates an indirect call to the function at `indexArg` in the function table. */
makeCallIndirect(signature: Signature, indexArg: ExpressionRef, operands: ExpressionRef[]): ExpressionRef {
var returnType = signature.returnType;
this.currentType = returnType;
this.ensureFunctionType(signature);
return this.module.createCallIndirect(indexArg, operands, signature.toSignatureString());
}
compileCommaExpression(expression: CommaExpression, contextualType: Type): ExpressionRef {
var expressions = expression.expressions;
var numExpressions = expressions.length;
var exprs = new Array<ExpressionRef>(numExpressions--);
for (let i = 0; i < numExpressions; ++i) {
exprs[i] = this.compileExpression(expressions[i], Type.void); // drop all
}
exprs[numExpressions] = this.compileExpression(expressions[numExpressions], contextualType); // except last
return this.module.createBlock(null, exprs, this.currentType.toNativeType());
}
compileElementAccessExpression(expression: ElementAccessExpression, contextualType: Type): ExpressionRef {
var resolved = this.program.resolveElementAccess(expression, this.currentFunction); // reports
if (!resolved) return this.module.createUnreachable();
assert( // should be guaranteed by resolveElementAccess
resolved.element.kind == ElementKind.FUNCTION_PROTOTYPE &&
resolved.target &&
resolved.target.kind == ElementKind.CLASS
);
var target = <Class>resolved.target;
var instance = (<FunctionPrototype>resolved.element).resolve( // reports
null,
target.contextualTypeArguments
);
if (!instance) return this.module.createUnreachable();
var thisArg = this.compileExpression(expression.expression, target.type);
return this.compileCallDirect(instance, [
expression.elementExpression
], expression, thisArg);
}
compileFunctionExpression(expression: FunctionExpression, contextualType: Type): ExpressionRef {
var declaration = expression.declaration;
var name = declaration.name;
var simpleName = (name.text.length
? name.text
: "anonymous") + "|" + this.functionTable.length.toString(10);
var currentFunction = this.currentFunction;
var prototype = new FunctionPrototype(
this.program,
simpleName,
currentFunction.internalName + "~" + simpleName,
declaration
);
var instance = this.compileFunctionUsingTypeArguments(
prototype,
[],
currentFunction.contextualTypeArguments,
declaration
);
if (!instance) return this.module.createUnreachable();
this.currentType = Type.u32.asFunction(instance.signature); // TODO: get cached type?
// NOTE that, in order to make this work in every case, the function must be represented by a
// value, so we add it and rely on the optimizer to figure out where it can be called directly.
var index = this.ensureFunctionTableEntry(instance); // reports
return index < 0
? this.module.createUnreachable()
: this.module.createI32(index);
}
/**
* Compiles an identifier in the specified context.
* @param retainConstantType Retains the type of inlined constants if `true`, otherwise
* precomputes them according to context.
*/
compileIdentifierExpression(
expression: IdentifierExpression,
contextualType: Type,
retainConstantType: bool
): ExpressionRef {
var module = this.module;
// check special keywords first
switch (expression.kind) {
case NodeKind.NULL: {
let options = this.options;
if (!contextualType.classType) {
this.currentType = options.usizeType;
}
return options.isWasm64
? module.createI64(0)
: module.createI32(0);
}
case NodeKind.TRUE: {
this.currentType = Type.bool;
return module.createI32(1);
}
case NodeKind.FALSE: {
this.currentType = Type.bool;
return module.createI32(0);
}
case NodeKind.THIS: {
let currentFunction = this.currentFunction;
if (currentFunction.is(CommonFlags.INSTANCE)) {
let thisType = assert(currentFunction.instanceMethodOf).type;
this.currentType = thisType;
return module.createGetLocal(0, thisType.toNativeType());
}
this.error(
DiagnosticCode._this_cannot_be_referenced_in_current_location,
expression.range
);
this.currentType = this.options.usizeType;
return module.createUnreachable();
}
case NodeKind.SUPER: {
let currentFunction = this.currentFunction;
if (currentFunction.is(CommonFlags.INSTANCE)) {
let base = assert(currentFunction.instanceMethodOf).base;
if (base) {
let superType = base.type;
this.currentType = superType;
return module.createGetLocal(0, superType.toNativeType());
}
}
this.error(
DiagnosticCode._super_can_only_be_referenced_in_a_derived_class,
expression.range
);
this.currentType = this.options.usizeType;
return module.createUnreachable();
}
}
// otherwise resolve
var resolved = this.program.resolveIdentifier( // reports
expression,
this.currentFunction,
this.currentEnum
);
if (!resolved) return module.createUnreachable();
var element = resolved.element;
switch (element.kind) {
case ElementKind.LOCAL: {
if ((<Local>element).is(CommonFlags.INLINED)) {
return this.compileInlineConstant(<Local>element, contextualType, retainConstantType);
}
let localType = (<Local>element).type;
let localIndex = (<Local>element).index;
assert(localIndex >= 0);
this.currentType = localType;
return this.module.createGetLocal(localIndex, localType.toNativeType());
}
case ElementKind.GLOBAL: {
if (element.is(CommonFlags.BUILTIN)) {
return compileBuiltinGetConstant(this, <Global>element, expression);
}
if (!this.compileGlobal(<Global>element)) { // reports; not yet compiled if a static field
return this.module.createUnreachable();
}
let globalType = (<Global>element).type;
assert(globalType != Type.void);
if ((<Global>element).is(CommonFlags.INLINED)) {
return this.compileInlineConstant(<Global>element, contextualType, retainConstantType);
}
this.currentType = globalType;
return this.module.createGetGlobal((<Global>element).internalName, globalType.toNativeType());
}
case ElementKind.ENUMVALUE: { // here: if referenced from within the same enum
if (!element.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 ((<EnumValue>element).is(CommonFlags.INLINED)) {
return this.module.createI32((<EnumValue>element).constantValue);
}
return this.module.createGetGlobal((<EnumValue>element).internalName, NativeType.I32);
}
}
this.error(
DiagnosticCode.Operation_not_supported,
expression.range
);
return this.module.createUnreachable();
}
compileLiteralExpression(
expression: LiteralExpression,
contextualType: Type,
implicitNegate: bool = false
): ExpressionRef {
var module = this.module;
switch (expression.literalKind) {
case LiteralKind.ARRAY: {
assert(!implicitNegate);
let classType = contextualType.classType;
if (
classType &&
classType == this.program.elementsLookup.get("Array") &&
classType.typeArguments && classType.typeArguments.length == 1
) {
return this.compileStaticArray(
classType.typeArguments[0],
(<ArrayLiteralExpression>expression).elementExpressions,
expression
);
}
this.error(
DiagnosticCode.Operation_not_supported,
expression.range
);
return module.createUnreachable();
}
case LiteralKind.FLOAT: {
let floatValue = (<FloatLiteralExpression>expression).value;
if (implicitNegate) {
floatValue = -floatValue;
}
if (contextualType == Type.f32) {
return module.createF32(<f32>floatValue);
}
this.currentType = Type.f64;
return module.createF64(floatValue);
}
case LiteralKind.INTEGER: {
let intValue = (<IntegerLiteralExpression>expression).value;
if (implicitNegate) {
intValue = i64_sub(
i64_new(0),
intValue
);
}
switch (contextualType.kind) {
// compile to contextualType if matching
case TypeKind.I8: {
if (i64_is_i8(intValue)) return module.createI32(i64_low(intValue));
break;
}
case TypeKind.I16: {
if (i64_is_i16(intValue)) return module.createI32(i64_low(intValue));
break;
}
case TypeKind.I32: {
if (i64_is_i32(intValue)) return module.createI32(i64_low(intValue));
break;
}
case TypeKind.U8: {
if (i64_is_u8(intValue)) return module.createI32(i64_low(intValue));
break;
}
case TypeKind.U16: {
if (i64_is_u16(intValue)) return module.createI32(i64_low(intValue));
break;
}
case TypeKind.U32: {
if (i64_is_u32(intValue)) return module.createI32(i64_low(intValue));
break;
}
case TypeKind.BOOL: {
if (i64_is_bool(intValue)) return module.createI32(i64_low(intValue));
break;
}
case TypeKind.ISIZE: {
if (!this.options.isWasm64) {
if (i64_is_u32(intValue)) return module.createI32(i64_low(intValue));
break;
}
return module.createI64(i64_low(intValue), i64_high(intValue));
}
case TypeKind.USIZE: {
if (!this.options.isWasm64) {
if (i64_is_u32(intValue)) return module.createI32(i64_low(intValue));
break;
}
return module.createI64(i64_low(intValue), i64_high(intValue));
}
case TypeKind.I64:
case TypeKind.U64: {
return module.createI64(i64_low(intValue), i64_high(intValue));
}
case TypeKind.F32: {
if (i64_is_f32(intValue)) return module.createF32(i64_to_f32(intValue));
break;
}
case TypeKind.F64: {
if (i64_is_f64(intValue)) return module.createF64(i64_to_f64(intValue));
break;
}
case TypeKind.VOID: {
break; // compiles to best fitting type below, being dropped
}
default: {
assert(false);
this.error(
DiagnosticCode.Operation_not_supported,
expression.range
);
return module.createUnreachable();
}
}
// otherwise compile to best fitting native type
if (i64_is_i32(intValue)) {
this.currentType = Type.i32;
return module.createI32(i64_low(intValue));
} else {
this.currentType = Type.i64;
return module.createI64(i64_low(intValue), i64_high(intValue));
}
}
case LiteralKind.STRING: {
assert(!implicitNegate);
return this.compileStaticString((<StringLiteralExpression>expression).value);
}
// case LiteralKind.OBJECT:
// case LiteralKind.REGEXP:
}
this.error(
DiagnosticCode.Operation_not_supported,
expression.range
);
this.currentType = contextualType;
return module.createUnreachable();
}
compileStaticString(stringValue: string): ExpressionRef {
var module = this.module;
var options = this.options;
var stringSegments = this.stringSegments;
var stringSegment: MemorySegment | null = stringSegments.get(stringValue);
if (!stringSegment) {
let stringLength = stringValue.length;
let stringBuffer = new Uint8Array(4 + stringLength * 2);
stringBuffer[0] = stringLength & 0xff;
stringBuffer[1] = (stringLength >>> 8) & 0xff;
stringBuffer[2] = (stringLength >>> 16) & 0xff;
stringBuffer[3] = (stringLength >>> 24) & 0xff;
for (let i = 0; i < stringLength; ++i) {
stringBuffer[4 + i * 2] = stringValue.charCodeAt(i) & 0xff;
stringBuffer[5 + i * 2] = (stringValue.charCodeAt(i) >>> 8) & 0xff;
}
stringSegment = this.addMemorySegment(stringBuffer, options.usizeType.byteSize);
stringSegments.set(stringValue, stringSegment);
}
var stringOffset = stringSegment.offset;
var stringType = this.program.typesLookup.get("string");
this.currentType = stringType ? stringType : options.usizeType;
if (options.isWasm64) {
return module.createI64(i64_low(stringOffset), i64_high(stringOffset));
}
assert(i64_is_i32(stringOffset));
return module.createI32(i64_low(stringOffset));
}
compileStaticArray(elementType: Type, expressions: (Expression | null)[], reportNode: Node): ExpressionRef {
// compile as static if all element expressions are precomputable, otherwise
// initialize in place.
var isStatic = true;
var size = expressions.length;
var module = this.module;
var nativeType = elementType.toNativeType();
var values: usize;
switch (nativeType) {
case NativeType.I32: {
values = changetype<usize>(new Int32Array(size));
break;
}
case NativeType.I64: {
values = changetype<usize>(new Array<I64>(size));
break;
}
case NativeType.F32: {
values = changetype<usize>(new Float32Array(size));
break;
}
case NativeType.F64: {
values = changetype<usize>(new Float64Array(size));
break;
}
default: {
assert(false);
this.error(
DiagnosticCode.Operation_not_supported,
reportNode.range
);
return module.createUnreachable();
}
}
var exprs = new Array<ExpressionRef>(size);
var expr: BinaryenExpressionRef;
for (let i = 0; i < size; ++i) {
exprs[i] = expressions[i]
? this.compileExpression(<Expression>expressions[i], elementType)
: elementType.toNativeZero(module);
if (isStatic) {
expr = this.precomputeExpressionRef(exprs[i]);
if (_BinaryenExpressionGetId(expr) == ExpressionId.Const) {
assert(_BinaryenExpressionGetType(expr) == nativeType);
switch (nativeType) {
case NativeType.I32: {
changetype<i32[]>(values)[i] = _BinaryenConstGetValueI32(expr);
break;
}
case NativeType.I64: {
changetype<I64[]>(values)[i] = i64_new(
_BinaryenConstGetValueI64Low(expr),
_BinaryenConstGetValueI64High(expr)
);
break;
}
case NativeType.F32: {
changetype<f32[]>(values)[i] = _BinaryenConstGetValueF32(expr);
break;
}
case NativeType.F64: {
changetype<f64[]>(values)[i] = _BinaryenConstGetValueF64(expr);
break;
}
default: {
assert(false); // checked above
}
}
} else {
// TODO: emit a warning if declared 'const'
isStatic = false;
}
}
}
if (isStatic) {
// TODO: convert to Uint8Array and create the segment
} else {
// TODO: initialize in place
}
// TODO: alternatively, static elements could go into data segments while
// dynamic ones are initialized on top? any benefits? (doesn't seem so)
throw new Error("not implemented");
}
compileNewExpression(expression: NewExpression, contextualType: Type): ExpressionRef {
var module = this.module;
var options = this.options;
var currentFunction = this.currentFunction;
var resolved = this.program.resolveExpression( // reports
expression.expression,
currentFunction
);
if (resolved) {
if (resolved.element.kind == ElementKind.CLASS_PROTOTYPE) {
let prototype = <ClassPrototype>resolved.element;
let instance = prototype.resolveUsingTypeArguments( // reports
expression.typeArguments,
null,
expression
);
if (instance) {
let thisExpr = compileBuiltinAllocate(this, instance, expression);
let initializers = new Array<ExpressionRef>();
// use a temp local for 'this'
let tempLocal = currentFunction.getTempLocal(options.usizeType);
initializers.push(module.createSetLocal(tempLocal.index, thisExpr));
// apply field initializers
if (instance.members) {
for (let member of instance.members.values()) {
if (member.kind == ElementKind.FIELD) {
let field = <Field>member;
let fieldDeclaration = field.prototype.declaration;
if (field.is(CommonFlags.CONST)) {
assert(false); // there are no built-in fields currently
} else if (fieldDeclaration && fieldDeclaration.initializer) {
initializers.push(module.createStore(field.type.byteSize,
module.createGetLocal(tempLocal.index, options.nativeSizeType),
this.compileExpression(fieldDeclaration.initializer, field.type),
field.type.toNativeType(),
field.memoryOffset
));
}
}
}
}
// apply constructor
let constructorInstance = instance.constructorInstance;
if (constructorInstance) {
initializers.push(this.compileCallDirect(constructorInstance, expression.arguments, expression,
module.createGetLocal(tempLocal.index, options.nativeSizeType)
));
}
// return 'this'
initializers.push(module.createGetLocal(tempLocal.index, options.nativeSizeType));
currentFunction.freeTempLocal(tempLocal);
thisExpr = module.createBlock(null, initializers, options.nativeSizeType);
this.currentType = instance.type;
return thisExpr;
}
} else {
this.error(
DiagnosticCode.Cannot_use_new_with_an_expression_whose_type_lacks_a_construct_signature,
expression.expression.range
);
}
}
return module.createUnreachable();
}
compileParenthesizedExpression(
expression: ParenthesizedExpression,
contextualType: Type,
wrapSmallIntegers: bool = true
): ExpressionRef {
// does not change types, just order
return this.compileExpression(
expression.expression,
contextualType,
ConversionKind.NONE,
wrapSmallIntegers
);
}
/**
* Compiles a property access in the specified context.
* @param retainConstantType Retains the type of inlined constants if `true`, otherwise
* precomputes them according to context.
*/
compilePropertyAccessExpression(
propertyAccess: PropertyAccessExpression,
contextualType: Type,
retainConstantType: bool
): ExpressionRef {
var program = this.program;
var module = this.module;
var resolved = program.resolvePropertyAccess(propertyAccess, this.currentFunction); // reports
if (!resolved) return module.createUnreachable();
var element = resolved.element;
var targetExpr: ExpressionRef;
switch (element.kind) {
case ElementKind.GLOBAL: { // static property
if (element.is(CommonFlags.BUILTIN)) {
return compileBuiltinGetConstant(this, <Global>element, propertyAccess);
}
if (!this.compileGlobal(<Global>element)) { // reports; not yet compiled if a static field
return module.createUnreachable();
}
let globalType = (<Global>element).type;
assert(globalType != Type.void);
if ((<Global>element).is(CommonFlags.INLINED)) {
return this.compileInlineConstant(<Global>element, contextualType, retainConstantType);
}
this.currentType = globalType;
return module.createGetGlobal((<Global>element).internalName, globalType.toNativeType());
}
case ElementKind.ENUMVALUE: { // enum value
if (!this.compileEnum((<EnumValue>element).enum)) {
return this.module.createUnreachable();
}
this.currentType = Type.i32;
if ((<EnumValue>element).is(CommonFlags.INLINED)) {
return module.createI32((<EnumValue>element).constantValue);
}
return module.createGetGlobal((<EnumValue>element).internalName, NativeType.I32);
}
case ElementKind.FIELD: { // instance field
assert(resolved.isInstanceTarget);
assert((<Field>element).memoryOffset >= 0);
targetExpr = this.compileExpression(
<Expression>resolved.targetExpression,
this.options.usizeType,
ConversionKind.NONE
);
this.currentType = (<Field>element).type;
return module.createLoad(
(<Field>element).type.size >> 3,
(<Field>element).type.is(TypeFlags.SIGNED | TypeFlags.INTEGER),
targetExpr,
(<Field>element).type.toNativeType(),
(<Field>element).memoryOffset
);
}
case ElementKind.PROPERTY: { // instance property (here: getter)
let prototype = (<Property>element).getterPrototype;
if (prototype) {
let instance = prototype.resolve(null); // reports
if (!instance) return module.createUnreachable();
let signature = instance.signature;
if (!this.checkCallSignature( // reports
signature,
0,
instance.is(CommonFlags.INSTANCE),
propertyAccess
)) {
return module.createUnreachable();
}
if (instance.instanceMethodOf) {
targetExpr = this.compileExpression(
<Expression>resolved.targetExpression,
instance.instanceMethodOf.type
);
this.currentType = signature.returnType;
return this.compileCallDirect(instance, [], propertyAccess, targetExpr);
} else {
this.currentType = signature.returnType;
return this.compileCallDirect(instance, [], propertyAccess);
}
} else {
this.error(
DiagnosticCode.Property_0_does_not_exist_on_type_1,
propertyAccess.range, (<Property>element).simpleName, (<Property>element).parent.toString()
);
return module.createUnreachable();
}
}
}
this.error(
DiagnosticCode.Operation_not_supported,
propertyAccess.range
);
return module.createUnreachable();
}
compileTernaryExpression(expression: TernaryExpression, contextualType: Type): ExpressionRef {
var ifThen = expression.ifThen;
var ifElse = expression.ifElse;
var condExpr = makeIsTrueish(
this.compileExpression(expression.condition, Type.u32, ConversionKind.NONE),
this.currentType,
this.module
);
// Eliminate unnecesssary branches in generic contexts if the condition is constant
if (
this.currentFunction.isAny(CommonFlags.GENERIC | CommonFlags.GENERIC_CONTEXT) &&
_BinaryenExpressionGetId(condExpr = this.precomputeExpressionRef(condExpr)) == ExpressionId.Const &&
_BinaryenExpressionGetType(condExpr) == NativeType.I32
) {
return _BinaryenConstGetValueI32(condExpr)
? this.compileExpression(ifThen, contextualType)
: this.compileExpression(ifElse, contextualType);
}
var ifThenExpr = this.compileExpression(ifThen, contextualType);
var ifElseExpr = this.compileExpression(ifElse, contextualType);
return this.module.createIf(condExpr, ifThenExpr, ifElseExpr);
}
compileUnaryPostfixExpression(expression: UnaryPostfixExpression, contextualType: Type): ExpressionRef {
var module = this.module;
var currentFunction = this.currentFunction;
// make a getter for the expression (also obtains the type)
var getValue = this.compileExpression(
expression.operand,
contextualType == Type.void
? Type.i32
: contextualType,
ConversionKind.NONE,
false // wrapped below
);
var currentType = this.currentType;
var op: BinaryOp;
var nativeType: NativeType;
var nativeOne: ExpressionRef;
var possiblyOverflows = false;
switch (expression.operator) {
case Token.PLUS_PLUS: {
if (currentType.isReference) {
this.error(
DiagnosticCode.Operation_not_supported,
expression.range
);
return module.createUnreachable();
}
switch (currentType.kind) {
case TypeKind.I8:
case TypeKind.I16:
case TypeKind.U8:
case TypeKind.U16:
case TypeKind.BOOL: possiblyOverflows = true;
default: {
op = BinaryOp.AddI32;
nativeType = NativeType.I32;
nativeOne = module.createI32(1);
break;
}
case TypeKind.USIZE: // TODO: check operator overload
case TypeKind.ISIZE: {
let options = this.options;
op = options.isWasm64
? BinaryOp.AddI64
: BinaryOp.AddI32;
nativeType = options.nativeSizeType;
nativeOne = currentType.toNativeOne(module);
break;
}
case TypeKind.I64:
case TypeKind.U64: {
op = BinaryOp.AddI64;
nativeType = NativeType.I64;
nativeOne = module.createI64(1);
break;
}
case TypeKind.F32: {
op = BinaryOp.AddF32;
nativeType = NativeType.F32;
nativeOne = module.createF32(1);
break;
}
case TypeKind.F64: {
op = BinaryOp.AddF64;
nativeType = NativeType.F64;
nativeOne = module.createF64(1);
break;
}
case TypeKind.VOID: {
assert(false);
this.error(
DiagnosticCode.Operation_not_supported,
expression.range
);
return module.createUnreachable();
}
}
break;
}
case Token.MINUS_MINUS: {
if (currentType.isReference) {
this.error(
DiagnosticCode.Operation_not_supported,
expression.range
);
return module.createUnreachable();
}
switch (currentType.kind) {
case TypeKind.I8:
case TypeKind.I16:
case TypeKind.U8:
case TypeKind.U16:
case TypeKind.BOOL: possiblyOverflows = true;
default: {
op = BinaryOp.SubI32;
nativeType = NativeType.I32;
nativeOne = module.createI32(1);
break;
}
case TypeKind.USIZE: // TODO: check operator overload
case TypeKind.ISIZE: {
let options = this.options;
op = options.isWasm64
? BinaryOp.SubI64
: BinaryOp.SubI32;
nativeType = options.nativeSizeType;
nativeOne = currentType.toNativeOne(module);
break;
}
case TypeKind.I64:
case TypeKind.U64: {
op = BinaryOp.SubI64;
nativeType = NativeType.I64;
nativeOne = module.createI64(1);
break;
}
case TypeKind.F32: {
op = BinaryOp.SubF32;
nativeType = NativeType.F32;
nativeOne = module.createF32(1);
break;
}
case TypeKind.F64: {
op = BinaryOp.SubF64;
nativeType = NativeType.F64;
nativeOne = module.createF64(1);
break;
}
case TypeKind.VOID: {
assert(false);
this.error(
DiagnosticCode.Operation_not_supported,
expression.range
);
return module.createUnreachable();
}
}
break;
}
default: {
assert(false);
this.error(
DiagnosticCode.Operation_not_supported,
expression.range
);
return module.createUnreachable();
}
}
var setValue: ExpressionRef;
var tempLocal: Local | null = null;
// simplify if dropped anyway
if (contextualType == Type.void) {
setValue = module.createBinary(op,
getValue,
nativeOne
);
// otherwise use a temp local for the intermediate value
} else {
tempLocal = currentFunction.getTempLocal(currentType);
setValue = module.createBinary(op,
this.module.createGetLocal(tempLocal.index, nativeType),
nativeOne
);
}
if (possiblyOverflows) {
assert(currentType.is(TypeFlags.SMALL | TypeFlags.INTEGER));
setValue = makeSmallIntegerWrap(setValue, currentType, module);
}
setValue = this.compileAssignmentWithValue(expression.operand, setValue, false);
// ^ sets currentType = void
if (contextualType == Type.void) {
assert(!tempLocal);
return setValue;
}
this.currentType = assert(tempLocal).type;
currentFunction.freeTempLocal(<Local>tempLocal);
var localIndex = (<Local>tempLocal).index;
return module.createBlock(null, [
module.createSetLocal(localIndex, getValue),
setValue,
module.createGetLocal(localIndex, nativeType)
], nativeType);
}
compileUnaryPrefixExpression(
expression: UnaryPrefixExpression,
contextualType: Type,
wrapSmallIntegers: bool = true
): ExpressionRef {
var module = this.module;
var currentType = this.currentType;
var possiblyOverflows = false;
var compound = false;
var expr: ExpressionRef;
switch (expression.operator) {
case Token.PLUS: {
if (currentType.isReference) {
this.error(
DiagnosticCode.Operation_not_supported,
expression.range
);
return module.createUnreachable();
}
expr = this.compileExpression(
expression.operand,
contextualType == Type.void
? Type.i32
: contextualType,
ConversionKind.NONE,
false // wrapped below
);
currentType = this.currentType;
possiblyOverflows = currentType.is(TypeFlags.SMALL | TypeFlags.INTEGER); // if operand already did
break;
}
case Token.MINUS: {
if (currentType.isReference) {
this.error(
DiagnosticCode.Operation_not_supported,
expression.range
);
return module.createUnreachable();
}
if (expression.operand.kind == NodeKind.LITERAL && (
(<LiteralExpression>expression.operand).literalKind == LiteralKind.INTEGER ||
(<LiteralExpression>expression.operand).literalKind == LiteralKind.FLOAT
)) {
// implicitly negate integer and float literals. also enables proper checking of literal ranges.
expr = this.compileLiteralExpression(<LiteralExpression>expression.operand, contextualType, true);
if (this.options.sourceMap) {
// compileExpression normally does this
addDebugLocation(expr, expression.range, module, this.currentFunction);
}
currentType = this.currentType;
} else {
expr = this.compileExpression(
expression.operand,
contextualType == Type.void
? Type.i32
: contextualType,
ConversionKind.NONE,
false // wrapped below
);
currentType = this.currentType;
switch (currentType.kind) {
case TypeKind.I8:
case TypeKind.I16:
case TypeKind.U8:
case TypeKind.U16:
case TypeKind.BOOL: possiblyOverflows = true; // or if operand already did
default: {
expr = module.createBinary(BinaryOp.SubI32, module.createI32(0), expr);
break;
}
case TypeKind.USIZE: {
if (currentType.isReference) {
this.error(
DiagnosticCode.Operation_not_supported,
expression.range
);
return module.createUnreachable();
}
// fall-through
}
case TypeKind.ISIZE: {
expr = module.createBinary(
this.options.isWasm64
? BinaryOp.SubI64
: BinaryOp.SubI32,
currentType.toNativeZero(module),
expr
);
break;
}
case TypeKind.I64:
case TypeKind.U64: {
expr = module.createBinary(BinaryOp.SubI64, module.createI64(0), expr);
break;
}
case TypeKind.F32: {
expr = module.createUnary(UnaryOp.NegF32, expr);
break;
}
case TypeKind.F64: {
expr = module.createUnary(UnaryOp.NegF64, expr);
break;
}
}
}
break;
}
case Token.PLUS_PLUS: {
if (currentType.isReference) {
this.error(
DiagnosticCode.Operation_not_supported,
expression.range
);
return module.createUnreachable();
}
compound = true;
expr = this.compileExpression(
expression.operand,
contextualType == Type.void
? Type.i32
: contextualType,
ConversionKind.NONE,
false // wrapped below
);
currentType = this.currentType;
switch (currentType.kind) {
case TypeKind.I8:
case TypeKind.I16:
case TypeKind.U8:
case TypeKind.U16:
case TypeKind.BOOL: possiblyOverflows = true; // or if operand already did
default: {
expr = module.createBinary(BinaryOp.AddI32, expr, this.module.createI32(1));
break;
}
case TypeKind.USIZE: {
if (currentType.isReference) {
this.error(
DiagnosticCode.Operation_not_supported,
expression.range
);
return module.createUnreachable();
}
// fall-through
}
case TypeKind.ISIZE: {
expr = module.createBinary(
this.options.isWasm64
? BinaryOp.AddI64
: BinaryOp.AddI32,
expr,
currentType.toNativeOne(module)
);
break;
}
case TypeKind.I64:
case TypeKind.U64: {
expr = module.createBinary(BinaryOp.AddI64, expr, module.createI64(1));
break;
}
case TypeKind.F32: {
expr = module.createBinary(BinaryOp.AddF32, expr, module.createF32(1));
break;
}
case TypeKind.F64: {
expr = module.createBinary(BinaryOp.AddF64, expr, module.createF64(1));
break;
}
}
break;
}
case Token.MINUS_MINUS: {
if (currentType.isReference) {
this.error(
DiagnosticCode.Operation_not_supported,
expression.range
);
return module.createUnreachable();
}
compound = true;
expr = this.compileExpression(
expression.operand,
contextualType == Type.void
? Type.i32
: contextualType,
ConversionKind.NONE,
false // wrapped below
);
currentType = this.currentType;
switch (currentType.kind) {
case TypeKind.I8:
case TypeKind.I16:
case TypeKind.U8:
case TypeKind.U16:
case TypeKind.BOOL: possiblyOverflows = true; // or if operand already did
default: {
expr = module.createBinary(BinaryOp.SubI32, expr, module.createI32(1));
break;
}
case TypeKind.USIZE: {
if (currentType.isReference) {
this.error(
DiagnosticCode.Operation_not_supported,
expression.range
);
return module.createUnreachable();
}
// fall-through
}
case TypeKind.ISIZE: {
expr = module.createBinary(
this.options.isWasm64
? BinaryOp.SubI64
: BinaryOp.SubI32,
expr,
currentType.toNativeOne(module)
);
break;
}
case TypeKind.I64:
case TypeKind.U64: {
expr = module.createBinary(BinaryOp.SubI64, expr, module.createI64(1));
break;
}
case TypeKind.F32: {
expr = module.createBinary(BinaryOp.SubF32, expr, module.createF32(1));
break;
}
case TypeKind.F64: {
expr = module.createBinary(BinaryOp.SubF64, expr, module.createF64(1));
break;
}
}
break;
}
case Token.EXCLAMATION: {
expr = this.compileExpression(
expression.operand,
contextualType == Type.void
? Type.i32
: contextualType,
ConversionKind.NONE,
true // must wrap small integers
);
expr = makeIsFalseish(expr, this.currentType, module);
this.currentType = Type.bool;
break;
}
case Token.TILDE: {
if (currentType.isReference) {
this.error(
DiagnosticCode.Operation_not_supported,
expression.range
);
return module.createUnreachable();
}
expr = this.compileExpression(
expression.operand,
contextualType == Type.void
? Type.i32
: contextualType.is(TypeFlags.FLOAT)
? Type.i64
: contextualType,
contextualType == Type.void
? ConversionKind.NONE
: ConversionKind.IMPLICIT,
false // retains low bits of small integers
);
currentType = this.currentType;
switch (currentType.kind) {
case TypeKind.I8:
case TypeKind.I16:
case TypeKind.U8:
case TypeKind.U16:
case TypeKind.BOOL: possiblyOverflows = true; // or if operand already did
default: {
expr = module.createBinary(BinaryOp.XorI32, expr, module.createI32(-1));
break;
}
case TypeKind.USIZE: {
if (currentType.isReference) {
this.error(
DiagnosticCode.Operation_not_supported,
expression.range
);
return module.createUnreachable();
}
// fall-through
}
case TypeKind.ISIZE: {
expr = module.createBinary(
this.options.isWasm64
? BinaryOp.XorI64
: BinaryOp.XorI32,
expr,
currentType.toNativeNegOne(module)
);
break;
}
case TypeKind.I64:
case TypeKind.U64: {
expr = module.createBinary(BinaryOp.XorI64, expr, module.createI64(-1, -1));
break;
}
}
break;
}
case Token.TYPEOF: {
// it might make sense to implement typeof in a way that a generic function can detect
// whether its type argument is a class type or string. that could then be used, for
// example, to generate hash codes for sets and maps, depending on the kind of type
// parameter we have. ideally the comparison would not involve actual string comparison and
// limit available operations to hard-coded string literals.
this.error(
DiagnosticCode.Operation_not_supported,
expression.range
);
return module.createUnreachable();
}
default: {
assert(false);
this.error(
DiagnosticCode.Operation_not_supported,
expression.range
);
return module.createUnreachable();
}
}
if (possiblyOverflows && wrapSmallIntegers) {
assert(currentType.is(TypeFlags.SMALL | TypeFlags.INTEGER));
expr = makeSmallIntegerWrap(expr, currentType, module);
}
return compound
? this.compileAssignmentWithValue(expression.operand, expr, contextualType != Type.void)
: expr;
}
}
// helpers
/** Adds the debug location of the specified expression at the specified range to the source map. */
function addDebugLocation(expr: ExpressionRef, range: Range, module: Module, currentFunction: Function): void {
var source = range.source;
if (source.debugInfoIndex < 0) {
source.debugInfoIndex = module.addDebugInfoFile(source.normalizedPath);
}
range.debugInfoRef = expr;
if (!currentFunction.debugLocations) currentFunction.debugLocations = [];
currentFunction.debugLocations.push(range);
}
/** Wraps a 32-bit integer expression so it evaluates to a valid value of the specified type. */
export function makeSmallIntegerWrap(expr: ExpressionRef, type: Type, module: Module): ExpressionRef {
switch (type.kind) {
case TypeKind.I8: {
return module.createBinary(BinaryOp.ShrI32,
module.createBinary(BinaryOp.ShlI32,
expr,
module.createI32(24)
),
module.createI32(24)
);
}
case TypeKind.I16: {
return module.createBinary(BinaryOp.ShrI32,
module.createBinary(BinaryOp.ShlI32,
expr,
module.createI32(16)
),
module.createI32(16)
);
}
case TypeKind.U8: {
return module.createBinary(BinaryOp.AndI32,
expr,
module.createI32(0xff)
);
}
case TypeKind.U16: {
return module.createBinary(BinaryOp.AndI32,
expr,
module.createI32(0xffff)
);
}
case TypeKind.BOOL: {
return module.createBinary(BinaryOp.AndI32,
expr,
module.createI32(0x1)
);
}
default: {
assert(false);
return expr;
}
}
}
/** Creates a comparison whether an expression is not 'true' in a broader sense. */
export function makeIsFalseish(expr: ExpressionRef, type: Type, module: Module): ExpressionRef {
switch (type.kind) {
default: { // any native i32
return module.createUnary(UnaryOp.EqzI32, expr);
}
case TypeKind.I64:
case TypeKind.U64: {
return module.createUnary(UnaryOp.EqzI64, expr);
}
case TypeKind.USIZE: // TODO: strings?
case TypeKind.ISIZE: {
return module.createUnary(type.size == 64 ? UnaryOp.EqzI64 : UnaryOp.EqzI32, expr);
}
case TypeKind.F32: {
return module.createBinary(BinaryOp.EqF32, expr, module.createF32(0));
}
case TypeKind.F64: {
return module.createBinary(BinaryOp.EqF64, expr, module.createF64(0));
}
case TypeKind.VOID: {
assert(false);
return module.createI32(1);
}
}
}
/** Creates a comparison whether an expression is 'true' in a broader sense. */
export function makeIsTrueish(expr: ExpressionRef, type: Type, module: Module): ExpressionRef {
switch (type.kind) {
default: { // any native i32
return expr;
}
case TypeKind.I64:
case TypeKind.U64: {
return module.createBinary(BinaryOp.NeI64, expr, module.createI64(0));
}
case TypeKind.USIZE: // TODO: strings?
case TypeKind.ISIZE: {
return type.size == 64
? module.createBinary(BinaryOp.NeI64, expr, module.createI64(0))
: expr;
}
case TypeKind.F32: {
return module.createBinary(BinaryOp.NeF32, expr, module.createF32(0));
}
case TypeKind.F64: {
return module.createBinary(BinaryOp.NeF64, expr, module.createF64(0));
}
case TypeKind.VOID: {
assert(false);
return module.createI32(0);
}
}
}