#语法 # #作用域 #规则 #定义 #语言 #语法

sbnf

用于编写 sublime-syntax 文件的 BNF 风格语言

11 个版本

0.6.4 2023 年 10 月 9 日
0.6.2 2023 年 9 月 29 日
0.6.0 2023 年 5 月 25 日
0.5.1 2021 年 10 月 20 日
0.1.0 2020 年 3 月 3 日

#28 in 文本编辑器


用于 sbnfc

自定义许可

300KB
7.5K SLoC

SBNF

Build Status Crate

用于编写 sublime-syntax 文件的 BNF 风格语言。

现在就在 实时游乐场 尝试一下!

SBNF 目前用于 SWI-Prolog

动机与目标

编写语法定义容易出错,且结果难以维护。虽然 branch_point 是一个很棒的功能,但在使用时它会大大增加复杂性和重复性。

SBNF 试图做以下事情

  • 提供一种可维护的、声明性的语言来编写 sublime 语法定义
  • 快速编译,以便快速迭代
  • 编译为高效的语法,与手工制作的语法相当

安装

安装 rust 后,您可以使用以下命令下载、构建和安装 SBNF 的最新发布版本

$ cargo install sbnfc

或者如果您想使用最新功能,克隆此仓库,然后使用以下命令构建和安装

$ cargo install --path cli

请注意,为了使用生成的语法,您至少需要 Sublime Text build 4077,并支持 Sublime Syntax 的第 2 版。

Sublime 语法

SBNF 的语法定义位于 sbnf/sbnf.sbnf。要编译它,只需运行 sbnf sbnf/sbnf.sbnf,然后可以将 sbnf/ 目录链接或复制到您的用户包。

示例

以下是一个简化版C语言的SBNF语法。它只允许全局/局部变量声明、函数定义和简单的函数调用。即使是这个简化版,使用所需的meta.functionmeta.function-call作用域进行正确解析也非常困难,因为函数定义和函数调用都需要分支点。

NAME = `simplec`

prototype : ( ~comment )* ;

comment : '(//+).*\n?'{comment.line, 1: punctuation.definition.comment} ;

main : ( variable-declaration | function-definition )* ;

IDENTIFIER = '\b[A-Za-z_]+\b'

function-definition{meta.function}
: type
  IDENTIFIER{entity.name.function}
  `(`
  `)`
  block
;

block{meta.block} : '{' statement* '}' ;

statement : variable-declaration
          | value ';'
          | block
          ;

variable-declaration : type IDENTIFIER{variable} ( '=' value )? ';' ;

type : IDENTIFIER{storage.type} ;

value : '[0-9]+'{constant.numeric}
      | function-call
      ;

# Function calls don't have arguments :)
function-call{meta.function-call}
: IDENTIFIER{variable.function meta.path} `(` `)` ;

上述语法编译成以下

