21 Commits
Author SHA1 Message Date
josh 66f95cb6d8 v2.3.0 2025-07-28 20:40:24 -04:00
josh dbe0bf8ad0 Add github workflow to run unit tests 2025-07-28 20:35:06 -04:00
josh 75fb627602 Update CHANGELOG 2025-07-28 18:44:00 -04:00
josh 7ccb4c8730 Parallelize parser table generation on Linux hosts 2025-07-28 18:43:12 -04:00
josh 962b7125ec Build lookahead reduce actions for item sets before building reduce table 2025-07-27 17:24:35 -04:00
josh 2df27b04fe Update CHANGELOG 2025-07-27 14:56:09 -04:00
josh 17f1454a4f Speedup: avoid checking the same rule set more than once for lookahead tokens 2025-07-27 14:50:18 -04:00
josh b371f4b404 Speedup: calculate Item#hash once 2025-07-27 13:48:52 -04:00
josh 91f476187b Update CHANGELOG 2025-07-27 10:32:57 -04:00
josh 54a0629e60 Add spec for \s matching newline 2025-07-26 21:36:05 -04:00
josh 69aa3097c3 Update user guide for shorthand character classes 2025-07-26 21:34:22 -04:00
josh 5486e5f138 Add \D, \S, \w, \W special character classes 2025-07-26 21:21:29 -04:00
josh 5b243507cf Show grammar line numbers for regex errors 2025-07-25 21:27:20 -04:00
josh 25d6e3bc34 Fix user guide regular expression example 2025-07-25 16:24:05 -04:00
josh 035bb2fc60 Fix matching lexer patterns containing a negated character class which includes a backslash-escaped sequence that matches more than one character 2025-07-25 16:14:35 -04:00
josh 125c149750 Drop argument to CharacterRangeUnit constructor when possible 2025-07-22 21:53:18 -04:00
josh 9ef80e61d4 Update gems 2025-07-22 21:22:27 -04:00
josh 5b94b03b04 Fix user guide double backslashes in examples 2025-07-22 21:17:18 -04:00
josh 9d686989ec Fix C issue not fully initializing pvalues
This issue manifested when multiple ptypes are present with different sizes.
2025-07-22 21:05:32 -04:00
josh 87d892d0a3 Fix a couple clang warnings 2025-07-21 23:26:37 -04:00
josh 4ec57fa48d Fix vim syntax highlighting for a regex ending with an escaped backslash 2025-07-20 17:16:59 -04:00
17 changed files with 443 additions and 123 deletions

No files matched your search

