跳到主要内容

語法

註釋​

Nolang 支援三種單行註釋標記與一種多行(塊)註釋標記:

  • // —— 傳統單行標記(註釋到行尾)
  • ; —— 單行標記(註釋到行尾)
  • ;; <內容> —— 單行標記(;; 後同一行還有內容時,註釋到行尾,語義等同 ;)
  • ;;\n —— 多行(塊)註釋:;; 後緊跟換行(僅允許空白)時觸發多行模式,直到遇到同樣後跟換行/EOF 的 ;; 結束
; 這是註釋
; 這也是註釋,語義相同
;; 這還是單行註釋(;; 後沒有換行)
x = 1 ; 行內註釋,註釋到行尾
x = 2 ;; 行內單行註釋,語義相同

;;
這是多行(塊)註釋
可以跨多行
直到遇到單獨的 ';;'
;;

y = 3
;;
結束 ;; 後必須換行或文件尾
才會被視為結束定界符
;;

多行觸發規則: ;; 後只能有空白(空格/制表符)直到換行或 EOF,才會進入多行模式。若 ;; 後在同一行還有任何非空白字符,則視為單行註釋(註釋到行尾)。

多行結束規則: 結束的 ;; 同樣必須後跟換行或 EOF(中間僅允許空白)。未閉合的多行註釋會一直註釋到文件尾。

規則:每行一條語句,禁止使用逗號 , 將多條語句寫在同一行。(分號 ; 現在是註釋標記,不再能連接語句。) 這條規則同樣適用於註釋中的程式碼範例。即使在註釋裡,也不應該用逗號把多條語句放在同一行,以免給讀者造成困惑。

; ❌ 錯誤:註釋中使用逗號合併多條語句 ; h0 = 1732584193, h1 = 4023233417

; ❌ 錯誤:使用逗號合併多條語句 ; out = from-i64(v), out = from-u64(v) ; debug(msg), info(msg), warn(msg)

; ✅ 正確:每條語句獨立一行 ; h0 = 1732584193 ; h1 = 4023233417 ; out = from-i64(v) ; out = from-u64(v) ; debug(msg) ; info(msg) ; warn(msg)


## 數據類型

基礎類型

- byte
- bool ; 只允許小寫
- char ; 字符類型:單個 Unicode 標量碼點(rune),底層存儲 i32。雙引號包裹單字符,如 "中"
- str ; 字符串類型,單引號包裹 'hello',或反引號原始字符串 `多行內容`
- txt ; 定長文本類型,底層固定 256 字節(最多 255 字節數據),必須顯式類型標注,如 t txt = 'abc'
- i8
- i16
- i32
- i64 ; 數字默認類型,不區分架構
- i128 ; 128位有符號整數
- u8
- u16
- u32
- u64
- u128 ; 128位無符號整數
- f32
- f64

### char 類型(Unicode 碼點)

`char` 代表**單個 Unicode 純量碼點**(scalar value)。它的底層存儲類型是 **`i32`**,不是 `i64`:

- **合法範圍**:`0 ..= 0x10FFFF`(即 `0 ..= 1114111`)。碼點最大值只需 21 bit,`i32` 完全放得下。
- **越界報錯**:字面量超出該範圍時,編譯期(含 `no vet`)**直接報錯**。例如 `c char = 0x110000` ✗、`c char = 1114112` ✗;`c char = 1114111` ✓。
- **允許數值運算**:`char` **可以直接參與整數運算**,例如 `z = a + 25`、`u = ch - 32`(`ch` 為 `for ch <- s` 的迭代變數)都是**正常寫法**,按碼點值加減。運算一律按 **i32 寬度**進行,結果視為普通整數值;正因如此,`char` 的 `+ - * /` **不像整數家族那樣預設返回 `option<int>`**,不會觸發「未處理整數溢出」的編譯錯誤。要注意的只有範圍:碼點最大 `0x10FFFF`,超出 `i32`/碼點語義的場合(如做偏移量、累加計數)建議先顯式轉成 `i32`/`i64` 再算,讓意圖更清楚。
- **與 `str` 的關係**:`s[i]` 回傳 `char`;`char` 可隱式轉為 `str`(UTF-8 編碼),詳見 [字串 — 隱式轉換](str.md)。

> **「底層類型」與「調用轉換」是兩回事。** `char` 自身就是 `i32`。內置函數 `str_from_cp(cp char) -> ?str` 的**源碼簽名**參數是 `char`;只有在**調用外部 runtime** 時,編譯器才把該 i32 參數**零擴展**(`zext i32 → i64`)以匹配 `@str_from_cp` 的 IR 入參——這只是參數準備階段的臨時轉換,**不改變 `char` 的類型**。類似 C 的 `char c='a'; f((long)c);`:`c` 本身是 1 字節,調用時臨時提升。

容器類型

- obj ; 對象
- map ; 映射
- arr ; 定長數組
- vec ; 變長數組
- slice ; 切片(視圖)沒有獨立數據結構,必須依附於arr/vec/str/txt

- \* ; 指針 僅限 FFI `#{c}` 宣告與標準庫
- usize ; 僅用於ffi
- any ; 任意類型 僅限標準庫

高級類型

- bigint
- err

## 類型別名與聯合類型

類型別名為現有類型建立一個新名稱。使用等號語法 `name = type`,支援單一類型別名和多類型聯合。

### 語法

```no
; 聯合類型:多個類型用 | 分隔
int = i8 | i16 | i32 | i64 | i128 | u8 | u16 | u32 | u64 | u128
float = f32 | f64
num = int | float
string = str | txt

; 單一類型別名
bytes = []byte
buf = [16]u8

int、float、num 定義在 std/number,string 定義在 std/str——它們都不是編譯器內建關鍵字,而是標準庫用同一套別名語法寫的聯合類型。

聯合類型的鏈式引用​

聯合類型可以引用其他聯合類型,形成層次結構:

int = i8 | i16 | i32 | i64 | i128 | u8 | u16 | u32 | u64 | u128
float = f32 | f64
num = int | float ; num 是 int 和 float 的聯合
string = str | txt ; string 直接聯合兩個具體字符串類型

在函數中使用​

聯合類型可用於函數參數和返回值,編譯器會自動進行單態化(monomorphization),為每個成員類型生成獨立的函數版本:

; 參數類型為 num 聯合
max = (a ..num) (r num) {
r = a[0]
n = len(a)
i <- [1..n): {
a[i] > r -> r = a[i]
}
}

; 方法定義在聯合類型上
num.sign = () (r num) {
{
. > 0 -> r = 1
. < 0 -> r = -1
-> r = 0
}
}

string(= str | txt)同樣可用於參數與返回值:

; str 與 txt 實參都能傳入,各自單態化一份具體函數
take = (s string) (n i64) {
n = len(s)
}

a str = 'hello'
b txt = 'hi'
n1 = take(a) ; take__str
n2 = take(b) ; take__txt

聯合類型函數體的既有限制(num 與 string 相同):函數體內對 union 參數調用成員方法(如 s.len())尚不支持,需使用全域內建(len(s),字節數)或拆成具體類型的函數;聯合類型的變量聲明(s string = 'x')也尚不支持。另注意不要定義與成員方法同名的別名方法(如 string.len)——它會接管 s.len() 調用並在方法體中遞迴分发回自身。

偵測規則​

等號語法在以下情況下被識別為類型別名(而非變數賦值):

  • name = type | type | ...:聯合類型(含 |)
  • name = []type:切片類型
  • name = [N]type:陣列類型
  • name = ?type:可選類型
  • name = known-type:單一類型別名,其中 known-type 是內建類型名(如 i64、f64、bool、str 等)或先前已定義的類型別名名稱

變量聲明​


; 變量沒有關鍵字
; i64、f64、byte、bool、byte、str可以省略類型標注
i = 1

; f64 中間有.
f = 1.0

; byte
b = 0x00


; i8 如果變量名和類型一致,可以忽略類型標注
i8 = 3

; 顯式標註若與推斷型別相同則冗餘(no vet 報 tcpoxtfd,no fmt --fix=redundant 可自動去除)
x str = ''
x = '' ; 等價,標註冗餘

; 默認0值
; 變量定義不需要提前聲明
u16

; str 單引號包裹
name = 'nolang'

; bool true/false 全小寫
flag = true
flag = false

; bool 簡寫:!! 等價 true、! 等價 false(獨立字面量)
flag = !! ; true
flag = ! ; false

; 變量賦值
; 不允許同名,如果同名則視為修改變量
name = 'hello'
name = 'world'

; 字符串拼接
greeting = 'hello, ' - name

; 原始字符串(反引號包裹,多行,不轉義)
sql = `
SELECT id,name
FROM user
WHERE id > 100
`

; 顯式類型標注
a u64 = 10

; 十六進制字面量類型推斷
; - 十進制整數字面量(如 771)推斷為 i64(默認整數類型)
; - 十六進制字面量(如 0x0303)推斷為 byte(u8)
; - 如果 hex 值超過 byte 範圍(> 255),必須添加顯式類型標注:
PORT i64 = 0x0303 ; 正確:顯式 i64,值 = 771
; PORT = 0x0303 ; 錯誤:推斷為 byte,值被截斷!
; PORT = 771 ; 正確:十進制默認為 i64
; - 十六進制字面量建議使用小寫字母(0x00ff,而非 0x00FF)
; - 建議:通用整數常量使用十進制;協議/位掩碼常量使用 hex 並添加顯式 i64 類型標注

; 字符(雙引號 = char/rune,單字符)
c = "中"

; byte類型
b = 0x00

; arr 定長數組
arr [3] = [1, 2, 3]

; vec 動態數組(切片)
vec = [4, 5, 6]

; 顯式類型(切片)
typed []u8 = [1, 2, 3]

; 數組
typed [3]u16 = [1, 2, 3]

; 自動推斷長度(i64)
a [?] = [1, 2, 3]

原始字符串(Raw String)​

使用一對反引號 ` ` 聲明原始字符串,類型為 str。

語法​

sql = `
SELECT id,name
FROM user
WHERE id > 100
`

強制格式約束​

  1. 開頭 ` 後必須緊跟源碼換行;
  2. 結尾 ` 必須單獨佔一行,且該行僅允許空白 + 結束反引號;
  3. 首尾兩行的反引號所在行不計入字符串內容,只作為分隔標記;
  4. 轉義規則:內部所有 \、\n、\t、\'、\" 全部原樣保留,不做任何轉義解析;
  5. 源碼內的換行、縮進空格完整保留到字符串字節裡;
  6. 無法直接嵌入反引號字符,如需包含 ` 只能用普通單引號字符串拼接。

示例​

; 內容為 "SELECT id,name\nFROM user\nWHERE id > 100\n"
; 轉義字符 \n \t 原樣保留,不解釋
raw = `
line1\nline2
\ttabbed
`

正則字面量(Regex Literal)​

Nolang 支援 JavaScript 風格的正則字面量語法 /pattern/flags,用於創建已編譯的 regexp 實例。

語法​

; 基本正則字面量
re = /\d+/

; 帶旗標(flags)
re = /hello/gi

; 字符類、錨點、量詞
re = /[a-z]+/
re = /^hello.*world$/

; 轉義斜線
re = /a\/b/

旗標​

旗標含義
g全局匹配
i不區分大小寫
m多行模式
s. 匹配換行符

旗標是可選的,跟在結束 / 之後,由 ASCII 字母組成。

上下文敏感的詞法分析​

/ 既是除法運算符也是正則字面量的起始符,Nolang 採用與 JavaScript 相同的上下文敏感詞法分析來區分:

  • 表達式起始位置(語句開頭、= / ( / [ / { / , / : / ; 等之後)→ / 開始正則字面量
  • 值產生位置(標識符、字面量、) / ] / } 等之後)→ / 是除法運算符
  • // 永遠是行註釋(優先級最高)
; 正則字面量(= 後是表達式起始位置)
re = /\d+/
result = match-text(/[a-z]+/, text)

; 除法運算符(標識符後是值產生位置)
ratio = 100 / 4
x = a / b

脫糖​

正則字面量在代碼生成階段脫糖為對標準庫 regexp-compile 函數的調用:

; 源碼
re = /\d+/

; 脫糖後等價於
re = regexp-compile('\\d+')

regexp-compile 定義在 std/regexp.no,創建 regexp 結構體並調用 .compile() 編譯模式。

使用示例​

; 創建正則並匹配
re = /\d+/
matched = re.matches('hello 123 world')
print(matched) ; true

; 查找匹配
re = /[a-z]+/g
result = re.find('hello 42 world')
print(result) ; "hello"

; 作為函數參數
result = match-text(/\d+/, text)

注意: 空 pattern // 會與行註釋衝突(與 JavaScript 行為一致)。如需空匹配,使用 /(?:)/。

命名規則​

變量名、函數名、結構体名等可以以下劃線開頭,後續可以使用中連接符、字母、數字組成,不能以數字开头,不能以中連接符結尾,不能連續多個中連接符

大小寫規則(必須遵守):

  • 全局常量、全局變量:必須使用大寫字母開頭(如 NOLANG、MAX-SIZE、HEX-CHARS)。私有全局變量以下劃線開頭,其後仍需大寫(如 _NOLANG、_PRIVATE-CONST)。
  • 局部變量、函數參數:使用小寫字母(如 hex-chars、data-len)。不要使用 _ 前綴——局部變量本身就在作用域內私有,不需要可見性標記。_ 前綴僅用於私有全局變量和 FFI 私有宣告。
  • 函數名、結構體名:使用小寫字母(如 sha1-block、db-mysql)

函數命名慣例(強烈建議):

  • 不要在函數名前加上模組名前綴。模組內的函數只需用簡短直觀的名稱,模組前綴由跨模組調用時的 ShortName. 自動帶出。例如在 tail.no 中定義入口函數 tail 而非 tail-run,定義輔助函數 atoi 而非 tail-atoi。這樣程式碼更簡潔,跨模組調用時形如 tail.tail() 也更直觀。
  • 入口函數建議直接使用模組名本身(如 ping.no → ping、cat.no → cat)。跨模組導入形如 # /src/tail.tail。
  • 注意避開關鍵字:run(async 關鍵字)、match(條件匹配關鍵字)等不能用作函數名。如需對應語義,改用其他名稱(如 main 已廢棄,直接用模組名)。

全局變量必須大寫字母開頭,這是強制規則,不是慣例。小寫開頭的頂層變量會被編譯器視為局部變量,可能導致未定義引用等錯誤。

; ✅ 正確:全局數據使用大寫字母
NOLANG = 'nolang'
MAX-SIZE = 1024
HEX-CHARS = '0123456789abcdef'

