Skip to content

Commit

Permalink
Initial type parameter inference, see #61
Browse files Browse the repository at this point in the history
This catches the most common cases but doesn't yet implement inference involving the return type because some prequesites are not yet in place (see test case).
  • Loading branch information
dcodeIO committed Apr 13, 2018
1 parent 748e811 commit ee73a4d
Show file tree
Hide file tree
Showing 11 changed files with 385 additions and 43 deletions.
2 changes: 1 addition & 1 deletion dist/asc.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/asc.js.map

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/assemblyscript.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/assemblyscript.js.map

Large diffs are not rendered by default.

175 changes: 138 additions & 37 deletions src/compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1811,7 +1811,6 @@ export class Compiler extends DiagnosticEmitter {
}
}
if (!isInlined) {
let flow = currentFunction.flow;
if (
declaration.isAny(CommonFlags.LET | CommonFlags.CONST) ||
flow.is(FlowFlags.INLINE_CONTEXT)
Expand Down Expand Up @@ -4404,55 +4403,131 @@ export class Compiler extends DiagnosticEmitter {
// direct call: concrete function
case ElementKind.FUNCTION_PROTOTYPE: {
let prototype = <FunctionPrototype>target;
let typeArguments = expression.typeArguments;

// builtins are compiled on the fly
// builtins handle present respectively omitted type arguments on their own
if (prototype.is(CommonFlags.AMBIENT | CommonFlags.BUILTIN)) {
let expr = compileBuiltinCall( // reports
this,
prototype,
prototype.resolveBuiltinTypeArguments(
expression.typeArguments,
currentFunction.flow.contextualTypeArguments
),
expression.arguments,
contextualType,
expression
);
if (!expr) {
return this.compileCallExpressionBuiltin(prototype, expression, contextualType);
}

let instance: Function | null = null;

// resolve generic call if type arguments have been provided
if (typeArguments) {
if (!prototype.is(CommonFlags.GENERIC)) {
this.error(
DiagnosticCode.Operation_not_supported,
expression.range
DiagnosticCode.Type_0_is_not_generic,
expression.expression.range, prototype.internalName
);
return module.createUnreachable();
}
return expr;

// otherwise compile to a call (and maybe inline)
} else {
let instance = prototype.resolveUsingTypeArguments( // reports
expression.typeArguments,
currentFunction.flow.contextualTypeArguments,
instance = prototype.resolveUsingTypeArguments( // reports
typeArguments,
this.currentFunction.flow.contextualTypeArguments,
expression
);
if (!instance) return module.createUnreachable();
let thisExpr: ExpressionRef = 0;
if (instance.is(CommonFlags.INSTANCE)) {
thisExpr = this.compileExpressionRetainType(
assert(this.program.resolvedThisExpression),
this.options.usizeType
);

// infer generic call if type arguments have been omitted
} else if (prototype.is(CommonFlags.GENERIC)) {
let inferredTypes = new Map<string,Type | null>();
let typeParameters = assert(prototype.declaration.typeParameters);
let numTypeParameters = typeParameters.length;
for (let i = 0; i < numTypeParameters; ++i) {
inferredTypes.set(typeParameters[i].name.text, null);
}
// let numInferred = 0;
let parameterTypes = prototype.declaration.signature.parameterTypes;
let numParameterTypes = parameterTypes.length;
let argumentExpressions = expression.arguments;
let numArguments = argumentExpressions.length;
let argumentExprs = new Array<ExpressionRef>(numArguments);
for (let i = 0; i < numParameterTypes; ++i) {
let typeNode = parameterTypes[i].type;
let name = typeNode.kind == NodeKind.TYPE ? (<TypeNode>typeNode).name.text : null;
let argumentExpression = i < numArguments
? argumentExpressions[i]
: prototype.declaration.signature.parameterTypes[i].initializer;
if (!argumentExpression) { // missing initializer -> too few arguments
this.error(
DiagnosticCode.Expected_0_arguments_but_got_1,
expression.range, numParameterTypes.toString(10), numArguments.toString(10)
);
return module.createUnreachable();
}
if (name !== null && inferredTypes.has(name)) {
let inferredType = inferredTypes.get(name);
if (inferredType) {
argumentExprs[i] = this.compileExpressionRetainType(argumentExpression, inferredType);
let commonType: Type | null;
if (!(commonType = Type.commonCompatible(inferredType, this.currentType, true))) {
if (!(commonType = Type.commonCompatible(inferredType, this.currentType, false))) {
this.error(
DiagnosticCode.Type_0_is_not_assignable_to_type_1,
parameterTypes[i].type.range, this.currentType.toString(), inferredType.toString()
);
return module.createUnreachable();
}
}
inferredType = commonType;
} else {
argumentExprs[i] = this.compileExpressionRetainType(argumentExpression, Type.i32);
inferredType = this.currentType;
// ++numInferred;
}
inferredTypes.set(name, inferredType);
} else {
let concreteType = this.program.resolveType(
parameterTypes[i].type,
this.currentFunction.flow.contextualTypeArguments,
true
);
if (!concreteType) return module.createUnreachable();
argumentExprs[i] = this.compileExpression(argumentExpression, concreteType);
}
}
return this.compileCallDirect(
instance,
expression.arguments,
expression,
thisExpr,
instance.hasDecorator(DecoratorFlags.INLINE)
let resolvedTypeArguments = new Array<Type>(numTypeParameters);
for (let i = 0; i < numTypeParameters; ++i) {
let inferredType = assert(inferredTypes.get(typeParameters[i].name.text)); // TODO
resolvedTypeArguments[i] = inferredType;
}
instance = prototype.resolve(
resolvedTypeArguments,
this.currentFunction.flow.contextualTypeArguments
);
if (!instance) return this.module.createUnreachable();
return this.makeCallDirect(instance, argumentExprs);
// TODO: this skips inlining because inlining requires compiling its temporary locals in
// the scope of the inlined flow. might need another mechanism to lock temp. locals early,
// so inlining can be performed in `makeCallDirect` instead?

// otherwise resolve the non-generic call as usual
} else {
instance = prototype.resolve(
null,
this.currentFunction.flow.contextualTypeArguments
);
}
if (!instance) return this.module.createUnreachable();

// compile 'this' expression if an instance method
let thisExpr: ExpressionRef = 0;
if (instance.is(CommonFlags.INSTANCE)) {
thisExpr = this.compileExpressionRetainType(
assert(this.program.resolvedThisExpression),
this.options.usizeType
);
}

return this.compileCallDirect(
instance,
expression.arguments,
expression,
thisExpr,
instance.hasDecorator(DecoratorFlags.INLINE)
);
}

// indirect call: index argument with signature
// indirect call: index argument with signature (non-generic, can't be inlined)
case ElementKind.LOCAL: {
if (signature = (<Local>target).type.signatureReference) {
indexArg = module.createGetLocal((<Local>target).index, NativeType.I32);
Expand Down Expand Up @@ -4525,6 +4600,32 @@ export class Compiler extends DiagnosticEmitter {
);
}

private compileCallExpressionBuiltin(
prototype: FunctionPrototype,
expression: CallExpression,
contextualType: Type
): ExpressionRef {
var expr = compileBuiltinCall( // reports
this,
prototype,
prototype.resolveBuiltinTypeArguments(
expression.typeArguments,
this.currentFunction.flow.contextualTypeArguments
),
expression.arguments,
contextualType,
expression
);
if (!expr) {
this.error(
DiagnosticCode.Operation_not_supported,
expression.range
);
return this.module.createUnreachable();
}
return expr;
}

/**
* Checks that a call with the given number as arguments can be performed according to the
* specified signature.
Expand Down
2 changes: 1 addition & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -470,7 +470,7 @@ export function typesToString(types: Type[]): string {
for (let i = 0; i < numTypes; ++i) {
sb[i] = types[i].toString();
}
return sb.join(", ");
return sb.join(",");
}

/** Represents a fully resolved function signature. */
Expand Down
14 changes: 14 additions & 0 deletions std/assembly/builtins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,22 @@ export declare const NaN: f64; // | f32
export declare const Infinity: f64; // | f32

export declare function isNaN<T>(value: T): bool;
// export function isNaN<T>(value: T): bool {
// return isFloat(value)
// ? sizeof<T>() == 32
// ? (reinterpret<u32>(value) & -1 >>> 1) > 0xFF << 23
// : (reinterpret<u64>(value) & -1 >>> 1) > 0x7FF << 52
// : false;
// }

export declare function isFinite<T>(value: T): bool;
// export function isFinite<T>(value: T): bool {
// return isFloat(value)
// ? sizeof<T>() == 32
// ? (reinterpret<u32>(value) & -1 >>> 1) < 0xFF << 23
// : (reinterpret<u64>(value) & -1 >>> 1) < 0x7FF << 52
// : true;
// }

export declare function clz<T>(value: T): T;

Expand Down
2 changes: 1 addition & 1 deletion tests/compiler/builtins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -311,4 +311,4 @@ assert(f64.EPSILON == 2.2204460492503131e-16);

// should be importable
import { isNaN as isItNaN } from "builtins";
isItNaN(1);
isItNaN<f64>(1);
91 changes: 91 additions & 0 deletions tests/compiler/call-inferred.optimized.wat
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
(module
(type $ii (func (param i32) (result i32)))
(type $iiiiv (func (param i32 i32 i32 i32)))
(type $FF (func (param f64) (result f64)))
(type $ff (func (param f32) (result f32)))
(type $v (func))
(import "env" "abort" (func $abort (param i32 i32 i32 i32)))
(memory $0 1)
(data (i32.const 4) "\10\00\00\00c\00a\00l\00l\00-\00i\00n\00f\00e\00r\00r\00e\00d\00.\00t\00s")
(export "memory" (memory $0))
(start $start)
(func $call-inferred/foo<i32> (; 1 ;) (type $ii) (param $0 i32) (result i32)
(get_local $0)
)
(func $call-inferred/foo<f64> (; 2 ;) (type $FF) (param $0 f64) (result f64)
(get_local $0)
)
(func $call-inferred/foo<f32> (; 3 ;) (type $ff) (param $0 f32) (result f32)
(get_local $0)
)
(func $start (; 4 ;) (type $v)
(if
(i32.ne
(call $call-inferred/foo<i32>
(i32.const 42)
)
(i32.const 42)
)
(block
(call $abort
(i32.const 0)
(i32.const 4)
(i32.const 5)
(i32.const 0)
)
(unreachable)
)
)
(if
(f64.ne
(call $call-inferred/foo<f64>
(f64.const 42)
)
(f64.const 42)
)
(block
(call $abort
(i32.const 0)
(i32.const 4)
(i32.const 6)
(i32.const 0)
)
(unreachable)
)
)
(if
(f32.ne
(call $call-inferred/foo<f32>
(f32.const 42)
)
(f32.const 42)
)
(block
(call $abort
(i32.const 0)
(i32.const 4)
(i32.const 7)
(i32.const 0)
)
(unreachable)
)
)
(if
(f32.ne
(call $call-inferred/foo<f32>
(f32.const 42)
)
(f32.const 42)
)
(block
(call $abort
(i32.const 0)
(i32.const 4)
(i32.const 13)
(i32.const 0)
)
(unreachable)
)
)
)
)
25 changes: 25 additions & 0 deletions tests/compiler/call-inferred.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
function foo<T>(a: T): T {
return a;
}

assert(foo(42) == 42);
assert(foo(42.0) == 42);
assert(foo(<f32>42.0) == 42);

function bar<T>(a: T = <f32>42.0): T {
return a;
}

assert(bar() == 42);

// TODO: this'd require return type inference, i.e., omitted return type
// function baz<T>(a: i32): T {
// return a;
// }
// baz(42);

// TODO: this'd ideally be inferred by matching contextualType, avoiding conversions
// function baz<T>(): T {
// return 1;
// }
// baz(42);
Loading

0 comments on commit ee73a4d

Please sign in to comment.