導(dǎo)言
提到Rebol語(yǔ)言的優(yōu)秀特性那就不得不說(shuō)它的解析引擎涂屁,簡(jiǎn)稱Parse。這項(xiàng)來(lái)自Carl Sassenrath的偉大設(shè)計(jì)灰伟,在過(guò)去的15年里拆又,使得Rebol用戶免受正則表達(dá)式(以不可維護(hù)著稱)的折磨。現(xiàn)如今栏账,Parse的增強(qiáng)版本在Red語(yǔ)言中重裝上陣帖族。
簡(jiǎn)而言之,Parse是一個(gè)使用語(yǔ)法規(guī)則來(lái)解析輸入序列的內(nèi)部DSL(在Rebol生態(tài)圈稱為“方言”)挡爵。Parse方言是TDPL家族的突出一員竖般。常用來(lái)校驗(yàn),驗(yàn)證茶鹃,分解涣雕,修改輸入的數(shù)據(jù),甚至是實(shí)現(xiàn)內(nèi)部或者外部DSL闭翩。
parse
函數(shù)的用法很簡(jiǎn)單:
parse <輸入序列> <規(guī)則>
<輸入序列>: 任意序列類型的值(字符串胞谭,文件,區(qū)塊男杈,路徑...)
<規(guī)則>: 一個(gè)區(qū)塊(包含有效的Parse方言)
下面的示例代碼可以直接復(fù)制到Red控制臺(tái)中運(yùn)行丈屹,即便你不懂Red和Parse方言,也能觀其大略,不像正則表達(dá)式那樣讓人不知所云旺垒。
使用語(yǔ)法規(guī)則驗(yàn)證一些字符串和區(qū)塊的輸入:
parse "a plane" [["a" | "the"] space "plane"] ;規(guī)則中可以包含子規(guī)則
parse "the car" [["a" | "the"] space ["plane" | "car"]]
parse "123" ["1" "2" ["4" | "3"]]
parse "abbccc" ["a" 2 "b" 3 "c"] ;指定數(shù)量
parse "aaabbb" [copy letters some "a" (n: length? letters) n "b"] ;將匹配綁定到變量彩库,使用小括號(hào)執(zhí)行Red表達(dá)式
parse [a] ['b | 'a | 'c]
parse [hello nice world] [3 word!] ;匹配區(qū)塊可以使用類型
parse [a a a b b b] [copy words some 'a (n: length? words) n 'b]
下面展示如何解析IPv4地址:
four: charset "01234" ;charset函數(shù)用于創(chuàng)建一個(gè)bitset!類型的值
half: charset "012345"
non-zero: charset "123456789"
digit: union non-zero charset "0" ;bitset!類型可以使用Red的集合運(yùn)算union函數(shù)進(jìn)行組合
byte: [
"25" half
| "2" four digit
| "1" digit digit
| non-zero digit
| digit
]
ipv4: [byte dot byte dot byte dot byte]
parse "192.168.10.1" ipv4
parse "127.0.0.1" ipv4
parse "99.1234" ipv4
parse "10.12.260.1" ipv4
data: {
ID: 121.34
Version: 1.2.3-5.6
Your IP address is: 85.94.114.88.
NOTE: Your IP Address could be different tomorrow.
}
parse data [some [copy value ipv4 | skip]]
probe value ; 輸出: "85.94.114.88"
一個(gè)粗糙然而實(shí)用的email地址驗(yàn)證器:
digit: charset "0123456789"
letters: charset [#"a" - #"z" #"A" - #"Z"]
special: charset "-"
chars: union union letters special digit
word: [some chars]
host: [word]
domain: [word some [dot word]]
email: [host "@" domain]
parse "john@doe.com" email
parse "n00b@lost.island.org" email
parse "h4x0r-l33t@domain.net" email
驗(yàn)證字符串形式的數(shù)學(xué)表達(dá)式(來(lái)自Rebol/Core手冊(cè))
expr: [term ["+" | "-"] expr | term] ;規(guī)則可以遞歸定義
term: [factor ["*" | "/"] term | factor]
factor: [primary "**" factor | primary]
primary: [some digit | "(" expr ")"]
digit: charset "0123456789"
parse "1+2*(3-2)/4" expr ; 返回 true
parse "1-(3/)+2" expr ; 返回 false
創(chuàng)建簡(jiǎn)單的解析器用于解析一個(gè)HTML子集:
html: {
<html>
<head><title>Test</title></head>
<body><div><u>Hello</u> <b>World</b></div></body>
</html>
}
ws: charset reduce [space tab cr lf]
parse html tags: [
collect [any [
ws
| "</" thru ">" break
| "<" copy name to ">" skip keep (load name) opt tags
| keep to "<"
]]
]
; parse函數(shù)將會(huì)返回如下區(qū)塊樹(shù)
[
html [
head [
title ["Test"]
]
body [
div [
u ["Hello"]
b ["World"]
]
]
]
]
Parse方言
Parse的核心原則如下:
- 輸入序列的位置由語(yǔ)法規(guī)則驅(qū)動(dòng)前進(jìn),直到頂層規(guī)則匹配失斚冉(parse函數(shù)返回false)或者遇到輸入序列的末尾(parse函數(shù)返回true)骇钦。(*)
- 有順序地匹配(例如,在["a" | "ab"]這個(gè)規(guī)則來(lái)說(shuō)竞漾,第二個(gè)選項(xiàng)不會(huì)被匹配)蜒简。
- 語(yǔ)法規(guī)則可無(wú)限組合。
- 有限的回溯:只有輸入和規(guī)則的位置可回溯坟瓢,其他改動(dòng)仍然保留君躺。
- 兩種模式:字符串解析(例如實(shí)現(xiàn)外部DSL)或者區(qū)塊解析(例如實(shí)現(xiàn)內(nèi)部DSL)。
(*)只要規(guī)則中使用了collect
關(guān)鍵字笔时,parse函數(shù)就返回一個(gè)區(qū)塊棍好,不論頂層規(guī)則是否匹配成功。
Parse的規(guī)則由如下元素構(gòu)成:
- 關(guān)鍵字:Parse方言預(yù)留的單詞(見(jiàn)下表)允耿。
- 單字(word):?jiǎn)巫炙壎ǖ闹当挥糜谝?guī)則借笙。
- 設(shè)字(word:):將單字綁定到當(dāng)前的輸入流位置。
- 取字(:word):恢復(fù)單字綁定的輸入流位置较锡。
- 整型數(shù)值:指定規(guī)則重復(fù)的數(shù)量或者范圍业稼。
- 字面值:匹配輸入流中對(duì)應(yīng)的字面值。
- | : 回溯并且嘗試匹配其他可選規(guī)則蚂蕴。
- [rules]:子規(guī)則區(qū)塊盼忌。
- (expression):脫離Parse方言轉(zhuǎn)而執(zhí)行Red表達(dá)式,執(zhí)行完畢后返回到Parse方言掂墓。
Red的Parse實(shí)現(xiàn)可以使用下面的關(guān)鍵字谦纱,這些關(guān)鍵字可以自由組合。(共分成了5類)
匹配
ahead rule :預(yù)匹配規(guī)則君编,但是輸入流位置不前進(jìn)
end :當(dāng)前輸入流位置處于末尾則返回成功
none :總是返回成功(通用規(guī)則)(譯者注:有誤跨嘉?實(shí)驗(yàn)結(jié)果更像是匹配空而不是通配)
not rule :反轉(zhuǎn)子規(guī)則的結(jié)果
opt rule :預(yù)匹配規(guī)則,可選性地匹配該規(guī)則吃嘿。
quote value :以字面值匹配(用于Parse方言的轉(zhuǎn)義)
skip :輸入流位置前進(jìn)一個(gè)元素(字符或者一個(gè)值)
thru rule :前進(jìn)輸入流位置直到成功匹配規(guī)則祠乃,并將輸入流位置放到匹配之后。
to rule :前進(jìn)輸入流位置直到成功匹配規(guī)則兑燥,并將輸入流位置放到匹配之前亮瓷。
控制流
break :中斷匹配循環(huán)并返回成功
if (expr) :對(duì)Red表達(dá)式求值,如果為false或者none降瞳,匹配失敗并回溯
into rule :轉(zhuǎn)換輸入流為已匹配的序列(字符串或者區(qū)塊)并且使用指定的規(guī)則近一步解析
fail :強(qiáng)制當(dāng)前規(guī)則匹配失敗并回溯
then :不論接下來(lái)的匹配是否成功嘱支,都跳過(guò)下一個(gè)替代規(guī)則
reject :中斷匹配循環(huán)并返回失敗
迭代
any rule :重復(fù)匹配零次或者多次rule直到匹配失敗或者輸入流不再前進(jìn)
some rule :重復(fù)匹配一次或者多次rule直到匹配失敗或者輸入流不再前進(jìn)
while rule :重復(fù)匹配一次或者多次rule直到匹配失敗并且忽略輸入流是否前進(jìn)
分解
collect [rule] :以區(qū)塊的形式返回在匹配規(guī)則中收集到的值
collect set word [rule] :將匹配規(guī)則中收集到的值放入?yún)^(qū)塊蚓胸,并將區(qū)塊綁定到word
collect into word [rule] :將匹配規(guī)則中收集到的值插入到word所引用的區(qū)塊
copy word rule :拷貝匹配的內(nèi)容并綁定到word
keep rule :拷貝匹配的內(nèi)容并添加到一個(gè)區(qū)塊
keep (expr) :將Red表達(dá)式中的最后一個(gè)值添加到一個(gè)區(qū)塊
set word rule :將匹配的第一個(gè)值綁定到word
修改
insert only value :在當(dāng)前輸入流的位置插入一個(gè)值并將位置放置到該值之后
remove rule :刪除匹配項(xiàng)
在Parse的核心原則中提到了兩種解析模式。這在Red中是很有必要的(Rebol亦然)除师,因?yàn)槲覀冇袃煞N基本的序列類型:string!和block!沛膳。當(dāng)前字符串類型是由Unicode代碼點(diǎn)的數(shù)組表示的(后來(lái)的Red版本中將會(huì)支持字符數(shù)組),區(qū)塊類型是任意Red值的數(shù)組(包括其他區(qū)塊)汛聚。
這在Parse方言的使用中引起了一些微小的差別锹安。例如,新增的bitset!
數(shù)據(jù)類型可以定義任意地字符集倚舀,用于在字符串中一次性匹配大量的字符叹哭。下面是使用bitset和迭代進(jìn)行匹配的示例:
letter: charset [#"a" - #"z"]
digit: charset "0123456789"
parse "hello 123 world" [5 letter space 3 digit space some letter]
Bitset! 數(shù)據(jù)類型
bitset值是一個(gè)存儲(chǔ)布爾值的bit數(shù)組。在Parse上下文中痕貌,bitset可以表達(dá)整個(gè)Unicode范圍內(nèi)的任意字符集风罩,并用來(lái)在單個(gè)操作內(nèi)匹配一個(gè)輸入字符,這相當(dāng)有用芯侥。bitset!類型是在這次的0.4.1版本引入的,因而對(duì)其支持的特性有必要作一番概述乳讥。它和Rebol3中的bitset!實(shí)現(xiàn)基本是同一個(gè)水平柱查。
要?jiǎng)?chuàng)建一個(gè)bitset,你需要提供一個(gè)或者多個(gè)字符作為基本規(guī)格云石“ぃ可以使用不同的形式提供:表示Unicode代碼點(diǎn)的整型數(shù)值,char!類型的值汹忠,string!類型的值淋硝,前面幾種元素的范圍或者組合。一個(gè)新的bitset使用如下語(yǔ)法創(chuàng)建:
make bitset! <基本規(guī)格>
<基本規(guī)格>:char!, integer!, string! or block!
例如:
; 創(chuàng)建一個(gè)空的bitset宽菜,至少可以容納50個(gè)bits
make bitset! 50
; 創(chuàng)建一個(gè)bitset谣膳,第65位bit被設(shè)置
make bitset! #"A"
; 創(chuàng)建一個(gè)bitset,第104铅乡,105位bit被設(shè)置
make bitset! "hi"
; 使用不同的值設(shè)置bit位
make bitset! [120 "hello" #"A"]
; 使用char!值的范圍來(lái)創(chuàng)建bitset
make bitset! [#"0" - #"9" #"a" - #"z"]
兩個(gè)值(只允許char!和integer!類型)加一個(gè)破折號(hào)定義了一個(gè)范圍继谚。
bitset的大小是根據(jù)提供的基本規(guī)格調(diào)整的。跟最高位的字節(jié)邊界對(duì)齊阵幸。
charset
函數(shù)用于方便的創(chuàng)建bitset花履,因此你可以這樣寫(xiě):
charset "ABCDEF"
charset [120 "hello" #"A"]
charset [#"0" - #"9" #"a" - #"z"]
要讀寫(xiě)bitset的某個(gè)bit位,路徑表示法再方便不過(guò)了:
bs: charset [#"a" - #"z"]
bs/97 ; will return true
bs/40 ; will return false
bs/97: false
bs/97 ; will return false
(注意:bitset的索引是從0開(kāi)始的)
除此之外挚赊,bitset類型還支持下面的操作:
pick, poke, find, insert, append, copy, remove, clear, length?, mold, form
關(guān)于這些操作的詳細(xì)用法請(qǐng)參考Rebol3的bitset文檔
為了處理Unicode字符這么大的范圍诡壁,bitset之外的bit位被當(dāng)作虛擬位來(lái)處理,因此訪問(wèn)或者設(shè)置這些位不會(huì)發(fā)生錯(cuò)誤荠割,bitset會(huì)根據(jù)需要自動(dòng)擴(kuò)展妹卿。然而這還是不足以應(yīng)付很大的范圍,比如除了數(shù)字以外的所有Unicode字符。這種情況下通過(guò)定義bitset的補(bǔ)集纽帖,既可以表達(dá)大范圍的字符又占用很少的空間宠漩。
bitset補(bǔ)集的創(chuàng)建方式和普通bitset一樣,只是需要使用not
開(kāi)頭而且一般使用區(qū)塊作為規(guī)格:
; 除數(shù)字以外的所有字符
charset [not "0123456789"]
; 除十六進(jìn)制符號(hào)外的所有字符
charset [not "ABCDEF" #"0" - #"9"]
; 除空白字符外的所有字符
charset reduce ['not space tab cr lf]
集合運(yùn)算對(duì)于bitset也是可用的懊直,然而當(dāng)前Red只支持union(畢竟這是bitset用得最多的函數(shù))扒吁。使用union,你可以將兩個(gè)bitset合并為一個(gè)新的bitset:
digit: charset "0123456789"
lower: charset [#"a" - #"z"]
upper: charset [#"A" - #"Z"]
letters: union lower upper
hexa: union upper digit
alphanum: union letters digit
Parse的實(shí)現(xiàn)
Red中的Parse方言是使用FSM實(shí)現(xiàn)的室囊,不同于Rebol3依賴于遞歸函數(shù)調(diào)用雕崩。使用FSM使得一些有趣的特性成為可能,例如暫停和恢復(fù)解析融撞,序列化解析狀態(tài)然后發(fā)送到遠(yuǎn)端盼铁,然后重新加載并繼續(xù)解析。現(xiàn)在這些都可以輕易辦到尝偎。
Red的Parse由接近1300行Red/System代碼實(shí)現(xiàn)饶火,其中相當(dāng)一部分用于優(yōu)化常見(jiàn)使用情況下的迭代循環(huán)。手寫(xiě)了大約770個(gè)單元測(cè)試用于覆蓋基本特性致扯。
當(dāng)前Parse是以解釋器模式運(yùn)行的肤寝,這對(duì)于大部分使用情況都足夠快了。為了滿足那些需要最大化性能的嚴(yán)重依賴解析的應(yīng)用抖僵,給parse添加靜態(tài)編譯器的相關(guān)工作也已經(jīng)開(kāi)展鲤看。到時(shí)生成的Red/System代碼應(yīng)該平均比解釋器版本快一個(gè)量級(jí)。當(dāng)Red實(shí)現(xiàn)自舉后耍群,還會(huì)實(shí)現(xiàn)一個(gè)Parse的JIT編譯器用于處理靜態(tài)編譯器不能處理的問(wèn)題义桂。
當(dāng)Red有了更多新特性后,Parse也會(huì)相應(yīng)的持續(xù)改進(jìn)以便利用這些新特性蹈垢。將來(lái)的多項(xiàng)改進(jìn)包括慷吊,當(dāng)binary!類型可用后,Parse也會(huì)添加對(duì)binary!類型的解析曹抬,并且當(dāng)port!類型可用時(shí)罢浇,流解析也將成為可能。
Red的Parse還支持一個(gè)公開(kāi)的面向事件API沐祷,你可以通過(guò)/trace refinement給parse函數(shù)傳遞一個(gè)回調(diào)函數(shù)嚷闭。
parse/trace <輸入> <規(guī)則> <回調(diào)函數(shù)>
<回調(diào)函數(shù)> 規(guī)格:
func [
event [word!] ; 跟蹤事件
match? [logic!] ; 上次匹配操作的結(jié)果
rule [block!] ; 當(dāng)前位置的對(duì)應(yīng)規(guī)則
input [series!] ; 將要匹配的下一個(gè)位置的輸入序列
stack [block!] ; 內(nèi)部的解析規(guī)則棧
return: [logic!] ; TRUE: 繼續(xù)匹配, FALSE: 退出
]
事件列表:
- push : 當(dāng)一個(gè)規(guī)則或者區(qū)塊入棧時(shí)
- pop : 當(dāng)一個(gè)規(guī)則或者區(qū)塊出棧之前
- fetch : 當(dāng)一個(gè)新的規(guī)則被采用之前
- match : 當(dāng)一個(gè)值匹配發(fā)生了
- iterate : 當(dāng)一個(gè)新的迭代開(kāi)始了 (ANY, SOME, ...)
- paren : 當(dāng)一個(gè)小括號(hào)中的表達(dá)式被求值后
- end : 當(dāng)?shù)竭_(dá)輸入末尾時(shí)
該API將來(lái)會(huì)擴(kuò)展出更多細(xì)粒度的事件。該API可以被用于對(duì)Parse進(jìn)行跟蹤赖临,獲取狀態(tài)胞锰,調(diào)試,... 咱們且拭目以待Red用戶們會(huì)玩出什么新的花樣兢榨!;-)
有一個(gè)默認(rèn)的回調(diào)函數(shù)被用于跟蹤嗅榕。你可以方便的通過(guò)使用parse-trace函數(shù)來(lái)使用它:
parse-trace <輸入> <規(guī)則>
你可以試著用一些簡(jiǎn)單的解析規(guī)則來(lái)查看輸出結(jié)果顺饮。
關(guān)于DSL支持
Parse是實(shí)現(xiàn)DSL(內(nèi)部或者外部)解析器的強(qiáng)大工具,它可以在規(guī)則中內(nèi)聯(lián)Red表達(dá)式凌那,從而能夠輕易的將DSL語(yǔ)法和其語(yǔ)義關(guān)聯(lián)起來(lái)兼雄。為了表明這點(diǎn),下面是一個(gè)Parse寫(xiě)的解釋器示例帽蝶,用于解釋著名的晦澀語(yǔ)言Brainfuck赦肋。
bf: function [prog [string!]][
size: 30000
cells: make string! size
append/dup cells null size
parse prog [
some [
">" (cells: next cells)
| "<" (cells: back cells)
| "+" (cells/1: cells/1 + 1)
| "-" (cells/1: cells/1 - 1)
| "." (prin cells/1)
| "," (cells/1: first input "")
| "[" [if (cells/1 = null) thru "]" | none]
| "]" [
pos: if (cells/1 <> null)
(pos: find/reverse pos #"[") :pos
| none
]
| skip
]
]
]
; This code will print a Hello World! message
bf {
++++++++++[>+++++++>++++++++++>+++>+<<<<-]>++.>+.+++++++..+++.
>++.<<+++++++++++++++.>.+++.------.--------.>+.>.
}
; This one will print a famous quote
bf {
++++++++[>+>++>+++>++++>+++++>++++++>+++++++>++++++++>
+++++++++>++++++++++>+++++++++++>++++++++++++>++++++++
+++++>++++++++++++++>+++++++++++++++>++++++++++++++++<
<<<<<<<<<<<<<<<-]>>>>>>>>>>>----.++++<<<<<<<<<<<>>>>>>>
>>>>>>.<<<<<<<<<<<<<>>>>>>>>>>>>>---.+++<<<<<<<<<<<<<>>
>>>>>>>>>>>>++.--<<<<<<<<<<<<<<>>>>>>>>>>>>>---.+++<<<<
<<<<<<<<<>>>>.<<<<>>>>>>>>>>>>>+.-<<<<<<<<<<<<<>>>>>>>>
>>>>>>+++.---<<<<<<<<<<<<<<>>>>.<<<<>>>>>>>>>>>>>>--.++
<<<<<<<<<<<<<<>>>>>>>>>>>>>>-.+<<<<<<<<<<<<<<>>>>.<<<<>
>>>>>>>>>>>>>+++.---<<<<<<<<<<<<<<>>>>>>>>>>>>>>.<<<<<<
<<<<<<<<>>>>>>>>>>>>>>-.+<<<<<<<<<<<<<<>>>>>>>>>>>>>>-.
+<<<<<<<<<<<<<<>>>>>>>>>>>>>>--.++<<<<<<<<<<<<<<>>>>+.-
<<<<.
}
注意:為了使得實(shí)現(xiàn)更加簡(jiǎn)短,該實(shí)現(xiàn)僅限于處理一層的"[...]"結(jié)構(gòu)励稳。使用Parse寫(xiě)的稍長(zhǎng)和稍復(fù)雜的完整實(shí)現(xiàn)點(diǎn)這里佃乘。
所以當(dāng)前應(yīng)付較小的DSL應(yīng)該不在話下。對(duì)于較復(fù)雜的DSL理論上也沒(méi)問(wèn)題驹尼,然而當(dāng)DSL的語(yǔ)義變得更復(fù)雜時(shí)趣避,Parse能起的作用就沒(méi)這么大了。對(duì)于大部分的用戶來(lái)說(shuō)新翎,實(shí)現(xiàn)一個(gè)較高級(jí)DSL的解釋器或者編譯器都絕非易事程帕。針對(duì)這個(gè)問(wèn)題,Red將來(lái)要著手在Parse之上提供一套元DSL(meta-DSL)包裝地啰,暴露一組更高級(jí)的API用于抽象解釋器或者編譯器的核心組件愁拭,使得編寫(xiě)復(fù)雜DSL變得更容易。要實(shí)現(xiàn)一個(gè)DSL用來(lái)創(chuàng)建其他DSL的想法絕不是癡人說(shuō)夢(mèng)髓绽,Rascal語(yǔ)言中已經(jīng)存在這種強(qiáng)大的形式了敛苇。Red也會(huì)朝著這個(gè)方向邁進(jìn)妆绞,然而不會(huì)有Rascal這么復(fù)雜顺呕。