; ✅ 私有全局變量:下劃線開頭,其後仍需大寫
_NOLANG = 'nolang'
_PRIVATE-CONST = 42

; ❌ 錯誤:全局變量不能使用小寫字母開頭
; x1 = 10
; x = 10
; foo-bar = 42
; hello-world = 'Hello World'

; ✅ 局部變量(函數內)使用小寫字母,不加 _ 前綴
; fn-example = () {
; x1 = 10
; x = 10
; foo-bar = 42
; hello-world = 'Hello World'
; ; ❌ 錯誤:局部變量不需要 _ 前綴
; ; _x = 10
; }

模組中避免使用全局變量​

強烈建議:如非必要,不要在模組(.no 檔案)中使用全局變量。 全局變量會帶來以下問題:

  • 編譯器 bug 風險:Nolang 編譯器對全局結構體變量的跨函式記憶體位址處理存在已知限制,不同函式可能看到不同的位址,導致狀態不一致。
  • 並發安全:全局可變狀態在 fork 或異步場景下難以追蹤,容易引發競態條件。
  • 可測試性:全局狀態使函式產生隱式依賴,難以隔離測試。
  • 程式碼可讀性:全局變量使資料流隱晦,讀者需追蹤整個模組才能理解函式行為。

推薦做法:

  1. 優先使用局部變量:將狀態保留在函式內的局部變量中,透過參數和返回值傳遞。
  2. 使用結構體封裝狀態:將相關狀態組織到結構體中,透過方法操作(方法接收者為局部變量,位址一致)。
  3. 僅在必要時使用全局變量:如模組級常量(不可變)、單例資源(如全域日誌緩衝區)等場景。
  4. 全局變量必須大寫:這是強制規則(見上文),小寫開頭的頂層變量會被編譯器視為局部變量。
; ❌ 避免:在模組中使用全局可變變量
; g-conn = tls.conn {}
; g-buf = ' '
;
; fn-a = () {
; g-conn.send(g-buf) ; 全局變量跨函式可能位址不一致
; }

; ✅ 推薦:使用局部變量,狀態透過參數/返回值傳遞
fn-a = () {
conn = tls.conn {} ; 局部變量,位址一致
buf = ' '
conn.send(buf)
}

; ✅ 推薦:將完整流程封裝在單一函式中,避免狀態跨函式傳遞
serve-once = (listen-fd fd, body str) (ok bool) {
ok = false
client-fd = net.net-accept(listen-fd)
conn = tls.server-init(client-fd) ; 局部變量
conn: {
ok -> {
c = it
c.handshake()
c.send(body)
c.close()
ok = true
}
-> fs.close(client-fd)
}
}

實踐案例:std/net/tls.no 的 https-serve-once 函式將完整的 HTTPS 請求-回應週期(accept + handshake + recv + send + close)封裝在單一函式中,所有 TLS 狀態保留在局部變量中,成功避開了全局變量跨函式位址不一致的編譯器 bug。

API 文檔規範​

函數的文檔註釋應包含完整的參數名與型別、返回參數名與型別。

規則:

  • 函數定義上方的文檔註釋,必須列出每個參數的名稱和型別,以及返回參數的名稱和型別
  • 模組頂部的 API 摘要也應使用完整簽名(含參數名、型別、返回名、型別),不要使用省略寫法
; ❌ 錯誤:缺少型別,缺少返回參數名
; sha1(data) (hash)
; sha1-block(s, h0..h4)

; ✅ 正確:包含參數名、型別、返回參數名、型別
; sha1(data []byte) (hash [20]byte) — 完整雜湊
; sha1-block(s []u32, h0 u32, h1 u32, h2 u32, h3 u32, h4 u32) — 處理單一區塊

; 函數定義上方的文檔註釋也應遵守同樣規範:
; sha1: 計算 SHA-1 雜湊
; data []byte: 輸入位元組陣列
; 返回 hash [20]byte: 20 位元組雜湊值
sha1 = (data []byte) (hash [20]byte) {
...
}

優先使用標準庫​

Nolang 標準庫提供了豐富的常用功能,包括字串操作、位元組轉換、雜湊計算、網路通訊等。

規則:如果標準庫中已有對應功能,不建議自行重新實現。 開發者應仔細查看標準庫文檔(docs/docs/std/overview.md),避免重複造輪子。

; ❌ 錯誤:自行實現 str → []byte 轉換
str-to-bytes = (s str) (out []byte) {
n = s.len-bytes()
i = 0
{
out[i] = s[i]
i = i + 1
} (i < n)
}

; ✅ 正確:使用標準庫 str.to-bytes() 方法
data []byte = s.to-bytes()

常見的標準庫替代:

  • str.to-bytes() — 字串轉位元組陣列(替代手寫 str-to-bytes)
  • []byte.to-str() — 位元組陣列轉字串(替代手寫 bytes-to-str)
  • [n]t.to-vec() — 定長陣列轉切片([20]byte → []byte)
  • []byte.to-hex() / []byte.to-hex-lower() — 位元組陣列轉十六進制字串
  • str.to-i64() / str.to-f64() — 字串解析為數字
  • int.to-str() / float.to-str() — 數字轉字串
  • std/crypto/sha1、std/crypto/sha256、std/crypto/sha512 — 雜湊計算

TOML 設定檔(toml)​

標準庫 toml 提供 TOML 1.0 設定檔的解析與序列化。解析器支援註解、bare/quoted/dotted keys、[table]、[[array-of-tables]]、basic/literal/multiline strings、整數(十進位/十六進位/八進位/二進位)、浮點數、布林值、日期時間、陣列與 inline table。值以 TOML 原始字面值保存,因此不會遺失陣列、日期時間等資訊。

cfg = toml.parse('title = "Nolang"\n[server]\nport = 8080\n[[server.backends]]\nname = "local"')

doc = toml.new()
doc.put('name', '"nolang"')
doc.put('server.port', '8080')
doc.put('enabled', 'true')
text = toml.stringify(doc)

常用 API:

  • toml.new() — 建立空的 toml-doc
  • toml.parse(text) — 解析 TOML,回傳 ?toml-doc
  • doc.put(key, raw-value) — 新增或更新原始 TOML 值;會拒絕非法值、重複鍵由解析器報錯
  • toml.get(doc, key) — 取得原始值(?str)
  • toml.get-str(doc, key)、toml.get-i64(doc, key)、toml.get-f64(doc, key)、toml.get-bool(doc, key) — 取得已轉換的 scalar
  • toml.stringify(doc) — 輸出 TOML;array-of-tables 會輸出 [[...]] 標頭

目前文件模型限制單一文件最多 4 個 key、2 個普通表格與 2 個 array-of-tables;單一 key 最長 32 bytes、單一 value 最長 128 bytes,超出時應分割設定檔或由呼叫方處理。

YAML 文件(yaml)​

標準庫 yaml 解析與產生 YAML 1.2 core schema 文件,支援塊映射、塊序列、流式集合、三種引號標量、塊標量 | / >、註解、多文檔、錨點與別名、合併鍵 <<、顯式鍵,以及型別推斷(null / bool / int / float / .inf / .nan)。

doc ?yaml = yaml.parse('name: Alice\nserver:\n port: 8080\ntags:\n - a\n - b\n')
doc: {
nil -> print('nil')

err -> print(it)

-> {
name, ok = it.find-str('name')
port, ok2 = it.find-i64('server.port')
print(it.to-json())
}
}

常用 API:

  • yaml.parse(text) / yaml.parse-all(text) — 解析,回傳 ?yaml
  • yaml.valid-of(text) / yaml.error-of(text) — 只做校驗
  • it.find(path) / it.get(key) — 取子節點,回傳 (yaml, ok)
  • it.find-str(path)、find-i64、find-f64、find-bool — 便捷取值,回傳 (值, ok)
  • it.to-json() / it.to-yaml() — 序列化

注意:子節點以 (out yaml, ok bool) 回傳而非 ?yaml——MIR 後端目前不能安全地共享含切片的節點池。

檔案命名​

.no 檔名(含文件夾名)一律使用中連字符 - 連接單詞,不使用下劃線 _。這與變量名、函數名、結構體名等 Nolang 標識符的命名風格保持一致。

✅ 推薦:
utils/
├── string-helper.no
├── hash-table.no
└── http-client.no
❌ 避免:
utils/
├── string_helper.no
├── hash_table.no
└── http_client.no

函數定義​

函數可以透過**具名結果參數(named result parameters)**輸出結果,本質是 輸出參數(out-parameter)的語法糖;... 僅用於提前終止,不能跟結果。

Nolang 的函數定義形式為 name = (in-params) (out-params) { body }:第二個 括號組 (out-params) 宣告具名結果參數(語法糖)。函數體內對它們賦值 (與普通入參一樣皆為引用型別);真正接收結果的變數必須在調用處定義—— 定義裡的結果參數名只是佔位符,調用方的 LHS(或尾隨引數)決定結果落在哪個 變數:

parse-line = (s str, max-fields i64 = 1024) (fields []str) {
... ; 函數體內對 fields 賦值
}

; 調用處定義接收變數(LHS 綁定,型別由簽名推斷)
fields = parse-line(line)
; 多結果按順序綁定
a, b = swap(x, y)
; 或尾隨引數形式:res 作為額外的輸出引數傳入
add1(5, 3, res)

Nolang 的函數有以下特點:

  • 具名結果參數是語法糖:底層仍是透過入參(out-parameter)完成,不會產生 新的返回值物件,因此內部是安全的。
  • 輸入參數唯讀:標量傳值,複合類型傳唯讀引用;禁止在函數體內寫入輸入參數及其子欄位。
  • 輸出參數可寫:調用方可將已有變數綁定到輸出槽,函數直接在該記憶體修改;也可不綁定,由函數生成新值交付。
  • 函數內的變量在函數退出時自動銷毀。
  • 調用處必須提供接收變數(LHS 或尾隨引數);「函數沒有返回值」的說法不準確—— 準確說法是:結果透過具名結果參數(out-parameter 的語法糖)傳遞,且由調用處綁定。
  • 別名規則:允許輸入唯讀引用與輸出槽指向同一物件。只要寫操作僅發生在輸出區、輸入僅讀取,即為合法;編譯器不做靜態別名檢查。

參數默認值​

函數參數可以使用 name type = expr 語法指定默認值。帶有默認值的參數在調用時可以省略,編譯器會自動填充默認值。帶默認值的參數必須放在參數列表末尾。

; 帶默認值的函數定義
parse-line = (s str, max-fields i64 = 1024) (fields []str) {
...
}

; 以下兩種調用方式都合法:
fields = csv.parse-line(line) ; max-fields 默認為 1024
fields = csv.parse-line(line, 256) ; max-fields = 256

add = (a i64, b i64) (result i64) {
result = a + b ; 通過參數返回結果
... ; 提前終止(可選)
}

; 可變參數
add3 = (a ..i64) {
}

; 函數調用
sum = add(1, 2) ; sum == 3

; 匿名函數 和for? 有傳參?
(a i64) { print(a) }(10)

; 函数调用
add(a, b)

; 也可能有多个返回值
a, b = swap(5, 3)

流程控制​

Deprecated 語法(n 版本後移除):for { } / for cond { } / for init, cond, update { } / for i in [..) { } / match x { } / if/elif/else { } 仍可解析但會輸出 deprecation warning。請改用下方「新式語法」。

新舊對照​

舊式 / 後綴式(suffix)前置式(prefix,format 預設)
{ } (true) 無限循環!! { } 或 true { }
{ } (cond) 條件循環(cond) { }(空 () 視為 false,不執行)
{ } () 不執行! { } 或 false { } 或 () { }
{ } * n 常數計數n * { }(N ≤ 0 時循環體不執行)
for i=0, i<n, i++ { } 計數n * { }(常數計數)或 i <- [0..n): { }(變數)
for i <- [a..b] { } 範圍i <- [a..b]: { }
for i in [a..b) { } 範圍i <- [a..b): { }
match x { ... } 匹配x: { ... }
if/elif/else { } 分支{ cond -> body }(短路,必須用 {} 包裹)
continue**
break*
return...

Loop / While / for-in​

兩種等價寫法:同一個循環支援「前置式 (cond) { }」與「後綴式 { } (cond)」, 語意完全相同,只是條件與主體的先後順序不同。no fmt 預設輸出前置式; 可用 no fmt -loop-style=suffix 切回後綴式(兩種寫法都保證格式化冪等)。

; === 前置式(prefix,條件寫在主體前面)===

; 無限循環(條件恆為 true)—— 以下兩種等價
!! {
...
}
true {
...
}

; 不執行(條件恆為 false)—— 以下三種等價
! {
...
}
false {
...
}
() {
...
}

; 條件循環(檢查 cond,為真時執行主體)
(x == 1) {
do-something()
}

; 限定執行次數(前置式計數)
10 * {
do-something()
}

; N ≤ 0 時循環體不執行(0 次或負數次均跳過)
0 * {
print('不會執行')
}
-3 * {
print('也不會執行')
}

; 可加標籤(label 寫在條件/計數之前)
#1 (x == 1) {
do-something()
}

; === 後綴式(suffix,條件寫在主體後面,舊式)===
{
...
} (true)

; 不執行(空括號代表 false)
{
...
} ()

; 條件循環
{
do-something()
} (x == 1)

; 限定執行次數
{
} * 10

; N ≤ 0 時循環體不執行(0 次或負數次均跳過)
{
print('不會執行')
} * 0

{
print('也不會執行')
} * -3

; 區間語法(已支持 i64/u8 等數值類型、arr、vec、str)
; 共 16 種組合:'[' 包含左端點,'(' 不包含;']' 包含右端點,')' 不包含。
; 有界(4 種):兩端都有值
i <- [a..b]: { ; 闭区间:a ≤ i ≤ b
}
i <- (a..b]: { ; 左开右闭:a < i ≤ b
}
i <- [a..b): { ; 左闭右开:a ≤ i < b
}
i <- (a..b): { ; 开区间:a < i < b
}
; 無上限(4 種):省略右端點,迭代至類型最大值
i <- [a..]: { ; a ≤ i ≤ 類型最大值
}
i <- [a..): { ; a ≤ i < 類型最大值
}
i <- (a..]: { ; a < i ≤ 類型最大值
}
i <- (a..): { ; a < i < 類型最大值
}
; 無下限(4 種):省略左端點,從類型最小值開始迭代
i <- [..b]: { ; 類型最小值 ≤ i ≤ b
}
i <- [..b): { ; 類型最小值 ≤ i < b
}
i <- (..b]: { ; 類型最小值 < i ≤ b
}
i <- (..b): { ; 類型最小值 < i < b
}
; 完全無界(4 種):兩端都省略,基於類型最小值/最大值
i <- [..]: { ; 類型最小值 ≤ i ≤ 類型最大值
}
i <- [..): { ; 類型最小值 ≤ i < 類型最大值
}
i <- (..]: { ; 類型最小值 < i ≤ 類型最大值
}
i <- (..): { ; 類型最小值 < i < 類型最大值
}
i <- [5..0]: { ; 递减 — 运行时方向检测:start > end 时自动递减
}
i <- 'abc': { ; 遍历字符串中的每个字符
}

