Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions tslang/lib/TypeScript/MLIRGenClasses.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2389,6 +2389,48 @@ genContext);
}
}

// a class field holding a function (`fn: () => int = ...`) stores a hybrid function -
// {function, `this` or null} - or, unannotated (`fn = () => 7`), a plain one: neither can be handed out
// as a function taking the object as its `this`. A thunk calling `this.fn(...)` fills the
// slot as a method does; it reads the field at each call, so a field assigned later is seen.
if (found && isa<mlir_ts::FunctionType>(fieldType) && fieldType != foundField.type
&& mth.isAnyFunctionType(foundField.type))
{
auto fieldName = dyn_cast<mlir::StringAttr>(id);
auto match = mth.TestFunctionTypesMatchWithObjectMethods(location, foundField.type, fieldType);
if (!fieldName || match.result != MatchResultType::Match)
{
emitError(location) << "field type not matching for " << id << ":" << to_print(fieldType)
<< " for interface '" << newInterfacePtr->fullName << "' in class '"
<< newClassPtr->fullName << "'. Found field: " << id << ":"
<< to_print(foundField.type);
return {emptyFieldInfo, mlir::failure()};
}

// a field reached twice through a diamond `extends` has two slots and one cell
if (llvm::none_of(methodsForFields, [&](auto &methodForField) { return methodForField.first == id; }))
{
auto funcType = mlir::cast<mlir_ts::FunctionType>(fieldType);
auto thunkName = concat(interfaceVTableNameForClass(newClassPtr, newInterfacePtr),
fieldName.getValue(), "thunk");
if (mlir::failed(generateSynthMethodToCallField(location, newClassPtr, fieldName.getValue(),
thunkName, funcType, genContext)))
{
return {emptyFieldInfo, mlir::failure()};
}

MethodInfo thunkInfo{};
thunkInfo.name = thunkName.str();
thunkInfo.funcType = funcType;
thunkInfo.funcName = thunkName.str();
thunkInfo.virtualIndex = -1;
thunkInfo.accessLevel = mlir_ts::AccessLevel::Public;
methodsForFields.push_back({id, thunkInfo});
}

return {mlir_ts::FieldInfo{id, fieldType, false, mlir_ts::AccessLevel::Public}, mlir::success()};
}

// a class's `v?: T` is stored as optional<T>; the interface's `v?: T` reads it by its flag
// (a tagged slot, see isStoredAsOptional below)
auto optionalStorage = found && isConditional && isa<mlir_ts::OptionalType>(foundField.type)
Expand Down
66 changes: 66 additions & 0 deletions tslang/lib/TypeScript/MLIRGenImpl.h
Original file line number Diff line number Diff line change
Expand Up @@ -12130,6 +12130,72 @@ class MLIRGenImpl
return fullClassStaticName.str();
}

// `<thunkName>(this, p0..pn) { return this.<fieldName>(p0..pn); }`, typed as the interface's
// function-typed field (`this` opaque): the method an interface slot calls for a class whose
// field of that name holds the function (see mlirGenClassVirtualTableDefinitionForInterface)
mlir::LogicalResult generateSynthMethodToCallField(mlir::Location location, ClassInfo::TypePtr newClassPtr,
StringRef fieldName, StringRef thunkName,
mlir_ts::FunctionType funcType, const GenContext &genContext)
{
auto retType = mth.getReturnTypeFromFuncRef(funcType);

mlir::OpBuilder::InsertionGuard guard(builder);
builder.setInsertionPointToStart(theModule.getBody());

GenContext funcGenContext(genContext);
funcGenContext.clearScopeVars();
funcGenContext.thisType = newClassPtr->classType;
funcGenContext.thisClassType = newClassPtr->classType;
funcGenContext.disableSpreadParams = true;

return mlirGenFunctionBody(
location, fieldName, thunkName, funcType,
[&](mlir::Location location, const GenContext &genContext) -> mlir::LogicalResult {
// the interface passes the object as an opaque `this`
auto funcOp = genContext.funcOp;
auto opaqueThis = funcOp.getBody().front().getArgument(0);
auto thisValue = builder.create<mlir_ts::CastOp>(location, newClassPtr->classType, opaqueThis);
auto thisDecl = std::make_shared<VariableDeclarationDOM>(THIS_NAME, newClassPtr->classType, location);
DECLARE(thisDecl, thisValue);

NodeFactory nf(NodeFactoryFlags::None);

NodeArray<Expression> argumentsArray;
for (auto index = 1; index < funcType.getNumInputs(); index++)
{
std::string paramName("p");
paramName += std::to_string(index - 1);
argumentsArray.push_back(nf.createIdentifier(stows(paramName)));
}

auto fieldAccess = nf.createPropertyAccessExpression(
nf.createToken(SyntaxKind::ThisKeyword), nf.createIdentifier(stows(fieldName.str())));
auto callResult = mlirGen(nf.createCallExpression(fieldAccess, undefined, argumentsArray), genContext);
EXIT_IF_FAILED(callResult);

if (!retType || isa<mlir_ts::VoidType>(retType))
{
return mlir::success();
}

auto castToRet = cast(location, retType, V(callResult), genContext);
EXIT_IF_FAILED_OR_NO_VALUE(castToRet);
auto retVarInfo = symbolTable.lookup(RETURN_VARIABLE_NAME);
if (!retVarInfo.second)
{
return mlir::failure();
}

// built op by op, so the retain a `return` performs is repeated here (as in
// generateSynthMethodToCallNewCtor)
auto returnValue = V(castToRet);
mlirGenRetainCaptured(location, mlir::ValueRange{returnValue});
builder.create<mlir_ts::ReturnValOp>(location, returnValue, retVarInfo.first);
return mlir::success();
},
funcGenContext, 1/*to skip This*/);
}

