【问题标题】:Code substitution (a la #define). Lexer? or Parser?代码替换(la #define)。词法分析器?还是解析器?
【发布时间】:2014-01-16 22:08:06
【问题描述】:

tl;dr:如何在不执行预处理步骤的情况下使用 jison 模拟 C 的 #define 等效项?

我正在研究一种相对简单的语法,该语法具有为代码块分配标识符的功能,为了简洁起见,以后可以重复使用该标识符。示例:

# Valid grammar with various elements of different types
foo x.3 y.4 z.5

# Assign an id to a chunk of code. Everything after -> is assigned to id
fill_1-> bar a.1 b.2 c.3

# Use chunk of code later
# Should be equivalent to parsing: "baz d.4 bar a.1 b.2 c.3 e.5"
baz d.4 ["fill_1"] e.5

到目前为止,我的解析器设置已正确识别代码的分配行并将“->”右侧的部分存储在可用于其他解析器操作的字典中。与下面提供的定义操作相关的代码:

// Lexer
HSPC                      [ \t]
ID                        [a-zA-Z_][a-zA-Z0-9_]*
%%
{ID}{HSPC}*"->"       {
                        this.begin("FINISHLINE"); 
                        yytext = yytext.replace("->", "").trim();
                        return "DEFINE";
                      }

('"'{ID}'"')          { 
                        yytext = yytext.slice(1,-1);
                        return "QUOTED_ID"; 
                      }

<FINISHLINE>.*        {
                        this.begin("INITIAL");
                        yytext = yytext.trim();
                        return "REST_OF_LINE";
                      }

%%

// Parser
statement
  : "[" QUOTED_ID "]"
    { $$ = (defines[$2] ? defines[$2] : ""); }
  | DEFINE  REST_OF_LINE
    { 
      defines[$1] = $2;
    }
  ;

%% 
var defines = {};

如何让 jison 真正标记和解析保存的 sn-p 代码?我需要采用 AST 方法吗?有没有办法将代码注入解析器?这应该发生在词法分析阶段还是解析阶段?希望听到可以通过简短示例 sn-ps 采取的多种策略。

谢谢!

【问题讨论】:

  • 顺便说一句:您在宏调用中使用双引号的语法令人困惑。特别是,它看起来很像普通人按照惯例认为的文本字符串,他们会误认为它是这样的。鼓励这种错误毫无意义。我会选择不同的语法。在设计供他人使用的东西时,您希望使用最少意外原则
  • @IraBaxter 我认为你的观点很好,我会考虑更新该语法。

标签: parsing bison lex flex-lexer jison


【解决方案1】:

如果“采用 AST 方法”,您的意思是“为原始未替代程​​序和替代程序构建 AST,然后将它们拼接在一起”,那么您将遇到困难。无法保证您的替换字符串与语法中的任何有效非终结符匹配,因此为它构建树并不容易。替换之前的主程序也极不可能被您的完整语法解析。 [您可以通过构建子字符串解析器和使用树片段粘合进行魔法来克服这些困难,但是会做很多工作[我们正在为 C 预处理器分析器做类似的事情],我怀疑 ANTLR 对您有多大帮助]。

通常的方法是让词法分析器保留一堆部分读取的输入流,底部流是主程序,嵌套流对应于部分读取的宏调用(如果有一个宏,则需要多个可以调用另一个。当然你的语言允许“fill2 -> x.1 [fill1] y.3”?这意味着词法分析器必须:

  • 识别宏定义,因此可以访问名称和宏内容之间的映射
  • 识别宏调用(不干扰词法分析或解析状态;通常这意味着宏调用必须由词法分析器中的临时机器识别)
  • 在流​​堆栈上为调用的宏“推送”一个输入流
  • 到达流末尾时“弹出”流堆栈

您可能有一天会决定在宏中需要参数。您通常也可以将它们实现为流。

您可以想象对标记进行词法分析并存储标记流而不是文本作为宏主体;那么宏调用检测和正文插入可能发生在词法分析器之后和解析器之前。由于它们两者之间可能存在一个接口,因此在两者之间放置代码来管理它似乎是一种实用的方法。如果您的语言允许在程序的不同位置以不同方式解释相同的字符流,则可能会出现复杂情况;在这种情况下,宏捕获如何知道如何对宏内容进行 lex 分析?

我对 ANTLR3 的了解不够(甚至很多),无法详细告诉您如何实现这一点。

【讨论】:

  • 查看我的后续回答。我认为你提出了很好的观点。我的要求有点宽松,因为定义的有效语法块在您使用它的任何地方都有效(只要您在有效位置调用宏)。我的宏上的 RE 参数,我实际上实现了一个“乘法/重复”运算符,例如["macro_1"]*3 允许您多次重复一段代码。超级有帮助。
【解决方案2】:

所以在进一步阅读之后,这是一种不需要预处理的潜在方法。

我现在在我的解析器中为不同类型的节点生成一个抽象语法树。我的 AST 由各种类组成,一些代表基本单元,另一些代表元单元。我的“宏”用它们自己的 AST 元素表示,它包含对已解析 AST 元素集合的引用。因此,我没有进行代码替换然后多次解析的“代码替换”,而是一次解析定义的块并存储对创建的 AST 元素的引用。例如:

# fundamental units
class TabElement
# subclasses of TabElement
class NoteElement
class SlideElement
class MuteElement

# meta element: holds a collection of TabElements's
class NoteGroupElement
# meta element: holds a collection of TabElements's and otherstatements, has an ID
#               the collection is accessible via a global dict to other elements
class PredefElement
# meta element: represents a usage of a PredefElement
class PredefInvokeElement

用于执行此操作的缩写词法分析器和解析器规则如下所示(省略了很多无关的东西,希望你能明白):

/* LEXER */
<INITIAL>[\s]             /* ignore whitespace */
{ID}{HSPC}*"->"       {
                        this.begin("DEFINE"); 
                        yytext = yytext.replace("->", "").trim();
                        return "DEFINE";
                      }
/* using a pre-def */
('"'{ID}'"')|("'"{ID}"'") { 
                        yytext = yytext.slice(1,-1);
                        return "QUOTED_ID"; 
                      }

/* When defining a code chunk.  Newlines delimit the end of the definition */
<DEFINE>{HSPC}            /* ignore horizontal whitespace */
<DEFINE>({NL}|<<EOF>>)    { this.begin("INITIAL"); return "NL"; }

/* PARSER */
statement
  : statement_group
    { $$ = $1; }
  | DEFINE statements NL
    { 
      defines[$1] = new ast.PredefinedElement($1, $2, @1);
      $$ = defines[$1]
    }
  | predefine_invoke
    { $$ = $1; }
  | chord
    { $1 = $1; }
  | bar_token
    { $$ = $1; }
  | REPEAT_MEASURE
    { $$ = new ast.RepeatMeasureElement(@1); }
  | REST
    { $$ = new ast.RestElement(@1); }
  | DURATION
    { $$ = new ast.DurationElement($1, @1); }
  | note_token
    { $$ = $1; }
  ;

predefine_invoke
  : "[" QUOTED_ID "]"
    %{ 
      if (defines[$2]) { $$ = new ast.PredefinedInvokeElement(defines[$2], @2); } 
      else { $$ = undefined; }
    %}
  | "[" QUOTED_ID "]" "*" INTEGER
    %{ 
      if (defines[$2]) { $$ = new ast.PredefinedInvokeElement(defines[$2], @2, { repeat: parseInt($5) }); } 
      else { $$ = undefined; }
    %}
  ;

【讨论】:

  • 请注意,我在解析 DEFINE 右侧时使用了不同的词法分析器模式来注意换行符
  • 您的解析器如何“解析”未预处理的代码?假设我的语法由“G = I '+' N”组成,其中 I 是标识符,N 是数字。尝试使用您的符号,假设我的具体文本是 fill_1 -> + 3 \n x ["fill_1"] \n 其中 \n 是换行符。我可以看到您的解析器如何获取宏定义。它如何解析 x [ "fill_1"] 部分?
【解决方案3】:

实现像 C 预处理器这样的预处理器的常用方法是让它处理词法分析器和解析器之间的标记流。因此词法分析器识别标记,将输入的字符流转换为标记流,然后预处理器对该标记流进行操作,将其转换为新的标记流,最后解析器解析该标记流。

现在,如果您使用的是 yacc/lex(或 bison/flex),这有点棘手,因为它们被设计为通过调用 yylex 的解析器直接进行通信,没有任何内容介于两者之间。使用 flex,您可以使用 YY_DECL 宏来更改它定义的 yylex 函数的声明,并插入您自己的 yylex 函数:

%{
#define YY_DECL static int token()
%}
%%
 ... lex rules
%%

int yylex() {
    int tok = token();
    if (tok == IDENTIFIER) {
        Macro *mac = find_macro(yylval.str);
        if (mac) {
            yypush_buffer_state(dummy_buffer);
            yy_scan_string(mac->definition);
            return yylex(); } }
    return tok;
}

使用 lex,您可以在适当的位置使用#define yylex token/#undef yylex 以获得相同的效果。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2016-04-04
    • 2013-01-31
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-04-07
    相关资源
    最近更新 更多