serenityos / jakt Goto Github PK
View Code? Open in Web Editor NEWThe Jakt Programming Language
License: BSD 2-Clause "Simplified" License
The Jakt Programming Language
License: BSD 2-Clause "Simplified" License
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()
}
Being able to split code across files is crucial for writing maintainable and reusable software. No strong feelings regarding syntax and actual implementation.
For example:
enum a { b=[ }
struct Foo {
}
=>
#include "runtime/lib.h"
struct Foo;
struct Foo {
public:
Foo(): {}
};
Note that there are no member initialisers, yet :
is present.
One feature I like about Go is gouroutines and channels.
I think it leads to code which is much more readable than the approches using callbacks or promises or async/await in JS.
Do you plan to implement lightweight processes at some point?
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.
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
reflect
reflect type-name
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()
}
}
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 })
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"));
}
}
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() }
I don't like this:
as
works on all types and might have user-defined behavior in the future.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.
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:
throws -> c_int
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;
}
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")
}
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)
.
For example:
function foo<Ts..>()
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.
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.
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
.
The following code does not compile:
let floating_point: f32 = 0.25;
Error: TypecheckError("tuple index used non-tuple value", Span { file_id: 1, start: 45, end: 46 })
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.
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;
}
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.
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.
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)
...
We should allow JavaScript-style optional chaining: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining
struct Bar {
baz: i64
}
struct Foo {
bar: Bar?
}
function main() {
let mut foo = Foo(bar: None)
let baz = foo?.bar?.baz
}
Implementation wise, this should become an expression that returns an Optional<i64>
, since that's the type of the last ?.
If we short-circuit on any of the ?.
operators, we should return an empty Optional<i64>
.
function foo() {}
function main() {
foo<String>()
}
thread 'main' panicked at 'index out of bounds: the len is 0 but the index is 0', src/typechecker.rs:3554:49
For example:
extern
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.
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.
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.
This causes an infinite loop in the parser:
function foo() {
return {1:1}
}
Removing return
makes it error out correctly:
-----
function foo() {
{1:1}
}
-----
Error: ParserError("unsupported expression", Span { file_id: 1, start: 23, end: 24
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 ).
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.
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.
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)
}
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
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...
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
How do you import à Jakt file into another file?
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
.
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.
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.
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])
}
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));
}
}
}
Create a repository like SerenityOS/jakt.vim
so that we can use a vim plugin manager to get plugin updates
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.
6/11 occurrences are bolded right now
This causes the parser to go into an infinite loop:
function main() {
match foo {
(++foo) => 1
}
}
Removing the ()
around ++foo
makes it error:
-----
function main() {
match foo {
++foo => 1
}
}
-----
Error: ParserError("expected '=>' after pattern case", Span { file_id: 1, start: 42, end: 44 })
This came up on discord. Current suggestion is [:]
to create an empty dictionary, similar to []
creating an empty array.
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);
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
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.