Skip to content

Commit

Permalink
Update type system design. Reserve auto keyword.
Browse files Browse the repository at this point in the history
  • Loading branch information
fubark committed Oct 2, 2023
1 parent c8f633a commit dc40940
Show file tree
Hide file tree
Showing 15 changed files with 426 additions and 268 deletions.
10 changes: 6 additions & 4 deletions docs/hugo/content/docs/toc/syntax.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,9 @@ func foo():
```

## Variables.
In Cyber, there are local variables and static variables.
In Cyber, there are local variables and static variables. The following sections show how variables are declared with the dynamic type.

For declaring typed variables, see [Typed variables]({{<relref "/docs/toc/type-system#typed-variables">}}) and [`auto` declarations]({{<relref "/docs/toc/type-system#auto-declarations">}}).

### Local Variables.
Local variables exist until the end of their scope.
Expand Down Expand Up @@ -163,11 +165,11 @@ var myImage:
The final resulting value that is assigned to the static variable is provided by a `break` statement. If a `break` statement is not provided, `none` is assigned instead.
## Keywords.
There are currently `33` keywords in Cyber. This list categorizes them and shows you when you might need them.
There are currently `34` keywords in Cyber. This list categorizes them and shows you when you might need them.
- [Control Flow]({{<relref "/docs/toc/control-flow">}}): `if` `else` `match` `while` `for` `each` `break` `continue` `pass` `some`
- [Control Flow]({{<relref "/docs/toc/control-flow">}}): `if` `else` `match` `case` `while` `for` `each` `break` `continue` `pass` `some`
- [Operators](#operators): `or` `and` `not` `is`
- [Variables](#variables): `var` `as`
- [Variables](#variables): `var` `as` `auto`
- [Functions]({{<relref "/docs/toc/functions">}}): `func` `return`
- [Coroutines]({{<relref "/docs/toc/concurrency#fibers">}}): `coinit` `coyield`, `coresume`
- [Data Types]({{<relref "/docs/toc/data-types">}}): `type` `object` `enum` `true` `false` `none`
Expand Down
225 changes: 156 additions & 69 deletions docs/hugo/content/docs/toc/type-system.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,37 +5,84 @@ weight: 9
---

# Type System.
Cyber supports gradual typing which allows the use of both dynamically and statically typed code.
> _Incomplete: Types in general is in development. One of the goals of Cyber is to let dynamic code mix with typed code. At the moment, there are places where it works and other places where it won't. Keep that in mind when using types._
Cyber supports the use of both dynamically and statically typed code.

## Dynamic typing.
Dynamic typing can reduce the amount of friction when writing code, but it can also result in more runtime errors.
Gradual typing allows you to add static typing incrementally which provides compile-time guarantees and prevents runtime errors.
Static typing also makes it easier to maintain and refactor your code.

## Dynamic typing.
A variable with the `any` type can hold any value. It can only be copied to destinations that also accept the `any` type. An `any` value can be used as the callee for a function call or the receiver for a method call. It can be used with any operators.
### `dynamic` vs `any`
Variables without a type specifier are implicitly assigned the `dynamic` type.
`dynamic` values can be freely used and copied without any compile errors (if there is a chance it can succeed at runtime, see [Recent type inference](#recent-type-inference)):
```cy
var a = 123

func getFirstRune(s string):
return s[0]

getFirstRune(a) -- RuntimeError. Expected `string`.
```
Since `a` is dynamic, passing it to a typed function parameter is allowed at compile-time, but will fail when the function is invoked at runtime.

The `any` type on the otherhand is a **static type** and must be explicitly declared as a variable's type specifier:
```cy
var a any = 123

func getFirstRune(s string):
return s[0]

getFirstRune(a) -- CompileError. Expected `string`.
```
This same setup will now fail at compile-time because `any` does not satisfy the destination's `string` type constraint.

The use of the `dynamic` type effectively defers type checking to runtime while `any` is a static type and must adhere to type constraints at compile-time.

A `dynamic` value can be used in any operation. It can be invoked as the callee, invoked as the receiver of a method call, or used with operators.

### Invoking `dynamic` values.
When a `dynamic` value is invoked, checks on whether the callee is a function is deferred to runtime.
```cy
var op = 123
print op(1, 2, 3) -- RuntimeError. Expected a function.
```

### Dynamic return value.
When the return type of a function is not specified, it defaults to the `dynamic` type.
This allows copying the return value to a typed destination without casting:
```cy
func getValue():
return 123

func add(a int, b int):
return a + b

