SerenityOS / jakt 基本使用方法和特点分析

545 阅读11分钟

Jakt编程语言

Jakt是一种内存安全的系统编程语言。

目前它可以转译为C++。

注意: 该语言正在大力开发中。

注意: 如果你要克隆到Windows PC上(不是WSL),请确保你的Git客户端保持行尾为\n 。你可以通过git config --global core.autocrlf false ,将其设置为全局配置。

使用方法

转译成C++语言需要clang 。请确保你已经安装了该软件。

jakt file.jakt
./build/file

目标

  1. 内存安全
  2. 代码的可读性
  3. 开发者的生产力
  4. 可执行的性能
  5. 好玩!

内存安全

为了实现内存安全,我们采用了以下策略。

  • 自动引用计数
  • 强类型化
  • 界限检查
  • 在安全模式下没有原始指针

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中,有两种主要的方法来声明结构:structclass

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 -> 0true -> 1 ,甚至对浮点数也是如此。
  • 如果类型是两个不同的指针类型(见上文),那么转换基本上是不可行的。转移到T 将按预期增加引用计数;这是从弱引用创建强引用的首选方法。从和raw T ,是不安全的。
  • 如果类型是同一类型层次的一部分(即,一个是另一个的子类型)。
    • 一个子代可以无误地被投到它的父代。
    • 父类可以投给子类,但这将在运行时检查类型,如果对象不属于子类或其子类型之一,则会失败。
  • 如果类型不兼容,将尝试使用一个用户定义的投射。这里的细节还没有决定。
  • 如果没有任何作用,那么这个转换甚至不会被编译。

在标准库中还有其他的投射。两个重要的是as_saturatedas_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表达式的执行