The Jakt Programming Language
探索 Jakt 编程语言
Jakt 是一种内存安全的系统编程语言。
它目前可以转译为 C++ 代码。
注意: 该语言正处于积极开发阶段。
注意 如果你克隆到 Windows 电脑上 (不是 WSL),请确保你的 Git 客户端保持行尾为 \n
。 你可以通过 git config --global core.autocrlf false
将其设置为全局配置。
使用方法
将代码转译为 C++ 需要 clang
。 请确保已安装。
jakt file.jakt
./build/file
构建
请参考这里。
目标
- 内存安全
- 代码可读性
- 开发者生产力
- 可执行文件性能
- 乐趣!
内存安全
以下策略用于实现内存安全:
- 自动引用计数
- 强类型
- 边界检查
- 在安全模式下没有原始指针
在 Jakt 中,有三种指针类型:
- T (指向引用计数类
T
的强指针。) - weak T (指向引用计数类
T
的弱指针。在被指向对象销毁时变为空。) - raw T (指向任意类型
T
的原始指针。仅可在unsafe
代码块中使用。)
在安全模式下,空指针是不可能的,但指针可以包装在 Optional
中,例如 Optional<T>
或简写为 T?
。
数学安全
- 整数溢出(有符号和无符号)是一个运行时错误。
- 数值不会自动强制转换为
int
。所有类型转换都必须是显式的。
如果需要静默整数溢出,可以使用显式函数来提供此功能。
代码可读性
阅读代码所花费的时间比编写代码的时间要多得多。 因此,Jakt 非常重视可读性。
以下是一些鼓励编写更具可读性程序的特性:
- 默认不可变。
- 调用表达式中的参数标签 (
object.function(width: 10, height: 5)
) - 推断的
enum
作用域。 (你可以说Foo
而不是MyEnum::Foo
)。 - 使用
match
进行模式匹配。 - 可选链式调用 (
foo?.bar?.baz
(可失败) 和foo!.bar!.baz
(不可失败)) - 可选项的空值合并 (
foo ?? bar
如果foo
有值则返回foo
,否则返回bar
) defer
语句。- 指针始终使用
.
解引用(从不使用->
) - 后置闭包参数可以在调用括号之外传递。
- 使用
ErrorOr<T>
返回类型和专用try
/must
关键字进行错误传播。
代码重用
Jakt 在如何构建项目方面非常灵活,它内置了一个模块系统。
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)
- 从与文件相同的目录导入模块。
- 仅从模块
a
导入use_cool_things()
。 - 导入可以在编译时计算。 参见 Comptime Imports
- 当模块是包含该文件的目录的子路径时,使用
relative
关键字导入模块。 - 从包含该文件的目录的上级目录导入模块。
- 从包含该文件的目录的上三级父路径导入模块的语法糖。
The Jakt Standard Library
Jakt 有一个标准库,可以使用 jakt::
命名空间访问:
import jakt::arguments
import jakt::libc::io { system }
The Jakt Standard Library 还在起步阶段,请考虑做出贡献!
函数调用
调用函数时,必须在传递每个参数时指定其名称:
rect.set_size(width: 640, height: 480)
对此有两个例外:
- 如果函数声明中的参数声明为
anon
,则允许省略参数标签。 - 当传递的变量与参数名称相同时。
结构体和类
在 Jakt 中,有两种声明结构的主要方式:struct
和 class
。
struct
基本语法:
struct Point {
x: i64
y: i64
}
Jakt 中的结构体具有 值语义:
- 包含结构体的变量始终具有该结构体的唯一实例。
- 复制
struct
实例始终进行深拷贝。
let a = Point(x: 10, y: 5)
let b = a
// "b" 是 "a" 的深拷贝,它们不指向同一个 Point
Jakt 为结构体生成默认构造函数。 它通过名称获取所有字段。 对于上面的 Point
结构体,它看起来像这样:
Point(x: i64, y: i64)
结构体成员默认为 public。
class
- 基本类支持
- 默认私有成员
- 继承
- 基于类的多态性 (将子实例分配给需要父类型的事物)
Super
类型Self
类型
与 struct
相同的基本语法:
class Size {
width: i64
height: i64
public fn area(this) => .width * .height
}
Jakt 中的类具有 引用语义:
- 复制
class
实例(也称为“对象”)会复制对该对象的引用。 - 默认情况下,所有对象都进行引用计数。 这确保了在删除对象后不会访问它们。
类成员默认为 private。
成员函数
结构体和类都可以具有成员函数。
成员函数有三种:
静态成员函数 不需要对象即可调用。 它们没有 this
参数。
class Foo {
fn func() => println("Hello!")
}
// Foo::func() 可以在没有对象的情况下调用。
Foo::func()
非可变成员函数 需要调用对象,但不能改变对象。 第一个参数是 this
。
class Foo {
fn func(this) => println("Hello!")
}
// Foo::func() 只能在 Foo 的实例上调用。
let x = Foo()
x.func()
可变成员函数 需要调用对象,并且可以修改对象。 第一个参数是 mut this
。
class Foo {
x: i64
fn set(mut this, anon x: i64) {
this.x = x
}
}
// Foo::set() 只能在 mut Foo 上调用:
mut foo = Foo(x: 3)
foo.set(9)
访问成员变量的简写形式
为了减少方法中重复的 this.
,简写形式 .foo
扩展为 this.foo
。
字符串
该语言主要以类型 String
提供字符串,它是一种引用计数(和堆分配)的字符串类型。 字符串字面量用双引号括起来,例如 "Hello, world!"
。
重载字符串字面量
默认情况下,字符串字面量的类型为 String
; 但是,它们可以用于隐式构造任何实现 FromStringLiteral
(或 ThrowingFromStringLiteral
)trait 的类型。 在语言 prelude 中,目前只有 StringView
实现了这个 trait,它只能用于引用具有静态生命周期的字符串:
let foo: StringView = "foo" // 此字符串未在堆上分配,foo 只是一个指向静态字符串的胖指针。
可以通过提供类型提示来使用重载字符串字面量,无论是通过显式类型注解,还是通过将字面量传递给需要特定类型的函数:
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")
}
数组
动态数组通过内置的 Array<T>
类型提供。 它们可以在运行时增长和缩小。
Array
是内存安全的:
- 越界访问将导致程序崩溃,并显示运行时错误。
Array
的切片通过自动引用计数来保持底层数据的活动状态。
声明数组
// 接受 Array<i64> 并返回 Array<String> 的函数
fn foo(numbers: [i64]) -> [String] {
...
}
创建数组的简写形式
// Array<i64>,包含 256 个元素,全部初始化为 0。
let values = [0; 256]
// Array<String>,包含 3 个元素:“foo”、“bar”和“baz”。
let values = ["foo", "bar", "baz"]
字典
- 创建字典
- 索引字典
- 分配到索引中(即左值)
fn main() {
let dict = ["a": 1, "b": 2]
println("{}", dict["a"])
}
声明字典
// 接受 Dictionary<i64, String> 并返回 Dictionary<String, bool> 的函数
fn foo(numbers: [i64:String]) -> [String:bool] {
...
}
创建字典的简写形式
// Dictionary<String, i64>,包含 3 个条目。
let values = ["foo": 500, "bar": 600, "baz": 700]
集合
- 创建集合
- 引用语义
fn main() {
let set = {1, 2, 3}
println("{}", set.contains(1))
println("{}", set.contains(5))
}
元组
- 创建元组
- 索引元组
- 元组类型
fn main() {
let x = ("a", 2, true)
println("{}", x.1)
}
枚举和模式匹配
- 枚举作为和类型
- 泛型枚举
- 枚举作为底层类型值的名称
match
表达式match
arm 中的枚举范围推断- 从 match 块产生值
- 嵌套
match
模式 - Trait 作为
match
模式 - 支持与
?
、??
和!
运算符互操作
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 => { ... }
}
泛型
- 泛型类型
- 常量泛型(最小支持)
- 常量泛型(完全支持)
- 泛型类型推断
- Traits
Jakt 支持泛型结构体和泛型函数。
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: 当前无法访问值 'U',引用 'U' 仅作为类型有效。
data: [T]
}
命名空间
- 支持函数和 struct/class/enum 的命名空间
- 深度命名空间支持
namespace Greeters {
fn greet() {
println("Well, hello friends")
}
}
fn main() {
Greeters::greet()
}
类型转换
Jakt 中有两个内置的转换运算符。
as? T
: 返回一个Optional<T>
,如果源值不能转换为T
,则为空。as! T
: 返回一个T
,如果源值不能转换为T
,则中止程序。
as
转换可以执行以下操作(请注意,实现可能尚未达成一致):
- 转换为相同类型是绝对安全的,并且毫无意义,因此将来可能会被禁止。
- 如果源类型是 unknown,则该转换作为类型断言有效。
- 如果两种类型都是原始类型,则执行安全转换。
- 如果值超出范围,整数转换将失败。 这意味着像 i32 -> i64 这样的提升转换是绝对安全的。
- Float -> Integer 转换会截断小数点 (?)
- Integer -> Float 转换为最接近可由浮点类型表示的整数值 (?)。 如果整数值太大,它们将转换为无穷大 (?)
- 任何 primitive -> bool 都将为任何值(0 除外)创建
true
,0 为false
。 - bool -> 任何 primitive 都将执行
false -> 0
和true -> 1
,即使对于浮点数也是如此。
- 如果类型是两种不同的指针类型(见上文),则转换本质上是空操作。 转换为
T
将按预期增加引用计数; 这是从弱引用创建强引用的首选方法。 从raw T
转换为和从raw T
转换都是不安全的。 - 如果类型是同一类型层次结构的一部分(即,一个是另一个的子类型):
- 子类型可以绝对安全地转换为其父类型。
- 父类型可以转换为子类型,但这将在运行时检查类型,如果对象不是子类型或其子类型之一,则会失败。
- 如果类型不兼容,则会尝试使用用户定义的转换。 此处的细节尚未确定。
- 如果没有任何效果,则转换甚至不会编译。
标准库中提供了其他转换。 两个重要的转换是 as_saturated
和 as_truncated
,它们在将积分值转换为饱和到边界或截断位时使用。
Traits
为了使泛型更强大和更具表现力,您可以向其中添加其他信息:
trait Hashable<Output> {
fn hash(self) -> Output
}
class Foo implements(Hashable<i64>) {
fn hash(self) => 42
}
Traits 可用于向泛型类型添加约束,但也可以基于最小的要求集提供默认实现 - 例如:
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")
}
// 请注意,我们不必在此处实现 `do_something_twice`,因为它具有默认实现。
}
struct Better implements(Fancy) {
fn do_something(this) -> void {
println("I'm not boring")
}
// 但是,自定义实现仍然有效。
fn do_something_twice(this) -> void {
println("I'm not boring, but I'm doing it twice")
}
}
Traits 可以有引用其他 traits 作为类型的方法,这可以用来描述 trait 的层次结构:
trait ConstIterable<T> {
fn next(this) -> T?
}
trait IntoIterator<T> {
// 请注意返回类型如何引用 ConstIterable trait(而不是具体类型)
fn iterator(this) -> ConstIterable<T>
}
运算符重载和 Traits
运算符作为 traits 实现,并且可以通过在给定类型上实现它们来重载:
struct Foo implements(Add<Foo, Foo>) {
x: i32
fn add(this, anon rhs: Foo) -> Foo {
return Foo(x: .x + other.x)
}
}
运算符和 traits 之间的关系如下(请注意,@
用作任何二元运算符的名称或符号的占位符):
运算符 | Trait | 方法名称 | 从方法派生
---|---|---|---
+
| 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
| -
其他运算符尚未转换为 traits、确定或实现:
运算符 | 说明 | 状态
---|---|---
&
| 按位与 | 未确定
|
| 按位或 | 未确定
^
| 按位异或 | 未确定
~
| 按位非 | 未确定
<<
| 按位左移 | 未确定
>>
| 按位右移 | 未确定
and
| 逻辑与 | 未确定
or
| 逻辑或 | 未确定
not
| 逻辑非 | 未确定
=
| 赋值 | 未确定
安全性分析
(尚未实施)
为了保证安全,我们需要做一些分析(非详尽):
- 阻止方法调用重叠,这将相互冲突。 例如,在容器上创建一个迭代器,并在迭代器有效时调整容器大小。
- 使用和操作原始指针
- 调用可能具有副作用的 C 代码
错误处理
可以使用 throws
关键字标记可以失败并返回错误而不是正常返回的函数:
fn task_that_might_fail() throws -> usize {
if problem {
throw Error::from_errno(EPROBLEM)
}
...
return result
}
fn task_that_cannot_fail() -> usize {
...
return result
}
与 C++ 和 Java 等语言不同,错误不会自动展开调用堆栈。 相反,它们会冒泡到最近的调用者。
如果未指定其他内容,则从 throws
函数中调用 throws
函数将隐式地冒泡错误。
捕获错误的语法
如果您想在本地捕获错误,而不是让它们冒泡到调用者,请使用如下所示的 try
/ catch
结构:
try {
task_that_might_fail()
} catch error {
println("Caught error: {}", error)
}
还有一种较短的形式:
try task_that_might_fail() catch error {
println("Caught error: {}", error)
}
重新抛出错误
(尚未实施)
内联 C++
为了更好地与现有的 C++ 代码互操作,以及 Jakt 在 unsafe
块中的功能不够强大的情况,可以以 cpp
块的形式将内联 C++ 代码嵌入到程序中:
mut x = 0
unsafe {
cpp {
"x = (i64)&x;"
}
}
println("{}", x)
引用
在某些情况下,当证明这样做是安全的时候,值和对象可以通过引用传递。
引用是不可变的(默认)或可变的。
引用类型语法
&T
是对类型为T
的值的不可变引用。&mut T
是对类型为T
的值的可变引用。
引用表达式语法
&foo
创建对变量foo
的不可变引用。&mut foo
创建对变量foo
的可变引用。
解引用引用
要“取出”引用的值,必须使用 *
运算符解引用它,但是,如果解引用是引用唯一明确正确的用法,则编译器将自动解引用引用(实际上,只有在引用被存储或传递给函数时才需要手动解引用)。
fn sum(a: &i64, b: &i64) -> i64 {
return a + b
// 或者使用手动解引用:
return *a + *b
}
fn test() {
let a = 1
let b = 2
let c = sum(&a, &b)
}
对于结构体的可变引用,您需要将解引用包装在括号中才能进行字段访问:
struct Foo {
x: i64
}
fn zero_out(foo: &mut Foo) {
foo.x = 0
// 或者使用手动解引用:
(*foo).x = 0
}
引用(第一版)功能列表:
- 引用类型
- 引用函数参数
- 具有基本生命周期分析的局部引用变量
- 结构体中没有引用
- 返回类型中没有引用
- 没有对不可变值的可变引用
- 允许对名为
foo
的参数使用不带参数标签的&foo
和&mut foo
- 在适用的情况下自动解引用引用
引用 TODO:
- (
unsafe
) 引用和原始指针双向可转换 - 在持久闭包中没有按引用捕获
闭包(第一版)功能列表:
- 函数作为函数的参数
- 函数作为变量
- 没有从函数返回函数
- Lambdas 可以抛出
- 显式捕获
闭包 TODO:
- 从函数返回函数
编译时执行
Jakt 中的编译时函数执行 (Compiletime Function Execution, CTFE) 允许在编译时执行任何 jakt 函数,前提是可以使用其字段合成结果值 - 目前,这仅禁止一些无法通过其字段构造的 prelude 对象(如 Iterator 对象和 StringBuilders)。
可以通过将其声明中的 function
关键字替换为 comptime
关键字将任何常规 Jakt 函数转换为编译时函数,这将强制在编译时评估对该特定函数的所有调用。
调用限制
Comptime 函数只能由常量表达式调用; 此限制包括方法的 this
对象。
在 comptime 上下文中抛出
抛出的行为与正常错误控制流的行为方式相同,如果错误离开 comptime 上下文(通过到达原始调用站点),它将被提升为编译错误。
副作用
当前,具有副作用的所有 prelude 函数的行为与它们在运行时中的行为方式相同。 这允许例如将文件拉入二进制文件中; 以后可能会更改某些函数以执行更有用的操作。
Comptime imports
可以根据编译时可用的数据设计自定义导入处理。 在 Jakt 编译器中,这方面的一个极好的例子是 Platform Module。
在 comptime imports sample 中查看一个更小的示例。
Comptime TODO
- 实现所有 Jakt 表达式的执行
关于
The Jakt Programming Language
Topics
programming-language serenityos jakt
Resources
License
Code of conduct
Code of conduct Activity Custom properties
Stars
Watchers
Forks
Releases
No releases published
Packages 0
No packages published