%YAML 1.2
---
# https://text.sublime.net.cn/docs/syntax.html
version: 2
name: simplec
scope: source.simplec
contexts:
  # Rule: block
  block|0:
    - meta_content_scope: meta.block.simplec
    - match: '{'
      scope: meta.block.simplec
      set: block|1
    - match: '\S'
      scope: invalid.illegal.simplec
      pop: true
  # Rule: block
  block|1:
    - meta_content_scope: meta.block.simplec
    - include: include!block@1
    - match: '[0-9]+'
      scope: meta.block.simplec constant.numeric.simplec
      push: [block|meta, statement|0]
    - match: '{'
      scope: meta.block.simplec meta.block.simplec
      push: [block|meta, block|1]
    - match: '}'
      scope: meta.block.simplec
      pop: true
    - match: '\S'
      scope: invalid.illegal.simplec
      pop: true
  # Rule: block
  #  For branch point 'block@1'
  block|2|block@1:
    - match: '\b[A-Za-z_]+\b'
      scope: meta.block.simplec variable.simplec
      set: [block|meta, variable-declaration|2]
    - match: '\S'
      fail: block@1
  # Rule: block
  #  For branch point 'block@1'
  block|3|block@1:
    - match: '\('
      scope: meta.block.simplec meta.function-call.simplec
      set: [block|meta, statement|0, function-call|1]
    - match: '\S'
      scope: invalid.illegal.simplec
      pop: true
  # Meta scope context for block
  block|meta:
    - meta_content_scope: meta.block.simplec
    - match: ''
      pop: true
  # Rule: function-call
  function-call|0:
    - meta_content_scope: meta.function-call.simplec
    - match: '\('
      scope: meta.function-call.simplec
      set: function-call|1
    - match: '\S'
      scope: invalid.illegal.simplec
      pop: true
  # Rule: function-call
  function-call|1:
    - meta_content_scope: meta.function-call.simplec
    - match: '\)'
      scope: meta.function-call.simplec
      pop: true
    - match: '\S'
      scope: invalid.illegal.simplec
      pop: true
  function-call|2|block@1:
    - meta_include_prototype: false
    - match: '\b[A-Za-z_]+\b'
      scope: meta.function-call.simplec variable.function.simplec meta.path.simplec
      push: block|3|block@1
      pop: true
  # Rule: function-definition
  function-definition|0:
    - meta_content_scope: meta.function.simplec
    - match: '\)'
      scope: meta.function.simplec
      set: [function-definition|meta, block|0]
    - match: '\S'
      scope: invalid.illegal.simplec
      pop: true
  # Meta scope context for function-definition
  function-definition|meta:
    - meta_content_scope: meta.function.simplec
    - match: ''
      pop: true
  # Include context for branch point block@1
  include!block@1:
    - match: '(?=\b[A-Za-z_]+\b)'
      branch_point: block@1
      branch:
        - type|2|block@1
        - function-call|2|block@1
  # Include context for branch point main@1
  include!main@1:
    - match: '(?=\b[A-Za-z_]+\b)'
      branch_point: main@1
      branch:
        - type|0|main@1
        - type|1|main@1
  # Rule: main
  main:
    - include: include!main@1
    - match: '\S'
      scope: invalid.illegal.simplec
  # Rule: main
  #  For branch point 'main@1'
  main|0|main@1:
    - match: '\b[A-Za-z_]+\b'
      scope: variable.simplec
      push: main|2|main@1
      pop: true
    - match: '\S'
      fail: main@1
  # Rule: main
  #  For branch point 'main@1'
  main|1|main@1:
    - match: '\b[A-Za-z_]+\b'
      scope: meta.function.simplec entity.name.function.simplec
      push: main|3|main@1
      pop: true
    - match: '\S'
      scope: invalid.illegal.simplec
      pop: true
  # Rule: main
  #  For branch point 'main@1'
  main|2|main@1:
    - match: '='
      set: variable-declaration|0
    - match: ';'
      pop: true
    - match: '\S'
      fail: main@1
  # Rule: main
  #  For branch point 'main@1'
  main|3|main@1:
    - match: '\('
      scope: meta.function.simplec
      set: function-definition|0
    - match: '\S'
      scope: invalid.illegal.simplec
      pop: true
  # Rule: prototype
  prototype:
    - match: '(//+).*\n?'
      scope: comment.line.simplec
      captures:
        1: punctuation.definition.comment.simplec
  # Rule: statement
  statement|0:
    - match: ';'
      pop: true
    - match: '\S'
      scope: invalid.illegal.simplec
      pop: true
  type|0|main@1:
    - meta_include_prototype: false
    - match: '\b[A-Za-z_]+\b'
      scope: storage.type.simplec
      push: main|0|main@1
      pop: true
  type|1|main@1:
    - meta_include_prototype: false
    - match: '\b[A-Za-z_]+\b'
      scope: meta.function.simplec storage.type.simplec
      push: main|1|main@1
      pop: true
  type|2|block@1:
    - meta_include_prototype: false
    - match: '\b[A-Za-z_]+\b'
      scope: storage.type.simplec
      push: block|2|block@1
      pop: true
  # Rule: variable-declaration
  variable-declaration|0:
    - match: '[0-9]+'
      scope: constant.numeric.simplec
      set: variable-declaration|1
    - match: '\b[A-Za-z_]+\b'
      scope: meta.function-call.simplec variable.function.simplec meta.path.simplec
      set: [variable-declaration|1, function-call|0]
    - match: '\S'
      scope: invalid.illegal.simplec
      pop: true
  # Rule: variable-declaration
  variable-declaration|1:
    - match: ';'
      pop: true
    - match: '\S'
      scope: invalid.illegal.simplec
      pop: true
  # Rule: variable-declaration
  variable-declaration|2:
    - match: '='
      set: variable-declaration|0
    - match: ';'
      pop: true
    - match: '\S'
      scope: invalid.illegal.simplec
      pop: true

