#编程语言 #编译器 #解释器 #实验性 #模块 #变量 #而不是

bin+lib y-lang

Y 编程语言(相对较新且非常实验性)的编译器和解释器

4 个版本

0.1.3 2023 年 5 月 1 日
0.1.2 2023 年 3 月 13 日
0.1.1 2023 年 2 月 26 日
0.1.0 2023 年 2 月 23 日

#250编程语言

每月 22 下载

GPL-3.0 许可证

180KB
4.5K SLoC

Y 语言

Crates.io CI Checks Crates.io GitHub issues

为什么有人要构建一个(相对较新且非常实验性)的新语言,而它没有实际的应用场景。

设计

Y(发音为“why”)基于这样的理念:一切都是表达式,并评估为一个值。例如,7 3 + 4 直接评估为 7(后者以二进制表达式的形式进行评估),3 > 5 评估为 false,而 "Hello" 评估为 "Hello"

除了这些“原始”表达式之外,更复杂的表达式,如代码块(即用 {} 括起来的语句)、if 语句和函数(调用)也评估为一个值。为了简化这种做法,这些复杂“结构”中最后一个表达式的值自动是该结构的值。

例如

{
    "Hello, World"
    3 + 4
}

评估为 7。在这个例子中,"Hello, World" 被忽略,因为最后一个表达式不依赖于它。另一个例子

if 3 < 5 {
    "foo"
} else {
    "bar"
}

您可以选择显式地使用分号结束表达式

if 3 < 5 {
    "foo"
} else {
    "bar"
};

在某些情况下,这是必需的,因为 Y 会以不同的方式解释提供的表达式。请参阅有关函数的部分。

变量

为了存储表达式的值,您可以声明变量

let foo := "bar"

变量定义始终以关键字 let 开始,后跟一个标识符(例如,foo)和“walrus 操作符” := 以及值。要为变量分配新值,可以使用类似的模式

foo = "another value"

请注意,在这种情况下,您既不使用关键字 let,也不使用 :=

遵循“一切求值都返回一个值”的想法,您可以将复杂结构(代码块、函数、函数调用、if 语句等)分配给变量

let foo := {
    let a := 16 // Yes, variable definitions also work in blocks
    a + 26
}

let some_variable := if foo > 30 {
    "foo"
} else {
    "bar"
}

类型系统

Y 是强类型的。这意味着您不能将一个变量赋值给与之前类型不同的新值。也就是说,以下是不正确的

let foo := "bar"
foo = 42 // TypeError!

因此,如果您将 if 语句分配给变量,则两个代码块都必须返回相同类型的值

// works
let foo := if a > b {
    42
} else {
    1337
}

// TypeError!
let bar := if a > b {
    42
} else {
    "bar"
}

原语

Y 支持一些原语类型,这些类型直接构建在语言中

  • int 用于数字(目前为 64 位)
  • char 用于字符(8 位值,因此,可以使用小的 int
  • str 用于字符串 常量
  • bool 用于布尔值
  • void 用于“空”值
  • 函数(有关如何声明函数类型的更多信息,请参阅后面的内容)

更复杂的数据类型将是未来功能的主题。

可变性

目前,Y 只允许在当前作用域内(即在当前代码块中)定义的变量进行可变。您仍然可以访问在外部作用域中定义的变量(只写访问)

let foo := 42

if a > b {
    let bar := foo // works, because it is read-only
    bar = 1337
} else {
    foo = 1337 // TypeError!
}

控制流

Y 支持不同类型的控制流语句。

循环

如果您想多次重复执行指令,您可以在循环中打包它们。目前,只有一种循环类型:例如,while 循环

let mut x := 0
while x < 5 {
    doSomething()
    x = x + 1
}

while 循环的头部必须包含一个求值为布尔值的表达式,而循环体可以包含任何内容。因此,以下结构是有效的 Y

while {
    let foo := bar()
    foo < 5
} {
    doSomething()
}

注意: 默认情况下,Y 中的循环求值为 void 类型。使用循环的返回值是未定义的行为。

函数

您可以在函数中封装行为。函数是 Y 中(目前)唯一需要显式注释类型的地方(对于参数和返回类型)

let add := (x : int, y : int) : int => {
    x + y
}

函数定义与常规变量定义类似,因为函数在 Y 中被视为一等公民。

调用后缀

要调用函数,您可以后缀任何求值为函数的表达式(目前仅为标识符)并使用 ([param, [param, ...]]) 来调用它并传递给定的参数。

这可能会导致某些陷阱,因为 Y 不是空白敏感的!例如,以下会导致类型错误

let foo := (some_condition: bool) : int => {
    if some_condition {
        print("Condition is true!")
    }
    (3 + 4)
}

这里,(3 + 4)(尽管它被用作函数的返回表达式)被解释为对 if 表达式结果的调用。为了避免这种情况,您必须显式使用分号终止 if 表达式

let foo := (some_condition: bool) : int => {
    if some_condition {
        print("Condition is true!")
    }; // <- semicolon to terminate expression
    (3 + 4)
}

函数类型

如果您想声明函数的参数本身是一个函数,可以这样做

let foo := (bar : (int, int) -> int) : int => {
    bar(3, 4)
}

在这个例子中,我们声明了一个变量 foo 并将其赋值为一个函数,该函数期望一个参数(在这个例子中名为 bar),参数类型为 (int, int) -> int,这意味着提供的函数应该接受两个类型为 int 的参数,并产生/返回一个类型为 int 的值。

⚠️ 已知限制

目前,您无法从其他函数返回函数或使用在函数外部作用域中定义的值。我正在寻找实现这一目标的方法。

数组与索引

Y提供了与类似数组结构交互的不同方式:TupleArrayArraySlice

TupleArray

TupleArray 是来自其他语言的标准数组类型。包含的元素长度和类型需要在编译时已知

// this creates an array of 10 integers, filled with all 0
let foo := [0; 10]

与上述情况相对应,您可以为此定义一个类型

let bar := (some_array: [int; 10]): void => { ... }

通过提供索引来访问数组中的元素

// get the value at index 5 (i.e., the 6th position)
let a := some_array[5]

// the the value at index 3
some_array[3] = 42
ArraySlice

另一方面,ArraySlice 表示一个大小未定义(或未知)的数组。因此,您不能直接定义一个,但可以将它指定为函数参数的类型

let baz := (some_slice: &[int]): void => { ... }

索引工作方式与 TupleArray 相同。

注意: Y(在撰写本文时)不会执行任何可靠的边界检查。

字符串索引

在Y中,字符串和数组(在一定程度上)可以相互转换。您可以用与数组相同的方式索引字符串

let foo := "Hello, World!"
foo[2] = 'n'
print(foo) // "Henlo, World!"
类型转换

一些类型可以转换为其他类型。例如,一个 TupleArray 可以转换为 ArraySlice,但反之则不行。类型为 charArraySliceTupleArray 可以转换为 str 必须确保最后一个字节是 0)。最后但并非最不重要的是,str 可以转换为类型为 charArraySlice

