antlr 教程_ANTLR教程– Hello Word

这篇ANTLR教程从一个简单的'Hello Word!'语言解析示例开始,介绍ANTLR的工作原理,逐步展示如何构建和测试解析器,以及如何在ANTLR中实现错误处理。文章涵盖了从ANTLR生成代码、创建抽象语法树到在解析过程中处理错误的各种方面,适用于想要学习ANTLR的开发者。
摘要由CSDN通过智能技术生成

antlr 教程

Antlr代表另一种语言识别工具。 该工具能够为任何计算机语言生成编译器或解释器。 除了明显的用途(例如需要解析一种真正的“大型”编程语言,例如Java,PHP或SQL)外,它还可以帮助执行更小,更常见的任务。

每当您需要评估编译时未知的表达式,或者解析奇怪的用户输入或文件时,这都是有用的。 当然,可以为任何这些任务创建定制的手工分析器。 但是,这通常需要更多的时间和精力。 对良好的解析器生成器的一点了解可能会将这些耗时的任务变成简单而又快速的练习。

这篇文章从ANTLR有用性的一个小例子开始。 然后,我们解释什么是ANTLR以及它如何工作。 最后,我们展示如何编译一个简单的“ Hello word!” 语言转换成抽象语法树。 该帖子还说明了如何添加错误处理以及如何测试语言。

下一篇文章展示了如何创建一种真实的表达语言。

实词示例

ANTLR在开源单词中似乎很流行。 其中, Apache CamelApache LuceneApache HadoopGroovyHibernate都使用它 。 他们都需要用于自定义语言的解析器。 例如,Hibernate使用ANTLR解析其查询语言HQL。

所有这些都是大型框架,因此与小型应用程序相比,它们更可能需要特定领域的语言。 使用ANTLR的较小项目的列表可在其展示列表中找到 。 我们还找到了关于该主题的一个stackoverflow讨论。

若要查看ANTLR在哪里有用以及如何节省时间,请尝试估算以下要求:

  • 将公式计算器添加到会计系统中。 它将计算公式的值,例如(10 + 80)*sales_tax
  • 将扩展的搜索字段添加到配方搜索引擎中。 它将搜索匹配表达式的收据,例如(chicken and orange) or (no meat and carrot)

我们的安全评估需要一天半的时间,其中包括文档,测试以及与项目的集成。 如果您面临类似的要求并且做出了更高的估计,那么ANTLR值得一看。

总览

ANTLR是代码生成器。 它以所谓的语法文件作为输入,并生成两个类:lexer和parser。

Lexer首先运行,然后将输入分成称为令牌的片段。 每个令牌代表或多或少有意义的输入。 标记流被传递到解析器,解析器完成所有必要的工作。 解析器负责构建抽象语法树,解释代码或将其转换为其他形式。

语法文件包含ANTLR生成正确的词法分析器和解析器所需的所有内容。 它是否应该生成Java或python类,解析器是否生成抽象语法树,汇编代码或直接解释代码等。 正如本教程显示如何构建抽象语法树一样,在以下说明中我们将忽略其他选项。

最重要的是,语法文件描述了如何将输入分为令牌以及如何从令牌构建树。 换句话说,语法文件包含词法分析器规则和解析器规则。

每个词法分析器规则描述一个令牌:

TokenName: regular expression;

解析器规则更加复杂。 最基本的版本类似于lexer规则中的版本:

ParserRuleName: regular expression;

它们可能包含修饰符,这些修饰符在结果抽象语法树中指定输入,根和子元素上的特殊转换,或在使用规则时执行的操作。 几乎所有工作通常都在解析器规则内完成。

基础设施

首先,我们展示使ANTLR开发更容易的工具。 当然,本章中所描述的内容都不是必需的。 所有示例仅适用于maven,文本编辑器和Internet连接。

ANTLR项目制作了独立的IDEEclipse插件Idea插件 。 我们没有找到NetBeans插件。

ANTLRWorks

独立的想法称为ANTLRWorks 。 从项目下载页面下载它。 ANTLRWorks是单个jar文件,请使用java -jar antlrworks-1.4.3.jar命令运行它。

IDE具有更多功能,并且比Eclipse插件更稳定。

Eclipse插件

从ANTLR 下载页面下载并解压缩ANTLR v3。 然后,从Eclipse Marketplace安装ANTLR插件:

转到“首选项”并配置ANTLR v3安装目录:

要测试配置,请下载示例语法文件并在eclipse中打开它。 它将在ANTLR编辑器中打开。 编辑器具有三个选项卡:

  • 语法–具有语法突出显示,代码完成等功能的文本编辑器。
  • 解释器–将测试表达式编译成语法树,可能会产生与生成的解析器不同的结果。 它倾向于在正确的表达式上抛出失败的谓词异常。
  • 铁路视图–绘制您的词法分析器和解析器规则的漂亮图形。

空项目– Maven配置

本章说明如何将ANTLR添加到Maven项目中。 如果您使用Eclipse且尚未安装m2eclipse插件,请从http://download.eclipse.org/technology/m2e/releases更新站点进行安装。 这将使您的生活更加轻松。

建立专案

创建新的Maven项目,并在“选择原型”屏幕上指定maven-archetype-quickstart。 如果不使用Eclipse,则命令mvn archetype:generate可以达到相同的目的。

相依性

将ANTLR依赖项添加到pom.xml中

org.antlr
    
    
    
     antlr
    
    
    
     3.3
    
   
    
     jar
    
    
    
     compile

注意:由于ANTLR没有向后兼容的历史记录,因此最好指定所需的版本。

外挂程式

Antlr maven插件在generate-sources阶段运行,并从语法(.g)文件生成lexer和parser java类。 将其添加到pom.xml中

org.antlr
   
  
   
    antlr3-maven-plugin
   
  
   
    3.3
   
  
   
    
    
      
     
      run antlr
     
      
     
      generate-sources
     
      
     
        
      
       antlr

创建src/main/antlr3文件夹。 该插件希望其中包含所有语法文件。

生成的文件放在target/generated-sources/antlr3目录中。 由于此目录不在默认的maven构建路径中,因此我们使用build-helper-maven-plugin将其添加到该目录中:

org.codehaus.mojo
   
  
   
    build-helper-maven-plugin
   
  
   
    
    
      
     
      add-source
     
      
     
      generate-sources
     
      
     
        
      
       add-source
      
      
     
      
     
        
      
          
       ${basedir}/target/generated-sources/antlr3

如果使用eclipse,则必须更新项目配置:右键单击项目->'maven'->'更新项目配置'。

测试一下

调用maven以测试项目配置:右键单击项目->'Run As'->'Maven generate-sources'。 或者,使用mvn generate-sources命令。

构建应该成功。 控制台输出应包含antlr3-maven-plugin插件输出:

[INFO] --- antlr3-maven-plugin:3.3:antlr (run antlr) @ antlr-step-by-step ---
[INFO] ANTLR: Processing source directory C:\meri\ANTLR\workspace\antlr-step-by-step\src\main\antlr3
[INFO] No grammars to process
ANTLR Parser Generator  Version 3.3 Nov 30, 2010 12:46:29

它之后应该是build-helper-maven-plugin插件输出:

[INFO] --- build-helper-maven-plugin:1.7:add-source (add-source) @ antlr-step-by-step ---
[INFO] Source directory: C:\meri\ANTLR\workspace\antlr-step-by-step\target\generated-sources\antlr3 added.

此阶段的结果位于github上,标记为001-configured_antlr

你好字

我们将创建最简单的语言解析器– hello word解析器。 它通过一个表达式构建一个小的抽象语法树:“ Hello word!”。

我们将使用它来显示如何创建语法文件并从中生成ANTLR类。 然后,我们将展示如何使用生成的文件并创建单元测试。

第一个语法文件

Antlr3-maven-plugin在src/main/antlr3目录中搜索语法文件。 它使用语法为每个子目录创建新程序包,并在其中生成解析器和词法分析器类。 由于我们希望将类生成到org.meri.antlr_step_by_step.parsers包中,因此我们必须创建src/main/antlr3/org/meri/antlr_step_by_step/parsers目录。

语法名称和文件名必须相同。 文件必须带有.g后缀。 此外,每个语法文件都以语法名称声明开头。 我们的S001HelloWord语法从以下几行开始:

grammar S001HelloWord;

声明之后始终是生成器选项。 我们正在研究Java项目,希望将表达式编译成抽象语法树:

options {
    // antlr will generate java lexer and parser
    language = Java;
    // generated parser should create abstract syntax tree
    output = AST;
}

Antlr不会在生成的类之上生成包声明。 我们必须使用@parser::header@lexer::header块来实施它。 标头必须遵循选项块:

@lexer::header {
  package org.meri.antlr_step_by_step.parsers;
}

@parser::header {
  package org.meri.antlr_step_by_step.parsers;
}

每个语法文件必须至少具有一个词法分析器规则。 每个词法分析器规则必须以大写字母开头。 我们有两个规则,第一个定义一个称呼令牌,第二个定义一个endsymbol令牌。 称呼必须为“ Hello word”,且结尾符号必须为“!”。

SALUTATION:'Hello word';   
ENDSYMBOL:'!';

同样,每个语法文件必须至少具有一个解析器规则。 每个解析器规则必须以小写字母开头。 我们只有一个解析器规则:我们语言中的任何表达式都必须由称呼后跟一个结尾符号组成。

expression : SALUTATION ENDSYMBOL;

注意:语法文件元素的顺序是固定的。 如果更改它,则antlr插件将失败。

生成词法分析器和解析器

使用mvn generate-sources命令或从Eclipse从命令行生成词法分析器和解析器:

  • 右键单击该项目。
  • 点击“运行方式”。
  • 单击“ Maven生成源”。

Antlr插件将创建target / generated-sources / antlr / org / meri / antlr_step_by_step / parsers文件夹,并将S001HelloWordLexer.java和S001HelloWordParser.java文件放入其中。

使用Lexer和Parser

最后,我们创建编译器类。 它只有一种公共方法,该方法:

  • 调用生成的词法分析器将输入拆分为令牌,
  • 调用生成的解析器以根据令牌构建AST,
  • 将结果AST树打印到控制台中,
  • 返回抽象语法树。

编译器位于S001HelloWordCompiler类中:

public CommonTree compile(String expression) {
    try {
      //lexer splits input into tokens
      ANTLRStringStream input = new ANTLRStringStream(expression);
      TokenStream tokens = new CommonTokenStream( new S001HelloWordLexer( input ) );
  
      //parser generates abstract syntax tree
      S001HelloWordParser parser = new S001HelloWordParser(tokens);
      S001HelloWordParser.expression_return ret = parser.expression();
  
      //acquire parse result
      CommonTree ast = (CommonTree) ret.tree;
      printTree(ast);
      return ast;
    } catch (RecognitionException e) {
      throw new IllegalStateException("Recognition exception is never thrown, only declared.");
  }

注意:不必担心在S001HelloWordParser.expression()方法上声明的RecognitionException异常。 它永远不会被抛出。

测试它

在本章结束时,我们将使用一个针对新编译器的小测试用例。 创建S001HelloWordTest类:

public class S001HelloWordTest {
 /**
  * Abstract syntax tree generated from "Hello word!" should have an 
  * unnamed root node with two children. First child corresponds to 
  * salutation token and second child corresponds to end symbol token.
  * 
  * Token type constants are defined in generated S001HelloWordParser 
  * class.
  */
 @Test
 public void testCorrectExpression() {
  //compile the expression
  S001HelloWordCompiler compiler = new S001HelloWordCompiler();
  CommonTree ast = compiler.compile("Hello word!");
  CommonTree leftChild = ast.getChild(0);
  CommonTree rightChild = ast.getChild(1);

  //check ast structure
  assertEquals(S001HelloWordParser.SALUTATION, leftChild.getType());
  assertEquals(S001HelloWordParser.ENDSYMBOL, rightChild.getType());
 }

}

测试将成功通过。 它将抽象语法树打印到控制台:

0 null
  -- 4 Hello word
  -- 5 !

IDE中的语法

在编辑器中打开S001HelloWord.g并进入解释器选项卡。

  • 在左上方视图中突出显示表达式规则。
  • 写下“你好字!” 进入右上方的视图。
  • 按左上角的绿色箭头。

解释器将生成解析树:

复制语法

本教程中的每个新语法都基于先前的语法。 我们汇总了将旧语法复制到新语法所需的步骤列表。 使用它们将OldGrammar复制到NewGrammar:

错误处理

没有适当的错误处理,没有任何任务真正完成。 生成的ANTLR类尽可能尝试从错误中恢复。 它们的确向控制台报告错误,但是没有现成的API可以以编程方式查找语法错误。

如果我们只构建命令行编译器,那可能很好。 但是,假设我们正在为我们的语言构建GUI,或将结果用作其他工具的输入。 在这种情况下,我们需要对所有生成的错误进行API访问。

在本章的开头,我们将尝试使用默认错误处理并为其创建测试用例。 然后,我们将添加一个简单的错误处理,只要发生第一个错误,该处理就会抛出异常。 最后,我们将转向“真实”解决方案。 它将在内部列表中收集所有错误并提供访问它们的方法。

作为副产品,本章介绍了如何:

默认错误处理

首先,我们将尝试解析各种不正确的表达式。 目的是了解默认的ANTLR错误处理行为。 我们将根据每个实验创建测试用例。 所有测试用例都位于S001HelloWordExperimentsTest类中。

表达式1Hello word?

结果树与正确的树非常相似:

0 null
  -- 4 Hello word
  -- 5 ?<missing ENDSYMBOL>

控制台输出包含错误:

line 1:10 no viable alternative at character '?'
line 1:11 missing ENDSYMBOL at '<eof>'

测试用例 :以下测试用例通过均没有问题。 不会引发异常,并且抽象语法树节点类型与正确表达式中的相同。

@Test
 public void testSmallError() {
  //compile the expression
  S001HelloWordCompiler compiler = new S001HelloWordCompiler();
  CommonTree ast = compiler.compile("Hello word?");

  //check AST structure
  assertEquals(S001HelloWordParser.SALUTATION, ast.getChild(0).getType());
  assertEquals(S001HelloWordParser.ENDSYMBOL, ast.getChild(1).getType());
 }

表情2Bye!

结果树与正确的树非常相似:

0 null
  -- 4 
  <missing>
   
  -- 5 !

  </missing>

控制台输出包含错误:

line 1:0 no viable alternative at character 'B'
line 1:1 no viable alternative at character 'y'
line 1:2 no viable alternative at character 'e'
line 1:3 missing SALUTATION at '!'

测试用例 :以下测试用例通过均没有问题。 不会引发异常,并且抽象语法树节点类型与正确表达式中的相同。

@Test
 public void testBiggerError() {
  //compile the expression
  S001HelloWordCompiler compiler = new S001HelloWordCompiler();
  CommonTree ast = compiler.compile("Bye!");

  //check AST structure
  assertEquals(S001HelloWordParser.SALUTATION, ast.getChild(0).getType());
  assertEquals(S001HelloWordParser.ENDSYMBOL, ast.getChild(1).getType());
 }

表达式3Incorrect Expression

结果树只有根节点,没有子节点:

0

控制台输出包含很多错误:

line 1:0 no viable alternative at character 'I'
line 1:1 no viable alternative at character 'n'
line 1:2 no viable alternative at character 'c'
line 1:3 no viable alternative at character 'o'
line 1:4 no viable alternative at character 'r'
line 1:5 no viable alternative at character 'r'
line 1:6 no viable alternative at character 'e'
line 1:7 no viable alternative at character 'c'
line 1:8 no viable alternative at character 't'
line 1:9 no viable alternative at character ' '
line 1:10 no viable alternative at character 'E'
line 1:11 no viable alternative at character 'x'
line 1:12 no viable alternative at character 'p'
line 1:13 no viable alternative at character 'r'
line 1:14 no viable alternative at character 'e'
line 1:15 no viable alternative at character 's'
line 1:16 no viable alternative at character 's'
line 1:17 no viable alternative at character 'i'
line 1:18 no viable alternative at character 'o'
line 1:19 no viable alternative at character 'n'
line 1:20 mismatched input '&ltEOF>' expecting SALUTATION

测试用例 :我们终于找到了一个导致树结构不同的表达式。

@Test
 public void testCompletelyWrong() {
  //compile the expression
  S001HelloWordCompiler compiler = new S001HelloWordCompiler();
  CommonTree ast = compiler.compile("Incorrect Expression");

  //check AST structure
  assertEquals(0, ast.getChildCount());
 }

Lexer中的错误处理

每个词法分析器规则“ RULE”对应于生成的词法分析器中的“ mRULE”方法。 例如,我们的语法有两个规则:

SALUTATION:'Hello word';   
ENDSYMBOL:'!';

并且生成的词法分析器有两种相应的方法

public final void mSALUTATION() throws RecognitionException {
    // ...
}

public final void mENDSYMBOL() throws RecognitionException {
    // ...
}

根据抛出的异常,lexer可能会也可能不会尝试从中恢复。 但是,每个错误都以reportError(RecognitionException e)方法结尾。 生成的词法分析器继承它:

public void reportError(RecognitionException e) {
  displayRecognitionError(this.getTokenNames(), e);
 }

结果:我们必须在lexer中更改reportError或displayRecognitionError方法。

解析器中的错误处理

我们的语法只有一个解析器规则“表达式”:

expression SALUTATION ENDSYMBOL;

该表达式对应于生成的解析器中的expression()方法:

public final expression_return expression() throws RecognitionException {
  //initialization
  try {
    //parsing
  }
  catch (RecognitionException re) {
    reportError(re);
    recover(input,re);
    retval.tree = (Object) adaptor.errorNode(input, retval.start, input.LT(-1), re);
  } finally {
  }
  //return result;
}

如果发生错误,解析器将:

  • 向控制台报告错误,
  • 从错误中恢复
  • 将错误节点(而不是普通节点)添加到抽象语法树。

解析器中的错误报告比lexer中的错误报告稍微复杂一些:

/** Report a recognition problem.
  *
  *  This method sets errorRecovery to indicate the parser is recovering
  *  not parsing.  Once in recovery mode, no errors are generated.
  *  To get out of recovery mode, the parser must successfully match
  *  a token (after a resync).  So it will go:
  *
  *   1. error occurs
  *   2. enter recovery mode, report error
  *   3. consume until token found in resynch set
  *   4. try to resume parsing
  *   5. next match() will reset errorRecovery mode
  *
  *  If you override, make sure to update syntaxErrors if you care about that.
  */
 public void reportError(RecognitionException e) {
  // if we've already reported an error and have not matched a token
  // yet successfully, don't report any errors.
  if ( state.errorRecovery ) {
   return;
  }
  state.syntaxErrors++; // don't count spurious
  state.errorRecovery = true;

  displayRecognitionError(this.getTokenNames(), e);
 }

这次我们有两个可能的选择:

  • 通过自己的处理替换解析器规则方法中的catch子句,
  • 覆盖解析器方法。

在解析器中更改捕获

Antlr提供了两种方法来更改解析器中生成的catch子句。 我们将创建两个新的语法,每个都演示一种方法。 在这两种情况下,我们都会使解析器在第一个错误时退出。

首先,我们可以将rulecatch添加到新S002HelloWordWithErrorHandling语法的解析器规则中:

expression : SALUTATION ENDSYMBOL;
catch [RecognitionException e] {
  //Custom handling of an exception. Any java code is allowed.
  throw new S002HelloWordError(":(", e);
}

当然,我们必须将S002HelloWordError异常的导入添加到headers块中

@parser::header {
  package org.meri.antlr_step_by_step.parsers;

  //add imports (see full line on Github)
  import ... .S002HelloWordWithErrorHandlingCompiler.S002HelloWordError;
}

编译器类与以前几乎相同。 它声明了新的异常:

public class S002HelloWordWithErrorHandlingCompiler extends AbstractCompiler {

  public CommonTree compile(String expression) {
    // no change here
  }

  @SuppressWarnings("serial")
  public static class S002HelloWordError extends RuntimeException {
    public S002HelloWordError(String arg0, Throwable arg1) {
      super(arg0, arg1);
    }
  }
}

然后,ANTLR将用我们自己的处理方式替换表达式规则方法中的默认catch子句:

public final expression_return expression() throws RecognitionException {
  //initialization
  try {
    //parsing
  }
  catch (RecognitionException re) {
    //Custom handling of an exception. Any java code is allowed.
    throw new S002HelloWordError(":(", e); 
  } finally {
  }
  //return result;
}

通常, 语法编译器类测试类在Github上可用。

或者,我们可以将rulecatch规则放在标题块和第一个lexer规则之间。 S003HelloWordWithErrorHandling语法演示了此方法:

//change error handling in all parser rules
@rulecatch {
  catch (RecognitionException e) {
    //Custom handling of an exception. Any java code is allowed.
    throw new S003HelloWordError(":(", e);
  }
}

我们必须将S003HelloWordError异常的导入添加到标头块中:

@parser::header {
  package org.meri.antlr_step_by_step.parsers;

  //add imports (see full line on Github)
  import ... .S003HelloWordWithErrorHandlingCompiler.S003HelloWordError;
}

编译器类与前面的情况完全相同。 ANTLR将替换所有解析器规则中的默认catch子句:

public final expression_return expression() throws RecognitionException {
  //initialization
  try {
    //parsing
  }
  catch (RecognitionException re) {
    //Custom handling of an exception. Any java code is allowed.
    throw new S003HelloWordError(":(", e); 
  } finally {
  }
  //return result;
}

同样,Github上提供了语法编译器类测试类

不幸的是,这种方法有两个缺点。 首先,它仅在解析器中不适用于lexer。 其次,默认报告和恢复功能以合理的方式工作。 它尝试从错误中恢复。 一旦开始恢复,就不会产生新的错误。 仅当解析器未处于错误恢复模式时,才会生成错误消息。

我们喜欢此功能,因此我们决定仅更改错误报告的默认实现。


将方法和字段添加到生成的类

我们会将所有词法分析器/解析器错误存储在私有列表中。 此外,我们将在生成的类中添加两个方法:

  • hasErrors –如果发生至少一个错误,则返回true,
  • getErrors –返回所有生成的错误。

在@members块内添加了新的字段和方法:

@lexer::members {
  //everything you need to add to the lexer
}

@parser::members {
  //everything you need to add to the parser
}

成员块必须放置在标题块和第一个词法分析器规则之间。 该示例的语法为S004HelloWordWithErrorHandling

//add new members to generated lexer
@lexer::members {
  //add new field
  private List<RecognitionException> errors = new ArrayList <RecognitionException> ();
  
  //add new method
  public List<RecognitionException> getAllErrors() {
    return new ArrayList<RecognitionException>(errors);
  }

  //add new method
  public boolean hasErrors() {
    return !errors.isEmpty();
  }
}

//add new members to generated parser
@parser::members {
  //add new field
  private List<RecognitionException> errors = new ArrayList <RecognitionException> ();
  
  //add new method
  public List<RecognitionException> getAllErrors() {
    return new ArrayList<RecognitionException>(errors);
  }

  //add new method
  public boolean hasErrors() {
    return !errors.isEmpty();
  }
}

生成的词法分析器生成的解析器都包含用members块编写的所有字段和方法。

覆盖生成的方法

要覆盖生成的方法,请执行与要添加新方法相同的操作,例如,将其添加到@members块中:

//override generated method in lexer
@lexer::members {
  //override method
  public void reportError(RecognitionException e) {
    errors.add(e);
    displayRecognitionError(this.getTokenNames(), e);
  }
}

//override generated method in parser
@parser::members {
  //override method
  public void reportError(RecognitionException e) {
    errors.add(e);
    displayRecognitionError(this.getTokenNames(), e);
  }
}

现在,reportError方法将覆盖lexerparser中的默认行为。

收集编译器中的错误

最后,我们必须更改编译器类。 新版本将在输入解析阶段之后收集所有错误:

private List<RecognitionException> errors = new ArrayList<RecognitionException>();

public CommonTree compile(String expression) {
  try {

    ... init lexer ...
  
    ... init parser ...
    ret = parser.expression();

    //collect all errors
    if (lexer.hasErrors())
      errors.addAll(lexer.getAllErrors());
  
    if (parser.hasErrors())
      errors.addAll(parser.getAllErrors());
  
    //acquire parse result
    ... as usually ...
  } catch (RecognitionException e) {
    ...
  }
}
  
/**
* @return all errors found during last run
*/
public List<RecognitionException> getAllErrors() {
  return errors;
}

解析器完成工作后,我们必须收集词法分析器错误。 从它调用词法分析器,之前没有任何错误。 像往常一样,我们将语法编译器类测试类放在Github上。

下载antlr分步项目的标记003-S002-to-S004HelloWordWithErrorHandling ,以查找同一java项目中的所有三种错误处理方法。

参考: ANTLR教程–我们的JCG合作伙伴 Maria Jurcovicova在This is Stuff博客上的问候语。


翻译自: https://www.javacodegeeks.com/2012/04/antlr-tutorial-hello-word.html

antlr 教程

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值