+38
View File
@@ -0,0 +1,38 @@
name: Run Propane Tests
on:
push:
branches:
- master
pull_request:
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
ruby-version: ['3.4']
steps:
- name: Install dependencies (Linux)
if: runner.os == 'Linux'
run: sudo apt-get update && sudo apt-get install -y gcc gdc ldc
- name: Install dependencies (macOS)
if: runner.os == 'macOS'
run: brew install gcc ldc
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ matrix.ruby-version }}
- name: Install dependencies
run: bundle install
- name: Run tests
run: rake all
+25
View File
@@ -1,3 +1,28 @@
## v2.3.0
### New Features
- Add \D, \S, \w, \W special character classes
### Improvements
- Include line numbers for pattern errors
- Improve performance in a few places
- Parallelize parser table generation on Linux hosts
- Add github workflow to run unit tests
### Fixes
- Fix a couple clang warnings for C backend
- Fix C backend not fully initializing pvalues when multiple ptypes are used with different sizes.
- Fix some user guide examples
## v2.2.1
### Fixes
- Fix GC issue for D backend when AST is enabled (#36)
## v2.2.0 ## v2.2.0
### Improvements ### Improvements
+14 -12
View File
@@ -1,37 +1,39 @@
GEM GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
base64 (0.2.0) base64 (0.3.0)
date (3.4.1) date (3.4.1)
diff-lcs (1.6.1) diff-lcs (1.6.2)
docile (1.4.1) docile (1.4.1)
psych (5.2.3) erb (5.0.2)
psych (5.2.6)
date date
stringio stringio
rake (13.2.1) rake (13.3.0)
rdoc (6.13.1) rdoc (6.14.2)
erb
psych (>= 4.0.0) psych (>= 4.0.0)
redcarpet (3.6.1) redcarpet (3.6.1)
rspec (3.13.0) rspec (3.13.1)
rspec-core (~> 3.13.0) rspec-core (~> 3.13.0)
rspec-expectations (~> 3.13.0) rspec-expectations (~> 3.13.0)
rspec-mocks (~> 3.13.0) rspec-mocks (~> 3.13.0)
rspec-core (3.13.3) rspec-core (3.13.5)
rspec-support (~> 3.13.0) rspec-support (~> 3.13.0)
rspec-expectations (3.13.3) rspec-expectations (3.13.5)
diff-lcs (>= 1.2.0, < 2.0) diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0) rspec-support (~> 3.13.0)
rspec-mocks (3.13.2) rspec-mocks (3.13.5)
diff-lcs (>= 1.2.0, < 2.0) diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0) rspec-support (~> 3.13.0)
rspec-support (3.13.2) rspec-support (3.13.4)
simplecov (0.22.0) simplecov (0.22.0)
docile (~> 1.1) docile (~> 1.1)
simplecov-html (~> 0.11) simplecov-html (~> 0.11)
simplecov_json_formatter (~> 0.1) simplecov_json_formatter (~> 0.1)
simplecov-html (0.13.1) simplecov-html (0.13.2)
simplecov_json_formatter (0.1.4) simplecov_json_formatter (0.1.4)
stringio (3.1.6) stringio (3.1.7)
syntax (1.2.2) syntax (1.2.2)
PLATFORMS PLATFORMS
+16 -6
View File
@@ -55,7 +55,8 @@ const char * <%= @grammar.prefix %>token_names[] = {
void <%= @grammar.prefix %>context_init(<%= @grammar.prefix %>context_t * context, uint8_t const * input, size_t input_length) void <%= @grammar.prefix %>context_init(<%= @grammar.prefix %>context_t * context, uint8_t const * input, size_t input_length)
{ {
/* New default-initialized context structure. */ /* New default-initialized context structure. */
<%= @grammar.prefix %>context_t newcontext = {0}; <%= @grammar.prefix %>context_t newcontext;
memset(&newcontext, 0, sizeof(newcontext));
/* Lexer initialization. */ /* Lexer initialization. */
newcontext.input = input; newcontext.input = input;
@@ -344,8 +345,10 @@ static lexer_state_id_t check_lexer_transition(uint32_t current_state, uint32_t
static size_t find_longest_match(<%= @grammar.prefix %>context_t * context, static size_t find_longest_match(<%= @grammar.prefix %>context_t * context,
lexer_match_info_t * out_match_info, size_t * out_unexpected_input_length) lexer_match_info_t * out_match_info, size_t * out_unexpected_input_length)
{ {
lexer_match_info_t longest_match = {0}; lexer_match_info_t longest_match;
lexer_match_info_t attempt_match = {0}; memset(&longest_match, 0, sizeof(longest_match));
lexer_match_info_t attempt_match;
memset(&attempt_match, 0, sizeof(attempt_match));
*out_match_info = longest_match; *out_match_info = longest_match;
uint32_t current_state = lexer_mode_table[context->mode].state_table_offset; uint32_t current_state = lexer_mode_table[context->mode].state_table_offset;
for (;;) for (;;)
@@ -359,6 +362,7 @@ static size_t find_longest_match(<%= @grammar.prefix %>context_t * context,
switch (result) switch (result)
{ {
case P_SUCCESS: case P_SUCCESS:
{
lexer_state_id_t transition_state = check_lexer_transition(current_state, code_point); lexer_state_id_t transition_state = check_lexer_transition(current_state, code_point);
if (transition_state != INVALID_LEXER_STATE_ID) if (transition_state != INVALID_LEXER_STATE_ID)
{ {
@@ -390,6 +394,7 @@ static size_t find_longest_match(<%= @grammar.prefix %>context_t * context,
*out_unexpected_input_length = attempt_match.length + code_point_length; *out_unexpected_input_length = attempt_match.length + code_point_length;
return P_UNEXPECTED_INPUT; return P_UNEXPECTED_INPUT;
} }
}
break; break;
case P_EOF: case P_EOF:
@@ -447,7 +452,8 @@ static size_t find_longest_match(<%= @grammar.prefix %>context_t * context,
*/ */
static size_t attempt_lex_token(<%= @grammar.prefix %>context_t * context, <%= @grammar.prefix %>token_info_t * out_token_info) static size_t attempt_lex_token(<%= @grammar.prefix %>context_t * context, <%= @grammar.prefix %>token_info_t * out_token_info)
{ {
<%= @grammar.prefix %>token_info_t token_info = {0}; <%= @grammar.prefix %>token_info_t token_info;
memset(&token_info, 0, sizeof(token_info));
token_info.position = context->text_position; token_info.position = context->text_position;
token_info.token = INVALID_TOKEN_ID; token_info.token = INVALID_TOKEN_ID;
lexer_match_info_t match_info; lexer_match_info_t match_info;
@@ -456,6 +462,7 @@ static size_t attempt_lex_token(<%= @grammar.prefix %>context_t * context, <%= @
switch (result) switch (result)
{ {
case P_SUCCESS: case P_SUCCESS:
{
<%= @grammar.prefix %>token_t token_to_accept = match_info.accepting_state->token; <%= @grammar.prefix %>token_t token_to_accept = match_info.accepting_state->token;
if (match_info.accepting_state->code_id != INVALID_USER_CODE_ID) if (match_info.accepting_state->code_id != INVALID_USER_CODE_ID)
{ {
@@ -507,6 +514,7 @@ static size_t attempt_lex_token(<%= @grammar.prefix %>context_t * context, <%= @
token_info.end_position.col = token_info.position.col + match_info.end_delta_position.col; token_info.end_position.col = token_info.position.col + match_info.end_delta_position.col;
} }
*out_token_info = token_info; *out_token_info = token_info;
}
return P_SUCCESS; return P_SUCCESS;
case P_EOF: case P_EOF:
@@ -999,7 +1007,8 @@ size_t <%= @grammar.prefix %>parse(<%= @grammar.prefix %>context_t * context)
state_values_stack_index(&statevalues, -1)->ast_node = reduced_parser_node; state_values_stack_index(&statevalues, -1)->ast_node = reduced_parser_node;
<% else %> <% else %>
state_values_stack_index(&statevalues, -1)->pvalue = reduced_parser_value; state_values_stack_index(&statevalues, -1)->pvalue = reduced_parser_value;
<%= @grammar.prefix %>value_t new_parse_result = {0}; <%= @grammar.prefix %>value_t new_parse_result;
memset(&new_parse_result, 0, sizeof(new_parse_result));
reduced_parser_value = new_parse_result; reduced_parser_value = new_parse_result;
<% end %> <% end %>
reduced_rule_set = INVALID_ID; reduced_rule_set = INVALID_ID;
@@ -1061,7 +1070,8 @@ size_t <%= @grammar.prefix %>parse(<%= @grammar.prefix %>context_t * context)
reduced_parser_node = NULL; reduced_parser_node = NULL;
} }
<% else %> <% else %>
<%= @grammar.prefix %>value_t reduced_parser_value2 = {0}; <%= @grammar.prefix %>value_t reduced_parser_value2;
memset(&reduced_parser_value2, 0, sizeof(reduced_parser_value2));
if (parser_user_code(&reduced_parser_value2, parser_reduce_table[reduce_index].rule, &statevalues, parser_reduce_table[reduce_index].n_states, context) == P_USER_TERMINATED) if (parser_user_code(&reduced_parser_value2, parser_reduce_table[reduce_index].rule, &statevalues, parser_reduce_table[reduce_index].n_states, context) == P_USER_TERMINATED)
{ {
return P_USER_TERMINATED; return P_USER_TERMINATED;
+21 -18
View File
@@ -67,10 +67,10 @@ import std.math;
ptype ulong; ptype ulong;
# A few basic arithmetic operators. # A few basic arithmetic operators.
token plus /\\+/; token plus /\+/;
token times /\\*/; token times /\*/;
token power /\\*\\*/; token power /\*\*/;
token integer /\\d+/ << token integer /\d+/ <<
ulong v; ulong v;
foreach (c; match) foreach (c; match)
{ {
@@ -79,10 +79,10 @@ token integer /\\d+/ <<
} }
$$ = v; $$ = v;
>> >>
token lparen /\\(/; token lparen /\(/;
token rparen /\\)/; token rparen /\)/;
# Drop whitespace. # Drop whitespace.
drop /\\s+/; drop /\s+/;
Start -> E1 << $$ = $1; >> Start -> E1 << $$ = $1; >>
E1 -> E2 << $$ = $1; >> E1 -> E2 << $$ = $1; >>
@@ -155,7 +155,7 @@ Example:
``` ```
ptype ulong; ptype ulong;
token integer /\\d+/ << token integer /\d+/ <<
ulong v; ulong v;
foreach (c; match) foreach (c; match)
{ {
@@ -228,9 +228,9 @@ token two /2/;
token comma /,/ << token comma /,/ <<
$$ = 42; $$ = 42;
>> >>
token lparen /\\(/; token lparen /\(/;
token rparen /\\)/; token rparen /\)/;
drop /\\s+/; drop /\s+/;
Start -> Items; Start -> Items;
@@ -432,10 +432,10 @@ A regular expression begins and ends with a `/` character.
Example: Example:
``` ```
/#.*$/ /#.*/
``` ```
Regular expressions can include many special characters: Regular expressions can include many special characters/sequences:
* The `.` character matches any input character other than a newline. * The `.` character matches any input character other than a newline.
* The `*` character matches any number of the previous regex element. * The `*` character matches any number of the previous regex element.
@@ -447,14 +447,17 @@ Regular expressions can include many special characters:
* The `\` character escapes the following character and changes its meaning: * The `\` character escapes the following character and changes its meaning:
* The `\a` sequence matches an ASCII bell character (0x07). * The `\a` sequence matches an ASCII bell character (0x07).
* The `\b` sequence matches an ASCII backspace character (0x08). * The `\b` sequence matches an ASCII backspace character (0x08).
* The `\d` sequence matches any character `0` through `9`. * The `\d` sequence is shorthand for the `[0-9]` character class.
* The `\D` sequence matches every code point not matched by `\d`.
* The `\f` sequence matches an ASCII form feed character (0x0C). * The `\f` sequence matches an ASCII form feed character (0x0C).
* The `\n` sequence matches an ASCII new line character (0x0A). * The `\n` sequence matches an ASCII new line character (0x0A).
* The `\r` sequence matches an ASCII carriage return character (0x0D). * The `\r` sequence matches an ASCII carriage return character (0x0D).
* The `\s` sequence matches a space, horizontal tab `\t`, carriage return * The `\s` sequence is shorthand for the `[ \t\r\n\f\v]` character class.
`\r`, a form feed `\f`, or a vertical tab `\v` character. * The `\S` sequence matches every code point not matched by `\s`.
* The `\t` sequence matches an ASCII tab character (0x09). * The `\t` sequence matches an ASCII tab character (0x09).
* The `\v` sequence matches an ASCII vertical tab character (0x0B). * The `\v` sequence matches an ASCII vertical tab character (0x0B).
* The `\w` sequence is shorthand for the `[a-zA-Z0-9_]` character class.
* The `\W` sequence matches every code point not matched by `\w`.
* Any other character matches itself. * Any other character matches itself.
* The `|` character creates an alternate match. * The `|` character creates an alternate match.
@@ -552,10 +555,10 @@ default, identonly: token ident /[a-z]+/ <<
$mode(default); $mode(default);
return $token(ident); return $token(ident);
>> >>
token dot /\\./ << token dot /\./ <<
$mode(identonly); $mode(identonly);
>> >>
default, identonly: drop /\\s+/; default, identonly: drop /\s+/;
``` ```
##> Specifying parser value types - the `ptype` statement ##> Specifying parser value types - the `ptype` statement
+1 -1
View File
@@ -22,7 +22,7 @@ syn match propaneFieldOperator ":" contained
syn match propaneOperator "?" syn match propaneOperator "?"
syn keyword propaneKeyword ast ast_prefix ast_suffix drop module prefix ptype start token tokenid syn keyword propaneKeyword ast ast_prefix ast_suffix drop module prefix ptype start token tokenid
syn region propaneRegex start="/" end="/" skip="\\/" syn region propaneRegex start="/" end="/" skip="\v\\\\|\\/"
hi def link propaneComment Comment hi def link propaneComment Comment
hi def link propaneKeyword Keyword hi def link propaneKeyword Keyword
+117 -37
View File
@@ -64,24 +64,13 @@ class Propane
@shift_table = [] @shift_table = []
@reduce_table = [] @reduce_table = []
@item_sets.each do |item_set| @item_sets.each do |item_set|
shift_entries = item_set.next_symbols.map do |next_symbol|
state_id =
if next_symbol.name == "$EOF"
0
else
item_set.next_item_set[next_symbol].id
end
{
symbol: next_symbol,
state_id: state_id,
}
end
unless item_set.reduce_rules.empty? unless item_set.reduce_rules.empty?
shift_entries.each do |shift_entry| item_set.shift_entries.each do |shift_entry|
token = shift_entry[:symbol] token = shift_entry[:symbol]
if get_lookahead_reduce_actions_for_item_set(item_set).include?(token) if item_set.reduce_actions
rule = item_set.reduce_actions[token] if rule = item_set.reduce_actions[token]
@warnings << "Shift/Reduce conflict (state #{item_set.id}) between token #{token.name} and rule #{rule.name} (defined on line #{rule.line_number})" @warnings << "Shift/Reduce conflict (state #{item_set.id}) between token #{token.name} and rule #{rule.name} (defined on line #{rule.line_number})"
end
end end
end end
end end
@@ -101,11 +90,11 @@ class Propane
end end
@state_table << { @state_table << {
shift_index: @shift_table.size, shift_index: @shift_table.size,
n_shifts: shift_entries.size, n_shifts: item_set.shift_entries.size,
reduce_index: @reduce_table.size, reduce_index: @reduce_table.size,
n_reduces: reduce_entries.size, n_reduces: reduce_entries.size,
} }
@shift_table += shift_entries @shift_table += item_set.shift_entries
@reduce_table += reduce_entries @reduce_table += reduce_entries
end end
end end
@@ -125,8 +114,110 @@ class Propane
# @return [void] # @return [void]
def build_reduce_actions! def build_reduce_actions!
@item_sets.each do |item_set| @item_sets.each do |item_set|
build_shift_entries(item_set)
build_reduce_actions_for_item_set(item_set) build_reduce_actions_for_item_set(item_set)
end end
item_sets_to_process = @item_sets.select do |item_set|
# We need lookahead reduce actions if:
# 1) There is more than one possible rule to reduce. In this case the
# lookahead token can help choose which rule to reduce.
# 2) There is at least one shift action and one reduce action for
# this item set. In this case the lookahead reduce actions are
# needed to test for a Shift/Reduce conflict.
item_set.reduce_rules.size > 1 ||
(item_set.reduce_rules.size > 0 && item_set.shift_entries.size > 0)
end
if RbConfig::CONFIG["host_os"] =~ /linux/
item_sets_by_id = {}
item_sets_to_process.each do |item_set|
item_sets_by_id[item_set.object_id] = item_set
end
tokens_by_id = {}
@grammar.tokens.each do |token|
tokens_by_id[token.object_id] = token
end
rules_by_id = {}
@grammar.rules.each do |rule|
rules_by_id[rule.object_id] = rule
end
n_threads = Util.determine_n_threads
semaphore = Mutex.new
queue = Queue.new
threads = {}
n_threads.times do
piper, pipew = IO.pipe
thread = Thread.new do
loop do
item_set = nil
semaphore.synchronize do
item_set = item_sets_to_process.slice!(0)
end
break if item_set.nil?
fork do
piper.close
build_lookahead_reduce_actions_for_item_set(item_set, pipew)
end
end
queue.push(Thread.current)
end
threads[thread] = [piper, pipew]
end
until threads.empty?
thread = queue.pop
piper, pipew = threads[thread]
pipew.close
thread_txt = piper.read
thread_txt.each_line do |line|
if line.start_with?("RA,")
parts = line.split(",")
item_set_id, token_id, rule_id = parts[1..3].map(&:to_i)
item_set = item_sets_by_id[item_set_id]
unless item_set
raise "Internal error: could not find item set from thread"
end
token = tokens_by_id[token_id]
unless item_set
raise "Internal error: could not find token from thread"
end
rule = rules_by_id[rule_id]
unless item_set
raise "Internal error: could not find rule from thread"
end
item_set.reduce_actions ||= {}
item_set.reduce_actions[token] = rule
elsif line.start_with?("Error: ")
@errors << line.chomp
else
raise "Internal error: unhandled thread line #{line}"
end
end
thread.join
threads.delete(thread)
end
else
# Fall back to single threaded algorithm.
item_sets_to_process.each do |item_set|
item_set.reduce_actions = build_lookahead_reduce_actions_for_item_set(item_set)
end
end
end
# Build the shift entries for a single item set.
#
# @return [void]
def build_shift_entries(item_set)
item_set.shift_entries = item_set.next_symbols.map do |next_symbol|
state_id =
if next_symbol.name == "$EOF"
0
else
item_set.next_item_set[next_symbol].id
end
{
symbol: next_symbol,
state_id: state_id,
}
end
end end
# Build the reduce actions for a single item set (parser state). # Build the reduce actions for a single item set (parser state).
@@ -145,33 +236,18 @@ class Propane
if item_set.reduce_rules.size == 1 if item_set.reduce_rules.size == 1
item_set.reduce_rule = item_set.reduce_rules.first item_set.reduce_rule = item_set.reduce_rules.first
end end
if item_set.reduce_rules.size > 1
# Force item_set.reduce_actions to be built to store the lookahead
# tokens for the possible reduce rules if there is more than one.
get_lookahead_reduce_actions_for_item_set(item_set)
end
end
# Get the reduce actions for a single item set (parser state).
#
# @param item_set [ItemSet]
# ItemSet (parser state)
#
# @return [Hash]
# Mapping of lookahead Tokens to the Rules to reduce.
def get_lookahead_reduce_actions_for_item_set(item_set)
item_set.reduce_actions ||= build_lookahead_reduce_actions_for_item_set(item_set)
end end
# Build the reduce actions for a single item set (parser state). # Build the reduce actions for a single item set (parser state).
# #
# @param item_set [ItemSet] # @param item_set [ItemSet]
# ItemSet (parser state) # ItemSet (parser state)
# @param fh [File]
# Output file handle for multiprocessing mode.
# #
# @return [Hash] # @return [Hash]
# Mapping of lookahead Tokens to the Rules to reduce. # Mapping of lookahead Tokens to the Rules to reduce.
def build_lookahead_reduce_actions_for_item_set(item_set) def build_lookahead_reduce_actions_for_item_set(item_set, fh = nil)
# We will be looking for all possible tokens that can follow instances of # We will be looking for all possible tokens that can follow instances of
# these rules. Rather than looking through the entire grammar for the # these rules. Rather than looking through the entire grammar for the
# possible following tokens, we will only look in the item sets leading # possible following tokens, we will only look in the item sets leading
@@ -182,9 +258,12 @@ class Propane
lookahead_tokens_for_rule = build_lookahead_tokens_to_reduce(reduce_rule, item_sets) lookahead_tokens_for_rule = build_lookahead_tokens_to_reduce(reduce_rule, item_sets)
lookahead_tokens_for_rule.each do |lookahead_token| lookahead_tokens_for_rule.each do |lookahead_token|
if existing_reduce_rule = reduce_actions[lookahead_token] if existing_reduce_rule = reduce_actions[lookahead_token]
@errors << "Error: reduce/reduce conflict (state #{item_set.id}) between rule #{existing_reduce_rule.name}##{existing_reduce_rule.id} (defined on line #{existing_reduce_rule.line_number}) and rule #{reduce_rule.name}##{reduce_rule.id} (defined on line #{reduce_rule.line_number}) for lookahead token #{lookahead_token}" error = "Error: reduce/reduce conflict (state #{item_set.id}) between rule #{existing_reduce_rule.name}##{existing_reduce_rule.id} (defined on line #{existing_reduce_rule.line_number}) and rule #{reduce_rule.name}##{reduce_rule.id} (defined on line #{reduce_rule.line_number}) for lookahead token #{lookahead_token}"
@errors << error
fh.puts(error) if fh
end end
reduce_actions[lookahead_token] = reduce_rule reduce_actions[lookahead_token] = reduce_rule
fh.puts "RA,#{item_set.object_id},#{lookahead_token.object_id},#{reduce_rule.object_id}" if fh
end end
reduce_actions reduce_actions
end end
@@ -231,6 +310,7 @@ class Propane
rule_set = item.rule.rule_set rule_set = item.rule.rule_set
unless checked_rule_sets.include?(rule_set) unless checked_rule_sets.include?(rule_set)
rule_sets_to_check_after << rule_set rule_sets_to_check_after << rule_set
checked_rule_sets << rule_set
end end
break break
when Token when Token
+2 -1
View File
@@ -22,6 +22,7 @@ class Propane
def initialize(rule, position) def initialize(rule, position)
@rule = rule @rule = rule
@position = position @position = position
@_hash = [@rule, @position].hash
end end
# Hash function. # Hash function.
@@ -29,7 +30,7 @@ class Propane
# @return [Integer] # @return [Integer]
# Hash code. # Hash code.
def hash def hash
[@rule, @position].hash @_hash
end end
# Compare Item objects. # Compare Item objects.
+4
View File
@@ -34,6 +34,10 @@ class Propane
# more than one rule that could be reduced. # more than one rule that could be reduced.
attr_accessor :reduce_actions attr_accessor :reduce_actions
# @return [Array<Hash>]
# Shift table entries.
attr_accessor :shift_entries
# Build an ItemSet. # Build an ItemSet.
# #
# @param items [Array<Item>] # @param items [Array<Item>]
+1 -1
View File
@@ -55,7 +55,7 @@ class Propane
@line_number = options[:line_number] @line_number = options[:line_number]
@modes = options[:modes] @modes = options[:modes]
@ptypename = options[:ptypename] @ptypename = options[:ptypename]
regex = Regex.new(@pattern) regex = Regex.new(@pattern, @line_number)
regex.nfa.end_state.accepts = self regex.nfa.end_state.accepts = self
@nfa = regex.nfa @nfa = regex.nfa
end end
+48 -17
View File
@@ -4,12 +4,13 @@ class Propane
attr_reader :unit attr_reader :unit
attr_reader :nfa attr_reader :nfa
def initialize(pattern) def initialize(pattern, line_number)
@pattern = pattern.dup @pattern = pattern.dup
@line_number = line_number
@unit = parse_alternates @unit = parse_alternates
@nfa = @unit.to_nfa @nfa = @unit.to_nfa
if @pattern != "" if @pattern != ""
raise Error.new(%[Unexpected "#{@pattern}" in pattern]) raise Error.new(%[Line #{@line_number}: unexpected "#{@pattern}" in pattern])
end end
end end
@@ -41,7 +42,7 @@ class Propane
mu = MultiplicityUnit.new(last_unit, min_count, max_count) mu = MultiplicityUnit.new(last_unit, min_count, max_count)
au.replace_last!(mu) au.replace_last!(mu)
else else
raise Error.new("#{c} follows nothing") raise Error.new("Line #{@line_number}: #{c} follows nothing")
end end
when "|" when "|"
au.new_alternate! au.new_alternate!
@@ -59,7 +60,7 @@ class Propane
def parse_group def parse_group
au = parse_alternates au = parse_alternates
if @pattern[0] != ")" if @pattern[0] != ")"
raise Error.new("Unterminated group in pattern") raise Error.new("Line #{@line_number}: unterminated group in pattern")
end end
@pattern.slice!(0) @pattern.slice!(0)
au au
@@ -70,7 +71,7 @@ class Propane
index = 0 index = 0
loop do loop do
if @pattern == "" if @pattern == ""
raise Error.new("Unterminated character class") raise Error.new("Line #{@line_number}: unterminated character class")
end end
c = @pattern.slice!(0) c = @pattern.slice!(0)
if c == "]" if c == "]"
@@ -84,13 +85,13 @@ class Propane
elsif c == "-" && @pattern[0] != "]" elsif c == "-" && @pattern[0] != "]"
begin_cu = ccu.last_unit begin_cu = ccu.last_unit
unless begin_cu.is_a?(CharacterRangeUnit) && begin_cu.code_point_range.size == 1 unless begin_cu.is_a?(CharacterRangeUnit) && begin_cu.code_point_range.size == 1
raise Error.new("Character range must be between single characters") raise Error.new("Line #{@line_number}: character range must be between single characters")
end end
if @pattern[0] == "\\" if @pattern[0] == "\\"
@pattern.slice!(0) @pattern.slice!(0)
end_cu = parse_backslash end_cu = parse_backslash
unless end_cu.is_a?(CharacterRangeUnit) && end_cu.code_point_range.size == 1 unless end_cu.is_a?(CharacterRangeUnit) && end_cu.code_point_range.size == 1
raise Error.new("Character range must be between single characters") raise Error.new("Line #{@line_number}: character range must be between single characters")
end end
max_code_point = end_cu.code_point max_code_point = end_cu.code_point
else else
@@ -116,7 +117,7 @@ class Propane
elsif max_count.to_s != "" elsif max_count.to_s != ""
max_count = max_count.to_i max_count = max_count.to_i
if max_count < min_count if max_count < min_count
raise Error.new("Maximum repetition count cannot be less than minimum repetition count") raise Error.new("Line #{@line_number}: maximum repetition count cannot be less than minimum repetition count")
end end
else else
max_count = nil max_count = nil
@@ -124,28 +125,33 @@ class Propane
@pattern = pattern @pattern = pattern
[min_count, max_count] [min_count, max_count]
else else
raise Error.new("Unexpected match count at #{@pattern}") raise Error.new("Line #{@line_number}: unexpected match count following {")
end end
end end
def parse_backslash def parse_backslash
if @pattern == "" if @pattern == ""
raise Error.new("Error: unfollowed \\") raise Error.new("Line #{@line_number}: error: unfollowed \\")
else else
c = @pattern.slice!(0) c = @pattern.slice!(0)
case c case c
when "a" when "a"
CharacterRangeUnit.new("\a", "\a") CharacterRangeUnit.new("\a")
when "b" when "b"
CharacterRangeUnit.new("\b", "\b") CharacterRangeUnit.new("\b")
when "d" when "d"
CharacterRangeUnit.new("0", "9") CharacterRangeUnit.new("0", "9")
when "D"
ccu = CharacterClassUnit.new
ccu << CharacterRangeUnit.new("0", "9")
ccu.negate = true
ccu
when "f" when "f"
CharacterRangeUnit.new("\f", "\f") CharacterRangeUnit.new("\f")
when "n" when "n"
CharacterRangeUnit.new("\n", "\n") CharacterRangeUnit.new("\n")
when "r" when "r"
CharacterRangeUnit.new("\r", "\r") CharacterRangeUnit.new("\r")
when "s" when "s"
ccu = CharacterClassUnit.new ccu = CharacterClassUnit.new
ccu << CharacterRangeUnit.new(" ") ccu << CharacterRangeUnit.new(" ")
@@ -155,10 +161,35 @@ class Propane
ccu << CharacterRangeUnit.new("\f") ccu << CharacterRangeUnit.new("\f")
ccu << CharacterRangeUnit.new("\v") ccu << CharacterRangeUnit.new("\v")
ccu ccu
when "S"
ccu = CharacterClassUnit.new
ccu << CharacterRangeUnit.new(" ")
ccu << CharacterRangeUnit.new("\t")
ccu << CharacterRangeUnit.new("\r")
ccu << CharacterRangeUnit.new("\n")
ccu << CharacterRangeUnit.new("\f")
ccu << CharacterRangeUnit.new("\v")
ccu.negate = true
ccu
when "t" when "t"
CharacterRangeUnit.new("\t", "\t") CharacterRangeUnit.new("\t")
when "v" when "v"
CharacterRangeUnit.new("\v", "\v") CharacterRangeUnit.new("\v")
when "w"
ccu = CharacterClassUnit.new
ccu << CharacterRangeUnit.new("_")
ccu << CharacterRangeUnit.new("0", "9")
ccu << CharacterRangeUnit.new("a", "z")
ccu << CharacterRangeUnit.new("A", "Z")
ccu
when "W"
ccu = CharacterClassUnit.new
ccu << CharacterRangeUnit.new("_")
ccu << CharacterRangeUnit.new("0", "9")
ccu << CharacterRangeUnit.new("a", "z")
ccu << CharacterRangeUnit.new("A", "Z")
ccu.negate = true
ccu
else else
CharacterRangeUnit.new(c) CharacterRangeUnit.new(c)
end end
+10 -7
View File
@@ -92,16 +92,19 @@ class Propane
@units = [] @units = []
@negate = false @negate = false
end end
def initialize def method_missing(*args, &block)
@units = [] @units.__send__(*args, &block)
end
def method_missing(*args)
@units.__send__(*args)
end end
def <<(thing) def <<(thing)
if thing.is_a?(CharacterClassUnit) if thing.is_a?(CharacterClassUnit)
thing.each do |ccu_unit| if thing.negate
@units << ccu_unit CodePointRange.invert_ranges(thing.map(&:code_point_range)).each do |cpr|
CharacterRangeUnit.new(cpr.first, cpr.last)
end
else
thing.each do |ccu_unit|
@units << ccu_unit
end
end end
else else
@units << thing @units << thing
+26
View File
@@ -10,6 +10,32 @@ class Propane
"#{s}\n* #{message} *\n#{s}\n" "#{s}\n* #{message} *\n#{s}\n"
end end
# Determine the number of threads to use.
#
# @return [Integer]
# The number of threads to use.
def determine_n_threads
# Try to figure out how many threads are available on the host hardware.
begin
case RbConfig::CONFIG["host_os"]
when /linux/
return File.read("/proc/cpuinfo").scan(/^processor\s*:/).size
when /mswin|mingw|msys/
if `wmic cpu get NumberOfLogicalProcessors -value` =~ /NumberOfLogicalProcessors=(\d+)/
return $1.to_i
end
when /darwin/
if `sysctl -n hw.ncpu` =~ /(\d+)/
return $1.to_i
end
end
rescue
end
# If we can't figure it out, default to 4.
4
end
end end
end end
+1 -1
View File
@@ -1,3 +1,3 @@
class Propane class Propane
VERSION = "2.2.1" VERSION = "2.3.0"
end end
+68
View File
@@ -126,6 +126,74 @@ EOF
] ]
expect(run(<<EOF, ";")).to eq expected expect(run(<<EOF, ";")).to eq expected
token semicolon /;/; token semicolon /;/;
EOF
end
it "matches a negated character class" do
expected = [
["pattern", "/abc/"],
]
expect(run(<<EOF, "/abc/")).to eq expected
token pattern /\\/[^\\s]*\\//;
EOF
end
it "matches special character classes " do
expected = [
["a", "abc123_FOO"],
]
expect(run(<<EOF, "abc123_FOO")).to eq expected
token a /\\w+/;
EOF
expected = [
["b", "FROG*%$#"],
]
expect(run(<<EOF, "FROG*%$#")).to eq expected
token b /FROG\\D{1,4}/;
EOF
expected = [
["c", "$883366"],
]
expect(run(<<EOF, "$883366")).to eq expected
token c /$\\d+/;
EOF
expected = [
["d", "^&$@"],
]
expect(run(<<EOF, "^&$@")).to eq expected
token d /^\\W+/;
EOF
expected = [
["a", "abc123_FOO"],
[nil, " "],
["b", "FROG*%$#"],
[nil, " "],
["c", "$883366"],
[nil, " "],
["d", "^&$@"],
]
expect(run(<<EOF, "abc123_FOO FROG*%$# $883366 ^&$@")).to eq expected
token a /\\w+/;
token b /FROG\\D{1,4}/;
token c /$\\d+/;
token d /^\\W+/;
drop /\\s+/;
EOF
end
it "matches a negated character class with a nested inner negated character class" do
expected = [
["t", "$&*"],
]
expect(run(<<EOF, "$&*")).to eq expected
token t /[^%\\W]+/;
EOF
end
it "\\s matches a newline" do
expected = [["s", "\n"]]
expect(run(<<EOF, "\n")).to eq expected
token s /\\s/;
EOF EOF
end end
end end
+39 -22
View File
@@ -2,14 +2,14 @@ class Propane
RSpec.describe Regex do RSpec.describe Regex do
it "parses an empty expression" do it "parses an empty expression" do
regex = Regex.new("") regex = Regex.new("", 1)
expect(regex.unit).to be_a Regex::AlternatesUnit expect(regex.unit).to be_a Regex::AlternatesUnit
expect(regex.unit.alternates.size).to eq 1 expect(regex.unit.alternates.size).to eq 1
expect(regex.unit.alternates[0].size).to eq 0 expect(regex.unit.alternates[0].size).to eq 0
end end
it "parses a single character unit expression" do it "parses a single character unit expression" do
regex = Regex.new("a") regex = Regex.new("a", 1)
expect(regex.unit).to be_a Regex::AlternatesUnit expect(regex.unit).to be_a Regex::AlternatesUnit
expect(regex.unit.alternates.size).to eq 1 expect(regex.unit.alternates.size).to eq 1
expect(regex.unit.alternates[0]).to be_a Regex::SequenceUnit expect(regex.unit.alternates[0]).to be_a Regex::SequenceUnit
@@ -19,7 +19,7 @@ class Propane
end end
it "parses a group with a single character unit expression" do it "parses a group with a single character unit expression" do
regex = Regex.new("(a)") regex = Regex.new("(a)", 1)
expect(regex.unit).to be_a Regex::AlternatesUnit expect(regex.unit).to be_a Regex::AlternatesUnit
expect(regex.unit.alternates.size).to eq 1 expect(regex.unit.alternates.size).to eq 1
expect(regex.unit.alternates[0]).to be_a Regex::SequenceUnit expect(regex.unit.alternates[0]).to be_a Regex::SequenceUnit
@@ -33,7 +33,7 @@ class Propane
end end
it "parses a *" do it "parses a *" do
regex = Regex.new("a*") regex = Regex.new("a*", 1)
expect(regex.unit).to be_a Regex::AlternatesUnit expect(regex.unit).to be_a Regex::AlternatesUnit
expect(regex.unit.alternates.size).to eq 1 expect(regex.unit.alternates.size).to eq 1
expect(regex.unit.alternates[0]).to be_a Regex::SequenceUnit expect(regex.unit.alternates[0]).to be_a Regex::SequenceUnit
@@ -47,7 +47,7 @@ class Propane
end end
it "parses a +" do it "parses a +" do
regex = Regex.new("a+") regex = Regex.new("a+", 1)
expect(regex.unit).to be_a Regex::AlternatesUnit expect(regex.unit).to be_a Regex::AlternatesUnit
expect(regex.unit.alternates.size).to eq 1 expect(regex.unit.alternates.size).to eq 1
expect(regex.unit.alternates[0]).to be_a Regex::SequenceUnit expect(regex.unit.alternates[0]).to be_a Regex::SequenceUnit
@@ -61,7 +61,7 @@ class Propane
end end
it "parses a ?" do it "parses a ?" do
regex = Regex.new("a?") regex = Regex.new("a?", 1)
expect(regex.unit).to be_a Regex::AlternatesUnit expect(regex.unit).to be_a Regex::AlternatesUnit
expect(regex.unit.alternates.size).to eq 1 expect(regex.unit.alternates.size).to eq 1
expect(regex.unit.alternates[0]).to be_a Regex::SequenceUnit expect(regex.unit.alternates[0]).to be_a Regex::SequenceUnit
@@ -75,7 +75,7 @@ class Propane
end end
it "parses a multiplicity count" do it "parses a multiplicity count" do
regex = Regex.new("a{5}") regex = Regex.new("a{5}", 1)
expect(regex.unit).to be_a Regex::AlternatesUnit expect(regex.unit).to be_a Regex::AlternatesUnit
expect(regex.unit.alternates.size).to eq 1 expect(regex.unit.alternates.size).to eq 1
expect(regex.unit.alternates[0]).to be_a Regex::SequenceUnit expect(regex.unit.alternates[0]).to be_a Regex::SequenceUnit
@@ -89,7 +89,7 @@ class Propane
end end
it "parses a minimum-only multiplicity count" do it "parses a minimum-only multiplicity count" do
regex = Regex.new("a{5,}") regex = Regex.new("a{5,}", 1)
expect(regex.unit).to be_a Regex::AlternatesUnit expect(regex.unit).to be_a Regex::AlternatesUnit
expect(regex.unit.alternates.size).to eq 1 expect(regex.unit.alternates.size).to eq 1
expect(regex.unit.alternates[0]).to be_a Regex::SequenceUnit expect(regex.unit.alternates[0]).to be_a Regex::SequenceUnit
@@ -103,7 +103,7 @@ class Propane
end end
it "parses a minimum and maximum multiplicity count" do it "parses a minimum and maximum multiplicity count" do
regex = Regex.new("a{5,8}") regex = Regex.new("a{5,8}", 1)
expect(regex.unit).to be_a Regex::AlternatesUnit expect(regex.unit).to be_a Regex::AlternatesUnit
expect(regex.unit.alternates.size).to eq 1 expect(regex.unit.alternates.size).to eq 1
expect(regex.unit.alternates[0]).to be_a Regex::SequenceUnit expect(regex.unit.alternates[0]).to be_a Regex::SequenceUnit
@@ -118,7 +118,7 @@ class Propane
end end
it "parses an escaped *" do it "parses an escaped *" do
regex = Regex.new("a\\*") regex = Regex.new("a\\*", 1)
expect(regex.unit).to be_a Regex::AlternatesUnit expect(regex.unit).to be_a Regex::AlternatesUnit
expect(regex.unit.alternates.size).to eq 1 expect(regex.unit.alternates.size).to eq 1
expect(regex.unit.alternates[0]).to be_a Regex::SequenceUnit expect(regex.unit.alternates[0]).to be_a Regex::SequenceUnit
@@ -131,7 +131,7 @@ class Propane
end end
it "parses an escaped +" do it "parses an escaped +" do
regex = Regex.new("a\\+") regex = Regex.new("a\\+", 1)
expect(regex.unit).to be_a Regex::AlternatesUnit expect(regex.unit).to be_a Regex::AlternatesUnit
expect(regex.unit.alternates.size).to eq 1 expect(regex.unit.alternates.size).to eq 1
expect(regex.unit.alternates[0]).to be_a Regex::SequenceUnit expect(regex.unit.alternates[0]).to be_a Regex::SequenceUnit
@@ -144,7 +144,7 @@ class Propane
end end
it "parses an escaped \\" do it "parses an escaped \\" do
regex = Regex.new("\\\\d") regex = Regex.new("\\\\d", 1)
expect(regex.unit).to be_a Regex::AlternatesUnit expect(regex.unit).to be_a Regex::AlternatesUnit
expect(regex.unit.alternates.size).to eq 1 expect(regex.unit.alternates.size).to eq 1
expect(regex.unit.alternates[0]).to be_a Regex::SequenceUnit expect(regex.unit.alternates[0]).to be_a Regex::SequenceUnit
@@ -157,7 +157,7 @@ class Propane
end end
it "parses a character class" do it "parses a character class" do
regex = Regex.new("[a-z_]") regex = Regex.new("[a-z_]", 1)
expect(regex.unit).to be_a Regex::AlternatesUnit expect(regex.unit).to be_a Regex::AlternatesUnit
expect(regex.unit.alternates.size).to eq 1 expect(regex.unit.alternates.size).to eq 1
expect(regex.unit.alternates[0]).to be_a Regex::SequenceUnit expect(regex.unit.alternates[0]).to be_a Regex::SequenceUnit
@@ -175,7 +175,7 @@ class Propane
end end
it "parses a negated character class" do it "parses a negated character class" do
regex = Regex.new("[^xyz]") regex = Regex.new("[^xyz]", 1)
expect(regex.unit).to be_a Regex::AlternatesUnit expect(regex.unit).to be_a Regex::AlternatesUnit
expect(regex.unit.alternates.size).to eq 1 expect(regex.unit.alternates.size).to eq 1
expect(regex.unit.alternates[0]).to be_a Regex::SequenceUnit expect(regex.unit.alternates[0]).to be_a Regex::SequenceUnit
@@ -189,8 +189,25 @@ class Propane
expect(ccu[0].first).to eq "x".ord expect(ccu[0].first).to eq "x".ord
end end
it "parses a negated character class with inner character classes" do
regex = Regex.new("[^x\\sz]", 1)
expect(regex.unit).to be_a Regex::AlternatesUnit
expect(regex.unit.alternates.size).to eq 1
expect(regex.unit.alternates[0]).to be_a Regex::SequenceUnit
seq_unit = regex.unit.alternates[0]
expect(seq_unit.size).to eq 1
expect(seq_unit[0]).to be_a Regex::CharacterClassUnit
ccu = seq_unit[0]
expect(ccu.negate).to be_truthy
expect(ccu.size).to eq 8
expect(ccu[0]).to be_a Regex::CharacterRangeUnit
expect(ccu[0].first).to eq "x".ord
expect(ccu[1].first).to eq " ".ord
expect(ccu[7].first).to eq "z".ord
end
it "parses - as a plain character at beginning of a character class" do it "parses - as a plain character at beginning of a character class" do
regex = Regex.new("[-9]") regex = Regex.new("[-9]", 1)
expect(regex.unit).to be_a Regex::AlternatesUnit expect(regex.unit).to be_a Regex::AlternatesUnit
expect(regex.unit.alternates.size).to eq 1 expect(regex.unit.alternates.size).to eq 1
expect(regex.unit.alternates[0]).to be_a Regex::SequenceUnit expect(regex.unit.alternates[0]).to be_a Regex::SequenceUnit
@@ -204,7 +221,7 @@ class Propane
end end
it "parses - as a plain character at end of a character class" do it "parses - as a plain character at end of a character class" do
regex = Regex.new("[0-]") regex = Regex.new("[0-]", 1)
expect(regex.unit).to be_a Regex::AlternatesUnit expect(regex.unit).to be_a Regex::AlternatesUnit
expect(regex.unit.alternates.size).to eq 1 expect(regex.unit.alternates.size).to eq 1
expect(regex.unit.alternates[0]).to be_a Regex::SequenceUnit expect(regex.unit.alternates[0]).to be_a Regex::SequenceUnit
@@ -220,7 +237,7 @@ class Propane
end end
it "parses - as a plain character at beginning of a negated character class" do it "parses - as a plain character at beginning of a negated character class" do
regex = Regex.new("[^-9]") regex = Regex.new("[^-9]", 1)
expect(regex.unit).to be_a Regex::AlternatesUnit expect(regex.unit).to be_a Regex::AlternatesUnit
expect(regex.unit.alternates.size).to eq 1 expect(regex.unit.alternates.size).to eq 1
expect(regex.unit.alternates[0]).to be_a Regex::SequenceUnit expect(regex.unit.alternates[0]).to be_a Regex::SequenceUnit
@@ -235,7 +252,7 @@ class Propane
end end
it "parses . as a plain character in a character class" do it "parses . as a plain character in a character class" do
regex = Regex.new("[.]") regex = Regex.new("[.]", 1)
expect(regex.unit).to be_a Regex::AlternatesUnit expect(regex.unit).to be_a Regex::AlternatesUnit
expect(regex.unit.alternates.size).to eq 1 expect(regex.unit.alternates.size).to eq 1
expect(regex.unit.alternates[0]).to be_a Regex::SequenceUnit expect(regex.unit.alternates[0]).to be_a Regex::SequenceUnit
@@ -250,7 +267,7 @@ class Propane
end end
it "parses - as a plain character when escaped in middle of character class" do it "parses - as a plain character when escaped in middle of character class" do
regex = Regex.new("[0\\-9]") regex = Regex.new("[0\\-9]", 1)
expect(regex.unit).to be_a Regex::AlternatesUnit expect(regex.unit).to be_a Regex::AlternatesUnit
expect(regex.unit.alternates.size).to eq 1 expect(regex.unit.alternates.size).to eq 1
expect(regex.unit.alternates[0]).to be_a Regex::SequenceUnit expect(regex.unit.alternates[0]).to be_a Regex::SequenceUnit
@@ -269,7 +286,7 @@ class Propane
end end
it "parses alternates" do it "parses alternates" do
regex = Regex.new("ab|c") regex = Regex.new("ab|c", 1)
expect(regex.unit).to be_a Regex::AlternatesUnit expect(regex.unit).to be_a Regex::AlternatesUnit
expect(regex.unit.alternates.size).to eq 2 expect(regex.unit.alternates.size).to eq 2
expect(regex.unit.alternates[0]).to be_a Regex::SequenceUnit expect(regex.unit.alternates[0]).to be_a Regex::SequenceUnit
@@ -279,7 +296,7 @@ class Propane
end end
it "parses a ." do it "parses a ." do
regex = Regex.new("a.b") regex = Regex.new("a.b", 1)
expect(regex.unit).to be_a Regex::AlternatesUnit expect(regex.unit).to be_a Regex::AlternatesUnit
expect(regex.unit.alternates.size).to eq 1 expect(regex.unit.alternates.size).to eq 1
expect(regex.unit.alternates[0]).to be_a Regex::SequenceUnit expect(regex.unit.alternates[0]).to be_a Regex::SequenceUnit
@@ -290,7 +307,7 @@ class Propane
end end
it "parses something complex" do it "parses something complex" do
regex = Regex.new("(a|)*|[^^]|\\|v|[x-y]+") regex = Regex.new("(a|)*|[^^]|\\|v|[x-y]+", 1)
expect(regex.unit).to be_a Regex::AlternatesUnit expect(regex.unit).to be_a Regex::AlternatesUnit
expect(regex.unit.alternates.size).to eq 4 expect(regex.unit.alternates.size).to eq 4
expect(regex.unit.alternates[0]).to be_a Regex::SequenceUnit expect(regex.unit.alternates[0]).to be_a Regex::SequenceUnit
+12
View File
@@ -249,6 +249,18 @@ EOF
expect(results.status).to eq 0 expect(results.status).to eq 0
end end
it "shows error line number for unmatched left curly brace" do
write_grammar <<EOF
# Line 1
# Line 2
token a /a{/;
Start -> a;
EOF
results = run_propane(capture: true)
expect(results.stderr).to match /Line 3: unexpected match count following \{/
expect(results.status).to_not eq 0
end
%w[d c].each do |language| %w[d c].each do |language|
context "#{language.upcase} language" do context "#{language.upcase} language" do