Permalink
Browse files

First commit

  • Loading branch information...
0 parents commit 9ed7da46cdb9344abd3291b29b910014761a512b @aslakhellesoy aslakhellesoy committed Jan 31, 2013
Showing with 1,405 additions and 0 deletions.
  1. +24 −0 Makefile
  2. +36 −0 README.md
  3. +6 −0 c/.gitignore
  4. +21 −0 c/Makefile
  5. +11 −0 c/README.md
  6. +98 −0 c/bool_ast.c
  7. +39 −0 c/bool_ast.h
  8. +20 −0 c/lexer.l
  9. +61 −0 c/parser.y
  10. +3 −0 java/.gitignore
  11. +9 −0 java/README.md
  12. +123 −0 java/pom.xml
  13. +51 −0 java/src/main/bison/parser.y
  14. +12 −0 java/src/main/java/bool/And.java
  15. +11 −0 java/src/main/java/bool/Binary.java
  16. +25 −0 java/src/main/java/bool/EvalVisitor.java
  17. +7 −0 java/src/main/java/bool/Expr.java
  18. +9 −0 java/src/main/java/bool/Lexer.java
  19. +26 −0 java/src/main/java/bool/LexerAdapter.java
  20. +14 −0 java/src/main/java/bool/Not.java
  21. +12 −0 java/src/main/java/bool/Or.java
  22. +7 −0 java/src/main/java/bool/ParseException.java
  23. +14 −0 java/src/main/java/bool/Var.java
  24. +11 −0 java/src/main/java/bool/Visitor.java
  25. +41 −0 java/src/main/jflex/lexer.jflex
  26. +51 −0 java/src/main/ragel/lexer.rl
  27. +40 −0 java/src/test/java/bool/LexerTest.java
  28. +33 −0 java/src/test/java/bool/ParserTest.java
  29. +3 −0 javascript/.gitignore
  30. +27 −0 javascript/Makefile
  31. +7 −0 javascript/README.md
  32. +2 −0 javascript/lib/bool.js
  33. +35 −0 javascript/lib/bool/ast.js
  34. +17 −0 javascript/lib/bool/eval_visitor.js
  35. +10 −0 javascript/lib/bool/lexer.jisonlex
  36. +22 −0 javascript/lib/bool/parser.jison
  37. +38 −0 javascript/package.json
  38. +38 −0 javascript/test/bool-test.js
  39. +18 −0 javascript/test/testdata-test.js
  40. +12 −0 ruby/.gitignore
  41. +2 −0 ruby/Gemfile
  42. +22 −0 ruby/README.md
  43. +48 −0 ruby/Rakefile
  44. +17 −0 ruby/bool.gemspec
  45. +21 −0 ruby/ext/bool_ext/extconf.rb
  46. +56 −0 ruby/ext/bool_ext/ruby_bool.c
  47. +1 −0 ruby/lib/.gitignore
  48. +23 −0 ruby/lib/bool.rb
  49. +55 −0 ruby/lib/bool/ast.rb
  50. +28 −0 ruby/lib/bool/eval_visitor.rb
  51. +69 −0 ruby/spec/bool_spec.rb
  52. +13 −0 ruby/spec/testdata_spec.rb
  53. +3 −0 testdata/a.txt
  54. +3 −0 testdata/b.txt
24 Makefile
@@ -0,0 +1,24 @@
+all: c java javascript ruby jruby
+
+c:
+ cd c && make
+
+java:
+ cd java && mvn package
+
+javascript:
+ cd javascript && make
+
+ruby:
+ cd ruby && rake
+
+jruby:
+ cd ruby && jruby -S rake
+
+clean:
+ cd c && make clean
+ cd java && mvn clean
+ cd javascript && make clean
+ cd ruby && rake clean
+
+.PHONY: all c java javascript ruby jruby clean
36 README.md
@@ -0,0 +1,36 @@
+# bool
+
+This is a cross-platform library for parsing boolean arithmetic expressions like `a && b && (!c || !d)` and evaluating them by assigning values to the variables.
+
+Boolean expressions are parsed into an [abstract syntax tree](http://en.wikipedia.org/wiki/Abstract_syntax_tree) (AST) using a
+lexer/parser generated by various Lex/Yacc clones for the various programming languages. This allows grammars to be fairly similar
+across languages.
+
+Evaluation of the boolean expressions is done by traversing the AST with a visitor. (This is obviously overkill for something as
+simple as boolean expressions, keep reading to understand why).
+
+Supported platforms are Ruby, JRuby, Java and JavaScript. The Ruby gem uses a C extension (for speed) and the JRuby gem uses a Java
+extension (also for speed). Support for e.g. Python could be added easily by using the same C code as the Ruby gem. Java programs (or
+any other JVM-based program such as Scala or Clojure) can also use the Java library as-is.
+
+## Why?
+
+The purpose of this library is twofold.
+
+First, it serves as a simple example of how to build a custom interpreted language with a fast lexer/parser that build a
+visitor-traversable AST, and that runs on many different platforms. People who want to build a bigger cross-platform language could
+leverage the structure and build files in this project. A new [Gherkin](https://github.com/cucumber/gherkin) 3.0 project may use this
+project as a template.
+
+Second, this project actually has a use. The Cucumber project may use it to evaluate _tag expressions_ as current Cucumber
+implementations don't have a proper parser for this.
+
+## Building for all platforms
+
+If you're lucky and already have all the needed software installed you can just run
+
+```
+make
+```
+
+If that fails for you (it probably will the first time), don't worry. See the individual platform-specific READMEs.
6 c/.gitignore
@@ -0,0 +1,6 @@
+*.o
+*.a
+lexer.h
+lexer.c
+parser.h
+parser.c
21 c/Makefile
@@ -0,0 +1,21 @@
+OBJECTS = lexer.o parser.o bool_ast.o
+
+all: libbool.o libbool.a
+
+libbool.a: $(OBJECTS)
+ ar rcs $@ $(OBJECTS)
+
+libbool.o: $(OBJECTS)
+ $(CC) -shared -o $@ $(OBJECTS) -lfl
+
+lexer.o: parser.o
+nodes.o: nodes.h
+
+lexer.h lexer.c: lexer.l
+ flex lexer.l
+
+parser.h parser.c: parser.y
+ bison parser.y
+
+clean:
+ rm -f lexer.h lexer.c parser.h parser.c *.o *.a
11 c/README.md
@@ -0,0 +1,11 @@
+## C
+
+You need `make`, a C compiler, `flex` and `bison` on your `PATH`. Cd into `c` and run
+
+```
+make
+```
+
+This should create a shared library, but no executable. The C library only contains a lexer, parser and a simple AST.
+It does not implement the visitor that traverses the AST that evaluates the parsed expression. This is done in the ruby extension,
+and a future Python extension could do the same.
98 c/bool_ast.c
@@ -0,0 +1,98 @@
+#include "bool_ast.h"
+#include "parser.h"
+#include "lexer.h"
+
+void yyerror(yyscan_t scanner, Node** node, const char* msg) {
+ //fprintf(stderr,"Error: %s\n", msg);
+}
+
+Node* parse_bool_ast(const char* source) {
+ Node* node;
+ yyscan_t scanner;
+ YY_BUFFER_STATE state;
+
+ if (yylex_init(&scanner)) {
+ // couldn't initialize
+ return NULL;
+ }
+
+ // TODO: Check state here?
+ state = yy_scan_string(source, scanner);
+
+ if (yyparse(&node, scanner)) {
+ // error parsing
+ return NULL;
+ }
+
+ yy_delete_buffer(state, scanner);
+ yylex_destroy(scanner);
+ return node;
+}
+
+Node* create_var(char* value) {
+ Var* node = (Var*) malloc(sizeof* node);
+ if (node == NULL) return NULL;
+
+ node->type = eVAR;
+ node->value = strdup(value);
+ return (Node*) node;
+}
+
+Node* create_binary(NodeType type, Node* left, Node* right) {
+ Binary* node = (Binary*) malloc(sizeof* node);
+ if (node == NULL) return NULL;
+
+ node->type = type;
+ node->left = left;
+ node->right = right;
+ return (Node*) node;
+}
+
+Node* create_and(Node* left, Node* right) {
+ return create_binary(eAND, left, right);
+}
+
+Node* create_or(Node* left, Node* right) {
+ return create_binary(eOR, left, right);
+}
+
+Node* create_unary(NodeType type, Node* refnode) {
+ Unary* node = (Unary*) malloc(sizeof* node);
+ if (node == NULL) return NULL;
+
+ node->type = type;
+ node->refnode = refnode;
+ return (Node*) node;
+}
+
+Node* create_not(Node* node) {
+ return create_unary(eNOT, node);
+}
+
+void free_bool_ast(Node* node) {
+ switch (node->type) {
+ case eVAR:
+ {
+ Var* var = (Var*) node;
+ free(var->value);
+ free(var);
+ break;
+ }
+ case eAND:
+ case eOR:
+ {
+ Binary* binary = (Binary*) node;
+ free_bool_ast(binary->left);
+ free_bool_ast(binary->right);
+ free(binary);
+ break;
+ }
+ case eNOT:
+ {
+ Unary* unary = (Unary*) node;
+ free_bool_ast(unary->refnode);
+ free(unary);
+ break;
+ }
+ }
+}
39 c/bool_ast.h
@@ -0,0 +1,39 @@
+#ifndef __EXPRESSION_H__
+#define __EXPRESSION_H__
+
+typedef enum NodeType {
+ eVAR,
+ eAND,
+ eOR,
+ eNOT,
+} NodeType;
+
+typedef struct Node {
+ NodeType type;
+} Node;
+
+typedef struct Var {
+ NodeType type;
+ char* value;
+} Var;
+
+typedef struct Binary {
+ NodeType type;
+ Node* left;
+ Node* right;
+} Binary;
+
+typedef struct Unary {
+ NodeType type;
+ Node* refnode;
+} Unary;
+
+Node* parse_bool_ast(const char* source);
+void free_bool_ast(Node* node);
+
+Node* create_var(char* value);
+Node* create_and(Node* left, Node* right);
+Node* create_or(Node* left, Node* right);
+Node* create_not(Node* node);
+
+#endif
20 c/lexer.l
@@ -0,0 +1,20 @@
+%{
+#include "parser.h"
+%}
+
+%option outfile="lexer.c" header-file="lexer.h"
+%option warn
+%option reentrant noyywrap never-interactive nounistd
+%option bison-bridge
+
+%%
+
+[ \r\n\t]* { /* Skip blanks. */ }
+[A-Za-z0-9_\-@]+ { yylval->value = strdup(yytext); return TOKEN_VAR; }
+"&&" { return TOKEN_AND; }
+"||" { return TOKEN_OR; }
+"!" { return TOKEN_NOT; }
+"(" { return TOKEN_LPAREN; }
+")" { return TOKEN_RPAREN; }
+
+%%
61 c/parser.y
@@ -0,0 +1,61 @@
+%{
+
+#include "parser.h"
+#include "lexer.h"
+
+void yyerror(yyscan_t scanner, Node** node, const char* msg);
+
+%}
+
+%code requires {
+
+#include "bool_ast.h"
+#ifndef YY_TYPEDEF_YY_SCANNER_T
+#define YY_TYPEDEF_YY_SCANNER_T
+typedef void* yyscan_t;
+#endif
+
+}
+
+%output "parser.c"
+%defines "parser.h"
+
+%define api.pure
+%lex-param { yyscan_t scanner }
+%parse-param { Node** node }
+%parse-param { yyscan_t scanner }
+
+%union {
+ char* value;
+ Node* node;
+}
+
+%left TOKEN_OR
+%left TOKEN_AND
+%left UNOT
+
+%token <value> TOKEN_VAR
+%token TOKEN_AND
+%token TOKEN_OR
+%token TOKEN_NOT
+%token TOKEN_LPAREN
+%token TOKEN_RPAREN
+%token TOKEN_ERROR
+
+%type <node> expr
+
+%%
+
+input
+ : expr { *node = $1; }
+ ;
+
+expr
+ : TOKEN_VAR { $$ = create_var($1); }
+ | expr TOKEN_AND expr { $$ = create_and($1, $3); }
+ | expr TOKEN_OR expr { $$ = create_or($1, $3); }
+ | TOKEN_NOT expr %prec UNOT { $$ = create_not($2); }
+ | TOKEN_LPAREN expr TOKEN_RPAREN { $$ = $2; }
+ ;
+
+%%
3 java/.gitignore
@@ -0,0 +1,3 @@
+*.iml
+.idea
+target
9 java/README.md
@@ -0,0 +1,9 @@
+## Java
+
+You need `mvn`, `ragel` and `bison` on your `PATH`. Cd into `java` and run
+
+```
+mvn package
+```
+
+The Java build actually builds a lexer with both Ragel and JFlex. The only reason there are two at the moment is that I haven't decided which one to use yet, so I'm experimenting with both.
123 java/pom.xml
@@ -0,0 +1,123 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <groupId>info.cukes</groupId>
+ <artifactId>bool</artifactId>
+ <version>1.0.0</version>
+ <packaging>jar</packaging>
+
+ <dependencies>
+ <dependency>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ <version>4.10</version>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-antrun-plugin</artifactId>
+ <version>1.7</version>
+ <executions>
+ <execution>
+ <id>mkdirs</id>
+ <phase>generate-sources</phase>
+ <configuration>
+ <target>
+ <mkdir dir="${project.build.directory}/generated-sources/ragel/bool/ragel"/>
+ <mkdir dir="${project.build.directory}/generated-sources/bison/bool/bison"/>
+ </target>
+ </configuration>
+ <goals>
+ <goal>run</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ <plugin>
+ <groupId>de.jflex</groupId>
+ <artifactId>maven-jflex-plugin</artifactId>
+ <version>1.4.3</version>
+ <executions>
+ <execution>
+ <goals>
+ <goal>generate</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ <plugin>
+ <groupId>org.codehaus.mojo</groupId>
+ <artifactId>exec-maven-plugin</artifactId>
+ <version>1.2.1</version>
+ <executions>
+ <execution>
+ <id>bison-grammar</id>
+ <phase>generate-sources</phase>
+ <goals>
+ <goal>exec</goal>
+ </goals>
+ <configuration>
+ <executable>bison</executable>
+ <arguments>
+ <argument>--output</argument>
+ <argument>
+ ${project.build.directory}/generated-sources/bison/bool/Parser.java
+ </argument>
+ <argument>${basedir}/src/main/bison/parser.y</argument>
+ </arguments>
+ </configuration>
+ </execution>
+
+ <execution>
+ <id>ragel-lexer</id>
+ <phase>generate-sources</phase>
+ <goals>
+ <goal>exec</goal>
+ </goals>
+ <configuration>
+ <executable>ragel</executable>
+ <arguments>
+ <argument>-J</argument>
+ <argument>-o</argument>
+ <argument>
+ ${project.build.directory}/generated-sources/ragel/bool/RagelLexer.java
+ </argument>
+ <argument>${basedir}/src/main/ragel/lexer.rl</argument>
+ </arguments>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
+
+ <plugin>
+ <groupId>org.codehaus.mojo</groupId>
+ <artifactId>build-helper-maven-plugin</artifactId>
+ <version>1.7</version>
+ <executions>
+ <execution>
+ <id>add-parser-sources</id>
+ <phase>generate-sources</phase>
+ <goals>
+ <goal>add-source</goal>
+ </goals>
+ <configuration>
+ <sources>
+ <source>${project.build.directory}/generated-sources/bison</source>
+ <source>${project.build.directory}/generated-sources/ragel</source>
+ </sources>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+
+ <properties>
+ <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+ </properties>
+</project>
51 java/src/main/bison/parser.y
@@ -0,0 +1,51 @@
+%{
+package bool;
+
+import java.io.IOException;
+%}
+
+%language "Java"
+%name-prefix ""
+%define public
+%define stype "Expr"
+%error-verbose
+
+%code {
+ private Expr expr;
+
+ public Parser (bool.Lexer lexer) {
+ this(new LexerAdapter(lexer));
+ }
+
+ public Expr parseExpr() throws ParseException, IOException {
+ parse();
+ return expr;
+ }
+}
+
+%token TOKEN_VAR
+%token TOKEN_AND
+%token TOKEN_OR
+%token TOKEN_NOT
+%token TOKEN_LPAREN
+%token TOKEN_RPAREN
+
+%left TOKEN_OR
+%left TOKEN_AND
+%left UNOT
+
+%%
+
+input
+ : expr { expr = $1; }
+ ;
+
+expr
+ : TOKEN_VAR { $$ = yylexer.getLVal(); }
+ | expr TOKEN_AND expr { $$ = new And($1, $3); }
+ | expr TOKEN_OR expr { $$ = new Or($1, $3); }
+ | TOKEN_NOT expr %prec UNOT { $$ = new Not($2); }
+ | TOKEN_LPAREN expr TOKEN_RPAREN { $$ = $2; }
+ ;
+
+%%
12 java/src/main/java/bool/And.java
@@ -0,0 +1,12 @@
+package bool;
+
+public class And extends Binary {
+ public And(Expr left, Expr right) {
+ super(left, right);
+ }
+
+ @Override
+ public <R, A> R accept(Visitor<R, A> visitor, A arg) {
+ return visitor.visitAnd(this, arg);
+ }
+}
11 java/src/main/java/bool/Binary.java
@@ -0,0 +1,11 @@
+package bool;
+
+public class Binary extends Expr {
+ public final Expr left;
+ public final Expr right;
+
+ public Binary(Expr left, Expr right) {
+ this.left = left;
+ this.right = right;
+ }
+}
25 java/src/main/java/bool/EvalVisitor.java
@@ -0,0 +1,25 @@
+package bool;
+
+import java.util.List;
+
+public class EvalVisitor implements Visitor<Boolean, List<String>> {
+ @Override
+ public Boolean visitVar(Var var, List<String> vars) {
+ return vars.contains(var.name);
+ }
+
+ @Override
+ public Boolean visitAnd(And and, List<String> vars) {
+ return and.left.accept(this, vars) && and.right.accept(this, vars);
+ }
+
+ @Override
+ public Boolean visitOr(Or or, List<String> vars) {
+ return or.left.accept(this, vars) || or.right.accept(this, vars);
+ }
+
+ @Override
+ public Boolean visitNot(Not not, List<String> vars) {
+ return !not.node.accept(this, vars);
+ }
+}
7 java/src/main/java/bool/Expr.java
@@ -0,0 +1,7 @@
+package bool;
+
+public class Expr {
+ public <R, A> R accept(Visitor<R, A> visitor, A arg) {
+ throw new UnsupportedOperationException();
+ }
+}
9 java/src/main/java/bool/Lexer.java
@@ -0,0 +1,9 @@
+package bool;
+
+import java.io.IOException;
+
+public interface Lexer {
+ int lex() throws IOException;
+
+ String text();
+}
26 java/src/main/java/bool/LexerAdapter.java
@@ -0,0 +1,26 @@
+package bool;
+
+import java.io.IOException;
+
+class LexerAdapter implements Parser.Lexer {
+ private final Lexer lexer;
+
+ public LexerAdapter(Lexer lexer) {
+ this.lexer = lexer;
+ }
+
+ @Override
+ public Expr getLVal() {
+ return new Var(lexer.text());
+ }
+
+ @Override
+ public int yylex() throws IOException {
+ return lexer.lex();
+ }
+
+ @Override
+ public void yyerror(String s) {
+ throw new ParseException(s);
+ }
+}
14 java/src/main/java/bool/Not.java
@@ -0,0 +1,14 @@
+package bool;
+
+public class Not extends Expr {
+ public final Expr node;
+
+ public Not(Expr node) {
+ this.node = node;
+ }
+
+ @Override
+ public <R, A> R accept(Visitor<R, A> visitor, A arg) {
+ return visitor.visitNot(this, arg);
+ }
+}
12 java/src/main/java/bool/Or.java
@@ -0,0 +1,12 @@
+package bool;
+
+public class Or extends Binary {
+ public Or(Expr left, Expr right) {
+ super(left, right);
+ }
+
+ @Override
+ public <R, A> R accept(Visitor<R, A> visitor, A arg) {
+ return visitor.visitOr(this, arg);
+ }
+}
7 java/src/main/java/bool/ParseException.java
@@ -0,0 +1,7 @@
+package bool;
+
+public class ParseException extends RuntimeException {
+ public ParseException(String msg) {
+ super(msg);
+ }
+}
14 java/src/main/java/bool/Var.java
@@ -0,0 +1,14 @@
+package bool;
+
+public class Var extends Expr {
+ public final String name;
+
+ public Var(String name) {
+ this.name = name;
+ }
+
+ @Override
+ public <R, A> R accept(Visitor<R, A> visitor, A arg) {
+ return visitor.visitVar(this, arg);
+ }
+}
11 java/src/main/java/bool/Visitor.java
@@ -0,0 +1,11 @@
+package bool;
+
+public interface Visitor<R, A> {
+ R visitVar(Var var, A arg);
+
+ R visitAnd(And and, A arg);
+
+ R visitOr(Or or, A arg);
+
+ R visitNot(Not not, A arg);
+}
41 java/src/main/jflex/lexer.jflex
@@ -0,0 +1,41 @@
+package bool;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.io.StringReader;
+
+%%
+
+%public
+%class JFlexLexer
+%implements Lexer
+%byaccj
+%unicode
+%column
+
+%{
+ public JFlexLexer(String expr) {
+ this(new StringReader(expr));
+ }
+
+ @Override
+ public final int lex() throws IOException {
+ return yylex();
+ }
+
+ @Override
+ public final String text() {
+ return yytext();
+ }
+%}
+
+%%
+
+[ \r\n\t]* { /* skip whitespace */ }
+[A-Za-z0-9_\-@]+ { return Parser.TOKEN_VAR; }
+"&&" { return Parser.TOKEN_AND; }
+"||" { return Parser.TOKEN_OR; }
+"!" { return Parser.TOKEN_NOT; }
+"(" { return Parser.TOKEN_LPAREN; }
+")" { return Parser.TOKEN_RPAREN; }
+
51 java/src/main/ragel/lexer.rl
@@ -0,0 +1,51 @@
+package bool;
+
+import java.io.IOException;
+
+public class RagelLexer implements Lexer {
+ %%{
+ machine lexer;
+ alphtype char;
+
+ main := |*
+ [ \t\n];
+ [a-z]+ => {state = Parser.TOKEN_VAR; fbreak;};
+ '&&' => {state = Parser.TOKEN_AND; fbreak;};
+ '||' => {state = Parser.TOKEN_OR; fbreak;};
+ '!' => {state = Parser.TOKEN_NOT; fbreak;};
+ '(' => {state = Parser.TOKEN_LPAREN; fbreak;};
+ ')' => {state = Parser.TOKEN_RPAREN; fbreak;};
+ *|;
+ }%%
+
+ %%write data noerror;
+
+ private int cs, ts, te, p, act;
+ private final int pe, eof;
+ private final char[] data;
+
+ public RagelLexer(char[] data) {
+ this.data = data;
+
+ pe = data.length;
+ eof = pe;
+
+ %% write init;
+ }
+
+ public RagelLexer(String data) {
+ this(data.toCharArray());
+ }
+
+ @Override
+ public final int lex() throws IOException {
+ int state = -1;
+ %% write exec;
+ return state;
+ }
+
+ @Override
+ public String text() {
+ return new String(data, ts, te-ts);
+ }
+}
40 java/src/test/java/bool/LexerTest.java
@@ -0,0 +1,40 @@
+package bool;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.io.IOException;
+import java.util.Collection;
+
+import static java.util.Arrays.asList;
+import static org.junit.Assert.assertEquals;
+
+@RunWith(Parameterized.class)
+public class LexerTest {
+ private final Lexer lexer;
+
+ @Parameterized.Parameters
+ public static Collection<Object[]> data() {
+ return asList(new Object[][]{
+ {new JFlexLexer("foo && bar")},
+ {new RagelLexer("foo && bar")},
+ });
+ }
+
+ public LexerTest(Lexer lexer) {
+ this.lexer = lexer;
+ }
+
+ @Test
+ public void test_lex() throws IOException {
+ assertEquals(Parser.TOKEN_VAR, lexer.lex());
+ assertEquals("foo", lexer.text());
+
+ assertEquals(Parser.TOKEN_AND, lexer.lex());
+ assertEquals("&&", lexer.text());
+
+ assertEquals(Parser.TOKEN_VAR, lexer.lex());
+ assertEquals("bar", lexer.text());
+ }
+}
33 java/src/test/java/bool/ParserTest.java
@@ -0,0 +1,33 @@
+package bool;
+
+import org.junit.Test;
+
+import java.io.IOException;
+
+import static java.util.Arrays.asList;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+public class ParserTest {
+
+ @Test
+ public void test_parse() throws IOException {
+ Parser parser = new Parser(new RagelLexer("foo && bar"));
+ Expr expr = parser.parseExpr();
+ assertTrue(expr.accept(new EvalVisitor(), asList("foo", "bar")));
+ assertFalse(expr.accept(new EvalVisitor(), asList("foo")));
+ }
+
+ @Test
+ public void test_parse_error() throws IOException {
+ Parser parser = new Parser(new bool.RagelLexer("foo && bar &&"));
+ try {
+ parser.parseExpr();
+ fail();
+ } catch (ParseException expected) {
+ assertEquals("syntax error, unexpected end of input, expecting TOKEN_VAR or TOKEN_NOT or TOKEN_LPAREN", expected.getMessage());
+ }
+ }
+}
3 javascript/.gitignore
@@ -0,0 +1,3 @@
+/node_modules/
+npm-debug.log
+lib/bool/parser.js
27 javascript/Makefile
@@ -0,0 +1,27 @@
+VERSION := $(shell node -e "console.log(JSON.parse(require('fs').readFileSync('package.json', 'utf8')).version)")
+
+all: mocha
+
+mocha: node_modules lib/bool/parser.js
+ @NODE_PATH=lib ./node_modules/.bin/mocha
+.PHONY: mocha
+
+clean:
+ rm -f lib/bool/parser.js
+.PHONY: clean
+
+clobber:
+ git clean -dfx
+.PHONY: clobber
+
+lib/bool/parser.js: lib/bool/parser.jison lib/bool/lexer.jisonlex node_modules
+ ./node_modules/.bin/jison -o $@ lib/bool/parser.jison lib/bool/lexer.jisonlex
+
+publish: lib/bool/parser.js
+ npm publish && git tag v$(VERSION) -m "Release v$(VERSION)" && git push && git push --tags
+.PHONY: publish
+
+node_modules: package.json
+ npm install
+ touch $@
+
7 javascript/README.md
@@ -0,0 +1,7 @@
+## JavaScript
+
+You need `node`, `npm` and `make` on your `PATH`. Now just run
+
+```
+make
+```
2 javascript/lib/bool.js
@@ -0,0 +1,2 @@
+module.exports.parse = require('./bool/parser').parse;
+module.exports.EvalVisitor = require('./bool/eval_visitor');
35 javascript/lib/bool/ast.js
@@ -0,0 +1,35 @@
+module.exports = {
+ And: function And(left, right) {
+ this.left = left;
+ this.right = right;
+
+ this.accept = function(visitor, args) {
+ return visitor.visitAnd(this, args);
+ }
+ },
+
+ Or: function Or(left, right) {
+ this.left = left;
+ this.right = right;
+
+ this.accept = function(visitor, args) {
+ return visitor.visitOr(this, args);
+ }
+ },
+
+ Not: function Not(refnode) {
+ this.refnode = refnode;
+
+ this.accept = function(visitor, args) {
+ return visitor.visitNot(this, args);
+ }
+ },
+
+ Var: function Var(name) {
+ this.name = name;
+
+ this.accept = function(visitor, args) {
+ return visitor.visitVar(this, args);
+ }
+ }
+};
17 javascript/lib/bool/eval_visitor.js
@@ -0,0 +1,17 @@
+module.exports = function EvalVisitor() {
+ this.visitVar = function visitVar(var_node, vars) {
+ return vars.indexOf(var_node.name) != -1;
+ };
+
+ this.visitAnd = function visitAnd(and_node, vars) {
+ return and_node.left.accept(this, vars) && and_node.right.accept(this, vars);
+ };
+
+ this.visitOr = function visitAnd(or_node, vars) {
+ return or_node.left.accept(this, vars) || or_node.right.accept(this, vars);
+ };
+
+ this.visitNot = function visitNot(not_node, vars) {
+ return !not_node.refnode.accept(this, vars);
+ };
+};
10 javascript/lib/bool/lexer.jisonlex
@@ -0,0 +1,10 @@
+%%
+
+\s+ { /* skip whitespace */ }
+[A-Za-z0-9_\-@]+ { return 'TOKEN_VAR'; }
+"&&" { return 'TOKEN_AND'; }
+"||" { return 'TOKEN_OR'; }
+"!" { return 'TOKEN_NOT'; }
+"(" { return 'TOKEN_LPAREN'; }
+")" { return 'TOKEN_RPAREN'; }
+<<EOF>> { return 'EOF'; }
22 javascript/lib/bool/parser.jison
@@ -0,0 +1,22 @@
+%left TOKEN_AND TOKEN_OR
+%left UNOT
+
+%start expressions
+
+%% /* language grammar */
+
+expressions
+ : expr EOF { return $1; }
+ ;
+
+expr
+ : TOKEN_VAR { $$ = new ast.Var(yytext); }
+ | expr TOKEN_AND expr { $$ = new ast.And($1, $3); }
+ | expr TOKEN_OR expr { $$ = new ast.Or($1, $3); }
+ | TOKEN_NOT expr %prec UNOT { $$ = new ast.Not($2); }
+ | TOKEN_LPAREN expr TOKEN_RPAREN { $$ = $2; }
+ ;
+
+%%
+
+var ast = require('./ast');
38 javascript/package.json
@@ -0,0 +1,38 @@
+{
+ "name": "bool",
+ "version": "0.0.1",
+ "description": "Boolean expression evaluator",
+ "keywords": [ "boolean", "logic" ],
+ "homepage": "http://github.com/cucumber/bool-js",
+ "author": "Aslak Hellesøy <aslak.hellesoy@gmail.com>",
+ "contributors": [
+ "Aslak Hellesøy <aslak.hellesoy@gmail.com>"
+ ],
+ "repository": {
+ "type": "git",
+ "url": "git://github.com/cucumber/bool-js.git"
+ },
+ "bugs": {
+ "url": "http://github.com/cucumber/bool-js/issues"
+ },
+ "directories": {
+ "lib" : "./lib"
+ },
+ "main": "./lib/bool",
+ "licenses": [
+ {
+ "type": "MIT",
+ "url": "http://github.com/cucumber/bool-js/raw/master/LICENSE"
+ }
+ ],
+ "devDependencies": {
+ "mocha" : "1.7.4",
+ "jison": "0.3.12"
+ },
+ "scripts": {
+ "test": "make mocha"
+ },
+ "engines": {
+ "node" : ">=0.8.11"
+ }
+}
38 javascript/test/bool-test.js
@@ -0,0 +1,38 @@
+var bool = require('bool');
+var assert = require('assert');
+
+describe('Bool', function() {
+ it('sole tag', function() {
+ var expr = bool.parse('@a');
+
+ assert.equal(true, expr.accept(new bool.EvalVisitor(), ['@a']));
+ assert.equal(false, expr.accept(new bool.EvalVisitor(), ['@b']));
+ });
+
+ it('does and', function() {
+ var expr = bool.parse('@a && @b');
+ assert.equal(true, expr.accept(new bool.EvalVisitor(), ['@a', '@b']));
+ assert.equal(false, expr.accept(new bool.EvalVisitor(), ['@a']));
+ assert.equal(false, expr.accept(new bool.EvalVisitor(), ['@b']));
+ assert.equal(false, expr.accept(new bool.EvalVisitor(), []));
+ });
+
+ it('Does it all', function() {
+ var expr = bool.parse('@a && @b || !@c');
+ assert.equal(true, expr.accept(new bool.EvalVisitor(), ['@a', '@b']));
+ assert.equal(false, expr.accept(new bool.EvalVisitor(), ['@c']));
+ assert.equal(true, expr.accept(new bool.EvalVisitor(), []));
+ });
+
+ it('double negation', function() {
+ var expr = bool.parse('!!@a');
+ assert.equal(true, expr.accept(new bool.EvalVisitor(), ['@a']));
+ assert.equal(false, expr.accept(new bool.EvalVisitor(), ['@b']));
+ });
+
+ it('tag syntax', function() {
+ var expr = bool.parse('!@a1A');
+ assert.equal(false, expr.accept(new bool.EvalVisitor(), ['@a1A']));
+ });
+});
+
18 javascript/test/testdata-test.js
@@ -0,0 +1,18 @@
+var bool = require('bool');
+var assert = require('assert');
+var fs = require('fs');
+var path = require('path');
+
+describe('Testdata', function() {
+ var dir = path.join(__dirname, '../../testdata');
+ fs.readdirSync(dir).forEach(function(f) {
+ var lines = fs.readFileSync(path.join(dir, f), 'UTF-8').split(/\n/);
+ it(f, function() {
+ var expr = bool.parse(lines[0]);
+ var vars = lines[1].split(/\s+/);
+ var expected = lines[2];
+ assert.equal(expected, expr.accept(new bool.EvalVisitor(), vars).toString());
+ });
+ });
+});
+
12 ruby/.gitignore
@@ -0,0 +1,12 @@
+/lib/bool.bundle
+/tmp/
+.DS_Store
+.jbundler
+*.jar
+*.lock
+*.idea
+target
+*.iml
+*.ipr
+*.iws
+ext/bool_ext/libbool
2 ruby/Gemfile
@@ -0,0 +1,2 @@
+source :rubygems
+gemspec
22 ruby/README.md
@@ -0,0 +1,22 @@
+## Ruby (MRI)
+
+If you have already managed to build the C code you should be fine. Just run
+
+```
+rake
+```
+
+## JRuby
+
+If you have already managed to build the Java code you should be fine. You also need `jruby` on your `PATH`. RVM users can do this like so:
+
+```
+ln -s ~/.rvm/rubies/jruby-1.7.2/bin/jruby ~/bin/jruby
+export PATH=$PATH:~/bin
+```
+
+Now just run
+
+```
+rake
+```
48 ruby/Rakefile
@@ -0,0 +1,48 @@
+require 'rake/testtask'
+require 'rake/clean'
+
+native_lib_jar = 'lib/bool_ext.jar'
+native_lib_jar_mvn = "../java/target/bool-1.0.0.jar"
+native_lib_c = 'ext/bool_ext/libbool'
+CLEAN << native_lib_jar
+CLEAN << native_lib_c
+
+spec = Gem::Specification.load('bool.gemspec')
+
+if RUBY_PLATFORM =~ /java/
+ file native_lib_jar => native_lib_jar_mvn do
+ cp native_lib_jar_mvn, native_lib_jar
+ end
+
+ file native_lib_jar_mvn do
+ Dir.chdir('../java') do
+ sh 'mvn package'
+ end
+ end
+
+ task :test => native_lib_jar
+else
+ file native_lib_c do
+ cp_r '../c', native_lib_c
+ end
+
+ require 'rake/extensiontask'
+ # defines :compile task
+ Rake::ExtensionTask.new('bool_ext', spec) do |ext|
+ ext.cross_compile = true
+ ext.cross_platform = 'x86-mingw32'
+ end
+
+ # Ideally we'd just :compile => native_lib
+ # but RakeCompiler defines :compile in a way
+ # that breaks dependencies like that.
+ task :test => :_compile
+ task :_compile => [native_lib_c, :compile]
+end
+
+# defines :test task
+Rake::TestTask.new do |t|
+ t.pattern = "spec/*_spec.rb"
+end
+
+task :default => :test
17 ruby/bool.gemspec
@@ -0,0 +1,17 @@
+Gem::Specification.new do |s|
+ s.name = "bool"
+ s.version = "0.0.1"
+ s.summary = "Boolean expression parser"
+ s.author = "Aslak Hellesøy"
+
+ s.files = Dir.glob("lib/**/*.rb")
+
+ if RUBY_PLATFORM =~ /java/
+ s.platform = "java"
+ s.files << "lib/bool_ext.jar"
+ else
+ s.extensions << "ext/bool/extconf.rb"
+ s.files << Dir.glob("ext/**/*.{c,rb}")
+ s.add_development_dependency('rake-compiler', '>= 0.8.2')
+ end
+end
21 ruby/ext/bool_ext/extconf.rb
@@ -0,0 +1,21 @@
+require 'mkmf'
+
+LIBBOOL = File.expand_path(File.dirname(__FILE__) + '/libbool')
+HEADER_DIRS = [LIBBOOL, RbConfig::CONFIG['includedir']]
+LIB_DIRS = [LIBBOOL, RbConfig::CONFIG['libdir']]
+
+Dir.chdir(LIBBOOL) do
+ system 'make clean all'
+end
+
+extension_name = 'bool_ext'
+dir_config(extension_name, HEADER_DIRS, LIB_DIRS)
+
+unless find_header('bool_ast.h')
+ abort "bool_ast.h is missing."
+end
+unless find_library('bool', 'parse_bool_ast')
+ abort "libbool is missing."
+end
+
+create_makefile(extension_name)
56 ruby/ext/bool_ext/ruby_bool.c
@@ -0,0 +1,56 @@
+#include <ruby.h>
+#include "bool_ast.h"
+
+VALUE rb_cVar;
+VALUE rb_cAnd;
+VALUE rb_cOr;
+VALUE rb_cNot;
+VALUE rb_eParseError;
+
+/**
+ * Transforms the C AST to a Ruby AST.
+ */
+static VALUE transform(Node *node) {
+ switch (node->type) {
+ case eVAR:
+ return rb_funcall(rb_cVar, rb_intern("new"), 1, rb_str_new2(((Var*)node)->value));
+ case eAND:
+ return rb_funcall(rb_cAnd, rb_intern("new"), 2, transform(((Binary*)node)->left), transform(((Binary*)node)->right));
+ case eOR:
+ return rb_funcall(rb_cOr, rb_intern("new"), 2, transform(((Binary*)node)->left), transform(((Binary*)node)->right));
+ case eNOT:
+ return rb_funcall(rb_cNot, rb_intern("new"), 1, transform(((Unary*)node)->refnode));
+ default:
+ rb_raise(rb_eArgError, "Should never happen");
+ return 0;
+ }
+}
+
+static VALUE Bool_parse(VALUE ast_klass, VALUE r_expr) {
+ // TODO: Verify that r_expr is a String
+ Node* ast = NULL;
+ char* expr = RSTRING_PTR(r_expr);
+ ast = parse_bool_ast(expr);
+ if(ast != NULL) {
+ VALUE result = transform(ast);
+ free_bool_ast(ast);
+ return result;
+ } else {
+ rb_raise(rb_eParseError, "Couldn't parse boolean expression");
+ }
+}
+
+void Init_bool_ext() {
+ VALUE rb_mBool = rb_define_module("Bool");
+ VALUE rb_eStandardError = rb_const_get(rb_cObject, rb_intern("StandardError"));
+ rb_eParseError = rb_define_class_under(rb_mBool, "ParseError", rb_eStandardError);
+
+ VALUE rb_cBinary = rb_define_class_under(rb_mBool, "Binary", rb_cObject);
+ VALUE rb_cUnary = rb_define_class_under(rb_mBool, "Unary", rb_cObject);
+ rb_cVar = rb_define_class_under(rb_mBool, "Var", rb_cObject);
+ rb_cAnd = rb_define_class_under(rb_mBool, "And", rb_cBinary);
+ rb_cOr = rb_define_class_under(rb_mBool, "Or", rb_cBinary);
+ rb_cNot = rb_define_class_under(rb_mBool, "Not", rb_cUnary);
+
+ rb_define_singleton_method(rb_mBool, "parse", Bool_parse, 1);
+}
1 ruby/lib/.gitignore
@@ -0,0 +1 @@
+*.bundle
23 ruby/lib/bool.rb
@@ -0,0 +1,23 @@
+# This loads either bool_ext.so, bool_ext.bundle or
+# bool_ext.jar, depending on your Ruby platform and OS
+require 'bool_ext'
+require 'bool/ast'
+require 'bool/eval_visitor'
+
+module Bool
+ class ParseError < StandardError
+ end
+
+ if RUBY_PLATFORM =~ /java/
+ def parse(source)
+ lexer = Java::Bool::JFlexLexer.new(source)
+ parser = Java::Bool::Parser.new(lexer)
+ parser.parseExpr()
+ rescue => e
+ raise ParseError.new(e.message)
+ end
+ module_function(:parse)
+ else
+ # parse is defined in ruby_bool.c
+ end
+end
55 ruby/lib/bool/ast.rb
@@ -0,0 +1,55 @@
+module Bool
+ if RUBY_PLATFORM =~ /java/
+ # AST classes defined in bool_ext.jar (bool-jvm)
+ else
+ class Var
+ attr_reader :name
+
+ def initialize(name)
+ @name = name
+ end
+
+ def accept(visitor, arg)
+ visitor.visit_var(self, arg)
+ end
+ end
+
+ class Binary
+ attr_reader :left, :right
+
+ def initialize(left, right)
+ @left, @right = left, right
+ end
+ end
+
+ class And < Binary
+ def accept(visitor, arg)
+ visitor.visit_and(self, arg)
+ end
+ end
+
+ class Or < Binary
+ def accept(visitor, arg)
+ visitor.visit_or(self, arg)
+ end
+ end
+
+ class Unary
+ attr_reader :node
+
+ def initialize(node)
+ @node = node
+ end
+ end
+
+ class Not < Unary
+ def initialize(node)
+ @node = node
+ end
+
+ def accept(visitor, arg)
+ visitor.visit_not(self, arg)
+ end
+ end
+ end
+end
28 ruby/lib/bool/eval_visitor.rb
@@ -0,0 +1,28 @@
+module Bool
+ class EvalVisitor
+ if RUBY_PLATFORM =~ /java/
+ require 'libbool'
+
+ def self.new
+ Java::Bool::EvalVisitor.new
+ end
+
+ else
+ def visit_var(var_node, vars)
+ !!vars.index(var_node.name)
+ end
+
+ def visit_and(and_node, vars)
+ and_node.left.accept(self, vars) && and_node.right.accept(self, vars)
+ end
+
+ def visit_or(and_node, vars)
+ and_node.left.accept(self, vars) || and_node.right.accept(self, vars)
+ end
+
+ def visit_not(not_node, vars)
+ !not_node.node.accept(self, vars)
+ end
+ end
+ end
+end
69 ruby/spec/bool_spec.rb
@@ -0,0 +1,69 @@
+$:.unshift(File.dirname(__FILE__) + '/../lib')
+require 'bool'
+require 'minitest/autorun'
+
+describe 'Bool' do
+ describe "AND expression" do
+ before do
+ @ast = Bool.parse("a && b")
+ end
+
+ it "is false when one operand is false" do
+ @ast.accept(Bool::EvalVisitor.new, ["a"]).must_equal(false)
+ end
+
+ it "is true when both operands are true" do
+ @ast.accept(Bool::EvalVisitor.new, ["a", "b"]).must_equal(true)
+ end
+ end
+
+ describe "OR expression" do
+ before do
+ @ast = Bool.parse("a || b")
+ end
+
+ it "is true when one operand is false" do
+ @ast.accept(Bool::EvalVisitor.new, ["a"]).must_equal(true)
+ end
+
+ it "is false when both operands are false" do
+ @ast.accept(Bool::EvalVisitor.new, []).must_equal(false)
+ end
+ end
+
+ describe "NOT expression" do
+ before do
+ @ast = Bool.parse("!a")
+ end
+
+ it "is true when operand is false" do
+ @ast.accept(Bool::EvalVisitor.new, []).must_equal(true)
+ end
+
+ it "is false when operand is true" do
+ @ast.accept(Bool::EvalVisitor.new, ["a"]).must_equal(false)
+ end
+ end
+
+ describe "Exception" do
+ it 'is raised on parse error' do
+ begin
+ Bool.parse("a ||")
+ fail
+ rescue Bool::ParseError => expected
+ # TODO
+ # expected.message.must_equal("syntax error, unexpected end of input, expecting TOKEN_VAR or TOKEN_NOT or TOKEN_LPAREN");
+ end
+ end
+
+ it 'is raised on lexing error' do
+ begin
+ Bool.parse("a ^ e")
+ fail
+ rescue Bool::ParseError => expected
+ # TODO
+ # expected.message.must_equal("Error: could not match input");
+ end
+ end
+ end
+end
13 ruby/spec/testdata_spec.rb
@@ -0,0 +1,13 @@
+$:.unshift(File.dirname(__FILE__) + '/../lib')
+require 'bool'
+require 'minitest/autorun'
+
+describe "Testdata" do
+ Dir[File.dirname(__FILE__) + '/../../testdata/*.txt'].each do |f|
+ expr, vars, result = IO.read(f).split(/\n/)
+ it f do
+ @ast = Bool.parse(expr)
+ @ast.accept(Bool::EvalVisitor.new, vars.split(/\s+/)).to_s.must_equal(result)
+ end
+ end
+end
3 testdata/a.txt
@@ -0,0 +1,3 @@
+a && !b && c
+a c
+true
3 testdata/b.txt
@@ -0,0 +1,3 @@
+a && b && (!c || !d)
+a b
+true

0 comments on commit 9ed7da4

Please sign in to comment.