kengorab / abra-lang Goto Github PK
View Code? Open in Web Editor NEW๐งโโ๏ธA small programming language with static typing and native compilation, written in Rust (selfhosting WIP)
Home Page: https://abra.kengorab.dev
๐งโโ๏ธA small programming language with static typing and native compilation, written in Rust (selfhosting WIP)
Home Page: https://abra.kengorab.dev
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
}
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...
There should be some mechanism to comment-out code, both line-by-line and multi-line.
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.
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.
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"
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
I should be able to say
type Node {
prev: Node? = None
next: Node? = None
value: String
}
like for a linked list.
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'
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.
I should be able to type
val nums: Int[] = []
and it should successfully typecheck
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:
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:
POP
opcode and emit it after every expression-statement (top-level expressions)POP
opcode after expression-statements which are the last in a block, as that represents a return value
locals
vec can be implementedIt should be possible to write a basic fibonacci function
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.
Much like in python, there should be a None
constant, such that None
is an Optional
of any type, and None ?: <anything> == <anything>
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!
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"
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 })
}
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
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.
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
There should be a way of interacting with the language in an adhoc environment, via a command line REPL
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.
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 (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?)
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.
It doesn't really add anything, and just adds complexity and unnecessary opcodes
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
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.
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]
There should be an operator that performs the modulo operation:
5 % 4 // 1
10 % 3 // 1
3 % 10 // 3
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.
There are quite a few error that can be raised which have unimplemented!()
messages, mostly TypecheckerErrors
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.
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
}
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.
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
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?
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)
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)?
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
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.
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
}
}
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
}
}
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
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).
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 "!"
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
val
/var
declarationThis is demonstrated above
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 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, 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)| { ... }
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)
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
By having tokens keep track of their start and end positions, error messages can be displayed in editors (and possibly be even more helpful in the command line)
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()
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
๐ค
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
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
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
}
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"
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.
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.
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.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.