print add(getValue(), 2) -- Prints "125"
```
The `add` function defers type checking of `getValue()` to runtime because it has the `dynamic` type.

## Compile-time dynamic typing.
Cyber introduces the concept of compile-time dynamic typing. This allows a local variable to gain additional compile-time features while using it as a dynamic value. It can prevent inevitable runtime errors and avoid unnecessary type casts.
### Recent type inference.
Although a `dynamic` variable has the most flexibility, in some situations it is advantageous to know what type it could be.

Local variables declared without a type specifier start off with the type of their initializer. In the following, `a` is implicity declared as a `float` at compile-time because numeric literals default to the `float` type.
The compiler keeps a running record of a `dynamic` variable's most **recent type** to gain additional compile-time features without sacrificing flexibility. It can prevent inevitable runtime errors and avoid unnecessary type casts.

When a `dynamic` variable is first initialized, it has a recent type inferred from its initializer. In the following, `a` has the recent type of `int` at compile-time because numeric literals default to the `int` type:
```cy
var a = 123
```

The type can change at compile-time from another assignment.
If `a` is then assigned to a string literal, `a` from that point on becomes the `string` type at compile-time.
The recent type can change at compile-time from another assignment.
If `a` is then assigned to a string literal, `a` from that point on has the recent type of `string` at compile-time:
```cy
var a = 123
foo(a) -- Valid call expression.
a = 'hello'
foo(a) -- CompileError. Expected `float` argument, got `string`.
foo(a) -- CompileError. Expected `int` argument, got `string`.

func foo(n float):
func foo(n int):
pass
```
Even though `a` is `dynamic` and is usually allowed to defer type checking to runtime, the compiler knows that doing so in this context would **always** result in a runtime error, so it provides a compile error instead. This provides a quicker feedback to fix the problem.

The type of `a` can also change in branches. However, after the branch block, `a` will have a merged type determined by the types assigned to `a` from the two branched code paths. Currently, the `any` type is used if the types from the two branches differ. At the end of the following `if` block, `a` assumes the `any` type after merging the `float` and `string` types.
The recent type of `a` can also change in branches. However, after the branch block, `a` will have a recent type after merging the types assigned to `a` from the two branched code paths. Currently, the `any` type is used if the types from the two branches differ. At the end of the following `if` block, `a` has the recent type of `any` type after merging the `int` and `string` types:
```cy
var a = 123
if a > 20:
Expand All @@ -48,71 +95,72 @@ func foo(s string):
pass
```

## Default types.
Static variables without a type specifier will always default to the `any` type. In the following, `a` is compiled with the `any` type despite being initialized to a numeric literal.
## Static typing.
Static typing can be incrementally applied which provides compile-time guarantees and prevents runtime errors.
Static typing also makes it easier to maintain and refactor your code.
> _Incomplete: Static types in general is in development. One of the goals of Cyber is to let dynamic code mix with typed code. At the moment, there are places where it works and other places where it won't. Keep that in mind when using types._

### Builtin types.
The following builtin types are available in every module: `boolean`, `float`, `int`, `string`, `List`, `Map`, `error`, `fiber`, `any`.

### Typed variables.
A typed local variable can be declared by attaching a type specifier after its name. The value assigned to the variable must satisfy the type constraint or a compile error is issued.
> _Incomplete: Only function parameter and object member type specifiers have meaning to the VM at the moment. Variable type specifiers have no meaning and will be discarded._
```cy
var a: 123
a = 'hello'
var a float = 123

var b int = 123.0 -- CompileError. Expected `int`, got `float`.
```

Function parameters without a type specifier will default to the `any` type. The return type also defaults to `any`. In the following, both `a` and `b` have the `any` type despite being only used for arithmetic.
Any operation afterwards that violates the type constraint of the variable will result in a compile error.
```cy
func add(a, b):
return a + b

print add(3, 4)
a = 'hello' -- CompileError. Expected `float`, got `string`.
```

## Static typing.
In Cyber, types can be optionally declared with variables, parameters, and return values.
The following builtin types are available in every namespace: `bool`, `float`, `int`, `string`, `List`, `Map`, `error`, `fiber`, `any`.
Static variables are declared in a similar way except `:` is used instead of `=`:
```cy
var global Map: {}
```

A `type object` declaration creates a new object type.
Type specifiers must be resolved at compile-time.
```cy
type Student object: -- Creates a new type named `Student`
name string
age int
gpa float
var foo Foo = none -- CompileError. Type `Foo` is not declared.
```

When a type specifier follows a variable name, it declares the variable with the type. Any operation afterwards that violates the type constraint will result in a compile error.
> _Incomplete: Only function parameter and object member type specifiers have meaning to the VM at the moment. Variable type specifiers have no meaning and will be discarded._
### `auto` declarations.
The `auto` declaration infers the type of the assigned value and initializes the variable with the same type.
> _Planned Feature_
```cy
a float = 123
a = 'hello' -- CompileError. Type mismatch.
-- Initialized as an `int` variable.
auto a = 123
```

Parameter and return type specifiers in a function signature follows the same syntax.
`auto` declarations are strictly for static typing. If the assigned value's type is `dynamic`, the variable's type becomes `any`.
```cy
func mul(a float, b float) float:
return a * b
func getValue():
return ['a', 'list']

