【Janet】语法与解析器
一个 Janet 程序的生命周期开始于一个文本文件,像系统上的其他文件一样,就是一个字节序列。Janet 源文件必须是 UTF-8 或 ASCII 编码。程序编译或运行之前,需要将源码转换到一个数据结构。像 Lisp 一样,Janet 源代码是同构的,即代码由 Janet 的核心数据结构表示,因此,语言中用于操作元组、字符串和表(table)的所有功能都可以轻松地用于操作源代码。
Janet 代码转换成数据结构之前,必须先由 Janet 解析器读取解析。解析器在 Lisp 中通常被称为读取器,它将纯文本翻译成可用于编译器和宏的数据结构。在 Janet 中它叫做解析器而不是读取器,是因为读取阶段并不会执行代码。这种方式更安全直接,也让语法高亮,格式化以及语法分析变得更简单。因为解析器不可扩展,Janet 的哲学是通过宏来扩展语言,而不是读取器宏。
nil,true和false
值 nil, true 和 false 都是可以在解析器中以其本身形式输入的字面量。
nil
true
false
符号
Janet 符号是一串不以数字或冒号开头的字母数字组成的字符序列。可以包含字符 !,@,$,%,^,&,*,-,_,+,=,:,<,>,.,? 以及 Unicode 字符。
为方便起见,绝大多数符号应该采用全小写以破折号连接的形式(称为 kebab case[烤串格式])。
来自其他模块的符号通常会包含一个正斜杠,它用来将模块名和模块中定义的符号名区分开(就是 Go 语言中的 . 运算符)。
symbol
kebab-case-symbol
snake_case_symbol
my-module/my-function
*****
!%$^*__--__._+++===-crazy-symbol
*global-var*
你好
Keywords
Janet 关键字类似于符号但是以 : 开头。它于符号的用法不同,关键字会被编译器当作常量,而不是名称。关键字常用于表(table)和结构体的键,或者宏的部分语法。
:keyword
:range
:0x0x0x0
:a-keyword
::
:
数字
Janet 数字表示为 IEEE 754 浮点数。语法于大多数语言类似。数字可以以十进制书写,并添加下划线分隔。小数点可以用来表示浮点数。使用进制加 r 前缀可以将数字表示为其他进制。例如16可以写成 16, 1_6, 16r10, 4r100 或 0x10 。 0x 前缀因为广泛使用也被支持。进制基数必须以十进制书写,范围是 2 到 36 之间。超过十进制的数用字母表示数字,大小写不敏感。
0
12
-65912
4.98
1.3e18
1.3E18
18r123C
11raaa&a
1_000_000
0xbeef
字符串
Janet 字符串使用双引号包裹。字符串是 8-bit clean 的,可以包含任意字节序列,包括内嵌的0字节。在字符串中插入双引号需要使用反斜杠转义。对于不可打印字符,你可以选择使用几种常见的转义字符,或者使用 \xHH 转义序列来以十六进制形式转义一个字节。支持的转义字符如下:
\xHH以十六进制转义一个字节。\n换行(ASCII 10)\t制表符(ASCII 9)\r回车(ASCII 13)\0空(ASCII 0)\z空(ASCII 0)\f换页符(ASCII 12)\e退出键(ASCII 27)\"双引号(ASCII 34)\uxxxx转义4个十六进制字符表示的 UTF-8 码点\Uxxxxxx转义6个十六进制字符表示的 UTF-8 码点\\反斜杠(ASCII 92)
字符串中的换行符会被忽略,这样可以定义一个不包含换行的多行字符串。
# same as "Thisisastring"
"This
is
a
string"
长字符串
Janet 中另一种表示字符串的方式是长字符串,用反引号表示。长字符串以任意数量的反引号开始,并以相同数量的反引号结束。长字符串不会进行转义,它可以用来定义包含换行,不可打印字符或需要多重转义字符的字符串。
# same as "This\nis\na\nstring."
``
This
is
a
string
``# same as "This is\na string."
`
This is
a string.
`
长字符串常用于文档,详见文档章节。
Buffers
缓冲区和字符串非常相似,但是它是可修改的。Janet 中的字符串创建后是不可修改的,但是缓冲区创建之后可以修改。缓冲区的语法和字符串和长字符串一样,但是缓冲区必须以 @ 开头。
@""
@"Buffer."
@``Another buffer``
@`
Yet another buffer
`
元组
元组是由圆括号或方括号包裹的以空格分隔的一些列值。解析器将字符 ASCII 32, \0, \f, \n, \r, \t 或 \v 当作空白符。
(do 1 2 3)
[do 1 2 3]
方括号表示这个元组会被用作元组字面量而不是函数调用,宏调用或特殊形式。如果一个元组有方括号,解析器会在该元组上设置一个标志,以便让编译器知道要将这个元组编译成一个构造器。程序员可以通过 tuple/type 函数来检查一个元组是否有括号。
数组
数组与元组类似,但是以 @ 开头表示可以修改。
@(:one :two :three)
@[:one :two :three]
结构体
结构体是由大括号包裹的以空格分隔的一系列键值对。键值对序列定义为 key1,value1,key2,value2,等等。元素数量必须是偶数,否则解析器会发出解析错误。任何值都可以做为键和值。使用 nil 或 math/nan 做为键时,该键值对在解析后会被丢弃。
{}
{:key1 "value1" :key2 :value2 :key3 3}
{[1 2 3] [4 5 6]}
{@[] @[]}
{1 2 3 4 5 6}
Tables
表和结构体有相同的语法,但是以 @ 开头表示可以修改。
@{}
@{:key1 "value1" :key2 :value2 :key3 3}
@{[1 2 3] [4 5 6]}
@{@[] @[]}
@{1 2 3 4 5 6}
注释
单行注释以 # 开始,没有多行注释。
语法糖
在其他编程语言中,通常被称为读取宏(reader macros)的东西,Janet 为其中几种形式提供了简写符号。在 Janet 中这些语法以前缀形式出现且不可扩展。
'x
(quote x) 的语法糖。
;x
(splice x) 的语法糖。
~x
(quasiquote x) 的语法糖。
,x
(unquote x) 的语法糖。
|(body $)
(short-fn (body $)) 的语法糖。
这些简写符号可以以任意顺序组合,例如 ''x ((quote (quote x))) 或 ,;x ((unquote (splice x)))。
语法高亮
对于语法高亮,janet.vim 中有一些初步的Vim语法高亮支持。然而,通用的Lisp语法高亮应该也能提供很好的效果。此外,还可以通过从 Janet 源代码中运行 make grammar 命令来生成一个 janet.tmLanguage 文件,供其他程序使用。
语法
对于那些正在寻找更简洁语法描述的人来说,下面给出了一个用于识别 Janet 源代码的 PEG 语法。PEG 语法本身与 EBNF 类似。更多关于 PEG 语法的信息可以在 PEG 部分找到。
PEG(Parsing Expression Grammar,解析表达式语法)和EBNF(Extended Backus-Naur Form,扩展巴科斯范式)都是用于描述语法的工具,它们在编程语言的解析和编译器设计中非常重要。
(def grammar~{:ws (set " \t\r\f\n\0\v"):readermac (set "';~,|"):symchars (+ (range "09" "AZ" "az" "\x80\xFF") (set "!$%&*+-./:<?=>@^_")):token (some :symchars):hex (range "09" "af" "AF"):escape (* "\\" (+ (set `"'0?\abefnrtvz`)(* "x" :hex :hex)(* "u" [4 :hex])(* "U" [6 :hex])(error (constant "bad escape")))):comment (* "#" (any (if-not (+ "\n" -1) 1))):symbol :token:keyword (* ":" (any :symchars)):constant (* (+ "true" "false" "nil") (not :symchars)):bytes (* "\"" (any (+ :escape (if-not "\"" 1))) "\""):string :bytes:buffer (* "@" :bytes):long-bytes {:delim (some "`"):open (capture :delim :n):close (cmt (* (not (> -1 "`")) (-> :n) '(backmatch :n)) ,=):main (drop (* :open (any (if-not :close 1)) :close))}:long-string :long-bytes:long-buffer (* "@" :long-bytes):number (cmt (<- :token) ,scan-number):raw-value (+ :comment :constant :number :keyword:string :buffer :long-string :long-buffer:parray :barray :ptuple :btuple :struct :dict :symbol):value (* (any (+ :ws :readermac)) :raw-value (any :ws)):root (any :value):root2 (any (* :value :value)):ptuple (* "(" :root (+ ")" (error ""))):btuple (* "[" :root (+ "]" (error ""))):struct (* "{" :root2 (+ "}" (error ""))):parray (* "@" :ptuple):barray (* "@" :btuple):dict (* "@" :struct):main :root})