用法

SBNF文件包含两种类型的元素:子句和规则。子句提供语法元数据,如文件扩展名,以及一些元编程。规则是定义语法解析和作用域的BNF风格的规则。

SBNF中的注释从#开始,并在下一行结束。

有关完整语法示例,请参阅sbnf.sbnf

子句

子句的形式为<name> <parameters> = <value>。名称必须遵循大写蛇形命名法。以下名称为元数据保留:

  • NAME:语法的名称。默认为SBNF文件的基名。
  • EXTENSIONS:文件扩展名的空格分隔列表。与sublime-syntax中的file_extensions等效。
  • FIRST_LINE:匹配文件第一行的正则表达式。与sublime-syntax中的first_line_match等效。
  • SCOPE:语法的默认作用域。默认为source.后跟语法的名称的小写形式。
  • SCOPE_POSTFIX:附加到语法中所有作用域的后缀(不包括SCOPE子句)。默认为名称的小写形式。可以留空以省略后缀。
  • HIDDEN:语法是否将在Sublime Text的菜单中显示。

示例

NAME = `SBNF`
EXTENSIONS = `sbnf`
# Don't need this, as this is already the default
# SCOPE = `source.sbnf`

规则

规则的形式为<name> <parameters> <options> : <expression> ;。名称必须遵循短横线命名法

与sublime-syntax文件一样,SBNF语法有两个入口点:mainprototype。它们的行为与sublime-syntax文件中的相同。只有从入口点直接或间接使用的规则会被编译。

规则可以可选地具有参数和选项。参数用于元编程,选项用于Sublime Text特定的选项。

示例

a : 'a' ;
b{source.b} : 'b' ;
c[S] : 'c'{#[S]} ;
d[S]{text.d} : a b c[S] ;

表达式

表达式可以采取以下任何形式

  • `<literal>` <options>:匹配文本字面量的终止符。
  • '<regex>' <options>:根据正则表达式匹配文本的终止符。
  • <identifier> <arguments>:与非终结符号匹配的规则。
  • <expr| <expr>:表达式的选择。语法匹配左表达式或右表达式。可以用作列表,例如:'a' | 'b' | 'c'
  • <expr> <expr>:表达式的连接。语法匹配左表达式后跟右表达式。可以用作列表,例如:'a' 'b' 'c'
  • (<expr>):分组。
  • <expr>?:可选表达式。语法匹配无内容或表达式。
  • <expr>*:重复表达式。语法匹配表达式任意次数,包括0次。
  • ~<expr>:被动表达式。语法匹配直到表达式匹配的任意文本。

选项

选项的形式如下:{<param>, <key>: <value>}。代码,:}除外,<param><key><value>可以包含任意文本。可以提供任意数量的选项,具体取决于选项的内容。如果没有选项,则大括号{}是可选的。

规则允许以下选项

  • <meta-scope>:规则的元范围。相当于sublime-syntax中的meta_scopemeta_content_scope

文本和正则表达式终结符允许以下参数

  • <scope>:终结符的范围。
  • <capture>: <scope>:正则表达式捕获组的范围。 <capture>必须是一个整数。

参数

规则和子句的参数形式为:[<value>, <value>]<value>可以是正则表达式终结符、字面终结符或标识符。相同的名称可用于具有不同参数集的规则/子句。

当使用时,带有参数的规则被实例化。匹配基于每个参数的类型和值。终结符参数基于正则表达式等价性进行匹配,而规则参数基于名称进行匹配。

不引用规则的标识符是规则作用域中唯一的自由变量。它可以匹配任何参数,并且可以被传递或插入。

可以使用以下语法插入变量:#[]。这可以在任何终结符内部或选项内部完成。

示例

main
: a['a'] # instantiates rule 1
| a[a]   # instantiates rule 2
| a['b'] # instantiates rule 3
| b['b'] # error: Ambiguous instantiation
;

# Rule 1.
a['a'] : 'a' ;

# Rule 2.
a[a] : 'a' ;

# Rule 3.
a[A] : 'a' ;

b[A] : 'a' ;
b[B] : 'b' ;

