跳到正文
cajulang

语言规范

Caju 的工作方式:语法、类型、语句、函数、错误与模块。示例均用 0.5.0 版本编译,展示的是真实输出。

01概览

Caju 是一门编译型编程语言,采用静态类型并支持类型推断。编译器生成 x86-64 机器码:Linux 上为 ELF 可执行文件,Windows 上为 PE。它还能生成可在 Node 和浏览器中运行的 WebAssembly 模块。编译器和运行时都用 Caju 本身编写。

语言自带开发所需的一切:编译器、测试运行器(caju test)和包管理器(caju pkg),统一在一个 caju 命令中。

一个程序由一个或多个 .caju 文件组成。文件顶层的语句按从上到下的顺序执行:无需声明 main 函数。

hello.caju
fn greet(name: str) -> str {
return "Hello, " + name + "!"
}
print(greet("world"))
$ caju run hello.caju
Hello, world!

02词法元素

注释。以 # 或 // 开头,直到行尾。

语句结束。换行或 ; 结束一条语句。表达式不能在运算符之后延续到下一行:请把它拆成几个变量。

标识符。由字母(包括带变音符的拉丁字母,如 é 和 ç)、数字和 _ 组成,不能以数字开头。

关键字。不能用作名称:

letconstifelsewhilefortoandornottruefalseimportfromexportfnreturnexterndllexportstructinterfacenilswitchcasedefaultbreakcontinuefallthroughdefergoselectchanmaprange

字面量。

类型示例说明
整数42 1_000 0xFF 0b1010 0o17_ 用于分隔数字
浮点数3.14 1.5e364 位
字符串"你好\n"UTF-8;转义符 \n \t \r \0 \\ \" \'、\xHH、\uHHHH、\UHHHHHHHH
原始字符串`c:\folder\n`用反引号括起,不处理转义
字符'a'字符的码值,为整数
布尔值与空值true false nil
literals.caju
a := 1_000; b := 0xFF; c := 0b1010; d := 0o17
print(a, b, c, d, 1.5e3)
print("tab\tquote\" \u00e9 \x41", `raw\n`, 'a')
$ caju run literals.caju
1000 255 10 15 1500
tab quote" é A raw\n 97

03类型

声明中的类型是可选的:不标注时由编译器推断;标注后,赋予其他类型的值会导致编译错误。

类型说明
int64 位有符号整数
int8 int16 int32 int64固定宽度的有符号整数
uint uint8 uint16 uint32 uint64无符号整数
num64 位浮点数,可接受 int 值
float3232 位浮点数
strUTF-8 文本。s[i] 是 uint8,len(s) 计算字节数
booltrue 或 false
any任意值。用类型断言或类型 switch 取回原类型
error带有 Error() -> str 方法的接口

别名。byte = uint8,rune = int32,string = str,float64 = num。

复合类型形式行为
数组[3]int长度固定;赋值时复制
切片[]int数组上的视图;用 append 增长
映射map[str]int键值对;键为基本类型
指针*T&x 取地址,*p 取值
函数fn(int) -> int函数是值
结构体struct Point { x: int, y: int }带命名字段的值
接口interface Shape { area() -> num }方法的集合

零值。未赋值的变量以其类型的零值开始:0、""、false、nil,或所有字段均为零值的结构体。

04变量与常量

形式效果
x := 10声明并根据值推断类型(此处为 int)
let x: int = 10以显式类型声明
let x: int声明为该类型的零值
let x = 0不标注类型时,let 配合数字会推断为 num,以便之后接受小数
const N = 3常量;必须有值
变量的作用域是整个函数,而不只是所在的代码块。不要在同一函数中以不同类型声明同名变量。
types.caju
x := 10
x = "ten"
$ caju run types.caju
Type error in types.caju, line 2: "x" (line 1) is int, but got str.

05表达式与运算符

类别运算符
算术+ - * / % **(幂)
位运算& | ^ &^ << >>
比较== != < <= > >=
逻辑&& 或 and,|| 或 or,! 或 not
赋值= := += -= *= /= %= &= |= ^= <<= >>= &^=
自增自减x++ x--
指针&x(取地址)和 *p(取所指的值)

+ 也用于拼接字符串。没有 ~ 运算符:要按位取反,请用 x ^ -1。

类型转换写成函数调用的形式:int(x)、num(x)、str(x)、uint8(x)、float32(x)、byte(x)、rune(x) 等。

