Giter VIP home page Giter VIP logo

jakt's Introduction

The Jakt programming language

Jakt is a memory-safe systems programming language.

It currently transpiles to C++.

NOTE: The language is under heavy development.

NOTE If you're cloning to a Windows PC (not WSL), make sure that your Git client keeps the line endings as \n. You can set this as a global config via git config --global core.autocrlf false.

Usage

The transpilation to C++ requires clang. Make sure you have that installed.

jakt file.jakt
./build/file

Building

See here.

Goals

  1. Memory safety
  2. Code readability
  3. Developer productivity
  4. Executable performance
  5. Fun!

Memory safety

The following strategies are employed to achieve memory safety:

  • Automatic reference counting
  • Strong typing
  • Bounds checking
  • No raw pointers in safe mode

In Jakt, there are three pointer types:

  • T (Strong pointer to reference-counted class T.)
  • weak T (Weak pointer to reference-counted class T. Becomes empty on pointee destruction.)
  • raw T (Raw pointer to arbitrary type T. Only usable in unsafe blocks.)

Null pointers are not possible in safe mode, but pointers can be wrapped in Optional, i.e Optional<T> or T? for short.

Math safety

  • Integer overflow (both signed and unsigned) is a runtime error.
  • Numeric values are not automatically coerced to int. All casts must be explicit.

For cases where silent integer overflow is desired, there are explicit functions that provide this functionality.

Code readability

Far more time is spent reading code than writing it. For that reason, Jakt puts a high emphasis on readability.

Some of the features that encourage more readable programs:

  • Immutable by default.
  • Argument labels in call expressions (object.function(width: 10, height: 5))
  • Inferred enum scope. (You can say Foo instead of MyEnum::Foo).
  • Pattern matching with match.
  • Optional chaining (foo?.bar?.baz (fallible) and foo!.bar!.baz (infallible))
  • None coalescing for optionals (foo ?? bar yields foo if foo has a value, otherwise bar)
  • defer statements.
  • Pointers are always dereferenced with . (never ->)
  • Trailing closure parameters can be passed outside the call parentheses.
  • Error propagation with ErrorOr<T> return type and dedicated try / must keywords.

Code reuse

Jakt is flexible in how a project can be structured with a built-in module system.

import a                                // (1)
import a { use_cool_things }            // (2)
import fn()                             // (3)
import relative foo::bar                // (4)
import relative parent::foo::baz        // (5)
import relative parent(3)::foo::baz     // (6)
  1. Import a module from the same directory as the file.
  2. Import only use_cool_things() from module a.
  3. Imports can be calculated at compile time. See Comptime Imports
  4. Import a module using the relative keyword when the module is a sub path of the directory containing the file.
  5. Import a module in a parent path one directory up from the file.
  6. Syntactic sugar for importing a module three parent paths up from the file.

The Jakt Standard Library

Jakt has a Standard Library that is accessed using the jakt:: namespace:

import jakt::arguments
import jakt::libc::io { system }

The Jakt Standard Library is in its infancy, so please consider making a contribution!

Function calls

When calling a function, you must specify the name of each argument as you're passing it:

rect.set_size(width: 640, height: 480)

There are two exceptions to this:

  • If the parameter in the function declaration is declared as anon, omitting the argument label is allowed.
  • When passing a variable with the same name as the parameter.

Structures and classes

There are two main ways to declare a structure in Jakt: struct and class.

struct

Basic syntax:

struct Point {
    x: i64
    y: i64
}

Structs in Jakt have value semantics:

  • Variables that contain a struct always have a unique instance of the struct.
  • Copying a struct instance always makes a deep copy.
let a = Point(x: 10, y: 5)
let b = a
// "b" is a deep copy of "a", they do not refer to the same Point

Jakt generates a default constructor for structs. It takes all fields by name. For the Point struct above, it looks like this:

Point(x: i64, y: i64)

Struct members are public by default.

class

  • basic class support
  • private-by-default members
  • inheritance
  • class-based polymorphism (assign child instance to things requiring the parent type)
  • Super type
  • Self type

Same basic syntax as struct:

class Size {
    width: i64
    height: i64

    public fn area(this) => .width * .height
}

Classes in Jakt have reference semantics:

  • Copying a class instance (aka an "object") copies a reference to the object.
  • All objects are reference-counted by default. This ensures that objects don't get accessed after being deleted.

Class members are private by default.

Member functions

Both structs and classes can have member functions.

There are three kinds of member functions:

Static member functions don't require an object to call. They have no this parameter.

class Foo {
    fn func() => println("Hello!")
}

// Foo::func() can be called without an object.
Foo::func()

Non-mutating member functions require an object to be called, but cannot mutate the object. The first parameter is this.

class Foo {
    fn func(this) => println("Hello!")
}

// Foo::func() can only be called on an instance of Foo.
let x = Foo()
x.func()

Mutating member functions require an object to be called, and may modify the object. The first parameter is mut this.

class Foo {
    x: i64

    fn set(mut this, anon x: i64) {
        this.x = x
    }
}

