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

[WIP] feat: traits, initial implementation #46

Merged
merged 5 commits into from
Dec 16, 2019
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
16 changes: 13 additions & 3 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,21 @@
- figure out private fields structs
- for loops sugar syntax
- for in sugar syntax
- robust type alias use cases and test suites

every declaration with the same name of different impl must have the same visibility

exhaustive test of implicit coercion

ensure "is" always returns a boolean

internal variables/state are only accessible via getters/setters

arrays
strings
closure functions

```
```lys
// test parsing
enum test {}
```
Expand All @@ -31,14 +40,13 @@ fun malloc(size: i32): i32 = {
}
}
}

```

---

parser fails with

```
```lys
match x {
case None -> 123
case is Some -> 123
Expand Down Expand Up @@ -79,6 +87,8 @@ grammar changes:
# ??? is invalid

# index selectors and function name

16u32
```

porque si dallta checkeo de tipos sigue de largo para ExecutionHelper#testSrc?
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Test

**THIS TEST IS SKIPPED**

This tests an emergent pattern, it is a C enum-like pattern but stricter.

It is compatible with C-enums.
Expand Down
1 change: 1 addition & 0 deletions reference/Reference.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Reference
49 changes: 49 additions & 0 deletions reference/ReferenceCounter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Reference counting

In order to release unused memory allocations, count references is a pretty good mechanism, it is also easier than multi state garbage collectors. To implement it, it is necessary to add a header to every dynamic allocation. The header will be prepended to the allocated size and remain aligned to 16 bytes. The size of the header will be 16 bytes too. The returned ref will be the `address + header_size`

The allocation algorithm will be modified as follows:

```diff
// pseudo

fun malloc(size): ref = {
val ptr = malloc(size + 16)
+ setUpHeader(ptr, size)
+ ref(ptr + 16)
- ref(ptr)
}

+ fun setUpHeader(ptr, size) = {
+ // write allocation size in ptr
+ }
```

## Header

The header will contain information about the size of the allocation and the reference counter.

## Counting references

The algorithm uses two functions: `retain(ref)` and `release(ref)`. `retain` increments the reference counter by one. `release` decrements the reference counter by one.

The functions are located in the module `system::core::rtti`

## Scopes

A list of retained references will be added to the scopes. A statement to release all the retained references of the scope will be generated and called whenever the scope is left.

## Heuristics about reference counting

- If `retain` or `release` receive static allocations (strings or nullary structs) a short circuit is triggered.
- Allocators DO NOT call to `retain`
- Setters call to `release(old)` and `retain(new)`
- Variable assignations call to `release(old)` and `retain(new)`
- Variable declarations call `retain(value)`
- Variable declarations are registered in the current scope
- At the end of the scope a `release(V) for each declaration V in SCOPE` must be executed
- When a `break` or `continue` is executed, the code to release the variables of the scopes up until the loop declaration's scope (not inclusive) must be called
- BlockNode now produce scopes
- IfNode branches now produce new scopes
- MatcherNode now produce scopes
- Entering a function will retain every argument, leaving the function will release every argument
36 changes: 36 additions & 0 deletions reference/Traits.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Traits

Traits are an approach to polymorphism. Functions are defined in the trait and then a type can implement the trait.

In traits, functions may or may not have a function body. If the body is absent, then it will be required in the type's implementation of the trait. If the body is present in the trait, the whole implementation of the function can be skipped.

Inside the trait's scope there is a declared type called `Self`. It acts as a type parameter for the type used in the implementation of the trait.

```lys
trait Printable {
fun toString(self: Self): string
}

impl Printable for i32 {
#[method]
fun toString(self: Self): string = ???
// ^^^^ Here "Self" is literally "i32"
}
```

## Implementation strategy

### First implementation

The immediate of objective of this first implementation is to correctly implement some functions like `fun is(self: ref): boolean` and also to annotate types with "tag traits" for reference types. That will be used by the compiler to detect and inject reference counting in references.

- Traits are only used to ensure interfaces in the implemented types.
- No default implementations are allowed, every trait implementation must be verbose.
- No decorations are allowed in the trait's declaration.

### Future

- Traits with default implementations.
- Annotations in trait functions.
- Traits as type references, it will require dynamic dispatch.
- `ref is SomeTrait`
38 changes: 27 additions & 11 deletions src/compiler/NodeError.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { Nodes } from './nodes';
import { Type, IntersectionType } from './types';
import { Type, IntersectionType, TraitType } from './types';

export const DEBUG_TYPES = process.env.DEBUG_TYPES === '1' || process.env.DEBUG_TYPES === 'true';

export interface IPositionCapable {
readonly start: number;
Expand All @@ -25,6 +27,13 @@ export function isSamePositionOrInside(parent: IPositionCapable, child: IPositio
);
}

export function printDebugType(type?: Type | null | undefined | void) {
if (type) {
return `"${type}"${DEBUG_TYPES ? ' ' + type.inspect(1) : ''}`;
}
return `<Type ${type}>`;
}

export class PositionCapableError extends Error implements IErrorPositionCapable {
constructor(public message: string, public readonly position: IPositionCapable, public warning: boolean = false) {
super(message);
Expand Down Expand Up @@ -58,7 +67,12 @@ export class LysCompilerError extends AstNodeError {}

export class TypeMismatch extends LysTypeError {
constructor(public givenType: Type, public expectedType: Type, node: Nodes.Node) {
super(`Type mismatch: Type "${givenType}" is not assignable to "${expectedType}"`, node);
super(
expectedType instanceof TraitType
? `Type mismatch: Type ${printDebugType(givenType)} does not implement ${printDebugType(expectedType)}`
: `Type mismatch: Type ${printDebugType(givenType)} is not assignable to ${printDebugType(expectedType)}`,
node
);
}
}

Expand All @@ -69,23 +83,23 @@ export class CannotInferReturnType extends LysTypeError {
}

export class NotAValidType extends LysTypeError {
constructor(node: Nodes.Node) {
super(`This is not a type`, node);
constructor(node: Nodes.Node, type: Type | null) {
super(type ? `${printDebugType(type)} is not a type` : `This is not a type`, node);
}
}

export class UnexpectedType extends LysTypeError {
constructor(public type: Type, node: Nodes.Node) {
super(`${type} ${type.inspect(100)} is not a value, constructor or function.`, node);
super(`${printDebugType(type)} is not a value, constructor or function.`, node);
}
}

export class InvalidOverload extends LysTypeError {
constructor(public functionType: IntersectionType, public givenTypes: Type[], node: Nodes.Node) {
super(
`Could not find a valid overload for function of type ${functionType} with the arguments of type (${givenTypes.join(
', '
)})`,
`Could not find a valid overload for function of type ${printDebugType(
functionType
)} with the arguments of type (${givenTypes.join(', ')})`,
node
);
}
Expand All @@ -94,7 +108,9 @@ export class InvalidOverload extends LysTypeError {
export class InvalidCall extends LysTypeError {
constructor(public expectedTypes: Type[], public givenTypes: Type[], node: Nodes.Node) {
super(
`Invalid signature. Expecting arguments type (${expectedTypes.join(', ')}) but got (${givenTypes.join(', ')})`,
`Invalid signature. Expecting arguments type:\n (${expectedTypes.join(', ')})\nbut got:\n (${givenTypes.join(
', '
)})`,
node
);
}
Expand All @@ -107,7 +123,7 @@ export class UnreachableCode extends LysSemanticError {
}

export class NotAFunction extends LysTypeError {
constructor(public givenType: Type, node: Nodes.Node) {
super(`Type mismatch: Type "${givenType}" is not a function`, node);
constructor(public type: Type, node: Nodes.Node) {
super(`Type mismatch: Type ${printDebugType(type)}" is not a function`, node);
}
}
2 changes: 1 addition & 1 deletion src/compiler/Scope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { LysScopeError } from './NodeError';
import { indent } from '../utils/astPrinter';
import { TypeHelpers } from './types';