ops.caju
print(2 ** 10, 7 % 3, 6 &^ 3, 1 << 4, -8 >> 1)
print(true and not false, !true && false)
print(str(3.5), int(3.9), num(2))
$ caju run ops.caju
1024 1 4 16 -4
true false
3.5 3 2

06控制流

if 可以在条件前加一条语句,用 ; 分隔。条件两侧不加括号。

循环。for 有四种形式,另外还有 while:

形式示例
经典形式for i := 0; i < n; i++ { }
仅条件for i < n { } 或 while i < n { }
区间for i = 1 to 10 { }(包含两端)
rangefor i, v := range list { } 和 for k, v := range m { }

break 和 continue 可以带标签,用于跳出或继续外层循环。

switch 依次与每个 case 比较,一个 case 可以列出多个值。各分支不会自动进入下一个分支,除非以 fallthrough 结尾。

flow.caju
for j = 1 to 2 { print("j", j) }
outer: for p := 0; p < 3; p++ {
for q := 0; q < 3; q++ {
if q == 1 { continue outer }
if p == 2 { break outer }
print(p, q)
}
}
i := 3
switch i {
case 1, 2: print("small")
case 3:
print("three")
fallthrough
default: print("default")
}
$ caju run flow.caju
j 1
j 2
0 0
1 0
three
default

07函数

函数在文件顶层用 fn 声明。每个参数都有类型。返回类型写在 -> 之后,省略时根据 return 推断。

函数可以返回多个值,如 -> (int, error),返回值也可以命名:单独的 return 会返回它们的当前值。

在函数内部,可以用 fn(...) { } 创建函数值。它们会捕获外部变量(闭包)。

functions.caju
fn split(n: int) -> (q: int, r: int) {
q = n / 10
r = n % 10
return
}
print(split(47))
add := fn(a: int, b: int) -> int { return a + b }
print(add(2, 3))
$ caju run functions.caju
4 7
5
声明的函数不支持可变数量的参数。... 用于在调用时展开列表,如 append(a, b...)。

08结构体、方法与接口

结构体把带类型的字段组合在一起。字面量可以按顺序给值,如 Point{1, 2},也可以按名称给值,如 Point{y: 2}。未给出的字段取零值。

方法是带接收者的函数:fn (p: Point) dist() 接收一份副本,而 fn (p: *Point) move() 接收原值并可以修改它。

嵌入:把一个结构体嵌入另一个结构体,外层结构体就获得内层的字段和方法。

接口是隐式实现的:一个类型只要拥有接口的方法,就实现了该接口。接口可以嵌入其他接口。x.(T) 取回存放的类型;使用 v, ok := x.(T) 时不会引发 panic。

interfaces.caju
interface Named { name() -> str }
interface Greeter { Named; greet() -> str }
struct P { n: str }
fn (p: P) name() -> str { return p.n }
fn (p: P) greet() -> str { return "hi " + p.n }
gs := []Greeter{P{"ana"}}
print(gs[0].greet(), gs[0].name())
$ caju run interfaces.caju
hi ana ana

09错误、defer 与 panic

错误是 error 类型的值。可以用 errors.New("message") 创建,也可以用任何带有 Error() -> str 方法的类型。可能失败的函数把错误作为最后一个返回值。

defer 安排一个调用在函数结束时执行;这些调用按从后到前的顺序运行。panic 中断执行并逐层回退调用;在 defer 中调用 recover 可以止住 panic。运行时错误(如索引越界)同样会引发 panic。

errors.caju
struct NotFound { name: str }
fn (e: *NotFound) Error() -> str { return e.name + " not found" }
fn find(name: str) -> error {
if name != "caju" { return &NotFound{name} }
return nil
}
err := find("mango")
if err != nil { print(err.Error()) }
$ caju run errors.caju
mango not found

10内置函数

函数用途
print(a, b, ...)打印以空格分隔的值并换行
len(x) 和 cap(x)字符串、数组、切片和映射的长度与容量
append(s, v...)向切片追加值并返回该切片
copy(dst, src)复制元素并返回复制的数量
delete(m, k)从映射中删除一个键
make(T, n, c)创建切片和映射
new(T)分配一个零值并返回指向它的指针
panic(v) 和 recover()中断执行,以及在 defer 中恢复
errors.New(msg)用给定消息创建错误

11模块与包

每个文件都是一个模块。只有用 export 声明的名称才能被其他文件使用。

