Skip to content

Commit

Permalink
[V-19] Traits (#58)
Browse files Browse the repository at this point in the history
* Simplify fn body

* Add trait syntax object

* WIP Type compatibility helpers

* WIP continued

* Make isStructural a first class attribute again

* WIP

* Add RTT type compatibility.

* Impl getTrait

* Add resolve trait

* Add support for default trait implementations

* Bugfix

* It works... Already. Probably with dragons.

* Remove rtt compat checker for now

* Add trait impl type checker

* Add unit test

* Update docs
  • Loading branch information
drew-y authored Oct 15, 2024
1 parent 58e1420 commit 117b085
Show file tree
Hide file tree
Showing 25 changed files with 273 additions and 82 deletions.
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

0 comments on commit 117b085

Please sign in to comment.