开发工具编译器【免费下载链接】ohmA library and language for building parsers, interpreters, compilers, etc.项目地址https://gitcode.com/gh_mirrors/oh/ohm点击查看免费下载本文基于 Ohm 项目官方文档入口 doc/README.md 编写。Ohm 是一套由解析表达式文法PEG语言与 JavaScript 解析库共同构成的解析工具包可用于解析自定义文件格式以及快速构建编程语言的解析器parser、解释器interpreter与编译器compiler。读完本文你将掌握三条核心实战路径用ohm.grammar()实例化文法并用match()识别输入、用createSemantics()与addOperation()编写语义动作完成求值与翻译以及用trace()和可视化工具调试文法同时理解语法规则syntactic/lexical、左递归、行内规则声明等关键概念为深入阅读 API 参考 与 语法参考 打好基础。一、Ohm 是什么文法与语义的彻底分离Ohm 由两部分组成Ohm 语言一种基于 PEG 的领域特定语言用于描述语法与Ohm 库提供 JavaScript 接口把文法变成可用的解析器。它与常规 PEG 框架最大的不同在于彻底分离文法与语义动作文法只负责识别recognition确定哪些输入是合法的不包含任何求值、绑定或捕获逻辑语义动作semantic actions单独定义解析出合法输入后由语义决定拿它做什么如求值、翻译、语法高亮、编译。这种设计带来的好处在 doc/philosophy.md 中有系统论述文法保持纯净、可读性好文法与语义可以各自独立扩展同一份文法可以搭配多套不同的语义比如同一输入同时做求值和语法高亮且不需要对输入做多趟扫描。Ohm 还以**惰性lazily**方式应用语义动作——只有当某个结果真正被需要时才求值因此回溯失败的分支、未被子表达式引用的部分都不会触发无谓的计算。在仓库中Ohm 库的实现位于 packages/ohm-js/src/main.js其入口ohm.grammar()与ohm.grammars()正是所有示例的第一步。二、安装与引入方式Ohm 已发布为 npm 包ohm-js仓库当前版本为 17.1.0见 packages/ohm-js/package.json支持浏览器、Node.js 与 Deno 三种环境浏览器引入单个script标签即获得全局变量ohmscript srchttps://unpkg.com/ohm-js17/dist/ohm.min.js/scriptNode.jsCommonJSnpm install ohm-js后用require引入const ohm require(ohm-js);也可用 ES Module 引入import * as ohm from ohm-js;Deno直接以 URL 方式导入import * as ohm from https://unpkg.com/ohm-js17;在仓库内部如 examples/csv/index.js 与 examples/math/index.html示例均以相对路径引用packages/ohm-js的构建产物。三、定义文法Grammar三种方式与ohm.grammar()要使用 Ohm第一步是写一份用 Ohm 语言描述的文法。文法是对目标语言或数据格式的形式化定义。仓库文档总结了三种定义途径方式一JavaScript 字符串内联定义推荐用String.raw模板字符串const myGrammar ohm.grammar(String.raw MyGrammar { greeting Hello | Hola } );推荐使用String.raw模板字符串原因在于 Ohm 文法中反斜杠转义序列如\n、\与 JavaScript 字符串转义规则冲突不经过String.raw时你需要双重转义写\\n而不是\n。方式二独立.ohm文件Node.js 场景在myGrammar.ohm文件中写MyGrammar { greeting Hello | Hola }然后在 JavaScript 中读取文件内容并实例化const fs require(fs); const ohm require(ohm-js); const contents fs.readFileSync(myGrammar.ohm, utf-8); const myGrammar ohm.grammar(contents);仓库中的真实案例与此完全一致examples/csv/index.js 用fs.readFileSync(join(__dirname, csv.ohm))读取文法后调用ohm.grammar(contents)doc/README.md 中的算术示例也是ohm.grammar(fs.readFileSync(arithmetic.ohm))。方式三一次定义多个文法当源文本中包含多个文法定义时应使用ohm.grammars(source, optNamespace?)它返回一个包含所有 Grammar 实例的对象而ohm.grammar()只接受恰好一个文法定义否则会抛出错误。关于ohm.grammar(source, optNamespace?)的第二个参数optNamespace是一个用于解析对其他文法的引用的对象。例如文法源以继承声明开头MyGrammar : OtherGrammar { ... }则optNamespace中应有名为OtherGrammar的属性。从源码看packages/ohm-js/src/main.jsgrammar()内部实际调用grammars()后者以Object.create(optNamespace || {})创建命名空间并自动识别 Node.js Buffer 输入自动调用toString()文法源会先经过 Ohm 自举的ohmGrammar.match(source, Grammars)语法校验失败时抛出grammarSyntaxError。换句话说文法本身的语法错误会在实例化阶段被立即发现。文法实例化的整体流程可参考 doc/images/instantiating-grammars.png从 Ohm 文法源码出发经过 Ohm 语言自身的解析与构建得到可用的 Grammar 对象。四、匹配输入match()、succeeded()与failed()得到 Grammar 对象后用match()方法识别输入const userInput Hello; const m myGrammar.match(userInput); if (m.succeeded()) { console.log(Greetings, human.); } else { console.log(Thats not a greeting!); }match()返回一个MatchResult对象对应源码 packages/ohm-js/src/MatchResult.js用succeeded()/failed()判断是否识别成功。匹配过程见 doc/images/matching.png 的示意输入串被文法规则逐步消费并构造出语法树。match()的完整签名是g.match(str, optStartRule?)见 doc/api-reference.md第二参数optStartRule指定起始规则名不指定时默认起始规则继承自父文法supergrammar若无父文法则取文法定义中第一条规则。匹配失败的 MatchResult 还额外提供面向用户友好的错误信息r.message含出错位置与输入摘录、r.shortMessage不含摘录的精简版、r.getRightmostFailurePosition()失败位置索引与r.getRightmostFailures()最右失败位置处的 Failure 对象数组。examples/math/index.html 的界面正是用这些 API 定位并展示期望什么输入的错误气泡。doc/README.md还给出了一个紧凑的完整示例——用ohm.grammar()从字符串实例化Laugh文法并匹配多种输入const ohm require(ohm-js); const g ohm.grammar( Laugh { laugh lol | lmao lol l o l }); assert(g.match(lol).succeeded()); assert(!g.match(lmao).failed()); assert(g.match(loooooool).succeeded());这里lol l o l中的是贪婪重复操作符o会尽可能多地匹配o因此loooooool也能成功匹配。补充增量解析 Matcher除了一次性match()g.matcher()可创建支持增量匹配的 Matcher 对象用于编辑器/IDE 场景。当输入通过m.replaceInputRange(startIdx, endIdx, str)修改后再次m.match()会尽量复用之前的局部匹配结果小改动通常只需极短的重匹配时间。Matcher 的完整方法getInput、setInput、replaceInputRange、match、trace见 doc/api-reference.md。仓库还提供了完整的增量解析演示页面 examples/incremental/index.html。五、实现语义从算术文法到eval求值器5.1 文法示例arithmetic.ohmdoc/README.md的核心示例是一份算术文法与仓库中的 packages/ohm-js/test/data/arithmetic.ohm 完全一致Arithmetic { Exp AddExp AddExp AddExp PriExp -- plus | AddExp - PriExp -- minus | PriExp PriExp ( Exp ) -- paren | number number digit }这份文法演示了两个 Ohm 的关键特性左递归支持AddExp的第一种情形以AddExp自身开头。Ohm 完整支持左递归规则见仓库根 README.md因此你可以用最自然的方式定义左结合left-associative的运算符。行内规则声明inline rule declarations-- plus、-- minus、-- paren是大小写标签case labels。AddExp AddExp PriExp -- plus会被展开为独立规则AddExp_plus AddExp PriExp。其作用是让同一规则各分支保持一致的元素个数arity——若直接写AddExp AddExp PriExp | PriExp两个分支分别产生 3 个和 1 个语法树节点语义动作将无法统一签名ohm.grammar()会直接抛错。展开后AddExp恒为 arity 1AddExp_plus恒为 arity 3。行内规则声明的完整语法见 doc/syntax-reference.md。5.2 定义eval操作文法只回答什么输入合法不回答输入是什么意思。要对合法输入做点事需要创建语义Semantics——一组针对某文法相关的操作operation与属性attribute的集合。语法如下const fs require(fs); const g ohm.grammar(fs.readFileSync(arithmetic.ohm)); const semantics g.createSemantics().addOperation(eval, { Exp(e) { return e.eval(); }, AddExp(e) { return e.eval(); }, AddExp_plus(left, op, right) { return left.eval() right.eval(); }, AddExp_minus(left, op, right) { return left.eval() - right.eval(); }, PriExp(e) { return e.eval(); }, PriExp_paren(open, exp, close) { return exp.eval(); }, number(chars) { return parseInt(this.sourceString, 10); } }); const match g.match(1 (2 - 3) 4); assert.equal(semantics(match).eval(), 4);这段代码即doc/README.md的核心完整示例其中蕴含的机制是操作Operation对成功匹配结果求值的一个函数类似访问者Visitor模式——递归遍历语法树在每个节点调用动作字典action dictionary中匹配的语义动作。语义动作Semantic Action一个普通 JS 函数其参数就是该规则体产出的语法树节点。例如AddExp_plus的规则体AddExp PriExp产出 3 个节点因此动作签名为(left, op, right)。动作内部再调用子节点的eval()完成递归求值。this.sourceString在语义动作中this绑定到产生该节点的 CST 节点doc/api-reference.md 定义的n.sourceString等价于n.source.contents即该节点消费的输入子串。number(chars)中chars是digit的迭代节点参数而真正的数值来自this.sourceString如123经parseInt转为数字。算术优先级由文法结构保证AddExp_plus的右操作数是PriExp括号或数字因此(2 - 3)会先被整体求值最终1 (2 - 3) 4 4。值得注意的是语义动作的参数个数会被 Ohm 在创建操作时静态校验arity checking如果与文法规则产出的节点数不符创建阶段即抛错从而把一类常见错误提前暴露见 examples/math/index.html 的详细注释。5.3 更完整的实战参考math 示例doc/README.md特别推荐了 examples/math/index.html它是仓库中注释最详尽extensively commented的示例在算术文法基础上进一步加入了MulExp、ExpExp幂运算与一元正负号并为同一份文法定义了多套语义interpret操作直接求值支持pi、e常量ident动作通过constants[this.sourceString]查表asLisp属性把表达式翻译成 Lisp 风格的树如(24)*7→[*, [, 2, 4], 7]。属性Attribute与操作的区别在于属性像属性一样访问n.asLisp而非n.asLisp()且结果被记忆化memoized——每个节点最多求值一次toTree/toTwoD操作生成 HTML 元素树与二维数学排版_nonterminal特殊动作当动作字典缺少与规则同名的动作时会回退调用_nonterminal(...children)。示例用children.length 1时透传子节点结果从而省去为Exp、AddExp、MulExp等单子节点规则逐一编写动作。页面还演示了同一 MatchResult 可同时喂给多套语义的用法输入框每按键一次只做一次g.match()随后分别调用s(r).interpret()、s(r).asLisp、s(r).toTree()、s(r).toTwoD()同时刷新四个视图——这正是文法与语义分离 惰性求值带来的实用收益。六、语法要点速览规则、表达式与内建规则为读懂更多文法以及下文常见模式这里整理 doc/syntax-reference.md 的核心语法基本术语文法如Arithmetic { Expr 1 1 }包含名为Arithmetic的文法与规则Expr右侧为规则体parsing expression。Parsing Expressions解析表达式表达式形式说明终结符hello there精确匹配引号内字符\转义特殊字符支持\b \f \n \r \t、\xHH、\uHHHH、\u{hexDigits}任意 Unicode 码点字符范围a..c匹配落在区间内的单个码点含边界规则应用ruleName/ruleNameexpr匹配规则体参数化规则如ListOffield, ;重复expr*、expr、expr?0 次或多次 / 1 次或多次 / 可选不消耗输入序列expr1 expr2依次匹配选择expr1 \| expr2尝试前者失败则尝试后者肯定前瞻 expr能匹配但不消耗输入否定前瞻~ expr不能匹配则成功不消耗输入词法化# expr在句法规则中以词法上下文匹配阻止跳过空白内建规则定义见 packages/ohm-js/src/built-in-rules.ohmany下一个 Unicode 字符/码点、letter、lower、upper、digit0..9、hexDigit、alnum、space、end等价~any以及参数化规则caseInsensitiveohm、ListOfelem, sep、NonemptyListOfelem, sep、listOfelem, sep词法版、applySyntacticruleName。值得注意自 v17 起any消耗的是完整 Unicode 码点而非 16 位码元如 emoji 视为单个字符。句法规则 vs 词法规则Syntactic vs. Lexical Rules——这是 Ohm 最需要记住的约定规则名以大写字母开头→句法规则隐式跳过空白规则名以小写字母开头→词法规则不跳空白空白由文法中的space规则定义默认匹配空格、制表符、换行等 ES5 空白字符可被覆盖实现上句法规则体在每个表达式前隐式插入spacesspaces space*的应用若起始规则是句法规则顶层应用前后也会跳过空白用#运算符可在句法规则内部某处禁止跳空白如KeyAndValue #(letter alnum) : #(digit)只能匹配count :33而匹配不了count: 33。文法级语法文法可继承grammarName : Supergrammar { ... }规则可定义、覆盖:、扩展等价于expr | oldBody自 v15.3.0 起支持超类拼接super-splice运算符...规则可带参数Repeatx x x规则声明后可带规则描述如ident (an identifier)用于生成更友好的错误消息——比如匹配123失败时提示期望一个标识符而非底层的期望一个字母。七、调试文本 trace 与可视化Ohm 提供两种调试工具文本轨迹text trace与图形可视化器visualizer。文本 trace 用法极其简单把g.match(str)换成g.trace(str)返回的是 Trace 对象其toString()输出解析器在匹配过程中做出的全部决策。doc/README.md给出的真实输出示例文法G { start letter }匹配abab ✓ start ⇒ ab ab ✓ letter ⇒ ab ab ✓ letter ⇒ a ab ✓ lower ⇒ a ab ✓ Unicode [Ll] character ⇒ a b ✓ letter ⇒ b b ✓ lower ⇒ b b ✓ Unicode [Ll] character ⇒ b ✗ letter ✗ lower ✗ Unicode [Ll] character ✗ upper ✗ Unicode [Lu] character ✗ unicodeLtmo ✗ Unicode [Ltmo] character ✓ end ⇒ 每一行左侧是当时的输入余量✓/✗标记成功/失败缩进反映规则的嵌套深度⇒后是匹配结果。从输出可以清楚看到letter依次尝试lower、upper、unicodeLtmo三个子规则直到输入耗尽后的end匹配。类似的深度调试能力也可以从 packages/ohm-js/src/Trace.js 的实现中印证。图形可视化器则是交互式的解析过程动画仓库文档用图展示了其效果doc/images/visualizer.png / doc/images/visualizer-small.png适合在 examples/viz/index.html 与 examples/incremental/index.html 中体验本地版本。八、常见模式与陷阱来自 Patterns and Pitfallsdoc/patterns-and-pitfalls.md 总结了开发中最常遇到问题的解决方案这里摘取与入门强相关的几条贪婪匹配*和总是尽可能多地消费输入因此等价于正则/^a*a/的 Ohm 表达式a* a永远匹配不了任何输入。可用否定前瞻限定allButLastA (~(a end) a)*匹配除最后一个a外的所有a。分隔符字符串delimited strings用否定前瞻实现任意非终结符字符string stringDelimiter (~stringDelimiter any)* stringDelimiter注释支持先把注释定义为规则再扩展space规则使其被视为空白comment /* (~*/ any)* */ space comment保留字/关键字为防前缀冲突invsinstanceof与误伤合法标识符inProgress对关键字加否定前瞻in in ~identifierPart精确匹配 n 次Ohm 不支持量词语法用序列展开即可zipCode digit digit digit digit。运算符优先级用左递归规则分层编码低优先级规则引用高优先级规则addExp引用mulExp后者引用priExp同时警惕歧义递归如addExp addExp addExp这类写法在 Ohm 中会得到右结合解析。迭代节点处理letter的动作参数是迭代节点可iterNode.children.map(c c.prettyPrint())或为操作定义_iter(...children)动作后直接调用可选节点?可用可选链optNode.child(0)?.myOperation()。九、进一步阅读API 参考Grammar、Matcher、MatchResult、Semantics/Operation/Attribute、解析节点 APIchild、children、numChildren、ctorName、source、sourceString等、内建asIteration操作把ListOf表达式适配为迭代节点接口语法参考全部解析表达式与文法语法的权威定义Ohm 设计哲学文法/语义分离与惰性求值的动机与收益模式与陷阱上述实战问题的完整讨论发布文法指南分享自写文法的建议仓库示例目录 examples/CSV 解析examples/csv/index.js、ECMAScript 文法与测试examples/ecmascript/、运算符优先级examples/operators/、缩进敏感文法examples/indentation-sensitive/、Markdown 解析等。十、结语一条清晰的 Ohm 上手路径是先用ohm.grammar()match()让文法认得输入再用createSemantics().addOperation(...)让程序理解输入最后用trace()定位问题。本文所有示例均可直接复制运行并可在 examples/math/index.html 中看到完整可交互的求值/翻译/排版实现。若要在自己的语言设计中处理更复杂的语法继承、参数化规则、缩进、增量解析请以上述官方文档与仓库源码为参照逐步深入。赞分享开发工具编译器【免费下载链接】ohmA library and language for building parsers, interpreters, compilers, etc.项目地址https://gitcode.com/gh_mirrors/oh/ohm点击查看免费下载相关推荐VS Code 自动更新与版本管理一份完整的实操指南VS Code 自动更新与版本管理一份完整的实操指南 VS CodeVisual Studio Code的自动更新机制决定着你和团队用的到底是哪个版本、开发工具代码编辑器DeepLabV3Plus语义分割终极指南从入门到精通完整实践DeepLabV3Plus语义分割终极指南从入门到精通完整实践 DeepLabV3Plus是当前语义分割领域中最具影响力的模型之一在Pascal VOC和C人工智能计算机视觉深度学习如何为CotEditor创建自定义语法定义文件完整入门指南如何为CotEditor创建自定义语法定义文件完整入门指南 CotEditor作为macOS上轻量级的纯文本编辑器其强大的 语法高亮功能 让代码编辑变得更加开发工具桌面应用上一篇终极PostgreSQL触发器与存储过程指南掌握PL/pgSQL高级编程技巧下一篇Slidev 上下文菜单定制指南通过 context-menu.ts 扩展与 frontmatter 开关控制右键菜单创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考