print mul(3, 4)
print mul(3, '4') -- CompileError. Function signature mismatch.
-- Initialized as an `any` variable.
auto a = getValue()
```

Type specifiers must be resolved at compile-time.
### Object types.
A `type object` declaration creates a new object type. Member types are declared with a type specifier after their name.
```cy
type Foo object:
a float
b string
c Bar -- CompileError. Bar is not declared.
type Student object: -- Creates a new type named `Student`
name string
age int
gpa float
```

Circular type references are allowed.
```cy
type Node object:
val any
val any
next Node -- Valid type specifier.
```

## Union types.
> _Planned Feature_
## Traits.
> _Planned Feature_
## Type aliases.
### Type aliases.
A type alias is declared from a single line `type` statement. This creates a new type symbol for an existing data type.
```cy
import util './util.cy'
Expand All @@ -122,29 +170,68 @@ type Vec3 util.Vec3
var v = Vec3{ x: 3, y: 4, z: 5 }
```

## Type casting.
The `as` keyword can be used to cast a value to a specific type. Casting lets the compiler know what the expected type is and does not perform any conversions.
If the compiler knows the cast will always fail at runtime, a compile error is returned instead.
If the cast fails at runtime, a panic is returned.
### Functions.
Function parameter and return type specifiers follows a similiar syntax.
```cy
print('123' as float) -- CompileError. Can not cast `string` to `float`.
func mul(a float, b float) float:
return a * b

erased any = 123
add(1, erased as float) -- Success.
print mul(3, 4)
print mul(3, '4') -- CompileError. Function signature mismatch.
```

print(erased as string) -- Panic. Can not cast `float` to `string`.
### Traits.
> _Planned Feature_
func add(a float, b float):
### Union types.
> _Planned Feature_
### `any` type.
A variable with the `any` type can hold any value, but copying it to narrowed type destination will result in a compile error:
```cy
func square(i int):
return i * i

var a any = 123
a = ['a', 'list'] -- Valid assignment to a value with a different type.
a = 10

print square(a) -- CompileError. Expected `int`, got `any`.
```
`a` must be explicitly casted to satisfy the type constraint:
```cy
print square(a as int) -- Prints "100".
```

### Invoking `any` values.
Since `any` is a static type, invoking an `any` value must be explicitly casted to the appropriate function type.
> _Planned Feature: Casting to a function type is not currently supported._
```cy
func add(a int, b int) int:
return a + b

var op any = add
print op(1, 2) -- CompileError. Expected `func (int, int) any`

auto opFunc = op as (func (int, int) int)
print opFunc(1, 2) -- Prints "3".
```

### Type casting.
The `as` keyword can be used to cast a value to a specific type. Casting lets the compiler know what the expected type is and does not perform any conversions.

If the compiler knows the cast will always fail at runtime, a compile error is returned instead.
```cy
print('123' as int) -- CompileError. Can not cast `string` to `int`.
```

## Runtime type checking.
Since Cyber allows invoking `any` function values, the callee's function signature is not always known at compile-time. To ensure type safety in this situation, type checking is done at runtime and with no additional overhead compared to calling an untyped function.
If the cast fails at runtime, a panic is returned.
```cy
op any = add
print op(1, 2) -- '3'
print op(1, '2') -- Panic. Function signature mismatch.
var erased any = 123
add(1, erased as int) -- Success.
print(erased as string) -- Panic. Can not cast `int` to `string`.

func add(a float, b float) float:
func add(a int, b int):
return a + b
```
```
2 changes: 1 addition & 1 deletion docs/hugo/layouts/partials/docs/inject/body.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
'func', 'import', 'for', 'coinit', 'coresume', 'coyield',
'return', 'if', 'else', 'as', 'each', 'while', 'var', 'object',
'break', 'continue', 'match', 'pass', 'or', 'and', 'not', 'is', 'some', 'error',
'true', 'false', 'none', 'throw', 'try', 'catch', 'recover', 'enum', 'type', 'case'
'true', 'false', 'none', 'throw', 'try', 'catch', 'recover', 'enum', 'type', 'case', 'auto'
],
type: [
'float', 'string', 'bool', 'any', 'int', 'List', 'Map', 'rawstring', 'symbol', 'pointer'
Expand Down
2 changes: 2 additions & 0 deletions docs/hugo/static/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ html, body {
.markdown h3 {
color: var(--accent-light);
/* font-weight: bold; */
font-size: 1.2em;
margin-top: 2.2em;
}

.markdown table tr:nth-child(2n) {
Expand Down
2 changes: 1 addition & 1 deletion exts/sublime/cyber.sublime-syntax
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ contexts:
- match: '\b(or|and|not|is)\b'
scope: keyword.operator.cyber

- match: '\b(var|static|capture|as)\b'
- match: '\b(var|as|auto)\b'
scope: keyword.variable.cyber

- match: '\b(func|return)\b'
Expand Down
Loading

0 comments on commit dc40940

Please sign in to comment.