Jakt编程语言
Jakt是一种内存安全的系统编程语言。
目前它可以转译为C++。
注意: 该语言正在大力开发中。
注意: 如果你要克隆到Windows PC上(不是WSL),请确保你的Git客户端保持行尾为\n 。你可以通过git config --global core.autocrlf false ,将其设置为全局配置。
使用方法
转译成C++语言需要clang 。请确保你已经安装了该软件。
jakt file.jakt
./build/file
目标
- 内存安全
- 代码的可读性
- 开发者的生产力
- 可执行的性能
- 好玩!
内存安全
为了实现内存安全,我们采用了以下策略。
- 自动引用计数
- 强类型化
- 界限检查
- 在安全模式下没有原始指针
在Jakt中,有三种指针类型。
- T(指向引用计数类的强指针
T。) - 弱T?(弱指针指向引用计数类
T。指针销毁时变成空的)。 - raw T(指向任意类型的原始指针
T。只在unsafe块中可用。)
在安全模式下,空指针是不可能的,但是指针可以被包裹在Optional ,即Optional<T> 或简称T? 。
请注意,弱指针必须总是被包裹在Optional 。没有弱T,只有弱T?
数学安全
- 整数溢出(包括有符号和无符号)是一个运行时错误。
- 数值不会被自动强制到
int。所有的转换必须是明确的。
对于需要沉默的整数溢出的情况,有显式函数提供这个功能。
代码的可读性
阅读代码的时间远远多于编写代码的时间。出于这个原因,Jakt高度重视代码的可读性。
一些鼓励更多可读程序的特性。
- 默认情况下是不可变的。
- 调用表达式中的参数标签 (
object.function(width: 10, height: 5)) - 推断的
enum范围。(你可以说Foo,而不是MyEnum::Foo)。 - 用
match进行模式匹配。 - 可选链 (
foo?.bar?.baz(fallible) andfoo!.bar!.baz(infallible)) - 对可选项无凝聚力 (
foo ?? bar产生foo如果foo有一个值,否则bar) defer语句。- 指针总是用
.(从不用->)进行解引用。 - 后面的闭合参数可以在调用括号外传递。
- 错误传播与
ErrorOr<T>返回类型和专用try/must关键字。
函数调用
当调用一个函数时,你必须在传递每个参数时指定它的名字。
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" is a deep copy of "a", they do not refer to the same Point
Jakt为结构体生成了一个默认的构造函数。它通过名称获取所有字段。对于上面的Point 结构,它看起来像这样。
Point(x: i64, y: i64)
结构成员默认是公共的。
class
- 基本类支持
- 默认为私有的成员
- 继承
- 基于类的多态性(将子实例分配给需要父类型的事物)
Super类型Self类型
与struct 的基本语法相同。
class Size {
width: i64
height: i64
public function area(this) => .width * .height
}
Jakt中的类有引用语义。
- 复制一个
class实例(又称 "对象")会复制一个对该对象的引用。 - 所有的对象默认都是引用计数的。这可以确保对象在被删除后不会被访问。
类成员默认为私有。
成员函数
结构和类都可以有成员函数。
有三种类型的成员函数。
静态成员函数不需要对象来调用。它们没有this 参数。
class Foo {
function func() => println("Hello!")
}
// Foo::func() can be called without an object.
Foo::func()
非变异成员函数需要调用一个对象,但不能变异对象。第一个参数是this 。
class Foo {
function func(this) => println("Hello!")
}
// Foo::func() can only be called on an instance of Foo.
let x = Foo()
x.func()
变异成员函数需要调用一个对象,并且可以修改该对象。第一个参数是mut this 。
class Foo {
x: i64
function 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)
访问成员变量的速记法
为了减少方法中重复的this. 垃圾邮件,速记法.foo 扩展为this.foo 。
数组
动态数组是通过一个内置的Array<T> 类型提供的。它们可以在运行时增长和缩小。
Array 是内存安全的。
- 超出范围将使程序在运行时出现错误。
Array的片断通过自动引用计数使底层数据保持活力。
声明数组
// Function that takes an Array<i64> and returns an Array<String>
function foo(numbers: [i64]) -> [String] {
...
}
创建数组的速记法
// 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"]
字典
- 创建字典
- 索引字典
- 赋值到索引中(又称lvalue)。
function main() {
let dict = ["a": 1, "b": 2]
println("{}", dict["a"])
}
声明字典
// Function that takes a Dictionary<i64, String> and returns an Dictionary<String, bool>
function foo(numbers: [i64:String]) -> [String:bool] {
...
}
创建字典的速记法
// Dictionary<String, i64> with 3 entries.
let values = ["foo": 500, "bar": 600, "baz": 700]
集合
- 创建集合
- 参考语义
function main() {
let set = {1, 2, 3}
println("{}", set.contains(1))
println("{}", set.contains(5))
}
图元
- 创建图元
- 索引图元
- 元组类型
function main() {
let x = ("a", 2, true)
println("{}", x.1)
}
枚举和模式匹配
- 作为总和类型的枚举
- 通用的枚举
- 作为底层类型的值的名称的枚举
match表达式match武器中的枚举范围推断- 从匹配块中产生的值
- 嵌套的
match模式 - 作为
match模式的特质 - 支持与
?,??和!操作符的互操作。
enum MyOptional<T> {
Some(T)
None
}
function 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
)
}
function 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:
function do_nothing_in_particular() => match AlertDescription::CloseNotify {
CloseNotify => { ... }
UnexpectedMessage => { ... }
BadRecordMAC => { ... }
}
通用类型
- 泛型类型
- 泛型类型推理
- 特质
Jakt同时支持泛型结构和泛型函数。
function id<T>(anon x: T) -> T {
return x
}
function main() {
let y = id(3)
println("{}", y + 1000)
}
struct Foo<T> {
x: T
}
function main() {
let f = Foo(x: 100)
println("{}", f.x)
}
命名空间
- 对函数和结构/类/枚举的命名空间支持
- 深度命名空间支持
namespace Greeters {
function greet() {
println("Well, hello friends")
}
}
function main() {
Greeters::greet()
}
类型转换
在Jakt中,有两个内置的转换操作。
as? T:返回一个Optional<T>,如果源值不能转换为T,则为空。as! T:返回一个T,如果源值不能转换为T,则中止程序。
as cast可以做这些事情(注意,实现上可能还不一致)。
- 对同一类型的转换是无懈可击和毫无意义的,所以在未来可能会被禁止。
- 如果两个类型都是原始的,就会进行安全转换。
- 如果值超出了范围,整数投掷将失败。这意味着像i32->i64这样的推广转换是不可靠的。
- Float -> Integer casts truncate the decimal point (?)
- 整数->浮点数转换解决了与浮点数类型可表示的整数最接近的值(?)如果整数值过大,它们会解析为无穷大(?)
- 任何基元 -> bool 将为任何值创建
true,除了 0,这就是false。 - bool -> 任何基元都会做
false -> 0和true -> 1,甚至对浮点数也是如此。
- 如果类型是两个不同的指针类型(见上文),那么转换基本上是不可行的。转移到
T将按预期增加引用计数;这是从弱引用创建强引用的首选方法。从和raw T,是不安全的。 - 如果类型是同一类型层次的一部分(即,一个是另一个的子类型)。
- 一个子代可以无误地被投到它的父代。
- 父类可以投给子类,但这将在运行时检查类型,如果对象不属于子类或其子类型之一,则会失败。
- 如果类型不兼容,将尝试使用一个用户定义的投射。这里的细节还没有决定。
- 如果没有任何作用,那么这个转换甚至不会被编译。
在标准库中还有其他的投射。两个重要的是as_saturated 和as_truncated ,它们分别在饱和到边界或截断位的情况下投掷积分值。
特质
(尚未实现)
为了使泛型更加强大和富有表现力,你可以给它们添加额外的信息。
trait Hashable {
function hash(self) -> i128
}
class Foo implements Hashable {
function hash(self) => 42
}
type i64 implements Hashable {
function hash(self) => 100
}
我们的意图是,泛型使用特质来限制传入泛型参数的内容,同时也在主体中赋予该变量更多的能力。它并不打算用来做vtable类型的事情(为此,只需使用子类)。
安全分析
(还没有实现)
为了保证事情的安全,我们想做一些分析(并非详尽无遗)。
- 防止会相互碰撞的方法调用的重叠。例如,在一个容器上创建一个迭代器,并在其运行时调整容器的大小。
- 使用和操作原始指针
- 调用可能有副作用的C代码
错误处理
那些可能出错而不是正常返回的函数用throws 关键字来标记。
function task_that_might_fail() throws -> usize {
if problem {
throw Error::from_errno(EPROBLEM)
}
...
return result
}
function 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++代码有更好的互操作性,以及在unsafe 块内的Jakt能力不够强大的情况下,可以将内联C++代码以cpp 块的形式嵌入到程序中。
mut x = 0
unsafe {
cpp {
"x = (i64)&x;"
}
}
println("{}", x)
引用
在某些情况下,值和对象可以通过引用来传递,而这样做是可以证明是安全的。
一个引用要么是不可变的(默认),要么是可变的。
引用类型的语法
&T是对一个类型为 的值的不可变的引用。T&mut T是对一个类型为 的值的可变引用。T
引用表达式语法
&foo为变量 ,创建一个不可变的引用。foo&mut foo创建对变量的可变引用 。foo
解除对一个引用的引用
为了从引用中 "获取值",必须使用* 操作符解除引用,但是如果解除引用是对引用的唯一明确的正确使用,编译器将自动解除引用(实际上,只有在引用被存储或传递给函数时才需要手动解除引用)。
function sum(a: &i64, b: &i64) -> i64 {
return a + b
// Or with manual dereferencing:
return *a + *b
}
function test() {
let a = 1
let b = 2
let c = sum(&a, &b)
}
对于结构体的可变引用,你需要用圆括号包住解除引用,以便进行字段访问。
struct Foo {
x: i64
}
function zero_out(foo: &mut Foo) {
foo.x = 0
// Or with manual dereferencing:
(*foo).x = 0
}
引用(第一版)特征列表。
- 引用类型
- 引用函数参数
- 没有引用局部
- 结构体中没有引用
- 在返回类型中没有引用
- 不允许对不可变的值进行可变的引用
- 允许
&foo和&mut foo,而不对命名的参数进行参数标签。foo - 在适当的地方自动引用引用
引用TODO。
- (
unsafe) 引用和原始指针可双向转换 - 在持久化闭包中不允许通过引用捕获
闭包(第一版)特征列表。
- 函数作为函数的参数
- 函数作为变量
- 没有从函数返回的函数
- 可以抛出Lambdas
- 明确的抓取
闭包 TODO:
- [] 从函数返回函数
编译时间执行
Jakt中的Compiletime Function Execution(或CTFE)允许在compiletime执行任何jakt函数,条件是结果值可以用它的字段来合成--目前这只不允许一些不能用字段构造的前奏对象(如Iterator对象和StringBuilders)。
任何常规的Jakt函数都可以通过将其声明中的function 关键字替换为comptime 关键字而变成一个编译时函数,这将迫使对该特定函数的所有调用在编译时被评估。
调用限制
Comptime函数只能被常量表达式调用;这个限制包括方法的this 对象。
在comptime上下文中抛出
抛出的行为与普通错误控制流的行为相同,如果错误离开了comptime上下文(通过到达原始调用点),它将被提升为一个compiliation错误。
侧面效应
目前,所有带有副作用的前奏函数的行为与它们在运行时的行为相同。这允许例如将文件拉入二进制文件;一些函数可能会在以后被改变,以执行更有用的操作。
comptime TODO
- 实现所有Jakt表达式的执行