; 运行时方向检测:当 start > end 时,迭代自动递减(步长 -1)。
; 四种括号组合均支持递减:
; [5..1] → 5 4 3 2 1 左闭右闭,递减
; (5..1] → 4 3 2 1 左开右闭,递减
; [5..1) → 5 4 3 2 左闭右开,递减
; (5..1) → 4 3 2 左开右开,递减
; (3..0] → 2 1 0 左开右闭,递减到 0
; 当 start <= end 时,按递增方向迭代(步长 +1)。
;
; 無界區間的端點由變量的靜態類型決定:
; i64 的 [..] 從 i64.MIN (-9223372036854775808) 到 i64.MAX (9223372036854775807)
; u8 的 [..] 從 0 到 255
; i64 的 [..) 從 i64.MIN 到 i64.MAX - 1(不包含最大值)
; i64 的 (..] 從 i64.MIN + 1 到 i64.MAX(不包含最小值)
; i64 的 (..) 從 i64.MIN + 1 到 i64.MAX - 1(兩端都不包含)
; 規律:'[' 包含類型最小值,'(' 從最小值+1 開始;
; ']' 包含類型最大值,')' 到最大值-1 結束。

; ❌ 明確拒絕
; 區間邊界必須是整數;不支持嵌套表達式
; for i <- [1.5..5.5] { } ; 編譯錯誤
; for i <- [0..[1..5][0]] { } ; 語法錯誤

; ⚠️ 避免 ... 歧義
; 範圍運算子是 ..(兩點),self 方法調用是 .len()。
; 連寫時 [0.. .len()) → [0...len()),三個點看起來像單一運算子
; (而 ... 是 return/終止運算子)。
; 應使用 self.len() 代替 .len() 消歧:i <- [0..self.len()): { }
; (方法體內 self 與 . 語義等價)

; 條件循環(新式:前置式 (cond) { },`no fmt` 預設;取代舊式 for cond { })
; 後綴式 { } (cond) 與之完全等價(舊式寫法)
; 大多數情況可改用 range-for:i <- [0..n): { }
(x == 1) {
do-something()
}

跳出 / 跳過 / 提前返回​

i <- [0..10): {
* ; break
** ; continue
... ; return/terminate(提前返回,僅終止函數)
}

; 也可使用英文關鍵字(與 C/Rust 一致,便於移植、且能正常編譯執行):
i <- [0..10): {
break ; 等價 *
continue ; 等價 **
return ; 等價 ...,僅提前終止函數
}

⚠️ return(及其符號形式 ...)只能裸用,不能帶返回值。 Nolang 函數沒有「返回值」機制——結果一律通過具名結果參數(out-param)在函數體內賦值傳出(見「函數定義」)。 因此 return <值>、return(expr)、... <值> 都被禁止,編譯器 / formatter / LSP 都會報錯:

  • 編譯器(no build):Error: compilation error: parser errors: line L, column C: 'return' 後不能跟返回值;… [E_GENERAL]
  • no vet:<file>:L:C: [ERROR] nolang-compile: 'return' 後不能跟返回值;… [E_GENERAL](與其他診斷同一格式,每條一行)
  • no fmt:format error: … 並以非零狀態退出
  • LSP:在對應行顯示紅色錯誤診斷

錯誤寫法:

has = (n i64) (r i64) {
n == 0 -> return 0 ; ❌ 編譯 / formatter / LSP 均報錯
r = n
}

正確寫法(先給結果參數賦值,再裸 return 提前終止):

has = (n i64) (r i64) {
n == 0 -> { r = 0; return } ; ✓
r = n
}

Match​

; 簡單寫法,it 用於取參數
x: {
err -> log(it)
nil -> log('nil')
->
do-right-thing(it)
}

; 析構寫法
x: {
err(e) -> log(e)
nil -> log('nil')
ok(v) ->
do-right-thing(v)
}

user: {
User{id=1} -> print('管理員')
User{name=n} -> print('用戶:', n)
-> print('匿名')
}

score: {
[0..59] -> print('不及格')
[60..89] -> print('良好')
[90..100] -> print('優秀')
-> print('分數非法')
}

; 區間語法共 16 種組合,規律:'[' 表示包含左端點,'(' 表示不包含;
; ']' 表示包含右端點,')' 表示不包含。
;
; 【有界區間】(4 種)— 兩端都有具體值
; [a..b] → x >= a && x <= b 兩端都包含
; [a..b) → x >= a && x < b 左包含右不包含
; (a..b] → x > a && x <= b 左不包含右包含
; (a..b) → x > a && x < b 兩端都不包含
;
; 【無上限區間】(4 種)— 省略右端點(End=nil),僅檢查下界
; [a..) → x >= a 左包含,無上限
; [a..] → x >= a 左包含,無上限
; (a..) → x > a 左不包含,無上限
; (a..] → x > a 左不包含,無上限
;
; 【無下限區間】(4 種)— 省略左端點(Start=nil),僅檢查上界
; [..b] → x <= b 無下限,右包含
; [..b) → x < b 無下限,右不包含
; (..b] → x <= b 無下限,右包含
; (..b) → x < b 無下限,右不包含
;
; 【完全無界區間】(4 種)— 兩端都省略,基於類型最小值/最大值
; [..] → true 類型最小值 ≤ x ≤ 類型最大值(兩端都包含)
; [..) → true 類型最小值 ≤ x < 類型最大值
; (..] → true 類型最小值 < x ≤ 類型最大值
; (..) → true 類型最小值 < x < 類型最大值
;
; 在 match 中,完全無界區間基於被匹配變量的類型值域(必然為真,等同 catch-all)。
; 在 range-for 中,基於迭代變量的類型範圍(如 i64 的 [..] 從 i64.MIN 到 i64.MAX)。
;
; 範例:使用無界區間做完整分類
v = 85
result = v: {
[..0) -> 'negative'
[0..60) -> 'fail'
[60..80) -> 'pass'
[80..90) -> 'good'
[90..100]-> 'excellent'
(100..) -> 'extraordinary'
}
print(result)

num: {
1 || 3 || 5 || 7 -> print('奇數小數')
2 || 4 || 6 -> print('偶數小數')
-> print('更大數字')
}

; 有返回值,最後一個語句/值
result = x: {
1 -> 1
2 -> 2 + 1
-> a + b
}

; 多行 arm body 必須使用大括號 -> { ... }
x: {
nil -> {
log('nil')
do-cleanup()
return
}
err -> {
log(it)
do-cleanup()
return
}
ok -> print(it)
}

多行 arm body 規則:當 arm body 包含多個語句時,必須使用大括號 -> { ... } 括起來。單行 body 可直接寫在 -> 之後。這是因為多行 body 若不使用大括號,option match 的 it 綁定將無法正確插入,導致編譯錯誤。

for-in 體內 match 語義:i <- (a..b]: { 1 -> ... 2 -> ... } 對每個迭代變量 i 執行一次 match 體(1 -> 等同於 i == 1 ->,依此類推)。這是每輪迭代執行一次 match 的語法糖。

Match 風格指引​

; ❌ 避免重複分支體
w = tls-c.send(req)
w: {
nil -> {
tls-c.close()
return
}
err -> {
tls-c.close()
return
}
ok -> n = it
}

; ✅ 公共邏輯放 -> catch-all
w = tls-c.send(req)
w: {
ok -> n = it
-> {
tls-c.close()
return
}
}

; ✅ 反之亦然:簡單分支命名,複雜邏輯放 ->
val: {
nil -> return
err -> log(it)
-> {
n = it
total = total + n
process(n)
}
}
; 單語句 — 不加大括號
val: {
ok -> print(it)
-> print('empty or error')
}

; 多語句 — 必須加大括號
val: {
ok -> {
n = it
total = total + n
}
-> {
log('failed')
return
}
}
; it 隱式綁定
val: {
ok -> process(it) ; it = 解包後的值
err -> log(it) ; it = 錯誤訊息字串
-> log('empty') ; catch-all,此處為 nil
}
; it 的作用域:巢狀 match 按層還原
outer: {
-> {
use(it) ; 外層 it
inner ?T = f(it)
inner: {
-> use(it) ; 內層 it = inner 的解包值
}
use(it) ; ✅ 內層臂結束後 it 已還原成外層值
}
}

注意:還原只發生在巢狀的 match。最外層 match 結束後 it 仍保留最後一個臂的值, 不要在 match 之外使用 it。需要跨層使用時請存到具名變數(doc = it),或用析構綁定 ok(v) -> ... 把載荷綁到自己的名字上。

it 只能在「可證明是單一情況」的臂裡使用​

catch-all -> 臂承接的是前面沒有被顯式臂認領的所有情況。只有當剩下的情況恰好一種 時 it 才有單一明確的意義:

; ✅ -> 就是 ok(nil / err 都已被認領)
v: {
nil -> log('empty')
err -> log(it)
-> process(it)
}

; ✅ -> 就是 err(ok / nil 都已被認領)
v: {
ok -> process(it)
nil -> log('empty')
-> log(it)
}

; ❌ -> 可能是 nil 也可能是 err —— 編譯錯誤
v: {
ok -> process(it)
-> log(it)
}

補上 nil -> / err ->,或改用 ok ->,即可通過。enum 與裸 match({ cond -> ... }) 不在此限:it 指的是被匹配的值本身,沒有 nil / err 的可能。

err -> 臂的 it 永遠是錯誤訊息(str),與 option 的元素型別無關—— option { ok(v t), nil, err(e str) }。所以就算匹配的是 ?i64 / ?bool 這類 純量 option,err -> 臂裡的 it 也是字串,可以直接 msg = it。

; ✅ 組合 option 模式:nil || err -> body
; 當 option 為 nil 或 err 時共用同一個分支
val: {
nil || err -> {
cleanup()
return
}
ok -> process(it)
}

; ✅ 也可與 -> catch-all 混用
val: {
nil || err -> log('failed')
ok -> process(it)
}

短路管道(->)​

-> 是左結合的短路管道運算符,語義固定,不受外層是否賦值、是否在 match 臂內影響。裸語句、match 臂的單行 body、賦值右側三處共用同一套 lowering。

管道內的節點分三類:

節點對管道 state 的影響
副作用呼叫(無回傳,如 print('A'))執行,不改變 state;只要先前是 Ok 就繼續下一個節點
回傳 ?T 的呼叫(可失敗,如 might-fail())執行,覆蓋 state;一旦變成 nil/err,後續節點全部跳過
末尾普通值表達式僅在 state 仍為 Ok 時求值

只有回傳 ?T 的節點有能力把管道切到失敗;print 這類副作用永遠不會。所以「整條鏈都是 print」看起來像沒有短路,其實只是沒有節點能產生失敗。

; 沒有可失敗節點 → 全部執行
ok -> print('A') -> print('B')

; might-fail() 回傳 ?T → 失敗時 print('B') 不執行
ok -> print('A') -> might-fail(x) -> print('B')

; 賦值右側:同一套語義
r = print('E') -> might-fail(x) -> 42

邊界澄清:

  1. -> 語義唯一,裸語句與賦值表達式共用同一套管道 lowering,不會因為外層有沒有賦值而切換模式。
  2. match 臂單行 body 裡的 -> 是管道延續,不是新臂;臂與臂之間由換行分隔:
; ✅ 一個臂,body 是管道 → 兩行都印
ok -> print('A') -> print('B')

; ✅ 兩個臂(換行分隔)→ 命中 ok 就不會印 'miss'
v: {
ok -> print('hit')
-> print('miss')
}

⚠️ 臂是以換行分隔的,所以寫在同一行的 pat -> A -> B 是一個臂的管道,不是「if A else B」。需要 if/else 時請把臂分行寫,或用 {} 包裹的短路組。

  1. 管道可以產值:寫在賦值右側時,管道的結果就是末尾的值節點(僅在 state 仍為 Ok 時求值)。
n = 7
x = 1 > 2 -> 42 ; 條件不成立 → x 保持原值 7
y = 2 > 1 -> 42 ; y == 42
s str = 'old'
s = 1 > 2 -> 'new' ; 短路 → s 仍是 'old'
r = print('E') -> might-fail(bad) -> 99 ; 中段失敗 → r 保持原值

失敗(條件不成立或被 ?T 節點短路)時賦值不生效:變數保留它原本的值;若是首次宣告則為該型別的零值。這一點和敘述形式一致 —— { 1 > 2 -> x = 42 } 同樣不會改動 x。

值的型別由臂的末尾表達式推斷(str / i64 / ?T 皆可),與 x = subject: { arms } 這既有的 match 取值形式共用同一套推斷。

If / Else​

if-else 組(短路)必須用 {} 包裹。第一個匹配的條件勝出,後續條件不再檢查。

; 多分支(推薦新式)— 短路 if-else 鏈
{
a == 1 -> {
a = 1
b = 2
}
a == 2 || a == 3 -> do-something()
-> {
c = 0
}
}

; 單 if(保留)— 獨立 if,不短路
x == 1 -> do-something()

; 三元表達式 condition ? true-value : false-value
c = flag ? 1 : 2
max = sum > 10 ? sum : 10

重要規則:

  1. 短路組 — 多個 cond -> body 用 {} 包裹時,形成 if-elif-else 鏈,只有第一個匹配的分支執行。
  2. 獨立 if — 在函數體/循環體中直接寫 cond -> body(不包裹 {})是獨立 if,不會與相鄰的 if-then 短路。
  3. 禁止混雜 — {} 短路組內的直接子節點必須全部是 cond -> body 分支,不能混入普通語句。
; ❌ 不會短路 — 這些是獨立 if,所有條件都會被檢查
git-dispatch = (cmd str) {
cmd == 'a' -> { fa() }
cmd == 'b' -> { fb() }
}

; ✅ 短路 — 用 {} 包裹,第一個匹配勝出
git-dispatch = (cmd str) {
{
cmd == 'a' -> { fa() }
cmd == 'b' -> { fb() }
true -> {}
}
}

