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 下载
180KB
4.5K SLoC
Y 语言
为什么有人要构建一个(相对较新且非常实验性)的新语言,而它没有实际的应用场景。
设计
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提供了与类似数组结构交互的不同方式:TupleArray
和 ArraySlice
。
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
,但反之则不行。类型为 char
的 ArraySlice
和 TupleArray
可以转换为 str
(您 必须确保最后一个字节是 0
)。最后但并非最不重要的是,str
可以转换为类型为 char
的 ArraySlice
。
模块
您可以将代码拆分成模块。模块只是以 .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