// Foo::set() can only be called on a mut Foo:
mut foo = Foo(x: 3)
foo.set(9)

Shorthand for accessing member variables

To reduce repetitive this. spam in methods, the shorthand .foo expands to this.foo.

Strings

Strings are provided in the language mainly as the type String, which is a reference-counted (and heap-allocated) string type. String literals are written with double quotes, like "Hello, world!".

Overloaded string literals

String literals are of type String by default; however, they can be used to implicitly construct any type that implements the FromStringLiteral (or ThrowingFromStringLiteral) trait. In the language prelude, currently only StringView implements this trait, which can be used only to refer to strings with a static lifetime:

let foo: StringView = "foo" // This string is not allocated on the heap, and foo is only a fat pointer to the static string.

Overloaded string literals can be used by providing a type hint, whether by explicit type annotations, or by passing the literal to a function that expects a specific type:

struct NotString implements(FromStringLiteral) {
    fn from_string_literal(anon string: StringView) -> NotString => NotString()
}

fn test(x: NotString) {}

fn main() {
    let foo: NotString = "foo"
    test(x: "Some string literal")
}

Arrays

Dynamic arrays are provided via a built-in Array<T> type. They can grow and shrink at runtime.

Array is memory safe:

  • Out-of-bounds will panic the program with a runtime error.
  • Slices of an Array keep the underlying data alive via automatic reference counting.

Declaring arrays

// Function that takes an Array<i64> and returns an Array<String>
fn foo(numbers: [i64]) -> [String] {
    ...
}

Shorthand for creating arrays

// Array<i64> with 256 elements, all initialized to 0.
let values = [0; 256]

// Array<String> with 3 elements: "foo", "bar" and "baz".
let values = ["foo", "bar", "baz"]

Dictionaries

  • Creating dictionaries
  • Indexing dictionaries
  • Assigning into indexes (aka lvalue)
fn main() {
    let dict = ["a": 1, "b": 2]

    println("{}", dict["a"])
}

Declaring dictionaries

// Function that takes a Dictionary<i64, String> and returns an Dictionary<String, bool>
fn foo(numbers: [i64:String]) -> [String:bool] {
    ...
}

Shorthand for creating dictionaries

// Dictionary<String, i64> with 3 entries.
let values = ["foo": 500, "bar": 600, "baz": 700]

Sets

  • Creating sets
  • Reference semantics
fn main() {
    let set = {1, 2, 3}

    println("{}", set.contains(1))
    println("{}", set.contains(5))
}

Tuples

  • Creating tuples
  • Index tuples
  • Tuple types
fn main() {
    let x = ("a", 2, true)

    println("{}", x.1)
}

Enums and Pattern Matching

  • Enums as sum-types
  • Generic enums
  • Enums as names for values of an underlying type
  • match expressions
  • Enum scope inference in match arms
  • Yielding values from match blocks
  • Nested match patterns
  • Traits as match patterns
  • Support for interop with the ?, ?? and ! operators
enum MyOptional<T> {
    Some(T)
    None
}

fn value_or_default<T>(anon x: MyOptional<T>, default: T) -> T {
    return match x {
        Some(value) => {
            let stuff = maybe_do_stuff_with(value)
            let more_stuff = stuff.do_some_more_processing()
            yield more_stuff
        }
        None => default
    }
}

enum Foo {
    StructLikeThingy (
        field_a: i32
        field_b: i32
    )
}

fn look_at_foo(anon x: Foo) -> i32 {
    match x {
        StructLikeThingy(field_a: a, field_b) => {
            return a + field_b
        }
    }
}

enum AlertDescription: i8 {
    CloseNotify = 0
    UnexpectedMessage = 10
    BadRecordMAC = 20
    // etc
}

// Use in match:
fn do_nothing_in_particular() => match AlertDescription::CloseNotify {
    CloseNotify => { ... }
    UnexpectedMessage => { ... }
    BadRecordMAC => { ... }
}

Generics

  • Generic types
  • Constant generics (minimal support)
  • Constant generics (full support)
  • Generic type inference
  • Traits

Jakt supports both generic structures and generic functions.

fn id<T>(anon x: T) -> T {
    return x
}

fn main() {
    let y = id(3)

    println("{}", y + 1000)
}
struct Foo<T> {
    x: T
}

fn main() {
    let f = Foo(x: 100)

    println("{}", f.x)
}
struct MyArray<T, comptime U> {
    // NOTE: There is currently no way to access the value 'U', referring to 'U' is only valid as the type at the moment.
    data: [T]
}

Namespaces

  • Namespace support for functions and struct/class/enum
  • Deep namespace support
namespace Greeters {
    fn greet() {
        println("Well, hello friends")
    }
}

fn main() {
    Greeters::greet()
}

Type casts

There are two built-in casting operators in Jakt.

  • as? T: Returns an Optional<T>, empty if the source value isn't convertible to T.
  • as! T: Returns a T, aborts the program if the source value isn't convertible to T.

