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

[V-19] Traits #58

Merged
merged 16 commits into from
Oct 15, 2024
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -351,17 +351,17 @@ a union, they must have a case for each object in the union

## Traits

> Status: Not yet implemented
> Status: Partially implemented
> Supported for nominal types. Trait scoping is not yet enforced.

Traits define a set of behavior that can be implemented on any object type
(nominal, structural, union, or intersection)
Traits define a set of behavior that can be implemented on any nominal object or intersection.

```rust
trait Walk
fn walk() -> i32

// Implement walk for any type that contains the field legs: i32
impl Walk for { legs: i32 }
// Implement walk for any animal that contains the field legs: i32
impl Walk for Animal & { legs: i32 }
fn walk(self)
self.walk

Expand Down
16 changes: 11 additions & 5 deletions reference/types/traits.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# Traits

Traits are first class types that define the behavior of a nominal object.
> Status: Partially implemented

Traits are first class types that define the behavior of a nominal object or intersection.

```voyd
trait Run
Expand All @@ -22,7 +24,7 @@ let car = Car { speed: 10 }
log car.run() // "Vroom!"
&car.stop()

car can Run // true
car has_trait Run // true

// Because traits are first class types, they can be used to define parameters
// that will accept any type that implements the trait
Expand All @@ -34,7 +36,7 @@ run_thing(car) // Vroom!

## Default Implementations

Status: Not yet implemented
> Status: Complete

Traits can specify default implementations which are automatically applied
on implementation, but may still be overridden by that impl if desired
Expand All @@ -47,15 +49,17 @@ trait One

## Trait Requirements

Status: Not yet implemented
> Status: Not yet implemented

Traits can specify that implementors must also implement other traits:

```voyd
trait DoWork requires: This & That
```

## Trait limitations
## Trait Scoping

> Status: Not yet implemented

Traits must be in scope to be used. If the `Run` trait were defined
in a different file (or module), it would have to be imported before its
Expand All @@ -69,6 +73,8 @@ use other_file::{ Run }
car.run() // Vroom!
```

## Trait limitations

Trait implementations cannot have overlapping target types:

```voyd
Expand Down
1 change: 1 addition & 0 deletions src/__tests__/compiler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ describe("E2E Compiler Pipeline", () => {
173, // Array test
4, // Structural object re-assignment
"world",
8, // trait impls
]);
});

Expand Down
25 changes: 25 additions & 0 deletions src/__tests__/fixtures/e2e-file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,31 @@ pub fn test24()
v.value
None:
"not found"

trait Math
fn add(self, b: i32) -> self
fn sub(self, b: i32) -> self
fn mul(self, b: i32) -> self

obj MathBox<T> {
value: T
}

impl<T> Math for MathBox<T>
fn add(self, b: i32) -> self
MathBox<T> { value: self.value + b }

fn sub(self, b: i32) -> self
MathBox<T> { value: self.value - b }

fn mul(self, b: i32) -> self
MathBox<T> { value: self.value * b }

// Test trait impls, should return 8
pub fn test25()
let a = MathBox<i32> { value: 4 }
let b = a.add(4)
b.value
`;

export const tcoText = `
Expand Down
13 changes: 6 additions & 7 deletions src/assembler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,10 @@ import * as gc from "./lib/binaryen-gc/index.js";
import { TypeRef } from "./lib/binaryen-gc/types.js";
import { getExprType } from "./semantics/resolution/get-expr-type.js";
import { Match, MatchCase } from "./syntax-objects/match.js";
import { initExtensionHelpers } from "./assembler/extension-helpers.js";
import { initExtensionHelpers } from "./assembler/rtt/extension.js";
import { returnCall } from "./assembler/return-call.js";
import { Float } from "./syntax-objects/float.js";
import { initFieldLookupHelpers } from "./assembler/field-lookup-helpers.js";
import { initFieldLookupHelpers } from "./assembler/index.js";
import { StringLiteral } from "./syntax-objects/string-literal.js";

