Giter Club home page Giter Club logo

abra-lang's People

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

abra-lang's Issues

If/Else expressions/statements

If in a statement context, the two branches don't have to be type-equivalent:

if a < 4 {
  true
} else {
  "34"
}

If used in an expression syntax, the branches must be equal in type:

val x = if a == 3 {
  14
} else if a > 3 {
  24
} else {
  34
}

If used as an expression and no else-block is provided, the resulting type will be an Option of that type (though that has yet to be fully defined...

val i: Int? = if a == 4 {
  14
}

Disassembler

This is more of a nice-to-have, but it'd be cool to have a disassembled view of the compiled module, for easier debugging purposes.

But for code like this:

if 1 < 2 {
  val a = 3
  a + 3
}
println("Done")

it'd be cool to have something like

  IConst1
  IConst2
  LT
  JumpIfF 6  ; some-random-label
  IConst3
  LLoad0  ; a
  IConst3
  IAdd
  Pop
  Pop
some-random-label:
  Constant 0  ; "Done"
  Constant 1  ; "println"
  Invoke 1
  Return

This could optionally tie in #32 and include line numbers per instruction too?

1 | IConst1
  | IConst2
  | LT
  | JumpIfF 6  ; some-random-label
2 | IConst3
3 | LLoad0  ; a
  | IConst3
  | IAdd
4 | Pop
  | Pop
some-random-label:
5 | Constant 0  ; "Done"
  | Constant 1  ; "println"
  | Invoke 1
~ | Return

Though that could maybe be another improvement later down the line...

Comments

There should be some mechanism to comment-out code, both line-by-line and multi-line.

Reassign builtins / reifying builtins

Since builtin functions should just be normal functions, I should be able to do

val myPrintln = println
myPrintln("hello")

But, right now this fails because println doesn't actually resolve to a value at runtime - instead it's handled as a special case in invocation only.

Closures

Functions are first-class citizens, and should be assignable to bindings inline. These functions, which can be thought of as lambdas, can have bodies which "close over" variables.

Syntax

func getCounter() {
  var count = 0
  val inc = () => count = count + 1
  val printCount = () => println("Count: " + count)

  { 
    inc: inc, 
    printCount: printCount
  }
}

val counter = getCounter()
counter.printCount()  // Prints "Count: 0"
counter.inc()
counter.printCount() // Prints "Count: 1"

Remove parens from if-conditions

Just a stylistic change, and one which will be in keeping with the syntax used to define loops in #45.

if (someCond) {
  // Some logic
} else if (someOtherCond) {
  // Some more logic
} else {
  // Default logic
}

becomes

if someCond {
  // Some logic
} else if someOtherCond {
  // Some more logic
} else {
  // Default logic
}

which I think is a little cleaner

Self-referencing types

I should be able to say

type Node {
  prev: Node? = None
  next: Node? = None
  value: String
}

like for a linked list.

Object literals

Object literals (also called map literals) represent a map from keys to values.

val me = { firstName: "Ken", lastName: "Gorab" }

The type of a map is said to be "homogeneous" if all of the value's types are the same; the type of the binding me above is homogeneous. Fields on maps can be accessed via the indexing operator:

val n = me["firstName"]

However, the type of n is an optional string String?, since we cannot be certain that the field firstName exists on the map at runtime (ie. what if instead of the string literal "firstName", we had indexed into the map with a variable?).

Non-homogeneous maps cannot be indexed into, since it's impossible to know at compile-time the type of the value:

val myMap = { a: 1, b: true } // <-- Non-homogeneous map type
myMap["b"] // <-- Typechecker error here

Maps can be "lifted" to struct types (related to #28):

type Person {
  firstName: String,
  age: Int
}

val map = { firstName: "Ken", age: 27 } // <-- has a (non-homogeneous) map type
val obj: Person = { firstName: "Ken", age: 27 } // <-- has a type of Person

map["firstName"] // Typechecker error; non-homogeneous maps cannot be indexed into
obj["firstName"] // Typechecker error; type instances cannot be indexed into
obj.firstName // Valid, has type String
obj.lastName // Typechecker error; type Person has no field 'lastName'

Nil-safe accessors

I'd like to be able to have a nil-safe accessor operator (?.) to access fields on Optional variables. If the variable is None, the entire rest of the expression is skipped and the result is None; if the value is not None, the next expression in the chain is evaluated. For example:

type Person {
  name: String
}

val people = [
  Person(name: "Ken")
]

people[0]?.name?.toLower() // "ken"

people[0]?.name.toLower()
               ^ Error, no field 'toLower' on String?

There should also be nil-safe accessors for indexing:

val arr = [[0], [1]]
arr[0]?.[1]

which should behave the same way; a None value would be chained along if ever present.

Locals should be stored on stack

This block of code does not work correctly:

val greeting = "Hello"

func greet(recipient: String) = greeting + ", " + recipient

val languageName = "Abra"
greet(languageName) + " " + greet(languageName)

It prints Hello, Abra Hello, <func greet>.

This is because the vars vector gets quickly out of sync with the current scope (slot 1 was initially the function greet, but then when the function runs it assigns slot 1 to be the value "Abra" (its param). This causes the other vars in the vector to be shifted, so languageName no longer points to the string "Abra" but instead the function greet, which had initially been in slot 2).

There are 2 approaches which could work here:

  1. There could be some (complex) notion of scope hierarchy in the form of call frames
  2. I could store all locals on the stack and ensure that the stack indices always coincide with the indices of locals stored in some locals vector.

I think I like approach 2 because it involves the fewest changes, and I think is just a better design.

There are a few things that need to happen in order to pull this off:

  • The top of the stack must remain pure at all times; we need to implement a POP opcode and emit it after every expression-statement (top-level expressions)
  • We should not emit a POP opcode after expression-statements which are the last in a block, as that represents a return value
    • An exception here is in an if-statement (vs an if-expression)
  • Then, the locals vec can be implemented

Respect newlines in expression statements

The following code should work:

println("hello")
[1, 2, 3]

but it produces this error

Unexpected token ',' (2:3)
  |  [1, 2, 3]

This is because it's interpreting this as if it were an array indexing operator, like

println("hello")[1, 2, 3]

and failing due to an unexpected comma. This can be resolved by treating a newline as a terminator when parsing.

Implement `None` as a constant

Much like in python, there should be a None constant, such that None is an Optional of any type, and None ?: <anything> == <anything>

Other compilation targets?

It could be fun to attempt to write additional compilation targets (aside from AbraVM). Options include javascript (browser + node), LLVM (using Inkwell maybe?), or others.

I would certainly not consider this to be very important, but it could be fun and interesting to experiment!

Array indexing

Add support (fully vertical) for accessing members of an array:

val nums = [1, 2, 3]
val first = nums[0]  // 1
val last = nums[-1] // 3

val str = "hello!"
str[0] // "h"
str[1:] // "ello!"
str[:2] // "he"
str[1:3] // "el"
str[-1:] // "!"
str[-2:5] // "o"

Type functions

Syntax

This evolves from the work done in #28

Struct types can be declared as follows:

type Person {
  name: String,
  age: Int = 0
}

This defines a type Person with fields name of type String and age of type Int; the age field is optional and has a default value of 0.

This same struct type can be represented as follows, with no change at all to the underlying representation:

type Person {
  fields {
    name: String,
    age: Int = 0
  }
}

However, the second form allows for functions to be bound to a type. These functions can either be "methods", functions which operate on an instance of that type and receive it as the first (implicit) parameter, or "static functions", functions which are bound lexically to the type but do not operate on an instance of that type.

type Person {
  fields {
    name: String,
    age: Int = 0
  }

  func toString(self) = self.name + ": " + self.age

  func incrementAge(p: Person) = Person({ name: p.name, age: p.age + 1 })
}

Note that #73 is likely a prerequisite for accomplishing methods.

Semantics

Calling Methods

Let's say we have an instance p of the Person type declared above:

val p = Person({ name: "Meg", age: 26 })

Calling a method on the instance p, for example, the toString method, would be done as so:

println(p.toString())

However, since functions are first-class citizens, we can do this:

val pToString = p.toString
println(pToString())

which should result in the same value. If we somehow had a mechanism for mutating p (we currently don't), it should still behave as we expect:

val pToString = p.toString
p.age = p.age + 1
println(pToString())  // Should print with updated age

Calling Static Functions

Let's say we have an instance p of the Person type declared above:

val p = Person({ name: "Meg", age: 26 })

We can invoke the static function incrementAge on the Person type (which just returns a new Person instance whose name is the same and whose age is +1):

val otherPerson = Person.incrementAge(p)

We use the dot to denote "static access", accessing the incrementAge function off of the Person type; in this sense, the incrementAge function is a field of the Person type, similarly to how the toString method is a field of a Person instance.

Option-types

There should be a way to declare a type as an Optional/nullable type, meaning that the variable could either be an instance of that given type or it could be nil.

val array = [1, 2, 3] // Int[]
val elem = array[0] // Int?
val sum1 = elem + 1 // <-- err, cannot add Int and Int?
val sum2: Int = (elem ?: 0) + 1 <-- will use 0 if elem is nil

REPL

There should be a way of interacting with the language in an adhoc environment, via a command line REPL

Accessor assignments

I should be able to assign to other targets than just an Identifier. For example, I should be able to say:

type Person {
  name: String
}

val me = Person(name: "ken")
me.name = "Ken"

This would also apply for any arbitrary amount of accessors, in addition to array and map indexing:

val people = [
  Person(name: "ken"),
  Person(name: "Meg"),
]
people[0].name = "Ken"

val namedPeople = {
  ken: Person(name: "ken"),
  meg: Person(name: "Meg"),
}
namedPeople["ken"].name = "Ken"

Or even:

people[0] = Person(name: "Ken")

As of right now, it's not possible to do a nested index assignment, due to the Optional nature of the returned values:

val arr = [[0]]
arr[0][0] = 1
      ^
Type Int[]? is not indexable

This could potentially be addressed by #124 but I doubt it'd ever make sense to have the LHS of an assignment be an optional field access. See this typescript playground example for a potential implementation consideration.

Generic types

I should be able to define generic types, types that accept a type-variable which can be used within the fields/methods.

type List<T> {
  items: T[]

  func head(self) = self.items[0]
  func add(self, item: T) = self.items.push(item)
}

val stringList: List<String> = List(items: ["a", "b"])
stringList.add("c")
stringList.add(123)
//        ^ Error here
stringList.head() // <-- Option("a")

This should also enable a rewrite of the way that NativeArray currently works (and consequently maybe even NativeString too?).

Improvements/maintenance/refactoring

Improvements (like pulling in the enum_primitive crate, or implementing iconst0-iconst4 or something)

Refactorings (any that I happen to see, maybe implementing a macro for known-enum-variant-assignments?)

Invocation of more than just identifiers

The following code should work:

func outer() {
  func inner() {
    println("hello")
  }

  inner
}

outer()()

but right now it doesn't, since the visit_invocation function in Compiler only supports compilation of invocation of identifier nodes.

Remove global scope

It doesn't really add anything, and just adds complexity and unnecessary opcodes

Declaration order should not matter

Right now, functions and types referenced earlier in the code than their declaration will not work. For example:

func abc() = def()
func def() = 3
abc()

will raise the error

Unknown identifier 'def' (8:14)
  |  func abc() = def()
                  ^
No binding with that name is visible in current scope

There's a similar situation with types:

type Person {
  name: Name,
  age: Int
}

type Name {
  firstName: String,
  lastName: String,
}

Person

This causes the error:

Unknown type 'Name' (2:9)
  |    name: Name,
             ^
No type with that name is visible in current scope

Rewrite &&, ||, and ?: in terms of if-expression

Right now these boolean binary operators do not short-circuit. They should be written using jumps, which can be achieved by rewriting the AST node in terms of an IfExpression.

The default-parameter function compilation code can also be rewritten using a coalesce operator, once this is done.

Property getters

Add support (fully vertical) for property getters (ie. dot-access):

val nums = [1, 2, 3]
nums.length // 3

// Or, on the literal itself
"hello".length // 5

// Accessing methods too
[1, 2, 3].tail // [Function Array#tail]

Modulus operator

There should be an operator that performs the modulo operation:

5 % 4  // 1
10 % 3  // 1
3 % 10  // 3

Bug: Loops + fns w/ args

This code seems to fail for some reason:

func innerLoop() {
  for a2, i2 in range(0, 5) {
    println("Inner " + a2)
  }
}

func runLoop() {
  for a1, i1 in range(0, 5) {
     println("Outer " + a1)

     innerLoop()
  }
}

runLoop()

It panics at stack_insert_at, with message ""No stack slot available at index 0". This is interesting... I should probably add some more helpful message to suggest where in the code (or bytecode) the error occurred.

Builtin functions

There are functions that should just be built into the language.

println(a: Any): Unit)
range(start: Int, end: Int, increment = 1): Int[]
...

Setting this up should mean that there's an easy way to add new builtin functions to the language, as the needs for them present themselves.

Function declaration

There must be a way of defining static, standalone, top-level functions. They have a name, arguments, a body, and an optional return type annotation. The body can either be a single expression, or a block; if a block, the return value will be the last expression in the block.

func myAdd(a: Int, b: Int) = a + b

func myAdd2(a: Int, b: Int): Int = a + b

func myAdd3(a: Int, b: Int) {
  a + b
}

func myAdd4(a: Int, b: Int) {
  val c = a + b
  c
}

Functions should also have optional parameters with default values. Default-valued parameters must come at the end of the argument list. Arguments with default values can have optional type annotations, but they're not required.

func myAdd(a: Int, b: Int, c: Int = 0) = a + b + c

func myAdd2(a: Int, b: Int, c = 0) {
  val sum = a + b + c
  sum
}

If-checks for Optional values

Right now, if-conditions can only be booleans. But I think it makes a little more sense to have them support Optionals as well; a None value will be false, and any other value will be true.

val arr = [[0]]
if (arr[0]) {
  ...
}

This is more of a stretch goal, but also within the if-block, we should be able to infer that arr[0] is not None, so we should be able to do

val arr = [[0]]
if (arr[0]) {
  println(arr[0][0])
}

Right now, this would fail because arr[0] is of type Int[]? which is not indexable.

Clean up named arguments + instantiation

Right now, the situation with named function arguments is a little strange. You can call a function with named args, but they still have to be in the same order - the arg names in this case were just for readability. This means that a function call like this is possible:

abc(a: 1, b: 2, 3)

which looks a little ugly. One of the value props for named arguments should be that the order matters less.

This also has implications for how I want struct object instantiation to work. Right now the syntax is a confusing combination of what appears to be a function call and a hashmap:

type Person {
  name: String
}

val me = Person({ name: 'Ken' })

This looks strange and a bit non-standard, and adds a lot of special cases to the typechecking code. It'd be better if instantiation could be written as an ordinary function call:

val me = Person(name: 'Ken')

Default values in the struct will behave just like ordinary default parameters would, and since the order of named arguments doesn't matter, encapsulates all the behaviors of that weird hash literal syntax.

There could (and maybe even should) be an error that gets raised, enforcing that all "constructor" functions require named arguments. This would help prevent any issues if fields get shifted around in struct definitions over time.


Tech note/hint: when typechecking an invocation that uses at least 1 named argument, re-order the arguments so that the resulting TypedInvocationNode that gets generated has its args in the correct order. This might actually help with compilation too, since that args vector could be changed from a Vec<TypedAstNode> to a Vec<Option<TypedAstNode>>, to help account for named arguments that were omitted, but optional, as in:

func abc(a = 1, b = 2, c = 3) = a + b + c

abc(b: 12) // <-- this should result in Nil, 12, Nil being the arguments to this function

Bug: Array precedence issue

I discovered that the following code does not properly work:

func abc(a: Int = 2, b = 3, c = 5) = a * b * c
[abc(), abc(7), abc(7, 11), abc(7, 11, 13)]
      ^ unexpected comma

This is likely due to the fact that it's trying to parse this as

func abc(a: Int = 2, b = 3, c = 5) = a * b * c[abc(),

and failing. Possibly the precedences of [ need to be adjusted?

Type declarations (structs)

Spec

I should be able to declare struct types, which are named and have any number of typed fields. Type names must begin with a capital letter.

type Person {
  name: String,
  age: Int,
  favoriteColor: Color
}

// To be handled in another issue
type Color enum {
  Blue,
  Purple,
  Green,
  Red,
  Other
}

To create instances of a type, a "constructor function" will be made accessible, which has an argument for each field.

type Person {
  name: String,
  age: Int
}

val ken = Person(name: "Ken", age: 27)

Questions

How to handle Optional fields in a constructor?

If there's a type declaration like

type Person {
  name: String,
  favoriteColor: Color?
}

what should the constructor function look like? Should that field be omittable in the constructor function, the absence of which should denote an empty Optional? What happens if the type declaration is

type Person {
  name: String,
  favoriteColor: Color?,
  age: Int
}

with an Optional in between two non-Optional fields? Should Optional fields always be bubbled down to the end of the declaration? Should constructor functions work a little bit differently and not require the order of the named arguments to match up (I dislike this)? Should there be an explicit None type, and it should have to be passed for empty optionals (I like this one the best)?

Should we support default field values?

This one might wait until #29 is done, but I'm imagining a situation like

type Person {
  name: String = "Ken",
  age: Int
}

where the name field is pre-filled with a default value. Should there be 2 constructor functions generated?

func Person(name: String, age: Int): Person
func Person(age: Int): Person

Should we not have a "constructor function" and instead use something else?

type Person {
  name: String,
  age: Int
}

val ken: Person = {
  name: "Ken",
  age: 27
}

This option is a little better, because it solves the problems above. Something to think about when the time comes.

Should we have some mechanism for methods?

type Person {
  fields {
    name: String,
    age: Int
  }

  func toString() = self.name + ": " + self.age

  static func incrAge(p: Person) = { ...p, age: p.age + 1 }
}

In this model, the previous example would be a shorthand for when we don't want to attach specific instance methods to a struct; all we'd want in that case is a simple data class or container. In other words,

// This definition is a shorthand for ...
type Person {
  name: String,
  age: Int
}

// ... this more verbose representation
type Person {
  fields {
    name: String,
    age: Int
  }
}

Loops

I should be able to loop, either over a range of data or until a certain condition is no longer true.

var a = 0
while a < 3 {
  a = a + 1
}
a  // 2

var greeting = "He"
for ch in ["l", "l", "o"] {
  greeting = greeting + ch
}

// This would also produce the same effect, since strings should be considered iterable:
for ch in "llo" { .. }

// Also, to obtain the index there is an optional second parameter
for ch, idx in ["l", "l", "o"] {
  println("Index: " + idx)
  greeting = greeting + ch
}

// The `range` builtin can also be used to iterate over ranges
var sum = 0
for i in range(0, 10) {
  sum = sum + i
}

Loops can also be infinite-loops, meaning that they will continue to loop until a break keyword is met

var a = 0
while a < 3 {
  a = a - 1  // a will always be less than 3, so infinite loop occurs
  if a == -100 {
    break
  }
}

Destructuring

Overview / Motivation

For convenience, the language should provide destructuring syntax for bindings. For example,

val coord = (1, 2, 3)
val x = coord[0]
val y = coord[1]
val z = coord[2]

// vs

val (x, y, z) = coord

Tuples

For tuples (as in the above example), the number of variables in the destructuring must match the arity of the tuple. This is because, since the size of a tuple is known at compile-time, we want to be able to guarantee that each of the destructured values is a non-optional type. For example, the following should be considered invalid:

val coord = (1, 2, 3)
val (x, y) = coord // <-- cannot destructure a 3-tuple into 2 values
val (x, y, z, w) = coord // <-- cannot destructure a 3-tuple into 4 values

(Arguably, the val (x, y) = (1, 2, 3) case should be fine, but that can come later).

Arrays

A similar structure should work for arrays, but unlike with tuples the size doesn't matter:

val arr = [1, 2, 3, 4, 5]
val [first, second] = arr

Since we can't ensure at compile-time that there are at least as many destructuring elements as there are elements in the array, the types here will be Int?.

To capture multiple elements in a destructuring, we could use a splat operator ...:

val arr = [1, 2, 3, 4, 5]
val [head, ...rest] = arr
head // type is Int?, value is 1
rest // type is Int?, value is [2, 3, 4, 5]

The splat operator is greedy; it tries to consume as many elements as it can. If we were to place another binding after a splat, the splatted binding would include everything up until that value:

val arr = [1, 2, 3, 4, 5]
val [head, ...middle, penultimate, tail] = arr
head // type is Int?, value is 1
middle // type is Int[], value is [2, 3]
penultimate // type is Int?, value is 4
tail // type is Int?, value is 5

Strings can also be destructured in the same way as arrays can:

val str = "hello world!"
val [head, ...middle, tail] = str
head // type is String, value is "h"
middle // type is String, value is "ello world"
tail // type is String, value is "!"

Nested

Destructured patterns can also be nested, eg

val coords = [(1, 2), (2, 3), (3, 4)]
val [(x1, y1), ...middle, (xn, yn)] = coords
x1 // type is Int?, value is 1
y1 // type is Int?, value is 2
middle // type is (Int, Int)[], value is [(2, 3)]
xn // type is Int?, value is 3
yn // type is Int?, value is 4

Usages

val/var declaration

This is demonstrated above

for-loop iterator

When iterating over a destructurable value, the iteratee could be a destructuring pattern:

val coords = [(1, 2), (2, 3), (3, 4)]
for (x, y), idx in coords {
  // ...
}

val numLists = [[1, 2, 3], [4, 5, 6, 7], [8, 9]]
for [head, ..._, tail], idx in numLists {
  // ..
}

function parameters

Function declaration parameters could be written using destructuring patterns:

func sum([head, ...middle, tail]: Int[]): Int {}
func manhattanDistance((x1, y1): (Int, Int), (x2, y2): (Int, Int)): Int { ... }

The same applies for lambdas:

val coords = [(1, 2), (2, 3), (3, 4)]
coords.map(((x, y)) => (x + 1, y + 1)) // <-- note the necessary extra parens, denoting `(x, y)` as a destructured pattern

convenience bindings

Convenience bindings, like in while or if conditions, should also be destructurable:

val coords = [(1, 2), (2, 3), (3, 4)]
if coords[0] |(x, y)| { ... }

var idx = 0
while coords[idx] |(x, y)| { ... }

match case pattern matchings (possibly punt on this, it seems complicated)

There are a few additions that should be made to match-expressions. The first would be to support destructuring in type-matching case patterns, such as:

type Line { start: (Int, Int), end: (Int, Int) }
val l: Line | Int = Line(start: (0, 0), end: (1, 1))
match l {
  Int i => println(i)
  Line((x1, y1), (x2, y2)) l => println("line")
}

The other modification (which may need to happen, depending on whether or not #231 is done yet), would be to support destructuring value cases:

val coords = [(1, 2), (2, 3), (3, 4)]
match coords[0] {
  None => println("none!")
  (1, 3) => println("yay, saw point (1, 3)!")
  (x, y) => println("saw point (" + x + ", " + y + ")")
}

(Note that this is still up in the air and subject to change)

Remove lazy_static macro

I removed most of the use cases of it, and really the only remaining use case doesn't need to exist. I think I just got excited to use this crate when I first discovered it

Redo default-value parameter functions

The current implementation is unnecessarily janky, and makes it impossible to use with closures. For example, the following code will not work:

func outer() {
  func inner(param = 1) {
    param + 1
  }

  inner
}

val fn = outer()
fn()

Inner functions with default argument values

The following code should work:

func getCounter() {
  var count = 0

  func tick(incr = 1) {
    count = count + incr

    println("Count: " + count)
  }

  tick
}

val tick = getCounter()
tick()

It currently does not because of how default-argument functions are compiled. The last line will attempt to call a function named tick_$0, but since the getCounter function returned a reference to the Value::Closure that contained the code for tick only, tick_$0 does not exist anywhere. This is an interesting problem, and will require some thought.


It's also worth noting that this also seems to produce odd results:

func getCounter(incr = 1) {
  var count = 0

  func tick() {
    count = count + incr

    println("Count: " + count)
  }

  tick
}

val tick = getCounter()
tick()
tick()
tick()
tick()
tick()
tick()
tick()

which outputs:

Count: 2
Count: 4
Count: 8
Count: 16
Count: 32
Count: 64
Count: 128

๐Ÿค”

Line counting is super messed up

Consider this code:

val a = 1
val b = 2
val c = 3
func abc(b: Int) {
  val a1 = a
  val c = b + a1
  c
}

The "main" chunk looks like this:

lines: vec![2, 2, 2, 3, 1],
code: vec![
    Opcode::IConst1 as u8,
    Opcode::Store0 as u8,
    Opcode::IConst2 as u8,
    Opcode::Store1 as u8,
    Opcode::IConst3 as u8,
    Opcode::Store2 as u8,
    Opcode::Constant as u8, 0,
    Opcode::Store3 as u8,
    Opcode::Return as u8
],

Basically, it correctly assigns 2 instructions to line idx 0/1/2 (the val assignment), and 3 for line idx 3 (the function declaration). But it totally skips over the lines which make up the function declaration, and treats the last_line as the line immediately following the function declaration because that's the last one it knew about.

I believe this only happens when a function block is the last thing in an input? Not 100% sure though

Disallow return types of Unit for if-expressions

Since an if-expression resolves to a value (the last value in the block), its block cannot end with a Unit-typed expression/statement. For example:

val a = if (true) {
  val b = 1
} else {
  print("hello")
}

What should the type of a be? This should fail typechecking, especially since there's no real course of action during compilation

Default function argument values

Related to #23, this is the ability for functions to have default parameter values

func myAdd(a: Int, b: Int, c: Int = 0) = a + b + c

func myAdd2(a: Int, b: Int, c = 0) {
  val sum = a + b + c
  sum
}

Implementation notes:

Let's say we have this function:

func abc(a: Int, b = 1, c = "hello"): String = a + b + c

Behind the scenes, the following code will be generated:

func abc_$1(a: Int) = abc(a, 1, "hello")
func abc_$2(a: Int, b: Int) = abc(a, b, "hello")

(the _$2 means "the version of the abc function, with arity 2). Those 2 parameters would be forwarded to the underlying function, with the 3rd param receiving its default value of "hello".

This means that the logic for invocation also needs to be aware of these "pesudo-functions"

Forward-references

I should be able to forward-reference functions that haven't been defined yet. For example:

func abc(a: Int): Int = abc1(123)
func abc1(a: Int): Int = a

fails with the following error:

Unknown identifier 'abc1' (1:25)
  |  func abc(a: Int): Int = abc1(123)
                             ^
No binding with that name is visible in current scope

This also has implications in types:

type Person {
  func yellHello(self) = self.hello() + "!"

  func hello(self) = "hello"
}

results in an error, since self.hello is undefined when typechecking/compiling self.yellHello. The same holds for static functions defined on types.

Also for types, the type itself should be able to be referenced within a function body:

type Person {
  name: String

  func kensName() = Person(name: "Ken").getName()

  func getName(self) = self.name
}

The static function kensName fails to compile because Person is not yet accessible at typecheck-time.

Require methods to have explicit return type annotations

In order to enable calling methods within other methods, we need to require that methods have explicit return type annotations. That way, we can make 2 passes over a type declaration; the first will just gather the types of the fields and methods, and then the second will actually typecheck each method. This is suboptimal, since it requires return type annotations even when it should be obvious (ie. func sayHi(self) = "hello" vs func sayHi(self): String = "hello") but it's a necessary (hopefully temporary) measure, that can be removed if/when type inference ever becomes a thing.

Instances & methods

Right now, instances are just glorified hashmaps under the hood, and field accesses require adding string constants to the constant pool. Instead, an instance could be represented as a Vec of instance values.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.