The as cast can do these things (note that the implementation may not agree yet):

  • Casts to the same type are infallible and pointless, so might be forbidden in the future.
  • If the source type is unknown, the cast is valid as a type assertion.
  • If both types are primitive, a safe conversion is done.
    • Integer casts will fail if the value is out of range. This means that promotion casts like i32 -> i64 are infallible.
    • Float -> Integer casts truncate the decimal point (?)
    • Integer -> Float casts resolve to the closest value to the integer representable by the floating-point type (?). If the integer value is too large, they resolve to infinity (?)
    • Any primitive -> bool will create true for any value except 0, which is false.
    • bool -> any primitive will do false -> 0 and true -> 1, even for floats.
  • If the types are two different pointer types (see above), the cast is essentially a no-op. A cast to T will increment the reference count as expected; that's the preferred way of creating a strong reference from a weak reference. A cast from and to raw T is unsafe.
  • If the types are part of the same type hierarchy (i.e. one is a child type of another):
    • A child can be cast to its parent infallibly.
    • A parent can be cast to a child, but this will check the type at runtime and fail if the object was not of the child type or one of its subtypes.
  • If the types are incompatible, a user-defined cast is attempted to be used. The details here are not decided yet.
  • If nothing works, the cast will not even compile.

Additional casts are available in the standard library. Two important ones are as_saturated and as_truncated, which cast integral values while saturating to the boundaries or truncating bits, respectively.

Traits

To make generics a bit more powerful and expressive, you can add additional information to them:

trait Hashable<Output> {
    fn hash(self) -> Output
}

class Foo implements(Hashable<i64>) {
    fn hash(self) => 42
}

Traits can be used to add constraints to generic types, but also provide default implementations based on a minimal set of requirements - for instance:

trait Fancy {
    fn do_something(this) -> void
    fn do_something_twice(this) -> void {
        .do_something()
        .do_something()
    }
}

struct Boring implements(Fancy) {
    fn do_something(this) -> void {
        println("I'm so boring")
    }

    // Note that we don't have to implement `do_something_twice` here, because it has a default implementation.
}

struct Better implements(Fancy) {
    fn do_something(this) -> void {
        println("I'm not boring")
    }

    // However, a custom implementation is still valid.
    fn do_something_twice(this) -> void {
        println("I'm not boring, but I'm doing it twice")
    }
}

Traits can have methods that reference other traits as types, which can be used to describe a hierarchy of traits:

trait ConstIterable<T> {
    fn next(this) -> T?
}

trait IntoIterator<T> {
    // Note how the return type is a reference to the ConstIterable trait (and not a concrete type)
    fn iterator(this) -> ConstIterable<T>
}

Operator Overloading and Traits

Operators are implemented as traits, and can be overloaded by implementing them on a given type:

struct Foo implements(Add<Foo, Foo>) {
    x: i32

    fn add(this, anon rhs: Foo) -> Foo {
        return Foo(x: .x + other.x)
    }
}