export const assemble = (ast: Expr) => {
Expand Down Expand Up @@ -75,6 +75,7 @@ export const compileExpression = (opts: CompileExprOpts): number => {
if (expr.isUse()) return mod.nop();
if (expr.isMacro()) return mod.nop();
if (expr.isMacroVariable()) return mod.nop();
if (expr.isTrait()) return mod.nop();

if (expr.isBool()) {
return expr.value ? mod.i32.const(1) : mod.i32.const(0);
Expand Down Expand Up @@ -379,7 +380,7 @@ const compileFieldAssign = (opts: CompileExprOpts<Call>) => {
const target = access.exprArgAt(0);
const type = getExprType(target) as ObjectType | IntersectionType;

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

Expand Down Expand Up @@ -636,8 +637,6 @@ const buildObjectType = (opts: MapBinTypeOpts, obj: ObjectType): TypeRef => {
type: opts.fieldLookupHelpers.lookupTableType,
name: "__field_index_table",
},
// Reference to the field index lookup function
// TODO
// Fields
...obj.fields.map((field) => ({
type: mapBinaryenType(opts, field.type!),
Expand Down Expand Up @@ -675,7 +674,7 @@ const buildObjectType = (opts: MapBinTypeOpts, obj: ObjectType): TypeRef => {
);
}

if (obj.getAttribute("isStructural")) {
if (obj.isStructural) {
obj.setAttribute("originalType", obj.binaryenType);
obj.binaryenType = mapBinaryenType(opts, voydBaseObject);
}
Expand All @@ -691,7 +690,7 @@ const compileObjMemberAccess = (opts: CompileExprOpts<Call>) => {
const objValue = compileExpression({ ...opts, expr: obj });
const type = getExprType(obj) as ObjectType | IntersectionType;

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

Expand Down
2 changes: 2 additions & 0 deletions src/assembler/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./rtt/index.js";
export * from "./return-call.js";
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import binaryen from "binaryen";
import { AugmentedBinaryen } from "../lib/binaryen-gc/types.js";
import { AugmentedBinaryen } from "../../lib/binaryen-gc/types.js";
import {
defineArrayType,
arrayLen,
arrayGet,
arrayNewFixed,
binaryenTypeToHeapType,
} from "../lib/binaryen-gc/index.js";
} from "../../lib/binaryen-gc/index.js";

const bin = binaryen as unknown as AugmentedBinaryen;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import binaryen from "binaryen";
import { AugmentedBinaryen } from "../lib/binaryen-gc/types.js";
import { AugmentedBinaryen } from "../../lib/binaryen-gc/types.js";
import {
defineArrayType,
arrayLen,
Expand All @@ -13,24 +13,23 @@ import {
callRef,
refCast,
structSetFieldValue,
} from "../lib/binaryen-gc/index.js";
} from "../../lib/binaryen-gc/index.js";
import {
IntersectionType,
ObjectType,
voydBaseObject,
} from "../syntax-objects/types.js";
import { murmurHash3 } from "../lib/murmur-hash.js";
} from "../../syntax-objects/types.js";
import { murmurHash3 } from "../../lib/murmur-hash.js";
import {
compileExpression,
CompileExprOpts,
mapBinaryenType,
} from "../assembler.js";
import { Call } from "../syntax-objects/call.js";
import { getExprType } from "../semantics/resolution/get-expr-type.js";
} from "../../assembler.js";
import { Call } from "../../syntax-objects/call.js";
import { getExprType } from "../../semantics/resolution/get-expr-type.js";

const bin = binaryen as unknown as AugmentedBinaryen;

/** DOES NOT ACCOUNT FOR FIELD OFFSET */
export const initFieldLookupHelpers = (mod: binaryen.Module) => {
const fieldAccessorStruct = defineStructType(mod, {
name: "FieldAccessor",
Expand Down
3 changes: 3 additions & 0 deletions src/assembler/rtt/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from "./extension.js";
export * from "./rtt.js";
export * from "./field-accessor.js";
1 change: 1 addition & 0 deletions src/assembler/rtt/rtt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const PLACEHOLDER = 0;
8 changes: 8 additions & 0 deletions src/lib/binaryen-gc/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,14 @@ export const arrayNewFixed = (
return result;
};

export const initFixedArray = (
mod: binaryen.Module,
type: TypeRef,
values: ExpressionRef[]
): ExpressionRef => {
return arrayNewFixed(mod, binaryenTypeToHeapType(type), values);
};

export const arrayCopy = (
mod: binaryen.Module,
destRef: ExpressionRef,
Expand Down
49 changes: 45 additions & 4 deletions src/semantics/check-types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Implementation } from "../syntax-objects/implementation.js";
import {
List,
Expr,
Expand All @@ -23,6 +24,7 @@ import {
import { Match } from "../syntax-objects/match.js";
import { getExprType } from "./resolution/get-expr-type.js";
import { typesAreCompatible } from "./resolution/index.js";
import { resolveFnSignature } from "./resolution/resolve-fn.js";

export const checkTypes = (expr: Expr | undefined): Expr => {
if (!expr) return nop();
Expand Down Expand Up @@ -280,7 +282,9 @@ const checkFnTypes = (fn: Fn): Fn => {
const checkParameters = (params: Parameter[]) => {
params.forEach((p) => {
if (!p.type) {
throw new Error(`Unable to determine type for ${p} at ${p.location}`);
throw new Error(
`Unable to determine type for ${p} at ${p.name.location}`
);
}

checkTypeExpr(p.typeExpr);
Expand Down Expand Up @@ -354,7 +358,20 @@ const checkObjectType = (obj: ObjectType): ObjectType => {
}
});

obj.implementations.forEach((impl) => impl.methods.forEach(checkTypes));
const implementedTraits = new Set<string>();
obj.implementations.forEach((impl) => {
if (!impl.trait) return;

if (implementedTraits.has(impl.trait.id)) {
throw new Error(
`Trait ${impl.trait.name} implemented multiple times for obj ${obj.name} at ${obj.location}`
);
}

implementedTraits.add(impl.trait.id);
});

obj.implementations.forEach(checkImpl);

if (obj.parentObjExpr) {
assertValidExtension(obj, obj.parentObjType);
Expand Down Expand Up @@ -400,7 +417,7 @@ const checkTypeExpr = (expr?: Expr) => {
return;
}

if (expr.isIdentifier()) {
if (expr.isIdentifier() && !expr.is("self")) {
const entity = expr.resolve();
if (!entity) {
throw new Error(`Unrecognized identifier ${expr} at ${expr.location}`);
Expand Down Expand Up @@ -447,6 +464,30 @@ const checkTypeAlias = (alias: TypeAlias): TypeAlias => {
return alias;
};

const checkImpl = (impl: Implementation): Implementation => {
if (impl.traitExpr.value && !impl.trait) {
throw new Error(`Unable to resolve trait for impl at ${impl.location}`);
}

if (!impl.trait) return impl;

for (const method of impl.trait.methods.toArray()) {
const mClone = resolveFnSignature(method.clone(impl));

if (
!impl.exports.some((fn) =>
typesAreCompatible(fn.getType(), mClone.getType())
)
) {
throw new Error(
`Impl does not implement ${method.name} at ${impl.location}`
);
}
}

return impl;
};

const checkListTypes = (list: List) => {
console.log("Unexpected list");
console.log(JSON.stringify(list, undefined, 2));
Expand Down Expand Up @@ -478,7 +519,7 @@ const checkIntersectionType = (inter: IntersectionType) => {
throw new Error(`Unable to resolve intersection type ${inter.location}`);
}

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