Skip to content

Commit

Permalink
[VOI-111] Object implementations (#45)
Browse files Browse the repository at this point in the history
* WIP

* Implementation type

* WIP

* Maybe working prototype?

* Automatic clone ID updates

* Attempt 1.

* Working (non-generic) impl

Within same module

* Working generics

* Move method lookup to get call fn

Rather than importing all methods of a type to module scope on use type.

This should reduce instances of ambiguous calls.

* Add some docs

* WIP

* Update getCall test

* Add tests, squash bugs

* Update test

* Add docs
  • Loading branch information
drew-y committed Sep 17, 2024
1 parent f9cc66b commit fdf4688
Show file tree
Hide file tree
Showing 32 changed files with 733 additions and 247 deletions.
79 changes: 46 additions & 33 deletions reference/functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,17 @@ move({ x, y, z })
move(x: x, y: y, z: z)
```

Labeled arguments also support concise closure sugar on call sites:

```void
fn try({ do: ((): throws -> void), catch: (e: Error) -> void })
try do():
this_may_throw()
catch(e):
log e
```

[1] The compiler will typically optimize this away, so there is no performance
penalty for using labeled arguments.

Expand Down Expand Up @@ -123,25 +134,6 @@ fn add<T: Numeric>(a: T, b: T) -> T

See the chapter on [Generics](./generics.md) for more information.

## Call By Name Parameters

Call by name parameters automatically wrap the passed expression in a closure.
Call by name parameters are defined by prefixing the parameter type with `@`.
Their type must always be a function type with no parameters.

```rust
fn eval_twice(@f: () -> void) -> void
f()
f()

fn main()
var x = 0
eval_twice(x = x + 1)
print(x) // 2
```

Use by name parameters very SPARINGLY. And only when the function name makes
it obvious that the parameter is a function.

## Parenthetical Elision

Expand Down Expand Up @@ -226,40 +218,61 @@ See the chapter on [Syntax](./syntax.md) for more information and detailed rules

## Function Overloading

Void functions can be overloaded. Provided that function overload can be
unambiguously distinguished via their parameters and return type.
Void functions can be overloaded. Provided that function overload can be unambiguously distinguished via their parameters and return type.

```void
fn sum(a: i32, b: i32)
print("Def 1")
a + b
fn sum(vec: {a:i32, b: i32})
fn sum(vec: { a:i32, b: i32 })
print("Def 2")
vec.a + vec.b
sum a: 1, b: 2 // Def 1
sum { a: 1, b: 2 } // Def 2
// ERROR: sum(numbers: ...Int) overlaps ambiguously with sum(a: Int, b: Int)
fn sum(numbers: ...Int)
print("Def 3")
```

This can be especially useful for overloading operators to support a custom
type:

```
```void
fn '+'(a: Vec3, b: Vec3) -> Vec3
Vec3(a.x + b.x, a.y + b.y, a.z + b.z)
Vec3(1, 2, 3) + Vec3(4, 5, 6) // Vec3(5, 7, 9)
```

### Rules
A function call is considered to be ambiguous when multiple functions in
the same scope share the same name, and the types of each parameter overlap
in order.

```void
fn add(a: i32, b: i32) -> i32
fn add(d: i32, e: i32) -> i32 // Ambiguous collision
fn add(f: i32, c: f32) -> i32 // This is fine, the second parameter does not overlap with previous
```

Object types overlap if one is an extension of the other:

```void
obj Animal {}
obj Dog extends Animal {}
obj Cat extends Animal {}
fn walk(animal: Animal)
fn walk(dog: Dog) // Ambiguous collision with walk(animal: Animal)
// Sibling types do not overlap
fn speak(cat: Cat)
fn speak(dog: Dog) // This is fine
// Labeled parameters can be to add an explicit distinction between two overlapping
// types, (provided they have different labels)
fn walk({animal: Animal})
fn walk({dog: Dog}) // This is fine, the label, dog, distinguishes this function from the previous
walk(dog: dexter) // Woof!
```

- A function signature is:
- Its identifier
- Its parameters, their name, types, order, and label (if applicable)
- Each full function signature must be unique in a given scope
- TBD...
## Function Resolution
102 changes: 61 additions & 41 deletions reference/types/objects.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ pairs (fields).

They are defined by listing their fields between curly braces `{}`.

```
```void
type MyObject = {
a: i32,
b: i32
Expand All @@ -17,7 +17,7 @@ type MyObject = {
An object is initialized using object literal syntax. Listing the fields and
their corresponding values between curly braces `{}`.

```
```void
let my_object: MyObject = {
a: 5,
b: 4
Expand Down Expand Up @@ -195,27 +195,62 @@ impl Animal
self.name = name
let me = Animal { name: "John" }
log(me.run()) // "John is running!"
log me.run // "John is running!"
// The & prefix must be used to call methods that mutate the object
&me.change_name("Bob")
```

## Final Objects
### Inheritance

Objects can be defined as final, meaning they cannot be extended.
Unlike other languages, objects do not inherit methods from their parent
type by default. Instead, they must be opted in with use statements:

```void
final obj Animal {
name: String
}
obj Dog extends Animal {}
// Error - Animal is final
obj Cat extends Animal {
lives_remaining: i32
}
impl Dog
use super::{ run } // or `all` to inherit all methods
fn main()
let dog = Dog { name: "Dexter" }
dog.run() // Dexter is running
```


Void uses static dispatch for all methods defined on a type. That is, when
a function is called on a method, the function is determined at compile time.

In practice, this means that compiler will pick the method on the declared type,
even if a subtype is passed. For example:

```void
obj Animal {}
obj Dog extends Animal {}
impl Animal
pub fn talk()
log "Glub glub"
impl Dog
pub fn talk()
log "Bark"
fn interact(animal: Animal)
animal.talk()
let dog = Dog {}
// Here, because interact only knows it will receive an animal, it calls talk from Animal
interact(dog) // Glub glub
// Here, the compiler knows dog is Dog, so it calls talk from Dog
dog.talk() // Bark
```

The next section will discuss how to coerce a function like `interact` into
using the methods of a subtype.

## Object Type Narrowing

```void
Expand All @@ -242,34 +277,19 @@ fn main(a: i32, b: i32)
log "Error: divide by zero"
```

# Traits

Traits are first class types that define the behavior of a nominal object.
## Final Objects

```
trait Runnable
fn run(self) -> String
fn stop(mut self) -> void
Objects can be defined as final, meaning they cannot be extended.

obj Car {
speed: i32
```void
final obj Animal {
name: String
}
impl Runnable for Car
fn run(self) -> String
"Vroom!"
fn stop(mut self) -> void
self.speed = 0
let car = Car { speed: 10 }
log(car.run()) // "Vroom!"
&car.stop()
car is Runnable // true
fn run_thing(thing: Runnable) -> void
log(thing.run())
// Error - Animal is final
obj Cat extends Animal {
lives_remaining: i32
}
```

# Built in Object Types
Expand All @@ -281,7 +301,7 @@ grow and shrink in size when defined as a mutable variable.

Type: `String`

```
```void
let my_string = String()
// String literals are of type `String`
Expand All @@ -294,16 +314,16 @@ Arrays are a growable sequence of values of the same type.

Type: `Array`

```
```void
let my_array = Array(1, 2, 3)
```

## Dictionaries
## Maps

Dictionaries are a growable collection of key-value pairs.

Type: `Dictionary`
Type: `Map`

```
```void
let my_dict = Dict { a: 1, b: 2, c: 3 }
```
86 changes: 86 additions & 0 deletions reference/types/traits.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# Traits

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

```void
trait Run
fn run(self) -> String
fn stop(&self) -> void
obj Car {
speed: i32
}
impl Run for Car
fn run(self) -> String
"Vroom!"
fn stop(&self) -> void
self.speed = 0
let car = Car { speed: 10 }
log car.run() // "Vroom!"
&car.stop()
car can Run // true
// Because traits are first class types, they can be used to define parameters
// that will accept any type that implements the trait
fn run_thing(thing: Run) -> void
log thing.run()
run_thing(car) // Vroom!
```

## Default Implementations

Status: Not yet implemented

Traits can specify default implementations which are automatically applied
on implementation, but may still be overridden by that impl if desired

```void
trait One
fn one() -> i32
1
```

## Trait Requirements

Status: Not yet implemented

Traits can specify that implementors must also implement other traits:

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

## Trait limitations

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
methods could be used

```void
car.run() // Error, no function found for run
use other_file::{ Run }
car.run() // Vroom!
```

Trait implementations cannot have overlapping target types:

```void
obj Animal {}
obj Dog {}
trait Speak
fn speak() -> void
impl Speak for Animal
fn speak()
log "Glub glub"
impl Speak for Dog // ERROR: Speak is already implemented for Dog via parent type Animal
```
Loading

0 comments on commit fdf4688

Please sign in to comment.