模块

您可以将代码拆分成模块。模块只是以 .why 结尾的其他文件,并且可以通过其名称(不带相应的文件扩展名)导入

import foo

foo::bar()

在这里,我们导入一个名为 foo 的模块(来自文件 foo.why),并通过其“全名”调用一个导出的函数(即 bar)。默认情况下,您必须以 module::function() 的形式指定导入函数的完整解析名称。

您还可以从其他目录导入模块

import some::dir::foo

some::dir::foo::bar()

如果您想直接调用一个函数而不指定模块名称,您必须将模块作为通配符导入

import foo::*

bar()

这在导入来自实用模块的常用函数时很有用。

导入是递归遍历的。因此,如果您导入模块 foo,它导入模块 bar,则这两个模块都会被解析、类型检查和编译。但是,如果您想在根模块中使用模块 bar,您也必须在根模块中导入它。为了避免模块的双倍解析和检查,加载器会跟踪已加载的模块并仅引用它们(如果已存在)。

⚠️ 非函数导出

请注意,模块中所有非功能成员(即所有其他变量等)都不会被导出。它们将从程序中“完全删除”。因此,您导出的函数不允许使用除其他导出函数之外的任何其他变量。

将来,我们计划添加对导出常量的支持,但在此之前,请注意这个限制。

声明

如果您想声明一个已经预定义的函数(或变量),您可以通过declare关键字来做到。声明由要声明的变量的名称和相应的类型注解组成。例如:

declare print : (str) -> void

内置函数

目前,Y提供了一个内置函数:syscall_4(用于调用具有4个参数的系统调用)。要使用它,您必须在程序中的某个位置声明它

declare syscall_4 : (int, any, any, any) -> any

注意:第一个参数是此系统调用的标识符。

如果您想了解当前可用的系统调用抽象的概述,请查看示例文件夹中的std.why

编译器指令

Y支持(或多或少)根据当前操作系统进行条件编译。要声明某事是“OS”相关的,您必须相应地注解它

#[os == "linux"]
let value := "We are on linux"

#[os == "macos"]
let value := "We are on macOS"

管道

要将Y程序转换为可执行文件(或解释它),编译器需要执行多个步骤。

解析

作为第一步,解析器试图从给定的源代码生成一个更有意义的AST。虽然解析器依赖于由y-lang.pest定义的语法,但生成的AST在结构上更加具体。

类型检查器

为了提供强类型的安全性,类型检查器检查所有表达式、变量和赋值的类型。此外,它检查变量是否在当前作用域中定义,以及它们是否可变(如果需要的话)。

解释器 & 编译器

作为最后一步,生成的AST要么被解释,要么被编译成汇编。然后使用NASM将生成的汇编编译成目标文件,然后通过cc进行链接。

使用方法

在撰写本文时,我们不提供Y的二进制文件。如果您想使用或实验y,您可以自己编译工具链。为此,您需要在系统上安装rust和cargo。如果您想实际编译程序,您还需要安装NASM。此包提供了一个名为why的二进制文件。

您可以使用why来检查类型、解释和编译您的程序

why path/to/program.why # typechecking

why path/to/program.why -r # typecheck & run/interpret

why path/to/program.why -o path/to/output # typecheck and compile

操作系统

Y在macOS下积极开发。我在某些方面测试了Linux(CI也应该测试),但不能保证完全兼容。

贡献

代码是强大的意大利面的化身。我没有真正的时间来重构任何东西,甚至编写有用的测试。

尽管我现在没有贡献指南,但请随时打开带有功能请求的问题。请注意,我可能不会接受任何PR,直到我定义了一些贡献或代码/汇编风格的指南。

依赖关系

~3–13MB
~128K SLoC