Parcom:CL Parser Combinators 解析器组合库

简单 Common Lisp 解析器组合库

fosskers/parcom

Simple parser combinators for Common Lisp. parcom 是一个简洁的解析器组合库,风格类似于 Haskell 的 parsec 和 Rust 的 nom

(in-package :parcom)
(parse (*> (string "Tempus") #'space (string "fugit")) "Tempus fugit.")
fugit

parcom 严格操作字符串,而不是流式字节数据,但在其他方面是“零拷贝”的,即原始输入的提取子字符串不会被重新分配。 parcom 没有依赖项。

目录

Compatibility

Compiler| Status ---|--- SBCL| ✅ ECL| ✅ Clasp| ❓ ABCL| ✅ CCL| ✅ Clisp| ✅ Allegro| ✅ LispWorks| ❓

API

以下示例为了简洁起见使用了 (in-package :parcom),但假定您将在实际代码中使用局部别名,如 pc。 此外,大多数示例都使用 parse 运行解析器,但偶尔会使用 funcall 来演示解析成功后剩余的输入是什么。 您通常会在自己的代码中使用 parse

Types and Running Parsers 类型和运行解析器

所有解析器都具有函数签名 string -> parser,其中 parser 是一个结构,其中包含解析成功的 value 以及剩余的输入字符串。

(in-package :parcom)
(funcall (string "Hello") "Hello there")
#S(PARSER :INPUT " there" :VALUE "Hello")

当然,解析器可能会失败,在这种情况下,将返回一个 failure 结构:

(in-package :parcom)
(funcall (string "Hello") "Bye!")
#S(FAILURE :EXPECTED "string: Hello" :ACTUAL "string: Bye!")

通常,我们调用 parse 来完全运行一些组合的解析器,并产生最终输出:

(in-package :parcom)
(apply #'+ (parse (sep (char #\.) #'unsigned) "123.456.789!"))
1368

否则,parse 将忽略任何最终的、未使用的输入。 如果解析失败,它还会引发 Condition 错误。

Parsers 解析器

“解析器”是一个消耗某些特定输入并产生单个结果的函数。

Characters and Strings 字符和字符串

char

解析给定的字符。

(in-package :parcom)
(parse (char #\a) "apple")
#\a

string

解析给定的字符串。

(in-package :parcom)
(parse (string "Hello") "Hello there!")
Hello

any

解析任何字符。

(in-package :parcom)
(parse #'any "Hello there!")
#\H

anybut

解析除了你不想解析的字符之外的任何字符。

(in-package :parcom)
(parse (anybut #\!) "Hello there!")
#\H
(in-package :parcom)
(funcall (anybut #\H) "Hello there!")
#S(FAILURE :EXPECTED "anybut: not H" :ACTUAL "Hello there!")

hex

解析任何大小写的十六进制字符。

(in-package :parcom)
(funcall (many #'hex) "abcd0efgh")
#S(PARSER :INPUT "gh" :VALUE (#\a #\b #\c #\d #\0 #\e #\f))

eof

识别输入的结束。

(in-package :parcom)
(parse #'eof "")
T
(in-package :parcom)
(parse (*> (string "Mālum") #'eof) "Mālum")
T
(in-package :parcom)
(funcall (*> (string "Mālum") #'eof) "Mālum rubrum")
#S(FAILURE :EXPECTED "the end of the input" :ACTUAL " rubrum")

Numbers 数字

unsigned

将正整数解析为 fixnum

(in-package :parcom)
(parse #'unsigned "44")
44

integer

将正整数或负整数解析为 fixnum

(in-package :parcom)
(parse #'integer "-44")
-44

float

将正浮点数或负浮点数解析为 float

(in-package :parcom)
(parse #'float "123.0456")
123.0456

Whitespace 空白字符

newline

匹配单个换行符。

(in-package :parcom)
(let ((s (concatenate 'cl:string '(#\newline #\a #\b #\c)))) ; "\nabc"
 (parse #'newline s))
#\Newline

space, space1

解析 0 个或多个 ASCII 空格和制表符。

(in-package :parcom)
(length (parse #'space "  Salvē!"))
3

解析 1 个或多个 ASCII 空格和制表符。

(in-package :parcom)
(length (parse #'space1 "  Salvē!"))
3
(in-package :parcom)
(funcall #'space1 "Salvē!")
#S(FAILURE :EXPECTED "space1: at least one whitespace" :ACTUAL "Salvē!")

multispace, multispace1

解析 0 个或多个 ASCII 空格、制表符、换行符和回车符。

(in-package :parcom)
(length (parse #'multispace (concatenate 'cl:string '(#\tab #\newline #\tab))))
3

解析 1 个或多个 ASCII 空格、制表符、换行符和回车符。

(in-package :parcom)
(length (parse #'multispace1 (concatenate 'cl:string '(#\tab #\newline #\tab))))
3
(in-package :parcom)
(funcall #'multispace1 "Ārcus")
#S(FAILURE
  :EXPECTED "multispace1: at least one space-like character"
  :ACTUAL "Ārcus")

Taking in Bulk 批量获取

这些始终产生直接从原始输入借用的子字符串。

take

从输入中提取 n 个字符。

(in-package :parcom)
(parse (take 3) "Arbor")
Arb

take-while, take-while1

当某个谓词成立时,提取字符。

(in-package :parcom)
(parse (take-while (lambda (c) (equal #\a c))) "aaabbb")
aaa

take-while1 类似于 take-while,但必须至少产生一个字符。

(in-package :parcom)
(funcall (take-while1 (lambda (c) (equal #\a c))) "bbb")
#S(FAILURE :EXPECTED "take-while1: at least one success" :ACTUAL "bbb")

rest

消耗剩余的输入。 总是成功。

(in-package :parcom)
(parse (<*> (string "Salvē") (*> #'space #'rest)) "Salvē domine!")
("Salvē" "domine!")

Combinators 组合子

“组合子”将子解析器组合在一起以形成复合结果。 它们使我们能够表达诸如“解析这个然后解析那个”和“解析这个,然后可能解析那个,但前提是……”等意图。

*>, right

依次运行多个解析器,但产生最右边的解析器的值。 right 是别名。

(in-package :parcom)
(funcall (*> (char #\!) #'unsigned) "!123?")
#S(PARSER :INPUT "?" :VALUE 123)

<*, left

依次运行多个解析器,但产生最左边的解析器的值。 left 是别名。

(in-package :parcom)
(funcall (<* (char #\!) #'unsigned) "!123?")
#S(PARSER :INPUT "?" :VALUE #\!)

<*>, all

解析器组合,将所有结果作为列表产生。 all 是别名。

(in-package :parcom)
(parse (<*> #'unsigned (char #\!) #'unsigned) "123!456")
(123 #\! 456)

此库不提供柯里化机制,因此通常在 Haskell 中可用的技术,即将函数 fmap 到 <*> 链上,必须改为使用 apply 来完成:

(in-package :parcom)
(apply #'+ (parse (<*> #'unsigned (*> (char #\!) #'unsigned)) "123!456"))
579

<$, instead

运行某个解析器,但如果解析成功,则将其内部值替换为其他值。 instead 是别名。

(in-package :parcom)
(parse (<$ :roma (string "Roma")) "Roma!")
:ROMA

alt

接受一组解析器中第一个成功的解析器的结果。 可以组合任意数量的解析器。

(in-package :parcom)
(parse (alt (string "dog") (string "cat")) "cat")
cat

opt

如果解析器失败,则产生 nil,但不会使整个过程失败,也不会消耗任何输入。

(in-package :parcom)
(parse (opt (string "Ex")) "Exercitus")
Ex
(in-package :parcom)
(parse (opt (string "Ex")) "Facēre")
NIL

between

一个由另外两个解析器夹在中间的主解析器。 仅保留主解析器的值。 适用于解析括号等。

(in-package :parcom)
(parse (between (char #\!) (string "Salvē") (char #\!)) "!Salvē!")
Salvē

many, many1

many 解析 0 个或多个解析器的出现。 many1 要求至少一个解析成功,否则将引发 Condition 错误。

(in-package :parcom)
(parse (many (alt (string "ovēs") (string "avis"))) "ovēsovēsavis!")
("ovēs" "ovēs" "avis")

sep, sep1

sep 解析 0 个或多个由某个 sep 解析器分隔的解析器实例。 sep1 要求至少一个解析成功,否则将引发 Condition 错误。

(in-package :parcom)
(parse (sep (char #\!) (string "pilum")) "pilum!pilum!pilum.")
("pilum" "pilum" "pilum")

至关重要的是,如果检测到分隔符,则父解析器也必须成功,否则整个组合将失败。 例如,由于末尾的 !,这将不会解析:

(in-package :parcom)
(parse (sep (char #\!) (string "pilum")) "pilum!pilum!pilum!")

有关分隔符的更宽松行为,请参见 sep-end

sep-end, sep-end1

sep 相同,但分隔符 may 可能出现在最终“父级”的末尾。 同样,sep-end1 要求至少一个父级解析成功。

(in-package :parcom)
(funcall (sep-end (char #\!) (string "pilum")) "pilum!pilum!pilum!scūtum")
#S(PARSER :INPUT "scūtum" :VALUE ("pilum" "pilum" "pilum"))

skip

解析某个解析器 0 次或多次,但丢弃所有结果。

(in-package :parcom)
(parse (*> (skip (char #\!)) #'unsigned) "!!!123")
123

peek

产生解析器的值,但不消耗输入。

(in-package :parcom)
(funcall (peek (string "he")) "hello")
#S(PARSER :INPUT "hello" :VALUE "he")

count

将解析器应用给定的次数,并将结果收集为列表。

(in-package :parcom)
(funcall (count 3 (char #\a)) "aaaaaa")
#S(PARSER :INPUT "aaa" :VALUE (#\a #\a #\a))

recognize

如果给定的解析器成功,则返回消耗的输入作为字符串。

(in-package :parcom)
(funcall (recognize (<*> (string "hi") #'unsigned)) "hi123there")
#S(PARSER :INPUT "there" :VALUE "hi123")

Utilities 实用工具

empty?

给定的字符串是否为空?

(in-package :parcom)
(empty? "")
T

digit?

给定的字符是否为 0 到 9 之间的数字?

(in-package :parcom)
(digit? #\7)
T

fmap

将纯函数应用于解析器的内部内容。

(in-package :parcom)
(fmap #'1+ (funcall #'unsigned "1"))
#S(PARSER :INPUT "" :VALUE 2)

const

产生一个忽略其输入并返回某个原始种子的函数。

(in-package :parcom)
(funcall (const 1) 5)
1

JSON

通过依赖于可选的 parcom/json 系统,您可以解析简单的 JSON 或将 parcom 兼容的 JSON 解析器包含到您自己的自定义解析代码中。 (in-package :parcom/json) 在下面为了简洁起见而使用,但假定您将在自己的代码中使用一个昵称,可能为 pj。 如果您不关心每个 JSON 解析器的具体内容,而只想简单地解析一些 JSON,请使用 parse

转换:

JSON| Lisp ---|--- true| T false| NIL Array| Vector Object| Hash Table Number| double-float String| String null| :NULL

与父级 parcom 库一样,parcom/json 严格地处理字符串,并且不会尝试变得聪明或高性能。 有关更“工业强度”的 JSON 解析库,请参见 jzonparcom/json 的优势在于其简单性和轻量级。

parse

尝试解析任何 JSON 值。 类似于主库中的 parse

(in-package :parcom/json)
(parse "{\"x\": 1, \"y\": 2, \"z\": [1, {\"a\":true}]}")
#<HASH-TABLE :TEST EQUAL :COUNT 3 {100985EBF3}>
(in-package :parcom/json)
(parse "[1.9,true,3e+7,\"hi\",[4],null]")
#(1.9d0 T 3.0d7 "hi" #(4) :NULL)

支持非 ASCII 和 Unicode 字符:

(in-package :parcom/json)
(parse "\"hēllお🐂\\u03B1\"")
hēllお🐂α

json

解析任何类型的 JSON(实际解析器)。

(in-package :parcom/json)
(json "{\"x\": 1, \"y\": 2, \"z\": [1, {\"a\":true}]} ")
#S(P:PARSER :INPUT " " :VALUE #<HASH-TABLE :TEST EQUAL :COUNT 3 {1009D16A63}>)

还有其他子解析器公开,但为了简洁起见,此处将其省略。 如果您需要它们,请查阅源代码。

Writing your own Parsers 编写您自己的解析器

Basics 基础知识

解析器组合子的全部意义在于,编写您自己的解析函数变得很简单。 回想一下,“完全实现的”解析器具有签名 string -> parser。 在最简单的情况下,您的解析器可能如下所示:

(in-package :parcom)
(defun excited-apple (input)
 (funcall (<* (string "Mālum") (char #\!)) input))
(funcall #'excited-apple "Mālum! Ō!")
#S(PARSER :INPUT " Ō!" :VALUE "Mālum")

您可以在其中利用此库提供的组合子来构建对您有用的复合解析器。

Parameterized Parsers 参数化解析器

您还可以参数化您的解析器,类似于 take 之类的解析器或 count 之类的组合子:

(in-package :parcom)
(defun excited-apple (input)
 (funcall (<* (string "Mālum") (char #\!)) input))
(defun excited-apples (n)
 "Parse a certain number of excited apples."
 (lambda (input)
  (funcall (count #'excited-apple n) input)))
(funcall (excited-apples 3) "Mālum!Mālum!Mālum!Mālum!")
#S(PARSER :INPUT "Mālum!" :VALUE ("Mālum" "Mālum" "Mālum"))

因此,如果您的解析器由某个初始参数参数化,则它必须返回一个接受 input 字符串的 lambda。

Failure 失败

您可以在更复杂的手写解析器中使用 fail 来显式地使用您自己的诊断信息进行失败:

(in-package :parcom)
(defun three-sad-pears (input)
 (let ((res (funcall (many (string "Pirum trīste")) input)))
  (cond ((failure-p res)
      (fail "Three sad pears" "No pears at all!"))
     ((< (length (parser-value res)) 3)
      (fail "Three sad pears" "Not enough pears"))
     ((> (length (parser-value res)) 3)
      (fail "Three sad pears" "Way too many pears"))
     (t res))))
(three-sad-pears "Pirum trīste")
#S(FAILURE :EXPECTED "Three sad pears" :ACTUAL "Not enough pears")

请注意使用 parser-value 来访问 parser 结果的当前内部成功值。 同样,parser-input 用于访问剩余的输入。