diff --git a/.github/workflows/cs.yml b/.github/workflows/cs.yml new file mode 100644 index 0000000..f172cae --- /dev/null +++ b/.github/workflows/cs.yml @@ -0,0 +1,85 @@ +on: + - push + - pull_request + +jobs: + test-dotnet21: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + - name: Setup .NET Core SDK 2.1.x + uses: actions/setup-dotnet@v3 + with: + dotnet-version: '2.1.x' + - name: Run test 2.1.x + run: make test-cs + env: + DOTNET_SDK: netcoreapp2.1 + test-dotnet22: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + - name: Setup .NET Core SDK 2.2.x + uses: actions/setup-dotnet@v3 + with: + dotnet-version: '2.2.x' + - name: Run test 2.2.x + run: make test-cs + env: + DOTNET_SDK: netcoreapp2.2 + test-dotnet30: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + - name: Setup .NET Core SDK 3.0.x + uses: actions/setup-dotnet@v3 + with: + dotnet-version: '3.0.x' + - name: Run test 3.0.x + run: make test-cs + env: + DOTNET_SDK: netcoreapp3.0 + test-dotnet31: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + - name: Setup .NET Core SDK 3.1.x + uses: actions/setup-dotnet@v3 + with: + dotnet-version: '3.1.x' + - name: Run test 3.1.x + run: make test-cs + env: + DOTNET_SDK: netcoreapp3.1 + test-dotnet50: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + - name: Setup .NET Core SDK 5.0.x + uses: actions/setup-dotnet@v3 + with: + dotnet-version: '5.0.x' + - name: Run test 5.0.x + run: make test-cs + env: + DOTNET_SDK: net5.0 + test-dotnet60: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + - name: Setup .NET Core SDK 6.0.x + uses: actions/setup-dotnet@v3 + with: + dotnet-version: '6.0.x' + - name: Run test 6.0.x + run: make test-cs + env: + DOTNET_SDK: net6.0 + + diff --git a/Makefile b/Makefile index 1178f78..4a6e9e1 100644 --- a/Makefile +++ b/Makefile @@ -33,6 +33,9 @@ test/%.js: test/%.peg $(lib_files) %/Grammar.java: %.peg $(lib_files) ./bin/canopy --lang java $< +%/Grammar.cs: %.peg $(lib_files) + ./bin/canopy --lang cs $< + %.py: %.peg $(lib_files) ./bin/canopy --lang python $< @@ -50,6 +53,15 @@ test-all: test-java test-js test-python test-ruby test-java: $(test_grammars:%.peg=%/Grammar.java) cd test/java && mvn clean test +DOTNET_SDK?=netcoreapp3.1 +test-cs: $(test_grammars:%.peg=%/Grammar.cs) + cd test/cs/choices && dotnet test --framework ${DOTNET_SDK} + cd test/cs/node_actions && dotnet test --framework ${DOTNET_SDK} + cd test/cs/predicates && dotnet test --framework ${DOTNET_SDK} + cd test/cs/quantifiers && dotnet test --framework ${DOTNET_SDK} + cd test/cs/sequences && dotnet test --framework ${DOTNET_SDK} + cd test/cs/terminals && dotnet test --framework ${DOTNET_SDK} + test-js: test/javascript/node_modules $(test_grammars:%.peg=%.js) cd test/javascript && npm test diff --git a/package.json b/package.json index b5b3717..a66ce12 100644 --- a/package.json +++ b/package.json @@ -1,22 +1,42 @@ -{ "name" : "canopy" -, "description" : "PEG parser compiler for JavaScript" -, "homepage" : "https://canopy.jcoglan.com" -, "author" : "James Coglan (http://jcoglan.com/)" -, "keywords" : ["parser", "compiler", "peg"] -, "license" : "MPL-2.0" - -, "version" : "0.4.0" -, "engines" : { "node": ">=8.0.0" } -, "files" : ["bin", "lib", "templates"] -, "main" : "./lib/canopy.js" -, "bin" : { "canopy": "./bin/canopy" } - -, "dependencies" : { "handlebars": ">=4.0.0", "mkdirp": ">=3.0.0", "nopt": "*" } -, "devDependencies" : { "benchmark": "", "jstest": "", "pegjs": "" } - -, "bugs" : { "url": "https://github.com/jcoglan/canopy/issues" } - -, "repository" : { "type" : "git" - , "url" : "git://github.com/jcoglan/canopy.git" - } +{ + "name": "canopy", + "description": "PEG parser compiler for JavaScript", + "homepage": "https://canopy.jcoglan.com", + "author": "James Coglan (http://jcoglan.com/)", + "keywords": [ + "parser", + "compiler", + "peg" + ], + "license": "MPL-2.0", + "version": "0.4.0", + "engines": { + "node": ">=8.0.0" + }, + "files": [ + "bin", + "lib", + "templates" + ], + "main": "./lib/canopy.js", + "bin": { + "canopy": "./bin/canopy" + }, + "dependencies": { + "handlebars": ">=4.0.0", + "mkdirp": ">=3.0.0", + "nopt": "*" + }, + "devDependencies": { + "benchmark": "", + "jstest": "", + "pegjs": "" + }, + "bugs": { + "url": "https://github.com/jcoglan/canopy/issues" + }, + "repository": { + "type": "git", + "url": "git://github.com/jcoglan/canopy.git" + } } diff --git a/site/index.md b/site/index.md index f0ab240..1861479 100644 --- a/site/index.md +++ b/site/index.md @@ -20,3 +20,5 @@ Canopy can generate parsers in the following languages: * [JavaScript](/langs/javascript.html) * [Python](/langs/python.html) * [Ruby](/langs/ruby.html) +* [.Net](/langs/cs.html) + diff --git a/site/langs/cs.md b/site/langs/cs.md new file mode 100644 index 0000000..0b1cfc8 --- /dev/null +++ b/site/langs/cs.md @@ -0,0 +1,260 @@ +--- +layout: default +title: .Net +--- + +## .Net + +To get an overview of how to use Canopy with .Net/C#, consider this example of a +simplified grammar for URLs: + +###### url.peg + + grammar URL + url <- scheme "://" host pathname search hash? + scheme <- "http" "s"? + host <- hostname port? + hostname <- segment ("." segment)* + segment <- [a-z0-9-]+ + port <- ":" [0-9]+ + pathname <- "/" [^ ?]* + search <- ("?" query:[^ #]*)? + hash <- "#" [^ ]* + +We can compile this grammar into a C# namespace using `canopy`: + + $ canopy url.peg --lang cs + +This creates a folder called `url` that contains all the parser logic. The +folder name and location is based on the path to the `.peg` file when you run `canopy`, for +example if you run: + + $ canopy com/jcoglan/canopy/url.peg --lang cs + +then you will get the files at the `com/jcoglan/canopy/url` folder under the namespace `canopy.com.jcoglan.canopy.url`. The `--output` option can be used to override this: + + $ canopy com/jcoglan/canopy/url.peg --lang cs --output some/dir/url + +This will write the generated files into the directory `some/dir/url`. + +Let's try out our parser: + +```cs +using System; +using canopy.url; + +namespace Example +{ + class CanopyExample { + static void Main(string[] args) + { + TreeNode tree = URL.parse("http://example.com/search?q=hello#page=1"); + + foreach (TreeNode node in tree.elements) { + System.Console.WriteLine(node.offset + ", " + node.text); + } + + /* prints: + + 0, http + 4, :// + 7, example.com + 18, /search + 25, ?q=hello + 33, #page=1 */ + } + } +} +``` + +This little example shows a few important things: + +You invoke the parser by calling the module's `parse()` function with a string. + +The `parse()` method returns a tree of *nodes*. + +Each node has three properties: + +* `String text`, the snippet of the input text that node represents +* `int offset`, the number of characters into the input text the node appears +* `List elements`, an array of nodes matching the sub-expressions + +## Walking the parse tree + +You can use `elements` to walk into the structure of the tree, or, you can use +the labels that Canopy generates, which can make your code clearer: + +```cs +using System; +using canopy.url; + +namespace Example +{ + class CanopyExample { + static void Main(string[] args) + { + + TreeNode tree = URL.parse("http://example.com/search?q=hello#page=1"); + + System.Console.WriteLine(tree.elements[4].elements[1].text); + // -> 'q=hello' + + System.Console.WriteLine(tree.get(Label.peg_search).get(Label.peg_query).text); + // -> 'q=hello' + } + } +} +``` + +## Parsing errors + +If you give the parser an input text that does not match the grammar, a +`url.ParseError` is thrown. The error message will list any of the strings or +character classes the parser was expecting to find at the furthest position it +got to, along with the rule those expectations come from, and it will highlight +the line of the input where the syntax error occurs. + +```cs +using System; +using canopy.url; + +namespace Example +{ + class CanopyExample { + static void Main(string[] args) + { + TreeNode tree = URL.parse("https://example.com./"); + } + } +} + +/* Unhandled exception. canopy.url.ParseError: Line 1: expected one of: + + - [a-z0-9-] from URL::segment + + 1 | https://example.com./ ^ */ +``` + +## Implementing actions + +Say you have a grammar that uses action annotations, for example: + +###### maps.peg + + grammar Maps + map <- "{" string ":" value "}" %make_map + string <- "'" [^']* "'" %make_string + value <- list / number + list <- "[" value ("," value)* "]" %make_list + number <- [0-9]+ %make_number + +In C#, compiling the above grammar produces a namespace called `canopy.maps` that +contains classes called `Maps`, `TreeNode` and `ParseError`, an enum called +`Label` and an interface called `Actions`. You supply the action functions to +the parser by implementing the `Actions` interface, which has one method for +each action named in the grammar, each of which must return a `TreeNode`. +`TreeNode` has a no-argument constructor so making subclasses of it is +relatively easy. + +The following example parses the input `{'ints':[1,2,3]}`. It defines one +`TreeNode` subclass for each kind of value in the tree: + +* `Pair` wraps a `Map>` +* `Text` wraps a `String` +* `Array` wraps a `List` +* `Number` wraps an `int` + +It then implements the `Actions` interface to generate values of these types +from the parser matches. + +```cs +using System; +using System.Collections; +using System.Collections.Generic; +using canopy.maps; + +namespace Example +{ + + public class Pair : TreeNode { + public Dictionary> pair; + + public Pair(String key, List value) { + pair = new Dictionary>(); + pair[key] = value; + } + + public override string ToString() + { + String ret="{"; + foreach (var kvp in pair) { + List values = kvp.Value.ConvertAll(x => x.ToString()); + ret += string.Format("{0}=[{1}],", kvp.Key, string.Join( ",", values)); + } + return ret+"}"; + } + } + + public class Text : TreeNode { + public String str; + + public Text(String str) { + this.str = str; + } + } + + public class Array : TreeNode { + public List list; + + public Array(List list) { + this.list = list; + } + } + + public class Number : TreeNode { + public int number; + + public Number(int number) { + this.number = number; + } + } + + public class MapsActions : Actions { + public TreeNode make_map(String input, int start, int end, List elements) { + Text str = (Text)elements[1]; + Array array = (Array)elements[3]; + return new Pair(str.str, array.list); + } + + public TreeNode make_string(String input, int start, int end, List elements) { + return new Text(elements[1].text); + } + + public TreeNode make_list(String input, int start, int end, List elements) { + List list = new List(); + list.Add(((Number)elements[1]).number); + foreach (TreeNode el in elements[2]) { + Number number = (Number)el.get(Label.peg_value); + list.Add(number.number); + } + return new Array(list); + } + + public TreeNode make_number(String input, int start, int end, List elements) { + return new Number(Int32.Parse(input.Substring(start, end-start))); + } + } + + public class CanopyExample { + static void Main(string[] args){ + Pair result = (Pair)Maps.parse("{'ints':[1,2,3]}", new MapsActions()); + System.Console.WriteLine(result); + // -> {ints=[1, 2, 3],} + } + } +} +``` + +## Extended node types + +Using the `` grammar annotation is not supported in the C# version. diff --git a/src/builders/cs.js b/src/builders/cs.js new file mode 100644 index 0000000..d6b72c6 --- /dev/null +++ b/src/builders/cs.js @@ -0,0 +1,351 @@ +'use strict' + +const { sep } = require('path') +const Base = require('./base') + +const TYPES = { + address: 'TreeNode', + chunk: 'String', + elements: 'List', + index: 'int', + max: 'int' +} + +class Builder extends Base { + constructor (...args) { + super(...args) + this._labels = new Set() + this.namespace = "" + } + + _tab () { + return ' ' + } + + _initBuffer (pathname) { + this.namespace = pathname.split(sep) + this.namespace.pop() + this.namespace = this.namespace.join('.') + return '//canopy namespace: ' + this.namespace + ';\n\n' + } + + _quote (string) { + string = string.replace(/\\/g, '\\\\') + .replace(/"/g, '\\"') + .replace(/\x08/g, '\\b') + .replace(/\t/g, '\\t') + .replace(/\n/g, '\\n') + .replace(/\f/g, '\\f') + .replace(/\r/g, '\\r') + + return '"' + string + '"' + } + + comment (lines) { + lines = lines.map((line) => ' * ' + line) + return ['/**'].concat(lines).concat([' */']) + } + + pragma (directive) { + this._line("#pragma " + directive, false) + } + + package_ (name, actions, block) { + this._grammarName = name + + this._newBuffer('cs', 'Actions') + this._template('cs', 'Actions.cs', { actions, namespace:this.namespace }) + + this._newBuffer('cs', 'CacheRecord') + this._template('cs', 'CacheRecord.cs', { namespace:this.namespace }) + + block() + } + + syntaxNodeClass_ () { + let name = 'TreeNode' + + this._newBuffer('cs', name) + this._template('cs', 'TreeNode.cs', { name, namespace:this.namespace}) + + return name + } + + grammarModule_ (block) { + this._newBuffer('cs', 'Grammar') + //some pragmas to kill warnings + this.pragma("warning disable CS1717") + this._line('using System') + this._line('using System.Collections') + this._line('using System.Collections.Generic') + this._line('using System.Text.RegularExpressions') + this._newline() + this._line('namespace canopy.' + this.namespace + ' {', false) + this._indent(() => { + this._line('public abstract class Grammar {', false) + this._indent(() => { + this.assign_('public static TreeNode ' + this.nullNode_(), 'new TreeNode()') + this._newline() + + this._line('public int inputSize, offset, failure') + this._line('public String input') + this._line('public List expected') + this._line('public Dictionary> cache') + this._line('public Actions actions') + //default constructor + this._line('public Grammar() {',false) + this._indent(() => { + this.assign_('this.input','\"\"') + this.assign_('this.inputSize','0') + this.assign_('this.actions','null') + this.assign_('this.offset','0') + this.assign_('this.cache','new Dictionary>()') + this.assign_('this.failure','0') + this.assign_('this.expected','new List()') + }) + this._line('}',false) + this._newline() + block() + }) + }) + this._line('}}', false) + } + + compileRegex_ (charClass, name) { + let regex = charClass.regex, + source = regex.source.replace(/^\^/, '\\A') + this.assign_('private static Regex ' + name, 'new Regex(' + this._quote(source) + ')') + charClass.constName = name + } + + parserClass_ (root) { + this._newBuffer('cs', 'ParseError') + this._template('cs', 'ParseError.cs', { namespace:this.namespace}) + + let grammar = this._quote(this._grammarName) + let name = this._grammarName.replace(/\./g, '') + this._newBuffer('cs', name) + this._template('cs', 'Parser.cs', { grammar, root, name, namespace:this.namespace }) + + let labels = [...this._labels].sort() + + this._newBuffer('cs', 'Label') + this._template('cs', 'Label.cs', { labels, namespace:this.namespace }) + } + + class_ (name, parent, block) { + this._newline() + this._line('namespace canopy.' + this.namespace + ' {', false) + this._indent(() => { + this._line('class ' + name + ' : ' + parent + ' {', false) + this._scope(block, name) + this._line('}', false) + }) + this._line('}', false) + } + + constructor_ (args, block) { + this._line('public ' + this._currentScope.name + '(String text, int offset, List elements) : base(text, offset, elements){', false) + this._indent(() => { + block() + }) + this._line('}', false) + } + + method_ (name, args, block) { + this._newline() + this._line('public TreeNode ' + name + '() {', false) + this._scope(block) + this._line('}', false) + } + set_label_name(name) { + return 'peg_' + name + } + cache_ (name, block) { + name = this.set_label_name(name)//we have to do this in case the name is a keyword + this._labels.add(name) + + let temp = this.localVars_({ address: this.nullNode_(), index: 'offset' }), + address = temp.address, + offset = temp.index + this._line('Dictionary rule') + this._line('cache.TryGetValue(Label.' + name + ', out rule)') + this.if_('rule == null', () => { + this.assign_('rule', 'new Dictionary()') + this.assign_('cache[Label.' + name + ']','rule') + }) + this.if_('rule.ContainsKey(offset)', () => { + this.assign_(address, 'rule[offset].node') + this.assign_('offset', 'rule[offset].tail') + }, () => { + block(address) + this.assign_('rule[' + offset + ']', 'new CacheRecord(' + address + ', offset)') + }) + this._return(address) + } + + attribute_ (name, value) { + name = this.set_label_name(name)//we have to do this in case the name is a keyword + this._labels.add(name) + this.assign_('labelled[Label.' + name + ']', value) + } + + localVars_ (vars) { + let names = {} + for (let name in vars) + names[name] = this.localVar_(name, vars[name]) + return names + } + + localVar_ (name, value) { + let varName = this._varName(name) + + if (value === undefined) value = this.nullNode_() + this.assign_(TYPES[name] + ' ' + varName, value) + + return varName + } + + chunk_ (length) { + let input = 'input', + ofs = 'offset', + temp = this.localVars_({ chunk: this.null_(), max: ofs + ' + ' + length }) + + this.if_(temp.max + ' <= inputSize', () => { + this._line(temp.chunk + ' = ' + input + '.Substring(' + ofs + ', ' + temp.max + ' - ' + ofs + ')') + }) + return temp.chunk + } + + syntaxNode_ (address, start, end, elements, action, nodeClass) { + let args + + if (action) { + action = 'actions.' + action + args = ['input', start, end] + } else { + action = 'new ' + (nodeClass || 'TreeNode') + args = ['input.Substring(' + start + ', ' + end + ' - ' + start +')', start] + } + args.push(elements || this.emptyList_()) + + this.assign_(address, action + '(' + args.join(', ') + ')') + this.assign_('offset', end) + } + + ifNode_ (address, block, else_) { + this.if_(address + ' != ' + this.nullNode_(), block, else_) + } + + unlessNode_ (address, block, else_) { + this.if_(address + ' == ' + this.nullNode_(), block, else_) + } + + ifNull_ (elements, block, else_) { + this.if_(elements + ' == null', block, else_) + } + + extendNode_ (address, nodeType) { + // TODO + } + + failure_ (address, expected) { + let rule = this._quote(this._grammarName + '::' + this._ruleName) + expected = this._quote(expected) + + this.assign_(address, this.nullNode_()) + + this.if_('offset > failure', () => { + this.assign_('failure', 'offset') + this.assign_('expected', 'new List()') + }) + this.if_('offset == failure', () => { + this.append_('expected', 'new String[] { ' + rule + ', ' + expected + ' }') + }) + } + + jump_ (address, rule) { + this.assign_(address, '_read_' + rule + '()') + } + + _conditional (kwd, condition, block, else_) { + this._line(kwd + ' (' + condition + ') {', false) + this._indent(block) + if (else_) { + this._line('} else {', false) + this._indent(else_) + } + this._line('}', false) + } + + if_ (condition, block, else_) { + this._conditional('if', condition, block, else_) + } + + loop_ (block) { + this._conditional('while', 'true', block) + } + + break_ () { + this._line('break') + } + + sizeInRange_ (address, [min, max]) { + if (max === -1) { + return address + '.Count >= ' + min + } else if (max === 0) { + return address + '.Count == ' + min + } else { + return address + '.Count >= ' + min + ' && ' + address + '.Count <= ' + max + } + } + + stringMatch_ (expression, string) { + return expression + ' != null && ' + expression + '.Equals(' + this._quote(string) + ')' + } + + stringMatchCI_ (expression, string) { + return expression + ' != null && ' + expression + '.ToLower().Equals(' + this._quote(string) + '.ToLower())' + } + + regexMatch_ (regex, string) { + return string + ' != null && ' + regex + '.IsMatch(' + string + ')' + } + + arrayLookup_ (expression, offset) { + return expression + '[' + offset + ']' + } + + append_ (list, value, index) { + if (index === undefined) + this._line(list + '.Add(' + value + ')') + else + this._line(list + '.Insert(' + index + ', ' + value + ')') + } + + hasChars_ () { + return 'offset < inputSize' + } + + nullNode_ () { + return 'FAILURE' + } + + offset_ () { + return 'offset' + } + + emptyList_ (size) { + return 'new List(' + (size || '') + ')' + } + + _emptyString () { + return '""' + } + + null_ () { + return 'null' + } +} + +module.exports = Builder diff --git a/src/canopy.js b/src/canopy.js index f696dcc..ba57742 100644 --- a/src/canopy.js +++ b/src/canopy.js @@ -5,6 +5,7 @@ const Compiler = require('./compiler') module.exports = { builders: { java: require('./builders/java'), + cs: require('./builders/cs'), javascript: require('./builders/javascript'), python: require('./builders/python'), ruby: require('./builders/ruby') diff --git a/templates/cs/Actions.cs b/templates/cs/Actions.cs new file mode 100644 index 0000000..58d96af --- /dev/null +++ b/templates/cs/Actions.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +using System; + +namespace canopy.{{namespace}} { + public interface Actions { + {{#each actions}} + public TreeNode {{this}}(String input, int start, int end, List elements); + {{/each}} + } +} diff --git a/templates/cs/CacheRecord.cs b/templates/cs/CacheRecord.cs new file mode 100644 index 0000000..e83cd1c --- /dev/null +++ b/templates/cs/CacheRecord.cs @@ -0,0 +1,12 @@ +namespace canopy.{{namespace}} { + + public class CacheRecord { + public TreeNode node; + public int tail; + + public CacheRecord(TreeNode node, int tail) { + this.node = node; + this.tail = tail; + } + } +} \ No newline at end of file diff --git a/templates/cs/Label.cs b/templates/cs/Label.cs new file mode 100644 index 0000000..db9d286 --- /dev/null +++ b/templates/cs/Label.cs @@ -0,0 +1,7 @@ +namespace canopy.{{namespace}} { + public enum Label { + {{#each labels}} + {{this}}{{#unless @last}},{{/unless}} + {{/each}} + } +} diff --git a/templates/cs/ParseError.cs b/templates/cs/ParseError.cs new file mode 100644 index 0000000..1b91734 --- /dev/null +++ b/templates/cs/ParseError.cs @@ -0,0 +1,7 @@ +using System; +namespace canopy.{{namespace}} { + public class ParseError : Exception { + public ParseError(String message) : base(message) { + } + } +} \ No newline at end of file diff --git a/templates/cs/Parser.cs b/templates/cs/Parser.cs new file mode 100644 index 0000000..b264430 --- /dev/null +++ b/templates/cs/Parser.cs @@ -0,0 +1,67 @@ +using System.Collections.Generic; +using System.Collections; +using System; + +namespace canopy.{{namespace}} { + public class {{name}} : Grammar { + public {{name}}(String input, Actions actions) { + this.input = input; + this.inputSize = input.Length; + this.actions = actions; + this.offset = 0; + this.cache = new Dictionary>(); + this.failure = 0; + this.expected = new List(); + } + + public static TreeNode parse(String input, Actions actions) { + {{name}} parser = new {{name}}(input, actions); + return parser.parse(); + } + + public static TreeNode parse(String input){ + return parse(input, null); + } + + private static String formatError(String input, int offset, List expected) { + String[] lines = input.Split('\n'); + int lineNo = 0, position = 0; + + while (position <= offset) { + position += lines[lineNo].Length + 1; + lineNo += 1; + } + + String line = lines[lineNo - 1]; + String message = "Line " + lineNo + ": expected one of:\n\n"; + + foreach (String[] pair in expected) { + message += " - " + pair[1] + " from " + pair[0] + "\n"; + } + + String number = "" + lineNo; + while (number.Length < 6) number = " " + number; + message += "\n" + number + " | " + line + "\n"; + + position -= line.Length + 10; + + while (position < offset) { + message += " "; + position += 1; + } + return message + "^"; + } + + private TreeNode parse(){ + TreeNode tree = _read_{{root}}(); + if (tree != FAILURE && offset == inputSize) { + return tree; + } + if (expected.Count <= 0) { + failure = offset; + expected.Add(new String[] { {{{grammar}}}, "" }); + } + throw new ParseError(formatError(input, failure, expected)); + } + } +} diff --git a/templates/cs/TreeNode.cs b/templates/cs/TreeNode.cs new file mode 100644 index 0000000..12b4666 --- /dev/null +++ b/templates/cs/TreeNode.cs @@ -0,0 +1,52 @@ +using System.Collections.Generic; +using System.Collections; +using System; + +namespace canopy.{{namespace}} { + public class {{name}} : IEnumerable<{{name}}> { + public String text; + public int offset; + public List<{{name}}> elements; + + public Dictionary labelled; + + public {{name}}() : this("", -1, new List<{{name}}>(0)) { + + } + + public {{name}}(String text, int offset, List<{{name}}> elements) { + this.text = text; + this.offset = offset; + this.elements = elements; + this.labelled = new Dictionary(); + } + + public {{name}} get(Label key) { + {{name}} ret; + labelled.TryGetValue(key, out ret); + return ret; + } + + public IEnumerator<{{name}}> iterator() { + foreach(var items in elements) + { + // Returning the element after every iteration + yield return items; + } + } + + public IEnumerator<{{name}}> GetEnumerator() { + foreach(var items in elements) + { + // Returning the element after every iteration + yield return items; + } + } + System.Collections.IEnumerator + System.Collections.IEnumerable.GetEnumerator() + { + // Invoke IEnumerator GetEnumerator() above. + return GetEnumerator(); + } + } +} \ No newline at end of file diff --git a/test/cs/choices/ChoicesTest.cs b/test/cs/choices/ChoicesTest.cs new file mode 100644 index 0000000..a3acc53 --- /dev/null +++ b/test/cs/choices/ChoicesTest.cs @@ -0,0 +1,136 @@ +//package canopy.choices; +using System.Collections.Generic; +using System.Collections; +using System; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using canopy.test.grammars.choices; + +[TestClass] +public class ChoiceStringsTest : ParseHelper { + [TestMethod] + public void parsesAnyOfTheChoiceOptions(){ + expect(Choices.parse("choice-abc: a")).toMatch(node("a", 12)); + expect(Choices.parse("choice-abc: b")).toMatch(node("b", 12)); + expect(Choices.parse("choice-abc: c")).toMatch(node("c", 12)); + } + + [TestMethod] + [ExpectedException(typeof(ParseError), + "Expected a ParseError")] + public void rejectsInputMatchingNoneOfTheOptions() { + Choices.parse("choice-abc: d"); + } + + [TestMethod] + [ExpectedException(typeof(ParseError), + "Expected a ParseError")] + public void rejectsSuperstringsOfTheOptions() { + Choices.parse("choice-abc: ab"); + } + + [TestMethod] + public void parsesAChoiceAsPartOfASequence(){ + expect(Choices.parse("choice-seq: repeat")).toMatch( + node("repeat", 12) + .elem(node("re", 12).noElems()) + .elem(node("peat", 14).noElems()) + ); + } + + [TestMethod] + [ExpectedException(typeof(ParseError), + "Expected a ParseError")] + public void doesNotBacktrackIfLaterRulesFail() { + Choices.parse("choice-seq: reppeat"); + } +} + +[TestClass] +public class ChoiceRepetitionTest : ParseHelper { + [TestMethod] + public void parsesADifferentOptionOnEachIteration(){ + expect(Choices.parse("choice-rep: abcabba")).toMatch( + node("abcabba", 12) + .elem(node("a", 12).noElems()) + .elem(node("b", 13).noElems()) + .elem(node("c", 14).noElems()) + .elem(node("a", 15).noElems()) + .elem(node("b", 16).noElems()) + .elem(node("b", 17).noElems()) + .elem(node("a", 18).noElems()) + ); + } + + [TestMethod] + [ExpectedException(typeof(ParseError), + "Expected a ParseError")] + public void rejectsIfAnyIterationDoesNotMatchTheOptions() { + Choices.parse("choice-rep: abcadba"); + } +} + +[TestClass] +public class ChoiceSequenceTest : ParseHelper { + [TestMethod] + public void parsesOneBranchOfTheChoice(){ + expect(Choices.parse("choice-bind: ab")).toMatch( + node("ab", 13) + .elem(node("a", 13).noElems()) + .elem(node("b", 14).noElems()) + ); + } + + [TestMethod] + [ExpectedException(typeof(ParseError), + "Expected a ParseError")] + public void testBindsSequencesTighterThanChoices() { + Choices.parse("choice-bind: abef"); + } +} + +public class ParseHelper { + public Node