/tmp/duangsuse.sock
23 subscribers
303 photos
3 videos
92 files
337 links
从 duangsuse::Echo (@dsuse) 跟进出来的分支,将在作者恢复原帐号访问的时候合并删除。
Download Telegram
那么要实现这么一个了不起的语法(迫真,不过其实你实现了这个之后,类似 JSON、TOML 这样的语法的解析器也不难做了,YAML 这种你需要加一个手写子解析器)

首先咱得做好理论准备,是吧? 所以感兴趣的朋友请耐心地接着看,你马上就要自创一门配置文件语法了呢~ 而且,这个语言的代码还可以在 JavaScript 里被求值为标准的 object,是不是很 Happy 呢~

要解析 [1, 2, 3] 这种列表,我们就得先重视 FP.js parserc 的一个缺陷 -- seq/manyFold/someFold 里不能内嵌 seq
这是为什么呢?因为你输入的字符串不是魔法的就被一大堆松耦合的解析器抽提了句子结构,而是通过一个 Lookahead1 『流』(stream) 输入的:比如你有两个 charP

pp = p.run( p.seq([p.charP('H'), p.charP('i')]) );

这样我们就得到了一个解析 'Hi' 的解析器,不过这是个简单的例子,如果我们从输入数据(就是字符)流的角度看:

输入流是 tryLookAhead1Iter`Hi`:
如果 'H' 解析失败,则 seq 也会 pfail(parser fail),并且下一个字符是 'H'
如果这个 seq 上面是还有一个 seq/some/many 呢,则上面的 seq 也会解析失败
如果这个 seq 上面是有一个 possible/lookahead1,则会就这这个 'H' 进行下一次解析(工作符合预期)

这是因为 seq 成功在『pmatch』解析成(所有子解析器都匹配)功和『pfail』解析失败,所有解析器都回滚的情况下保证了输入流符合预期(成功则矣、不成回滚输入流到进入时的状态,这是因为第一个字符 pfail 的时候保留了当前字符 'H',而 seq 本身的 next 不会执行)

瞎猫撞着死耗子 -- 碰巧成功符合预期。

那咱知道了这一点 -- 就是 seq 里可以随便嵌套 seq、需要的时候(保证只有 match 的可能,最多只能有一个 seq 会被安排解析输入流,或者 seq 失败的的一定只可能是第一个解析器)可以在 possible/lookahead1 里嵌套 --- 不过有个问题,如果第一个解析器 charP`H` 成功并且第二个失败会怎么样? 这就爆炸了,因为 seq 失败的时候,不能把流回滚...

如果父层是 possible,则下一个尝试就是在流的不同位置 'i' 进行了,你们这个样子不行的!
所以就不能用来解析 Value (',' ws0P ValueP ws0P | ']'); 可能,我们需要类似这种解析器啊!

不要忘了类 LL(1) 的限制 — 每个子解析器,最多只向前看一个字符就决定是 match 还是 unmatch,所以利用 Feeder 架构 charP, elemP, notElemP, kwP 什么的都可以正常工作,可是 seq([charP, charP]) 就不可以

所以我们来看看到底,这个列表的语法是什么:

List = '[' ws0P ']' | '[' ws0P ValueP { ws0P Value COMMA ws0P} ']'
COMMA = ','

我们这么说:

match("[ ]")
match("[ 1, 2 , 3 ]")
unmatch("[,]")
unmatch("[1,]")

有时候你会发现循环的东西还是比较有意思的,好像总是要弄个特例 -- 1/0 这两个特例
它等价:

List = '[' (ws0 Value ws0 { Value ws0 ',' ws0 })? ']'

那就有一个问题 -- 废 的 FP.js 解析组合子,无法弄那么多分支(不能在 possible 里使用 sequence 解析器),如何去解析这种语法?
答案已经给出来了 -- 等价代换!

-- 奇妙的等价代换

List = '[' ws0 ']' | '[' (ws0 Value ws0 { Num ws0 ',' ws0 }) ']'

符合 LL(1) 的预期吗?
我们先展开 ? (optional) 解析器,把 (|) possible 解析器左边两边都有的操作忽略掉进行合并。
为了等价,右边也一样