mlir::LogicalResult mlirGenClassBaseInterfaces(mlir::Location location, ClassInfo::TypePtr newClassPtr,
const GenContext &genContext);

Expand Down
3 changes: 3 additions & 0 deletions tslang/test/tester/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -612,6 +612,7 @@ tslang_add_test(NAME test-compile-00-interface-optional COMMAND test-runner "${P
tslang_add_test(NAME test-compile-00-interface-optional-cast-order COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_optional_cast_order.ts")
tslang_add_test(NAME test-compile-00-interface-function-typed-field COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_function_typed_field.ts")
tslang_add_test(NAME test-compile-00-interface-function-typed-field-class COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_function_typed_field_class.ts")
tslang_add_test(NAME test-compile-00-interface-function-typed-field-class-field COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_function_typed_field_class_field.ts")
tslang_add_test(NAME test-compile-00-interface-captures COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_captures.ts")
tslang_add_test(NAME test-compile-00-object-annotated-method COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00object_annotated_method.ts")
tslang_add_test(NAME test-compile-00-object-annotated-method-params COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00object_annotated_method_params.ts")
Expand Down Expand Up @@ -1166,6 +1167,7 @@ tslang_add_test(NAME test-jit-00-interface-optional COMMAND test-runner -jit "${
tslang_add_test(NAME test-jit-00-interface-optional-cast-order COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_optional_cast_order.ts")
tslang_add_test(NAME test-jit-00-interface-function-typed-field COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_function_typed_field.ts")
tslang_add_test(NAME test-jit-00-interface-function-typed-field-class COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_function_typed_field_class.ts")
tslang_add_test(NAME test-jit-00-interface-function-typed-field-class-field COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_function_typed_field_class_field.ts")
tslang_add_test(NAME test-jit-00-interface-captures COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_captures.ts")
tslang_add_test(NAME test-jit-00-object-annotated-method COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00object_annotated_method.ts")
tslang_add_test(NAME test-jit-00-object-annotated-method-params COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00object_annotated_method_params.ts")
Expand Down Expand Up @@ -2028,6 +2030,7 @@ set(TSLANG_CORPUS
00interface_conjunction.ts
00interface_function_typed_field.ts
00interface_function_typed_field_class.ts
00interface_function_typed_field_class_field.ts
00interface_generic.ts
00interface_global_method.ts
00interface_indexer.ts
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
// regression test: a class FIELD holding a function fills an interface member
// declared as a function-typed field (`fn: () => int`). The class stores the
// field as a hybrid function ({function, `this` or null}), which the interface
// slot can't hand out as a function taking the object as its `this`, so the
// cast used to fail ("field type not matching for "fn""). The slot now points
// at a cell holding a thunk that calls `this.fn(...)` (INTERFACE_METHOD_CELL_TAG,
// Defines.h), so the field is read at every call.

interface HasFn {
fn: () => int;
}

class WithField {
fn: () => int = () => 7;
}

// unannotated: stored as a plain function
class Unannotated {
fn = () => 6;
}

class Capturing {
fn: () => int;
constructor(public x: int, k: int) {
this.fn = () => this.x + k;
}
}

class BoundMethod {
fn: () => int;
constructor(public v: int) {
this.fn = this.m;
}
m(): int {
return this.v * 2;
}
}

interface Fmt {
fmt: (a: int, s: string) => string;
}

class FmtField {
constructor(public prefix: string) {}
fmt: (a: int, s: string) => string = (a: int, s: string) => `${s}=${a}`;
}

let calls = 0;

interface Sink {
put: (v: int) => void;
}

class SinkField {
put: (v: int) => void = (v: int) => {
calls += v;
};
}

class Implements implements HasFn {
fn: () => int = () => 11;
}

class FieldBase {
fn: () => int = () => 12;
}

class FieldDerived extends FieldBase {}

interface P1 extends HasFn {
a: int;
}

interface P2 extends HasFn {
b: int;
}

interface Both extends P1, P2 {}

class Diamond {
a = 1;
b = 2;
fn: () => int = () => 13;
}

function callFn(h: HasFn): int {
return h.fn();
}

function fromField(): () => int {
const h: HasFn = new WithField();
return h.fn;
}

function main() {
const h: HasFn = new WithField();
print(h.fn());
assert(h.fn() == 7);

const u: HasFn = new Unannotated();
assert(u.fn() == 6);

const c: HasFn = new Capturing(30, 5);
assert(c.fn() == 35);

const b: HasFn = new BoundMethod(21);
assert(b.fn() == 42);

// the thunk reads the field at each call
const w = new WithField();
const hw: HasFn = w;
w.fn = () => 8;
assert(hw.fn() == 8);

const f: Fmt = new FmtField("p");
print(f.fmt(3, "x"));
assert(f.fmt(3, "x") == "x=3");

const s: Sink = new SinkField();
s.put(4);
s.put(5);
assert(calls == 9);

const i: HasFn = new Implements();
assert(i.fn() == 11);
const d: HasFn = new FieldDerived();
assert(d.fn() == 12);
const both: Both = new Diamond();
assert(both.fn() == 13);
assert(both.a == 1 && both.b == 2);

// one access site: a class field, a class with a method, an object literal
assert(callFn(new WithField()) == 7);
assert(callFn({ fn() { return 41; } }) == 41);

const g = fromField();
assert(g() == 7);

print("done.");
}