还存在一组全局参数,这些参数是从命令行传递的。这些参数的形式与其他参数相同,应放在文件顶部。它们可能仅由变量组成,并且在全局范围内可用,包括子句。

示例

# Declares a single global parameter
[TYPE]

# Can be used in clauses
NAME = 'd-#[TYPE]'

# As well as rules
main : '#[TYPE]' ;
# 'dmd' is passed to TYPE when compiled
$ sbnf syntax.sbnf dmd

包含/嵌入

SBNF还支持包含/嵌入其他Sublime语法。这只能在带有后缀的文本书面或正则表达式终结符表达式中完成:%include[<with_prototype>]{<syntax>}用于包含语法或%embed[<regex>]{<syntax>}用于嵌入。

请注意,这些直接转换为Sublime语法包含/嵌入功能,因此具有相同的限制。

示例

# This is a basic implementation of the html script tag embedding the javascript
# syntax.
script
: '<script>'{tag.begin.script}
  %embed['</script>']{scope:source.js, embedded.js, 0: tag.end.script}
;
# The above translates to the following context
script:
  - match: '<script>'
    scope: tag.begin.script.example
    embed: scope:source.js
    embed_scope: embedded.js.example
    escape: '</script>'
    escape_captures:
      0: tag.end.script.example
    pop: true
  - match: '\S'
    scope: invalid.illegal.example
# This is a basic implementation of a regex string. It has a prototype rule that
# extends the regex syntax with an escape sequence for the string.

regex-prototype{include-prototype: false}
: ( ~`\'`{constant.character.escape} )*
  # A lookahead is required here, as otherwise we would only pop one context
  # The same is required in a sublime-syntax file
  ~'(?=\')'
;

regex-string{string.quoted}
: `'`{punctuation.definition.string.begin}
  %include[regex-prototype]{scope:source.regexp}
  `'`{punctuation.definition.string.end}
;
# The above translates to the following contexts
regex-string:
  - meta_content_scope: string.quoted.example
  - match: ''''
    scope: string.quoted.example punctuation.definition.string.begin.example
    set: [regex-string|0, regex-string|1]
  - match: '\S'
    scope: invalid.illegal.example
regex-string|0:
  - meta_content_scope: string.quoted.example
  - match: ''''
    scope: string.quoted.example punctuation.definition.string.end.example
    pop: true
  - match: '\S'
    scope: invalid.illegal.example
    pop: true
regex-string|1:
  - meta_include_prototype: false
  - match: ''
    set: scope:source.regexp
    with_prototype:
      - include: regex-prototype|0
regex-prototype|0:
  - meta_include_prototype: false
  - match: '\\'''
    scope: constant.character.escape.example
  - match: '(?='')'
    pop: true

命令行

$ sbnf --help
SBNF compiler 0.4.0

USAGE:
    sbnf [FLAGS] [OPTIONS] <INPUT> [ARGS]...

FLAGS:
    -g               Compile with debug scopes
    -h, --help       Prints help information
    -q               Do not display warnings
    -V, --version    Prints version information

OPTIONS:
    -o <output>        The file to write the compiled sublime-syntax to. Defaults to $INPUT.sublime-syntax if left out. Use a single dash `-` to write to stdout instead.

ARGS:
    <INPUT>      The SBNF file to compile
    <ARGS>...    Arguments to pass to the main and prototype rules

限制

正则表达式等价性

在确定是否在sublime-syntax中创建分支点时,SBNF必须考虑正则表达式是否重叠。以下是一个示例

main : 'aa?'{scope1} 'b'
     | 'a'{scope2} 'c'
     ;

正则表达式'aa?''a'都匹配a,这意味着需要分支点来正确解析此语法。SBNF 不会在这里创建分支点。由于正则表达式的复杂性,分支点仅在等效正则表达式上创建。将示例重写为与SBNF一起使用,得到以下内容

main : 'aa'{scope1} 'b'
     | 'a'{scope1} 'b'
     | 'a'{scope2} 'c'
     ;

将来不太可能改变,因为SBNF不试图理解任何正则表达式。

待办事项

  • 修复编译器中的已知边缘情况。在几个地方,我们使用panic!()而不是提供实现。
  • 当在非弹出循环中使用分支时添加警告。
  • 修复规则引用自身时的无限循环/递归

依赖关系

~2.5MB
~38K SLoC