我自己编写了 SLIC(用于实现编译器的语言系统)。然后手工编译成程序集。 SLIC 有很多功能,因为它是五种子语言的单一编译器:
- SYNTAX 解析器编程语言 PPL
- GENERATOR LISP 2 基于树爬行伪代码生成语言
- ISO In Sequence、PSEUDO 代码、优化语言
- PSEUDO 宏类似于汇编代码生成语言。
- MACHOP 汇编机器指令定义语言。
SLIC 的灵感来自 CWIC(用于编写和实现编译器的编译器)。与大多数编译器开发包不同,SLIC 和 CWIC 使用专门的、特定于领域的语言来解决代码生成问题。 SLIC 扩展了 CWIC 代码生成,添加了 ISO、PSEUDO 和 MACHOP 子语言,将目标机器的细节从树爬行生成器语言中分离出来。
LISP 2 树和列表
基于 LISP 2 的生成器语言的动态内存管理系统是一个关键组件。列表用方括号括起来的语言表示,其组成部分用逗号分隔,即三元素 [a,b,c] 列表。
树木:
ADD
/ \
MPY 3
/ \
5 x
由第一个条目是节点对象的列表表示:
[ADD,[MPY,5,x],3]
树通常以节点分开的方式显示在分支之前:
ADD[MPY[5,x],3]
使用基于 LISP 2 的生成器函数进行解析
生成器函数是一组命名的 (unparse)=>action> 对 ...
<NAME>(<unparse>)=><action>;
(<unparse>)=><action>;
...
(<unparse>)=><action>;
Unparse 表达式是匹配树模式和/或对象类型的测试,将它们分开并将这些部分分配给局部变量以通过其程序操作进行处理。有点像采用不同参数类型的重载函数。除了 ()=> ... 测试按编码顺序进行尝试。第一个成功的 unparse 执行其相应的操作。 unparse 表达式是反汇编测试。 ADD[x,y] 匹配两个分支的 ADD 树,将其分支分配给局部变量 x 和 y。动作可能是一个简单的表达式或一个 .BEGIN ... .END 有界代码块。我今天会使用 c style { ... } 块。树匹配、[]、未解析规则可能会调用生成器,将返回的结果传递给操作:
expr_gen(ADD[expr_gen(x),expr_gen(y)])=> x+y;
具体来说,上面的 expr_gen unparse 匹配一个有两个分支的 ADD 树。在测试模式中,将使用该分支调用放置在树分支中的单个参数生成器。它的参数列表虽然是分配返回对象的局部变量。上面的unparse指定了两个分支是ADD树反汇编,递归压每个分支到expr_gen。左分支返回放入局部变量 x。同样,右分支通过返回对象 y 传递给 expr_gen。以上可能是数字表达式求值器的一部分。上面有称为向量的快捷功能,而不是节点字符串,节点向量可以与相应动作的向量一起使用:
expr_gen(#node[expr_gen(x),expr_gen(y)])=> #action;
node: ADD, SUB, MPY, DIV;
action: x+y, x-y, x*y, x/y;
(NUMBER(x))=> x;
(SYMBOL(x))=> val:(x);
上述更完整的表达式求值器将 expr_gen 左分支的返回值分配给 x,将右分支的返回值分配给 y。返回在 x 和 y 上执行的相应动作向量。最后一个 unparse=>action 对匹配数字和符号对象。
符号和符号属性
符号可能具有命名属性。 val:(x) 访问 x 中包含的符号对象的 val 属性。广义符号表堆栈是 SLIC 的一部分。 SYMBOL 表可以被推送和弹出,为函数提供本地符号。新创建的符号在顶部符号表中进行编目。符号查找首先从顶部表向后向下搜索符号表堆栈。
生成机器无关代码
SLIC 的生成器语言生成 PSEUDO 指令对象,并将它们附加到节代码列表中。 .FLUSH 导致其 PSEUDO 代码列表运行,从列表中删除每个 PSEUDO 指令并调用它。执行后,一个 PSEUDO 对象的内存被释放。 PSEUDO 和 GENERATOR 动作的程序主体除了输出之外基本上是相同的语言。 PSEUDO 旨在充当提供与机器无关的代码序列化的汇编宏。它们提供了从树爬行生成器语言中分离特定目标机器的方法。 PSEUDO 调用 MACHOP 函数来输出机器代码。 MACHOP 用于定义汇编伪操作(如 dc、定义常量等)和机器指令或使用向量入口的类似格式指令系列。它们只是将参数转换为组成指令的位域序列。 MACHOP 调用旨在看起来像汇编,并在汇编显示在编译列表中时提供字段的打印格式。在示例代码中,我使用了可以轻松添加但不是原始语言的 c 样式注释。 MACHOP 正在将代码生成到可寻址的内存中。 SLIC 链接器处理编译器的输出。使用向量入口的 DEC-10 用户模式指令的 MACHOP:
.MACHOP #opnm register,@indirect offset (index): // Instruction's parameters.
.MORG 36, O(18): $/36; // Align to 36 bit boundary print format: 18 bit octal $/36
O(9): #opcd; // Op code 9 bit octal print out
(4): register; // 4 bit register field appended print
(1): indirect; // 1 bit appended print
(4): index; // 4 bit index register appended print
O(18): if (#opcd&&3==1) offset // immediate mode use value else
else offset/36; // memory address divide by 36
// to get word address.
// Vectored entry opcode table:
#opnm := MOVE, MOVEI, MOVEM, MOVES, MOVS, MOVSI, MOVSM, MOVSS,
MOVN, MOVNI, MOVNM, MOVNS, MOVM, MOVMI, MOVMM, MOVMS,
IMUL, IMULI, IMULM, IMULB, MUL, MULI, MULM, MULB,
...
TDO, TSO, TDOE, TSOE, TDOA, TSOA, TDON, TSON;
// corresponding opcode value:
#opcd := 0O200, 0O201, 0O202, 0O203, 0O204, 0O205, 0O206, 0O207,
0O210, 0O211, 0O212, 0O213, 0O214, 0O215, 0O216, 0O217,
0O220, 0O221, 0O222, 0O223, 0O224, 0O225, 0O226, 0O227,
...
0O670, 0O671, 0O672, 0O673, 0O674, 0O675, 0O676, 0O677;
.MORG 36, O(18): $/36;将位置与 36 位边界对齐,以八进制打印 18 位的位置 $/36 字地址。将 9 位 opcd、4 位寄存器、间接位和 4 位索引寄存器组合起来打印,就像单个 18 位字段一样。 18 位地址/36 或立即值以八进制输出和打印。使用 r1 = 1 和 r2=2 打印出 MOVEI 示例:
400020 201082 000005 MOVEI r1,5(r2)
使用编译器汇编选项,您可以在编译列表中获得生成的汇编代码。
将其链接在一起
SLIC 链接器作为处理链接和符号解析的库提供。尽管必须为目标机器编写目标特定的输出加载文件格式并与链接器库链接。
生成器语言能够将树写入文件并读取它们,从而实现多通道编译器。
代码生成和起源的简短总结
我首先检查了代码生成,以确保 SLIC 是真正的编译器编译器。 SLIC 的灵感来自于 1960 年代后期由 Systems Development Corporation 开发的 CWIC(编写和实现编译器的编译器)。 CWIC 只有 SYNTAX 和 GENERATOR 语言从 GENERATOR 语言中产生数字字节码。字节代码被放置或植入(CWIC 文档中使用的术语)到与命名部分相关的内存缓冲区中,并由 .FLUSH 语句写出。 ACM 档案中提供了有关 CWIC 的 ACM 论文。
成功实现一种主要的编程语言
在 1970 年代后期,SLIC 被用于编写 COBOL 交叉编译器。主要由一个程序员在大约 3 个月内完成。我根据需要与程序员一起工作。另一位程序员为目标 TI-990 mini-COMPUTER 编写了运行时库和 MACHOP。该 COBOL 编译器每秒编译的行数比用汇编编写的 DEC-10 原生 COBOL 编译器要多得多。
更多关于编译器然后通常谈论的内容
从头开始编写编译器的很大一部分是运行时库。你需要一个符号表。你需要输入和输出。动态内存管理等。为编译器编写运行时库比编写编译器更容易。但是对于 SLIC,运行时库对于在 SLIC 中开发的所有编译器都是通用的。请注意,有两个运行时库。一种用于语言的(例如 COBOL)目标机器。另一个是编译器编译器运行时库。
我想我已经确定这些不是解析器生成器。所以现在对后端有一点了解,我可以解释解析器编程语言了。
解析器编程语言
解析器是使用以简单方程形式编写的公式编写的。
<name> <formula type operator> <expression> ;
最低级别的语言元素是字符。标记由语言字符的子集形成。字符类用于命名和定义这些字符子集。定义运算符的字符类是冒号 (:) 字符。作为该类成员的字符在定义的右侧编码。可打印字符包含在素数单 ' 字符串中。非打印字符和特殊字符可以用它们的数字序号表示。类成员由替代 | 分隔操作员。类公式以分号结尾。字符类可能包括先前定义的类:
/* Character Class Formula class_mask */
bin: '0'|'1'; // 0b00000010
oct: bin|'2'|'3'|'4'|'5'|'6'|'7'; // 0b00000110
dgt: oct|'8'|'9'; // 0b00001110
hex: dgt|'A'|'B'|'C'|'D'|'E'|'F'|'a'|'b'|'c'|'d'|'e'|'f'; // 0b00011110
upr: 'A'|'B'|'C'|'D'|'E'|'F'|'G'|'H'|'I'|'J'|'K'|'L'|'M'|
'N'|'O'|'P'|'Q'|'R'|'S'|'T'|'U'|'V'|'W'|'X'|'Y'|'Z'; // 0b00100000
lwr: 'a'|'b'|'c'|'d'|'e'|'f'|'g'|'h'|'i'|'j'|'k'|'l'|'m'|
'n'|'o'|'p'|'q'|'r'|'s'|'t'|'u'|'v'|'w'|'x'|'y'|'z'; // 0b01000000
alpha: upr|lwr; // 0b01100000
alphanum: alpha|dgt; // 0b01101110
skip_class 0b00000001 是预定义的,但可能会过度定义skip_class。
总而言之:字符类是一个替代列表,只能是字符常量、字符序数或先前定义的字符类。当我实现字符类时:类公式被分配了一个类位掩码。 (如上面的 cmets 所示)任何具有任何字符文字或序数的类公式都会导致分配一个类位。掩码是通过将包含的类的类掩码与分配的位(如果有)一起进行或运算而制成的。从字符类创建一个类表。由字符序号索引的条目包含指示字符的类成员资格的位。类测试是内联完成的。一个带有 eax 中字符序数的 IA-86 代码示例说明了类测试:
test byte ptr [eax+_classmap],dgt
后跟一个:
jne <success>
或
je <failure>
之所以使用 IA-86 指令代码示例,是因为我认为 IA-86 指令在今天更为广为人知。评估到其类掩码的类名与由字符序数(在 eax 中)索引的类表进行非破坏性与运算。非零结果表示类成员资格。 (除了包含字符的 al(EAX 的低 8 位)之外,EAX 为零)。
这些旧编译器中的标记有点不同。关键词没有被解释为记号。它们只是通过解析器语言中的引用字符串常量进行匹配。通常不保留带引号的字符串。可以使用修饰符。 A + 保持字符串匹配。 (即 +'-' 匹配 - 成功时保留字符的字符) , 操作(即,'E')将字符串插入到标记中。空格由令牌公式处理,跳过前导 SKIP_CLASS 字符,直到进行第一次匹配。请注意,明确的 skip_class 字符匹配将停止跳过,从而允许令牌以 skip_class 字符开头。字符串标记公式会跳过与单引号相当dd 字符或双引号字符串匹配的前导skip_class 字符。有趣的是在 " 引用的字符串中匹配 " 字符:
string .. (''' .ANY ''' | '"' $(-"""" .ANY | """""","""") '"') MAKSTR[];
第一个替代匹配任何单引号引号字符。正确的选择匹配一个双引号引用的字符串,该字符串可能包含使用两个 " 字符一起表示单个 " 字符的双引号字符。此公式定义了在其自己的定义中使用的字符串。右内选项 '"' $(-"""" .ANY | """""","""") '"' 匹配双引号引用的字符串。我们可以使用单个 ' 引号字符来匹配双引号 " 字符。但是在双引号 " 字符串中,如果我们希望使用 " 字符,我们必须使用两个 " 字符来获得一个。例如,在左内替代匹配除引号之外的任何字符:
-"""" .ANY
使用负向窥视 -"""" 表示成功时(不匹配 " 字符)然后匹配 .ANY 字符(不能是 " 字符,因为 -"""" 消除了这种可能性)。正确的选择是采用 -"""" 匹配 " 字符并且失败是正确的选择:
"""""",""""
尝试匹配两个 " 字符,将它们替换为单个双 " 使用 ,"""" 以插入单个 " 字符。匹配结束字符串引号字符失败的两个内部替代方案,并调用 MAKSTR[] 以创建字符串对象. $ 序列,成功时循环,运算符用于匹配序列。标记公式跳过前导跳过类字符(空白)。一旦进行第一次匹配,skip_class 跳过被禁用。我们可以使用 [ 调用以其他语言编写的函数]. MAKSTR[]、MAKBIN[]、MAKOCT[]、MAKHEX[]、MAKFLOAT[] 和 MAKINT[] 是提供的库函数,可将匹配的标记字符串转换为类型化对象。下面的数字公式说明了一个相当复杂的标记识别:
number .. "0B" bin $bin MAKBIN[] // binary integer
|"0O" oct $oct MAKOCT[] // octal integer
|("0H"|"0X") hex $hex MAKHEX[] // hexadecimal integer
// look for decimal number determining if integer or floating point.
| ('+'|+'-'|--) // only - matters
dgt $dgt // integer part
( +'.' $dgt // fractional part?
((+'E'|'e','E') // exponent part
('+'|+'-'|--) // Only negative matters
dgt(dgt(dgt|--)|--)|--) // 1 2 or 3 digit exponent
MAKFLOAT[] ) // floating point
MAKINT[]; // decimal integer
上述数字标记公式可识别整数和浮点数。 -- 替代方案总是成功的。数字对象可用于计算。公式成功后,令牌对象被推入解析堆栈。 (+'E'|'e','E') 中的指数前导很有趣。我们希望 MAKEFLOAT[] 总是有一个大写的 E。但是我们允许使用小写的 'e' 替换它,'E'。
您可能已经注意到字符类和标记公式的一致性。解析公式继续添加回溯选项和树构造运算符。回溯和非回溯替代运算符不得在表达式级别内混合。你可能没有(a | b \ c)混合非回溯| withe \回溯替代。 (a\b\c)、(a|b|c) 和 ((a|b)\c) 是有效的。 \ 回溯替代在尝试其左侧替代之前保存解析状态,并且在失败时在尝试正确替代之前恢复解析状态。在一系列备选方案中,第一个成功的备选方案满足该组。没有尝试进一步的替代方案。分解和分组提供了连续推进的解析。回溯替代在尝试其左替代之前创建解析的保存状态。当解析可能进行部分匹配然后失败时,需要回溯:
(a b | c d)\ e
在上面,如果 a 返回失败,则尝试替代 c d。如果然后 c 返回失败,则将尝试回溯替代方案。如果 a 成功而 b 失败,则将回溯并尝试解析。同样,a 失败 c 成功且 b 失败,则回溯解析并采用替代 e。回溯不限于公式内。如果任何解析公式在任何时候进行部分匹配然后失败,则解析将重置为顶部回溯并采用其替代方案。如果代码已输出,则可能会发生编译失败,因为回溯已创建。在开始编译之前设置回溯。返回失败或回溯到它是编译器失败。回溯是堆叠的。我们可以使用负数和正数? peek/look ahead 运算符在不推进解析的情况下进行测试。字符串测试是一个预览,只需要保存和重置输入状态。向前看将是一个解析表达式,它在失败之前进行部分匹配。使用回溯实现前瞻。
解析器语言既不是 LL 也不是 LR 解析器。但是一种用于编写递归体面解析器的编程语言,您可以在其中编程树构造:
:<node name> creates a node object and pushes it onto the node stack.
.. Token formula create token objects and push them onto
the parse stack.
!<number> pops the top node object and top <number> of parstack
entries into a list representation of the tree. The
tree then pushed onto the parse stack.
+[ ... ]+ creates a list of the parse stack entries created
between them:
'(' +[argument $(',' argument]+ ')'
could parse an argument list. into a list.
一个常用的解析例子是算术表达式:
Exp = Term $(('+':ADD|'-':SUB) Term!2);
Term = Factor $(('*':MPY|'/':DIV) Factor!2);
Factor = ( number
| id ( '(' +[Exp $(',' Exp)]+ ')' :FUN!2
| --)
| '(' Exp ')" )
(^' Factor:XPO!2 |--);
Exp 和 Term 使用循环创建左手树。使用右递归的因子创建右手树:
d^(x+5)^3-a+b*c => ADD[SUB[EXP[EXP[d,ADD[x,5]],3],a],MPY[b,c]]
ADD
/ \
SUB MPY
/ \ / \
EXP a b c
/ \
d EXP
/ \
ADD 3
/ \
x 5
这里有一点 cc 编译器,一个带有 c 风格 cmets 的 SLIC 的更新版本。函数类型(语法、标记、字符类、生成器、PSEUDO 或 MACHOP)由其 id 后面的初始语法确定。
使用这些自上而下的解析器,您可以从程序定义公式开始:
program = $((declaration // A program is a sequence of
// declarations terminated by
|.EOF .STOP) // End Of File finish & stop compile
\ // Backtrack: .EOF failed or
// declaration long-failed.
(ERRORX["?Error?"] // report unknown error
// flagging furthest parse point.
$(-';' (.ANY // find a ';'. skiping .ANY
| .STOP)) // character: .ANY fails on end of file
// so .STOP ends the compile.
// (-';') failing breaks loop.
';')); // Match ';' and continue
declaration = "#" directive // Compiler directive.
| comment // skips comment text
| global DECLAR[*1] // Global linkage
|(id // functions starting with an id:
( formula PARSER[*1] // Parsing formula
| sequencer GENERATOR[*1] // Code generator
| optimizer ISO[*1] // Optimizer
| pseudo_op PRODUCTION[*1] // Pseudo instruction
| emitor_op MACHOP[*1] // Machine instruction
) // All the above start with an identifier
\ (ERRORX["Syntax error."]
garbol); // skip over error.
// 请注意在创建树时如何将 id 分解并随后合并。
formula = ("==" syntax :BCKTRAK // backtrack grammar formula
|'=' syntax :SYNTAX // grammar formula.
|':' chclass :CLASS // character class define
|".." token :TOKEN // token formula
)';' !2 // Combine node name with id
// parsed in calling declaration
// formula and tree produced
// by the called syntax, token
// or character class formula.
$(-(.NL |"/*") (.ANY|.STOP)); Comment ; to line separator?
chclass = +[ letter $('|' letter) ]+;// a simple list of character codes
// except
letter = char | number | id; // when including another class
syntax = seq ('|' alt1|'\' alt2 |--);
alt1 = seq:ALT!2 ('|' alt1|--); Non-backtrack alternative sequence.
alt2 = seq:BKTK!2 ('\' alt2|--); backtrack alternative sequence
seq = +[oper $oper]+;
oper = test | action | '(' syntax ')' | comment;
test = string | id ('[' (arg_list| ,NILL) ']':GENCALL!2|.EMPTY);
action = ':' id:NODE!1
| '!' number:MAKTREE!1
| "+[" seq "]+" :MAKLST!1;
// C style comments
comment = "//" $(-.NL .ANY)
| "/*" $(-"*/" .ANY) "*/";
值得注意的是解析器语言如何处理注释和错误恢复。
我想我已经回答了这个问题。已经编写了 SLIC 继任者的很大一部分,这里是 cc 语言本身。目前还没有编译器。但我可以手动将其编译成汇编代码、裸 asm c 或 c++ 函数。