人类是如何理解一段英语的
1.2 经济型程序语言
问1:为什么有这么多程序语言?
如,科学计算→Fortran,商业程序→SQL,系统程序→C/C++
答:不同程序所解决的领域(application domains)是不同的
问2:为什么有新的程序语言出现?
答:对编程语言来说,需要投入的前期编程教育占据了支出的主要部分。
且现在主流语言之间的差距并不大。
创建一个新编程语言很容易。当新语言带来的生产力大于培训成本时,选择创建新语言。
编程语言尝试填补空缺
问3:好的程序语言是什么?
将源码分解为
名词 | 词义 |
---|---|
Identifier | 字符串或由字符开始的数字串 |
Integer | 非空数字组成的字符串 |
Keyword | “else” or “if” or “begin” or … |
Witespace | 非空字符串,这个字符串由空格、换行符、制表符构成 |
Operator | 运算符 |
FORTRAN规则:空格是无影响的(“var1” == “va r1”)
向前看规则
这说明了词法分析的任务:
(PL/1是一个IBM设计的编程语言)
特点:
在<>和>>、<<之间的问题
如:Foo
Lexical structure = token classes
正规表达式由{单个字符,空字符}构成
空字符用" ε \varepsilon ε"表示
操作名 | 方法示意 |
---|---|
Union | |
Concatenation | |
Iteration |
定义: Σ \Sigma Σ 是一个正规表达式中各正规式的组成元素集合
上图中,都是在给定的 Σ \Sigma Σ(即正规表达式构成元素)组成的语法 R R R(grammer)
举例说明:
定义:设 Σ \Sigma Σ是一个字符集。一个在 Σ \Sigma Σ上产生的语法是,从 Σ \Sigma Σ上产生的字符串集。(主要部分:语法是字符串集,其他定语自己看懂)
就像英文字母表是英语字符构成的,而英文语言是由英文句子构成的
Alphabet = ASCII
Language = C programs
Meaning Function L将语法(Syntax)映射到语义(Semantics)上去
(上图exp为expression缩写)
使用Meaning Function的意义:
上图展示了,不同语法通过Meaning Function可能映射到相同语义上去。这有助于我们将相同功能、不同语法写成的程序,用高效的程序代替低效的程序。
并且,语法不会映射到多个语义上去。(无二义性)
注: A + = A A ∗ = ⋃ i > 0 A A^{+}=AA^{*}=\bigcup\limits_{i>0}A A+=AA∗=i>0⋃A
例子:识别email地址的正规表达式
[email protected]
正规式表达: l e t t e r + ′ @ ′ ∣ l e t t e r + ′ . ′ + l e t t e r + ′ . ′ + l e t t e r letter+'@'|letter+'.'+letter+'.'+letter letter+′@′∣letter+′.′+letter+′.′+letter
PASCAL语言中的正规表达式例子:
(这里的+表示连接,在往上的文章中有用(1+0)这样表示的正规式中的+表示或,因为课件中用的是+表示或,十分有歧义,并且龙书中用的也是|。下面我尽量使用|,用以区分+,请注意)如下图所示。。。这老师用的跟我校用的那本清华的编译原理讲的也不一样,真讨厌
词法检测过程:
解决方法:将二义文法改写成非二义文法
改写成:(消除左递归)
处理方式:一直接着吃,直到找到了一个正确的role
寻找同步token
z.B.
可以使用一个特殊的终结符"error"来描述有多少输入需要略过
现在并不常用这种
编译器剩下一部分需要一个能代替程序的结构。
抽象语法树:近似语法树但忽略一些细节、简单地描述成AST(Abstract Syntax Tree)
改写成这样
语法树构建方法:自顶向下,从左到右
先序遍历地生成Terminals
z.B.
生成过程,注意有回归
accept
对于语法E:{
E → T | T+E
T → int | int*T | (E)
}
Token有:INT, OPEN, CLOSE, PLUS, TIMES
global next指向下一输入的token
// 返回当前token是否符合选择的token,并将指针移到下一个输入上去
bool term(Token tok){
return *next++==tok;
}
定义一系列的代表产生式的函数S " bool Sn() { … } ",只有在相同时才返回为真。
定义一个包含所有产生式的函数S " bool S() { … } "
// z.B. 对于产生式 "E→T"有函数
bool E1() { return T(); }
// z.B. 2: "E→T+E":
bool E2() { return T() && term(PLUS) && E(); }
bool E(){
Token *save = next; // 在尝试任何匹配前,先把**接下来**要从哪去token的位置记录下来。
return (next = save, E1())
|| (next = save, E2());
}
// 对于 T→int
bool T1() { return term(INT); }
// 对于 T→int * T
bool T2() { return term(INT) && term(TIMES) && T(); }
// 对于 T→(E)
bool T3() { return term(OPEN) && E() && term(CLOSE); }
// T → int | int*T | (E)
bool T(){
TOKEN *save = next;
return (next = save, T1()) || (next = save, T2()) || (next = save, T3());
}
问题: 这对于输入"int * int "会reject,因为第一次使用的是E→int进行推导,如果使用E→int*T就不会出错。所以有问题”如果一个产生非终结符的产生式被使用了,则不再有回溯回来检测此时使用别的产生式的可能“
通常上,自顶向下递归分析需要支持全回溯,才可以进行完整的语法检测。
虽然正常情况下不使用这种算法,但这算法容易手工实现。在一个非终结符只能推导出一个终结符情况下是可用的。消除例子中的公共前缀left factoring就可以用了:)。
举个例子:
// S → Sa
bool S1() { return S() && term(a); }
bool S() {return S1();}
这里的S会产生无穷的递归。一个左递归语法要求没有这样的S,这样的非终结符S使得S可以加步推导出S α \alpha α
考虑这样的语法:
S → S α ∣ β S→S\alpha|\beta S→Sα∣β
它会产生这样的语言:
这导致最后生成了 β \beta β,它从右向左依次生成,因此可以右递归地生成。
上式也可写成右递归式:(从左向右生成)
更多的消除左递归式的例子: