From: Tan Kian-ting Date: Fri, 1 Dec 2023 19:46:41 +0000 (+0800) Subject: parser X-Git-Url: https://git.kianting.info/?a=commitdiff_plain;h=b7905be5187dbaab7a5af2e2c889c525d55534e3;p=anotherTypesetter parser --- diff --git a/docs/#defineASTandGrammar.md# b/docs/#defineASTandGrammar.md# deleted file mode 100644 index cdb8bfa..0000000 --- a/docs/#defineASTandGrammar.md# +++ /dev/null @@ -1,2 +0,0 @@ -# 定義抽象語法樹和語法 - diff --git a/docs/.#defineASTandGrammar.md b/docs/.#defineASTandGrammar.md deleted file mode 120000 index 0c2d549..0000000 --- a/docs/.#defineASTandGrammar.md +++ /dev/null @@ -1 +0,0 @@ -yoxem@yoxem-HP-Laptop-15-fd0072.6714:1700948777 \ No newline at end of file diff --git a/docs/defineASTandGrammar.md b/docs/defineASTandGrammar.md index e69de29..6d3c8c4 100644 --- a/docs/defineASTandGrammar.md +++ b/docs/defineASTandGrammar.md @@ -0,0 +1,301 @@ +# Ch1 定義抽象語法樹和語法 + +## 抽象語法樹 + +C語言、Python語言就算有許多的關鍵字、操作符、符號或是常數變數,在編譯器分析語法以後,最後會轉成編譯器可以操作的樹結構,然後再轉成我們想要的另一個語言的樹,最後輸出另一個語言的程式碼。 + +但是什麼叫做抽象語法樹呢?我們先從一點句法知識來談。 + +學過中學國文文法的課程,會背一堆類似「主詞+動詞+受詞」、「主詞+(有/無)+受詞」的結構。可以換個說法,是句子=「主詞+動詞+受詞」或是「主詞+(有/無)+賓詞」的形式。我們將「=」寫成「::=」,「/」(或是)寫成「|」,「動詞」擴充變成「動詞片語」,就變成: + +``` + 句子 ::= (主詞 動詞片語 受詞) | (主詞 (有 | 無) 受詞)... + +``` + +為了易讀所以寫成: + +``` +句子 ::= 主詞 動詞片語 受詞 + | 主詞 (有 | 無) 受詞 + | ... + +``` + +用這種形式表示的語言句法,叫做「BNF文法」。這種句法看起來很語言學,但是我們想:受詞和主詞可以為名詞、專有名詞或是「形容詞+名詞」;動詞片語可以為動詞或是「副詞+動詞」。因此這樣之規則,就可以生成許多句子,比如「我有筆」、「張三養貓」、「小芳慢慢移動檯燈」等等的句子。然後句子可以用上述規則,分析成語法的樹狀結構,如下圖把「我曾旅居新竹」寫成語法樹。 + +
+ ![「我曾旅居新竹」的語法樹](syntaxtree.svg "") +
「我曾旅居新竹」的語法樹
+
+ + + + +同理,程式語言通常也有更嚴謹的這樣生成文法,可以用幾個簡單規則生出繁多的程式碼,而且合乎語法規定。這種生成文法也可檢查輸入的程式碼有沒有符合句法的規定。而這種語法生成的程式碼,去掉不需要的逗號等等符號,當然也可以做成語法樹,就是抽象語法樹 (abstract syntax tree, AST),如下圖所示。 +
+ ![「(2+2) == 4」的語法樹。注意括號已經刪除。](syntaxtree2.svg "") +
「(2+2) == 4」的語法樹。注意括號已經刪除。
+
+ + +而上文的抽象語法樹,可以是我們把程式經過編譯器分析之後,用「樹」儲存的資料結構。而樹形結構我們可以使用Lisp語言的S表達式(S-expressiom; S-exp)來表示,本文採用這樣的表示方法。所以上文的`(2+2)==4`即`(== (+ 2 2) 4)`;`let baz = foo("bar")`,若是把foo("bar")這種函數套用(apply)寫成`(APPLY foo "bar")`,則其S-exp語法樹可寫為`(let baz(APPLY foo "bar"))`。 + +## 決定語法 +那我們要如何制定這個語言的語法,這樣我們才能夠寫出符合這個語法的函數,然後再用tokenizer和parser轉成AST樹。 + +不考慮` + - * /`這種運算子,以及向量的表示子,函數可以用`ID(arg1, arg2, ...)`這種方式來表示,其中`arg_x`是引數,`ID`是識別子(identifier,可以把它想成變函數的名字)。 + +變數可以是`ID`,`arg_n`可以是`ID`或常數(量)。 + +常數(量)的表示法可以是下列任一: + + - 浮點數如0.0, 36.8,BNF風格的表達法為:`[0-9]+ '.' [0-9]+`。`'c'`指c這個文字,`+`表示前面的重複1次以上;`[0-9]`表示數字0到9。 + + - 整數如22、0:`[0-9]+` + + - 字串:`'"' (不是「"」的任一字元|('\' '"')) '"'`(`.`表示任何一個字元) + +然而我們還是需要綁定變數`let x = var in boby`(在`body`裡面,`x`指代`var`)、`set x = var`(改變變數值)、lambda`lambda (x)=>{body}`。另外為了要區別要在PDF印上去的一般字元,在這個檔案的常數、變數、函數、關鍵字等前後需要加@表示(但是函數、lambda裡面的變數不用)。比如`@foo(a, b)@`、`@lambda(x)@`、`@"IAmAString"@`、`@2.2@`、`@3@`(後三者應該很少用到)可是若需在PDF印`@`時怎辦?那就用`\@`。比如`foo\@example.com`。 + +所以我們可以定義以下的BNF風文法: + +``` +Language ::= PrintTxt | Exprs + +PrintTxt ::= (('\' '@')| 非@字元)+ //「我是一隻貓」或是「www\@example.com」 + +Exprs ::= @ Expr* @ // *表示前面的重複0次以上(包含不出現) + +Expr ::= (Letting | Setting | Lambda | Apply | Var| Const) | "(" Expr ")" + +Letting ::= "let" Var "=" Expr "in" Expr // let foo = 12 in ... + +Setting ::= Var ":=" Expr "in" Expr // foo := a in ... + +Lambda ::= "fn" Var "->" Expr // fn x -> 12 + +Apply ::= Expr Expr // foo 3 即foo(3) + +Var ::= ID + +Const ::= String | Float | Int + +ID ::= ("_" | [a-z] | [A-Z]) ("_" | [0-9] | [a-z] | [A-Z])+ + +Integer ::= [0-9]+ + +Float ::= [0-9]+ "." [0-9]+ + +String ::= '"' (不是「"」的任一字元|('\' '"')) '"' +``` + +## 用ParserCombinator進行tokenize +Parser combinator(分析器組合子)是一種利用高階函數來簡化分析器撰寫的辦法。這講到頭來會涉及「遞歸下降分析」以及其他編譯理論的東西,但太難了(聽說可以讀編譯理論的「龍書我們可以製作一個小的tokenizer。但是因為自己寫parser combinator太累了,所以我們就用nom來幫我們代勞。 +」)。講一個簡單的案例吧: + +假設我們想要將字串的開頭match 0~9 之中的其中一個,我們可以寫一個函數match0to9如下: + +``` +function match0to9(string){ +if (string[0] in 0,1,..,9){ + let rest = string[1:]; + let matched = string[0]; + return {type: "OK", rest : rest, matched : matched}; +} +else{ + return {type : "Nothing"}; + } +} +``` + +假設我們要將字串`s`的前3個字的match 0~9呢?如果會高階函數的話,引入一個`then`函數,然後把`match0to9`傳進去,這樣寫起來比較簡潔,行數可以比較少: + +``` +function thenDo(input, fun){ + if (input.type != "Nothing"{ + + middle = fun(input.rest); + if (middle.type != "Nothing"){ + // add the matched character of input to the head of the result + middle.matched = input.matched + middle.matched + return middle; + }else{ + return middle; // return nothing + } + }else{ + input; // return nothing + } + +} + +// "s" should be wrapped in a object +let sWrapped = {type : "OK", rest : s, matched : ""}; + +// match0~9 3 times +thenDo(thenDo(thenDo(sWrapped, match0to9), match0to9), match0to9) +``` +我們可以製作一個小的tokenizer。但是因為自己寫parser combinator太累了,所以我們就用nom來幫我們代勞。 + + +安裝nom可以用:`cargo run nom`。 + +假設我們要match 0-9任意次以上(就是integer),我們可以這樣寫: + +``` + +// import all the parser unit for string +use nom::character::complete::*; +// for the return type +use nom::IResult; + +// integer ::= [0-9]+ +pub fn integer(input: &str) -> IResult<&str, &str> { + return digit1(input) ; // [0-9]+ +} + +// test parser +#[cfg(test)] +mod tests { + // import the functions ouside mod tests + use super::*; + + // test integer + #[test] + fn test_integer() { + //if no error is shown, the function passes the test + assert_eq!(integer("12345"), Ok(("", "12345"))); + assert_eq!(integer("0"), Ok(("", "0"))); + } +} + + +``` + +用`cargo run`可以順利通過: + +``` +running 1 test +test tests::test_integer ... ok + +test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s +``` + +我們做第二個tokenizer,`float`: + +其中的`recognize`蒐集所有包在裡面的`parsers`的string。 +``` +// collect matched strings of all the parsers, +use nom::combinator::recognize; +// given 2 parser and gets the result as (1st_matched, 2nd_matched), +use nom::sequence::pair; +// exact matching characters +use nom::bytes::complete::tag; + +// float ::= [0-9]+ "." [0-9]+ +pub fn float(input: &str) -> IResult<&str, &str>{ + // [0-9]+ "." [0-9]+ + // "12.345" returns Ok((else, (("12", '.'), "345"))), then recgonize them as + // Ok("12.345") + let a = + recognize(pair(pair(digit1, tag(".")), digit1))(input); + return a; + +} + +``` + +parser `identifier`(引用的函數的名稱空間略)。使用`fold_may0`和新的空vector來儲存match多次的parser的符合結果: +``` +pub fn identifier(input : &str) -> IResult<&str, &str>{ + return recognize(pair( + // 1st character is a-z, A-Z or _ + satisfy(|c| (is_alphabetic(c as u8) || c == '_')), + // the tail characters (0+ times matched) storing in a vector + fold_many0( + // a-z, A-Z, 0-9, _ + satisfy(|c| (is_alphanumeric(c as u8) || c == '_')), + // initial vector + Vec::new, + // once it matches, append the matched item to the vector. + |mut acc: Vec<_>, item| { + acc.push(item); + acc + } + )))(input); + +} + + +``` + + +## 平面操作 + +### 基本函數與直譯器 +我們藉由以上的概念,可以定義一個將文字、線條等形狀排列到2D平面的語法,畢竟不論輸出PDF、SVG等等,粗略而言,就是一種2D平面安放文字的語言。另外PDF的格式相當晦澀,就算_PDF Explained_的PDF教學,也還是要輔助使用其他的工具,沒辦法看了就自己手刻PDF,所以還是用`printpdf`來教學吧。 + +現在我們初始化一個專案目錄,然後將需要的S-exp函式庫和pdf函數庫指定為相依函式庫: + +``` +cargo init; + +cargo add rsexp printpdf; +``` + +我們可以定義一些表達式(包含函數、資料結構,S-exp形式)的說明如下。`'()`表示空列表(empty list),因為都要表達是函數的引用,所有的函數寫成形式`(Func "函數名稱" (引數1 引數2 ....))`。Float指64位元浮點數: + +``` +(px Float) ; px表達pixel單位,儲存浮點數 +(pt Float) ; pt表達point單位,儲存浮點數 +(style (str pt)) ; 文字樣式。String表示字型的路徑[fontPath],Float表示字型大小(in Pt) (fontSize) +(str String) ; 儲存字串 +(func "createPDF" '()) ;新增PDF +(func "createPage" '()) ;新增頁面 +(func "writePdf" '(str)) ;寫入PDF頁面,String是PATH + +(func "putchar" '(str style x y)) ; x 軸向右,y 軸向下,str 表示字元(char),style 表示文字樣式 +``` + +`main.rs`先引用函式庫: +`use printpdf::*;` + + +其中 `px`、`pt`是單位,所以可以在`main.rs`這樣定義: + +``` +enum Measure{ + Pt(f64), + Px(f64) +} +``` + +最後一次定義expression: +``` +enum Expr{ + Mea(Measure), // wrapper for measure + Str(&str), + Style{font_path : Measure, size : Measure}, + Func(&str, Vec), + Void // return nothing +} +``` + +然後我們可以這樣定義一個處理輸入輸出的interpreter於`interp`,並修改`main.rs`如下,縱使我們準時: +``` +fn interp(exp : Expr)->(){ + // the function will be extended. + match exp { + Expr::Mea(Measure::Pt(x)) => println!("{:?} pt", x), + Expr::Mea(Measure::Px(x)) => println!("{:?} px", x), + + _ => println!("not found expression"), + }; +} + +// exexute interpreter +fn main() { + interp(Expr::Mea(Measure::Pt(2.2))); + interp(Expr::Flo(2.2)); +} +``` diff --git a/docs/defineASTandGrammar.md~ b/docs/defineASTandGrammar.md~ new file mode 100644 index 0000000..e69de29 diff --git a/docs/index.md b/docs/index.md index e0cdf3a..71b582c 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,8 +1,26 @@ # Another Typesetter - 另一個排版器 -## 序言 -本文是講一個排版器的雛形如何製作的考察,若有任何建議,請聯絡作者 [yoxem@kianting.info](mailto:yoxem@kianting.info)。 +## 摘要 +本文是講一個排版器的雛形如何製作的考察,使用Rust語言。 -內容部分參考[Essentials of Compilation An Incremental Approach in Racket](https://mitpress.mit.edu/9780262047760/essentials-of-compilation/)(EoC)的思路,但因為目的不一樣,所以裡面的內容不太相同。另外EoC主要是講 +###序言 + +以前從國中時候試用Linux以及架站以後,就開始想用LaTeX排版些自己所寫的東西,其中包含覺得LaTeX的語法不好想要重造輪子。就算後來大學沒有走上資訊工程這條路,還是希望有天至少能夠完成個能用的雛形。 + +但是這是涉及字體檔案的處理、PDF的處理、語法分析,後來自己因為不知道如何開發,所以一直停擺。不是遇到很多蟲,就是效能問題有缺失。因為時間繁忙很少更不消說了。甚至買了Knuth教授的 _Digital Typography_,想要瞭解斷行演算法,結果粗估五、六十頁,所以幾乎沒有讀。 + +另外筆者一個分支興趣是編譯器的相關知識,所以開始讀和王垠的編譯器思想系出同門的Jeremy G. Siek所著作之 *Essential of Complication: An Incremental Approach in Racket*(編譯之要素:Racket語言的遞增的方法)。我想到:既然編譯器這種複雜的軟體,可以一層一層的用pass來遞增功能,就像水彩從背景、大物體一直由少漸多的完成。而排版軟體也是把使用者輸入的排版之領域特定語言(DSL)轉換成文字、圖形和二維座標對應關係(最後匯出成PDF或SVG等等)的編譯器,若是能夠用層層遞增的方法來完成,相信也能夠避免結構的複雜化導致錯誤容易發生的挫折。 + +然而排版語言不只是輸入文字轉圖形而已,更重要的是還要有因應美觀的自動斷行(justification)和斷字(hyphenation)等等的演算法、還有PDF的基本知識、字型函式庫的取用、排版要求(多欄)、甚至還牽涉到語言特有的特性:比如東亞全形文字(漢字、諺文、日文假名、注音符號)和非全形文字中間要加空白,以及從左寫到右的文字(希伯來字母和阿拉伯字母等)的排版方法,不一而足。 + +為了簡化起見,且目標讀者是臺灣的受眾,本書僅涉及到ASCII英文字母——頂多加些一些附加符號(diacritics)和漢字的排版。其他的功能希望讀者可以漸次由少漸多的附加。另外這邊會使用到一些LISP的表達式來表達抽象語法樹,若是不懂的話,可以看一點教 Lisp或是Scheme的書,如SICP。另外這本書不是編譯原理和描述PDF規格的書,不涉獵底層的知識,有需要的可以參考相關領域的書。 + +### 先備知識 + +這不是教一位入門使用者如從零知識撰寫排版軟體的書,讀者應該有知道如何使用靜態型別語言的經驗,比如一點C、或是Rust等等。另外抽象語法樹為求方便,使用LISP撰寫,所以需要會LISP和Scheme的知識(知名教科書SICP的開頭可以讀一讀)。 + +這本書也不教編譯理論和tokenizing、parsing、狀態機等等的,頂多只會帶到一些很基礎的知識,有需要的請另外再讀。所以使用者需要會有使用正規表達式(regex)的能力。 + +操作環境使用Linux。需要安裝fontconfig等套件。 - [定義抽象語法樹和語法](./defineASTandGrammar) diff --git a/docs/index.pdf b/docs/index.pdf new file mode 100644 index 0000000..561e90f Binary files /dev/null and b/docs/index.pdf differ diff --git a/docs/index.typ b/docs/index.typ new file mode 100644 index 0000000..14553d4 --- /dev/null +++ b/docs/index.typ @@ -0,0 +1,193 @@ +#set heading(numbering: "1.1.1.1.") + + #show raw: set text(font: "Noto Sans Mono CJK TC") + + +#set page("a5") + +#set text( + font: ("New Computer Modern", "AR PL UMing TW"), + size: 11pt +) + +#show heading: it => [ + #set text(font: "Noto Serif CJK TC", + weight: "black") + #it.body +] + +#set par( justify: true,leading: 1em, +) + +#align(center)[#set text( + font: ("EB Garamond 08"), + weight:"medium", + size: 20pt, +) + Clo: another typesetter] +#align(center)[#box([#set text(size: 15pt, + font: "Noto Serif CJK TC", + weight:"medium") +一個排版器的實作心得])] +#box( + height:0.5em +) +#align(center)[#box([#set text(size: 11pt, + font: "AR PL UMing TW", + weight:"light") +陳建町])] + +#pagebreak() + + + + +#set page( + margin: (top: 60pt, bottom: 20pt), + header: locate( + loc => if (calc.odd(loc.page()) == true){ + [#set align(right) + #numbering("i", loc.page()) + ] + } else { + [#set align(left) + #numbering("i", loc.page())] + } + )); + + + +#heading(level:2, "版權聲明",outlined:false) + +(c) 2023 陳建町 (Tan, Kian-ting) + +本書內容非經許可,禁止複製、分發、商業使用等違反著作權法之行為。 + +然書中之程式碼,採用 #link("https://opensource.org/license/mit/")[MIT許可證]授權。 + +#pagebreak() + +#outline( + title: align(left, [目#box(width:1em)錄 #box( + height:1.5em)]), + target: heading.where(outlined: true), +) + +#pagebreak() + +#set page( + numbering: "i", + number-align: top+right) + +#heading(numbering: none, "序言") + +以前從國中時候試用Linux以及架站以後,就開始想用LaTeX排版些自己所寫的東西,其中包含覺得LaTeX的語法不好想要重造輪子。就算後來大學沒有走上資訊工程這條路,還是希望有天至少能夠完成個能用的雛形。 + +但是這是涉及字體檔案的處理、PDF的處理、語法分析,後來自己因為不知道如何開發,所以一直停擺。不是遇到很多蟲,就是效能問題有缺失。因為時間繁忙很少更不消說了。甚至買了Knuth教授的 _Digital Typography_,想要瞭解斷行演算法,結果粗估五、六十頁,所以幾乎沒有讀。 + +另外筆者一個分支興趣是編譯器的相關知識,所以開始讀和王垠的編譯器思想系出同門的Jeremy G. Siek所著作之_Essential of Complication: An Incremental Approach in Racket_(編譯之要素:Racket語言的遞增的方法)。我想到:既然編譯器這種複雜的軟體,可以一層一層的用pass來遞增功能,就像水彩從背景、大物體一直由少漸多的完成。而排版軟體也是把使用者輸入的排版之領域特定語言(DSL)轉換成文字、圖形和二維座標對應關係(最後匯出成PDF或SVG等等)的編譯器,若是能夠用層層遞增的方法來完成,相信也能夠避免結構的複雜化導致錯誤容易發生的挫折。 + +然而排版語言不只是輸入文字轉圖形而已,更重要的是還要有因應美觀的自動斷行(justification)和斷字(hyphenation)等等的演算法、還有PDF的基本知識、字型函式庫的取用、排版要求(多欄)、甚至還牽涉到語言特有的特性:比如東亞全形文字(漢字、諺文、日文假名、注音符號)和非全形文字中間要加空白,以及從左寫到右的文字(希伯來字母和阿拉伯字母等)的排版方法,不一而足。 + +為了簡化起見,且目標讀者是臺灣的受眾,本書僅涉及到ASCII英文字母——頂多加些一些附加符號(diacritics)和漢字的排版。其他的功能希望讀者可以漸次由少漸多的附加。另外這邊會使用到一些LISP的表達式來表達抽象語法樹,若是不懂的話,可以看一點教 Lisp或是Scheme的書,如SICP。另外這本書不是編譯原理和描述PDF規格的書,不涉獵底層的知識,有需要的可以參考相關領域的書。 + +#heading(numbering: none, "致謝") + +感謝Donald Knuth教授開發出這麼一套排版系統以及排版的演算法,除了造福科學排版的諸多用戶外,也間接鼓舞我想要研究排版軟體如何實作;感謝Jeremy G.Siek老師的_Essential of Complication: An Incremental Approach in Racket_,讓我獲得排版語言編譯器設計的啟發。感謝王垠讓我對編譯器相關的技術有興趣,從而斷斷續續學習相關的資訊。 + +感謝愛爾蘭語,除了讓我對語言和語言復興的知識打開新的世界以外,這個軟體的名字Clo也是從這裡來的(cló有「活字」的意思,因為技術限制抱歉沒辦法輸入長音符號)。 + +感謝我的父母,雖然專長不是電腦資訊科技,但是要感謝他們讓我讓我有餘力能夠在中學的時候研究這種興趣,這條路才能走下去。 + +感謝這本書閱讀的人們,讓我知道筆者不是孤單的。 + +Siōng-āu Kám-siā góa ê Siōng Chú, nā-bô i ê hû-chhî kap pó-siú, chit-pún chheh iā bô-hó oân-sêng.(最後感謝上主,若無扶持保守,這本書也很難完成) + +#pagebreak() + + +#set page( + margin: (top: 60pt, bottom: 20pt), + header: locate( + loc =>{ + let chapter_query = query(selector(heading.where(level: 1)).after(loc),loc) + let section_query = query(selector(heading.where(level: 2)).after(loc),loc) + let chapter = ""; + let section = ""; + if chapter_query == (){chapter = ""} + else{chapter = chapter_query.at(0).body}; + if section_query == (){section = ""} + else{section = section_query.at(0).body} + + + + + if (calc.odd(loc.page()) == true){ + grid( + columns: (0.333333fr, 0.333333fr, 0.333333fr), + +text(style: "italic")[ ], +[#set align(center) +#chapter], +[ #h(1fr) #loc.page-numbering()]) + } else { + grid( + columns: (0.333333fr, 0.333333fr, 0.333333fr), + +text(style: "italic")[#loc.page-numbering()], +[#set align(center) +#section], +[ ]) + }} + )); + +#show heading: it => [ + #set text(font: ("New Computer Modern", "Noto Serif CJK TC"), + weight: "black") + #counter(heading).display() #it +] + + +#set page(numbering: "1") +#counter(page).update(1) +#set heading(numbering: "1.1.1.1.1") += 先備知識 + +這不是教一位入門使用者如從零知識撰寫排版軟體的書,讀者應該有知道如何使用靜態型別語言的經驗,比如一點C、或是Rust等等。另外抽象語法樹為求方便,使用LISP撰寫,所以需要會LISP和Scheme的知識(知名教科書SICP的開頭可以讀一讀)。 + +這本書也不教編譯理論和tokenizing、parsing、狀態機等等的,頂多只會帶到一些很基礎的知識,有需要的請另外再讀。所以使用者需要會有使用正規表達式(regex)的能力。 + +== 抽象語法樹 + +C語言、Python語言就算有許多的關鍵字、操作符、符號或是常數變數,在編譯器分析語法以後,最後會轉成編譯器可以操作的樹結構,然後再轉成我們想要的另一個語言的樹,最後輸出另一個語言的程式碼。 + +但是什麼叫做抽象語法樹呢?我們先從一點句法知識來談。 + +學過中學國文文法的課程,會背一堆類似「主詞+動詞+受詞」、「主詞+(有/無)+受詞」的結構。可以換個說法,是句子=「主詞+動詞+受詞」或是「主詞+(有/無)+賓詞」的形式。我們將「=」寫成「::=」,「/」(或是)寫成「|」,動詞擴充變成「動詞片語」,就變成: + +``` + 句子 ::= (主詞 動詞片語 受詞) | (主詞 (有 | 無) 受詞)... + +``` + +用這種形式表示的語言句法,叫做「BNF文法」。這種句法看起來很語言學,但是我們想:受詞和主詞可以為名詞、專有名詞或是「形容詞+名詞」;動詞片語可以為動詞或是「副詞+動詞」。因此這樣之規則,就可以生成許多句子,比如「我有筆」、「張三養貓」、「小芳慢慢移動檯燈」等等的句子。然後句子可以用上述規則,分析成語法的樹狀結構,如圖1把「我曾旅居新竹」寫成語法樹。 + +#figure( + image("syntaxtree.svg", width: 40%), + caption: [ + 「我曾旅居新竹」的語法樹 + ], + supplement: [圖], +) + +同理,程式語言通常也有更嚴謹的這樣生成文法,可以用幾個簡單規則生出繁多的程式碼,而且合乎語法規定。這種生成文法也可檢查輸入的程式碼有沒有符合句法的規定。而這種語法生成的程式碼,去掉不需要的逗號等等符號,當然也可以做成語法樹,就是抽象語法樹 (abstract syntax tree, AST),如圖2所示。 + +#figure( + image("syntaxtree2.svg", width: 30%), + caption: [ + `(2+2) == 4`的語法樹。注意括號已經刪除。 + ], + supplement: [圖], +) + +而上文的抽象語法樹,可以是我們把程式經過編譯器分析之後,用「樹」儲存的資料結構。而樹形結構我們可以使用Lisp語言的S表達式(S-expressiom; S-exp)來表示,本文採用這樣的表示方法。所以上文的`(2+2)==4`即`(== (+ 2 2) 4)`;`let baz = foo("bar")`,若是把foo("bar")這種函數套用(apply)寫成`(APPLY foo "bar")`,則其S-exp語法樹可寫為`(let baz(APPLY foo "bar"))`。 diff --git a/docs/index2.md b/docs/index2.md new file mode 100644 index 0000000..624f631 --- /dev/null +++ b/docs/index2.md @@ -0,0 +1,24 @@ +[ Clo: another typesetter]{align="center"} [[ +一個排版器的實作心得]{.box}]{align="center"} []{.box} [[ +陳建町]{.box}]{align="center"} + +版權聲明 + +\(c\) 2023 陳建町 (Tan, Kian-ting) + +本書內容非經許可,禁止複製、分發、商業使用等違反著作權法之行為。 + +然書中之程式碼,採用 +[MIT許可證](https://opensource.org/license/mit/)授權。 + +序言 + +以前從國中時候試用Linux以及架站以後,就開始想用LaTeX排版些自己所寫的東西,其中包含覺得LaTeX的語法不好想要重造輪子。就算後來大學沒有走上資訊工程這條路,還是希望有天至少能夠完成個能用的雛形。 + +但是這是涉及字體檔案的處理、PDF的處理、語法分析,後來自己因為不知道如何開發,所以一直停擺。不是遇到很多蟲,就是效能問題有缺失。因為時間繁忙很少更不消說了。甚至買了Knuth教授的 +*Digital +Typography*,想要瞭解斷行演算法,結果粗估五、六十頁,所以幾乎沒有讀。 + +另外筆者一個分支興趣是編譯器的相關知識,所以開始讀和王垠的編譯器思想系出同門的Jeremy +G. Siek所著作之_Essential of Complication: An Incremental Approach in +Racket diff --git a/docs/syntaxtree.svg b/docs/syntaxtree.svg new file mode 100644 index 0000000..de300ba --- /dev/null +++ b/docs/syntaxtree.svg @@ -0,0 +1 @@ +句子主詞代詞 我謂語動詞片語 副詞曾動詞 旅居受詞專有名詞新竹 diff --git a/docs/syntaxtree2.svg b/docs/syntaxtree2.svg new file mode 100644 index 0000000..de5fd08 --- /dev/null +++ b/docs/syntaxtree2.svg @@ -0,0 +1 @@ +==+112 diff --git a/mkdocs.yml b/mkdocs.yml index ca96d3e..3be3945 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,5 +1,11 @@ site_name: Another Typesetter 另一個排版器 -site_url: https://blog.kianting.info/pages/typesetter +site_url: https://blog.kianting.info/pages/docs/anotherTypeSetter/ nav: - Home: index.md -theme: readthedocs \ No newline at end of file + - Ch 1: defineASTandGrammar.md + - Ch 2: 2DManipulating.md +theme: readthedocs + +markdown_extensions: + - attr_list + - md_in_html diff --git a/mkdocs.yml~ b/mkdocs.yml~ new file mode 100644 index 0000000..c97182f --- /dev/null +++ b/mkdocs.yml~ @@ -0,0 +1 @@ +site_name: My Docs diff --git a/site/#defineASTandGrammar.md# b/site/#defineASTandGrammar.md# deleted file mode 100644 index cdb8bfa..0000000 --- a/site/#defineASTandGrammar.md# +++ /dev/null @@ -1,2 +0,0 @@ -# 定義抽象語法樹和語法 - diff --git a/site/404.html b/site/404.html index 643d594..78c492b 100644 --- a/site/404.html +++ b/site/404.html @@ -8,14 +8,14 @@ - + Another Typesetter 另一個排版器 - - + + - - + + @@ -29,9 +29,9 @@ @@ -70,7 +92,7 @@ -
  • defineASTandGrammar
  • +
  • Ch 1
  • @@ -81,12 +103,175 @@
    - +

    Ch1 定義抽象語法樹和語法

    +

    抽象語法樹

    +

    C語言、Python語言就算有許多的關鍵字、操作符、符號或是常數變數,在編譯器分析語法以後,最後會轉成編譯器可以操作的樹結構,然後再轉成我們想要的另一個語言的樹,最後輸出另一個語言的程式碼。

    +

    但是什麼叫做抽象語法樹呢?我們先從一點句法知識來談。

    +

    學過中學國文文法的課程,會背一堆類似「主詞+動詞+受詞」、「主詞+(有/無)+受詞」的結構。可以換個說法,是句子=「主詞+動詞+受詞」或是「主詞+(有/無)+賓詞」的形式。我們將「=」寫成「::=」,「/」(或是)寫成「|」,「動詞」擴充變成「動詞片語」,就變成:

    +
      句子 ::= (主詞 動詞片語 受詞) | (主詞 (有 | 無) 受詞)...
    +
    +
    +

    為了易讀所以寫成:

    +
    句子 ::= 主詞 動詞片語 受詞
    +         | 主詞 (有 | 無) 受詞
    +         | ...
    +
    +
    +

    用這種形式表示的語言句法,叫做「BNF文法」。這種句法看起來很語言學,但是我們想:受詞和主詞可以為名詞、專有名詞或是「形容詞+名詞」;動詞片語可以為動詞或是「副詞+動詞」。因此這樣之規則,就可以生成許多句子,比如「我有筆」、「張三養貓」、「小芳慢慢移動檯燈」等等的句子。然後句子可以用上述規則,分析成語法的樹狀結構,如下圖把「我曾旅居新竹」寫成語法樹。

    +
    +

    「我曾旅居新竹」的語法樹 +

    +
    「我曾旅居新竹」的語法樹
    +
    +

    同理,程式語言通常也有更嚴謹的這樣生成文法,可以用幾個簡單規則生出繁多的程式碼,而且合乎語法規定。這種生成文法也可檢查輸入的程式碼有沒有符合句法的規定。而這種語法生成的程式碼,去掉不需要的逗號等等符號,當然也可以做成語法樹,就是抽象語法樹 (abstract syntax tree, AST),如下圖所示。

    +
    +

    「(2+2) == 4」的語法樹。注意括號已經刪除。 +

    +
    「(2+2) == 4」的語法樹。注意括號已經刪除。
    +
    +

    而上文的抽象語法樹,可以是我們把程式經過編譯器分析之後,用「樹」儲存的資料結構。而樹形結構我們可以使用Lisp語言的S表達式(S-expressiom; S-exp)來表示,本文採用這樣的表示方法。所以上文的(2+2)==4即(== (+ 2 2) 4);let baz = foo("bar"),若是把foo("bar")這種函數套用(apply)寫成(APPLY foo "bar"),則其S-exp語法樹可寫為(let baz(APPLY foo "bar"))。

    +

    決定語法

    +

    那我們要如何制定這個語言的語法,這樣我們才能夠寫出符合這個語法的函數,然後再用tokenizer和parser轉成AST樹。

    +

    不考慮+ - * /這種運算子,以及向量的表示子,函數可以用ID(arg1, arg2, ...)這種方式來表示,其中arg_x是引數,ID是識別子(identifier,可以把它想成變函數的名字)。

    +

    變數可以是ID,arg_n可以是ID或常數(量)。

    +

    常數(量)的表示法可以是下列任一:

    +
      +
    • +

      浮點數如0.0, 36.8,BNF風格的表達法為:[0-9]+ '.' [0-9]+。'c'指c這個文字,+表示前面的重複1次以上;[0-9]表示數字0到9。

      +
    • +
    • +

      整數如22、0:[0-9]+

      +
    • +
    • +

      字串:'"' (不是「"」的任一字元|('\' '"')) '"'(.表示任何一個字元)

      +
    • +
    +

    然而我們還是需要綁定變數let x = var in boby(在body裡面,x指代var)、set x = var(改變變數值)、lambdalambda (x)=>{body}。另外為了要區別要在PDF印上去的一般字元,在這個檔案的常數、變數、函數、關鍵字等前後需要加@表示(但是函數、lambda裡面的變數不用)。比如@foo(a, b)@、@lambda(x)@、@"IAmAString"@、@2.2@、@3@(後三者應該很少用到)可是若需在PDF印@時怎辦?那就用\@。比如foo\@example.com。

    +

    所以我們可以定義以下的BNF風文法:

    +
    Language ::= PrintTxt | Exprs
    +
    +PrintTxt ::= (('\' '@')| 非@字元)+ //「我是一隻貓」或是「www\@example.com」
    +
    +Exprs ::= @ Expr* @ // *表示前面的重複0次以上(包含不出現)
    +
    +Expr ::= (Letting | Setting | Lambda | Apply | Var| Const) | "(" Expr ")"
    +
    +Letting ::= "let" Var "=" Expr "in" Expr // let foo = 12 in ...
    +
    +Setting ::= Var ":=" Expr "in"  Expr // foo := a in ...
    +
    +Lambda ::= "fn" Var "->" Expr // fn x -> 12
    +
    +Apply ::= Expr Expr // foo 3 即foo(3)
    +
    +Var ::= ID
    +
    +Const ::= String | Float | Int
    +
    +Int ::= [0-9]+
    +
    +Float ::= [0-9]+ "." [0-9]+
    +
    +String ::= '"' (不是「"」的任一字元|('\' '"'))   '"'
    +
    +

    用ParserCombinator進行tokenize

    +

    Parser combinator(分析器組合子)是一種利用高階函數來簡化分析器撰寫的辦法。講一個簡單的案例吧:

    +

    假設我們想要將字串的開頭match 0~9 之中的其中一個,我們可以寫一個函數match0to9如下:

    +
    function match0to9(string){
    +if (string[0] in 0,1,..,9){
    +    let rest = string[1:];
    +    let matched = string[0];
    +    return {type: "OK", rest : rest, matched : matched};
    +}
    +else{
    +    return {type : "Nothing"};
    +    }
    +}
    +
    +

    假設我們要將字串的前兩個字的match 0~9呢?如果會高階函數的話,引入一個then函數,然後把match0to9傳進去,這樣寫起來比較簡潔:

    +
    function thenDo(input, fun){
    +    if (input.type != "Nothing"{
    +
    +        middle =  fun(input.rest);
    +        if (middle.type != "Nothing"){
    +            middle.matched = input.matched + middle.matched
    +            return middle;
    +         }else{
    +             return middle; // return nothing
    +         }
    +    }else{
    +        input; // return nothing
    +    }
    +
    +}
    +
    +
    +
    +

    平面操作

    +

    基本函數與直譯器

    +

    我們藉由以上的概念,可以定義一個將文字、線條等形狀排列到2D平面的語法,畢竟不論輸出PDF、SVG等等,粗略而言,就是一種2D平面安放文字的語言。另外PDF的格式相當晦澀,就算_PDF Explained_的PDF教學,也還是要輔助使用其他的工具,沒辦法看了就自己手刻PDF,所以還是用printpdf來教學吧。

    +

    現在我們初始化一個專案目錄,然後將需要的S-exp函式庫和pdf函數庫指定為相依函式庫:

    +
    cargo init; 
    +
    +cargo add rsexp printpdf;
    +
    +

    我們可以定義一些表達式(包含函數、資料結構,S-exp形式)的說明如下。'()表示空列表(empty list),因為都要表達是函數的引用,所有的函數寫成形式(Func "函數名稱" (引數1 引數2 ....))。Float指64位元浮點數:

    +
    (px Float) ; px表達pixel單位,儲存浮點數
    +(pt Float) ; pt表達point單位,儲存浮點數
    +(style (str pt)) ; 文字樣式。String表示字型的路徑[fontPath],Float表示字型大小(in Pt) (fontSize)
    +(str String) ; 儲存字串
    +(func "createPDF" '()) ;新增PDF
    +(func "createPage" '()) ;新增頁面
    +(func "writePdf" '(str)) ;寫入PDF頁面,String是PATH
    +
    +(func "putchar" '(str style x y)) ; x 軸向右,y 軸向下,str 表示字元(char),style 表示文字樣式
    +
    +

    main.rs先引用函式庫: +use printpdf::*;

    +

    其中 px、pt是單位,所以可以在main.rs這樣定義:

    +
    enum Measure{
    +    Pt(f64),
    +    Px(f64)
    +}
    +
    +

    最後一次定義expression:

    +
    enum Expr{
    +    Mea(Measure), // wrapper for measure
    +    Str(&str),
    +    Style{font_path : Measure, size : Measure},
    +    Func(&str, Vec<Expr>),
    +    Void // return nothing
    +}
    +
    +

    然後我們可以這樣定義一個處理輸入輸出的interpreter於interp,並修改main.rs如下,縱使我們準時:

    +
    fn interp(exp : Expr)->(){
    +    // the function will be extended.
    +      match exp {
    +      Expr::Mea(Measure::Pt(x)) => println!("{:?} pt", x),
    +      Expr::Mea(Measure::Px(x)) => println!("{:?} px", x),
    +
    +        _ => println!("not found expression"),
    +    };
    +}
    +
    +// exexute interpreter
    +fn main() {
    +    interp(Expr::Mea(Measure::Pt(2.2)));
    +    interp(Expr::Flo(2.2));
    +}
    +