The relationship between operators and traits is as follows (Note that @ is used as a placeholder for any binary operator's name or sigil):

Operator Trait Method Name Derived From Method
+ Add add -
- Subtract subtract -
* Multiply multiply -
/ Divide divide -
% Modulo modulo -
< Compare less_than compare
> Compare greater_than compare
<= Compare less_than_or_equal compare
>= Compare greater_than_or_equal compare
== Equal equals -
!= Equal not_equals equals
@= @Assignment @_assign -

Other operators have not yet been converted to traits, decided on, or implemented:

Operator Description Status
& Bitwise And Not Decided
| Bitwise Or Not Decided
^ Bitwise Xor Not Decided
~ Bitwise Not Not Decided
<< Bitwise Shift Left Not Decided
>> Bitwise Shift Right Not Decided
and Logical And Not Decided
or Logical Or Not Decided
not Logical Not Not Decided
= Assignment Not Decided

Safety analysis

(Not yet implemented)

To keep things safe, there are a few kinds of analysis we'd like to do (non-exhaustive):

  • Preventing overlapping of method calls that would collide with each other. For example, creating an iterator over a container, and while that's live, resizing the container
  • Using and manipulating raw pointers
  • Calling out to C code that may have side effects

Error handling

Functions that can fail with an error instead of returning normally are marked with the throws keyword:

fn task_that_might_fail() throws -> usize {
    if problem {
        throw Error::from_errno(EPROBLEM)
    }
    ...
    return result
}

fn task_that_cannot_fail() -> usize {
    ...
    return result
}

Unlike languages like C++ and Java, errors don't unwind the call stack automatically. Instead, they bubble up to the nearest caller.

If nothing else is specified, calling a function that throws from within a function that throws will implicitly bubble errors.

Syntax for catching errors

If you want to catch errors locally instead of letting them bubble up to the caller, use a try/catch construct like this:

try {
    task_that_might_fail()
} catch error {
    println("Caught error: {}", error)
}

There's also a shorter form:

try task_that_might_fail() catch error {
    println("Caught error: {}", error)
}

Rethrowing errors

(Not yet implemented)

Inline C++

For better interoperability with existing C++ code, as well as situations where the capabilities of Jakt within unsafe blocks are not powerful enough, the possibility of embedding inline C++ code into the program exists in the form of cpp blocks:

mut x = 0
unsafe {
    cpp {
        "x = (i64)&x;"
    }
}
println("{}", x)

References

Values and objects can be passed by reference in some situations where it's provably safe to do so.

A reference is either immutable (default) or mutable.

Reference type syntax

  • &T is an immutable reference to a value of type T.
  • &mut T is a mutable reference to a value of type T.

Reference expression syntax

  • &foo creates an immutable reference to the variable foo.
  • &mut foo creates a mutable reference to the variable foo.

Dereferencing a reference

To "get the value out" of a reference, it must be dereferenced using the * operator, however the compiler will automatically dereference references if the dereferencing is the single unambiguous correct use of the reference (in practice, manual dereferencing is only required where the reference is being stored or passed to functions).

fn sum(a: &i64, b: &i64) -> i64 {
    return a + b
    // Or with manual dereferencing:
    return *a + *b
}

fn test() {
    let a = 1
    let b = 2
    let c = sum(&a, &b)
}

For mutable references to structs, you'll need to wrap the dereference in parentheses in order to do a field access:

struct Foo {
    x: i64
}
fn zero_out(foo: &mut Foo) {
    foo.x = 0
    // Or with manual dereferencing:
    (*foo).x = 0
}

References (first version) feature list:

  • Reference types
  • Reference function parameters
  • Local reference variables with basic lifetime analysis
  • No references in structs
  • No references in return types
  • No mutable references to immutable values
  • Allow &foo and &mut foo without argument label for parameters named foo
  • Auto-dereference references where applicable

References TODO:

  • (unsafe) references and raw pointers bidirectionally convertible
  • No capture-by-reference in persistent closures

Closures (first version) feature list:

  • Function as parameter to function
  • Functions as variables
  • No returning functions from functions
  • Lambdas can throw
  • Explicit captures

Closures TODO:

  • [] Return function from function

Compiletime Execution

Compiletime Function Execution (or CTFE) in Jakt allows the execution of any jakt function at compiletime, provided that the result value may be synthesized using its fields - currently this only disallows a few prelude objects that cannot be constructed by their fields (like Iterator objects and StringBuilders).

Any regular Jakt function can be turned into a compiletime function by replacing the function keyword in its declaration with the comptime keyword, which will force all calls to that specific function to be evaluated at compile time.

Invocation Restrictions

Comptime functions may only be invoked by constant expressions; this restriction includes the this object of methods.

Throwing in a comptime context

Throwing behaves the same way as normal error control flow does, if the error leaves the comptime context (by reaching the original callsite), it will be promoted to a compilation error.

Side effects

Currently all prelude functions with side effects behave the same as they would in runtime. This allows e.g. pulling in files into the binary; some functions may be changed later to perform more useful actions.

Comptime imports

It is possible to design custom import handling based on data available at compile time. An excellent example of this in the Jakt compiler is the Platform Module.

See a smaller example in the comptime imports sample.

Comptime TODO

  • Implement execution of all Jakt expressions

jakt's People

Contributors

0greenclover0 avatar adkaster avatar adleroliveira avatar alimpfard avatar arthurpv avatar atkinssj avatar awesomekling avatar cg-jl avatar charliemirabile avatar dangrie158 avatar fahrradflucht avatar greytdepression avatar hectorpeeters avatar kleinesfilmroellchen avatar lanmonster avatar lenart12 avatar linusg avatar lubrsi avatar macdue avatar mattisboeckle avatar mnlrsn avatar mustafaquraish avatar ntg28 avatar robryanx avatar sin-ack avatar sophiajt avatar tophevich avatar trflynn89 avatar u9g avatar weltschildkroete avatar

Stargazers

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

Watchers

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

jakt's Issues

Parser hangs infinitely on unclosed brackets in statements

function main() {
    println("test"
}

Trace output:

...
parse_file
parse_function: Token { contents: Name("function"), span: Span { file_id: 1, start: 2, end: 10 } }
parse_block: Token { contents: LCurly, span: Span { file_id: 1, start: 18, end: 19 } }
parse_statement: Token { contents: Name("println"), span: Span { file_id: 1, start: 24, end: 31 } }
parsing expression from statement parser
parse_expression: Token { contents: Name("println"), span: Span { file_id: 1, start: 24, end: 31 } }
parse_operand: Token { contents: Name("println"), span: Span { file_id: 1, start: 24, end: 31 } }
parse_call: Token { contents: Name("println"), span: Span { file_id: 1, start: 24, end: 31 } }
parse_expression: Token { contents: QuotedString("test"), span: Span { file_id: 1, start: 32, end: 38 } }
parse_operand: Token { contents: QuotedString("test"), span: Span { file_id: 1, start: 32, end: 38 } }
...
parse_expression: Token { contents: RCurly, span: Span { file_id: 1, start: 39, end: 40 } }
parse_operand: Token { contents: RCurly, span: Span { file_id: 1, start: 39, end: 40 } }
ERROR: unsupported expression
parse_operator: Token { contents: RCurly, span: Span { file_id: 1, start: 39, end: 40 } }
ERROR: unsupported operator (possibly just the end of an expression)
...

Question : Do you consider adding operator overloading to Jakt?

I am looking for a new language that woulld be a replacement for Fortran for scientific computing.

I have considered : Nim, V, Crystal, Odin, Julia, Go, Swift, but they all have their own quirks and issues.

Therefore, I was wondering if you plan to implement operator overloading in Jakt.

Standard library cat sample is not compiling

It seems like there is something wrong with the visibility modifiers. None of the functions of the File class are set to public, so I suspect that would have to be changed. However, it is weird that the open_for_reading doesn't throw an error. Is there a difference between functions that take this as an argument and functions that do not?

Error: Can't access function `read` from scope None
-----
function main(args: [String]) {
    if args.size() <= 1 {
        eprintln("usage: cat <path>")
        return 1
    }

    let mutable file = File::open_for_reading(args[1])
    let mutable array: [u8] = [0u8]

    while file.read(array) != 0 {
        for idx in 0..array.size() {
            print("{:c}", array[idx])
        }
    }
}

-----
Error: TypecheckError("Can't access function `read` from scope None", Span { file_id: 1, start: 222, end: 231 })

Inheritance

Classes and structs should be allowed to inherit from other classes and structs respectively.

class Animal {
}

class CatDog: Animal {
}

struct Sport {
}

struct Football: Sport {
}

Note that a struct can't inherit from a class and vice versa, as that would break the reference counting ownership model.

Implement var keyword

One area where I personally see Jakt as hard to read is let mutable. After let, my brain wants to see an identifier. Can you declare more than one identifier per line? Is it let mutable a, mutable b or let mutable a, b? If the latter, it is really ambiguous. If the former, that is some real estate inflation if I want to declare say 5 mutable identifies in something like a math function.

We can say that Jakt is “immutable by default” but in the first two app samples there is mutability everywhere.

Consider reserving let to mean immutability unambiguously. Then use var when mutability is wanted.

Instead of

let mutable c = fgetc(file)
while feof(file) == 0 {
     putchar(c)
     c = fgetc(file)
} 

we would write

var c = fgetc(file)
while feof(file) == 0 {
     putchar(c)
     c = fgetc(file)
} 

Void member function should not return a value from array manipulations

The void member function quoted below is building an array intended to initialize a member variable. It is transpiled to C++ code that includes the TRY macro, thereby implicitly returning a value. This generated an error message when compiling the generated C++ code.

Reduced test case (leaving aside that it probably would produce a runtime error violating array boundaries):

class Row {
    entries: [u16]
}

class Test {
    rows: [Row]

    function init(mutable this, dims: u8)  {
        for i in 0..(dims-1) {
            let entries = [0u16; 2]
            this.rows[i] = Row(entries: entries)
        }
    }
}

$ ../../jakt/target/debug/jakt Test.jakt && clang++ -std=c++20 -I ../../jakt/ -I ../../jakt/runtime/ -Wno-user-defined-literals output.cpp

output.cpp:32:41: error: void function 'init' should not return a value [-Wreturn-type]
            const Array<u16> entries = (TRY(Array<u16>::filled(static_cast<i64>(2LL), static_cast<u16>(0))));
                                        ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
../../jakt/runtime/AK/Try.h:16:13: note: expanded from macro 'TRY'
            return _temporary_result.release_error(); \
            ^      ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
output.cpp:33:38: error: void function 'init' should not return a value [-Wreturn-type]
            (((((this)->rows))[i]) = TRY(Row::create(entries)));
                                     ^~~~~~~~~~~~~~~~~~~~~~~~~
../../jakt/runtime/AK/Try.h:16:13: note: expanded from macro 'TRY'
            return _temporary_result.release_error(); \
            ^      ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
2 errors generated.

Suggestion: iterator, list comprehension

I like Go, however it lacks a basic feature found in many other languages Python, etc, namely iterators and the corresponding list comprehension, so I basically had to write it myself :

https://github.com/serge-hulne/go_iter

So basically, my suggestion is: Since iterators and list comprehension (for arrays and/or streams) are very useful, it would be useful to have them incorporated in Jakt early on.

Error message "Type mismatch: expected unknown, but got unknown"

When compiling the following test code, an error message is produced:

Error: TypecheckError("Type mismatch: expected unknown, but got unknown", Span { file_id: 1, start: 89, end: 91 })

The error is at the first line of main(), specifically the empty array.

If the second line is uncommented (and commenting the first to skip the aforementioned error message), the following error message is produced:

Error: TypecheckError("Type mismatch: expected unknown, but got i64", Span { file_id: 1, start: 129, end: 135 })

Code:

class A {
    elements: [f64]
}

function main() {
    let mutable a3 = A(elements: [])
    //let mutable a2 = A(elements: [0f64])
}

Make many clippy warnings a hard error

In doing #168, I found a couple of ridiculous patterns, like the match for a single variant. We should probably do #[deny(clippy::all)] or at least some of the lints we find offensive so that we can automatically prevent them from getting through code review.

CC @jntrnr, is this something that sounds good? I'm very unsure as I love doing #[deny(clippy::all, clippy::pedantic, clippy::nursery)] in personal projects, which is too much here, but having the linter create hard errors is a good thing IMHO.

Remove `as truncated` and `as saturated` from the syntax

I don't like this:

  1. These two operators, by their semantics, only work on integer types, which treats them specially. Normal as works on all types and might have user-defined behavior in the future.
  2. We're bloating the syntax with features that are non-essential and not super common. Sure, they are extremely good and helpful features, but does that warrant their own syntax?

My suggestion would be to move them to generic prelude functions as_truncated<T>(anonymous a: T) throws -> T and as_saturated.... Then they create almost no additional visual noise while reducing compiler complexity.

CC @awesomekling , you wrote the original section on casts in the README so I figured you care a whole bunch just as I do :^)

The intent of this issue is to see whether this is something we want to do; I'll definitely implement it if it's okay but I don't want to waste my time.

Call or member function with `mutable this` on non-`mutable` variable should be rejected

struct Foo {
    function bar(mutable this) {}
}

function main() {
    let foo = Foo()
    foo.bar()
}

This generates:

#include "runtime/lib.h"
struct Foo;

struct Foo {
  public:
    void bar(){
using _JaktCurrentFunctionReturnType = void;
    {
    }
}
    Foo() {}

};


ErrorOr<int> JaktInternal::main(Array<String>)
{
    using _JaktCurrentFunctionReturnType = ErrorOr<int>;
    {
        const Foo foo = Foo();
        ((foo).bar());
    }
    return 0;
}

Which clang then fails to compile:

output.cpp:21:10: error: 'this' argument to member function 'bar' has type 'const Foo', but function is not marked const
        ((foo).bar());
         ^~~~~
output.cpp:6:10: note: 'bar' declared here
    void bar(){
         ^
1 error generated.

Empty structs lead to invalid codegen

struct Foo {

}

=>

#include "runtime/lib.h"
struct Foo;

struct Foo {
  public:
    Foo(): {}

};

Note that there are no member initialisers, yet : is present.

Array creation in functions that can't throw errors generates invalid cpp

When allocating arrays in functions that don't return an error type, the compiler generates incorrect cpp code as it uses the TRY macro.

It might also be nice to annotate that the main function returns and ErrorOr<T> as that is not clear from the current syntax. In the example below, it looks like the two functions both return void but in main, the array allocation generates correct code while in test if does not.

Example:

function test() {
    let mutable data = [0u32; 256]
}

function main() {
    test()
}

Remove inline C++

I may be more excited about Jakt in some ways than I am about Serenity. It makes a lot of nice choices including some very sane choices in terms of making it unambiguous to parse. It is easy to imagine how the syntax could be extended in the future. Of course, it solves the hardest new language problem as well which is “nobody is going to use it” as the footprint of Serenity alone probably guarantees at least some level of exposure and success.

What I cannot wrap my head around is “in-line C++”. This seems like a big design mistake. It is easy to see how this would be implemented now. I can imagine it will also seem handy at first as an escape hatch for missing features. But how will this work when Jakt is doing its own code generation? It would require an embedded C++ parser. That project alone is bigger than the rest of Jakt ( I think I would rather implement my own OS than implement C++ ). How does reference counting work when there is inline C++? One of the benefits of Jakt seems to be that it will be so much easier to understand than C++. But with inline C++, I might have to know C++ to understand a Jakt program. Another advantage, especially as the Serenity mono-repo grows, is faster compile times. But embedded C++ kills that too. Inline C++ seems like a nightmare.

Would it be possible to consider embedded C++ as a feature of the compiler ( of this specific implementation ) instead of as a feature of the language? Kind of like GCC extensions?

I guess I am hoping for this feature to be removed. If not removed, at least treated uniquely in the hopes that it could be removed long term. Or discouraged so that it is used only sparingly in practice.

Thinking ahead, what makes C++ so special anyway? I mean, it is what SerenityOS is written in but the stated goal is to rewrite in Jakt. Jakt itself is written in Rust after all ( though it will hopefully be written in Jakt as well at some point ).

Disallow returns in defer statements

For example:

function foo() {
    defer {
        return 2
    }

    return 1
}

This will return 1, because it's essentially doing:

ScopeGuard blah([] {
    return 2;
});

return 1;

The return 2; only exits the ScopeGuard lambda and not the top level function.

But this is not intuitive and hard to reason about, and like Agni says:

but it's probably better to disallow that in the language semantics, because it was mentioned that C++ is not going to be the only backend

Function overloading

We should allow function overloading by parameter count and parameter types.

function dump(value: i64) => println("{}", a)
function dump(value: String) => println("{}", a)

function main() {
    dump(123)
    dump("Hello")
}

Return type inference doesn't reject incompatible returns

function foo() {
    return true
    return "foo"
}

This should not compile, but generates the following:

#include "runtime/lib.h"


String foo();

String foo(){
using _JaktCurrentFunctionReturnType = String;
{
        return (true);
        return (String("foo"));
    }
}

Point out missing `throws` for functions that can fail

Currently, if a foo() not marked with throws calls another function bar() which does throw an error, the compiler doesn't point this out, and instead generates incorrect cpp code. This should ideally be caught before we generate any code.

Example:

function bar() throws -> i64 { return 5 }
function foo() -> i64 { return bar() }

Current sample apps don't compile

On the latest main branch, both the cat and crc32 sample programs fail with the following error: Condition must be a boolean expression.

To prevent this in the future, it might be a good idea to add these to cargo test.

Cat

For the cat example, compilation fails because of the following line:

20:    while not feof(file) {

Since feof returns a c_int and while expects a boolean. Changing the condition to feof(file) == 0 does not work as comparisons between c_int and normal numeric types is not supported.

CRC

For the crc example, the incorrect line is the following:

14:    if value & 1 {

Here, value & 1 does not result in a boolean value. Replacing this with value & 1 != 0 would solve the problem but that again needs the c_int and numeric comparisons.

Compiler should reject incorrect main function type annotations

Given that the main function is special and hardcodes ErrorOr<int> as its return type in codegen, the typechecker should reject any attempts of giving it:

  • A return type that is not throws -> c_int
  • Parameters other than the current Array<String>

E.g.

function main(x: i32) -> String {
    return ":^)"
}

Generates:

#include "runtime/lib.h"

ErrorOr<int> _jakt_main(const i32 x)
{
    using _JaktCurrentFunctionReturnType = ErrorOr<int>;
    {
        return (String(":^)"));
    }
    return 0;
}

Usage of String::push_str instead of write macro

I have seen in codegen.rs, that you use String::push_str:

            output.push_str("enum class ");
            output.push_str(&enum_.name);
            output.push_str(": ");
            output.push_str(&codegen_type(ty, project));
            output.push_str(" {\n");

This could be replaced with the write! or writeln! macro, if std::io::Write is in scope.

            writeln!("enum class {}: {} {{", enum_.name, codegen_type(ty, project));

This would also be a good first step into moving away from heap allocated strings as output and using the Write trait as output.

But while String::push_str returns a () type, the write! macros return Result<(), std::io::Error, which would need to be handled in some capacity.

I would be interested to hear, how you feel about this.

Sets

I want syntax for sets pls, they're underrated. But not in a way Python does it where an empty set can't be represented with set syntax, because it's actually an empty dict...

Add instructions on how to run rustfmt as a pre-commit hook

This seems to be a common issue in PRs and it would be helpful.

For instance, using pre-commit:

$ pip install pre-commit

Adding this to .pre-commit-config.yaml

-   repo: https://github.com/doublify/pre-commit-rust
    rev: master
    hooks:
    -   id: fmt

It also makes it trivial to add cargo-check and clippy.

Thoughts? I can create a PR with the hook config and README.md updates if needed.

Imports/includes

Being able to split code across files is crucial for writing maintainable and reusable software. No strong feelings regarding syntax and actual implementation.

Native Code Talk

Could we not use LLVM to generate native code. this would allow the compiler to use the well test and designed optimiser and multi platform backed for arm64 and x86_64

Code paths with missing return are not rejected

function foo() -> i64 {
    if true {
        return 42
    }
}

Generates:

#include "runtime/lib.h"


i64 foo();

i64 foo(){
using _JaktCurrentFunctionReturnType = i64;
{
        if (true) {
            return (static_cast<i64>(42LL));
        }
    }
}

Bogus typecheck error message if unrelated class is present

When compiling this code, an error message is produced:

function popcount( bitfield: u32) -> u8 {
    let mutable popcnt : u8 = 0u8;
    return popcnt
}

class Clif {
    function init(mutable this) {}
}

Error message:
Error: TypecheckError("Type mismatch: expected void, but got u8", Span { file_id: 1, start: 367, end: 373 })

The error goes away if the function init() of class Clif is commented out, or the Clif class is deleted altogether.

The error is highlighted at the return statement of the popcount() function.

First-class functions and function types

I think the language could benefit from having first-class functions neatly integrated into the syntax and type system.

Both C++ and Rust use "special syntax" for lambdas. C++ types for lambdas are (to my knowledge) nothing like function types and their semantics feels needlessly complex in my opinion.

Jakt still has a chance of being much more intuitive.

Let functions be objects of a function type, function type syntax follows function declaration syntax:

let f = function(x: i32) => x + 1 
// f : function(i32) -> i32

Pass functions to other functions:

frob.frobnicate(callback: function(result) {
  // ...
})
// frob.frobnicate : function(callback: function(anonymous result: FrobResult))

Using -> for return types maps nicely to function type notation:

function hello() -> function(String) -> String {
  return function(who: String) -> String {
    return "well hello " + who + "!"
  }
}
// hello : function() -> function(String) -> String 
// hello() : function(String) -> String

One could even take values of functions (or even bound methods) and treat them as ordinarily typed objects that could be passed to any function expecting to receive a function:

function compose<A, B, C>(f1: function(A) -> B, f2: function(B) -> C) -> function(A) -> C => function(a: A) => f2(f1(a))

function foo(x: i32) -> i32 {
  return x * 42
}

let f = compose(foo, function(x) => x + 96)
// f : function(i32) -> i32

I'm not proposing to make Jakt a functional-heavy language or make it so that it imposes functional style on the programmer. I just think that having lambdas is inevitable and it would be super nice if they were well integrated instead of bolted on as special guests.

Throw expression is not type checked sufficiently

This compiles:

function foo() throws -> u32 {
    throw 1
}

function main() {
    let x = foo()
    println("{}", x)
}

To:

#include "runtime/lib.h"


ErrorOr<u32> foo();

ErrorOr<u32> foo(){
using _JaktCurrentFunctionReturnType = ErrorOr<u32>;
{
        return static_cast<i64>(1LL);    }
}

ErrorOr<int> _jakt_main(Array<String>)
{
    using _JaktCurrentFunctionReturnType = ErrorOr<int>;
{
        const u32 x = TRY(foo());
        outln(String("{}"),x);
    }
    return 0;
}

Meaning it will unexpectedly print 1.

Typechecker isn't making all user types available before checking them

Example from discord:

Error: unknown type
-----
struct Foo {
    function bar(mutable this, baz: mutable Bar) {} // <-- error here on Bar
}

class Bar {}

function main() {
    let foo = Foo()
    let baz = Bar()
    foo.bar(baz)
}

What we currently do is check method prototypes in typecheck_struct_predecl but that doesn't allow all the user types to be known in that scope before they're looked up. We should go through all the user types for the scope first before doing the predecl so that the names are available, then we have something to bind to.

Binary operators not typechecked

At the moment, the normal binary operators (+, -, *, /, ...) are not typechecked at all.

Implementation wise, I think it would be best if we only allow the same types on both the left and right side without any upcasting. This adds some additional type casts but makes it more clear what exactly is happening in the code.

An example of what wouldn't work in that scenario:

function main() {
    let x: u8 = 12;
    let y: u16 = 34;
    println("{}", x + y) // ERROR: binary operation between incompatible types
}

In the future it would be a good addition to allow casting literals to the correct type so the following expression would work:

let x: u8 = 12;
println("{}", x + 34);

Weak references

weak T? should behave essentially like Optional<T&> in C++ but with the added feature that it automatically empties itself when the T is destroyed.

weak T is not allowed, since that implies it's always in a dereferenceable state.

Void function marked `throws` generates invalid code for non-throwing branches

This:

function foo() throws {}

function main() {
   foo()
}

Generates:

#include "runtime/lib.h"


ErrorOr<void> foo();

ErrorOr<void> foo(){
using _JaktCurrentFunctionReturnType = ErrorOr<void>;
    {
    }
}

ErrorOr<int> _jakt_main(Array<String>)
{
    using _JaktCurrentFunctionReturnType = ErrorOr<int>;
    {
        TRY(foo());
    }
    return 0;
}

Which clang warns about but still compiles:

output.cpp:13:1: warning: non-void function does not return a value [-Wreturn-type]

...causing the compiled executable to crash:

[1]    178382 illegal hardware instruction (core dumped)  ./a.out

Type checking does not reject invalid declarations

function main() {
    let foo: i8 = "Boo!";
}

Raises no errors and produces:

#include "runtime/lib.h"



ErrorOr<int> _jakt_main(Array<String>)
{
    using _JaktCurrentFunctionReturnType = ErrorOr<int>;
{
        const i8 foo = String("Boo!");
    }
    return 0;
}

Typechecker rejects assignment to weak on declaration without Some

The following program

class Foo {
    public function hello(this) => "friends"
}

function main() {
    let foo = Foo()
    let weak_foo: weak Foo = foo

    println("weak_foo hello: {}", weak_foo!.hello())
}

does not compile with Error: Type mismatch: expected struct WeakPtr<class Foo>, but got class Foo, instead of giving the expected output of weak_foo hello: friends. It does work, however, if let weak_foo: weak Foo = foo is changed to let weak_foo: weak Foo = Some(foo).

Static Reflection

It would be super nice to support this as it remains a major pain point of C++ (at least for myself); introspection of types being the main desired feature

  • the keyword reflect
  • an expression of the form reflect type-name
  • Implementation requirements:
    • Should present a meaningful interface to the user, perhaps through predeclared types
    • Should work with generic types
    • Should have all the information to reconstruct the reflected type (though not required for basic functionality)

small example (syntax is made-up and not decided)

enum Foo<T> {
    Var0: T
    Var1: i32
}

function foo() -> String {
	return match reflect Foo {
        Enum(variants: vs) {
            vs.first().name
        }
        _ => verify_not_reached()
    }
}

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.