那么要实现这么一个了不起的语法(迫真,不过其实你实现了这个之后,类似 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);
首先咱得做好理论准备,是吧? 所以感兴趣的朋友请耐心地接着看,你马上就要自创一门配置文件语法了呢~ 而且,这个语言的代码还可以在 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);
/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…
最终的解决方案呢?
不过得同步我最新的修复版本 FP.js...
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 里面, 我...
为什么 seq 要多管闲事,不消耗字符还得手动弄个 keepLast,还能够多次 keepLast... 只是因为支持内嵌在 seq 里面, 我...
从此 FP.js 的解析组合子无需再考虑『哥哥』是一个有强迫症的哥哥还是一个正常的哥哥了。
尤其是在组合的时候,绝对不需要了、不再需要了
不会再有莫名其妙多跳一两个字符的问题了,子解析器解析的了就跳、解析不了就不跳。解析不了 seq 直接失败、possible 尝试下一个解析器。
再也不会 seq 里嵌套 seq/many,还需要 duplast N (视嵌套情况而定,呵呵)次呦....
尤其是在组合的时候,绝对不需要了、不再需要了
不会再有莫名其妙多跳一两个字符的问题了,子解析器解析的了就跳、解析不了就不跳。解析不了 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)
/tmp/duangsuse.sock
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}…
🤔 JavaScript 不能 copy... 好麻烦哦,我只好弄个函数 来帮忙造新 Array... 毕竟不能一直用一个实例去折叠...