Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[VOI-34] Intersections #48

Merged
merged 8 commits into from
Sep 21, 2024
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
1 change: 1 addition & 0 deletions src/__tests__/compiler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ describe("E2E Compiler Pipeline", () => {
82,
3,
42,
2, // IntersectionType tests
]);
});

Expand Down
12 changes: 12 additions & 0 deletions src/__tests__/fixtures/e2e-file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,18 @@ pub fn test17()
match(x)
Some<i32>: x.value
None: -1

obj Animal { age: i32 }
obj Insect extends Animal { age: i32, legs: i32 }
obj Mammal extends Animal { age: i32, legs: i32 }

fn get_legs(a: Animal & { legs: i32 }) -> i32
a.legs

// Test intersection types
pub fn test18() -> i32
let human = Mammal { age: 10, legs: 2 }
get_legs(human)
`;

export const tcoText = `
Expand Down
30 changes: 28 additions & 2 deletions src/assembler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
DsArrayType,
voidBaseObject,
UnionType,
IntersectionType,
} from "./syntax-objects/types.js";
import { Variable } from "./syntax-objects/variable.js";
import { Block } from "./syntax-objects/block.js";
Expand Down Expand Up @@ -110,6 +111,16 @@ const compileType = (opts: CompileExprOpts<Type>) => {
return opts.mod.nop();
}

if (type.isUnionType()) {
buildUnionType(opts, type);
return opts.mod.nop();
}

if (type.isIntersectionType()) {
buildIntersectionType(opts, type);
return opts.mod.nop();
}

return opts.mod.nop();
};

Expand Down Expand Up @@ -462,6 +473,7 @@ export const mapBinaryenType = (
if (type.isObjectType()) return buildObjectType(opts, type);
if (type.isUnionType()) return buildUnionType(opts, type);
if (type.isDsArrayType()) return buildDsArrayType(opts, type);
if (type.isIntersectionType()) return buildIntersectionType(opts, type);
throw new Error(`Unsupported type ${type}`);
};

Expand All @@ -488,6 +500,20 @@ const buildUnionType = (opts: MapBinTypeOpts, union: UnionType): TypeRef => {
return typeRef;
};

const buildIntersectionType = (
opts: MapBinTypeOpts,
inter: IntersectionType
): TypeRef => {
if (inter.hasAttribute("binaryenType")) {
return inter.getAttribute("binaryenType") as TypeRef;
}

const typeRef = mapBinaryenType(opts, inter.nominalType!);
mapBinaryenType(opts, inter.structuralType!);
inter.setAttribute("binaryenType", typeRef);
return typeRef;
};

// Marks the start of the fields in an object after RTT info fields
const OBJECT_FIELDS_OFFSET = 2;