; ❌ match-block 混雜普通語句 → 編譯報錯
{
cmd == 'a' -> { fa() }
print(cmd)
cmd == 'b' -> { fb() }
}

; ✅ 普通語句放到分支 body 內部
{
cmd == 'a' -> {
print(cmd)
fa()
}
cmd == 'b' -> { fb() }
true -> {}
}

異步編程(run / awy)— 已廢棄手寫,推薦協程組​

⚠️ 已廢棄(手寫原語):run / awy / cancel / cancelled 已廢棄,不推薦手寫。手動管理 task 句柄不安全(未 await 會洩漏參數緩衝區、複製句柄後重複 await 會崩潰),協作式取消無法強制中斷長阻塞調用。請改用「協程組」(見下方),自動管理句柄與依賴。

Nolang 使用 run 和 awy 實現異步並發。異步函數的名稱必須以 -async 結尾,但不使用 async 關鍵字。

  • run — 啟動異步線程,返回一個 task handle
  • awy — 等待異步線程完成並取得結果
; 異步函數定義(名稱以 -async 結尾)
compute-async = (n i64) (r i64) {
r = n * 2
}

; 基本異步調用
test-basic = () {
h = run compute-async(21)
r = awy h
print(r) ; 42
}

; 並發多個任務
test-concurrent = () {
h1 = run compute-async(10)
h2 = run compute-async(20)
r1 = awy h1
r2 = awy h2
print(r1) ; 20
print(r2) ; 40
}

; 內聯等待
test-inline = () {
r = awy run compute-async(5)
print(r) ; 10
}

命名規則:異步函數名必須以 -async 結尾(如 compute-async、fetch-data-async)。不使用 async 關鍵字聲明。

協程組(coroutine group)— 推薦​

語句位置的裸塊 { ... } 是協程組:塊內每條「直接調用 -async 函數」的語句默認被 spawn(等價於 run), 結果在需要它的地方才 awy。不需要寫 run / awy。

hello-async = (i i64) (r i64) {
r = i
}

; 無依賴:兩個任務並發(先後 spawn,最後一起 await)
test-concurrent = () {
r1 i64
r2 i64
{
r1 = hello-async(1)
r2 = hello-async(2)
}
print(r1) ; 1
print(r2) ; 2
}

; 有依賴:r2 讀 r1,不能並發 —— 退化成 await(順序執行)
test-sequential = () {
r1 i64
r2 i64
{
r1 = hello-async(1)
r2 = hello-async(r1)
}
print(r1) ; 1
print(r2) ; 1
}

規則:

  • 降級條件:後面的語句讀到(或覆寫)前面某條語句綁定的變量時,那條語句的任務必須先落地, 於是在該處插入 awy,整組退化為順序執行。依賴是傳遞的(r3 讀 r2、r2 讀 r1 ⇒ 整條鏈順序)。
  • 屏障:組內不是「直接 -async 調用」的語句(普通賦值、print、迴圈、if…)是屏障, 它之前所有仍在飛行中的任務都會先 awy。這保證組內讀到的永遠是值,不會是 opaque 句柄。
  • 函數體不是協程組:只有語句位置的裸塊才是。函數體、if 分支、迴圈體都不是, 所以 f = hello-async(1) 在函數體裡仍然只是建出一個 future,不自動 await。
  • 丟棄結果也要 await:{ side-async(5) } 仍會 spawn + await —— 未 await 的任務會洩漏它的參數緩衝區。

等價展開(上面並發例子的實際 lowering): __ag0 = run hello-async(1) → __ag1 = run hello-async(2) → r1 = awy __ag0 → r2 = awy __ag1。

無色 async:co 關鍵字 — 推薦​

co 是建立在協程組之上的**無色(colorless)**語法:你寫未染色的函式名 worker,編譯器自動單態化出 worker-async 變體,並在底層以染色無棧協程執行。你不需要手寫 -async 後綴,也不需要手寫 run / awy。

worker = (n i64) (r i64) {
#{overflow=wrap}
r = n + 1
}
main = () {
r1 i64
r2 i64
{
r1 = co worker(1) ; 自動單態 worker-async 並 spawn
r2 = co worker(2) ; 與 r1 並發執行
}
print(r1.to-str() + ' ' + r2.to-str())
}

規則:

  • 單態化:co worker(1) 會自動生成 worker-async(複製 worker 的本體、改名)。若 worker 同時也被同步呼叫,則 worker 與 worker-async 兩個方法共存。
  • 傳遞性:若某函式內部呼叫了異步函式(例如 worker 內 co child(...)),則 worker 也會被單態為 worker-async,異步染色沿呼叫鏈向上傳遞。
  • 語句級 co(協程組之外):當 co 出現在協程組之外(例如在函式體裡 d = co worker(5)),編譯器會把它包成 run worker-async(5) + awy 的內聯 await,使 d 取到的是結果值而非 task 句柄。
  • 底層仍是染色無棧協程:co 只是語法糖。{ r1 = co worker(1); r2 = co worker(2) } 等價於協程組展開——先後 spawn worker-async,最後一起 awy。

診斷開關:NOLANG_ASYNC_GO=0 可停用單態化(僅作對照)。

多重賦值​

函數可以返回多個值,調用時使用多重賦值接收:

; 函數定義返回多個結果參數
swap = (a i64, b i64) (x i64, y i64) {
x = b
y = a
}

; 多重賦值
a, b = swap(5, 3)

; 使用 _ 忽略不需要的返回值(占位變數)
_, b = swap(5, 3) ; 只取第二個值,忽略第一個
a, _ = swap(5, 3) ; 只取第一個值,忽略第二個
_, _ = swap(5, 3) ; 忽略全部返回值(呼叫僅為副作用)

; 也支援作為 match arm 的 body
val: {
ok -> a, b = parse-pair(it)
-> return
}

數組與切片​

容器存儲數據副本,原變量與容器獨立,杜絕懸垂引用。

定長数组arr:


; 使用定長数组
a [3] = [1, 2, 3] ; 长度为 3 的定長数组 i64
a [3]u16 = [1, 2, 3] ; 指定类型的定長数组

a [?]u16 = [1, 2, 3] ; 自動推斷長度(指定類型)
a [?] = [1, 2, 3] ; 自動推斷長度(i64)

變長數組vec:

v = [1, 2, 3] ; 變長數組 i64
bs = [0x11, 0x22, 0x33]
v []u8 = [1, 2, 3] ; 指定类型的變長數組

; 預分配(避免反覆擴容)
v = with-cap(100) ; len=0, cap=100(需 push 後索引)
v = with-len(100) ; len=100, cap=100(可直接索引)
v = with-cap-len(200, 100) ; len=100, cap=200(預留擴容空間)

切片slice(視圖,非獨立類型):

切片是對原始資料的視圖(view),不會複製資料,也不會產生新的獨立類型。 切片內部只記錄一個指向原始緩衝區的指標、長度和容量,因此:

  • 通過切片修改元素會影響原始資料,反之亦然
  • 切片不擁有資料,原始變數釋放後切片即失效
  • 切片的類型由原始類型決定,方法自然適用,無需「繼承」機制
; 支持arr/vec/str
; 支持範圍 和for <- 的表示一致
nums [5]u8 = [0, 1, 2, 3, 4]

nums[..] ; [0 1 2 3 4] 完全無界,等同整個切片
nums[1..] ; [1 2 3 4] 無上限,從索引 1 到末尾
nums[..4] ; [0 1 2 3 4] 無下限,從開頭到索引 4
nums[2..3] ; [2 3] 有界,兩端都包含
nums[1..3] ; [1 2 3] 有界,兩端都包含
nums[1..3) ; [1 2] 左閉右開
nums(1..3) ; [2] 兩端開區間

; 字符串(切片結果型別為 str 視圖,共享底層記憶體;下標為碼點位置,與 s[i] 一致)
s = 'abc'
s[1..] ; 'bc'
s[1..s.len()) ; 'bc'
b str = s[0..1] ; 切片結果已是 str,可直接賦值給 str 變數

切片語法支援與 match/range-for 相同的 16 種區間組合。在切片上下文中,無界區間基於切片的長度:[..] 表示整個切片,[a..] 表示從索引 a 到末尾,[..b] 表示從開頭到索引 b。

切片的類型與方法:

切片不生成新的獨立類型,只是原始類型的一個視圖(調整了起始指標和長度)。 因此原始類型的方法直接可用:

原始類型切片視圖類型可用方法
arr ([n]t)[]t<range>[]t 的所有方法(如 len、push、pop、contains、reverse、clone、fill、to-arr 等)
vec ([]t)[]<range>同上
strstr<range>str 的所有方法(如 to-upper、to-lower、index、contains、slice、copy、fill 等)
; arr 切片 → vec 視圖,共享 arr 的底層記憶體
a [5]u8 = [0, 1, 2, 3, 4]
s = a[1..4] ; s 是 []u8 視圖,指向 a 的記憶體
n = s.len() ; slice.len()

; vec 切片 → vec 視圖,共享 vec 的底層記憶體
v = [10, 20, 30, 40, 50]
s = v[2..] ; s 是 []i64 視圖
s.reverse(s.len()) ; slice.reverse

; str 切片 → str 視圖,共享 str 的底層記憶體
s = 'Hello World'
sub = s[6..] ; sub 是 'World' 視圖
upper = sub.to-upper() ; str.to-upper

; 通過切片修改元素會影響原始資料
data = [10, 20, 30, 40, 50]
view = data[1..4] ; view = [20, 30, 40]
view[0] = 99 ; 修改 view 的元素
; data[1] 現在也是 99,因為 view 共享 data 的記憶體

索引​


; 字符串索引 → 回傳 char(Unicode 碼點,不是字節)
c char = str[i]

; arr、vec獲取元素
arr[i]
vec[i]

; map 獲取 value
map[str]

str[i] 的型別是 char(Unicode 碼點);str[a..b] 切片回傳 str(碼點語義視圖,共享底層記憶體)。 char 可隱式轉換為 str(編碼為單字元字串),因此下列寫法皆合法,無需手動呼叫 char.to-str():

s = 'héllo'
c char = s[1] ; 'é'(char,其碼點為 233)
a str = s[1] ; 'é'(char → str 隱式轉換)
b str = s[0..1] ; 'hé'(切片結果本身已是 str)
msg = 'first: ' - s[0] ; char 參與拼接(-)時隱式轉為 str
ok = s[0] == 'h' ; char 與 str 比較時隱式轉為 str

安全索引(Safe Indexing)​

對 arr / vec / slice([]T)的直接索引 v[i],若索引越界,普通寫法會直接 abort(runtime error: index out of bounds)。Nolang 提供三種安全寫法,保證越界時絕不靜默崩潰:

1. x ?= v[i] —— 向上傳播錯誤

安全索引會把 v[i] 視為回傳 ?elem 的運算:越界時回傳 None,否則回傳 some(元素)。配合 ?= 解箱,越界錯誤會自動向上傳播(函式回傳 ?T 時)。

safe-get = (arr []i64, i i64) (res ?i64) {
x ?= arr[i] ; 越界 → res = None(不崩潰)
}

2. x = v[i] #{index-out=DEF} —— 越界用預設值替代(zero 或字面量)

帶 #{index-out=DEF} 註解(獨立成行置於賦值上方,或寫在賦值同一行尾隨)時,越界不會崩潰,而是用 DEF 替代。DEF 有兩種形式:關鍵字 zero 或字面量。

  • #{index-out=zero}:不分容器元素型別,一律用「當前元素型別的零值」替代——整數族 / byte / char → 0、f32/f64 → 0.0、bool → false、str/txt → ''、容器 → 空容器 / 零定長陣列、結構體 → 零值結構體。無需知曉元素型別細節,最適合「越界即按預設零值處理」的場景。
  • #{index-out=DEF}(字面量):依容器元素型別決定允許的種類,DEF 必須與元素型別相容:
    • 整數 / 字元容器(i8i128、u8u128、byte、char):整數或字元字面量,如 0、'x'
    • 浮點容器(f32、f64):浮點字面量,如 0.0
    • 布林容器(bool):true / false
    • 字串容器(str):字串字面量,如 ''
get-default = (arr []i64, i i64) (res i64) {
res = arr[i] #{index-out=zero} ; 越界 → res = 0(顯式字面量)
}

get-zero = (arr []f64, i i64) (res f64) {
res = arr[i] #{index-out=zero} ; 越界 → res = 0.0(當前型別零值)
}

get-ch = (arr []byte, i i64) (res byte) {
res = arr[i] #{index-out=zero} ; byte 容器零值 0
}

註解也可以獨立成行、置於賦值上方:#{index-out=zero} ⏎ res = arr[i]。寫在賦值前方同一行(#{index-out=zero} res = arr[i])則會報錯(見〈註解位置〉)。

3. option 回傳函式內的裸 x = v[i] —— 就地捕獲

當所在函式回傳 ?T 結果時,裸的安全索引賦值 x = v[i] 會讓 x 推斷為 ?elem:越界時 x 就地存為 nil(不崩潰、也不提前 return),流程繼續,之後用 x: { ... } 消費。這與第 1 種 ?= 的差別在於「錯誤留在原地」而不是「向上拋」:

capture-prop = (arr []i64, i i64) (res ?i64) {
x = arr[i] ; x 推斷為 ?i64;越界 → x = nil
x: {
nil -> {} ; 越界:就地處理
err -> {}
-> { res = it } ; 成功:res = 元素值
}
}

適用範圍(重要): 這條捕獲規則只在回傳 ?T 結果的函式內生效,與 ?= 的適用範圍一致。在沒有 option 結果參數的函式、以及頂層指令稿中,x = v[i] 維持原本的「普通元素讀取」語義 —— 若需要越界安全,請用第 1 種 ?=(要求 option 結果參數)或第 2 種 #{index-out=DEF}。

vec(含由 to-vec() 產生的未標註區域變數)同樣安全:

get-vec-default = (a [4]i64, i i64) (res i64) {
v = a.to-vec() ; v 的元素型別由 a 推導,無需標註
res = v[i] #{index-out=zero} ; 越界 → res = 0(不崩潰)
}

適用範圍: 安全索引僅對 arr / vec / slice 的直接變數索引生效(v[i],v 為識別符)。str / txt 索引仍回傳字元(語義不同);receiver.field[i](結構體欄位索引)走原有 bounds-check 路徑,不會被改寫為安全形式。

核心保證: 無論用哪一種寫法,越界都不會讓程式靜默崩潰——或回傳 None、或回傳預設值、或向上傳播錯誤。

結構體​