export type ReferenceType = 'TYPE' | 'VALUE' | 'FUNCTION';
export type ReferenceType = 'TYPE' | 'VALUE' | 'FUNCTION' | 'TRAIT';

export class Scope {
localScopeDeclares: Set<string> = new Set();
Expand Down
2 changes: 2 additions & 0 deletions src/compiler/annotations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ export namespace annotations {
export class Method extends Annotation {}
export class MethodCall extends Annotation {}

export class SignatureDeclaration extends Annotation {}

export class Explicit extends Annotation {}
export class ByPassFunction extends Annotation {}

Expand Down
29 changes: 25 additions & 4 deletions src/compiler/nodes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,9 +143,8 @@ export namespace Nodes {
export abstract class ExpressionNode extends Node {}

export class NameIdentifierNode extends Node {
impls = new Set<ImplDirective>();
internalIdentifier?: string;
namespaceNames?: Map<string, NameIdentifierNode>;
parentNamespace?: NameIdentifierNode;

get childrenOrEmpty(): Node[] {
return [];
Expand Down Expand Up @@ -489,16 +488,25 @@ export namespace Nodes {
}

export class ImplDirective extends DirectiveNode {
baseImpl?: ReferenceNode;
selfTypeName?: NameIdentifierNode;
readonly namespaceNames: Map<string, NameIdentifierNode> = new Map();

constructor(
astNode: ASTNode,
public readonly reference: ReferenceNode,
public readonly targetImpl: ReferenceNode,
public readonly directives: DirectiveNode[]
) {
super(astNode);
}

get childrenOrEmpty() {
return [...(this.decorators || []), this.reference, ...(this.directives || [])];
return [
...(this.decorators || []),
this.targetImpl,
...(this.baseImpl ? [this.baseImpl] : []),
...(this.directives || [])
];
}
}

Expand Down Expand Up @@ -593,6 +601,19 @@ export namespace Nodes {
}
}

export class TraitDirectiveNode extends DirectiveNode {
readonly namespaceNames: Map<string, NameIdentifierNode> = new Map();
selfTypeName?: NameIdentifierNode;

constructor(astNode: ASTNode, public readonly traitName: NameIdentifierNode, public directives: DirectiveNode[]) {
super(astNode);
}

get childrenOrEmpty() {
return [...(this.decorators || []), this.traitName, ...this.directives];
}
}

export class EnumDirectiveNode extends DirectiveNode {
constructor(
astNode: ASTNode,
Expand Down
48 changes: 45 additions & 3 deletions src/compiler/phases/canonicalPhase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,10 @@ const visitor = {
return ret;
},
ImplDirective(astNode: Nodes.ASTNode) {
const reference = visit(findChildrenTypeOrFail(astNode, 'Reference'));
const references = astNode.children.filter($ => $.type === 'Reference');

// the last Reference is the target
const target = visit(references.pop()!);

const directivesNode = findChildrenType(astNode, 'NamespaceElementList');

Expand All @@ -121,7 +124,11 @@ const visitor = {
: [];
// TODO: warn

const ret = new Nodes.ImplDirective(astNode, reference, directives);
const ret = new Nodes.ImplDirective(astNode, target, directives);

if (references.length) {
ret.baseImpl = visit(references.pop()!);
}

ret.isPublic = !findChildrenType(astNode, 'PrivateModifier');

Expand Down Expand Up @@ -199,6 +206,8 @@ const visitor = {

const variableName = visit(child);

console.assert(!!variableName, 'missing variable name');

const ret = new Nodes.TypeDirectiveNode(astNode, variableName);

ret.isPublic = isPublic;
Expand Down Expand Up @@ -230,6 +239,34 @@ const visitor = {

return ret;
},
TraitDirective(astNode: Nodes.ASTNode) {
const children = astNode.children.slice();

let child = children.shift()!;
let isPublic = true;

if (child.type === 'PrivateModifier') {
isPublic = false;
child = children.shift()!;
}

const variableName = visit(child);

const typeDeclElements = findChildrenType(astNode, 'TraitDeclElements');

const declarations = typeDeclElements ? typeDeclElements.children.map($ => visit($)) : [];

declarations.forEach($ => {
if ($ instanceof Nodes.FunDirectiveNode && !$.functionNode.body) {
$.functionNode.annotate(new annotations.SignatureDeclaration());
}
});

const ret = new Nodes.TraitDirectiveNode(astNode, variableName, declarations);
ret.isPublic = isPublic;

return ret;
},
FunDeclaration(astNode: Nodes.ASTNode) {
const functionName = visit(
findChildrenTypeOrFail(astNode, 'FunctionName', 'A function name is required').children[0]
Expand All @@ -251,7 +288,12 @@ const visitor = {
}

fun.parameters = params.children.map($ => visit($));
fun.body = visitLastChild(astNode);

const body = findChildrenType(astNode, 'FunAssignExpression');

if (body) {
fun.body = visitLastChild(body);
}

return fun;
},
Expand Down
Loading