Expand Down Expand Up @@ -565,9 +591,9 @@ const compileObjMemberAccess = (opts: CompileExprOpts<Call>) => {
const obj = expr.exprArgAt(0);
const member = expr.identifierArgAt(1);
const objValue = compileExpression({ ...opts, expr: obj });
const type = getExprType(obj) as ObjectType;
const type = getExprType(obj) as ObjectType | IntersectionType;

if (type.getAttribute("isStructural")) {
if (type.getAttribute("isStructural") || type.isIntersectionType()) {
return opts.fieldLookupHelpers.getFieldValueByAccessor(opts);
}

Expand Down
20 changes: 17 additions & 3 deletions src/assembler/field-lookup-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@ import {
callRef,
refCast,
} from "../lib/binaryen-gc/index.js";
import { ObjectType, voidBaseObject } from "../syntax-objects/types.js";
import {
IntersectionType,
ObjectType,
voidBaseObject,
} from "../syntax-objects/types.js";
import { murmurHash3 } from "../lib/murmur-hash.js";
import {
compileExpression,
Expand Down Expand Up @@ -145,9 +149,19 @@ export const initFieldLookupHelpers = (mod: binaryen.Module) => {
const { expr, mod } = opts;
const obj = expr.exprArgAt(0);
const member = expr.identifierArgAt(1);
const objType = getExprType(obj) as ObjectType;
const objType = getExprType(obj) as ObjectType | IntersectionType;

const field = objType.isIntersectionType()
? objType.nominalType?.getField(member) ??
objType.structuralType?.getField(member)
: objType.getField(member);

if (!field) {
throw new Error(
`Field ${member.value} not found on object ${objType.id}`
);
}

const field = objType.getField(member)!;
const lookupTable = structGetFieldValue({
mod,
fieldType: lookupTableType,
Expand Down
41 changes: 30 additions & 11 deletions src/semantics/check-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,11 @@ import {
TypeAlias,
ObjectLiteral,
UnionType,
IntersectionType,
} from "../syntax-objects/index.js";
import { Match } from "../syntax-objects/match.js";
import { getExprType } from "./resolution/get-expr-type.js";
import { typesAreEquivalent } from "./resolution/index.js";
import { typesAreCompatible } from "./resolution/index.js";

export const checkTypes = (expr: Expr | undefined): Expr => {
if (!expr) return nop();
Expand All @@ -37,6 +38,7 @@ export const checkTypes = (expr: Expr | undefined): Expr => {
if (expr.isObjectLiteral()) return checkObjectLiteralType(expr);
if (expr.isUnionType()) return checkUnionType(expr);
if (expr.isMatch()) return checkMatch(expr);
if (expr.isIntersectionType()) return checkIntersectionType(expr);
return expr;
};

Expand Down Expand Up @@ -85,7 +87,7 @@ const checkObjectInit = (call: Call): Call => {
checkTypes(literal);

// Check to ensure literal structure is compatible with nominal structure
if (!typesAreEquivalent(literal.type, call.type, { structuralOnly: true })) {
if (!typesAreCompatible(literal.type, call.type, { structuralOnly: true })) {
throw new Error(`Object literal type does not match expected type`);
}

Expand All @@ -109,7 +111,7 @@ export const checkAssign = (call: Call) => {

const initType = getExprType(call.argAt(1));

if (!typesAreEquivalent(variable.type, initType)) {
if (!typesAreCompatible(variable.type, initType)) {
throw new Error(`${id} cannot be assigned to ${initType}`);
}

Expand All @@ -134,7 +136,7 @@ const checkIdentifier = (id: Identifier) => {
export const checkIf = (call: Call) => {
const cond = checkTypes(call.argAt(0));
const condType = getExprType(cond);
if (!condType || !typesAreEquivalent(condType, bool)) {
if (!condType || !typesAreCompatible(condType, bool)) {
throw new Error(
`If conditions must resolve to a boolean at ${cond.location}`
);
Expand All @@ -153,7 +155,7 @@ export const checkIf = (call: Call) => {
const elseType = getExprType(elseExpr);

// Until unions are supported, throw an error when types don't match
if (!typesAreEquivalent(thenType, elseType)) {
if (!typesAreCompatible(thenType, elseType)) {
throw new Error("If condition clauses do not return same type");
}

Expand Down Expand Up @@ -214,7 +216,7 @@ const checkFnTypes = (fn: Fn): Fn => {

if (
inferredReturnType &&
!typesAreEquivalent(inferredReturnType, fn.returnType)
!typesAreCompatible(inferredReturnType, fn.returnType)
) {
throw new Error(
`Fn, ${fn.name}, return value type (${inferredReturnType?.name}) is not compatible with annotated return type (${fn.returnType?.name}) at ${fn.location}`
Expand Down Expand Up @@ -269,7 +271,7 @@ const checkVarTypes = (variable: Variable): Variable => {

if (
variable.annotatedType &&
!typesAreEquivalent(variable.inferredType, variable.annotatedType)
!typesAreCompatible(variable.inferredType, variable.annotatedType)
) {
throw new Error(
`${variable.name} of type ${variable.type} is not assignable to ${variable.inferredType}`
Expand Down Expand Up @@ -316,7 +318,7 @@ export function assertValidExtension(

const validExtension = parent.fields.every((field) => {
const match = child.fields.find((f) => f.name === field.name);
return match && typesAreEquivalent(field.type, match.type);
return match && typesAreCompatible(field.type, match.type);
});

if (!validExtension) {
Expand Down Expand Up @@ -375,6 +377,23 @@ const checkMatch = (match: Match) => {
return checkObjectMatch(match);
};

const checkIntersectionType = (inter: IntersectionType) => {
checkTypeExpr(inter.nominalTypeExpr.value);
checkTypeExpr(inter.structuralTypeExpr.value);

if (!inter.nominalType || !inter.structuralType) {
throw new Error(`Unable to resolve intersection type ${inter.location}`);
}

if (!inter.structuralType.getAttribute("isStructural")) {
throw new Error(
`Structural type must be a structural type ${inter.structuralTypeExpr.value.location}`
);
}

return inter;
};

const checkUnionMatch = (match: Match) => {
const union = match.baseType as UnionType;

Expand All @@ -391,7 +410,7 @@ const checkUnionMatch = (match: Match) => {
);
}

if (!typesAreEquivalent(mCase.expr.type, match.type)) {
if (!typesAreCompatible(mCase.expr.type, match.type)) {
throw new Error(
`All cases must return the same type for now ${mCase.expr.location}`
);
Expand All @@ -400,7 +419,7 @@ const checkUnionMatch = (match: Match) => {

union.types.forEach((type) => {
if (
!match.cases.some((mCase) => typesAreEquivalent(mCase.matchType, type))
!match.cases.some((mCase) => typesAreCompatible(mCase.matchType, type))
) {
throw new Error(
`Match does not handle all possibilities of union ${match.location}`
Expand Down Expand Up @@ -436,7 +455,7 @@ const checkObjectMatch = (match: Match) => {
);
}

if (!typesAreEquivalent(mCase.expr.type, match.type)) {
if (!typesAreCompatible(mCase.expr.type, match.type)) {
throw new Error(
`All cases must return the same type for now ${mCase.expr.location}`
);
Expand Down
25 changes: 25 additions & 0 deletions src/semantics/init-entities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
DsArrayType,
nop,
UnionType,
IntersectionType,
} from "../syntax-objects/index.js";
import { Match, MatchCase } from "../syntax-objects/match.js";
import { SemanticProcessor } from "./types.js";
Expand Down Expand Up @@ -67,6 +68,10 @@ export const initEntities: SemanticProcessor = (expr) => {
return initPipedUnionType(expr);
}

if (expr.calls("&")) {
return initIntersection(expr);
}

return initCall(expr);
};

Expand Down Expand Up @@ -199,6 +204,22 @@ const initPipedUnionType = (union: List) => {
});
};

const initIntersection = (intersection: List): IntersectionType => {
const nominalObjectExpr = initTypeExprEntities(intersection.at(1));
const structuralObjectExpr = initTypeExprEntities(intersection.at(2));

if (!nominalObjectExpr || !structuralObjectExpr) {
throw new Error("Invalid intersection type");
}

return new IntersectionType({
...intersection.metadata,
name: intersection.syntaxId.toString(),
nominalObjectExpr,
structuralObjectExpr,
});
};

const initVar = (varDef: List): Variable => {
const isMutable = varDef.calls("define_mut");
const identifierExpr = varDef.at(1);
Expand Down Expand Up @@ -332,6 +353,10 @@ const initTypeExprEntities = (type?: Expr): Expr | undefined => {
return initPipedUnionType(type);
}

if (type.calls("&")) {
return initIntersection(type);
}

return initCall(type);
};

Expand Down
6 changes: 3 additions & 3 deletions src/semantics/resolution/get-call-fn.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Call, Expr, Fn } from "../../syntax-objects/index.js";
import { getExprType } from "./get-expr-type.js";
import { typesAreEquivalent } from "./types-are-equivalent.js";
import { typesAreCompatible } from "./types-are-compatible.js";
import { resolveFnTypes } from "./resolve-fn-type.js";

export const getCallFn = (call: Call): Fn | undefined => {
Expand Down Expand Up @@ -80,7 +80,7 @@ const typeArgsMatch = (call: Call, candidate: Fn): boolean =>
? candidate.appliedTypeArgs.every((t, i) => {
const argType = getExprType(call.typeArgs?.at(i));
const appliedType = getExprType(t);
return typesAreEquivalent(argType, appliedType, {
return typesAreCompatible(argType, appliedType, {
exactNominalMatch: true,
});
})
Expand All @@ -94,7 +94,7 @@ const parametersMatch = (candidate: Fn, call: Call) =>
if (!argType) return false;
const argLabel = getExprLabel(arg);
const labelsMatch = p.label === argLabel;
return typesAreEquivalent(argType, p.type!) && labelsMatch;
return typesAreCompatible(argType, p.type!) && labelsMatch;
});

const getExprLabel = (expr?: Expr): string | undefined => {
Expand Down
2 changes: 1 addition & 1 deletion src/semantics/resolution/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export { resolveTypes } from "./resolve-types.js";
export { typesAreEquivalent } from "./types-are-equivalent.js";
export { typesAreCompatible } from "./types-are-compatible.js";
export { resolveModulePath } from "./resolve-use.js";
35 changes: 27 additions & 8 deletions src/semantics/resolution/resolve-call-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,16 +79,35 @@ const getMemberAccessCall = (call: Call): Call | undefined => {
const a1 = call.argAt(0);
if (!a1) return;
const a1Type = getExprType(a1);
if (!a1Type || !a1Type.isObjectType() || !a1Type.hasField(call.fnName)) {
return;

if (a1Type && a1Type.isObjectType() && a1Type.hasField(call.fnName)) {
return new Call({
...call.metadata,
fnName: Identifier.from("member-access"),
args: new List({ value: [a1, call.fnName] }),
type: a1Type.getField(call.fnName)?.type,
});
}

if (
a1Type &&
a1Type.isIntersectionType() &&
(a1Type.nominalType?.hasField(call.fnName) ||
a1Type.structuralType?.hasField(call.fnName))
) {
const field =
a1Type.nominalType?.getField(call.fnName) ??
a1Type.structuralType?.getField(call.fnName);

return new Call({
...call.metadata,
fnName: Identifier.from("member-access"),
args: new List({ value: [a1, call.fnName] }),
type: field?.type,
});
}

return new Call({
...call.metadata,
fnName: Identifier.from("member-access"),
args: new List({ value: [a1, call.fnName] }),
type: a1Type.getField(call.fnName)?.type,
});
return undefined;
};

export const resolveIf = (call: Call) => {
Expand Down
Loading