結構體定義和字面量都必須使用多行形式,每個字段獨佔一行,字段之間不以逗號分隔,末尾也不跟逗號。

user {
name str
age i64
}

u = user {
name: 'Alice'
age: 30
}
u.name = 'Bob'
u.age = 25
print(u.name)

結構體實作介面​

結構體可以實作一個或多個介面,介面名寫在結構體名後面,以逗號分隔。

; 實作單個介面
user json {
name str
age i64
}

; 實作多個介面
file enter, leave {
path str
fd i64
}

跨模組介面實作​

當實作其他模組定義的介面時,介面名必須帶模組前綴(ShortName.)。參見跨模組調用前綴。

; ❌ 錯誤:db、rows、stmt 是 sql 模組定義的介面,不能省略前綴
db-mysql db {
fd i64
}

; ✅ 正確:使用 sql.db、sql.rows、sql.stmt
db-mysql sql.db {
fd i64
}

rows-mysql sql.rows {
fd i64
}

stmt-mysql sql.stmt {
fd i64
}

內置介面(enter、leave)和同檔案定義的介面不需模組前綴。

方法​

方法定義在類型上,使用 . 引用接收者(receiver)。

語法​

type.method-name = (params) (results) {
; . 是接收者
}

規則​

  1. 方法名使用 type.method 格式,type 必須是已定義的類型
  2. 接收者不需要顯式聲明參數,在方法體內用 . 引用
  3. 調用時使用 receiver.method(args) 語法
  4. 返回值放在第二組括號中,與普通函數一致
  5. 語法糖脫糖:type.method = (inputs) (rest-outputs...) {} 等價於普通函數 method = (inputs) (self type, rest-outputs...) {}。調用 instance.method(args) 時,instance 綁定到輸出列表第一個參數 self;函數體內 . 指代輸出區 self; 函數對 self 的寫入直接修改原實例。輸出列表除 self 以外的其餘輸出, 支持可選解構接收或直接丟棄。

示例​

; str 方法
str.to-upper = () (out str) {
out = with-cap(.len-bytes())
i <- [0...len-bytes()): {
c = .byte(i)
{
c >= 97 && c <= 122 -> out[i] = c - 32
-> out[i] = c
}
}
}

; char 方法
char.is-digit = () (result bool) {
result = false
. >= 48 && . <= 57 -> result = true
}

; struct 方法
user {
name str
age i64
}

user.greet = () {
print('Hello, ' - .name)
}

; 調用
s = 'hello'
u = s.to-upper() ; receiver.method()
c char = 5
d = c.is-digit() ; receiver.method()
u = user{
name: 'Alice'
age: 30
}
u.greet()

接口​

; 定義接口
json {
to-json()
}

; 接口默認實現
json.to-json = () {
}

; 接口實現
user json {
name str
age i64
}

; 重寫 + 調用父實現
user.to-json = () {
; 父實現
..to-json()
}

user.other = () {
; 當前實現
.to-json()

; 父實現
..to-json()
}

特殊接口​

file enter, leave {
}

枚舉​


; red=0, green=1, blue=2
color {
red,
green,
blue,
}

; 在普通方法中,a,b,c 實際是定義的a=0,b=1, c=2... 這是和其他語言不一致的地方。
; 所以正常不能用逗號的方式定義多個變量

; 這是一個特殊枚舉, 可以有類型,有逗號, 有別名
enum-name {
a t,
b u,
c v,
}

; 注意這是一個普通的struct,多個字段沒有逗號
struct-name {
a t
b u
c v
}

標籤列舉(帶載荷)​

變體可攜帶具名載荷欄位,寫成括號形式 變體(欄位 型別);無載荷的變體直接寫裸名。 一個變體可帶多個欄位(多欄位載荷在堆上以合成 struct 表示):

; 標籤列舉:變體可帶載荷,tag 依聲明順序為 0,1,2...
result {
ok(v t), ; 有值:載荷欄位 v,型別 t
nil, ; 空值:無載荷
err(e str), ; 錯誤:載荷欄位 e,型別 str
}

shape {
circle(r f64), ; 單欄位
rect(w f64, h f64), ; 多欄位
dot, ; 無載荷
}

匹配時用變體名作為臂模式;帶載荷的變體可析構綁定載荷(多欄位按位置一一綁定):

r result = ok(42)
r: {
ok(v) -> print(v) ; 把 ok 的載荷綁定到 v
nil -> print('empty')
err(e) -> print(e)
}

s shape = rect(3.0, 4.0)
s: {
circle(r) -> print(r) ; 綁定單欄位
rect(w, h) -> print(w * h) ; 按位置綁定多欄位
dot -> print('dot')
}

變體可在表達式位置直接建構,作為函數引數或其它變體的載荷:

x f64 = perimeter(rect(3.0, 4.0)) ; 內聯建構並傳參
o outer = wrap(a(5)) ; 枚舉載荷本身是另一個枚舉

命名空間與裸名解析:編譯器內部以「完整限定名」登記變體(模組.枚舉.變體, 如 option.option.ok、some-mod.my-result.ok),不同枚舉的同名變體互不衝突。 使用者只寫裸名(ok、rect),由編譯器依被匹配/被建構變數的靜態型別自動補全 完整命名。因此兩個枚舉即使都有 ok 變體、且 tag 順序不同,也能正確區分。

也可寫舊式空格分隔型別 ok t,等價於 ok(v t)(省略欄位名)。 標籤列舉的變體名稱會登記進編譯器的列舉變體表,供匹配與窮盡性檢查使用。

內建標籤列舉(#{buildin})​

在標籤列舉前加 #{buildin} 表示這是內建列舉:變體(名稱、順序、載荷型別)供 匹配與窮盡性檢查使用,但底層表示與構造由 runtime/builtin 提供,不生成使用者可見的 struct/union。?t option 即以此方式聲明:

; src/std/global.no
#{buildin}
option {
ok(v t),
nil,
err(e str),
}

option 的 tag 依序為 0=ok、1=nil、2=err,與內建 option 表示一致。使用者無需 自行聲明 option;此處的 #{buildin} 讓內建 option 的變體來源單一化(見「可空類型(option)」)。

枚舉註解與內存佈局​

枚舉支援 #{...} 註解,位置與其他目標相同(見〈註解位置〉):上方(獨立成行)或 後方(同一行尾隨);寫在目標前方同一行是編譯錯誤。

註解可放在兩處:

  1. 整個 enum 的上方 —— 作用於 enum 定義本身(如 #{buildin}、#{inline=true});
  2. 單個成員的上方或同行尾隨 —— 作用於該成員(C 風格枚舉的「值」與標籤列舉的「變體」皆可)。
; ① 整個 enum:獨立成行置於 enum 上方
#{inline=true}
box {
; ② 單個變體:獨立成行置於變體上方
#{doc = '有值'}
full(v i64),
; ② 單個變體:同一行尾隨
empty #{doc = '空值'},
}

; `#{inline=false}`:明確寫出「不用 inline 佈局」
#{inline=false}
plain {
a,
b,
}

color {
; C 風格枚舉的單個值同樣支援這兩種寫法
#{deprecated}
red,
green #{deprecated},
blue,
}

註解不改變 enum 的結構:變體名稱、宣告順序(tag)、載荷欄位與型別都不受註解影響, 匹配與窮盡性檢查也完全不變。註解只是附加在定義上的元數據。

內存佈局(棧式)​

標籤列舉在記憶體中採棧式佈局(inline stack form)——一個按值存放的小物件,載荷不散落在堆上:

%tenum_<name> = type { i64 tag, [N x i64] payload }
  • tag 是判別式(i64),依宣告順序取 0, 1, 2...;
  • payload 是所有變體共用的槽位陣列(union 語義:每個變體把自己的欄位寫進同一塊儲存);
  • N 是所有變體中最寬者所需的 8 位元組槽數,最小為 1(不會是零寬)。

因此大小是:

情形N大小
所有變體皆無載荷(如 color { red, green, blue } 這類純標籤)116 位元組
最寬變體為 i64 / f64 / 指標116 位元組
最寬變體為 ?T224 位元組
最寬變體為 str / vec / []T332 位元組
最寬變體為結構體 TT 各欄位槽數之和(遞迴展開)依 T 而定

也就是說:默認就是 16 位元組起,只有當某個變體的載荷確實更寬時才會長大—— 「默認固定 16 位元組」與「大小隨變體變化」是同一件事的兩面。

#{inline} 的三種寫法​

inline 是一個布爾註解,有三種寫法,且值會被真正遵循:

寫法含義
#{inline}簡寫,等於 inline=true(獨立鍵即為真)
#{inline=true}明確為真(#{inline=1} 亦可)
#{inline=false}明確為假(#{inline=0} 亦可)

非布爾值是編譯錯誤(#{inline=foo}、#{inline='true'}),不會被靜默當成真: ValidateFieldTags(TraceID fieldtag1)對結構體欄位與 enum 定義都會報。

在整個 enum 上方寫 #{inline=true} 是棧式佈局的顯式標記,表示「此枚舉使用棧式佈局」; 編譯器把它記在 TaggedEnumInfo.Inline 上。目前的默認佈局本來就是棧式,所以加不加 註解、寫 true 還是 false,佈局完全相同——標記的作用是讓意圖明確,並在日後若變更默認 佈局時作為穩定的選擇開關。

#{inline} 寫在整個 enum 上方表示「這個枚舉用棧式佈局」;寫在單個變體上目前 不改變佈局,僅保留為該變體的元數據(與結構體欄位不同,見下節)。

結構體欄位上的 #{inline}​

與 enum 不同,結構體欄位上的 #{inline} 是有實際效果的:它直接決定該欄位是 按值內聯在宿主結構體內(%pt),還是存成指標(ptr)。

pt {
x i64
y i64
}

holder {
inl pt #{inline=true} ; 按值:結構體直接長在 holder 裡
out pt #{inline=false} ; 指標:欄位存的是指向 pt 的指標
bare pt #{inline} ; 簡寫,等於 inline=true
}
寫法欄位佈局
#{inline} / #{inline=true} / #{inline=1}按值內聯在宿主內(%pt)
#{inline=false} / #{inline=0}指標(ptr)
不寫取決於 NOLANG_FIELD_PTR(見下表)
#{inline=foo} / #{inline='true'}編譯錯誤——不是布爾

沒有註解時的默認佈局由 NOLANG_FIELD_PTR 決定。 mir.FieldPtrLayout 等於 os.Getenv("NOLANG_FIELD_PTR") != "",默認為關;開啟它會啟動「Phase 1 佈局翻轉」, 把結構體欄位的默認由按值改成指標。

欄位旗標關(默認)旗標開(NOLANG_FIELD_PTR=1)
無註解%pt(按值)ptr(指標)
#{inline} / #{inline=true}%pt%pt
#{inline=false}ptrptr

也就是說:註解永遠生效,不受該環境變數影響,旗標只改「沒寫註解時」的默認。因此在 默認配置下,#{inline=false} 是「主動要求指標」,而 #{inline=true} 只是把本來就如此 的佈局再寫明一次;一旦翻轉開啟,兩者的角色就反過來。

檢查器有兩條規則(TraceID fieldtag1,ValidateFieldTags):

  1. 只對結構體型別的欄位有意義。 標量 / str / vec / 陣列 / 切片 / map / option / 指標欄位本來就一律按值存放,寫 #{inline} 等於什麼都沒說,因此報錯;寫 #{inline=false} 同樣報錯,因為那等於宣稱了與編譯器行為相反的佈局。

  2. 按值內聯不得成環。 被內聯的結構體存放在宿主內部,所以 a { #{inline} b b } 與 b { #{inline} a a } 併存時大小無限,必須報錯(直接自我 引用是退化情形)。只有 =true 才會建立按值邊,所以互相遞迴的結構體在兩側都寫 #{inline=false} 時合法。

    ⚠️ 「不寫注解」不等於「不用 inline」。 默認配置下不寫注解就是按值內聯,因此 a { x b } 配 b { y a }(都不寫注解)在默認配置下並不合法,會以 opt: error: identified structure type 'b' is recursive 失敗——而且這條錯誤目前 不會由檢查器報出,要到 LLVM 驗證階段才炸。要在默認配置下寫遞迴型別,必須 顯式寫 #{inline=false}。只有 NOLANG_FIELD_PTR=1 時,「不寫」才等於指標。

遞迴型別:為什麼需要 inline=false​

一個按值內聯自己的結構體大小無限,所以自我引用不能按值存放。在默認配置 (NOLANG_FIELD_PTR 關閉)下,結構體欄位預設就是按值,因此 node 要能參考自己, 唯一的寫法就是明確要求指標:

node {
v i64
next node #{inline=false} ; 指標:這樣 node 才有有限大小
}

n node
n.v = 1
n.next.v = 2
print(n.next.v) ; 2

若把上面的 #{inline=false} 拿掉(或寫成 #{inline=true}),LLVM 會直接拒絕這個 型別:identified structure type 'node' is recursive。

指標欄位擁有它的 pointee,生命週期由編譯器處理:

  • pointee 在首次寫入時惰性分配(@__nolang_get_<T>,內含 malloc + 清零), 不是在建構宿主時一次分配;
  • 宿主離開作用域時 @__nolang_drop_<T> 會空指標檢查後釋放它;
  • 按值拷貝(b = a、作為參數傳遞、寫入容器)會深拷貝 pointee,因此兩個宿主 不會共用同一塊記憶體、也不會被釋放兩次。

端到端測試見 tests/field-inline-annotation.no。

枚舉值引用​

規則:枚舉值必須使用 枚舉類型.值 的限定方式引用,不能直接使用裸值。 這樣可以防止命名衝突,也使外部包無法直接使用具體值。

; ❌ 錯誤:直接使用裸值
kind = null
yes = e.is(io)

; ✅ 正確:使用限定方式
kind = json-kind.null
yes = e.is(code.io)

枚舉類型可用於結構體字段類型、函數參數類型和返回值類型。 定義枚舉的模組內部和外部都應使用 枚舉類型.值 的方式引用枚舉值。

enter/leave​

實現了 enter / leave 接口的類型,在作用域進入和離開的時候自動調用:

file enter, leave {
path str
}

file.enter = () {
.open()
}

file.leave = () {
.close()
}

read-file = () {

; 自動 f.enter()
f = file{
path: 'data.txt',
}

; 使用 f
; 自動 f.leave()
read(f)
}

可空類型(option)​

在類型前面加 ? 表示可空類型:

可空類型變量可以合法持有空值/错误值,編譯器會進行相應的空值檢查。

?t 是內建標籤列舉,其變體定義於 src/std/global.no:

#{buildin}
option {
ok(v t), ; tag 0:有值
nil, ; tag 1:空值
err(e str), ; tag 2:錯誤
}

#{buildin} 表示底層表示與構造由內建 runtime 提供(%option 型別、ok(...)/nil/err(...) 構造器),option 的列舉聲明僅用於匹配與窮盡性檢查。詳見「標籤列舉」與「內建標籤列舉」。


o ?i64
o = nil ; 設為空
o = 42 ; 設為有值
o = err('msg') ; 設為錯誤

nullableValue ?[]str
nullableString ?str

; 修改可空類型
nullableString = 'test'

; 設置錯誤
nullableString = err('some error')

; 可通過match判斷
x: {
err -> log(it)
nil -> log(it)
->
do-right-thing(it)
}

; 強制解包
; 取消實現
//!x.say()

風格指引:使用 ?t option 取代 (val, ok)​

當函數可能失敗或返回空值時,應優先使用 ?t option 類型,而非 (val t, ok bool) 雙返回值模式。

?t 是標籤列舉,有三種狀態:ok(有值)、nil(空值)、err(錯誤)。正常值會隱性綁定,當操作只是找不到值時用 nil,當操作遇到實際錯誤時用 err(...)。

; ❌ 不推薦:雙返回值模式
stack.pop = () (val i64, ok bool) {
.n == 0 -> return
val = .data[.n]
ok = true
}

; ✅ 推薦:option 類型(nil 表示空,err 表示錯誤)
stack.pop = () (val ?i64) {
.n == 0 -> {
val = nil
return
}
val = .data[.n]
}

; ✅ 返回錯誤
file.read = () (data ?str) {
.fd < 0 -> {
data = err('file not open')
return
}
; ... 讀取資料
data = buf
}

使用 match 解包 option:

val = s.pop()
val: {
nil -> print('empty')
err -> print(it) ; it = 錯誤訊息
-> print(it) ; it = 彈出的值
}

整數運算溢出(#{overflow} 註解)​

Nolang 不支援 panic——任何可能溢出的操作都必須以 option 或顯式註解處理,絕不會在執行期拋出整數溢位異常。下列整數四則運算的溢出行為由 #{overflow = ...} 註解控制:

  • 有號與無號的 + - *:左右運算元為整數(含 int 字面量)時皆適用;
  • 有號 /:僅 INT_MIN / -1 這一種情況會溢出(無號除法 a/b ≤ a 永不溢出,不適用)。

無號整數(u8/u16/u32/u64/u128)的 + - * 與有號整數遵循完全相同的規則:未標註時同樣預設回傳 option<int>,溢出 → err。這是設計上的刻意選擇——「只要無法在編譯期證明不可能溢出,就按相同規則處理」。

模式註解溢出時返回型別適用場景
默認(未標註)—溢出 → err,正常 → ok(value)option<int>需要偵測溢出的運算
回繞#{overflow = wrap}靜默 2's 補數回繞int(plain)哈希、密碼學、計數器等語義上容許回繞
歸零#{overflow = clamp0}溢出(上溢或下溢)取 0int(plain)差值不得為負的語義(如剩餘量)
最小值箝位#{overflow = min}溢出箝位到型別最小值int(plain)計數器下限、索引保護
最大值箝位#{overflow = max}溢出箝位到型別最大值int(plain)容量上限、飽和累加
飽和#{overflow = saturate}上溢→最大值、下溢→最小值int(plain)訊號/顏色飽和等

此外,所有模式都支援型別前綴形式,精確指定某個窄型別的飽和界限,例如 #{overflow = u8-max}、#{overflow = i8-min}、#{overflow = u16-saturate}。

註解粒度:#{overflow = ...} 是行注解——只作用於緊接其後的下一條陳述。這是唯一完整受支援的形式:執行語意(codegen)與 ovfhndld 硬錯誤都只認它。

⚠️ 放在函數定義上方不再涵蓋整個函數體。它只會讓 ovf-int-default lint 跳過整個函式(清單層面看起來「乾淨」),但函式體內的未標註運算仍會觸發 ovfhndld 硬錯誤,實測與完全不寫註解無異。請逐陳述標註;no fmt --fix=overflow 產生的正是這種逐陳述形式。

LSP 快速修復:編輯器(nolang-lsp)會對未標註的整數運算報出錯誤(nolang-overflow,trace id ovf-int-default),並提供五個 quickfix——Add #{overflow = wrap} / clamp0 / min / max / saturate——自動在該運算所在語句上方、以與該行一致的縮排插入對應註解,把預設的 option<int> 結果改為 plain int。命令列等價物為 no fmt --fix=overflow(見下文)。

默認:返回 option<int>​

未標註時,a - b 的結果型別是 option<int>。接收端必須是 ?T(可空型別),並用 match 解構 err / nil / ok 三個分支:

main = () {
x i64 = 100
d ?i64 = x - 1 ; 默認:返回 option<i64>
d: {
err -> print(-1) ; 溢出(如 x - 2^63 下溢)
nil -> print(0)
-> print(1) ; ok(value):正常結果
}
}
main()

%option 的 data 欄為 i64;窄型別(i8/i16/i32)的結果會先符號擴展(sext)再存入,解包時再截斷回原寬度。

wrap:靜默回繞​

#{overflow = wrap}
sub-wrap = (a i64, b i64) (r i64) {
r = a - b ; 溢出時靜默 2's 補數回繞,返回 plain i64
}

語句級:

x i64 = -9223372036854775807
#{overflow = wrap}
w i64 = x - 2 ; 下溢回繞 → 9223372036854775807
print(w)

clamp0:溢出歸零​

x i64 = -9223372036854775807
#{overflow = clamp0}
c i64 = x - 2 ; 下溢 → 0(上溢同樣歸零)
print(c)

min / max / saturate:飽和箝位​

三種模式把溢出結果「夾」在型別可表示區間內,而非回繞或歸零:

#{overflow = min}
floor = (a i64, b i64) (r i64) {
r = a - b ; 下溢 → i64 最小值;上溢 → i64 最大值
}

#{overflow = max}
cap = (a i64, b i64) (r i64) {
r = a * b ; 上溢 → i64 最大值;下溢 → i64 最小值
}

#{overflow = saturate}
sat = (a i64, b i64) (r i64) {
r = a + b ; 上溢→max、下溢→min
}

型別前綴形式可精確控制窄型別的飽和界限(函數級註解會套用到體內所有對應型別運算):

#{overflow = u8-max}
inc = (x u8) (r u8) {
r = x + 1 ; x = 255 時上溢 → 255(而非回繞成 0)
}

#{overflow = i8-min}
dec = (x i8) (r i8) {
r = x - 1 ; x = -128 時下溢 → -128(而非回繞成 127)
}

編譯器強制​

未標註的整數運算預設回傳 option<int>,因此「未處理」會被強制攔下。是否算未處理採並行判斷——滿足下列任一項即不報錯:

  • ?= 上拋(要求函式有 option 結果參數);
  • 就地捕獲:用 = 綁定到一個新變數(變數自動推斷為 ?T),或顯式宣告 ?T(如 x ?i64 = a + b);
  • _ = expr 顯式丟棄(求值本身走安全路徑,錯誤一併忽略);
  • 加註解 #{overflow = wrap} / clamp0 / min / max / saturate(或型別前綴形式如 u8-max、i8-min),聲明接受回繞/飽和語義。

四者皆無時才報錯(ovfhndld)。

純算術不是可錯源:+ - * << 與取負本身不觸發捕獲(否則所有算術都會變成 option,標準庫爆炸)。所以 d = x - 1(不含 / %、不含安全索引、不含 option 運算元)仍然報錯——請加註解,或寫成 d ?i64 = x - 1。

既有變數不改型別:捕獲只對「本陳述首次宣告」的目標生效。若變數已經存在(例如先有 d = 0 再 d = x - 1,或先有 v i64 = 0 再 v = arr[i]),編譯器不會把它就地改成 option,而是報錯要求你顯式選擇語義。

另有兩條硬性錯誤不受上述豁免影響:

  • 未標註的整數運算若賦值給顯式 plain int 變數,編譯器報錯(cannot assign ?i64 value to i64 variable),強制你加 #{overflow = wrap} / #{overflow = clamp0} / #{overflow = min} / #{overflow = max} / #{overflow = saturate}(或型別前綴形式)或改用具名 ?T。
  • 未標註的整數運算若賦值給已存在、且型別非 option 的變數(d = 0 之後的 d = x - 1),編譯器報錯並提示加上註解或宣告 ?i64,避免產生非法 IR。

其餘規則:

  • 無號整數運算(u8/u16/u32/u64/u128)同樣適用此機制:未標註時 + - * 預設回傳 option<int>,溢出 → err,與有號整數規則一致。
  • i128 運算不支援 option 路徑(data 欄僅 i64),統一退化為靜默回繞以保留正確性。

為什麼默認是 option 而非 wrap? 靜默回繞會隱藏溢出 bug;預設回傳 option 強制呼叫方顯式處理溢出(或主動標註 wrap 表明「我接受回繞語義」),把「是否容許溢出」變成可見的設計決策。

未標註運算列為錯誤(ovf-int-default)​

no vet 把未標註的整數運算回報為錯誤(trace id ovf-int-default),而非提示。原因:預設的 option<int> 一旦被當成普通整數使用就是靜默的語意漂移,必須顯式二選一——加 #{overflow = ...} 註解,或用 ?T + ?= / match 顯式處理。

自動修復:no fmt --fix=overflow​

批量補註解不必手寫,用 no fmt 的修復類別參數:

no fmt --fix=overflow -w src/std # 就地修復整個目錄
no fmt --fix=overflow -d src/std/str.no # 只印 diff,不寫檔
no fmt --fix=overflow src/std/str.no # 印出修復後的內容

修復是精準且不污染的:只在 lint 實際報告的那條運算所在陳述上方插入 #{overflow=wrap},不改動其他陳述、不重排版、不動既有註解,且重跑冪等(已標註者不再變動)。這與直接跑 no fmt -w 不同——後者是完整重新格式化。

修復後請再跑一次 no vet 確認歸零。絕大多數情況一次即清空(src/std 由 4728 條降為 0)。已知的唯一例外形狀:報告落在 match arm 的條件上,而該 arm 本體沒有任何可標註陳述——行注解只作用於其後的第一條陳述,涵蓋不到 arm 自己的條件運算,此時 no vet 會再次報出。這種情況請先把該運算抽成獨立陳述再標註。

自動修復:冗餘型別標註​

當變量聲明的類型標註與右側值推斷出的類型完全相同時,該標註是冗餘的(no vet 會以 hint tcpoxtfd、type annotation '<T>' can be omitted (inferred from value) 報出)。

  • 預設 no fmt 就會自動去除:no fmt -w 在格式化時一併移除冗餘標註(同時做正常重新格式化),no fmt -d 預覽含此變更,-w 寫入。
no fmt -w main.no # 格式化並去掉冗餘標註
no fmt -d main.no # 只印 diff(含標註移除)
no fmt -w src/ # 整個目錄
  • --fix=redundant(手術式、只動標註):若只想移除冗餘標註、不要完整重新格式化(例如對 src/std,因 no fmt 會丟掉 #{index-out}),用此類別——它只刪標註、不重排版:
no fmt --fix=redundant -w src/std # 就地移除整個目錄的冗餘標註(不重排版)
no fmt --fix=redundant -d src/std/str.no # 只印 diff,不寫檔
no fmt --fix=redundant src/std/str.no # 印出修復後的內容
; 修復前
x str = ''
b i64 = 5
m [str]i64 = make-map()

; 修復後(標註 == 推斷型別,去掉)
x = ''
b = 5
m = make-map()

規則要點:

  • 標註型別 == 推斷型別才移除(如 str/i64/[str]i64 與其推斷值一致)。
  • 可隱式轉換但不相等的情況不會移除——保留標註才有轉換語意。例如 c i16 = 5(5 推斷為 i64,與 i16 不等)、p ?i64 = nil、last txt = ''(txt 與 str 可隱式互轉但型別不同)。
  • 無型別標註的寫法(如 d = 'x')本就不會被報,自然也不動。
  • 定長陣列標註 [N]T / [?]T 一律保留(永不視為冗餘)。[N] 不是單純的型別提示,而是決定表示形式的開關:寫了它,右側的 [...] 字面量才會被解析成固定長度的棧陣列;寫了它,推斷結果才會「剛好等於」標註——這是標註自己造出來的循環論證。刪掉後同一行重解析就變成堆上可增長的切片 []T,語意隨之改變:fixed [3]char = ["x", "y", "z"] 的 .push() 無效(.len() 恆為 3),退化成 fixed = ["x", "y", "z"] 後 .push() 會讓 .len() 變 4。切片標註 []T 不在其列——它刪掉後仍是切片,屬真冗餘。
  • 含十六進位字面量的值一律保留標註(保守例外)。no vet 的型別推斷對 0xNN 採用「推斷為 byte」的啟發式,與編譯器「無標註時整数字面量預設 i64」的實際語義不同,因此 IP-TBL [64]byte = [0x3a, ...]、s []byte = [0x50, 0x4b] 會被誤判為冗餘;一旦移除,字面量會退化成 i64 元素而產生 expected '[]byte', got '[]i64' 之類的型別錯誤。故只要值運算式文本中出現 0x/0X,hint 與移除都不會發生。

不論走預設 no fmt 還是 --fix=redundant,移除都只刪除被報告的那個型別標註及其前導空白、重跑冪等(已移除者不再變動)。修復後建議再跑一次 no vet 確認 tcpoxtfd 歸零。

註解綁定與單行合併​

#{...} 註解綁定到節點,因此同一條陳述可同時帶多個鍵;多個註解一律合併成單行:

i <- [0..16): {
#{index-out = 0, overflow=wrap}
buf[base + i] = data[i]
}

上面這一條同時需要「越界取默認值」(index-out)與「溢出回繞」(overflow)兩個語意。分成兩行寫解析結果相同,但 no fmt --fix=overflow 產出合併後的單行形式。

行注解的作用範圍:#{overflow = ...} 只作用於緊接其後的下一條陳述。中間若夾著無關的註解(如 #{index-out = 0})仍會正確套用到該陳述。安全索引寫入(arr[base + i] = v、.buf[.pos + i] = v)會被 desugar 成 match 形式,#{overflow = wrap} 會一併帶到 desugar 之後的節點,故索引算式同樣受註解管轄,不會被誤報為未標註。

錯誤上拋(?= 運算子)​

當函數返回 option 類型時,可以使用 ?= 運算子自動解包 option 並向上拋錯誤,簡化錯誤處理流程。

語法:

v ?= expr

語義:

  • 如果 expr 返回 ok(value),則 v 被賦值為解包後的內部值(自動解包)
  • 如果 expr 返回 nil 或 err,則將當前函數的 option 結果參數設為該值,並執行 return(自動上拋)

限制: ?= 只能在具有 option 類型結果參數的函數中使用。如果當前函數沒有 option 結果參數,編譯器會報錯。

示例:

; 逐行讀取檔案並處理 — 使用 ?= 簡化錯誤傳播
process-file = (path str) (result ?str) {
result = nil
f ?= open(path) ; 失敗時自動 result = f; return
line ?= f.read-line() ; EOF 或錯誤時自動上拋
result = line
}

等價的展開形式(編譯器自動生成的 match 鏈):

process-file = (path str) (result ?str) {
result = nil
__tmp = open(path)
__tmp: {
nil || err -> {
result = __tmp
return
}
-> f = it
}
__tmp2 = f.read-line()
__tmp2: {
nil || err -> {
result = __tmp2
return
}
-> line = it
}
result = line
}

鏈式使用: 多個 ?= 可以串聯使用,實現管道式錯誤傳播:

pipeline = (input str) (result ?str) {
result = nil
a ?= step1(input) ; 失敗則上拋
b ?= step2(a) ; 失敗則上拋
result = b
}

裸算術運算: ?= 的右側不限於函數呼叫——任何回傳 option 的整數運算(如未標註的 a - b、a + b、a * b)都可以直接用 ?= 上拋溢出錯誤,無需先綁定到中間變數:

; 溢出時自動 result = err; return,正常時 v 解包為內部值
sub-safe = (a i64, b i64) (result ?i64) {
result = nil
v ?= a - b ; 下溢/上溢 → err 自動上拋;否則 v = 內部 i64
result = v
}

add-safe = (a i64, b i64) (result ?i64) {
result = nil
v ?= a + b ; 同上,適用於 + * 與有號 /
result = v
}

等價展開(編譯器自動生成):

sub-safe = (a i64, b i64) (result ?i64) {
result = nil
__unwrap = a - b ; option<i64>
__unwrap: {
nil -> { result = __unwrap; return }
err -> { result = __unwrap; return }
-> v = it ; ok(value):解包內部值
}
result = v
}

複合右側(compound RHS): ?= 的右側可以是一個含多個 option 運算元的複合運算式。每個 option 運算元會被自動解包,並注入傳播守衛——只要任一運算元為 nil / err,當前函數的 option 結果參數即被設為該值並 return(向上拋);全部成功才把內部值賦給左值:

sum3 = () (result ?i64) {
result = nil
a ?i64 = ok(10)
b ?i64 = ok(20)
c ?i64 = ok(30)
total ?= a + b + c ; 每個 option 運算元自動解包並傳播
result = total
}

comp = () (result ?i64) {
result = nil
a ?i64 = ok(4)
b ?i64 = ok(6)
v ?= dbl(a + b) ; a + b 可能是 option,同樣自動解包並傳播
result = v
}

只有「運算元位置」上的 option 會被自動解包(如 a + b + c、f(b + c));作為函式引數直接傳入的 option 變數不在此列——它會走 ?= 既有的裸 option 引數豁免(a ?= f(v),其中 v 為 ?i64、f 接收 plain i64)。

非 option 右側報錯: 為了避免大範圍地把 = 誤寫成 ?=,當右側在編譯期可被確定為「明確非 option」時,編譯器會報錯並提示改用 =:

  • 字面量:v ?= 42
  • 常數可折疊的整數運算:v ?= 10 + 20
  • 已知型別且非 option 的區域變數:v ?= x(其中 x i64 = 5)

報錯訊息形如:`?=` RHS `42` is not an option — use `=` instead。

算術運算只要任一運算元是 option(或型別未知 / 為函式呼叫,可能回傳 option),即視為「可能 option」而放行,不會誤報——例如 v ?= a + b + c(a/b/c 為 ?i64)或 v ?= b + c(b/c 未知型別)都可正常編譯。

適用場景:

  • pop / peek 等可能為空的容器操作 → ?t(nil = 空)
  • read-line / read-byte 等 I/O 操作 → ?str / ?i64(nil = EOF,err = 錯誤)
  • lookup / get 等查找操作 → ?t(nil = 未找到)
  • parse / from-str 等解析操作 → ?t(nil = 空,err = 無效輸入)
  • accept / dial 等網路連接 → ?conn(nil = 無連接,err = 錯誤)

nil vs err: 當缺失是正常/預期的結果(空堆疊、鍵不存在、EOF)時用 nil;當缺失代表實際的錯誤狀態(I/O 失敗、輸入無效、連接被拒)時用 err('msg')。

例外: 當函數需要返回多個獨立值(如 (name str, value str, ok bool))時,可保留多返回值模式。

錯誤捕獲賦值(= 就地捕獲)​

?= 的語義是「錯誤向上拋」——它要求所在函式有 option 結果參數,失敗時提前 return。當你不希望提前返回、而是想在原地處理錯誤時,用普通 =:只要右側含可錯源,目標變數就會自動推斷為 ?T,nil / err 就地存進該變數,流程繼續。

handle = (b i64, c i64, d i64) (r i64) {
a = b + c / d ; a 推斷為 ?i64;除零 → a = err
a: {
err -> { r = -1 } ; 就地處理錯誤
nil -> { r = -1 }
-> { r = it } ; 成功:r = b + c/d
}
}

可錯源(捕獲觸發源):

來源例捕獲結果
整數除法 / 取模a = b + c / d、a = b % c除零、MIN / -1 → err
安全索引a = v[i](arr / vec / slice)越界 → nil
option 型運算元a = x + 1(x ?i64)x 為 nil / err 時原樣存入 a
回傳 option 的呼叫(作算術運算元)a = f(x) + 1(f 回傳 ?i64)同上

作為函式引數直接傳入的 option 不在此列——那交給被呼叫方處理(與 ?= 的裸 option 引數豁免一致)。

純算術不是可錯源:+ - * << 與取負本身不觸發捕獲(否則所有算術都會變成 option)。但一旦落在 option 路徑上(例如右側另有 /,或目標顯式宣告 ?T),這些運算會帶運行期溢出檢查,溢出 → err:

ovf = (x i64) (r i64) {
a ?i64 = x + 1 ; 顯式 ?T → 走 option 包裝路徑
a: {
err -> { r = 0 } ; x = i64-max 時溢出 → err
nil -> { r = 0 }
-> { r = it }
}
}

捕獲 vs 上拋:

寫法失敗時需要 option 結果參數之後如何取值
a ?= expr設結果參數為 nil/err 並 return(提前返回)是不適用(已返回)
a = expra 就地存 nil/err,流程繼續否a: { ok -> ... }
a ?T = expr同 =(顯式標註,含內部可錯子表達式)否同上
_ = expr求值但丟棄結果與錯誤(不報錯、不觸發 unused lint)否不適用

適用範圍: 安全索引的捕獲與 ?= 一樣,只在回傳 ?T 結果的函式內生效(見〈安全索引〉第 3 種寫法)。/ % 與 option 運算元的捕獲則適用於所有函式。

既有變數不改型別: 捕獲只對「本陳述首次宣告」的目標生效。若目標已經存在(v i64 = 0 之後的 v = arr[i]、或 d = 0 之後的 d = x - 1),編譯器不會就地改變它的型別,而是報錯要求顯式選擇語義(加 #{overflow=...} / #{index-out=...} 註解、用 ?=、或宣告 ?T)。

泛形​

arr_to_vec = (arr [n]t) (out []t) {
i <- [0..n): {
out[i] = arr[i]
}
}

類型強制轉換​


; 返回類型名稱字符串
a = typeof(x)

; `as` 僅允許用於 FFI 指標型別轉換(如 *byte、**byte、*i64)
; 整數內部皆為 i64,無需顯式轉換
y = x as *byte

整數賦值型別檢查​

編譯器會對整數賦值進行型別檢查,防止不安全的窄化(narrowing)導致資料遺失。以下是規則:

隱式拓寬(安全,自動允許)​

窄整數型別的值可以自動賦值給更寬的型別,因為目標型別的範圍完全包含來源型別:

b byte = 200
i i64 = b ; ✓ byte 範圍 [0,255] ⊆ i64 範圍
u u32 = b ; ✓ byte 範圍 ⊆ u32 範圍

整數字面量賦值​

整數字面量(預設推斷為 i64)可以賦值給任何範圍包含該值的整數型別:

n u8 = 200 ; ✓ 200 ∈ [0,255]
m u8 = 300 ; ✗ 300 > 255,編譯錯誤
big u64 = 18446744073709551615 ; ✓ 2^64-1,u64 最大值

不安全的窄化(編譯錯誤)​

將寬型別的變數直接賦值給窄型別會導致編譯錯誤,因為可能造成資料遺失。錯誤訊息會附帶可操作的修復提示,建議如何用位元運算安全窄化:

d u64 = 42
h u32 = d ; ✗ cannot assign u64 value to u32 variable 'h'; hint: narrow safely with a bitwise mask (e.g. `& 4294967295`) or right shift (e.g. `>> 32`)
h u16 = d ; ✗ cannot assign u64 value to u16 variable 'h'; hint: narrow safely with a bitwise mask (e.g. `& 65535`) or right shift (e.g. `>> 48`)
h u8 = d ; ✗ cannot assign u64 value to u8 variable 'h'; hint: narrow safely with a bitwise mask (e.g. `& 255`) or right shift (e.g. `>> 56`)
x u32 = d + 1 ; ✗ 加法結果仍為 u64,不安全
y u32 = foo() ; ✗ 函式呼叫結果型別不匹配

修復提示:編譯器會根據目標型別自動計算精確的 mask 值和位移量。按照提示套用 mask 或位移後,即可安全窄化(見下節)。

有號目標型別:對 i8/i16/i32/i64 等有號整數,提示會說明位元窄化不適用(符號位元截斷語義不明確),建議改用顯式範圍檢查。

位元運算安全窄化(自動允許)​

當賦值的右側是位元運算表達式(&、|、^、<<、>>)且目標型別為無號整數(u8/u16/u32/u64/byte)時,編譯器允許隱式窄化——因為截斷高位是位元操作的標準語義,不會造成非預期的資料遺失:

d u64 = 42

; ✓ mask 運算:結果必 ≤ mask 值,安全落入 u32
h u32 = d & 67108863 ; mask = 2^26-1 < 2^32
h u32 = d & 4294967295 ; mask = 2^32-1,正好 u32 範圍

; ✓ 位移運算:右移後高位為 0,安全
hi u32 = d >> 32 ; u64 >> 32 留 32 bits

; ✓ XOR / OR 組合
c u32 = a ^ b ; 位元運算結果
b byte = v & 255 ; mask 到 byte 範圍

; ✓ 複合位元運算(常見於密碼學/編解碼)
s u32 = (key[0] & 255) | ((key[1] & 255) << 8) | ((key[2] & 255) << 16) | ((key[3] & 255) << 24)

為什麼允許? 位元運算(mask、位移、XOR、OR)的語義就是構造一個位元模式。賦值給窄的無號型別時,截斷高位是刻意的操作——開發者已透過 mask 或位移保證了結果的範圍,或刻意丟棄高位。這是密碼學(如 ChaCha20、Poly1305、Blake2)和編解碼代碼中的標準模式。

僅限無號目標型別。 對有號整數目標(i8/i16/i32/i64),即使右側是位元運算也會報錯,因為符號位元的截斷語義不明確:

d u64 = 42
h i32 = d & 4294967295 ; ✗ 仍報錯:有號目標不適用安全窄化

頂層必須是位元運算。 只有當表達式的頂層運算子是 &/|/^/<</>> 時才放行。加法、減法、函式呼叫、直接變數引用等不在此列:

d u64 = 42
h u32 = d ; ✗ 頂層是 Identifier,不是位元運算
h u32 = d + 1 ; ✗ 頂層是 +,不是位元運算

模块系统​

  • 每个文件就是一个模块
  • 文件名和文件夹名使用中连接符
utils/
└── helper.no ; 模块名为 utils/helper

導入模塊​

新式語法使用 # 導入。舊式 use 關鍵字仍可使用,但已廢棄(deprecated),建議改用 #。

; 標準庫(新式語法,推薦)
# std/math.add

; 遠程模塊(非std/開頭)
# github.com/utils/math.add

; 本地模塊,必須/開頭
# /utils/math.add

; 別名
# std/math.add a

; ── 以下為舊式語法(deprecated,仍可使用但不推薦)──
; use std/math.add
; use github.com/utils/math.add
; use /utils/math.add
; use std/math.add a

導出模塊​

僅適用於lib.no

@ std/math.add a

FFI(#{c} 註解)​

透過 #{c} 註解宣告外部 C 函式,實現 FFI(Foreign Function Interface)。

語法:#{c} 獨立一行,標記下一行為 FFI 宣告。#{c} 是註解系統的 FFI 語言鍵,也支援 #{cpp}、#{rust} 等其他語言。舊語法 #c 仍向後相容。

私有宣告:名稱以 _ 開頭表示私有(不導出),C ABI 符號自動去除前綴 _ 並將連字號轉為底線。

不再需要分開檔案:FFI 宣告與普通代碼可寫在同一個 .no 檔案中。

指針型別語法:FFI 中使用 C 風格的 *T、**T、***T 表示指針,必須有具體型別 T。普通代碼不能使用此語法。

語法含義LLVM IR用途
*byte指向 byte 的指針i8*不透明指標(如 db handle)
**byte雙重指針i8**輸出參數(如 sqlite3**)
***byte三重指針i8***極少見的三重間接
; sqlite.no — FFI 綁定與安全包裝在同一檔案
; 編譯器自動將連字號 (-) 轉為底線 (_) 以匹配 C ABI 符號
; 以 _ 開頭的名稱為私有,C ABI 符號自動去除前綴 _

; 基本型別參數
#{c}
c-strlen = (s str) (n i64)

; 指針參數(*byte = 不透明指標),私有宣告
#{c}
_sqlite3-close = (db *byte) (rc i32)

; 雙重指針(**byte = 輸出參數,呼叫後值自動存回變數),私有宣告
#{c}
_sqlite3-open = (filename str, db **byte) (rc i32)

; 多個指針參數,私有宣告
#{c}
_sqlite3-exec = (db *byte, sql str, callback *byte, arg *byte, errmsg *byte) (rc i32)
; 同一檔案中的安全包裝

open = (dsn str) (d db-sqlite) {
handle i64 = 0
rc i32 = _sqlite3-open(dsn, handle)
rc != SQLITE-OK -> {
return
}
d.handle = handle
}

規則:

  1. #{c} 獨立一行,標記下一行為 FFI 宣告(舊語法 #c 仍向後相容)
  2. FFI 僅為宣告,無函式主體
  3. 指針必須有具體型別(如 *byte),不允許裸 ptr
  4. **byte 用於輸出參數:呼叫後 C 函式寫入的指針值會自動轉為 i64 存回呼叫端變數
  5. 所有指針在 Nolang 端以 i64 儲存(ptrtoint)
  6. str 型別參數自動轉為 null-terminated i8*
  7. 名稱以 _ 開頭表示私有(不導出),C ABI 符號去除前綴 _
  8. FFI 宣告與普通代碼可寫在同一個 .no 檔案中

註解系統(#{...})​

#{...} 是通用註解系統,以逗號分隔的鍵值對列表。支援以下值類型:

語法類型範例
獨立鍵布爾#{debug}
布爾值布爾#{inline=true} / #{inline=false}
數值整數#{max=100}
文字字串#{name='hello'}
識別字識別字#{mode=fast}
陣列陣列#{derive=[Serialize, Deserialize]}
範圍範圍#{range=[0..256)}

多個鍵值對以逗號分隔:

#{derive=[Serialize, Deserialize], range=[0..256), max=100, debug}

註解位置​

#{...} 只允許兩種寫法:

  • 上方:獨立成行,置於目標(陳述、結構體欄位、枚舉成員、宣告、match 臂)上方;
  • 後方:寫在目標同一行的尾隨位置(res = arr[i] #{index-out=zero}、a pt #{inline}、 green #{deprecated})。

寫在目標前方同一行(前綴,如 #{index-out=zero} res = arr[i])是編譯錯誤,編譯器與 LSP 會回報同一條診斷。判定方式:註解群組的 } 之後、同一行上不得再有程式碼(換行、 ; 行註釋、或緊接的另一個註解群組不算程式碼)。

; ✓ 上方:獨立成行
#{overflow = wrap}
x i8 = a + 100

; ✓ 後方:同一行尾隨
x i8 = a + 100 #{overflow = wrap}

; ✗ 前方:同一行前綴 → 報錯
#{overflow = wrap} x i8 = a + 100

結構體欄位與枚舉成員(C 風格枚舉的值、標籤列舉的變體)的註解同樣只允許上方或後方 (a pt #{inline}、a pt #{inline=true}、green #{deprecated}、ok(v i64) #{inline})。 注意 #{inline} 的值同樣受支援:#{inline=false} 是合法的(見下文 〈結構體欄位上的 #{inline}〉)。

if <cond> { ... } 寫法中,註解夾在條件與 { 之間(if x > 0 #{overflow=wrap} {)也是 「前方」位置,同樣報錯。該處的掃描是寬鬆的,過去會把整組註解吞掉——既不生效也不報錯, 是最難察覺的一種失效。

範圍語法支援 16 種括號組合(規律:[ 包含左端點,( 不包含;] 包含右端點,) 不包含):

有界區間(4 種)— 兩端都有值:

  • [a..b] — 兩端閉區間(x >= a && x <= b)
  • [a..b) — 左閉右開(x >= a && x < b)
  • (a..b] — 左開右閉(x > a && x <= b)
  • (a..b) — 兩端開區間(x > a && x < b)

無上限區間(4 種)— 省略右端點:

  • [a..]、[a..) — 左包含,無上限(x >= a)
  • (a..]、(a..) — 左不包含,無上限(x > a)

無下限區間(4 種)— 省略左端點:

  • [..b]、(..b] — 無下限,右包含(x <= b)
  • [..b)、(..b) — 無下限,右不包含(x < b)

完全無界區間(4 種)— 兩端都省略,基於類型最小值/最大值:

  • [..] — 類型最小值 ≤ x ≤ 類型最大值(兩端都包含)
  • [..) — 類型最小值 ≤ x < 類型最大值
  • (..] — 類型最小值 < x ≤ 類型最大值
  • (..) — 類型最小值 < x < 類型最大值

FFI 註解 #{c} 是註解系統的特殊形式,當註解包含 FFI 語言鍵(c、cpp、rust 等)且後續為函式宣告時,編譯器將其識別為 FFI 綁定:

; #{c} 帶額外註解
#{c, debug}
_sqlite3-open = (filename str, db **byte) (rc i32)

註解附加到宣告​

非 FFI 註解會自動附加到緊隨其後的宣告(變數宣告、結構體定義),可用於為數值型別(如 num、i64 等)標記範圍限制等元數據:

; 變數宣告帶 range 註解
#{range=[0..256)}
x num = 42

; 結構體定義帶註解
#{derive=[Serialize, Deserialize]}
point {
x i64
y i64
}

; 結構體欄位帶 range 註解(可用於 num 等數值型別)
person {
#{range=[0..150]}
age num
#{range=[0..256)}
score i64
name str
}

range 註解特別適用於 num 型別(num = int | float),用於標記數值的有效範圍。範圍值可以是整數或識別字:

; 使用常量識別字作為範圍邊界
#{range=[i8.MIN..i8.MAX]}
val i8 = 100

文件嵌入(#{embed=...})​

#{embed=...} 註解在編譯期將外部文件內容嵌入為 []byte 只讀常量,類似 Go 的 //go:embed。嵌入的數據直接編譯進可執行文件,實現單二進制分發,無需運行時讀取文件。

語法:

#{embed='path/to/file'}
ICON []byte

; 或使用裸路徑(無引號)
#{embed=assets/icon.ico}
ICON []byte

embed 註解附加到緊隨其後的 []byte 變數宣告上。變數宣告不能帶有顯式初始值——值由嵌入的文件內容提供。

路徑解析:

  • 相對路徑相對於包根目錄(package.jsonc 所在目錄)解析
  • 支援絕對路徑
  • 路徑中可包含 /、.、- 字符

範例:

; 嵌入 Windows 圖標
#{embed='assets/win-icon.ico'}
WIN-ICON []byte

; 嵌入 SSL 證書
#{embed='certs/server.pem'}
SERVER-CERT []byte

; 嵌入配置文件
#{embed=config/default.json}
CONFIG []byte

; 使用嵌入數據
print('icon size: ', len(WIN-ICON))
print('first byte: ', WIN-ICON[0])

注意事項:

  1. 嵌入數據為只讀常量,指向只讀內存區域。寫入嵌入數據的行為是未定義的(與 Go //go:embed 語義一致)
  2. 嵌入變數不參與堆釋放——程序退出時不會 free 嵌入數據的指針
  3. 僅支援 []byte 類型宣告([N]byte 固定陣列亦可)
  4. 文件必須在編譯期存在,否則報錯
  5. 不能與顯式初始值共存(如 ICON []byte = [1, 2, 3] 會報錯)

文件夾嵌入(#{embed='dir'})​

#{embed=...} 也支援嵌入整個目錄。當路徑指向目錄時,編譯器遞迴讀取目錄下所有文件,嵌入為 fs.embed 類型的只讀文件系統。運行時通過 read()、exists() 方法按路徑訪問嵌入的文件,無需任何 C 函數——查找邏輯完全由 Nolang 實現。

語法:

; 嵌入整個前端目錄
#{embed='../frontend/dist'}
DIST fs.embed

; 從嵌入的文件系統讀取文件
data = DIST.read('index.html')
; 檢查文件是否存在
ok = DIST.exists('css/style.css')

fs.embed 類型:

fs.embed 是 Nolang 標準庫 fs 模組中定義的結構體,包含嵌入文件的元資料(路徑、偏移、長度)。編譯器在編譯期讀取目錄並將所有文件內容拼接為常量全局變數,運行時由純 Nolang 實現的 read 方法遍歷查找。

方法簽名說明
read(path str) (data []byte, ok bool)讀取嵌入的文件,返回內容和是否找到
exists(path str) (yes bool)檢查嵌入的文件是否存在

範例:

; HTTP 靜態文件伺服器:將前端文件嵌入單一二進制
#{embed='../frontend/dist'}
DIST fs.embed

serve = (conn server-conn) {
conn.parse()
path = conn.path
path == '/' -> path = '/index.html'

; 去掉前導 / 得到相對路徑
rel = path.slice(1, path.len-bytes())

; 從嵌入的文件系統讀取
data, ok = DIST.read(rel)
ok == false -> {
conn.write-html(404, 'Not Found')
conn.close()
return
}
conn.write-status(200)
conn.write-header('Content-Type', get-content-type(path))
conn.write-body(data)
conn.close()
}

注意事項:

  1. 目錄中所有文件(含子目錄)遞迴嵌入,路徑以相對於嵌入目錄的路徑存儲(如 css/style.css)
  2. 路徑分隔符統一為正斜槓 /(跨平台一致)
  3. fs.embed 類型變數為只讀,不參與堆釋放
  4. 適用於單二進制分發場景(如 HTTP 靜態文件伺服器、CLI 工具嵌入資源)

平台註解​

平台註解是編譯期過濾器,根據目標平台決定是否包含代碼。使用扁平化鍵(如 #{mac-arm64})同時指定 OS 與架構,無歧義,附加到緊隨其後的宣告上。不匹配的代碼完全不參與編譯——不生成 LLVM IR,不進行類型檢查。

支援的平台鍵(6 種扁平組合):

鍵匹配
#{linux-amd64}Linux x86_64
#{linux-arm64}Linux ARM64
#{win-amd64}Windows x86_64
#{win-arm64}Windows ARM64
#{mac-amd64}macOS x86_64(Intel)
#{mac-arm64}macOS ARM64(Apple Silicon)
#{mac-arm64}
print('running on macOS ARM64')

#{linux-amd64}
print('running on Linux x86_64')

#{win-amd64}
print('running on Windows x86_64')

; 平台特定的變數
#{mac-amd64}
#{mac-arm64}
sep = '/'

#{win-amd64}
#{win-arm64}
sep = '\\'

; 平台特定的函數
#{mac-arm64}
#{mac-amd64}
greet = () {
print('hello from mac')
}

#{linux-amd64}
#{linux-arm64}
greet = () {
print('hello from linux')
}

greet()

同一宣告上的多個鍵為 OR 關係——任一匹配即包含。因每個鍵已同時指定 OS 與架構,無需 AND 邏輯。

註解含義
#{mac-arm64}僅 macOS ARM64
#{mac-amd64, mac-arm64}macOS 任意架構
#{linux-amd64, win-amd64}Linux x86_64 或 Windows x86_64
#{mac-arm64, linux-arm64}macOS ARM64 或 Linux ARM64
; macOS 和 Linux(所有架構)都執行
#{mac-amd64, mac-arm64, linux-amd64, linux-arm64}
shared = () {
print('unix-like')
}

; 僅 Windows x86_64
#{win-amd64}
reg-key = () {
print('reading registry on win/x64')
}

; 僅 macOS ARM64(Apple Silicon)
#{mac-arm64}
neural = () {
print('Apple Neural Engine available')
}

使用 os.get-arch() 可在執行期取得當前架構,使用平台註解則在編譯期包含或排除代碼。

package.jsonc 編譯器配置​

package.jsonc 中的 compiler 區塊控制編譯器行為。version 值約定帶前導 v(如 "v0.1.0");編譯器內部解析時對前綴不敏感,"v0.1.0" 與 "0.1.0" 皆視為同一版本。

{
"compiler": {
"version": "v0.1.0",
"anonymous-fn-type": false,
"emit": "js"
}
}

emit​

控制輸出目標後端:

值說明
"" (空,預設)使用 LLVM 後端生成原生可執行檔
"js"使用 JS 後端生成 JavaScript(型別擦除),無需 LLVM 工具鏈

設定為 "js" 後,no build 自動走 JS 後端,等效於命令列 --js。命令列旗標優先級高於配置文件。

// 自動生成 JavaScript
"compiler": {
"emit": "js"
}

anonymous-fn-type​

控制是否允許匿名函式型別語法(如 cb ()())。預設 false(禁用),需使用具名函式型別別名。

指定連結的 C 函式庫列表:

"compiler": {
"link-libs": ["crypto", "ssl"]
}

編譯期變數注入(-ld)​

no build、no run、no test 支援通過 -ld-KEY=VALUE 旗標在編譯期注入全域常量。注入的變數等同於在源碼頂部聲明 KEY = VALUE,可在程式中直接使用。

語法​

no build -ld-KEY=VALUE [更多 -ld...] <file>
  • -ld 前綴後緊跟變數名和值,用 = 分隔
  • 多個 -ld 旗標可同時使用
  • 布林簡寫:-ld-DEBUG(不含 =VALUE)等同於 -ld-DEBUG=true

值的型別推斷​

值的形式推斷型別範例
整數i64-ld-COUNT=42
浮點數f64-ld-PI=3.14
true / falsebool-ld-DEBUG=true
其他str(單引號字串字面量)-ld-VERSION=0.1.2

範例​

; 假設使用 -ld-VERSION=0.1.2 -ld-COUNT=42 -ld-DEBUG=true 編譯
print('VERSION:', VERSION) ; 輸出: VERSION: 0.1.2
print('COUNT:', COUNT) ; 輸出: COUNT: 42
print('DEBUG:', DEBUG) ; 輸出: DEBUG: 1 (bool 在原生後端印為 1)
; 注入多個變數
no build -ld-VERSION=0.1.2 -ld-COUNT=42 -ld-DEBUG=true main.no

; 布林簡寫
no run -ld-RELEASE main.no

; JS 後端也支援
no build --js -ld-VERSION=0.1.2 main.no

與源碼聲明的關係​

-ld 注入的變數與源碼聲明的互動規則:

  • 源碼已有同名聲明:注入的值會就地替換源碼中該變數的值。常用模式是在源碼中聲明占位變數(如 VERSION = ''),構建時用 -ld-VERSION=0.1.2 賦值。
  • 源碼中不存在:注入的變數會被前置到 AST 最前面,作為新的全域常量宣告,參與所有後續編譯 pass(型別推斷、驗證、模組合併、codegen)。
  • 不注入時:源碼中的聲明保持原樣,不受影響。
; 源碼聲明占位變數
VERSION = ''
COUNT = 0

print('VERSION:', VERSION)
print('COUNT:', COUNT)

; no build -ld-VERSION=0.1.2 main.no
; 輸出: VERSION: 0.1.2 (注入值覆蓋了源碼的 '')
; COUNT: 0 (未注入,保持源碼值)