語法
註釋
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
`
強制格式約束
- 開頭
`後必須緊跟源碼換行; - 結尾
`必須單獨佔一行,且該行僅允許空白 + 結束反引號; - 首尾兩行的反引號所在行不計入字符串內容,只作為分隔標記;
- 轉義規則:內部所有
\、\n、\t、\'、\"全部原樣保留,不做任何轉義解析; - 源碼內的換行、縮進空格完整保留到字符串字節裡;
- 無法直接嵌入反引號字符,如需包含
`只能用普通單引號字符串拼接。
示例
; 內容為 "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 或異步場景下難以追蹤,容易引發競態條件。
- 可測試性:全局狀態使函式產生隱式依賴,難以隔離測試。
- 程式碼可讀性:全局變量使資料流隱晦,讀者需追蹤整個模組才能理解函式行為。
推薦做法:
- 優先使用局部變量:將狀態保留在函式內的局部變量中,透過參數和返回值傳遞。
- 使用結構體封裝狀態:將相關狀態組織到結構體中,透過方法操作(方法接收者為局部變量,位址一致)。
- 僅在必要時使用全局變量:如模組級常量(不可變)、單例資源(如全域日誌緩衝區)等場景。
- 全局變量必須大寫:這是強制規則(見上文),小寫開頭的頂層變量會被編譯器視為局部變量。
; ❌ 避免:在模組中使用全局可變變量
; 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-doctoml.parse(text)— 解析 TOML,回傳?toml-docdoc.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)— 取得已轉換的 scalartoml.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)— 解析,回傳?yamlyaml.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
邊界澄清:
->語義唯一,裸語句與賦值表達式共用同一套管道 lowering,不會因為外層有沒有賦值而切換模式。- 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 時請把臂分行寫,或用{}包裹的短路組。
- 管道可以產值:寫在賦值右側時,管道的結果就是末尾的值節點(僅在 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
重要規則:
- 短路組 — 多個
cond -> body用{}包裹時,形成 if-elif-else 鏈,只有第一個匹配的分支執行。 - 獨立 if — 在函數體/循環體中直接寫
cond -> body(不包裹{})是獨立 if,不會與相鄰的 if-then 短路。 - 禁止混雜 —
{}短路組內的直接子節點必須全部是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 handleawy— 等待異步線程完成並取得結果
; 異步函數定義(名稱以 -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) }等價於協程組展開——先後 spawnworker-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> | 同上 |
str | str<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) {
; . 是接收者
}
規則
- 方法名使用
type.method格式,type 必須是已定義的類型 - 接收者不需要顯式聲明參數,在方法體內用
.引用 - 調用時使用
receiver.method(args)語法 - 返回值放在第二組括號中,與普通函數一致
- 語法糖脫糖:
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)」)。
枚舉註解與內存佈局
枚舉支援 #{...} 註解,位置與其他目標相同(見〈註解位置〉):上方(獨立成行)或
後方(同一行尾隨);寫在目標前方同一行是編譯錯誤。
註解可放在兩處:
- 整個 enum 的上方 —— 作用於 enum 定義本身(如
#{buildin}、#{inline=true}); - 單個成員的上方或同行尾隨 —— 作用於該成員(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 } 這類純標籤) | 1 | 16 位元組 |
最寬變體為 i64 / f64 / 指標 | 1 | 16 位元組 |
最寬變體為 ?T | 2 | 24 位元組 |
最寬變體為 str / vec / []T | 3 | 32 位元組 |
最寬變體為結構體 T | T 各欄位槽數之和(遞迴展開) | 依 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} | ptr | ptr |
也就是說:註解永遠生效,不受該環境變數影響,旗標只改「沒寫註解時」的默認。因此在
默認配置下,#{inline=false} 是「主動要求指標」,而 #{inline=true} 只是把本來就如此
的佈局再寫明一次;一旦翻轉開啟,兩者的角色就反過來。
檢查器有兩條規則(TraceID fieldtag1,ValidateFieldTags):
-
只對結構體型別的欄位有意義。 標量 /
str/vec/ 陣列 / 切片 / map / option / 指標欄位本來就一律按值存放,寫#{inline}等於什麼都沒說,因此報錯;寫#{inline=false}同樣報錯,因為那等於宣稱了與編譯器行為相反的佈局。 -
按值內聯不得成環。 被內聯的結構體存放在宿主內部,所以
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} | 溢出(上溢或下溢)取 0 | int(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-defaultlint 跳過整個函式(清單層面看起來「乾淨」),但函式體內的未標註運算仍會觸發ovfhndld硬錯誤,實測與完全不寫註解無異。請逐陳述標註;no fmt --fix=overflow產生的正是這種逐陳述形式。
LSP 快速修復:編輯器(nolang-lsp)會對未標註的整數運算報出錯誤(
nolang-overflow,trace idovf-int-default),並提供五個 quickfix——Add#{overflow = wrap}/clamp0/min/max/saturate——自動在該運算所在語句上方、以與該行一致的縮排插入對應註解,把預設的option<int>結果改為 plainint。命令列等價物為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接收 plaini64)。
非 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 = expr | a 就地存 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 = 42h i32 = d & 4294967295 ; ✗ 仍報錯:有號目標不適用安全窄化
頂層必須是位元運算。 只有當表達式的頂層運算子是
&/|/^/<</>>時才放行。加法、減法、函式呼叫、直接變數引用等不在此列:d u64 = 42h 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
}
規則:
#{c}獨立一行,標記下一行為 FFI 宣告(舊語法#c仍向後相容)- FFI 僅為宣告,無函式主體
- 指針必須有具體型別(如
*byte),不允許裸ptr **byte用於輸出參數:呼叫後 C 函式寫入的指針值會自動轉為i64存回呼叫端變數- 所有指針在 Nolang 端以
i64儲存(ptrtoint) str型別參數自動轉為 null-terminatedi8*- 名稱以
_開頭表示私有(不導出),C ABI 符號去除前綴_ - 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])
注意事項:
- 嵌入數據為只讀常量,指向只讀內存區域。寫入嵌入數據的行為是未定義的(與 Go
//go:embed語義一致) - 嵌入變數不參與堆釋放——程序退出時不會 free 嵌入數據的指針
- 僅支援
[]byte類型宣告([N]byte固定陣列亦可) - 文件必須在編譯期存在,否則報錯
- 不能與顯式初始值共存(如
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()
}
注意事項:
- 目錄中所有文件(含子目錄)遞迴嵌入,路徑以相對於嵌入目錄的路徑存儲(如
css/style.css) - 路徑分隔符統一為正斜槓
/(跨平台一致) fs.embed類型變數為只讀,不參與堆釋放- 適用於單二進制分發場景(如 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(禁用),需使用具名函式型別別名。
link-libs
指定連結的 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 / false | bool | -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 (未注入,保持源碼值)