|
[顶层] |
[内容] |
[索引] |
[ ? ] |
这个手册是针对GNU Bison (版本2.0,22 December 2004), GNU分析器生成器.
Copyright © 1988, 1989, 1990, 1991, 1992, 1993, 1995, 1998, 1999, 2000, 2001, 2002, 2003, 2004 Free Software Foundation, Inc.
Chinese(zh_CN) translation:Xiao Wang
Permission is granted to copy, distribute and/or modify this document under the terms of the GNU Free Documentation License, Version 1.1 or any later version published by the Free Software Foundation; with no Invariant Sections, with the Front-Cover texts being "A GNU Manual," and with the Back-Cover Texts as in (a) below. A copy of the license is included in the section entitled "GNU Free Documentation License."
(a) The FSF's Back-Cover Text is: "You have freedom to copy and modify this GNU Manual, like GNU software. Copies published by the Free Software Foundation raise funds for GNU development."
|
|
||
|
|
||
|
|
GNU General Public License 说明了你如何使用和共享Bison |
|
教学章节:
|
||
|
|
理解Bison的基本概念. |
|
|
|
三个详细解释的使用Binson的例子. |
|
参考章节:
|
||
|
|
编写Bison声明和规则. |
|
|
|
分析器函数 |
|
|
|
Bison分析器运行时如何工作. |
|
|
|
编写错误恢复规则. |
|
|
|
如果你的语言的语法过于凌乱以至于Bison不能直接处理,你该怎么做. |
|
|
|
理解或调试Bison分析器. |
|
|
|
如何运行Bison(来生成分析源文件). |
|
|
|
解释所有Bison语言的关键字. |
|
|
|
解释基本概念. |
|
|
|
常见问题 |
|
|
|
复制这个手册的许可 |
|
|
|
文本交叉索引 |
|
--- 详细节点列表 --- Bison的基本概念-The Concepts of Bison |
||
|
|
从数学的概念来介绍语言和上下文无关文法. |
|
|
|
怎样用Bison的语法表示各种文法. |
|
|
|
每个记号或者语法组可有一个语义值(例如:整数的数值,标识符的名称等等) |
|
|
|
每个规则可有一个包含C代码的动作 |
|
|
|
为普通的上下文无关文法编写分析器 |
|
|
|
追踪位置 |
|
|
|
Bison的输入和输出是什么,怎样使用Bison的输出 |
|
|
|
编写和运行Bison语法分析程序的流程. |
|
|
|
Bison语法文件的整体布局 |
|
编写GLR分析器-Writing GLR Parsers |
||
|
|
||
|
|
使用GLR分析器解决歧义 |
|
|
1.5.3 编译GLR分析器时需要考虑的问题-Considerations when Compiling GLR Parsers |
|
GLR分析器需要一个现代的C编译器 |
实例-Examples
|
||
|
|
逆波兰记号计算器,我们的第一个例子,并不带有操作符优先级 |
|
|
|
中缀代数符号计算器,简单地介绍操作符优先级. |
|
|
|
在出现语法错误后继续分析 |
|
|
|
展示@n和@$的用法 |
|
|
|
带有存储和触发功能的计算器.它使用了多种数据类型来表示语义值. |
|
|
|
一些改进多功能计算器的方案 |
|
逆波兰记号计算器-Reverse Polish Notation Calculator
|
||
|
|
rpclac的Prologue(声明)部分. |
|
|
|
带注释的rpcalc语法规则 |
|
|
|
词法分析器 |
|
|
|
控制函数 |
|
|
|
错误报告的规则 |
|
|
|
使用Bison生成分析器 |
|
|
|
使用C编译器编译得到最终结果 |
|
|
||
|
|
||
|
|
||
|
|
||
位置追踪计算器:
|
||
|
|
ltcalc的Bison和C语言的声明 |
|
|
|
详细解释ltcalc的语法规则 |
|
|
|
词法分析器 |
|
多功能计算器:
|
||
|
|
多功能计算器的Bison声明 |
|
|
|
计算器的语法声明 |
|
|
|
符号表管理规则 |
|
Bison语法文件-Bison Grammar Files |
||
|
|
语法文件的整体布局 |
|
|
|
终结符与非终结符 |
|
|
|
如何编写语法规则 |
|
|
|
编写递归规则 |
|
|
|
语义值和动作 |
|
|
|
位置和动作 |
|
|
|
所有种类的Bison声明在这里讨论 |
|
|
|
将多个Bison分析器放在一个程序中 |
|
Bison语法提纲-Outline of a Bison Grammar |
||
|
|
Prologue部分的语法和使用 |
|
|
|
Bison declarations部分的语法和使用 |
|
|
|
Grammar Rules部分的语法和使用 |
|
|
|
Epilogue部分的语法和使用 |
|
定义语言的语义-Defining Language Semantics
|
||
|
|
为所有的语义值指定一个类型. |
|
|
|
指定多种可选的数据类型. |
|
|
|
动作是一个语法规则的语义定义. |
|
|
|
为动作指定一个要操作的数据类型. |
|
|
|
多数动作在规则之后, 这一节讲述什么时候以及为什么要使用规则中间动作的特例. |
|
追踪位置-Tracking Locations
|
||
|
|
描述位置的数据类型. |
|
|
|
在动作中使用位置. |
|
|
|
定义了一个计算位置的通用方法. |
|
Bison声明-Bison Declarations |
||
|
|
声明终结符 |
|
|
|
声明终结符的优先级和结合性 |
|
|
|
声明一组语义值类型 |
|
|
|
声明非终结语义值的类型 |
|
|
|
在分析开始前执行的代码 |
|
|
|
声明如何释放符号 |
|
|
|
消除分析冲突时的警告 |
|
|
|
指明开始符号 |
|
|
|
请求一个可重入的分析器 |
|
|
|
一个所有Bison声明的总结 |
|
分析器C语言接口-Parser C-Language Interface |
||
|
|
如何调用 |
|
|
|
你必提供一个读入记号的函数 |
|
|
|
你必须提供一个函数 |
|
|
|
在动作中使用的特殊特征. |
|
词法分析器函数
|
||
|
|
|
|
|
|
|
|
|
|
如果动作需要, |
|
|
|
调用惯例如何区分一个纯分析器 (参阅一个纯(可重入)分析器-A Pure (Reentrant) Parser一章). |
|
Bison分析器算法-The Bison Parser Algorithm |
||
|
|
当分析器决定做什么的时候它查看的一个记号. |
|
|
|
冲突:移进和归约均有效. |
|
|
|
由于解决冲突的操作符优先级. |
|
|
|
当一个操作符的优先级依赖上下文. |
|
|
|
分析器是一个带有栈的有限状态机. |
|
|
|
在同意情况下可以应用两个规则. |
|
|
|
看起来不平等的归约/归约冲突. |
|
|
|
分析arbitrary上下文无关文法. |
|
|
|
当栈溢出时发生的事情以及如何避免它. |
|
操作符优先级-Operator Precedence
|
||
|
|
一个展示为什么需要优先级的例子 |
|
|
|
在Bison的语法中如何指定优先级 |
|
|
|
这些特性在前面的例子中是怎样使用的 |
|
|
|
它们如何工作 |
|
处理上下文依赖-Handling Context Dependencies
|
||
|
|
对记号的分析可能依赖于语义上下文. |
|
|
|
对记号的分析可能依赖上下文. |
|
|
|
词法关联含有如何编写错误恢复规则的暗示. |
|
调试你的分析器-Debugging Your Parser
|
||
|
|
理解你的分析器的结构 |
|
|
|
跟踪你的分析器的执行 |
|
调用Bison-Invoking Bison
|
||
|
|
按简写选项的字母顺序详细描述所有选项 |
|
|
|
按字母顺序列出长选项 |
|
|
|
与Yacc兼容的 |
|
常见问题-Frequently Asked Questions
|
||
|
|
突破栈限制 |
|
|
|
|
|
|
|
|
|
|
|
使用C++编译器编译分析器 |
|
|
|
在计算器中控制流 |
|
复制这个文档-Copying This Manual
|
||
|
|
复制这个手册的许可. |
|
|
[ < ] |
[ > ] |
|
[ << ] |
[ 上层 ] |
[ >> ] |
|
|
|
|
[顶层] |
[内容] |
[索引] |
[ ? ] |
Bison是一种通用目的的分析器生成器. 它将LALR(1)上下文无关文法的描述转化成分析该文法的C程序. 一旦你精通Bison, 你可以用它生成从简单的桌面计算器到复杂的程序设计语言等等许多语言的分析器.
Bison向上兼容Yacc:所有书写正确的Yacc语法都应该可以不加更改地与Bison一起工作. 熟悉Yacc的人能毫不费力地使用Bison. 你应该熟练地掌握C程序设计语言,这样你才能使用Bison和理解这个手册.
我们会在这个教程的最初几章解释BIson的基本概念,并且展示三个详细解释的例子, 这些例子将在教程的最后被构建. 如果你对Bison或者Yacc一无所知, 你应该首先阅读这些章节. 接下来的参阅章节详细地阐述了Bison的各个方面.
Bison主要由Rovert Corbett编写.Richard Stallman使它与Yacc兼容. Carnegie Mellon大学的Wilfred Hansen为Bison添加了 多字符字符串文字(multi-character string literals)和其它一些特性.
这个版本的手则主要与Bison2.0相一致.
|
[ < ] |
[ > ] |
|
[ << ] |
[ 上层 ] |
[ >> ] |
|
|
|
|
[顶层] |
[内容] |
[索引] |
[ ? ] |
为了允许非自由程序(nonfree
programs)使用Bison生成的LALR分析器C代码,
我们在Bison版本1.24的时已经修改了yyparse的发布条款(distribution
terms for yyparse). 在这之前,这些分析器只能用于自由软件(free
software)的程序.
其它GNU编程工具,例如GNU C编译器从未有过类似的要求. 它们总能用于非自由软件的发布. Bison与它们不同的原因并不是出于特殊策略的决定, 而是由于应用通用许可证(General Public License)到所有的Bison源代码造成的结果.
Bison工具的输出-也就是Bison分析器文件(the Bison parser file)包括大小不固定的Bison代码片段. 这些代码片段来源于yyparse函数.(你的语法的动作被Bison插入到这个函数的某个位置, 但是函数的其余部分并未更改).当我们应用GPL条款到yyparse的代码的时候, 它产生的效果就是Bison的输出只能到自由软件.
我们并未更改条款使出于对那些希望是软件私有化的人的同情. 所有的软件都应该是自由软件.但是我们的结论是: 使Bison只能用于自由软件,这对鼓励人们使其它软件变为自由软件作用甚微. 所以我们决定将使用Bison的条件与使用其它GNU工具的条件相一致. (注:即和GCC一样可以用于非自由软件).
这个特例仅仅在Bison生成LALR(1)分析器的C代码时适用. 否则,GPL条款仍然正常的执行. 你可以通过查看代码,看其是否由这样的说明"As a special execption, when this file is copied by Bison into a Bison output file, you may use that output file without restriction."来分辩这个特例是否适用于你的`.c'输出文件.
|
[ < ] |
[ > ] |
|
[ << ] |
[ 上层 ] |
[ >> ] |
|
|
|
|
[顶层] |
[内容] |
[索引] |
[ ? ] |
Version 2, June 1991
|
|
Copyright © 1989, 1991 Free Software Foundation, Inc. 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. |
|
[ < ] |
[ > ] |
|
[ << ] |
[ 上层 ] |
[ >> ] |
|
|
|
|
[顶层] |
[内容] |
[索引] |
[ ? ] |
The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Library General Public License instead.) You can apply it to your programs, too.
When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights.
We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations.
Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and modification follow.
This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does.
You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program.
You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee.
You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions:
You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change.
You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License.
If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program.
In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License.
You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following:
Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or,
Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or,
Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable.
If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code.
You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance.
You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it.
Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License.
If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances.
It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice.
This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License.
If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License.
The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation.
If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally.
BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
|
[ < ] |
[ > ] |
|
[ << ] |
[ 上层 ] |
[ >> ] |
|
|
|
|
[顶层] |
[内容] |
[索引] |
[ ? ] |
If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found.
|
|
one line to give the program's name and a brief idea of what it does. Copyright (C) yyyy name of author This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. |
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this when it starts in an interactive mode:
|
|
Gnomovision version 69, Copyright (C) 19yy name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. |
The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names:
|
|
Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. signature of Ty Coon, 1 April 1989 Ty Coon, President of Vice |
This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Library General Public License instead of this License.
|
[ < ] |
[ > ] |
|
[ << ] |
[ 上层 ] |
[ >> ] |
|
|
|
|
[顶层] |
[内容] |
[索引] |
[ ? ] |
这一章介绍了许多基本概念,但没有提及一些感觉不到的细节. 如果你并不了解如何使用Bison/Yacc,我们建议你仔细阅读这一章.
|
|
从数学的概念来介绍语言和上下文无关文法. |
|
|
|
怎样用Bison的语法表示各种文法. |
|
|
|
每个记号或者语法组可有一个语义值(例如:整数的数值,标识符的名称等等) |
|
|
|
每个规则可有一个包含C代码的动作 |
|
|
|
为普通的上下文无关文法编写分析器 |
|
|
|
追踪位置 |
|
|
|
Bison的输入和输出是什么,怎样使用Bison的输出 |
|
|
|
编写和运行Bison语法分析程序的流程. |
|
|
|
Bison语法文件的整体布局 |
|
[ < ] |
[ > ] |
|
[ << ] |
[ 上层 ] |
[ >> ] |
|
|
|
|
[顶层] |
[内容] |
[索引] |
[ ? ] |
为了使Bison能分析语言,这种语言必须由上下文无关文法(context-free grammar)描述. 也就是说,你必须指出一个或者多个语法组(syntactic groupings)以及从语法组的部分构它的建整体的规则. 例如,在C语言中,有一种我们称之为`表达式(expression)'的语法组. 一个生成表达式的规则可能是"一个表达式由一个减号和另一个表达式构成". 另外一个规则可能是"一个表达式可以是一个整数". 就象你看到的一样,规则经常是递归定义的,但是必须有一个结束递归的规则.
用于表示这些规则的最普遍的系统是Backus-Naur 范式(Backus-Naur Form)或者"BNF". 发明这种语言的目的是用来阐述Algol 60. 任何用BNF表示的文法都是一种上下文无关文法. Bison要求它的的输入必须用BNF表示.
上下文无关文法有许多重要的子集.尽管Bison可以处理几乎所有的上下文无关文法, 但Bison针对LALR(1) 文法(LALR(1) grammars)做了优化. 简而言之,在这些文法中(注:指LALR(1)),我们可以告之如何分析仅代有一个超前扫描记号的输入字符串的任意部分. 严格的说,这是一个LR(1)文法的描述. LALR(1)包括了与多难以分析的额外限制. 幸运的是,在实际中,我们很难找到一个是LR(1)文法而不是LALR(1)文法的例子. 参阅神秘的归约/归约冲突-Mysterious Reduce/Reduce Conflicts,以获取更多信息.
LALR(1)文法分析器具有确定性(deterministic), 这就意味着应用于输入的下一个文法规则取决于之前的输入和确定的部分剩余输入(我们称之为一个超前扫描记号(look-ahead). 一个上下文无关文法可能是有歧义的(ambiguous),即可能可以应用多种规则来获取某些输入. 即使非歧义性文法也可能使不确定(non-deterministic)的, 即没有总能足以决定下一个应用的文法规则的确定的超前扫描记号。 使用孰知的GLR技术,Bison的GLR就可以分析这些更为普通的上下文无关文法. 当任意给定字符串的可能的分析是确定的情况下,Bison可以处理任意上下文无关文法.
在正式的语言语法规则中,每一种语法单元或组合被称之为符号(symbol). 那些可以通过语法规则被分解成更小的结构的符号叫做非终结符(nonterminal symbols). 那些不能被再分的符号叫做终结符(terminal symbols)或者记号类型(token types). 我们把同终结符相对应的输入片段叫做记号(token), 把同单个非终结符相对应的输入片段叫做组(grouping).
我们可以使用C语言做为例子来解释什么是符号,以及终结符和非终结符的含义. C语言的记号包括标识符,常量(数字或者字符串)以及各种关键字,数学操作符和标点符号. 所以C语言语法的终结符包括`identifier',`number',`string',加上每个关键字的符号,操作符或者标点符号. 例如:`if',`return',`const',`static',`int',`char',`plus-sign',`open-brace',`close-brace',`comma'以及更多. (这些记号可以再分为字符,但是这些是词法学而不是语法学的事情)
这是一个如何将C函数分解成记号的例子:
|
|
int /* 关键字 `int' */ square (int x) /* identifier, open-paren, 关键字 `int', identifier, close-paren */ { /* open-brace */ return x * x; /* 关键字 `return', identifier, asterisk, identifier, semicolon */ } /* close-brace */ |
C语言的语法组包括表达式,语句,声明和函数定义. 这些由C语言语法中的非终结符`expression',`statement',`declaration'和`funcation definition'表示. 完整的语法还使用了许多额外的语言结构,每种结构都有自己的非终结符来表示上述四种的含义. 上面的例子是一个函数的定义,它包括了一个声明和一个语句. 每一个`x'一个表达式,而且`x * X'也是一个表达式.
每一个非终结符必须有一个描述如何由更简单结构组成这个非终结符的语法规则.
例如,一种C的语句是return语句的非正式表达将由如下的语法规则描述:
一种语句可以由一个`return'关键字,一个`expression'何以个`semicolon'组成.
还有许多其它对应`statement'的规则,每一种规则对应一种C语句.
我们必须注意到一种特殊的非终结符,这种非终结符定义了语言的一个完整表述. 我们称之为开始符号(start symbol). 在编译器中,这意味着一个完整的输入程序. 在C语言中,非终结符`sequence of definitions and declarations'扮演了这个角色.
例如,`1+2'是以个有效的C表达式--一个有效的C程序的部分--但它不能做为一个C程序的全部(entire). 在C语言的上下文无关文法中,这遵循了`expression'不是开始符号的事实.
Bison分析器读取一个记号序列做为它的输入并使用语法规则将记号组合. 如果输入是有效的,最终的结果是将整个的输入序列分析整理到开始符号. 如果我们使用C语言的语法,整个的输入必须是一个`sequence of definitions and declarations', 否则分析器会报告一个语法错误.
|
[ < ] |
[ > ] |
|
[ << ] |
[ 上层 ] |
[ >> ] |
|
|
|
|
[顶层] |
[内容] |
[索引] |
[ ? ] |
正规文法是一种数学结构.为了定义Bison要分析的语言, 你必须用Bison语法编写一个表达该语言的文件,即一个Bison 语法(Bison grammar)文件. 参阅 Bison的语法文件-Bison Grammar Files.
就象C语言中的标识符一样,在Bison的输入中,一个正规文法的非终结符由一个标识符表示.
根据惯例,非终结符应该用小写子母表示,例如expr,stmt或者declaration.
在Bison中,终结符也被称为符号类型(token
type). 符号类型也可以由类似C语言标识符来表示.
根据惯例,这些标识符因改用大写子母表示以区分它和非终结符.
例如,INTEGER,INDENTIFIER,IF或者RETURN.
一个表示某语言的特定关键字的终结符应该由紧随该关键字之后的它的大写表示来命名.
终结符error保留用作错误恢复之用.
参阅 符号-Symbols.
一个终结符也可以由一个像C中的字符常量一样的一个字符来表示. 当一个记号就是一个字符(括号,加号等等)的时候,你可以这样做: 使用同一个字符做为那个记号的终结符.
第三种表示终结符的方法是使用包含一些字符的C字符串常量. 获取更多这方面的信息可以参阅 符号-Symbols.
语法规则在Bison语法中也有相应的表示.例如,下面有一个C语言return语句的规则.
在引号中的分号,是一个字符记号,它是用来表示C语言部分语法的.
没有在引号中的分号和冒号是Bison用来表示每一条规则的标点符号.
|
|
stmt: RETURN expr ';' ; |
获取这方面更多信息,参阅 语法规则的语法-Syntax of Grammar Rules.
|
[ < ] |
[ > ] |
|
[ << ] |
[ 上层 ] |
[ >> ] |
|
|
|
|
[顶层] |
[内容] |
[索引] |
[ ? ] |
正规文法仅仅靠类别来选择记号:例如,如果一个规则提到了终结符`integer constant', 这就意味着任何整数常量在那个位置上都是语法有效的. 常量的精确值与如何分析输入不相关:如果`x+4'符合语法,那么`x+1'或者`x+3989'也符合语法.
但是,当分析输入时,它的精确值是非常重要的,通过精确值可以了解输入的含义. 如果一个编译器不能区别程序中的4,1和3989等常量,毫无疑问,这个编译器是没有用的! 因此,每一个Bison语法的记号既含有一个符号类别也有一个语义值(semantic value). 获取这方面的更多信息,参阅 定义语言的语义-Defining Language Semantics.
符号类型是在语法中定义的终结符,例如INTEGER,IDENTIFIRE或者','.
它告诉你决定记号可能有效出现的位置以及如何将它组合成其它记号的信息.
语法规则只知道符号的类型,其它的什么都不知道.
语义值包括了记号的所有剩余信息.例如整数的数值,标识符的名称.
(一个如','的记号只是一个标点,并不需要语义值.)
例如,一个分类为INTEGER的记号包含语义值4.
另一个也被分类为INTEGER的记号的语义值却是3989.
当一个语法规则表明INTEGER是允许的,任意的这些记号都是可接受的,因为它们都是INTEGER.
当一个分析器接受了记号,它会跟踪这个记号的语义值.
每一个语法组和它的非终结符也可已有语义值. 一个很典型的例子,在计算器中,一个表达式含有一个数值做为它的语义值, 在程序语言编译器中.一个典型的表达式含有一个用于描述它含义的树型结构做为语义值.
|
[ < ] |
[ > ] |
|
[ << ] |
[ 上层 ] |
[ >> ] |
|
|
|
|
[顶层] |
[内容] |
[索引] |
[ ? ] |
为了更加实用,一个程序不仅仅要分析输入而且必须做的更多. 它应该可以在输入的基础上产生一些输出. 在Bison语法中,一个语法规则可以有一个包括多个C语句的动作(action). 分析器每次识别一个规则的匹配,相应的动作就会被执行. 获取这方面的更多信息,参阅 动作-Actions.
大多数时候,动作的目的是从部分结构的语义值计算整个结构的语义值. 例如,加入我们有一个规则表明一个表达式可以由两个表达式相加而成. 当分析器识别了一个加法和,每一个子表达式都有一个描述其如何构建的语义值. 这个规的动做就是为了新识别的大表达式建立一个类似的语义值.
例如,这里的一个规则表明一个表达式可由两个表达式相加而成.
|
|
expr: expr '+' expr { $$ = $1 + $3; } ; |
这个动作表明了如何从子表达式的语义值产生加法和表达式的语义值.
|
[ < ] |
[ > ] |
|
[ << ] |
[ 上层 ] |
[ >> ] |
|
|
|
|
[顶层] |
[内容] |
[索引] |
[ ? ] |
在一些文法中,Bison标准的LALR(1)分析算法, 不能针对给定的输入应用一个确定的语法规则. 这就是说,Bison可能不能决定(在当前输入的基础上)应该使用两个可能的归约中的那一个, 或者不能决定到底应该应用一个归约还是先读取一些输入稍后再进行归约. 以上两种冲突分别被称为归约/归约(reduce/reduce)冲突(参阅归约/归约-Reduce/Reduce一章)和 移进/归约(shift/reduce)冲突(参阅移进/归约-Shift/Reduce一章).
有些时候,
为了使用一个很难被修改成LALR(1)文法的文法做为Bison的输入,
Bison需要使用通用的分析算法.
如果你在你的文件中加入了这样的声明%glr-parser(参阅语法大纲-Grammar
Outline一章),
Bison会产通用的LR(GLR)分析器.
这些分析器(例如,在应用了先前所述的声明之后)
在处理那些不包含未解决的冲突的文法时,
采用与LALR(1)分析器一样的处理方式.
但是当面临未解决的移进/归约冲突和归约/归约冲突的时候,
GLR分析器权宜地同时处理这两个可能,
即有效地克隆分析器自己以便于追踪这这两种可能性.
每一个克隆出来的分析器还可以再次被克隆,
这就保证在任意给定的时间,可以处理任意个可能的分析.
分析器逐步地进行分析,即所有的分析器它们进入到下一个输入之前,
都会消耗(归约)给定的输入符号.
每一个被克隆的分析器最终只有两个可能的归宿:
或者这个分析器因进入了一个分析错误而最终被销毁,
会这它和其它的分析器合并,因为它们把输入归约到了一个相同的符号集.
在有多个分析器并存的时刻,Bison只记录它们的语义动作而不是执行它们. 当一个分析器消失的时候,相应的语义动作记录也消失并且永远不会被执行. 当一个规约使得两个分析器等价而合并的时候, Bison会记录下它们两个的语义动作集. 每当最后两个分析器合并成为一个单独的分析器的时候, Bison执行所有未完成的动作.这些动作既可能依靠语法规则的优先级被执行也可能均被Bison执行. 在执行完动作之后,Bison调用指定的用户定义求值函数来产生一个独立的合并值.
|
|
||
|
|
使用GLR分析器解决歧义 |
|
|
1.5.3 编译GLR分析器时需要考虑的问题-Considerations when Compiling GLR Parsers |
|
GLR分析器需要一个现代的C编译器 |
|
[ < ] |
[ > ] |
|
[ << ] |
[ 上层 ] |
[ >> ] |
|
|
|
|
[顶层] |
[内容] |
[索引] |
[ ? ] |
在最简单的情况下,你可以使用GLR算法分析那些非歧义但不是LALR(1)文法的文法. 典型地,这些文法需要不止一个的超前扫描记号, 或者(在极少数情况下)由于LALR(1)算法丢弃太多的信息 而不属于LALR(1)的文法(它们却属于LR(1),参阅冲突-Mystery Conflicts).
考虑一个产生于Pascal语言枚举和子界类型声明中的问题,. 这里有一些例子:
|
|
type subrange = lo .. hi; type enum = (a, b, c); |
最初的语言标准只允许数字和常量标识符做为子界的范围(`lo'和`hi'). 但是扩展的Pascal(ISO/IEC10206)以及许多以它的Pascal的实现还允许独立的表达式. 这导致了如下一个包含过量括号的情形.
|
|
type subrange = (a) .. b;
|
考虑如下这个仅含一个值的枚举类型的声明.
|
|
type enum = (a);
|
(在这里的这些例子是人为制造的,但它们是语法有效的. 在实际的程序中可能会出现更复杂的例子)
这两个例子看起来相同(注:指语法上)直到`..'记号的出现. 对于只含有一个超前扫描记号的普通LALR(1)分析, 当`a'被分析的时,分析器候并不能两个形式中(注:指枚举或者子界)那一个是正确的. 因为在后一个例子中,`a'必须成为一个新的标识符来表示枚举; 而在前一个例子中必须使用`a'和它的含义来估计它可能是一个常量或者函数调用, 所以我们当然希望分析器可以对此作出正确的决定.
你可将`(a)'分析成"在括号中为说明的标识符"以便稍后进行分析. 但当括号嵌套在表达式的递归规则中的时候, 这种方式需要对语义动作和大部分语法做出相应的调整.
你可能会想到使用词法分析器通过返回已定义和未定地标识符来区别这两种形式. 但是如果这些声明在局部出现, 而`a'在外部定义的时候,这两种形式都是可能的- 既可能是局部重定义的`a'.也可能是使用外部定义的`a'的值. 所以这种方法不可行.
解决这个问题由一个简单的办法,那就是使用GLR算法. 当GLR分析器到达关键区域的时候, 它会同时沿着两个分支进行分析. 其中的一个分支迟早要进入分析错误. 如果有一个`..'记号在下一个`;'之前的化; 由于枚举类型的规则不能接受`..',所以它应用失败; 否则,子界类型规则应用失败,因为它要求一个`..'记号. 所以,一个分支无声地失败而另一个分支正常地进行, 并且执行所有的在拆分期间被推迟执行的中间动作.
如果输入不符和语法,则这两个分支的分析都会失败 并且如常地报告一个语法错误.
分析器的功能似乎是在"猜测"正确的语法分支,换句话说, 它使用了比LALR(1)算法允许使用的更多的超前扫描记号. LALR(2)有能力分析这个句子, 但是有些不符和LALR(k)的例子, 即使任意多的k可以这样地处理.
一般而言,一个GLR分析器在最坏情况下会花费二次方或者三次方的时间复杂度, 当前的Bison甚至会为了某些文法而花费指数的时间. 在实际应用中,这些几乎不会发生, 并且对于许多文法来说,证明它不能发生也是可能的. 当前讨论的例子在两个规则中只有一个冲突, 并且含有冲突的类型声明不能嵌套使用. 所以在任意时刻的分支数量被限制在常量2以下, 这时候分析时间仍然是线性的.
这是一个同上面描述相对应的例子. 它由Pascal类型声明大大简化而来. 如果输入不符和语法,两个分支都会失败并且如常地报告一个语法错误.
|
|
%token TYPE DOTDOT ID %left '+' '-' %left '*' '/' %% type_decl : TYPE ID '=' type ';' ; type : '(' id_list ')' | expr DOTDOT expr ; id_list : ID | id_list ',' ID ; expr : '(' expr ')' | expr '+' expr | expr '-' expr | expr '*' expr | expr '/' expr | ID ; |
当使用平常的LALR(1)文法的时候,Bison会报告一个归约/归约冲突. 在冲突的时候,分析器在会众多选择中选取一个-随意地选择那个先声明的. 所以下面的正确输入不能被识别.
|
|
type t = (a) .. b;
|
在Bison输入文件中, 加入这两个声明(在第一个`%%'之前)分析器可以将分析器编成一个GLR分析器, 并且Bison不会报告一个归约/归约冲突.
|
|
%glr-parser %expect-rr 1 |
并不需要对语法本身进行修改. 分析器现通过上面的限制语法后,可以认识所有有效的声明. 用户实际上并不能查觉分析器的拆分.
这就是我们使用GLR而几乎没有坏处的例子. 即使像这样简单的例子,至少两个潜在的问题值得我们注意. 第一,我们总应该分析Bison的冲突报告来确定GLR拆分总发生在我们想要的时候. 一个GLR分析器的拆分会不经意地产生比LALR分析器在冲突中静态的错误选择 更加不明显的问题. 第二,要仔细考虑与词法分析器的互动(参阅语义记号-Sematic Tokens一章). 由于在拆分期间的分析器消耗记号时并不产生任何动作, 词法分析器并不能通过分析动作获得信息. 一些与词法分析器的互动在使用GLR来消除从词法分析器到语法分析器的复杂读时可以忽略. 你必须监察其余情况下时的正确性.
在我们的例子中,因为并没有新的符号在类型声明中间被定义. 词法分析器基于记号当前在符号表的含意被返回是安全的. 即使在分析枚举常量时定义它们是可能的, 由于它们不能在相同的枚举类型声明中使用, 所以这实际上并没有什么不同.
|
[ < ] |
[ > ] |
|
[ << ] |
[ 上层 ] |
[ >> ] |
|
|
|
|
[顶层] |
[内容] |
[索引] |
[ ? ] |
让我们考虑由C++语法大大简化而来的例子.
|
|
%{ #include <stdio.h> #define YYSTYPE char const * int yylex (void); void yyerror (char const *); %} %token TYPENAME ID %right '=' %left '+' %glr-parser %% prog : | prog stmt { printf ("\n"); } ; stmt : expr ';' %dprec 1 | decl %dprec 2 ; expr : ID { printf ("%s ", $$); } | TYPENAME '(' expr ')' { printf ("%s <cast> ", $1); } | expr '+' expr { printf ("+ "); } | expr '=' expr { printf ("= "); } ; decl : TYPENAME declarator ';' { printf ("%s <declare> ", $1); } | TYPENAME declarator '=' expr ';' { printf ("%s <init-declare> ", $1); } ; declarator : ID { printf ("\"%s\" ", $1); } | '(' declarator ')' ; |
这个例子模拟了有问题的C++语法--某些声明和语句中的歧义.例如,
|
|
T (x) = y+z;
|
既可以被分析为一个expr也可被分析为一个stmt
(假定`T'被识别为一个TYPENAME并且`x'被识别为ID).
Biosn检测到了这个在规则expr
: ID和规则delarator
: ID之间的归约/归约冲突.
分析器遇到x的时候还不能解决冲突.
由于这是一个GLR分析器,所以它会把问题拆分成两个分析器,
每一个用于解决归约/归约冲突中的一个.
与上一节的例子不同(参阅简单的GLR分析器一章),两个分析器中的任一个都不"死亡,"
因为这个语法本身就是歧义的.
其中的一个分析最终归约到stmt
: expr ';'而另一个归约到stmt
: decl, 在这之后,两个分析器进入同一个状态:
它们都已经看到`prog
stmt'并且有相同的未处理剩余输入.
我们说这些分析器已经合并(merged).
在这个时候,GLR分析器需要一个关于如何在两个竞争的分析中做出选择的说明.
在上面的例子中,两个%dprec声明说明了Bison给予decl的解释以优先级.
这就表明x是一个声明符(declarator).
因此分析器会打印出:
|
|
"x" y z + T <init-declare>
|
%deprc声明仅在多于一个分析幸存的情况下起作用.
考虑这个分析器的一个不同的输入字符串:
|
|
T (x) + y;
|
像在上一节展示的一样(参阅简单的GLR分析器-Simple
GLR Parsers一章),
这是另外一个使用GLR分析非歧义结构的例子.
在这里并没有歧义(这个并不能被分析成一个声明).
但是在Bison分析器遇到x的时候,
它没有足够的信息解决归约/归约冲突(还是不能决定x是一个expr还是一个declarator).
在这种情况下,没有优先级可供使用.
分析器再次被拆分成两个,一个假定x是一个expr
而另一个假定x是一个declarator.
两个分析器中的第二个在遇到+的时候被销毁,并且分析器打印出
|
|
x T <cast> y +
|
假设你想查看所有的可能性而不是解决歧义.
你必须合并两个可能的分析器的语义动作而不是选择其中的一个.
为了达到这个目的,你需要像如下那样地改变stmt的声明:
|
|
stmt : expr ';' %merge <stmtMerge> | decl %merge <stmtMerge> ; |
并且定义stmtMerge函数如下:
|
|
static YYSTYPE stmtMerge (YYSTYPE x0, YYSTYPE x1) { printf ("<OR> "); return ""; } |
并且在文件的开头要伴随一个前置声明在C声明中:
|
|
%{ #define YYSTYPE char const * static YYSTYPE stmtMerge (YYSTYPE x0, YYSTYPE x1); %} |
使用这些声明以后,产生的的分析器将第一个例子既分析成一个expr又分析成一个decl,
并且打印
|
|
"x" y z + T <init-declare> x T <cast> y z + = <OR>
|
Bison要求所有参加任何特定合并的产品(productions)拥有相同的`%merge'语句. 否则,歧义不能被解决而且分析器会在任何导致违反规定的合并时候报告一个错误.
|
[ < ] |
[ > ] |
|
[ << ] |
[ 上层 ] |
[ >> ] |
|
|
|
|
[顶层] |
[内容] |
[索引] |
[ ? ] |
GLR分析器需要支持C89或更新标准的编译器.
额外地,Bison使用关键字inline,这个关键字不是C89标准但却是C99标准,
并且是许多C99之前编译器支持的扩展.
使用它是为了分析器的用户可以处理移植性问题.
例如,如果使用Autoconf和Autoconf宏AC_C_INLINE,一个单单的
|
|
%{ #include <config.h> %} |
就足够用了,否则我们建议使用
|
|
%{ #if __STDC_VERSION__ < 199901 && ! defined __GNUC__ && ! defined inline #define inline #endif %} |
|
[ < ] |
[ > ] |
|
[ << ] |
[ 上层 ] |
[ >> ] |
|
|
|
|
[顶层] |
[内容] |
[索引] |
[ ? ] |
许多应用程序,如解释器和编译器,需要产生一些有用信息或者出错的信息. 为了达到这个目的,我们必须追踪每个语法结构的原文位置(textual location)或位置(location). Bison提供了追踪这些位置的机制.
每一个记号有一个语义值.类似地,每个记号也有一个位置, 对于所有记号和组来说,它们的位置的类型是相同的. 此外,输出的分析器也带有默认的存储位置的数据结构 (获取更多信息,参阅位置-Locations一章).
像语义值一样,位置可以在动作中使用特定的一套结构来访问.
在上面个的例子中,这个组的的位置是@$,而子表达式的位置是@1和@3.
当一个规则被匹配,一个默认的动作用于计算左侧的语义值(参阅动作-Actions一章).
类似地,另外一个默认的动作用于计算位置.
然而,这个默认动作对于对于大多数情况已经足够永,
即经常没有必要为每个规则描述@$应该是如何形成的.
当为一个给定的组建立一个新的位置的时候,
输出的分析器的默认行为是取第一个符号的开头和最后一个符号的末尾.
|
[ < ] |
[ > ] |
|
[ << ] |
[ 上层 ] |
[ >> ] |
|
|
|
|
[顶层] |
[内容] |
[索引] |
[ ? ] |
当你运行Bison的时候,你需要给Bison一个语法文件做为其输入. Bison的输出是一个分析这个语法文件描述的语言的C源代码文件. 这个文件叫做Bison分析器(Bison parse). 我们要记住Bison工具和Bison分析器是两个明显不同的程序: Bison工具是一个以Bison分析器为输出的程序. 这个Bison分析器应是你程序的一部分.
Bison分析器的工作是依照语法规则组合记号--例如,将标识符和操作符构建成表达式. 在组合的过程中它还要执行相应的语法规定的动作.
记号是来源于称为词法分析器(lexical
analyzer)的程序.
你必须以某种形式提供词法分析器(如用C编写).
Bison分析器每当需要一个新的记号的时候就会调用词法分析器.
Bison分析器并不之道记号"中"有什么东西(即使它们的语义值可能反映这个).
典型的词法分析器靠分析字符来产生记号,但是Bison并不依靠这个.
获取更多细节,参阅
词法分析函数yylex-The
Lexical Analyzer Function yylex.
Bison分析器文件是定义了名为yyparse并且实现了那个语法的函数的C代码.
这个函数并不能成为一个完成的C程序:你必须提供额外的一些函数.
其中之一是词法分析器.另外的一个是一个分析器报告错误时调用的错误报告函数.
另外,一个完整的C程序必须以名为main的函数开头;
你必须提供这个函数.并且安排它调用yyparse.
否则分析器永远都不会运行.
参阅 分析器C语言接口-Parser
C-Language Interface.
除了你编写的动作中的记号类型名称和符号以外
,所有Bison分析器文件自己定义的符号都以`yy'或者`YY'开头.
这些符号包括了接口函数例如词法分析函数yylex,错误报告函数yyerror
和分析器函数yyparse.
这些符号也包括了许多内部目的的标识符.
所以你要在Bison语法文件中避免使用除了本手册定义的以外的以`yy'或者`YY'开头的C标识符.
在一些情况下,Bison分析器文件包含系统头文件.
在这中情况下,你的代码注意被这些文件保留的标识符.
在意些非GNU系统,<alloca.h>,<stddef.h>以及<stdlib.h>
被包含在内用于声明内存分配器及相关类型.
如果你定义YYDEBUG为非零值,其它的系统头文件也可能被包括进内.
(参阅跟踪你的分析器-Tracing
Your Parser一章)
|
[ < ] |
[ > ] |
|
[ << ] |
[ 上层 ] |
[ >> ] |
|
|
|
|
[顶层] |
[内容] |
[索引] |
[ ? ] |
实际使用Bison设计语言的流程,从语法描述到编写一个编译器或者解释器,有三个步骤:
以Bison可识别的格式正式地描述语法.(参阅Bison语法文件一章) 对每一个语法规则,描述当这个规则被识别时相应的执行动作. 动作由C语句序列描述.
编写一个词法分析器处理输入并将记号传递给语法分析器.
词法分析器既可是手工编写的C代码(参阅词法分析函数yylex一章),
也可以由lex产生,但是lex的使用并未在这个手册中讨论.
编写一个调用Bison产生的分析器的控制函数.
编写错误报告函数.
将这些源代码转换成可执行程序,你需要按以下步骤进行.
按语法运行Bison产生分析器.
同其它源代码一样编译Bison输出的代码.
链接目标文件以产生最终的产品.
|
[ < ] |
[ > ] |
|
[ << ] |
[ 上层 ] |
[ >> ] |
|
|
|
|
[顶层] |
[内容] |
[索引] |
[ ? ] |
Bison工具的输入文件是以个Bison语法文件(Bison grammar file). 通常的Bison语法文件格式如下:
|
|
%{ Prologue %} Bison declarations %% Grammar rules %% Epilogue |
`%%',`%{' 和`%}'是Bison在每个Bison语法文件中用于分隔部分的标点符号.
prologue可用来定义在动作中使用类型和变量.
你可以使用预处理器命令在那里来定义宏,
或者使用#include包含干这些事情的头文件.
你需要声在那里与许多要在语法规则的动总中使用的全局标识符一起
声明词法分析器yylex和错误打印程序yyerror.
Bison declarations声明了终结符和非终结符以及操作符的优先级和各种符号语义值的各种类型.
Grammar rules定义了如何从每一个非终结符的部分构建其整体的语法规则.
Epilogue可以包括任何你想使用的代码. 在Prologue中声明的函数经常定义在这里. 在简单的程序里,剩余的所有程序可以放在这里.
|
[ < ] |
[ > ] |
|
[ << ] |
[ 上层 ] |
[ >> ] |
|
|
|
|
[顶层] |
[内容] |
[索引] |
[ ? ] |
现在开始我们展示并解释三个使用Bison编写的示范程序: 一个逆波兰记号计算器,一个代数符号(中缀)计算器和一个多功能计算器. 这些程序在BSD Unix 4.3上测试无误;它们每一个虽然功能有限, 但确都是实用的互动桌面计算器.
这些例子虽然简单,但是用Bison语法编写真正的程序设计语言的道理与这相同.
|
|
逆波兰记号计算器,我们的第一个例子,并不带有操作符优先级 |
|
|
|
中缀代数符号计算器,简单地介绍操作符优先级. |
|
|
|
在出现语法错误后继续分析 |
|
|
|
展示@n和@$的用法 |
|
|
|
带有存储和触发功能的计算器.它使用了多种数据类型来表示语义值. |
|
|
|
一些改进多功能计算器的方案 |
|
[ < ] |
[ > ] |
|
[ << ] |
[ 上层 ] |
[ >> ] |
|
|
|
|
[顶层] |
[内容] |
[索引] |
[ ? ] |
我们的第一个例子是双精度逆波兰记号(reverse polish notation)计算器(一个使用后缀操作符的计算器). 这个例子是一个很好的起点,因为没有操作符优先级的问题. 第二个例子会说明如何处理运算符优先级.
这个计算器的源代码文件叫`rpcalc.y'. `.y'是Bison惯用的输入文件的扩展名.
|
|
rpclac的Prologue(声明)部分. |
|
|
|
带注释的rpcalc语法规则 |
|
|
|
词法分析器 |
|
|
|
控制函数 |
|
|
|
错误报告的规则 |
|
|
|
使用Bison生成分析器 |
|
|
|
使用C编译器编译得到最终结果 |
|
[ < ] |
[ > ] |
|
[ << ] |
[ 上层 ] |
[ >> ] |
|
|
|
|
[顶层] |
[内容] |
[索引] |
[ ? ] |
rpclac的声明部分-Declarations
for rpcalc
这就是逆波兰记号计算器的C和Bison声明部分. 像C语言一样,注释部分在`/*…*/'中.
|
|
/* Reverse polish notation calculator. */ /* 逆波兰记号计算器 */ %{ #define YYSTYPE double #include <math.h> int yylex (void); void yyerror (char const *); %} %token NUM %% /* Grammar rules and actions follow. */ |
声明部分(参阅Prologue部分-The prologue一章)包括了两个预处理指令和两个前置声明.
#define指令定义了YYSTYPE宏,
它指明了记号和组(参阅语义值的数据类型-Data
Types of Semantic Values一章)语义值的C数据类型.
Bison分析器会使用任何YYSTYPE定义的数据类型;
如果你没有定义它,int则是默认的类型.
因为我们指明了double,所以每个记号和表达式已经关联了一个浮点数值.
#include用来声明幂函数pow.
由于C语言要求函数必须在使用之前声明,
所以前置声明yylex和yyerror是必须的.
这些函数将在epilogue部分定义,但是分析器会调用它们,
所以它们必须在prologue部分声明.
第二部分-Bison
declarations,提供了有关记号类型的信息(参阅Bison
Declarations部分-The
Bison Declarations Section一章).
每一个不只一个字符组成的终结符必须在这里声明.(通常没有必要声明单一字符.)
在这个例子中,所有的算术操作符都是单一字符的,
所以我们在这里仅需要声明的终结符是NUM--数字常量的记号类型.
|
[ < ] |
[ > ] |
|
[ << ] |
[ 上层 ] |
[ >> ] |
|
|
|
|
[顶层] |
[内容] |
[索引] |
[ ? ] |
rpcalc的语法规则-Grammar
Rules for rpcalc
这就是逆波兰记号计算器的语法规则:
|
|
input: /* empty */ | input line ; line: '\n' | exp '\n' { printf ("\t%.10g\n", $1); } ; exp: NUM { $$ = $1; } | exp exp '+' { $$ = $1 + $2; } | exp exp '-' { $$ = $1 - $2; } | exp exp '*' { $$ = $1 * $2; } | exp exp '/' { $$ = $1 / $2; } /* Exponentiation */ | exp exp '^' { $$ = pow ($1, $2); } /* Unary minus */ | exp 'n' { $$ = -$1; } ; %% |
在这里定义的rpcalc"语言"的组是:表达式(名称是exp),
输入行(line),整个的输入脚本(input).
这些非终结符每个都有多个可选择的规则.
这些规则由`|'连接而成.
我们可以把`|'读做"或者".
接下来的部分解释了规则的含义.
语法的语义由当组组被识别时执行的动作决定. 动作由在大括号中C代码组成. 参阅动作-Actions一章.
你必须用C语言来指定这些动作,
但是Bison也提供了在规则之间传递语义值的方法.
在每个动作中,伪变量$$代表着将要构建的组的语义值.
赋值给$$是大多数动作的工作.
规则的组成部分的语义值由$1,$2等等指定.
|
|
||
|
|
||
|
|
|
[ < ] |
[ > ] |
|
[ << ] |
[ 上层 ] |
[ >> ] |
|
|
|
|
[顶层] |
[内容] |
[索引] |
[ ? ] |
input-Explanation
of input
考虑input的定义:
|
|
input: /* empty */ | input line ; |
这个定义可以被解释为:"一个完整的输入己可能是一个空字符串,
也可能是一个完整输入后紧跟一个输入行".
我们应该注意到"完整输入"定义在它自己的条款中.
由于input总是出现在符号序列的最左端,
我们称这种定义为左递归(left
recursive). (参阅 递归规则-Recursive
Rule.)
第一个可选择的规则为空是由于在冒号和第一个`|'之间没有任何符号;
这意味着input可以匹配一个空字符串的输入(没有符号).
我们这样编写规则是因为在你启动计算器后,在右端输入Ctrl-d是合法的.
把空规则放在最开始并伴随注释`/*
empty */'是使用惯例.
第二个可选的规则(input
line)处理了所有非平凡的输入.
它的含义是"在读取任意个数的line后,如果可能的化读取更多的line."
作递归使得这个规则进入循环.
由于第一个可选的规则与空输入匹配,循环可能被执行零次或多次.
分析器函数yyparse继续处理输入直到发现一个语法错误或者
词法分析器表明没有更多的输入记号为止;
我们会安排后者在输入结束的时候发生.
|
[ < ] |
[ > ] |
|
[ << ] |
[ 上层 ] |
[ >> ] |
|
|
|
|
[顶层] |
[内容] |
[索引] |
[ ? ] |
line-Explanation
of line
现在我们考虑line的定义:
|
|
line: '\n' | exp '\n' { printf ("\t%.10g\n", $1); } ; |
第一个选择是一个换行符号;
这意味着rpcalc接受一个空白行(并且忽略它,因为没有相关的动作).
第二个选择是一个表达式后紧跟着一个换行符.
这个选择使得rpcalc变得实用.
$1的值是exp组的语义值,
这是因为exp是诸多选择中的第一个符号.
这个值就是用户需要的计算结果.
相关的动作打印了这个这个值.
这个动作非同寻常,因为它并没有向$$赋值.
这样做的结果就是与line相关联的语义值并未被初始化(它的值是不可预期的).
如果那个值被使用的话,这会成为一个bug,但是我们并未使用它.
rpcalc一旦已经打印了用户输入行的值,就不再需要那个值了.
|
[ < ] |
[ > ] |
|
[ << ] |
[ 上层 ] |
[ >> ] |
|
|
|
|
[顶层] |
[内容] |
[索引] |
[ ? ] |
expr-Explanation
of expr
exp组有很多规则,每一个都是一种表达式.
第一个规则处理最简单的表达式:仅仅是数字的表达式.
第二个处理看起来像两个表达式后紧跟一个假发符号的加法表达式.
第三个处理减法等等.
|
|
exp: NUM | exp exp '+' { $$ = $1 + $2; } | exp exp '-' { $$ = $1 - $2; } … ; |
我们已经使用`|'将exp的规则连接起来.
但是我们也可以将它们分开来写:
|
|
exp: NUM ; exp: exp exp '+' { $$ = $1 + $2; } ; exp: exp exp '-' { $$ = $1 - $2; } ; … |
大部分规则都有从其部分值来计算表达式值的动作.
例如,在加法的规则中,$1指的是第一个部件exp,
而$2指的是第二个部件.
第三个部件<code>'+'并没有相关联的语义值.
但是如果它有语义值的话,你可以用$3来代表.
当yyparse使用这个规则识别了一个加法和表达式,
两个自表达式值的相加而得到了整个表达是的值.
参阅 动作-Actions.
你不必为每一个规则都指定动作.
当一个规则没有动作时,Bison会默认地将$1的值复制给$$.
这就是在第一规则被(使用NUM的规则)识别时发生的事情.
在这里展示的是推荐的惯用格式, 但是Bison并没有要求一定要这么做. 你可以增加或者更改任意你想要的数量的空白. 例如,这个:
|
|
exp : NUM | exp exp '+' {$$ = $1 + $2; } | … ;
|
与这个的意义相同
|
|
exp: NUM | exp exp '+' { $$ = $1 + $2; } | … ; |
然而,后一种写法明显更可读.
|
[ < ] |
[ > ] |
|
[ << ] |
[ 上层 ] |
[ >> ] |
|
|
|
|
[顶层] |
[内容] |
[索引] |
[ ? ] |
rpcalc的词法分析器-The
rpcalc Lexical Analyzer
词法分析器的工作是低级别的分析:
将字符或者字符序列转化成记号.
Bison分析器靠调用词法分析器来获取它的记号.
参阅 词法分析函数yylex-The
Lexical Analyzer Function yylex.
即使最简单的词法分析器也是RPN计算器所必须的.
这个词法分析器跳过了空白和制表符,
然后将数字读取为double并且以NUM记号返回它们.
任何不是数字的字符都是一个分隔记号.
注意到一个单个字符的记号是这个字符本身.
词法分析器的返回值是一个代表记号类型的数字码.
相同的文字(在Biosn规则中使用,代表这个记号类型)
也是一个代表这个类型数字码的C表达式.
它以两种方式工作.
如果记号类型是一个字符,那么它的数字码就是那个字符;
你可以在词法分析器中使用相同字符表达那个数字码.
如果记号类型是一个标识符,
那个标识符是一个由Bison定义为一个恰当的数字的宏,
因此在这个例子中,NUM是一个给yylex使用的宏.
记号的语义值(如果有的话)被存储在全局变量yylval中.
Bison分析器会在需要语义值适当的时候找到它.
(yylval的C数据类是YYSTYPE,它在语法的开始被定义;
参阅rpcalc的声明部分-Declarations
for rpcals一章.
符号类型码0在输入结束的时候被返回. (Bison将任何不正确的值识别为输入结束)
这就是词法分析器的代码:
|
|
/* The lexical analyzer returns a double floating point number on the stack and the token NUM, or the numeric code of the character read if not a number. It skips all blanks and tabs, and returns 0 for end-of-input. */ /* 词法分析起在栈上返回一个双精度浮点数(注:指yylval)并且返回记号NUM, 或者返回不是数字的字符的数字码.它跳过所有的空白和制表符, 并且返回0作为输入的结束. */ #include <ctype.h> int yylex (void) { int c; /* Skip white space. */ /* 处理空白. */ while ((c = getchar ()) == ' ' || c == '\t') ; /* Process numbers. */ /* 处理数字 */ if (c == '.' || isdigit (c)) { ungetc (c, stdin); scanf ("%lf", &yylval); return NUM; } /* Return end-of-input. */ /* 返回输入结束 */ if (c == EOF) return 0; /* Return a single char. */ /* 返回一个单一字符 */ return c; } |
|
[ < ] |
[ > ] |
|
[ << ] |
[ 上层 ] |
[ >> ] |
|
|
|
|
[顶层] |
[内容] |
[索引] |
[ ? ] |
为了保证这个例子的精巧,控制函数也保持了最小化。
它仅仅要求调用yyparse来开始分析的处理.
|
|
int main (void) { return yyparse (); } |
|
[ < ] |
[ > ] |
|
[ << ] |
[ 上层 ] |
[ >> ] |
|
|
|
|
[顶层] |
[内容] |
[索引] |
[ ? ] |
当yyparse侦测到语法错误的时候,
它会调用错误报告函数yyerror来打印一个错误信息(经常但不总是"syntax
error"). yyparse需要程序员来提供yyerror(参阅分析器C语言接口-Parser
C-Language Interface一章),
所我我们使用这样的定义:
|
|
#include <stdio.h> /* Called by yyparse on error. */ void yyerror (char const *s) { fprintf (stderr, "%s\n", s); } |
在yyerror返回之后,
如果语法包括了适当的错误规则(参阅错误恢复-Error
Recovery一章)
Bison分析器可以从错误中恢复并且继续分析.
否则,yyparse返回非零值.
我们在这个例子中并没有编写任何错误规则,
所以任何无效的输入会导致计算器程序退出.
这种做法显然对于这正的计算器是不够用的,
但是足够用于这个例子.
|
[ < ] |
[ > ] |
|
[ << ] |
[ 上层 ] |
[ >> ] |
|
|
|
|
[顶层] |
[内容] |
[索引] |
[ ? ] |
(参阅Bison语法文件的布局-The
Overall Layout of a Bison Grammar一章).
在运行Bison制造分析器之前,我们要决定如何把源代码安排在一个或多个源文件中.
对于这样一个简单的例子来说,
最简单的方法就是把所有的东西放到一个文件中.
yylex,yyerror和main放在末尾的epilogue部分中.
对于一个大工程来说,你可能会有很多的源代码文件,
这时候你需要使用make安排它们的重编译.
因为所有的代码都在一个文件中, 你使用下面的命令将它转化成一个分析器文件:
|
|
bison file_name.y
|
在这个例子中,这个文件是`rpcalc.y'(代表
"Reverse
Polish CALCulator").
Bison产生一个名为`file_name.tab.c'的文件.
并且将原文件的`.y'的扩展名移去.
在输入中的额外函数(yylex,yyerror和main)被逐字地复制到输出文件.
|
[ < ] |
[ > ] |
|
[ << ] |
[ 上层 ] |
[ >> ] |
|
|
|
|
[顶层] |
[内容] |
[索引] |
[ ? ] |
这就是如何编译并运行分析器文件:
|
|
# 列出当前目录的文件.
$ ls
rpcalc.tab.c rpcalc.y
# 编译Bison分析器.
# `-lm' 告诉编译器搜索与
|
文件`rpcalc'现在已经包含了可执行代码.
这就是使用rpcalc的会话的例子:
|
|
$ rpcalc 4 9 + 13 3 7 + 3 4 5 *+- -13 3 7 + 3 4 5 * + - n 注意负号操作符, `n' 13 5 6 / 4 n + -3.166666667 3 4 ^ 幂运算 81 ^D 文件结束标识符 $ |
|
[ < ] |
[ > ] |
|
[ << ] |
[ 上层 ] |
[ >> ] |
|
|
|
|
[顶层] |
[内容] |
[索引] |
[ ? ] |
calc-Infix
Notation Calculator: calc
现在我们可以通过修改rpcalc使其处理中缀操作符. 中缀操作符涉及到了操作符优先级的概念以及任意深度的括号嵌套. 这里是`calc.y',一个中缀桌面计算器的代码.
|
|
/* Infix notation calculator. */ /* 中缀符号计算器 */ %{ #define YYSTYPE double #include <math.h> #include <stdio.h> int yylex (void); void yyerror (char const *); %} /* Bison declarations. */ %token NUM %left '-' '+' %left '*' '/' %left NEG /* negation--unary minus */ /* 负号 */ %right '^' /* exponentiation */ /* 幂运算 */ %% /* The grammar follows. */ /* 下面是语法 */ input: /* empty */ | input line ; line: '\n' | exp '\n' { printf ("\t%.10g\n", $1); } ; exp: NUM { $$ = $1; } | exp '+' exp { $$ = $1 + $3; } | exp '-' exp { $$ = $1 - $3; } | exp '*' exp { $$ = $1 * $3; } | exp '/' exp { $$ = $1 / $3; } | '-' exp %prec NEG { $$ = -$2; } | exp '^' exp { $$ = pow ($1, $3); } | '(' exp ')' { $$ = $2; } ; %% |
yylex,yyerror和main可以与上一个例子一样.
这段代码展示了两个重要的新特征.
在第二部分中(Bison
declarations), %left声明了记号类型并且指明它们是左结合操作符.
%left和%right(右结合)的声明代替了%token.
%token是用来声明没有结合性的记号类型的.
(这些记号(注:是指`'+'',`'-'',`'*'',`'/'',`'NEG'')
是原本并不用声明的单字符记号.我们声明它们的目的是指出它们的结合性.)
操作符优先级是由声明所在行的顺序决定的.
行号越大的操作符(在一页或者屏幕底端)具有越高的优先级.
因此,幂运算具有最高优先级,负号(NEG)其次,
接这是`*'和`/'等等.
参阅 操作符优先级-Operator
Precedence.
另外一个重要的特征是在语法部分的负号操作符中使用了%prec.
语法中的%prec只是简单的告诉Bison规则`|
'-' exp'与NEG有相同的优先级--在前述的优先级规则中.
参阅 依赖上下文的优先级-Context-Dependent
Precedence.
这就是一个运行`calc.y'的例子:
|
|
$ calc 4 + 4.5 - (34/(8*3+-3)) 6.880952381 -56 + 2 -54 3 ^ 2 9 |
|
[ < ] |
[ > ] |
|
[ << ] |
[ 上层 ] |
[ >> ] |
|
|
|
|
[顶层] |
[内容] |
[索引] |
[ ? ] |
直到这一章之前,这个手册并未提及错误恢复(error
recovery)的内容--
即在分析器侦测到错误之后如何继续分析.
所有我们需要做的事情就是编写yyerror函数.
我们可以回忆一下以前的例子,
yyparse在调用yyerror后返回.
这就意味着任一个错误的输入会导致计算机程序的退出.
现在我们就展示如何改正这个不足.
Bison语言自己包括了可以插入到语法规则中去的保留字error.
在这个例子中,它被添加到line的一个可选的规则中.
|
|
line: '\n' | exp '\n' { printf ("\t%.10g\n", $1); } | error '\n' { yyerrok; } ; |
这个添加的规则允许在语法错误发生的时候有简单的错误恢复动作.
如果一个读入一个无法求值的表达式,
这个错误会被识别成line的第三个规则并且分析会继续执行.
(yyerror函数仍会被调用来打印它的信息).
执行这个动作的语句yyerrok是一个被Bison自动定义的宏.
它的含义是错误恢复已经完成(参阅错误恢复-Error
Recovery一章).
我们应当注意到yyerror和yyerrok的区别,
它们的印刷都没有错误.
这种形式的错误恢复用于处理语法错误.
还有很多其它形式的错误;
例如,除数为0,这会产生一个通常致命的异常信号(an
exception signal). 一个真正的计算器必须处理这种信号并且使用longjmp返回到main并且继续分析输入行;
它(注:真正的计算器)也可以丢弃剩余的输入行.
我们并不深入地讨论这个问题,
因为这与Bison程序无关.
|
[ < ] |
[ > ] |
|
[ << ] |
[ 上层 ] |
[ >> ] |
|
|
|
|
[顶层] |
[内容] |
[索引] |
[ ? ] |
ltcalc-Location
Tracking Calculator: ltcalc
这个例子将中缀符号计算器扩展以使其带有位置追踪功能. 这个特征将被应用于改进错误消息的显示. 为了把保持简洁性, 这个例子是一个简单的整数计算器. 为了使用位置,大多数工作将在词法分析器中完成.
|
|
ltcalc的Bison和C语言的声明 |
|
|
|
详细解释ltcalc的语法规则 |
|
|
|
词法分析器 |
|
[ < ] |
[ > ] |
|
[ << ] |
[ 上层 ] |
[ >> ] |
|
|
|
|
[顶层] |
[内容] |
[索引] |
[ ? ] |
ltcalc的Declarations-Declarations
for ltcalc
位置追踪计算器的C和Bison声明部分与中缀符号计算器的声明部分相同.
|
|
/* Location tracking calculator. */ /* 位置追踪计算器 */ %{ #define YYSTYPE int #include <math.h> int yylex (void); void yyerror (char const *); %} /* Bison declarations. */ /* Bison 声明 */ %token NUM %left '-' '+' %left '*' '/' %left NEG %right '^' %% /* The grammar follows. */ /* 下面是语法 */ |
注意到并没有位置的特别声明.
并不需要定义一个存储位置的数据类型:
我们使用Bison提供的默认的类型(参阅位置的数据类型-Data
Types of Locations一章).
这种数据类型是带有四个整数域的结构体:
first_line,first_column,last_line和last_column.
|
[ < ] |
[ > ] |
|
[ << ] |
[ 上层 ] |
[ >> ] |
|
|
|
|
[顶层] |
[内容] |
[索引] |
[ ? ] |
ltcalc的语法规则-Grammar
Rules for ltcalc
是否处理位置对于你的语言的语法明没有影响. 因此,这个语言的语法规则与前一个例子的非常接近; 我们仅仅修改它们以获取新的信息.
在这里,我们使用位置来报告除数为0并且对错误的表达式或子表达式进行定位.
|
|
input : /* empty */ | input line ; line : '\n' | exp '\n' { printf ("%d\n", $1); } ; exp : NUM { $$ = $1; } | exp '+' exp { $$ = $1 + $3; } | exp '-' exp { $$ = $1 - $3; } | exp '*' exp { $$ = $1 * $3; } | exp '/' exp { if ($3) $$ = $1 / $3; else { $$ = 1; fprintf (stderr, "%d.%d-%d.%d: division by zero", @3.first_line, @3.first_column, @3.last_line, @3.last_column); } } | '-' exp %preg NEG { $$ = -$2; } | exp '^' exp { $$ = pow ($1, $3); } | '(' exp ')' { $$ = $2; } |
这段代码展示了如何对规则部件使用伪变量@n
以及对组使用伪变量@$在语义动作中确定位置.
我们不需要向@$赋值:输出分析器会自动这样作.
默认地,在执行每个动作的C代码之前,
对于一个具有n个部件的规则,
@$被设定为从@1的开始到@n的结束.
我们可以重新定义这个动作(参阅位置的默认动作-Default
Action for Locations一章),
对于一些特殊的规则,@$可以手动计算.
|
[ < ] |
[ > ] |
|
[ << ] |
[ 上层 ] |
[ >> ] |
|
|
|
|
[顶层] |
[内容] |
[索引] |
[ ? ] |
ltcalc的词法分析器-The
ltcalc Lexical Analyzer.
直到现在,我们仍然在依靠Bison的默认来激活位置追踪. 下一步就我们重写词法分析器, 并且使它与语法分析器的记号位置相适合, 就像它已经为语义值做的那样.
在最后,为了避免计算的位置出错,我们必须对每个输入字符进行计数.
|
|
int yylex (void) { int c; /* Skip white space. */ /* 跳过空白 */ while ((c = getchar ()) == ' ' || c == '\t') ++yylloc.last_column; /* Step. */ yylloc.first_line = yylloc.last_line; yylloc.first_column = yylloc.last_column; /* Process numbers. */ /* 处理数字 */ if (isdigit (c)) { yylval = c - '0'; ++yylloc.last_column; while (isdigit (c = getchar ())) { ++yylloc.last_column; yylval = yylval * 10 + c - '0'; } ungetc (c, stdin); return NUM; } /* Return end-of-input. */ /* 返回输入结束 */ if (c == EOF) return 0; /* Return a single char, and update location. */ /* 返回一个单字符,并且更新位置 */ if (c == '\n') { ++yylloc.last_line; yylloc.last_column = 0; } else ++yylloc.last_column; return c; } |
词法分析器基本上做出了与前一个例子相同的处理:
它跳过了空格和制表符,并且读入了许多单字符记号.
而外地,它更新了含有记号位置的全局变量(类型是YYLTYPE)yyloc.
现在,每当这个函数返回一个记号,与语义值一样,
分析器就有了这个记号的编号和它的文字位置.
最后需要改变的就是初始化yyloc.
例如在控制函数中:
|
|
int main (void) { yylloc.first_line = yylloc.last_line = 1; yylloc.first_column = yylloc.last_column = 0; return yyparse (); } |
要记住:计算位置并不是语法的事情. 每个字符必须关联一个位置更新, 不论它是在一个有效的输入中还是在注释中或者字符串中等等.
|
[ < ] |
[ > ] |
|
[ << ] |
[ 上层 ] |
[ >> ] |
|
|
|
|
[顶层] |
[内容] |
[索引] |
[ ? ] |
mfcalc-Multi-Function
Calculator: mfcalc
到现在为止,我们已经讨论了Bison的基础部分.
是时间去转移到一些高级的问题中去.
上述的计算器仅仅提供了五个功能,
`+',`-',`*',`/'和`^'.
让我们的计算器提供其它数学函数如sin,cos等等是一个不错的想法.
一旦新的操作符只是单字符,将它们添加到中缀计算器是很简单的事情.
词法分析器yylex将所有非数字字符返回成记号,
所以新的语法规则对于新的操作符来说足够用.
但是我们需要一些更灵活的东西:采用这种格式的内建函数:
|
|
function_name (argument)
|
同时,我们靠建立命名变量来为计算器添加记忆功能. 我们可以在它们中存储数值,并在稍后使用它们. 这就是多功能计算器的一个会话的例子:
|
|
$ mfcalc pi = 3.141592653589 3.1415926536 sin(pi) 0.0000000000 alpha = beta1 = 2.3 2.3000000000 alpha 2.3000000000 ln(alpha) 0.8329091229 exp(ln(beta1)) 2.3000000000 $ |
注意到多重赋值和嵌套函数是允许使用的.
|
|
多功能计算器的Bison声明 |
|
|
|
计算器的语法声明 |
|
|
|
符号表管理规则 |
|
[ < ] |
[ > ] |
|
[ << ] |
[ 上层 ] |
[ >> ] |
|
|
|
|
[顶层] |
[内容] |
[索引] |
[ ? ] |
mfcalc的声明-Declarations
for mfcalc
这就是多功能计算器的C和Bison声明部分:
|
|
%{ #include <math.h> /* For math functions, cos(), sin(), etc. */ /* 为了使用数学函数, cos(), sin(), 等等 */ #include "calc.h" /* Contains definition of `symrec'. */ /* 包含了 `symrec'的定义 */ int yylex (void); void yyerror (char const *); %} %union { double val; /* For returning numbers. */ /* 返回的数值 */ symrec *tptr; /* For returning symbol-table pointers. */ /* 返回的符号表指针 */ } %token <val> NUM /* Simple double precision number. */ /* 简单的双精度数值 */ %token <tptr> VAR FNCT /* Variable and Function. */ /* 变量和函数 */ %type <val> exp %right '=' %left '-' '+' %left '*' '/' %left NEG /* negation--unary minus */ /* 负号 */ %right '^' /* exponentiation */ /* 幂 */ %% /* The grammar follows. */ |
上述的语法仅仅引进了两个Bison语言的信特征. 这些特征允许语义值拥有多种数据类型. (参阅多种值类型-More Than One Value Type一章).
%union声明了所有可能类型清单;
这是用来取代YYSTYPE的.
现在允许的类型是双精度(为了exp和NUM)和指向符号表目录项的指针.
参阅 值类型集-The
Collection of Value Types.
由于语义值值现在可以有多种类型,
对每一个使用语义值的语法符号关联一个语义值类型是很必要的.
这些符号是NUM,VAR,FNCT,和exp.
它们的在声明的时候已经指明了语义值类型(在中括号之间).
%type用来声明非终结符,就像%token用来声明符号类型(注:终结符)的一样.
我们之前并没有%type是因为非终结符经常在定义它们的规则中隐含地声明.
但是exp必须被明确地声明以便我们使用它的语义值类型.
参阅 非终结符-Nonterminal
Symbols.
|
[ < ] |
[ > ] |
|
[ << ] |
[ 上层 ] |
[ >> ] |
|
|
|
|
[顶层] |
[内容] |
[索引] |
[ ? ] |
mfcalc的语法规则-Grammar
Rules for mfcalc
这就是多功能计算器的语法规则.
它们之中的大多数都是从calc复制过来的;
三个提到VAR或者FNCT的规则是新增的.
|
|
input: /* empty */ | input line ; line: '\n' | exp '\n' { printf ("\t%.10g\n", $1); } | error '\n' { yyerrok; } ; exp: NUM { $$ = $1; } | VAR { $$ = $1->value.var; } | VAR '=' exp { $$ = $3; $1->value.var = $3; } | FNCT '(' exp ')' { $$ = (*($1->value.fnctptr))($3); } | exp '+' exp { $$ = $1 + $3; } | exp '-' exp { $$ = $1 - $3; } | exp '*' exp { $$ = $1 * $3; } | exp '/' exp { $$ = $1 / $3; } | '-' exp %prec NEG { $$ = -$2; } | exp '^' exp { $$ = pow ($1, $3); } | '(' exp ')' { $$ = $2; } ; /* End of grammar. */ %% |
|
[ < ] |
[ > ] |
|
[ << ] |
[ 上层 ] |
[ >> ] |
|
|
|
|
[顶层] |
[内容] |
[索引] |
[ ? ] |
mfcalc的符号表-The
mfcalc Symbol Table
多功能计算器需要一个符号表来追踪变量和符号的名称和意义. 这并不影响语法规则(除了动作以外)或者Biosn声明. 但是它要求一些额外的C函数支持.
符号表本身由记录的链表组成. 它的定义在头文件`calc.h'中,如下. 它提供了将函数或者变量插入符号表的功能.
|
|
/* Function type. */ /* 函数类型 */ typedef double (*func_t) (double); /* Data type for links in the chain of symbols. */ /* 链表节点的数据类型 */ struct symrec { char *name; /* name of symbol */ /* 符号的名称 */ int type; /* type of symbol: either VAR or FNCT */ /* 符号的类型: VAR 或 FNCT */ union { double var; /* value of a VAR */ /* VAR 的值 */ func_t fnctptr; /* value of a FNCT */ /* FNCT 的值 */ } value; struct symrec *next; /* link field */ /* 指针域 */ }; typedef struct symrec symrec; /* The symbol table: a chain of `struct symrec'. */ /* 符号表: `struct symrec'的链表 */ extern symrec *sym_table; symrec *putsym (char const *, func_t); symrec *getsym (char const *); |
新版本的main包含了一个init_table的调用,
这个函数用来初始化符号表.
这就是main和init_table的代码:
|
|
#include <stdio.h> /* Called by yyparse on error. */ /* 出错时被yyparse调用 */ void yyerror (char const *s) { printf ("%s\n", s); } struct init { char const *fname; double (*fnct) (double); }; struct init const arith_fncts[] = { "sin", sin, "cos", cos, "atan", atan, "ln", log, "exp", exp, "sqrt", sqrt, 0, 0 }; /* The symbol table: a chain of `struct symrec'. */ /* 符号表: `struct symrec'链表 */ symrec *sym_table; /* Put arithmetic functions in table. */ /* 将数学函数放入符号表(注:保留字的实现方式) */ void init_table (void) { int i; symrec *ptr; for (i = 0; arith_fncts[i].fname != 0; i++) { ptr = putsym (arith_fncts[i].fname, FNCT); ptr->value.fnctptr = arith_fncts[i].fnct; } } int main (void) { init_table (); return yyparse (); } |
靠简单地编辑初始化列表和添加必要的头文件, 你可以为计算器添加额外的功能.
两个重要的函数允许对符号表进行搜索和安置操作.
putsym函数需要传递要安置对象的名称和类型(VAR或FNCT).
对象被连接到链表的头部,一个指向这个对象的指针最后被返回.
getsym函数要传递需要查找的符号的名称.
如果找到,指向那个符号的指针回返回,否则返回0.
|
|
symrec * putsym (char const *sym_name, int sym_type) { symrec *ptr; ptr = (symrec *) malloc (sizeof (symrec)); ptr->name = (char *) malloc (strlen (sym_name) + 1); strcpy (ptr->name,sym_name); ptr->type = sym_type; ptr->value.var = 0; /* Set value to 0 even if fctn. */ /* 置0即使是fctn */ ptr->next = (struct symrec *)sym_table; sym_table = ptr; return ptr; } symrec * getsym (char const *sym_name) { symrec *ptr; for (ptr = sym_table; ptr != (symrec *) 0; ptr = (symrec *)ptr->next) if (strcmp (ptr->name,sym_name) == 0) return ptr; return 0; } |
yylex函数现在必须能识别出变量,数字值和单字符算术操作符.
开头为非字数字的字符串被识别为变量还是函数依赖于符号表对它们的描述.
字符串被传递到getsym用于在符号表中查找.
如果名称出现在表格中,指向它的位置的指针和它的类型会返回给yyparse.
如果尚未出现在符号表中,它会被安置为一个VAR使用函数putsym.
同样地,一个指针和它的类型(必须是VAR)被返回给yyparse.
yylex中处理数字制和算术运算符的代码并不需要更改.
|
|
#include <ctype.h> int yylex (void) { int c; /* Ignore white space, get first nonwhite character. */ /* 忽略空白,获取第一个非空白的字符 */ while ((c = getchar ()) == ' ' || c == '\t'); if (c == EOF) return 0; /* Char starts a number => parse the number. */ /* 以数字开头 => 分析数字 */ if (c == '.' || isdigit (c)) { ungetc (c, stdin); scanf ("%lf", &yylval.val); return NUM; } /* Char starts an identifier => read the name. */ /* 以标识符开头 => 读取名称 */ if (isalpha (c)) { symrec *s; static char *symbuf = 0; static int length = 0; int i; /* Initially make the buffer long enough for a 40-character symbol name. */ /* 在开始的时候使缓冲区足够容纳40字符长的符号名称*/ if (length == 0) length = 40, symbuf = (char *)malloc (length + 1); i = 0; do { /* If buffer is full, make it bigger. */ /* 如果缓冲区已满,使它大一点 */ if (i == length) { length *= 2; symbuf = (char *) realloc (symbuf, length + 1); } /* Add this character to the buffer. */ /* 将这个字符加入缓冲区 */ symbuf[i++] = c; /* Get another character. */ /* 获取另外一个字符 */ c = getchar (); } while (isalnum (c)); ungetc (c, stdin); symbuf[i] = '\0'; s = getsym (symbuf); if (s == 0) s = putsym (symbuf, VAR); yylval.tptr = s; return s->type; } /* Any other character is a token by itself. */ /* 其余的字符是自己为记号的字符 */ return c; } |
这个程序既有效又灵活.
你可以轻松地加入新的函数.
而且加入于预定义数值如pi或者e也是很简单的事情.
|
[ < ] |
[ > ] |
|
[ << ] |
[ 上层 ] |
[ >> ] |
|
|
|
|
[顶层] |
[内容] |
[索引] |
[ ? ] |
向文件`math.h'中的初始列表添加新的函数.
添加另外一个包括常量和它们数值的数组.
然活修改init_table将这些常量添加到符号表.
使这些常量类型为VAR最简单.
使这个程序当用户引用一个未初始化的变量时报告一个错误.
|
[ < ] |
[ > ] |
|
[ << ] |
[ 上层 ] |
[ >> ] |
|
|
|
|
[顶层] |
[内容] |
[索引] |
[ ? ] |
Bison使用一个描述上下文无关文法的文件做为输入, 并制造一个实别改文法的正确实例的C语言函数.
Bison语法输入文件通常以`.y'结尾.参阅 调用Bison-Invoking Bison.
|
|
语法文件的整体布局 |
|
|
|
终结符与非终结符 |
|
|
|
如何编写语法规则 |
|
|
|
编写递归规则 |
|
|
|
语义值和动作 |
|
|
|
位置和动作 |
|
|
|
所有种类的Bison声明在这里讨论 |
|
|
|
将多个Bison分析器放在一个程序中 |
|
[ < ] |
[ > ] |
|
[ << ] |
[ 上层 ] |
[ >> ] |
|
|
|
|
[顶层] |
[内容] |
[索引] |
[ ? ] |
一个Bison语法文件有四个主要的部分, 就像如下所示,由恰当的分隔符分隔.
|
|
%{ Prologue %} Bison declarations %% Grammar rules %% Epilogue |
注释包含在`/* … */'之中,并且可以在任意部分出现. 做为一个GNU扩展,`//'引进了一个直到这行末尾的注释.
|
|
Prologue部分的语法和使用 |
|
|
|
Bison declarations部分的语法和使用 |
|
|
|
Grammar Rules部分的语法和使用 |
|
|
|
Epilogue部分的语法和使用 |
|
[ < ] |
[ > ] |
|
[ << ] |
[ 上层 ] |
[ >> ] |
|
|
|
|
[顶层] |
[内容] |
[索引] |
[ ? ] |
Prologue部分包括宏定义和在语法规则动作中使用的函数和变量的声明.
这些将复制到分析器文件的开头以便先于yyparse的定义.
你可以使用`#include'来从头文件获取声明.
如果你不需要任何的C声明,你可以省略这个部分的括号分隔符`%{'和`%}'.
可以有多个与Bison
declarations混合的Prologue部分.
这种做法允许你拥有相互引用的C和Bison声明.
例如,%union可以使用定义在头文件的数据类型,
并且你希望使用带有YYSTYPE类型做为参数的函数.
可以通过两个Prologue块来实现这个,
一个在%union之前,另一个在之后.
|
|
%{ #include <stdio.h> #include "ptypes.h" %} %union { long int n; tree t; /* |
|
[ < ] |
[ > ] |
|
[ << ] |
[ 上层 ] |
[ >> ] |
|
|
|
|
[顶层] |
[内容] |
[索引] |
[ ? ] |
Bison declatations部分包含了定义终结符和非终结符的声明,优先级等等. 在一些简单的语法中,可以不需要任何声明. 参阅 Bison Declarations部分-Bison Declarations.
|
[ < ] |
[ > ] |
|
[ << ] |
[ 上层 ] |
[ >> ] |
|
|
|
|
[顶层] |
[内容] |
[索引] |
[ ? ] |
Grammar Rules部分包含了一个或多个Bison语法规则. 参阅 用来表示语法规则的语法-Syntax of Grammar Rules.
在这里至少应该有一个语法规则,并且第一个`%%'(先于语法规则的那个)绝对不能省略,解释它在文件的最开头.
|
[ < ] |
[ > ] |
|
[ << ] |
[ 上层 ] |
[ >> ] |
|
|
|
|
[顶层] |
[内容] |
[索引] |
[ ? ] |
就像Prologue部分被复制到开头一样,Epilogue部分被逐字地复制到分析器文件的结尾.
如果你想放一些代码却没必要放在yyparse的定义之前,这里是最方便的地方.
例如,yylex和yyerror的定义就经常放在这里.
因为C语言要求函数在使用之前必须声明.
你经常需要在Prologue部分声明类似yylex和yyerror的函数,
即使你在Epilogue部分已经定义了它们.
参阅 分析器C语言接口-Parser
C-Language Interface.
如果最后一部分为空,你可以省略分隔它的分隔符`%%'.
Bison分析器自己包含了许多以`yy'和`YY'开头宏和标识符的定义. 所以在Epilogue部分避免使用这种类型的名字(出了这个文档讨论的之外)是一个好主意.
|
[ < ] |
[ > ] |
|
[ << ] |
[ 上层 ] |
[ >> ] |
|
|
|
|
[顶层] |
[内容] |
[索引] |
[ ? ] |
Bison语法中的符号(Symbols)代表着语言的语法类型.
一个终结符(terminal
symbol)(也被称做符号类型(token
type)代表了一类从构造上等价的记号.
你在语法中使用符号的意思就是一个这种类型的记号是允许的.
Bison分析器将符号表示为数字码.
yylex返回一个记号类型来指明一个被读入的记号是什么类型的.
你不需要了解那个码的数值是多少;
你使用代表它的符号就可以了.
一个非终结符(noterminal symbol)代表一类从构造上等价的组. 符号名称用于编写语法规则. 按照惯例,所有的非终结符都应该是小写的.
符号名称可以是字母,数字(不在开头),下划线和句点. 句点只在非终结符中有意义.
在语法中书写终结符有三种方法:
一个命名符号类型(named
token type)用类似C语言的标识符书写.
按照惯例,它们应该是大写字母.
每一个这种名称必须由一个Bison声明%token定义.
参阅 符号类型的名称-Token
Type Names.
一个字符记号类型(character
token type(或者文字字符记号(literal
character token) 用如同C语言字符常量相同的语法书写;
例如,'+'是一个字符记号类型.
除非你要指明字符记号类型的语义值类型(参阅语义值的数据类型-Data
Types of Semantic Values一章),
结合性或优先级(参阅操作符优先级-Operator
Precedence一章),
否则没有必要声明它们.
按照惯例,一个字符记号类型只用于表示一个由特定字符组成的记号.
因此,记号类型'+'用于将字符`+'表示为一个记号.
没有对此惯例的强制要求,
但是如果你不按照惯例做,
你的程序会使其它的读者感到困惑.
所有常用的C语言的字符转义序列都可以在Bison中使用,
但是你不能使用一个空字符作为一个字符文字.
因为它的数字码是0,这表示输入结束.(参阅yylex的调用惯例-Calling
Convention for yylex一章).
并且,不像标准C 三字符词(trigraphs)(注:由"??"开头的九种转义,为了在缺少标准C标点的宿主上使用标准C,可参考标准C文档)
在Bison中并没有特殊意义并且反斜杠换行也是不允许的.
一个文字串记号(literal
string token)用类似C语言中的字符串常量来书写;
例如,"<="是一个文字串记号.
除非你要指明文字串记号的语义值类型(参阅值类型-Value
Type一章),
结合性或优先级(参阅优先级-Precedence一章),你没有必要声明它们.
你可以使用%token(参阅符号声明-Token
Declarations一章)将文字串记号关联一个符号名称作为别名.
如果你不这样做,词法分析器必须从yytname表中重新找到文字串记号的代码.
(参阅调用惯例-Calling
Convention一章).
警告: 文字串记号在Yacc中不能工作.
按照惯例,一个文字串记号只用于表示一个由特定串构成的记号.
因此,你用该使用类型"<="表示作为记号的字符串`<='.
Bison并没有强制要求这种管理,但是如果你偏离了惯例,
阅读你程序的人会感到困惑.
所有常用的C语言的字符转义序列都可以在Bison中使用,
但是你不能使用一个空字符作为一个字符文字.
因为它的数字码是0,这表示输入结束.
(参阅 yylex的调用惯例-Calling
Convention for yylex.) 并且,不像标准C,
三字符词(trigraphs)(注:由"??"开头的九种转义,为了在缺少标准C标点的宿主上使用标准C,可参考标准C文档)
在Bison中并没有特殊意义并且反斜杠换行也是不允许的.
一个文字串记号必须包括两个或更多个字符;
对于进包含一个字符的记号,你应该使用字符记号(参考以上).
怎样选择终结符的写法对于它的语法意义没有影响. 它只依赖于它出现在规则的什么地方以及什么时候分器起函数返回那个符号.
yylex的返回值通常是终结符,除非返回一个代表结束输入的0或者负值.
无论你采用那种方法在语法规则中书写符号类型,
你应该在yylex的定义中采用相同的写法.
单字符符号类型的数字码简单地就是那个字符的正数编码,
所以,即使当char是有符号时你需要将它转换成unsigned
char来避免主机上符号扩展
yylex仍可以使用相同的值来产生必要的代码.
每一个命名记号类型在分析器文件中变为一个C宏,
所以yylex可以使用名称代表那个编码.
(这就是为什么句点在终结符中不起作用.)
参阅 yylex的调用惯例-Calling
Convention for yylex.
如果yylex是在另外的文件中定义的,
你需要安排符号类型的宏定义在那里是可见的.
在你运行Bison的时候使用`-d'选项
以便让它将这些宏定义写入一个另外的头文件`name.tab.h'.
你可以将它加入其它需要它的文件.
参阅 调用Bison-Invoking
Bison.
如果你要编写一个可以移植到任何标准C宿主上的语法, 你必须只能从基本标准C字符集中选择使用非零字符记号类型. 这个字符集由10个数字,52个大小写英文字母,和在下列C语言字符串中的字符构成的:
|
|
"\a\b\t\n\v\f\r !\"#%&'()*+,-./:;<=>?[\\]^_{|}~"
|
yylex函数和Bison必须为字符记号使用一个一致的字符集和编码.
例如,如果你在ASCII环境中运行Bison,
但是在不兼容的例如EBCDIC的环境中编译和运行最终的程序,
最终程序可能不会工作.因为Bison产生的表格将字符记号假定为ASCII数字值.
发布带有Bison在ASCII环境中产生的C源文件的软件是标准的做法,
所以在与ASCII不兼容的平台的安装器必须在编译它们之前重新构建这些文件.
符号error是一个保留用作错误恢复的终结符(参阅错误恢复-Error
Recovery一章);
你不应该为了其它的目的而使用它.
特别地,yylex永远不应该返回这个值(注:error).
除非你明确地在声明中赋予一个你的记号值为256,否则error记号的默认值是256.
|
[ < ] |
[ > ] |
|
[ << ] |
[ 上层 ] |
[ >> ] |
|
|
|
|
[顶层] |
[内容] |
[索引] |
[ ? ] |
一个Bison语法规则通常有如下的下形式:
|
|
result: components… ; |
reault所在是这个规则描述的非终结符而components 是被这个规则组合在一起的多种终结符和非终结符.(参阅符号-Symbols一章)
例如:
|
|
exp: exp '+' exp ; |
表明两组exp类型和一个`+'记号在中间,
可以结合成一个更大的exp类型组.
规则中的空白只用来分隔符号.你可以在你希望的地方添加额外的空白.
决定规则的语义的动作可以分散在部件中.一个动组看起来是这样:
|
|
{C statements}
|
通常只有一个动作跟随着部件. 参阅 动作-Actions.
result的多种规则可以分别书写或者由垂直条`|'按如下的方法连接起来:
在这种方式下依然有我们之特考虑的特殊规则.
如果一个规则的components为空,它意味着result可以匹配空字符串.
例如,这就是一个定一个由逗号分隔的0个或多个exp组:
|
|
expseq: /* empty */ /* 空 */ | expseq1 ; expseq1: exp | expseq1 ',' exp ; |
我们通常对每个没有部件的规则加上一个`/* empty */'的注释.
|
[ < ] |
[ > ] |
|
[ << ] |
[ 上层 ] |
[ >> ] |
|
|
|
|
[顶层] |
[内容] |
[索引] |
[ ? ] |
一个规则被称为递归的(recursive)当它的result非终结符也出现在它的右手边. 因为这种方法是唯一可以定义一个特定事物的任意数字序列的方法, 几乎所有的Bison语法都需要使用递归. 考虑这个逗号分隔的一个或者多个表达式的递归定义:
|
|
expseq1: exp | expseq1 ',' exp ; |
由于expseq1的递归使用是在有手端的最左符号,
我们称这种递归为左递归(left
recursion). 相反地,这里有一个相同地使用右递归(right
recursion)的定义.
|
|
expseq1: exp | exp ',' expseq1 ; |
任意序列都可以使用作递归或者右递归定义, 但是通常你应该使用左递归, 因为它可以使用空间固定的栈来分析任意个数的元素序列. 由于即使规则只应用一次,在这之前,所有元素也都必须被移进到栈中, 右递归使用的Bison栈空间与序列中的元素个数成正比. 参阅 Bison分析器算法-The Bison Parser Alogorithm.获得这方面的深入解释.
间接(Indirect)或者相互(mutual)递归当规则的结果没有在它的右手端出现 但是出现在其它右手端的非终结符规则的右手端时候发生.
例如:
|
|
expr: primary | primary '+' primary ; primary: constant | '(' expr ')' ; |
定义了两个相互递归的非终结符,因为它们互相引用.
|
[ < ] |
[ > ] |
|
[ << ] |
[ 上层 ] |
[ >> ] |
|
|
|
|
[顶层] |
[内容] |
[索引] |
[ ? ] |
语言的语法规则之决定了语言的语法. 语义值却是由与多种记号和组相关联的语义值和当各种组被识别时执行的动作决定的.
例如,正是由于对每个表达式关联了正确的数值,计算器才可以正确的计算. 因为组`x + y'的动作是将x和y相关联的值相加, 所以计算器能正确地计算加法
|
|
为所有的语义值指定一个类型. |
|
|
|
指定多种可选的数据类型. |
|
|
|
动作是一个语法规则的语义定义. |
|
|
|
为动作指定一个要操作的数据类型. |
|
|
|
多数动作在规则之后, 这一节讲述什么时候以及为什么要使用规则中间动作的特例. |
|
[ < ] |
[ > ] |
|
[ << ] |
[ 上层 ] |
[ >> ] |
|
|
|
|
[顶层] |
[内容] |
[索引] |
[ ? ] |
在一个简单的程序中,对所有的语言结构的语义值使用同一个数据类型就足够用了. 在RPN和中缀计算器的例子中的确是这样.(参阅逆波兰记号计算器-Reverse Polish Notation Calculator一章).
Bison默认是对于所有语义值使用int类型.
如果要指明其它的类型,可以像这样将YYSTYPE定义成一个宏:
|
|
#define YYSTYPE double
|
这个宏定义比喻在语法文件的Prologue部分. (参阅Bison语法大纲-Outline of a Bison Grammar一章)
|
[ < ] |
[ > ] |
|
[ << ] |
[ 上层 ] |
[ >> ] |
|
|
|
|
[顶层] |
[内容] |
[索引] |
[ ? ] |
在大多数程序中,你需要对不同种类的记号和组使用不同的数据类型.
例如,一个数字常量可能需要类型int或long
int, 而一个字符常量可能许需要类型char
*, 并且一个标识符需要一个指向符号表项的指针做为其语义值的类型.
为了在一个分析器中使用多种语义值类型, Bison要求你做两件事情:
使用Bison声明%union指明全部可能的数据类型集.
(参阅值类型集-The
Collections of Value Types一章).
从这些类型中为每个符号(终结符或者非终结符)选择一个做为其语义值类型.
要做到这些,可以对记号使用Bison声明%token(参阅符号类型的名称-Token
Type Names一章);
并且对组使用Bison声明%type(参阅非终结符-Nonterminal
Symbols一章)
|
[ < ] |
[ > ] |
|
[ << ] |
[ 上层 ] |
[ >> ] |
|
|
|
|
[顶层] |
[内容] |
[索引] |
[ ? ] |
当一个规则的实例被识别的时候, 同这个规则关联的包含C代码的动作会被执行. 大多数动作是用来从记号的语义值或者更小组的语义值计算整个组的语义值.
一个动作由包含在大括号之内的几个C语句构成,很像一个C语句块. 一个动作可以包含任意数量的C语句. 然而,Bison并不搜索三字符词(trigraphs), 所以如果你的代码中使用了三字符词,你要保证它不影响嵌套的括号或者注释的边界,字符串或单个字符.
一个动作可以安放在规则的任意部分; 它就在那个位置执行. 大多数规则仅有一个在规则所有部件最后的动作. 在规则之中的动作是富有技巧性的并且仅用于特殊的目的 (参阅在规则中间的动作-Actions in Mid-Rule一章).
动作中的C代码可以使用结构$n来引用匹配规则的部件的语义值.
这个结构代表了第n个部件的值.
正在被构建的组的语义值为$$.
当这些被复制到分析器文件的时候,Bison负责将这些结构翻译成适当类型的表达式.
$$被翻译成一个可以修改的左值以便可以对它赋值.
这里是一个典型的例子:
|
|
exp: … | exp '+' exp { $$ = $1 + $3; } |
这个规则从两个由加号连接的稍小的exp组构建一个exp.
在这个动作中,$1和$3代着两个部件exp组的语义值.
它们是规则右手端第一个和第三个符号.
和被存储到$$以便成为刚刚被规则识别的加法表达式的语义值.
如果`+'记号有一个有用的语义值,可以通过$2引用它.
注意到垂直杠字符`|'的确是一个分隔符, 并且动作只被附加到一个单一的规则上. 这是一点和Flex工具不同的地方. 在Flex中,`|'既代表"或者"也能代表"与下一个规则有着相同的动作". 在下面的例子中,动作仅在`b'被发现的时候触发.
|
|
a-or-b: 'a'|'b' { a_or_b_found = 1; };
|
如果你并没有指明一个规则的动作,Bison提供了默认的动作:
$$ = $1. 因此,第一个符号的值变成了整个规则的值.
当然,仅当它们的数据类型相同时,默认动作才是有效的.
对于一个空规则,并没有有意义的默认动作;
除非这个规则的值无关紧要,否则每个空规则必须有明确的动作.
在<