探索 Jakt 编程语言

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

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

注意: 该语言正处于积极开发阶段。

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

使用方法

将代码转译为 C++ 需要 clang。 请确保已安装。

jakt file.jakt
./build/file

构建

请参考这里

目标

  1. 内存安全
  2. 代码可读性
  3. 开发者生产力
  4. 可执行文件性能
  5. 乐趣!

内存安全

以下策略用于实现内存安全:

Jakt 中,有三种指针类型:

在安全模式下,空指针是不可能的,但指针可以包装在 Optional 中,例如 Optional<T> 或简写为 T?

数学安全

如果需要静默整数溢出,可以使用显式函数来提供此功能。

代码可读性

阅读代码所花费的时间比编写代码的时间要多得多。 因此,Jakt 非常重视可读性。

以下是一些鼓励编写更具可读性程序的特性:

代码重用

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)
  1. 从与文件相同的目录导入模块。
  2. 仅从模块 a 导入 use_cool_things()
  3. 导入可以在编译时计算。 参见 Comptime Imports
  4. 当模块是包含该文件的目录的子路径时,使用 relative 关键字导入模块。
  5. 从包含该文件的目录的上级目录导入模块。
  6. 从包含该文件的目录的上三级父路径导入模块的语法糖。

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)

对此有两个例外:

结构体和类

Jakt 中,有两种声明结构的主要方式:structclass

struct

基本语法:

struct Point {
  x: i64
  y: i64
}

Jakt 中的结构体具有 值语义

let a = Point(x: 10, y: 5)
let b = a
// "b" 是 "a" 的深拷贝,它们不指向同一个 Point

Jakt 为结构体生成默认构造函数。 它通过名称获取所有字段。 对于上面的 Point 结构体,它看起来像这样:

Point(x: i64, y: i64)

结构体成员默认为 public

class

struct 相同的基本语法:

class Size {
  width: i64
  height: i64
  public fn area(this) => .width * .height
}

Jakt 中的类具有 引用语义

类成员默认为 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<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)
}

枚举和模式匹配

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 => { ... }
}

泛型

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]
}

命名空间

namespace Greeters {
  fn greet() {
    println("Well, hello friends")
  }
}
fn main() {
  Greeters::greet()
}

类型转换

Jakt 中有两个内置的转换运算符。

as 转换可以执行以下操作(请注意,实现可能尚未达成一致):

标准库中提供了其他转换。 两个重要的转换是 as_saturatedas_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 | 逻辑非 | 未确定 = | 赋值 | 未确定

安全性分析

(尚未实施)

为了保证安全,我们需要做一些分析(非详尽):

错误处理

可以使用 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++ 代码互操作,以及 Jaktunsafe 块中的功能不够强大的情况,可以以 cpp 块的形式将内联 C++ 代码嵌入到程序中:

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

引用

在某些情况下,当证明这样做是安全的时候,值和对象可以通过引用传递。

引用是不可变的(默认)或可变的。

引用类型语法

引用表达式语法

解引用引用

要“取出”引用的值,必须使用 * 运算符解引用它,但是,如果解引用是引用唯一明确正确的用法,则编译器将自动解引用引用(实际上,只有在引用被存储或传递给函数时才需要手动解引用)。

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
}

引用(第一版)功能列表:

引用 TODO:

闭包(第一版)功能列表:

闭包 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

关于

The Jakt Programming Language

Topics

programming-language serenityos jakt

Resources

Readme

License

BSD-2-Clause license

Code of conduct

Code of conduct Activity Custom properties

Stars

2.9k stars

Watchers

38 watching

Forks

241 forks Report repository

Releases

No releases published

Packages 0

No packages published

Contributors 112

+ 98 contributors

Languages