List
= '[' ws0 ']' | '[' (ws0 Num ws0 { Num ws0 ',' ws0 }) ']'
= '[' ws0 (1 vs. 2) Num ws0 { Num ws0 ',' ws0 } ']'
= '[' ws0 (Num ws0 { Num ws0 ',' ws0 } | ']'
{- Reorder branch priority -}
= '[' ws0 (']' | (Num ws0 { Num ws0 ',' ws0 } ']') (1)

我们看文法 (1),它服不符合 LL(1) 的文法呢?符合!
因为解析完 ws0 后,我们 possible([charP, seq]) 就可以了,如果 charP 成功,则列表已经结束、如果失败,则解析 (Value ws0 {Num ws0 ',' ws0} {- 直到 -} ']')

这个语法也是符合 LL(1) 的 -- 不过还需转化一下
因为 seq/fold {Num ws0 ',' ws0} 在失败时是无法完全重置的,虽然或许能(在 Value 不能识别 ']' 时)瞎猫再次撞上死耗子,但是我们不能指望次次走运。
(Num ws0 {Num ws0 ',' ws0} ']')
= Num ws0 {']' | Num ws0 ',' ws0}
只需把 ']' 放在前面就可以正确处理这种情况(很难看,而且不能区分错误的方法... 迫真 所以还是用上面的手段吧)。
因为后面的 Num ws0 ',' ws0 的第一项的确可以区分列表是否已经结束,而如果第一项可以解析后面却解析不了,说明用户输入是错的。

-- 编程实现

既然咱理论做足了,为什么不写码实际去尝试一下呢?
说的天花乱坠,不如实际下地干活。不能饿肚子。

> const p = require('./fp').parserc;
先引入我们优秀的 FP.js 先生(迫真)

> let numP = p.someFold(p.elemP('0123456789'.split(''), 'digit'), [0, (a,x) => a*10+(x-'0')], f => 'number value expected');
定义一个 number parser
> let __ = p.wsP(), _ = p.ws0P();
方便空格。

//'[' ws0 (']' | (Num ws0 { ',' ws0 Num ws0 } ']')
> let List_ItemsP = p.manyFold(p.seq([p.charP(','), _, numP, _], xs => xs[0]));
> let ListP = p.seq([ p.charP('['), _, p.possible([ p.charP(']'), p.skip(']'), p.seq([numP, _, List_ItemsP, p.charP(']')]) ]) ]);

possible 里引入的 charP(']') 不得不加上 p.skip,因为如果没有 match,则 charP 会自动 dup 一次 LastItem 使得后面的流上下文不正确(因为这个功能本来是为了自动 revert 父 seq 解析器的 next 操作的... 可是实际上,自己失败了 seq 不会 next... 全怪 some/many 实现不对,没有帮弟弟解决这个问题 设计有问题)

好了,测试一下

let pp = p.run(ListP);
我发现这个 ws0P 实现的果然还是不仅仅不对,而且还不能忽视它的不对...
/tmp/duangsuse.sock
我发现这个 ws0P 实现的果然还是不仅仅不对,而且还不能忽视它的不对...
它在有且只有一个空格的时候,直接把下一项目跳过了 然后继续扫描空格...
/tmp/duangsuse.sock
我发现这个 ws0P 实现的果然还是不仅仅不对,而且还不能忽视它的不对...
你说我这是设计个啥劲.... 放着好的 next 不用,反而舍近求远去用 duplast.... 根本没有组合的概念
真的是被自己蠢到了,为什么不懂得去复制自己的代码.... 非得再写第二遍
世界上不存在莫名其妙的问题,只存在会把简单问题弄复杂的傻人。(注意空格解析的结果是正确的)
又抓出一条虫子。刚才是不懂得『简单的』抄代码导致白白浪费时间,现在这个又是反面 — 过分依赖抄代码,改都不改想都不想导致的逻辑缺失
/tmp/duangsuse.sock
那么要实现这么一个了不起的语法(迫真,不过其实你实现了这个之后,类似 JSON、TOML 这样的语法的解析器也不难做了,YAML 这种你需要加一个手写子解析器) 首先咱得做好理论准备,是吧? 所以感兴趣的朋友请耐心地接着看,你马上就要自创一门配置文件语法了呢~ 而且,这个语言的代码还可以在 JavaScript 里被求值为标准的 object,是不是很 Happy 呢~ 要解析 [1, 2, 3] 这种列表,我们就得先重视 FP.js parserc 的一个缺陷 -- seq/manyFold/someFold…
最终的解决方案呢?

const p = require('./fp').parserc;
let __ = p.wsP(), _ = p.ws0P();

let numP = p.someFold(p.elemP('0123456789'.split(''), 'digit'), [0, (a,x) => a*10+(x-'0')], f => 'number value expected');
let pa = p.seq([p.charP(','), _, numP, _], xs => xs[2]);
let List_ItemsP = p.manyFold(pa, [[], (v,x)=>v.concat(x)]);
let ListP = p.seq([ p.charP('['), _, p.possible([ p.charP(']'), p.skip(']'), p.seq([numP, _, List_ItemsP, p.charP(']')]) ]) ]);
pp = p.run(ListP);

不过得同步我最新的修复版本 FP.js...
/tmp/duangsuse.sock
最终的解决方案呢? const p = require('./fp').parserc; let __ = p.wsP(), _ = p.ws0P(); let numP = p.someFold(p.elemP('0123456789'.split(''), 'digit'), [0, (a,x) => a*10+(x-'0')], f => 'number value expected'); let pa = p.seq([p.charP(','), _, numP, _], xs => xs[2]);…
... 我受不了目前这个辣鸡反向思维架构了,重构算了
为什么 seq 要多管闲事,不消耗字符还得手动弄个 keepLast,还能够多次 keepLast... 只是因为支持内嵌在 seq 里面, 我...
现在所有哥哥就是哥哥、弟弟就是弟弟,哥哥不再强迫弟弟 next 了
『当一个称职的哥哥、不再区分 possible 和 seq/some/many』
组合 不分家
从此 FP.js 的解析组合子无需再考虑『哥哥』是一个有强迫症的哥哥还是一个正常的哥哥了。
尤其是在组合的时候,绝对不需要了、不再需要了
不会再有莫名其妙多跳一两个字符的问题了,子解析器解析的了就跳、解析不了就不跳。解析不了 seq 直接失败、possible 尝试下一个解析器。
再也不会 seq 里嵌套 seq/many,还需要 duplast N (视嵌套情况而定,呵呵)次呦....
『以为是各种问题很奇怪,其实是因为思路不清晰,把简单的问题弄复杂了』
辣鸡架构,拜拜喽!
const p = require('./fp').parserc;
let __ = p.wsP(), _ = p.ws0P();

function NumList(l, ws0, rxs) {
this.wss = []; this.ary = [];
this.wss.push(ws0); // {ws0} x...
if (rxs === ']') { return; }
this.ary.push(rxs[0]); this.wss.push(rxs[1]); // x {a}, y, ..
for (let [wl, x, wr] of rxs[2]) { this.ary.push(x); this.wss.push(wl, wr); }
}
let numP = p.someFold(p.elemP('0123456789'.split(''), 'digit'), [0, (a,x) => a*10+(x-'0')], f => 'number value expected');
let pa = p.seq([p.charP(','), _, numP, _], xs => [xs[1], xs[2], xs[3]]);
let List_ItemsP = p.manyFold(pa, [p.makeNew(Array), (v,xs)=>{v.push(xs); return v;}]);
let ListP = p.seq([ p.charP('['), _, p.possible([ p.charP(']'), p.seq([numP, _, List_ItemsP, p.charP(']')]) ]) ], p.makeNew(NumList));
pp = p.run(ListP);


尾逗号的问题是正常情况,因为这个毕竟是 manyFold, 解析到不能匹配了就 fail... 第一个 charP 还是可以解析那个尾逗号的
如果需要不支持尾逗号的话,还需语法变形才行...(当然你也可以设置 pa 的 msgr 为直接 throw error 的那种,所有组合基本都只会 handle pfail,不会 handle msgr 抛出的 exception)
『就说呢,一定是可以做到的!』 这样就有了第一门自己的 markup language!
怎么样~ 成功了吗?
This media is not supported in your browser
VIEW IN TELEGRAM
接下来,一起实现这门新的 『DEF』 DSL (领域专属语言)语言吧! #Parser #PLT

使用 FP.js 的解析组合子功能,就可以快速制造各种拥有良好用户界面和错误处理的的 DSL!

就像之前写 GeekSpec 的时候说的:

面向语言编程,是个不错的逻辑。如果你觉得要处理的问题使用某门语言的文法十分鸡肋冗长繁复难看,为什么不创建一门专门的语言来清晰地描述它呢?
在完成这门 『DEF』 DSL 只后,你还可以写更多更好、甚至写的更像一种通用编程语言的 DSL

并且,以后的编程路上,拥有这类 DSL 的能力会让你写出更优质的程序、提升你编程的速度,避免模板代码的编写、完成更有特色的程序...