语法预览:
仓库地址:
Demo 中文编程语言: 展示如何使用 CovScript 从头打造一门简单的中文编程语言 (gitee.com)
第一步:编写 ParserGen 规则
@charset: utf8
import parsergen, unicode
constant syntax = parsergen.syntax
var cvt = new unicode.codecvt.utf8
var cpl_grammar = new parsergen.grammar
cpl_grammar.ext = ".*\\.cpl"
这里先通过 CovScript 预处理指令设置代码的 Unicode 编码,然后引入相关的模块
unicode.codecvt.utf8
是用于处理 ANSII 字符串到 Unicode 字符串之间转换的类
parsergen.grammar
是 ParserGen 的语法类,我们在这里先注册合法的后缀名
然后就是词法规则了
词法规则是基于正则表达式的,所以只要熟悉正则表达式规则就能很快编写出一个词法规则
@begin
cpl_grammar.lex = {
"endl" : unicode.build_wregex(cvt.local2wide("^\\n+$")),
"zh" : unicode.build_wregex(cvt.local2wide("^[\\u4E00-\\u9FA5\\u9FA6-\\u9FEF\\u3007][0-9\\u4E00-\\u9FA5\\u9FA6-\\u9FEF\\u3007]*$")),
"id" : unicode.build_wregex(cvt.local2wide("^[A-Za-z_]\\w*$")),
"num" : unicode.build_wregex(cvt.local2wide("^[0-9]+\\.?([0-9]+)?$")),
"str" : unicode.build_wregex(cvt.local2wide("^(\"|\"([^\"]|\\\\\")*\"?)$")),
"sig" : unicode.build_wregex(cvt.local2wide("^(\\+|-|\\*|/|<|<=|>|>=|=|==|!|!=?|(\\|)|(\\|\\|)|&|&&|\\.|;|,|\\(|\\)|\\[|\\]|\\{|\\})$")),
"ign" : unicode.build_wregex(cvt.local2wide("^([ \\f\\r\\t\\v]+|#.*)$")),
"err" : unicode.build_wregex(cvt.local2wide("^(\"|\\||&)$"))
}.to_hash_map()
@end
这里 zh
类型的词元是基于 UTF-8 中文字符范围,若使用 GBK 可更换为:
unicode.build_wregex(cvt.local2wide("^[\\uB0A1-\\uF7FE\\u8140-\\uA0FE\\uAA40-\\uFEA0\\uA996][0-9\\uB0A1-\\uF7FE\\u8140-\\uA0FE\\uAA40-\\uFEA0\\uA996]*$"))
至于语法规则,也很简单。ParserGen 使用类似 EBNF 的语法规则,只不过是使用 API 的形式来编写,免去了读取语法规则文件的额外开销
@begin
cpl_grammar.stx = {
"begin" : {
syntax.ref("stmts")
},
"ignore" : {
syntax.repeat(syntax.token("endl"))
},
"endline" : {syntax.cond_or(
{syntax.token("endl")},
{syntax.term(";")}
)},
"stmts" : {
syntax.repeat(syntax.ref("statement"), syntax.repeat(syntax.token("endl")))
},
"statement" : {syntax.cond_or(
{syntax.ref("import-stmt")},
{syntax.ref("decl-stmt")},
{syntax.ref("if-stmt")},
{syntax.ref("while-stmt")},
{syntax.ref("control-stmt")},
{syntax.ref("return-stmt")},
{syntax.ref("expr-stmt")}
)},
"identifier" : {
syntax.cond_or({syntax.token("zh")}, {syntax.token("id")})
},
"import-stmt" : {
syntax.term("引入"), syntax.ref("import-list"), syntax.ref("endline")
},
"module-list" : {
syntax.ref("identifier"), syntax.optional(syntax.term("."), syntax.cond_or({syntax.term("*")}, {syntax.ref("module-list")}))
},
"import-list" : {
syntax.ref("module-list"), syntax.optional(syntax.term("别名"), syntax.ref("identifier")), syntax.optional(syntax.term(","), syntax.ref("import-list"))
},
"block-body" : {
syntax.term("{"), syntax.ref("stmts"), syntax.term("}")
},
"argument-list" : {
syntax.ref("identifier"), syntax.repeat(syntax.term(","), syntax.ref("argument-list"))
},
"decl-stmt" : {
syntax.term("定义"), syntax.ref("decl-list"), syntax.ref("endline")
},
"decl-list" : {
syntax.ref("identifier"), syntax.cond_or(
{syntax.term("="), syntax.ref("single-expr")},
{syntax.term("("), syntax.optional(syntax.ref("argument-list")), syntax.term(")"), syntax.ref("block-body")}
), syntax.optional(syntax.term(","), syntax.ref("decl-list"))
},
"if-stmt" : {
syntax.term("如果"), syntax.term("("), syntax.ref("single-expr"), syntax.term(")"), syntax.ref("block-body"),
syntax.repeat(syntax.term("否则"), syntax.cond_or({syntax.ref("block-body")}, {syntax.ref("if-stmt")}))
},
"while-stmt" : {
syntax.term("循环"), syntax.term("("), syntax.ref("single-expr"), syntax.term(")"), syntax.ref("block-body")
},
"control-stmt" : {
syntax.cond_or({syntax.term("跳出")}, {syntax.term("继续")}), syntax.ref("endline")
},
"return-stmt" : {
syntax.term("返回"), syntax.optional(syntax.nlook(syntax.token("endl")), syntax.ref("expr")), syntax.ref("endline")
},
"expr-stmt" : {
syntax.ref("expr"), syntax.ref("endline")
},
"expr" : {
syntax.ref("single-expr"), syntax.optional(syntax.term(","), syntax.ref("expr"))
},
"single-expr" : {
syntax.ref("logic-or-expr"), syntax.optional(syntax.term("="), syntax.ref("single-expr"))
},
"logic-or-expr" : {
syntax.ref("logic-and-expr"), syntax.optional(syntax.cond_or({syntax.term("||")}, {syntax.term("或")}), syntax.ref("logic-or-expr"))
},
"logic-and-expr" : {
syntax.ref("equal-expr"), syntax.optional(syntax.cond_or({syntax.term("&&")}, {syntax.term("且")}), syntax.ref("logic-and-expr"))
},
"equal-expr" : {
syntax.ref("relat-expr"), syntax.optional(syntax.cond_or({syntax.term("==")}, {syntax.term("!=")}), syntax.ref("equal-expr"))
},
"relat-expr" : {
syntax.ref("add-expr"), syntax.optional(syntax.cond_or({syntax.term(">")}, {syntax.term("<")}, {syntax.term(">=")}, {syntax.term("<=")}), syntax.ref("relat-expr"))
},
"add-expr" : {
syntax.ref("mul-expr"), syntax.optional(syntax.cond_or({syntax.term("+")}, {syntax.term("-")}), syntax.ref("add-expr"))
},
"mul-expr" : {
syntax.ref("unary-expr"), syntax.optional(syntax.nlook(syntax.token("endl")), syntax.cond_or({syntax.term("*")}, {syntax.term("/")}), syntax.ref("mul-expr"))
},
"unary-expr" : {syntax.cond_or(
{syntax.ref("unary-op"), syntax.ref("unary-expr")},
{syntax.ref("prim-expr"), syntax.optional(syntax.nlook(syntax.token("endl")), syntax.ref("postfix-expr"))}
)},
"unary-op" : {syntax.cond_or(
{syntax.term("非")},
{syntax.term("++")},
{syntax.term("--")},
{syntax.term("-")},
{syntax.term("!")}
)},
"postfix-expr" : {
syntax.cond_or({syntax.term("++")}, {syntax.term("--")}), syntax.optional(syntax.ref("postfix-expr"))
},
"prim-expr" : {syntax.cond_or(
{syntax.ref("visit-expr")},
{syntax.ref("constant")}
)},
"visit-expr" : {
syntax.ref("object"), syntax.optional(syntax.term("."), syntax.ref("visit-expr"))
},
"object" : {syntax.cond_or(
{syntax.ref("array"), syntax.optional(syntax.ref("index"))},
{syntax.token("str"), syntax.optional(syntax.ref("index"))},
{syntax.ref("element")}
)},
"element" : {
syntax.cond_or({syntax.ref("identifier")}, {syntax.term("("), syntax.ref("single-expr"), syntax.term(")")}),
syntax.repeat(syntax.nlook(syntax.token("endl")), syntax.cond_or({syntax.ref("fcall")}, {syntax.ref("index")}))
},
"constant" : {syntax.cond_or(
{syntax.token("num")},
{syntax.term("空")},
{syntax.term("真")},
{syntax.term("假")}
)},
"array" : {
syntax.term("{"), syntax.optional(syntax.ref("expr")), syntax.term("}")
},
"fcall" : {
syntax.term("("), syntax.optional(syntax.ref("expr")), syntax.term(")")
},
"index" : {
syntax.term("["), syntax.optional(syntax.ref("add-expr")), syntax.term("]")
}
}.to_hash_map()
@end
整体来说,语法规则混合了脚本语言和 C 风格,为了省事只支持了分支语句、循环语句和函数
再增加一些逻辑上的代码就可以输出 AST 了
var main = new parsergen.generator
main.add_grammar("中文语言", cpl_grammar)
main.stop_on_error = false
main.unicode_cvt = cvt
# main.enable_log = true
main.from_file(context.cmd_args.at(1))
function compress_ast(n)
foreach it in n.nodes
while typeid it == typeid parsergen.syntax_tree && it.nodes.size == 1
it = it.nodes.front
end
if typeid it == typeid parsergen.syntax_tree
compress_ast(it)
else
if it.type == "endl"
it.data = "\\n"
end
end
end
end
if main.ast != null
compress_ast(main.ast)
parsergen.print_ast(main.ast)
end
这里 Compress AST 是将 AST 中冗余的节点删除掉,从而有利于可视化
如果用于分析,是万万不能删掉这些节点的
这是程序输出的 你好.cpl
的 AST
begin
begin -> stmts
stmts -> import-stmt
import-stmt -> "引入"
import-stmt -> module-list
module-list -> "标准"
module-list -> "."
module-list -> module-list
module-list -> "输入输出"
module-list -> "."
module-list -> "*"
import-stmt -> "\n"
stmts -> decl-stmt
decl-stmt -> "定义"
decl-stmt -> decl-list
decl-list -> "拼接问候语"
decl-list -> "("
decl-list -> argument-list
argument-list -> "姓名"
argument-list -> ","
argument-list -> "性别"
decl-list -> ")"
decl-list -> block-body
block-body -> "{"
block-body -> stmts
stmts -> if-stmt
if-stmt -> "如果"
if-stmt -> "("
if-stmt -> equal-expr
equal-expr -> "性别"
equal-expr -> "=="
equal-expr -> ""男""
if-stmt -> ")"
if-stmt -> block-body
block-body -> "{"
block-body -> return-stmt
return-stmt -> "返回"
return-stmt -> add-expr
add-expr -> ""Hello, Mr. ""
add-expr -> "+"
add-expr -> "姓名"
return-stmt -> "\n"
block-body -> "}"
if-stmt -> "否则"
if-stmt -> if-stmt
if-stmt -> "如果"
if-stmt -> "("
if-stmt -> equal-expr
equal-expr -> "性别"
equal-expr -> "=="
equal-expr -> ""女""
if-stmt -> ")"
if-stmt -> block-body
block-body -> "{"
block-body -> return-stmt
return-stmt -> "返回"
return-stmt -> add-expr
add-expr -> ""Hello, Mrs. ""
add-expr -> "+"
add-expr -> "姓名"
return-stmt -> "\n"
block-body -> "}"
if-stmt -> "否则"
if-stmt -> block-body
block-body -> "{"
block-body -> return-stmt
return-stmt -> "返回"
return-stmt -> add-expr
add-expr -> ""Hello, ""
add-expr -> "+"
add-expr -> "姓名"
return-stmt -> "\n"
block-body -> "}"
stmts -> "\n"
block-body -> "}"
decl-stmt -> "\n"
stmts -> decl-stmt
decl-stmt -> "定义"
element -> "输出"
element -> fcall
fcall -> "("
fcall -> element
element -> "拼接问候语"
element -> fcall
fcall -> "("
fcall -> expr
expr -> "姓名"
expr -> ","
expr -> "性别"
fcall -> ")"
fcall -> ")"
expr-stmt -> "\n"
到这里,我们已经完成最简单的部分:解析语法并得到 AST
ParserGen 有两个版本,Release 版能用 CSPKG 直接下载,但仓库里还附带了一个 parsergen_debug.csp
,这个版本性能会差一些,但能通过 main.enable_log = true
输出整个分析过程,用来调试语法非常好用
下一步我们使用 ParserGen 附带的工具生成一个 AST Visitor,并在其基础上编写 CPL 的编译器
为了方便代码复用,我们把上面编写的语法规则独立放在一个 Package 里
unicode/utf8/imports/cpl_grammar.csp · 李登淳/Demo 中文编程语言 - 码云 - 开源中国 (gitee.com)
然后就可以非常方便的使用 ParserGen 的附带工具生成 AST Visitor 了
import cpl_grammar_utf8 as cpl
import visitorgen
var ofs = iostream.ofstream("./cpl_ast_visitor.csp")
(new visitorgen.visitor_generator).run(ofs, cpl.grammar.stx)
生成的 Visitor 比较大,这里放链接:
unicode/utf8/imports/cpl_ast_visitor.csp · 李登淳/Demo 中文编程语言 - 码云 - 开源中国 (gitee.com)
这个 Visitor 功能非常简单,就是把所有的树节点遍历并输出,但因为 ParserGen 为每个 AST 节点都生成了遍历函数,我们可以很方便的在其基础上构建编译器
事实上,ECS 就是用这种方法编写的
ParserGen 的 API 挺实用。
好像没看到优先级的设置?另外,ParserGen 有哪些报错信息呢?比如语法歧义等。
如果语言以此框架开发,使用中会有哪几类反馈信息(如报错等)呢?
不知道你说的是否是运算符优先级?若是的话,运算符优先级是通过上下文无关文法的递推产生的,不需要单独设置
目前 ParserGen 的报错还是有些短板,主要问题是在 LR Parsing 中移进错误不一定是真正的错误,若为了报错单独追踪整个推导链性能开销又比较高
实际上,ParserGen 的报错是以 Error Log 的方式提供,每个 Log 会包含三个成员:
cursor
: 分析器指针,指向词元数据的位置text
: 错误信息,字符串pos
: 位置,二元数组基于 ParserGen 的语言若需要进行二次开发,只需要分析这个 Error Log 即可
这里再补充一下。关于 ParserGen 的语法规则,虽然整体来说更偏向于 LR,但实质上还是改进的 LL,所以无法完全解决左递归的问题,编写语法规则时要注意避免
现在开始改造 AST Visitor
实际上,ParserGen 生成的这个框架是专为代码生成和分析优化的。具体表现来说,首先整个遍历过程是完全遵循语法结构的,因此只需对着语法规则就能对框架进行修改;其次所有的输出全都是通过 target
成员变量来实现的,因此如果需要输出到其他位置,也可以灵活更改,甚至如果需要截取一部分代码生成结果,还可以把 target
临时指向一个 iostream.char_buff
在这里,我们需要处理的主要是引入
语句和花括号,其他部分基本上将中文翻译成 CovScript 关键字就可以了
function visit_module_list(nodes)
var idx = 0
# Recursive Visit identifier
import_stack.front.push_back(nodes[idx++].nodes.front.data)
# Optional
if idx < nodes.size && (typeid nodes[idx] == typeid parsergen.token_type && nodes[idx].data == ".")
# Visit term "."
++idx
# Condition
block
var matched = false
if !matched && (typeid nodes[idx] == typeid parsergen.token_type && nodes[idx].data == "*")
matched = true
# Visit term "*"
++idx
import_stack.front.push_back("*")
end
if !matched && (typeid nodes[idx] == typeid parsergen.syntax_tree && nodes[idx].root == "module-list")
matched = true
# Recursive Visit module-list
this.visit_module_list(nodes[idx++].nodes)
end
if !matched
# Error
return
end
end
end
end
引入语句的处理,我们首先是在 moulde_list
中将原有的输出替换成暂时把列表内容保存起来
然后就是在 import_list
中提取这部分内容,给通配符加上 using
语句
import_stack.push_front(new array)
# Recursive Visit module-list
this.visit_module_list(nodes[idx++].nodes)
link pack = import_stack.front
import_stack.pop_front()
var wildcard = false
if pack.back == "*"
wildcard = true
pack.pop_back()
end
var str = new string
foreach it in pack do str += it + "."
str.cut(1)
if wildcard
target.println("import " + pack.front)
target.print("using " + str)
else
target.print("import " + str)
end
当然,我们还要进行一些错误处理,很明显我们不能允许通配符后使用别名
在进行错误处理之前,我们先给主类增加一个成员函数 error
function error(pos, text)
var err = new parsergen.lex_error
err.text = text
err.pos = pos
parsergen.print_error(file_name, code_buff, {err})
system.exit(0)
end
这个函数的作用非常简单,即构造一个 parsergen.lex_error
然后调用 ParserGen 的内置方法输出这个错误。当然,我们还需要给主类增加 file_name
和 code_buff
这两个属性,其中 code_buff
可以轻松从 ParserGen 中获得(parsergen.generator.code_buff
,在parsergen.generator.code_buff.from_file
方法运行结束后会自动生成)
然后我们就可以进行错误处理了
# Optional
if idx < nodes.size && (typeid nodes[idx] == typeid parsergen.token_type && nodes[idx].data == "别名")
if wildcard
error(nodes[idx].pos, "在通配符后不允许使用别名")
end
# Visit term "别名"
++idx; target.print(" as ")
# Recursive Visit identifier
this.visit_identifier(nodes[idx++].nodes)
end
效果如下:
更多改造过程,大家可以自己去看一下仓库,我就不在帖子里赘述了
请问第一个错是因为少了参数间的逗号吗?
定义 拼接问候语(姓名, 性别)
感觉从语法层面的查错到结合语义的还是挺大一步。