导入方式效果
import "std/strings"标准库,以 strings.split(...) 的形式使用
import filepath "std/path"以别名导入
import { split, join } from "std/strings"导入名称以便直接使用
import "./mods/geo"项目中的其他文件,相对于当前文件
import "github.com/owner/package"第三方包,用 caju pkg add 安装
mods/geo.caju
# mods/geo.caju
export struct Point { x: int, y: int }
export fn origin() -> Point { return Point{} }
fn hidden() -> int { return 1 }
main.caju
import "./mods/geo"
print(geo.origin())
print(geo.hidden())
$ caju run main.caju
Link error in main.caju, line 4: "hidden" exists in ./mods/geo.caju,
but it is not exported. Write export before its declaration.

12包管理器

Caju 自带包管理器 caju pkg。没有中央服务器:一个包就是根目录下有 caju.toml 文件的 GitHub 仓库。包名就是仓库路径,程序导入的也正是这个名字。

caju.toml
[package]
name = "github.com/acme/strutil"
version = "1.1.0"
description = "Text helpers"
license = "MIT"
[dependencies]
"github.com/acme/other" = "1.0.0"

版本。已发布的版本是格式为 vX.Y.Z 的 git tag,其编号须与 caju.toml 中的 version 一致。两者不符时,包管理器会拒绝该包。

版本选择。每个依赖注明自己所需的最低版本。当两个包对同一个包要求不同版本时,取各最低版本中最高的那个。因此,只有当某个 caju.toml 要求更高版本时结果才会改变,而不会因为有人发布了新版本而改变。MAJOR 版本不同(如 1.x 与 2.x)的版本互不兼容。

版本锁定。caju.lock 为每个直接或间接使用的包记录版本、提交和文件的 SHA-256 哈希。请把它提交到仓库:这样所有人编译的都是完全相同的文件。

校验。编译前,caju build 和 caju run 会检查 caju.lock 是否与 caju.toml 同步,以及每个包的文件是否与锁定的哈希一致。任何差异都会导致编译中止:

app.caju
import "github.com/acme/strutil"
print(strutil.shout("caju"))
$ caju run app.caju # depois de alterar um arquivo do pacote
Package error: checksum mismatch for github.com/acme/strutil@1.1.0
caju.lock: sha256:aa012c96e037f3527d565bae3fbcb84c459070d694f2276e8149582ced94ee9a
files: sha256:67e6d47f83afb261e6971681c489a422c30f146d8fac2adf0732a7dba5f62756
命令作用
caju pkg init [name]创建项目的 caju.toml
caju pkg add name[@version]添加依赖(默认为最新发布的版本)
caju pkg remove name移除依赖
caju pkg update [name]将依赖更新到最新发布的版本
caju pkg install下载缺失的内容,并按 caju.lock 校验全部文件
caju pkg verify用 lock 中的哈希校验缓存中的包
caju pkg tree显示依赖树
caju pkg cache list|verify|clean本机已下载的包,所有项目共享,每次复用前都会校验哈希
caju pkg hash [dir]计算包目录的哈希

存放位置。包通过 git(按版本 tag)下载到 ~/.caju/pkg 缓存中。add、remove 和 update 只修改 caju.toml 的 [dependencies] 部分,不会改动文件的其他内容。

在包内部,文件之间按完整路径导入,如 import "github.com/acme/strutil/extra";一个包只应导入其 [dependencies] 中列出的内容。

13Windows 系统调用

extern 声明 Windows DLL 中的函数,调用约定为 stdcall(默认)或 cdecl。参数使用接口类型:i32、u32、i64、u64、f32、f64、ptr、bool、str 和 wstr,返回类型可以是 void。这类调用只存在于用 --target windows 编译的程序中。

message.caju
extern "user32.dll" fn MessageBoxA(hwnd: ptr, text: str, title: str, flags: u32) -> i32
MessageBoxA(0, "Hello from Caju", "caju", 0)

14编译与工具

命令作用
caju run file.caju在内存中编译并执行
caju build -o app file.caju生成 Linux 可执行文件(ELF)
caju build --target windows file.caju生成 Windows .exe(PE)
caju build --target wasm -o app.wasm file.caju生成 WebAssembly 模块(可在 Node 或浏览器中运行)
caju test运行 _test.caju 文件
caju pkg ...包管理器(见上一节)
caju version显示版本

15开发中

  • 原生编译器中的 goroutine、通道和 select。go、chan 和 select 已是保留字。
  • Windows 上的 alloc、peek、poke、readstr、dllexport 和原生 DLL。
  • 以结构体、数组或 any 为键的映射。